justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 1 | #!/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 | |
erikchen | da01626 | 2016-11-09 04:32:13 | [diff] [blame] | 6 | """ |
| 7 | If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of |
| 8 | date: |
| 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 Cohen | 170c409b7 | 2017-09-13 02:37:02 | [diff] [blame] | 14 | |
| 15 | The toolchain version can be overridden by setting IOS_TOOLCHAIN_REVISION or |
| 16 | MAC_TOOLCHAIN_REVISION with the full revision, e.g. 9A235-1. |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 17 | """ |
| 18 | |
Justin Cohen | 75d4a69f | 2017-08-12 03:50:36 | [diff] [blame] | 19 | from distutils.version import LooseVersion |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 20 | import os |
erikchen | d5dfcdb0 | 2017-06-29 00:22:53 | [diff] [blame] | 21 | import platform |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 22 | import plistlib |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 23 | import shutil |
| 24 | import subprocess |
| 25 | import sys |
| 26 | import tarfile |
| 27 | import time |
| 28 | import tempfile |
| 29 | import urllib2 |
| 30 | |
| 31 | # This can be changed after running /build/package_mac_toolchain.py. |
erikchen | d5dfcdb0 | 2017-06-29 00:22:53 | [diff] [blame] | 32 | MAC_TOOLCHAIN_VERSION = '8E2002' |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 33 | MAC_TOOLCHAIN_SUB_REVISION = 3 |
| 34 | MAC_TOOLCHAIN_VERSION = '%s-%s' % (MAC_TOOLCHAIN_VERSION, |
| 35 | MAC_TOOLCHAIN_SUB_REVISION) |
erikchen | d5dfcdb0 | 2017-06-29 00:22:53 | [diff] [blame] | 36 | # 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. |
| 38 | MAC_MINIMUM_OS_VERSION = 16 |
| 39 | |
Justin Cohen | 3163961 | 2017-12-07 21:18:14 | [diff] [blame] | 40 | IOS_TOOLCHAIN_VERSION = '9C40b' |
justincohen | 3c4d5088 | 2017-01-26 16:55:59 | [diff] [blame] | 41 | IOS_TOOLCHAIN_SUB_REVISION = 1 |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 42 | IOS_TOOLCHAIN_VERSION = '%s-%s' % (IOS_TOOLCHAIN_VERSION, |
| 43 | IOS_TOOLCHAIN_SUB_REVISION) |
| 44 | |
| 45 | # Absolute path to src/ directory. |
| 46 | REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) |
| 47 | |
| 48 | # Absolute path to a file with gclient solutions. |
| 49 | GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient') |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 50 | |
| 51 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 52 | TOOLCHAIN_BUILD_DIR = os.path.join(BASE_DIR, '%s_files', 'Xcode.app') |
| 53 | STAMP_FILE = os.path.join(BASE_DIR, '%s_files', 'toolchain_build_revision') |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 54 | TOOLCHAIN_URL = 'gs://chrome-mac-sdk/' |
| 55 | |
erikchen | d5dfcdb0 | 2017-06-29 00:22:53 | [diff] [blame] | 56 | |
| 57 | def PlatformMeetsHermeticXcodeRequirements(target_os): |
| 58 | if target_os == 'ios': |
| 59 | return True |
| 60 | return int(platform.release().split('.')[0]) >= MAC_MINIMUM_OS_VERSION |
| 61 | |
| 62 | |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 63 | def GetPlatforms(): |
Justin Cohen | 9d09a66e | 2017-08-31 22:05:36 | [diff] [blame] | 64 | target_os = set(['mac']) |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 65 | try: |
| 66 | env = {} |
| 67 | execfile(GCLIENT_CONFIG, env, env) |
Justin Cohen | 9d09a66e | 2017-08-31 22:05:36 | [diff] [blame] | 68 | target_os |= set(env.get('target_os', target_os)) |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 69 | except: |
| 70 | pass |
Justin Cohen | 9d09a66e | 2017-08-31 22:05:36 | [diff] [blame] | 71 | return target_os |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 72 | |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 73 | |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 74 | def ReadStampFile(target_os): |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 75 | """Return the contents of the stamp file, or '' if it doesn't exist.""" |
| 76 | try: |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 77 | with open(STAMP_FILE % target_os, 'r') as f: |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 78 | return f.read().rstrip() |
| 79 | except IOError: |
| 80 | return '' |
| 81 | |
| 82 | |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 83 | def WriteStampFile(target_os, s): |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 84 | """Write s to the stamp file.""" |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 85 | EnsureDirExists(os.path.dirname(STAMP_FILE % target_os)) |
| 86 | with open(STAMP_FILE % target_os, 'w') as f: |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 87 | f.write(s) |
| 88 | f.write('\n') |
| 89 | |
| 90 | |
| 91 | def EnsureDirExists(path): |
| 92 | if not os.path.exists(path): |
| 93 | os.makedirs(path) |
| 94 | |
| 95 | |
| 96 | def 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 | |
| 113 | def 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 | |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 120 | |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 121 | def 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 | |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 131 | |
Justin Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 132 | def FinalizeUnpack(output_dir, target_os): |
Justin Cohen | 75d4a69f | 2017-08-12 03:50:36 | [diff] [blame] | 133 | """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.""" |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 137 | |
| 138 | # Check old license |
| 139 | try: |
Justin Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 140 | target_license_plist_path = os.path.join( |
| 141 | output_dir, 'Contents','Resources','LicenseInfo.plist') |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 142 | 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 |
tikuta | 1a1610f | 2016-05-16 02:28:10 | [diff] [blame] | 158 | except (subprocess.CalledProcessError, KeyError): |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 159 | # 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 Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 164 | target_version_plist_path = os.path.join( |
| 165 | output_dir, 'Contents','version.plist') |
Justin Cohen | 75d4a69f | 2017-08-12 03:50:36 | [diff] [blame] | 166 | target_version_plist = LoadPlist(target_version_plist_path) |
| 167 | short_version_string = target_version_plist['CFBundleShortVersionString'] |
justincohen | 66d5aafd | 2016-03-29 23:45:18 | [diff] [blame] | 168 | old_path = subprocess.Popen(['/usr/bin/xcode-select', '-p'], |
| 169 | stdout=subprocess.PIPE).communicate()[0].strip() |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 170 | try: |
Justin Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 171 | build_dir = os.path.join(output_dir, 'Contents/Developer') |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 172 | subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', build_dir]) |
| 173 | subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-license', 'accept']) |
Justin Cohen | 75d4a69f | 2017-08-12 03:50:36 | [diff] [blame] | 174 | |
| 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']) |
justincohen | 10dc9038 | 2016-05-09 17:47:22 | [diff] [blame] | 179 | finally: |
| 180 | subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', old_path]) |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 181 | |
| 182 | |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 183 | def _UseHermeticToolchain(target_os): |
erikchen | da01626 | 2016-11-09 04:32:13 | [diff] [blame] | 184 | current_dir = os.path.dirname(os.path.realpath(__file__)) |
| 185 | script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py') |
justincohen | cf1c482d | 2016-12-08 20:39:34 | [diff] [blame] | 186 | proc = subprocess.Popen([script_path, target_os], stdout=subprocess.PIPE) |
erikchen | da01626 | 2016-11-09 04:32:13 | [diff] [blame] | 187 | return '1' in proc.stdout.readline() |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 188 | |
erikchen | da01626 | 2016-11-09 04:32:13 | [diff] [blame] | 189 | |
| 190 | def 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) |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 212 | |
| 213 | |
Justin Cohen | 170c409b7 | 2017-09-13 02:37:02 | [diff] [blame] | 214 | def DownloadHermeticBuild(target_os, toolchain_version, toolchain_filename): |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 215 | if not _UseHermeticToolchain(target_os): |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 216 | return 0 |
| 217 | |
Justin Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 218 | toolchain_output_path = TOOLCHAIN_BUILD_DIR % target_os |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 219 | if ReadStampFile(target_os) == toolchain_version: |
Justin Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 220 | FinalizeUnpack(toolchain_output_path, target_os) |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 221 | return 0 |
| 222 | |
| 223 | if not CanAccessToolchainBucket(): |
erikchen | da01626 | 2016-11-09 04:32:13 | [diff] [blame] | 224 | RequestGsAuthentication() |
| 225 | return 1 |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 226 | |
| 227 | # Reset the stamp file in case the build is unsuccessful. |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 228 | WriteStampFile(target_os, '') |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 229 | |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 230 | toolchain_file = '%s.tgz' % toolchain_version |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 231 | toolchain_full_url = TOOLCHAIN_URL + toolchain_file |
| 232 | |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 233 | print 'Updating toolchain to %s...' % toolchain_version |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 234 | try: |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 235 | toolchain_file = toolchain_filename % toolchain_version |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 236 | toolchain_full_url = TOOLCHAIN_URL + toolchain_file |
Justin Cohen | ae7265c | 2017-09-15 13:17:50 | [diff] [blame] | 237 | DownloadAndUnpack(toolchain_full_url, toolchain_output_path) |
| 238 | FinalizeUnpack(toolchain_output_path, target_os) |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 239 | |
justincohen | abbd93dc | 2016-11-30 20:11:28 | [diff] [blame] | 240 | print 'Toolchain %s unpacked.' % toolchain_version |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 241 | WriteStampFile(target_os, toolchain_version) |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 242 | return 0 |
tikuta | 1a1610f | 2016-05-16 02:28:10 | [diff] [blame] | 243 | except Exception as e: |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 244 | print 'Failed to download toolchain %s.' % toolchain_file |
tikuta | 1a1610f | 2016-05-16 02:28:10 | [diff] [blame] | 245 | print 'Exception %s' % e |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 246 | print 'Exiting.' |
| 247 | return 1 |
| 248 | |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 249 | |
| 250 | def main(): |
| 251 | if sys.platform != 'darwin': |
| 252 | return 0 |
| 253 | |
| 254 | for target_os in GetPlatforms(): |
erikchen | d5dfcdb0 | 2017-06-29 00:22:53 | [diff] [blame] | 255 | if not PlatformMeetsHermeticXcodeRequirements(target_os): |
| 256 | print 'OS version does not support toolchain.' |
| 257 | continue |
| 258 | |
Justin Cohen | 9d09a66e | 2017-08-31 22:05:36 | [diff] [blame] | 259 | if target_os == 'ios': |
Justin Cohen | 170c409b7 | 2017-09-13 02:37:02 | [diff] [blame] | 260 | toolchain_version = os.environ.get('IOS_TOOLCHAIN_REVISION', |
| 261 | IOS_TOOLCHAIN_VERSION) |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 262 | toolchain_filename = 'ios-toolchain-%s.tgz' |
| 263 | else: |
Justin Cohen | 170c409b7 | 2017-09-13 02:37:02 | [diff] [blame] | 264 | toolchain_version = os.environ.get('MAC_TOOLCHAIN_REVISION', |
| 265 | MAC_TOOLCHAIN_VERSION) |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 266 | toolchain_filename = 'toolchain-%s.tgz' |
| 267 | |
| 268 | return_value = DownloadHermeticBuild( |
Justin Cohen | 170c409b7 | 2017-09-13 02:37:02 | [diff] [blame] | 269 | target_os, toolchain_version, toolchain_filename) |
sdefresne | aacc2845 | 2017-01-12 10:33:37 | [diff] [blame] | 270 | if return_value: |
| 271 | return return_value |
| 272 | |
| 273 | return 0 |
| 274 | |
| 275 | |
justincohen | 6a03a3d | 2016-03-26 21:44:38 | [diff] [blame] | 276 | if __name__ == '__main__': |
| 277 | sys.exit(main()) |