mac_toolchain.py (7389B)
1 #!/usr/bin/env python3 2 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. 6 7 """ 8 If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of 9 date: 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 14 user interaction. 15 * Downloads standalone binaries from [a possibly different version of Xcode]. 16 17 The toolchain version can be overridden by setting MAC_TOOLCHAIN_REVISION with 18 the full revision, e.g. 9A235. 19 """ 20 21 import argparse 22 import os 23 import platform 24 import plistlib 25 import shutil 26 import subprocess 27 import sys 28 29 30 def LoadPList(path): 31 """Loads Plist at |path| and returns it as a dictionary.""" 32 with open(path, 'rb') as f: 33 return plistlib.load(f) 34 35 36 # This contains binaries from Xcode 16.2 (16C5032) along with 37 # the macOS SDK 15.2 (24C94). To build these packages, see comments in 38 # build/xcode_binaries.yaml. 39 # To update the version numbers, open Xcode's "About Xcode" or run 40 # `xcodebuild -version` for the Xcode version, and run 41 # `xcrun --show-sdk-version` and `xcrun --show-sdk-build-version`for 42 # the SDK version. To update the _TAG, use the output of the 43 # `cipd create` command mentioned in xcode_binaries.yaml; 44 # it's the part after the colon. 45 46 MAC_BINARIES_LABEL = 'infra_internal/ios/xcode/xcode_binaries/mac-amd64' 47 MAC_BINARIES_TAG = 'o5KxJtacGXzkEoORkVUIOEPgGnL2okJzM4Km91eod9EC' 48 49 # The toolchain will not be downloaded if the minimum OS version is not met. 19 50 # is the major version number for macOS 10.15. Xcode 15.0 only runs on macOS 51 # 13.5 and newer, but some bots are still running older OS versions. macOS 52 # 10.15.4, the OS minimum through Xcode 12.4, still seems to work. 53 MAC_MINIMUM_OS_VERSION = [19, 4] 54 55 BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 56 TOOLCHAIN_ROOT = os.path.join(BASE_DIR, 'mac_files') 57 TOOLCHAIN_BUILD_DIR = os.path.join(TOOLCHAIN_ROOT, 'Xcode.app') 58 59 # Always integrity-check the entire SDK. Mac SDK packages are complex and often 60 # hit edge cases in cipd (eg https://crbug.com/1033987, 61 # https://crbug.com/915278), and generally when this happens it requires manual 62 # intervention to fix. 63 # Note the trailing \n! 64 PARANOID_MODE = '$ParanoidMode CheckIntegrity\n' 65 66 67 def PlatformMeetsHermeticXcodeRequirements(): 68 if sys.platform != 'darwin': 69 return True 70 needed = MAC_MINIMUM_OS_VERSION 71 major_version = [int(v) for v in platform.release().split('.')[:len(needed)]] 72 return major_version >= needed 73 74 75 def _UseHermeticToolchain(): 76 current_dir = os.path.dirname(os.path.realpath(__file__)) 77 script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py') 78 proc = subprocess.Popen([script_path, 'mac'], stdout=subprocess.PIPE) 79 return '1' in proc.stdout.readline().decode() 80 81 82 def RequestCipdAuthentication(): 83 """Requests that the user authenticate to access Xcode CIPD packages.""" 84 85 print('Access to Xcode CIPD package requires authentication.') 86 print('-----------------------------------------------------------------') 87 print() 88 print('You appear to be a Googler.') 89 print() 90 print('I\'m sorry for the hassle, but you may need to do a one-time manual') 91 print('authentication. Please run:') 92 print() 93 print(' cipd auth-login') 94 print() 95 print('and follow the instructions.') 96 print() 97 print('NOTE: Use your google.com credentials, not chromium.org.') 98 print() 99 print('-----------------------------------------------------------------') 100 print() 101 sys.stdout.flush() 102 103 104 def PrintError(message): 105 # Flush buffers to ensure correct output ordering. 106 sys.stdout.flush() 107 sys.stderr.write(message + '\n') 108 sys.stderr.flush() 109 110 111 def InstallXcodeBinaries(): 112 """Installs the Xcode binaries needed to build Chrome and accepts the license. 113 114 This is the replacement for InstallXcode that installs a trimmed down version 115 of Xcode that is OS-version agnostic. 116 """ 117 # First make sure the directory exists. It will serve as the cipd root. This 118 # also ensures that there will be no conflicts of cipd root. 119 binaries_root = os.path.join(TOOLCHAIN_ROOT, 'xcode_binaries') 120 if not os.path.exists(binaries_root): 121 os.makedirs(binaries_root) 122 123 # 'cipd ensure' is idempotent. 124 args = ['cipd', 'ensure', '-root', binaries_root, '-ensure-file', '-'] 125 126 p = subprocess.Popen(args, 127 universal_newlines=True, 128 stdin=subprocess.PIPE, 129 stdout=subprocess.PIPE, 130 stderr=subprocess.PIPE) 131 stdout, stderr = p.communicate(input=PARANOID_MODE + MAC_BINARIES_LABEL + 132 ' ' + MAC_BINARIES_TAG) 133 if p.returncode != 0: 134 print(stdout) 135 print(stderr) 136 RequestCipdAuthentication() 137 return 1 138 139 if sys.platform != 'darwin': 140 return 0 141 142 # Accept the license for this version of Xcode if it's newer than the 143 # currently accepted version. 144 cipd_xcode_version_plist_path = os.path.join(binaries_root, 145 'Contents/version.plist') 146 cipd_xcode_version_plist = LoadPList(cipd_xcode_version_plist_path) 147 cipd_xcode_version = cipd_xcode_version_plist['CFBundleShortVersionString'] 148 149 cipd_license_path = os.path.join(binaries_root, 150 'Contents/Resources/LicenseInfo.plist') 151 cipd_license_plist = LoadPList(cipd_license_path) 152 cipd_license_version = cipd_license_plist['licenseID'] 153 154 should_overwrite_license = True 155 current_license_path = '/Library/Preferences/com.apple.dt.Xcode.plist' 156 if os.path.exists(current_license_path): 157 current_license_plist = LoadPList(current_license_path) 158 xcode_version = current_license_plist.get( 159 'IDEXcodeVersionForAgreedToGMLicense') 160 if (xcode_version is not None 161 and xcode_version.split('.') >= cipd_xcode_version.split('.')): 162 should_overwrite_license = False 163 164 if not should_overwrite_license: 165 return 0 166 167 # Use puppet's sudoers script to accept the license if its available. 168 license_accept_script = '/usr/local/bin/xcode_accept_license.sh' 169 if os.path.exists(license_accept_script): 170 args = [ 171 'sudo', license_accept_script, cipd_xcode_version, cipd_license_version 172 ] 173 subprocess.check_call(args) 174 return 0 175 176 # Otherwise manually accept the license. This will prompt for sudo. 177 print('Accepting new Xcode license. Requires sudo.') 178 sys.stdout.flush() 179 args = [ 180 'sudo', 'defaults', 'write', current_license_path, 181 'IDEXcodeVersionForAgreedToGMLicense', cipd_xcode_version 182 ] 183 subprocess.check_call(args) 184 args = [ 185 'sudo', 'defaults', 'write', current_license_path, 186 'IDELastGMLicenseAgreedTo', cipd_license_version 187 ] 188 subprocess.check_call(args) 189 args = ['sudo', 'plutil', '-convert', 'xml1', current_license_path] 190 subprocess.check_call(args) 191 192 return 0 193 194 195 def main(): 196 if not _UseHermeticToolchain(): 197 print('Skipping Mac toolchain installation for mac') 198 return 0 199 200 parser = argparse.ArgumentParser(description='Download hermetic Xcode.') 201 args = parser.parse_args() 202 203 if not PlatformMeetsHermeticXcodeRequirements(): 204 print('OS version does not support toolchain.') 205 return 0 206 207 return InstallXcodeBinaries() 208 209 210 if __name__ == '__main__': 211 sys.exit(main())