blob: 78871afa6dfa575111935c35e66d486383860c36 [file] [log] [blame]
justincohen6a03a3d2016-03-26 21:44:381#!/usr/bin/env python
2# Copyright 2016 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
erikchenda016262016-11-09 04:32:136"""
7If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of
8date:
9 * Downloads the hermetic mac toolchain
10 * Requires gsutil to be configured.
11 * Accepts the license.
12 * If xcode-select and xcodebuild are not passwordless in sudoers, requires
13 user interaction.
Justin Cohen170c409b72017-09-13 02:37:0214
15The toolchain version can be overridden by setting IOS_TOOLCHAIN_REVISION or
16MAC_TOOLCHAIN_REVISION with the full revision, e.g. 9A235-1.
justincohen6a03a3d2016-03-26 21:44:3817"""
18
Justin Cohen75d4a69f2017-08-12 03:50:3619from distutils.version import LooseVersion
justincohen6a03a3d2016-03-26 21:44:3820import os
erikchend5dfcdb02017-06-29 00:22:5321import platform
justincohen10dc90382016-05-09 17:47:2222import plistlib
justincohen6a03a3d2016-03-26 21:44:3823import shutil
24import subprocess
25import sys
26import tarfile
27import time
28import tempfile
29import urllib2
30
31# This can be changed after running /build/package_mac_toolchain.py.
erikchend5dfcdb02017-06-29 00:22:5332MAC_TOOLCHAIN_VERSION = '8E2002'
justincohenabbd93dc2016-11-30 20:11:2833MAC_TOOLCHAIN_SUB_REVISION = 3
34MAC_TOOLCHAIN_VERSION = '%s-%s' % (MAC_TOOLCHAIN_VERSION,
35 MAC_TOOLCHAIN_SUB_REVISION)
erikchend5dfcdb02017-06-29 00:22:5336# The toolchain will not be downloaded if the minimum OS version is not met.
37# 16 is the major version number for macOS 10.12.
38MAC_MINIMUM_OS_VERSION = 16
39
Justin Cohen31639612017-12-07 21:18:1440IOS_TOOLCHAIN_VERSION = '9C40b'
justincohen3c4d50882017-01-26 16:55:5941IOS_TOOLCHAIN_SUB_REVISION = 1
justincohenabbd93dc2016-11-30 20:11:2842IOS_TOOLCHAIN_VERSION = '%s-%s' % (IOS_TOOLCHAIN_VERSION,
43 IOS_TOOLCHAIN_SUB_REVISION)
44
45# Absolute path to src/ directory.
46REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
47
48# Absolute path to a file with gclient solutions.
49GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient')
justincohen6a03a3d2016-03-26 21:44:3850
51BASE_DIR = os.path.abspath(os.path.dirname(__file__))
sdefresneaacc28452017-01-12 10:33:3752TOOLCHAIN_BUILD_DIR = os.path.join(BASE_DIR, '%s_files', 'Xcode.app')
53STAMP_FILE = os.path.join(BASE_DIR, '%s_files', 'toolchain_build_revision')
justincohen6a03a3d2016-03-26 21:44:3854TOOLCHAIN_URL = 'gs://chrome-mac-sdk/'
55
erikchend5dfcdb02017-06-29 00:22:5356
57def PlatformMeetsHermeticXcodeRequirements(target_os):
58 if target_os == 'ios':
59 return True
60 return int(platform.release().split('.')[0]) >= MAC_MINIMUM_OS_VERSION
61
62
sdefresneaacc28452017-01-12 10:33:3763def GetPlatforms():
Justin Cohen9d09a66e2017-08-31 22:05:3664 target_os = set(['mac'])
justincohenabbd93dc2016-11-30 20:11:2865 try:
66 env = {}
67 execfile(GCLIENT_CONFIG, env, env)
Justin Cohen9d09a66e2017-08-31 22:05:3668 target_os |= set(env.get('target_os', target_os))
justincohenabbd93dc2016-11-30 20:11:2869 except:
70 pass
Justin Cohen9d09a66e2017-08-31 22:05:3671 return target_os
justincohenabbd93dc2016-11-30 20:11:2872
justincohen6a03a3d2016-03-26 21:44:3873
sdefresneaacc28452017-01-12 10:33:3774def ReadStampFile(target_os):
justincohen6a03a3d2016-03-26 21:44:3875 """Return the contents of the stamp file, or '' if it doesn't exist."""
76 try:
sdefresneaacc28452017-01-12 10:33:3777 with open(STAMP_FILE % target_os, 'r') as f:
justincohen6a03a3d2016-03-26 21:44:3878 return f.read().rstrip()
79 except IOError:
80 return ''
81
82
sdefresneaacc28452017-01-12 10:33:3783def WriteStampFile(target_os, s):
justincohen6a03a3d2016-03-26 21:44:3884 """Write s to the stamp file."""
sdefresneaacc28452017-01-12 10:33:3785 EnsureDirExists(os.path.dirname(STAMP_FILE % target_os))
86 with open(STAMP_FILE % target_os, 'w') as f:
justincohen6a03a3d2016-03-26 21:44:3887 f.write(s)
88 f.write('\n')
89
90
91def EnsureDirExists(path):
92 if not os.path.exists(path):
93 os.makedirs(path)
94
95
96def DownloadAndUnpack(url, output_dir):
97 """Decompresses |url| into a cleared |output_dir|."""
98 temp_name = tempfile.mktemp(prefix='mac_toolchain')
99 try:
100 print 'Downloading new toolchain.'
101 subprocess.check_call(['gsutil.py', 'cp', url, temp_name])
102 if os.path.exists(output_dir):
103 print 'Deleting old toolchain.'
104 shutil.rmtree(output_dir)
105 EnsureDirExists(output_dir)
106 print 'Unpacking new toolchain.'
107 tarfile.open(mode='r:gz', name=temp_name).extractall(path=output_dir)
108 finally:
109 if os.path.exists(temp_name):
110 os.unlink(temp_name)
111
112
113def CanAccessToolchainBucket():
114 """Checks whether the user has access to |TOOLCHAIN_URL|."""
115 proc = subprocess.Popen(['gsutil.py', 'ls', TOOLCHAIN_URL],
116 stdout=subprocess.PIPE)
117 proc.communicate()
118 return proc.returncode == 0
119
sdefresneaacc28452017-01-12 10:33:37120
justincohen10dc90382016-05-09 17:47:22121def LoadPlist(path):
122 """Loads Plist at |path| and returns it as a dictionary."""
123 fd, name = tempfile.mkstemp()
124 try:
125 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path])
126 with os.fdopen(fd, 'r') as f:
127 return plistlib.readPlist(f)
128 finally:
129 os.unlink(name)
130
justincohen6a03a3d2016-03-26 21:44:38131
Justin Cohenae7265c2017-09-15 13:17:50132def FinalizeUnpack(output_dir, target_os):
Justin Cohen75d4a69f2017-08-12 03:50:36133 """Use xcodebuild to accept new toolchain license and run first launch
134 installers if necessary. Don't accept the license if a newer license has
135 already been accepted. This only works if xcodebuild and xcode-select are
136 passwordless in sudoers."""
justincohen10dc90382016-05-09 17:47:22137
138 # Check old license
139 try:
Justin Cohenae7265c2017-09-15 13:17:50140 target_license_plist_path = os.path.join(
141 output_dir, 'Contents','Resources','LicenseInfo.plist')
justincohen10dc90382016-05-09 17:47:22142 target_license_plist = LoadPlist(target_license_plist_path)
143 build_type = target_license_plist['licenseType']
144 build_version = target_license_plist['licenseID']
145
146 accepted_license_plist = LoadPlist(
147 '/Library/Preferences/com.apple.dt.Xcode.plist')
148 agreed_to_key = 'IDELast%sLicenseAgreedTo' % build_type
149 last_license_agreed_to = accepted_license_plist[agreed_to_key]
150
151 # Historically all Xcode build numbers have been in the format of AANNNN, so
152 # a simple string compare works. If Xcode's build numbers change this may
153 # need a more complex compare.
154 if build_version <= last_license_agreed_to:
155 # Don't accept the license of older toolchain builds, this will break the
156 # license of newer builds.
157 return
tikuta1a1610f2016-05-16 02:28:10158 except (subprocess.CalledProcessError, KeyError):
justincohen10dc90382016-05-09 17:47:22159 # If there's never been a license of type |build_type| accepted,
160 # |target_license_plist_path| or |agreed_to_key| may not exist.
161 pass
162
163 print "Accepting license."
Justin Cohenae7265c2017-09-15 13:17:50164 target_version_plist_path = os.path.join(
165 output_dir, 'Contents','version.plist')
Justin Cohen75d4a69f2017-08-12 03:50:36166 target_version_plist = LoadPlist(target_version_plist_path)
167 short_version_string = target_version_plist['CFBundleShortVersionString']
justincohen66d5aafd2016-03-29 23:45:18168 old_path = subprocess.Popen(['/usr/bin/xcode-select', '-p'],
169 stdout=subprocess.PIPE).communicate()[0].strip()
justincohen10dc90382016-05-09 17:47:22170 try:
Justin Cohenae7265c2017-09-15 13:17:50171 build_dir = os.path.join(output_dir, 'Contents/Developer')
justincohen10dc90382016-05-09 17:47:22172 subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', build_dir])
173 subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-license', 'accept'])
Justin Cohen75d4a69f2017-08-12 03:50:36174
175 if target_os == 'ios' and \
176 LooseVersion(short_version_string) >= LooseVersion("9.0"):
177 print "Installing packages."
178 subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-runFirstLaunch'])
justincohen10dc90382016-05-09 17:47:22179 finally:
180 subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', old_path])
justincohen6a03a3d2016-03-26 21:44:38181
182
sdefresneaacc28452017-01-12 10:33:37183def _UseHermeticToolchain(target_os):
erikchenda016262016-11-09 04:32:13184 current_dir = os.path.dirname(os.path.realpath(__file__))
185 script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py')
justincohencf1c482d2016-12-08 20:39:34186 proc = subprocess.Popen([script_path, target_os], stdout=subprocess.PIPE)
erikchenda016262016-11-09 04:32:13187 return '1' in proc.stdout.readline()
justincohen6a03a3d2016-03-26 21:44:38188
erikchenda016262016-11-09 04:32:13189
190def RequestGsAuthentication():
191 """Requests that the user authenticate to be able to access gs://.
192 """
193 print 'Access to ' + TOOLCHAIN_URL + ' not configured.'
194 print '-----------------------------------------------------------------'
195 print
196 print 'You appear to be a Googler.'
197 print
198 print 'I\'m sorry for the hassle, but you need to do a one-time manual'
199 print 'authentication. Please run:'
200 print
201 print ' download_from_google_storage --config'
202 print
203 print 'and follow the instructions.'
204 print
205 print 'NOTE 1: Use your google.com credentials, not chromium.org.'
206 print 'NOTE 2: Enter 0 when asked for a "project-id".'
207 print
208 print '-----------------------------------------------------------------'
209 print
210 sys.stdout.flush()
211 sys.exit(1)
justincohen6a03a3d2016-03-26 21:44:38212
213
Justin Cohen170c409b72017-09-13 02:37:02214def DownloadHermeticBuild(target_os, toolchain_version, toolchain_filename):
sdefresneaacc28452017-01-12 10:33:37215 if not _UseHermeticToolchain(target_os):
justincohen6a03a3d2016-03-26 21:44:38216 return 0
217
Justin Cohenae7265c2017-09-15 13:17:50218 toolchain_output_path = TOOLCHAIN_BUILD_DIR % target_os
sdefresneaacc28452017-01-12 10:33:37219 if ReadStampFile(target_os) == toolchain_version:
Justin Cohenae7265c2017-09-15 13:17:50220 FinalizeUnpack(toolchain_output_path, target_os)
justincohen6a03a3d2016-03-26 21:44:38221 return 0
222
223 if not CanAccessToolchainBucket():
erikchenda016262016-11-09 04:32:13224 RequestGsAuthentication()
225 return 1
justincohen6a03a3d2016-03-26 21:44:38226
227 # Reset the stamp file in case the build is unsuccessful.
sdefresneaacc28452017-01-12 10:33:37228 WriteStampFile(target_os, '')
justincohen6a03a3d2016-03-26 21:44:38229
justincohenabbd93dc2016-11-30 20:11:28230 toolchain_file = '%s.tgz' % toolchain_version
justincohen6a03a3d2016-03-26 21:44:38231 toolchain_full_url = TOOLCHAIN_URL + toolchain_file
232
justincohenabbd93dc2016-11-30 20:11:28233 print 'Updating toolchain to %s...' % toolchain_version
justincohen6a03a3d2016-03-26 21:44:38234 try:
justincohenabbd93dc2016-11-30 20:11:28235 toolchain_file = toolchain_filename % toolchain_version
justincohen6a03a3d2016-03-26 21:44:38236 toolchain_full_url = TOOLCHAIN_URL + toolchain_file
Justin Cohenae7265c2017-09-15 13:17:50237 DownloadAndUnpack(toolchain_full_url, toolchain_output_path)
238 FinalizeUnpack(toolchain_output_path, target_os)
justincohen6a03a3d2016-03-26 21:44:38239
justincohenabbd93dc2016-11-30 20:11:28240 print 'Toolchain %s unpacked.' % toolchain_version
sdefresneaacc28452017-01-12 10:33:37241 WriteStampFile(target_os, toolchain_version)
justincohen6a03a3d2016-03-26 21:44:38242 return 0
tikuta1a1610f2016-05-16 02:28:10243 except Exception as e:
justincohen6a03a3d2016-03-26 21:44:38244 print 'Failed to download toolchain %s.' % toolchain_file
tikuta1a1610f2016-05-16 02:28:10245 print 'Exception %s' % e
justincohen6a03a3d2016-03-26 21:44:38246 print 'Exiting.'
247 return 1
248
sdefresneaacc28452017-01-12 10:33:37249
250def main():
251 if sys.platform != 'darwin':
252 return 0
253
254 for target_os in GetPlatforms():
erikchend5dfcdb02017-06-29 00:22:53255 if not PlatformMeetsHermeticXcodeRequirements(target_os):
256 print 'OS version does not support toolchain.'
257 continue
258
Justin Cohen9d09a66e2017-08-31 22:05:36259 if target_os == 'ios':
Justin Cohen170c409b72017-09-13 02:37:02260 toolchain_version = os.environ.get('IOS_TOOLCHAIN_REVISION',
261 IOS_TOOLCHAIN_VERSION)
sdefresneaacc28452017-01-12 10:33:37262 toolchain_filename = 'ios-toolchain-%s.tgz'
263 else:
Justin Cohen170c409b72017-09-13 02:37:02264 toolchain_version = os.environ.get('MAC_TOOLCHAIN_REVISION',
265 MAC_TOOLCHAIN_VERSION)
sdefresneaacc28452017-01-12 10:33:37266 toolchain_filename = 'toolchain-%s.tgz'
267
268 return_value = DownloadHermeticBuild(
Justin Cohen170c409b72017-09-13 02:37:02269 target_os, toolchain_version, toolchain_filename)
sdefresneaacc28452017-01-12 10:33:37270 if return_value:
271 return return_value
272
273 return 0
274
275
justincohen6a03a3d2016-03-26 21:44:38276if __name__ == '__main__':
277 sys.exit(main())