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 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 gsutil to be configured. 11 * Accepts the license. 12 * If xcode-select and xcodebuild are not passwordless in sudoers, requires 13 user interaction. 14""" 15 16import os 17import plistlib 18import shutil 19import subprocess 20import sys 21import tarfile 22import time 23import tempfile 24import urllib2 25 26# This can be changed after running /build/package_mac_toolchain.py. 27MAC_TOOLCHAIN_VERSION = '5B1008' 28MAC_TOOLCHAIN_SUB_REVISION = 3 29MAC_TOOLCHAIN_VERSION = '%s-%s' % (MAC_TOOLCHAIN_VERSION, 30 MAC_TOOLCHAIN_SUB_REVISION) 31IOS_TOOLCHAIN_VERSION = '8C1002' 32IOS_TOOLCHAIN_SUB_REVISION = 1 33IOS_TOOLCHAIN_VERSION = '%s-%s' % (IOS_TOOLCHAIN_VERSION, 34 IOS_TOOLCHAIN_SUB_REVISION) 35 36# Absolute path to src/ directory. 37REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 38 39# Absolute path to a file with gclient solutions. 40GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient') 41 42BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 43TOOLCHAIN_BUILD_DIR = os.path.join(BASE_DIR, '%s_files', 'Xcode.app') 44STAMP_FILE = os.path.join(BASE_DIR, '%s_files', 'toolchain_build_revision') 45TOOLCHAIN_URL = 'gs://chrome-mac-sdk/' 46 47def GetPlatforms(): 48 default_target_os = ["mac"] 49 try: 50 env = {} 51 execfile(GCLIENT_CONFIG, env, env) 52 return env.get('target_os', default_target_os) 53 except: 54 pass 55 return default_target_os 56 57 58def ReadStampFile(target_os): 59 """Return the contents of the stamp file, or '' if it doesn't exist.""" 60 try: 61 with open(STAMP_FILE % target_os, 'r') as f: 62 return f.read().rstrip() 63 except IOError: 64 return '' 65 66 67def WriteStampFile(target_os, s): 68 """Write s to the stamp file.""" 69 EnsureDirExists(os.path.dirname(STAMP_FILE % target_os)) 70 with open(STAMP_FILE % target_os, 'w') as f: 71 f.write(s) 72 f.write('\n') 73 74 75def EnsureDirExists(path): 76 if not os.path.exists(path): 77 os.makedirs(path) 78 79 80def DownloadAndUnpack(url, output_dir): 81 """Decompresses |url| into a cleared |output_dir|.""" 82 temp_name = tempfile.mktemp(prefix='mac_toolchain') 83 try: 84 print 'Downloading new toolchain.' 85 subprocess.check_call(['gsutil.py', 'cp', url, temp_name]) 86 if os.path.exists(output_dir): 87 print 'Deleting old toolchain.' 88 shutil.rmtree(output_dir) 89 EnsureDirExists(output_dir) 90 print 'Unpacking new toolchain.' 91 tarfile.open(mode='r:gz', name=temp_name).extractall(path=output_dir) 92 finally: 93 if os.path.exists(temp_name): 94 os.unlink(temp_name) 95 96 97def CanAccessToolchainBucket(): 98 """Checks whether the user has access to |TOOLCHAIN_URL|.""" 99 proc = subprocess.Popen(['gsutil.py', 'ls', TOOLCHAIN_URL], 100 stdout=subprocess.PIPE) 101 proc.communicate() 102 return proc.returncode == 0 103 104 105def LoadPlist(path): 106 """Loads Plist at |path| and returns it as a dictionary.""" 107 fd, name = tempfile.mkstemp() 108 try: 109 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) 110 with os.fdopen(fd, 'r') as f: 111 return plistlib.readPlist(f) 112 finally: 113 os.unlink(name) 114 115 116def AcceptLicense(target_os): 117 """Use xcodebuild to accept new toolchain license if necessary. Don't accept 118 the license if a newer license has already been accepted. This only works if 119 xcodebuild and xcode-select are passwordless in sudoers.""" 120 121 # Check old license 122 try: 123 target_license_plist_path = \ 124 os.path.join(TOOLCHAIN_BUILD_DIR % target_os, 125 *['Contents','Resources','LicenseInfo.plist']) 126 target_license_plist = LoadPlist(target_license_plist_path) 127 build_type = target_license_plist['licenseType'] 128 build_version = target_license_plist['licenseID'] 129 130 accepted_license_plist = LoadPlist( 131 '/Library/Preferences/com.apple.dt.Xcode.plist') 132 agreed_to_key = 'IDELast%sLicenseAgreedTo' % build_type 133 last_license_agreed_to = accepted_license_plist[agreed_to_key] 134 135 # Historically all Xcode build numbers have been in the format of AANNNN, so 136 # a simple string compare works. If Xcode's build numbers change this may 137 # need a more complex compare. 138 if build_version <= last_license_agreed_to: 139 # Don't accept the license of older toolchain builds, this will break the 140 # license of newer builds. 141 return 142 except (subprocess.CalledProcessError, KeyError): 143 # If there's never been a license of type |build_type| accepted, 144 # |target_license_plist_path| or |agreed_to_key| may not exist. 145 pass 146 147 print "Accepting license." 148 old_path = subprocess.Popen(['/usr/bin/xcode-select', '-p'], 149 stdout=subprocess.PIPE).communicate()[0].strip() 150 try: 151 build_dir = os.path.join( 152 TOOLCHAIN_BUILD_DIR % target_os, 'Contents/Developer') 153 subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', build_dir]) 154 subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-license', 'accept']) 155 finally: 156 subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', old_path]) 157 158 159def _UseHermeticToolchain(target_os): 160 current_dir = os.path.dirname(os.path.realpath(__file__)) 161 script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py') 162 proc = subprocess.Popen([script_path, target_os], stdout=subprocess.PIPE) 163 return '1' in proc.stdout.readline() 164 165 166def RequestGsAuthentication(): 167 """Requests that the user authenticate to be able to access gs://. 168 """ 169 print 'Access to ' + TOOLCHAIN_URL + ' not configured.' 170 print '-----------------------------------------------------------------' 171 print 172 print 'You appear to be a Googler.' 173 print 174 print 'I\'m sorry for the hassle, but you need to do a one-time manual' 175 print 'authentication. Please run:' 176 print 177 print ' download_from_google_storage --config' 178 print 179 print 'and follow the instructions.' 180 print 181 print 'NOTE 1: Use your google.com credentials, not chromium.org.' 182 print 'NOTE 2: Enter 0 when asked for a "project-id".' 183 print 184 print '-----------------------------------------------------------------' 185 print 186 sys.stdout.flush() 187 sys.exit(1) 188 189 190def DownloadHermeticBuild(target_os, default_version, toolchain_filename): 191 if not _UseHermeticToolchain(target_os): 192 print 'Using local toolchain for %s.' % target_os 193 return 0 194 195 toolchain_version = os.environ.get('MAC_TOOLCHAIN_REVISION', 196 default_version) 197 198 if ReadStampFile(target_os) == toolchain_version: 199 print 'Toolchain (%s) is already up to date.' % toolchain_version 200 AcceptLicense(target_os) 201 return 0 202 203 if not CanAccessToolchainBucket(): 204 RequestGsAuthentication() 205 return 1 206 207 # Reset the stamp file in case the build is unsuccessful. 208 WriteStampFile(target_os, '') 209 210 toolchain_file = '%s.tgz' % toolchain_version 211 toolchain_full_url = TOOLCHAIN_URL + toolchain_file 212 213 print 'Updating toolchain to %s...' % toolchain_version 214 try: 215 toolchain_file = toolchain_filename % toolchain_version 216 toolchain_full_url = TOOLCHAIN_URL + toolchain_file 217 DownloadAndUnpack(toolchain_full_url, TOOLCHAIN_BUILD_DIR % target_os) 218 AcceptLicense(target_os) 219 220 print 'Toolchain %s unpacked.' % toolchain_version 221 WriteStampFile(target_os, toolchain_version) 222 return 0 223 except Exception as e: 224 print 'Failed to download toolchain %s.' % toolchain_file 225 print 'Exception %s' % e 226 print 'Exiting.' 227 return 1 228 229 230def main(): 231 if sys.platform != 'darwin': 232 return 0 233 234 for target_os in GetPlatforms(): 235 if target_os == 'ios': 236 default_version = IOS_TOOLCHAIN_VERSION 237 toolchain_filename = 'ios-toolchain-%s.tgz' 238 else: 239 default_version = MAC_TOOLCHAIN_VERSION 240 toolchain_filename = 'toolchain-%s.tgz' 241 242 return_value = DownloadHermeticBuild( 243 target_os, default_version, toolchain_filename) 244 if return_value: 245 return return_value 246 247 return 0 248 249 250if __name__ == '__main__': 251 sys.exit(main()) 252