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""" 8If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of 9date: 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 17The toolchain version can be overridden by setting MAC_TOOLCHAIN_REVISION with 18the full revision, e.g. 9A235. 19""" 20 21import argparse 22import os 23import platform 24import plistlib 25import shutil 26import subprocess 27import sys 28 29 30def 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 46MAC_BINARIES_LABEL = 'infra_internal/ios/xcode/xcode_binaries/mac-amd64' 47MAC_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. 53MAC_MINIMUM_OS_VERSION = [19, 4] 54 55BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 56TOOLCHAIN_ROOT = os.path.join(BASE_DIR, 'mac_files') 57TOOLCHAIN_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! 64PARANOID_MODE = '$ParanoidMode CheckIntegrity\n' 65 66 67def 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 75def _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 82def 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 104def 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 111def 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 195def 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 210if __name__ == '__main__': 211 sys.exit(main()) 212