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