blob: 89febd8dc288484a75a693243acc3d6b4878ef1d [file] [log] [blame]
# Copyright 2019 The Fuchsia Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""The `macos_sdk` module provides safe functions to access a semi-hermetic XCode
installation.
Available only to Google-run bots.
"""
from contextlib import contextmanager
from recipe_engine import recipe_api
# Map OSX to default XCode versions. Recipes can override
# this value by setting macos_sdk.version.
# TODO(b/283993030): We should compress the Mac OSX versions
# to one.
DEFAULT_MAC_OS_SDK_VERSIONS = [
("10.13.5", "12D4e"),
("11.6.1", "13C100"),
("13.3.1", "14E222b"),
]
MAC_TOOLCHAIN_CIPD_PACKAGE = "infra/tools/mac_toolchain/${platform}"
# Version from https://source.chromium.org/chromium/chromium/tools/build/+/bce9a4137e36c26527f15d72e903d8ee5a441164:recipes/recipe_modules/chromium/config.py;l=227;bpv=1
MAC_TOOLCHAIN_CIPD_VERSION = "git_revision:3e597065cb23c1fe03aeb2ebd792d83e0709c5c2"
class MacOSSDKApi(recipe_api.RecipeApi):
"""API for using OS X SDK distributed via CIPD."""
def __init__(self, props, *args, **kwargs):
super().__init__(*args, **kwargs)
self._sdk_dir = props.sdk_dir
self._sdk_version = props.version
self._cipd_package = props.cipd_package or MAC_TOOLCHAIN_CIPD_PACKAGE
self._cipd_version = props.cipd_version or MAC_TOOLCHAIN_CIPD_VERSION
self._sysroot = None
self._mac_toolchain = None
# Whether the XCode SDK environment is currently set up. Read and
# modified only by __call__.
self._enabled = False
@property
def sdk_dir(self):
assert self._sdk_dir
return self._sdk_dir
@property
def sysroot(self):
assert self._sysroot
return self._sysroot
@contextmanager
def __call__(self, kind="mac", additional_sdks=None):
"""Sets up the XCode SDK environment.
This call is a no-op on non-Mac platforms.
This will deploy the helper tool and the XCode.app bundle at
`[START_DIR]/cache/macos_sdk`.
To avoid machines rebuilding these on every run, set up a named cache in
your cr-buildbucket.cfg file like:
caches: {
# Cache for mac_toolchain tool and XCode.app
name: "macos_sdk"
path: "macos_sdk"
}
If you have builders which e.g. use a non-current SDK, you can give them
a uniquely named cache:
caches: {
# Cache for N-1 version mac_toolchain tool and XCode.app
name: "macos_sdk_old"
path: "macos_sdk"
}
Usage:
with api.macos_sdk():
# sdk with mac build bits
Args:
kind (str): A type of SDK to install, can be mac or ios (default: mac).
additional_sdks (List(tuple)): A list of (sdk_version, xcode_version)
where xcode_version contains the sdk_version to be installed in the
selected XCode.
Raises:
StepFailure or InfraFailure.
"""
# No need to do anything if we're not running on a Mac, or if we're
# already running in a macos_sdk context.
if not self.m.platform.is_mac or self._enabled:
yield
return
self._enabled = True
if not self._sdk_version:
current_os = self.m.platform.mac_release
for target_os, xcode in reversed(DEFAULT_MAC_OS_SDK_VERSIONS):
if current_os >= self.m.version.parse(target_os):
self._sdk_version = xcode
break
if not self._sdk_version:
raise self.m.step.InfraFailure(
f"Unable to find an XCode version for {current_os}."
)
self._sdk_version = self._sdk_version.lower()
try:
with self.m.context(infra_steps=True), self.m.step.nest(
f"ensure XCode {self._sdk_version}"
):
cache_dir = self.m.path["cache"].join("macos_sdk")
if not self._mac_toolchain:
self._mac_toolchain = self.m.cipd.ensure_tool(
self._cipd_package, self._cipd_version
)
if not self._sdk_dir:
self._sdk_dir = self._ensure_xcode(
kind, self._sdk_version, cache_dir
)
for sdk_version, xcode_version in additional_sdks or []:
self._ensure_sdk(kind, sdk_version, xcode_version, cache_dir)
self.m.step(
f"select XCode {self._sdk_version}",
["sudo", "xcode-select", "--switch", self._sdk_dir],
)
assert self._sysroot is None
self._sysroot = self._get_sysroot()
yield
finally:
self._enabled = False
if not self.m.runtime.in_global_shutdown:
self.m.step(
"reset XCode", ["sudo", "xcode-select", "--reset"], infra_step=True
)
self._sysroot = None
def _ensure_xcode(self, kind, version, dir):
"""Ensures the MacOS SDK packages are installed.
Args:
kind (str): A type of SDK to install, can be mac or ios.
version (str): The XCode version to install.
dir (str): The directory to install XCode into.
Returns:
Path to the installed XCode bundle.
"""
assert kind in ("mac", "ios"), "invalid kind"
sdk_dir = dir.join("XCode.app")
self.m.step(
f"install XCode {version}",
[
self._mac_toolchain,
"install",
"-kind",
kind,
"-xcode-version",
version,
"-output-dir",
sdk_dir,
],
)
return sdk_dir
def _ensure_sdk(self, kind, sdk_version, xcode_version, cache_dir):
"""Ensures SDK package is installed.
Args:
kind (str): A type of SDK to install, can be mac or ios.
sdk_version (str): The SDK to install.
xcode_version (str): The XCode version that contains the SDK.
cache_dir (str): The directory to install XCode into.
"""
xcode_dir = cache_dir.join(xcode_version)
sdk_path_part = [
"Contents",
"Developer",
"Platforms",
"MacOSX.platform",
"Developer",
"SDKs",
]
sdk_path = cache_dir.join("XCode.app", *sdk_path_part).join(sdk_version)
if not self.m.path.exists(sdk_path):
self._ensure_xcode(kind, xcode_version, xcode_dir)
self.m.file.symlink(
f"install {sdk_version}",
xcode_dir.join("XCode.app", *sdk_path_part).join("MacOSX.sdk"),
sdk_path,
)
def _get_sysroot(self):
step = self.m.step(
"xcrun",
[
"xcrun",
"--sdk",
"macosx",
"--show-sdk-path",
],
stdout=self.m.raw_io.output_text(
name="sdk-path",
add_output_log=True,
),
step_test_data=lambda: self.m.raw_io.test_api.stream_output_text(
"/some/xcode/path"
),
)
return step.stdout.strip()