3 # Copyright 2018 The Chromium Authors
4 # Use of this source code is governed by a BSD-style license that can be
5 # found in the LICENSE file.
8 If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of
10 * Downloads the hermetic mac toolchain
11 * Requires CIPD authentication. Run `cipd auth-login`, use Google account.
12 * Accepts the license.
13 * If xcode-select and xcodebuild are not passwordless in sudoers, requires
15 * Downloads standalone binaries from [a possibly different version of Xcode].
17 The toolchain version can be overridden by setting MAC_TOOLCHAIN_REVISION with
18 the full revision, e.g. 9A235.
31 """Loads Plist at |path| and returns it as a dictionary."""
32 with open(path, 'rb') as f:
33 return plistlib.load(f)
36 # This contains binaries from Xcode 15.0 15A240d along with the macOS 14.0 SDK
37 # (14.0 23A334). To build these packages, see comments in
38 # build/xcode_binaries.yaml
39 # To update the version numbers, open Xcode's "About Xcode" for the first number
40 # and run `xcrun --show-sdk-build-version` for the second.
41 # To update the _TAG, use the output of the `cipd create` command mentioned in
42 # xcode_binaries.yaml.
44 MAC_BINARIES_LABEL = 'infra_internal/ios/xcode/xcode_binaries/mac-amd64'
45 MAC_BINARIES_TAG = 'dC_BLs9U850OLk8m4V7yxysPhP-ixJ2b5c7hVm8B7tIC'
47 # The toolchain will not be downloaded if the minimum OS version is not met. 19
48 # is the major version number for macOS 10.15. Xcode 15.0 only runs on macOS
49 # 13.5 and newer, but some bots are still running older OS versions. macOS
50 # 10.15.4, the OS minimum through Xcode 12.4, still seems to work.
51 MAC_MINIMUM_OS_VERSION = [19, 4]
53 BASE_DIR = os.path.abspath(os.path.dirname(__file__))
54 TOOLCHAIN_ROOT = os.path.join(BASE_DIR, 'mac_files')
55 TOOLCHAIN_BUILD_DIR = os.path.join(TOOLCHAIN_ROOT, 'Xcode.app')
57 # Always integrity-check the entire SDK. Mac SDK packages are complex and often
58 # hit edge cases in cipd (eg https://crbug.com/1033987,
59 # https://crbug.com/915278), and generally when this happens it requires manual
60 # intervention to fix.
61 # Note the trailing \n!
62 PARANOID_MODE = '$ParanoidMode CheckIntegrity\n'
65 def PlatformMeetsHermeticXcodeRequirements():
66 if sys.platform != 'darwin':
68 needed = MAC_MINIMUM_OS_VERSION
69 major_version = [int(v) for v in platform.release().split('.')[:len(needed)]]
70 return major_version >= needed
73 def _UseHermeticToolchain():
74 current_dir = os.path.dirname(os.path.realpath(__file__))
75 script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py')
76 proc = subprocess.Popen([script_path, 'mac'], stdout=subprocess.PIPE)
77 return '1' in proc.stdout.readline().decode()
80 def RequestCipdAuthentication():
81 """Requests that the user authenticate to access Xcode CIPD packages."""
83 print('Access to Xcode CIPD package requires authentication.')
84 print('-----------------------------------------------------------------')
86 print('You appear to be a Googler.')
88 print('I\'m sorry for the hassle, but you may need to do a one-time manual')
89 print('authentication. Please run:')
91 print(' cipd auth-login')
93 print('and follow the instructions.')
95 print('NOTE: Use your google.com credentials, not chromium.org.')
97 print('-----------------------------------------------------------------')
102 def PrintError(message):
103 # Flush buffers to ensure correct output ordering.
105 sys.stderr.write(message + '\n')
109 def InstallXcodeBinaries():
110 """Installs the Xcode binaries needed to build Chrome and accepts the license.
112 This is the replacement for InstallXcode that installs a trimmed down version
113 of Xcode that is OS-version agnostic.
115 # First make sure the directory exists. It will serve as the cipd root. This
116 # also ensures that there will be no conflicts of cipd root.
117 binaries_root = os.path.join(TOOLCHAIN_ROOT, 'xcode_binaries')
118 if not os.path.exists(binaries_root):
119 os.makedirs(binaries_root)
121 # 'cipd ensure' is idempotent.
122 args = ['cipd', 'ensure', '-root', binaries_root, '-ensure-file', '-']
124 p = subprocess.Popen(args,
125 universal_newlines=True,
126 stdin=subprocess.PIPE,
127 stdout=subprocess.PIPE,
128 stderr=subprocess.PIPE)
129 stdout, stderr = p.communicate(input=PARANOID_MODE + MAC_BINARIES_LABEL +
130 ' ' + MAC_BINARIES_TAG)
131 if p.returncode != 0:
134 RequestCipdAuthentication()
137 if sys.platform != 'darwin':
140 # Accept the license for this version of Xcode if it's newer than the
141 # currently accepted version.
142 cipd_xcode_version_plist_path = os.path.join(binaries_root,
143 'Contents/version.plist')
144 cipd_xcode_version_plist = LoadPList(cipd_xcode_version_plist_path)
145 cipd_xcode_version = cipd_xcode_version_plist['CFBundleShortVersionString']
147 cipd_license_path = os.path.join(binaries_root,
148 'Contents/Resources/LicenseInfo.plist')
149 cipd_license_plist = LoadPList(cipd_license_path)
150 cipd_license_version = cipd_license_plist['licenseID']
152 should_overwrite_license = True
153 current_license_path = '/Library/Preferences/com.apple.dt.Xcode.plist'
154 if os.path.exists(current_license_path):
155 current_license_plist = LoadPList(current_license_path)
156 xcode_version = current_license_plist.get(
157 'IDEXcodeVersionForAgreedToGMLicense')
158 if (xcode_version is not None
159 and xcode_version.split('.') >= cipd_xcode_version.split('.')):
160 should_overwrite_license = False
162 if not should_overwrite_license:
165 # Use puppet's sudoers script to accept the license if its available.
166 license_accept_script = '/usr/local/bin/xcode_accept_license.sh'
167 if os.path.exists(license_accept_script):
169 'sudo', license_accept_script, cipd_xcode_version, cipd_license_version
171 subprocess.check_call(args)
174 # Otherwise manually accept the license. This will prompt for sudo.
175 print('Accepting new Xcode license. Requires sudo.')
178 'sudo', 'defaults', 'write', current_license_path,
179 'IDEXcodeVersionForAgreedToGMLicense', cipd_xcode_version
181 subprocess.check_call(args)
183 'sudo', 'defaults', 'write', current_license_path,
184 'IDELastGMLicenseAgreedTo', cipd_license_version
186 subprocess.check_call(args)
187 args = ['sudo', 'plutil', '-convert', 'xml1', current_license_path]
188 subprocess.check_call(args)
194 if not _UseHermeticToolchain():
195 print('Skipping Mac toolchain installation for mac')
198 parser = argparse.ArgumentParser(description='Download hermetic Xcode.')
199 args = parser.parse_args()
201 if not PlatformMeetsHermeticXcodeRequirements():
202 print('OS version does not support toolchain.')
205 return InstallXcodeBinaries()
208 if __name__ == '__main__':