• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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