|  | #!/usr/bin/env python3 | 
|  |  | 
|  | # Copyright 2018 The Chromium Authors | 
|  | # Use of this source code is governed by a BSD-style license that can be | 
|  | # found in the LICENSE file. | 
|  |  | 
|  | """ | 
|  | If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of | 
|  | date: | 
|  | * Downloads the hermetic mac toolchain | 
|  | * Requires CIPD authentication. Run `cipd auth-login`, use Google account. | 
|  | * Accepts the license. | 
|  | * If xcode-select and xcodebuild are not passwordless in sudoers, requires | 
|  | user interaction. | 
|  | * Downloads standalone binaries from [a possibly different version of Xcode]. | 
|  |  | 
|  | The toolchain version can be overridden by setting MAC_TOOLCHAIN_REVISION with | 
|  | the full revision, e.g. 9A235. | 
|  | """ | 
|  |  | 
|  | import argparse | 
|  | import os | 
|  | import platform | 
|  | import plistlib | 
|  | import shutil | 
|  | import subprocess | 
|  | import sys | 
|  |  | 
|  |  | 
|  | def LoadPList(path): | 
|  | """Loads Plist at |path| and returns it as a dictionary.""" | 
|  | with open(path, 'rb') as f: | 
|  | return plistlib.load(f) | 
|  |  | 
|  |  | 
|  | # This contains binaries from Xcode 26.0 (17A324) along with the macOS 26.0 SDK | 
|  | # (25A352, which is like macOS 26.0 25A354) and the Metal toolchain (17A324, | 
|  | # 32023). To build these packages, see comments in build/xcode_binaries.yaml. | 
|  | # | 
|  | # To update the version numbers, open Xcode's "About Xcode" or run `xcodebuild | 
|  | # -version` for the Xcode version, and run `xcrun --show-sdk-version` and `xcrun | 
|  | # --show-sdk-build-version` for the SDK version. To update the _TAG, use the | 
|  | # output of the `cipd create` command mentioned in xcode_binaries.yaml; it's the | 
|  | # part after the colon. Or check the CIPD site | 
|  | # (https://chrome-infra-packages.appspot.com/) at the path given in | 
|  | # MAC_BINARIES_LABEL. | 
|  |  | 
|  | MAC_BINARIES_LABEL = 'infra_internal/ios/xcode/xcode_binaries/mac-amd64' | 
|  | MAC_BINARIES_TAG = 'R3tAh86UE4bodN1_DNbWgVXD2aNcpXpz9xo1JaPE-4oC' | 
|  |  | 
|  | # The toolchain will not be downloaded if the minimum OS version is not met. 19 | 
|  | # is the Darwin major version number for macOS 10.15. Xcode 26.0 17A324 only | 
|  | # runs on macOS 15.6 and newer, but some bots are still running older OS | 
|  | # versions. macOS 10.15.4, the OS minimum through Xcode 12.4, still seems to | 
|  | # work. | 
|  | MAC_MINIMUM_OS_VERSION = [19, 4] | 
|  |  | 
|  | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) | 
|  | TOOLCHAIN_ROOT = os.path.join(BASE_DIR, 'mac_files') | 
|  |  | 
|  | # Always integrity-check the entire SDK. Mac SDK packages are complex and often | 
|  | # hit edge cases in cipd (eg https://crbug.com/1033987, | 
|  | # https://crbug.com/915278), and generally when this happens it requires manual | 
|  | # intervention to fix. | 
|  | # Note the trailing \n! | 
|  | PARANOID_MODE = '$ParanoidMode CheckIntegrity\n' | 
|  |  | 
|  |  | 
|  | def PlatformMeetsHermeticXcodeRequirements(): | 
|  | if sys.platform != 'darwin': | 
|  | return True | 
|  | needed = MAC_MINIMUM_OS_VERSION | 
|  | major_version = [int(v) for v in platform.release().split('.')[:len(needed)]] | 
|  | return major_version >= needed | 
|  |  | 
|  |  | 
|  | def _UseHermeticToolchain(): | 
|  | current_dir = os.path.dirname(os.path.realpath(__file__)) | 
|  | script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py') | 
|  | proc = subprocess.Popen([script_path, 'mac'], stdout=subprocess.PIPE) | 
|  | return '1' in proc.stdout.readline().decode() | 
|  |  | 
|  |  | 
|  | def RequestCipdAuthentication(): | 
|  | """Requests that the user authenticate to access Xcode CIPD packages.""" | 
|  |  | 
|  | print('Access to Xcode CIPD package requires authentication.') | 
|  | print('-----------------------------------------------------------------') | 
|  | print() | 
|  | print('You appear to be a Googler.') | 
|  | print() | 
|  | print('I\'m sorry for the hassle, but you may need to do a one-time manual') | 
|  | print('authentication. Please run:') | 
|  | print() | 
|  | print('    cipd auth-login') | 
|  | print() | 
|  | print('and follow the instructions.') | 
|  | print() | 
|  | print('NOTE: Use your google.com credentials, not chromium.org.') | 
|  | print() | 
|  | print('-----------------------------------------------------------------') | 
|  | print() | 
|  | sys.stdout.flush() | 
|  |  | 
|  |  | 
|  | def PrintError(message): | 
|  | # Flush buffers to ensure correct output ordering. | 
|  | sys.stdout.flush() | 
|  | sys.stderr.write(message + '\n') | 
|  | sys.stderr.flush() | 
|  |  | 
|  |  | 
|  | def InstallXcodeBinaries(): | 
|  | """Installs the Xcode binaries needed to build Chrome and accepts the license. | 
|  |  | 
|  | This is the replacement for InstallXcode that installs a trimmed down version | 
|  | of Xcode that is OS-version agnostic. | 
|  | """ | 
|  | # First make sure the directory exists. It will serve as the cipd root. This | 
|  | # also ensures that there will be no conflicts of cipd root. | 
|  | binaries_root = os.path.join(TOOLCHAIN_ROOT, 'xcode_binaries') | 
|  | if not os.path.exists(binaries_root): | 
|  | os.makedirs(binaries_root) | 
|  |  | 
|  | # 'cipd ensure' is idempotent. | 
|  | args = ['cipd', 'ensure', '-root', binaries_root, '-ensure-file', '-'] | 
|  |  | 
|  | p = subprocess.Popen(args, | 
|  | universal_newlines=True, | 
|  | stdin=subprocess.PIPE, | 
|  | stdout=subprocess.PIPE, | 
|  | stderr=subprocess.PIPE) | 
|  | stdout, stderr = p.communicate(input=PARANOID_MODE + MAC_BINARIES_LABEL + | 
|  | ' ' + MAC_BINARIES_TAG) | 
|  | if p.returncode != 0: | 
|  | print(stdout) | 
|  | print(stderr) | 
|  | RequestCipdAuthentication() | 
|  | return 1 | 
|  |  | 
|  | if sys.platform != 'darwin': | 
|  | return 0 | 
|  |  | 
|  | # Accept the license for this version of Xcode if it's newer than the | 
|  | # currently accepted version. | 
|  | cipd_xcode_version_plist_path = os.path.join(binaries_root, | 
|  | 'Contents/version.plist') | 
|  | cipd_xcode_version_plist = LoadPList(cipd_xcode_version_plist_path) | 
|  | cipd_xcode_version = cipd_xcode_version_plist['CFBundleShortVersionString'] | 
|  |  | 
|  | cipd_license_path = os.path.join(binaries_root, | 
|  | 'Contents/Resources/LicenseInfo.plist') | 
|  | cipd_license_plist = LoadPList(cipd_license_path) | 
|  | cipd_license_version = cipd_license_plist['licenseID'] | 
|  |  | 
|  | should_overwrite_license = True | 
|  | current_license_path = '/Library/Preferences/com.apple.dt.Xcode.plist' | 
|  | if os.path.exists(current_license_path): | 
|  | current_license_plist = LoadPList(current_license_path) | 
|  | xcode_version = current_license_plist.get( | 
|  | 'IDEXcodeVersionForAgreedToGMLicense') | 
|  | if (xcode_version is not None | 
|  | and xcode_version.split('.') >= cipd_xcode_version.split('.')): | 
|  | should_overwrite_license = False | 
|  |  | 
|  | if not should_overwrite_license: | 
|  | return 0 | 
|  |  | 
|  | # Use puppet's sudoers script to accept the license if its available. | 
|  | license_accept_script = '/usr/local/bin/xcode_accept_license.sh' | 
|  | if os.path.exists(license_accept_script): | 
|  | args = [ | 
|  | 'sudo', license_accept_script, cipd_xcode_version, cipd_license_version | 
|  | ] | 
|  | subprocess.check_call(args) | 
|  | return 0 | 
|  |  | 
|  | # Otherwise manually accept the license. This will prompt for sudo. | 
|  | print('Accepting new Xcode license. Requires sudo.') | 
|  | sys.stdout.flush() | 
|  | args = [ | 
|  | 'sudo', 'defaults', 'write', current_license_path, | 
|  | 'IDEXcodeVersionForAgreedToGMLicense', cipd_xcode_version | 
|  | ] | 
|  | subprocess.check_call(args) | 
|  | args = [ | 
|  | 'sudo', 'defaults', 'write', current_license_path, | 
|  | 'IDELastGMLicenseAgreedTo', cipd_license_version | 
|  | ] | 
|  | subprocess.check_call(args) | 
|  | args = ['sudo', 'plutil', '-convert', 'xml1', current_license_path] | 
|  | subprocess.check_call(args) | 
|  |  | 
|  | return 0 | 
|  |  | 
|  |  | 
|  | def main(): | 
|  | if not _UseHermeticToolchain(): | 
|  | print('Skipping Mac toolchain installation for mac') | 
|  | return 0 | 
|  |  | 
|  | parser = argparse.ArgumentParser(description='Download hermetic Xcode.') | 
|  | args = parser.parse_args() | 
|  |  | 
|  | if not PlatformMeetsHermeticXcodeRequirements(): | 
|  | print('OS version does not support toolchain.') | 
|  | return 0 | 
|  |  | 
|  | return InstallXcodeBinaries() | 
|  |  | 
|  |  | 
|  | if __name__ == '__main__': | 
|  | sys.exit(main()) |