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"""Download necessary mac toolchain files under certain conditions. If 7xcode-select is already set and points to an external folder 8(e.g. /Application/Xcode.app), this script only runs if the GYP_DEFINE 9|force_mac_toolchain| is set. To override the values in 10|TOOLCHAIN_REVISION|-|TOOLCHAIN_SUB_REVISION| below, GYP_DEFINE 11mac_toolchain_revision can be used instead. 12 13This script will only run on machines if /usr/bin/xcodebuild and 14/usr/bin/xcode-select has been added to the sudoers list so the license can be 15accepted. 16 17Otherwise, user input would be required to complete the script. Perhaps future 18versions can be modified to allow for user input on developer machines. 19""" 20 21import os 22import plistlib 23import 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. 32TOOLCHAIN_REVISION = '5B1008' 33TOOLCHAIN_SUB_REVISION = 2 34TOOLCHAIN_VERSION = '%s-%s' % (TOOLCHAIN_REVISION, TOOLCHAIN_SUB_REVISION) 35 36BASE_DIR = os.path.abspath(os.path.dirname(__file__)) 37TOOLCHAIN_BUILD_DIR = os.path.join(BASE_DIR, 'mac_files', 'Xcode.app') 38STAMP_FILE = os.path.join(BASE_DIR, 'mac_files', 'toolchain_build_revision') 39TOOLCHAIN_URL = 'gs://chrome-mac-sdk/' 40 41 42def GetToolchainDirectory(): 43 if sys.platform == 'darwin' and not UseLocalMacSDK(): 44 return TOOLCHAIN_BUILD_DIR 45 else: 46 return None 47 48 49def SetToolchainEnvironment(): 50 mac_toolchain_dir = GetToolchainDirectory() 51 if mac_toolchain_dir: 52 os.environ['DEVELOPER_DIR'] = mac_toolchain_dir 53 54 55def ReadStampFile(): 56 """Return the contents of the stamp file, or '' if it doesn't exist.""" 57 try: 58 with open(STAMP_FILE, 'r') as f: 59 return f.read().rstrip() 60 except IOError: 61 return '' 62 63 64def WriteStampFile(s): 65 """Write s to the stamp file.""" 66 EnsureDirExists(os.path.dirname(STAMP_FILE)) 67 with open(STAMP_FILE, 'w') as f: 68 f.write(s) 69 f.write('\n') 70 71 72def EnsureDirExists(path): 73 if not os.path.exists(path): 74 os.makedirs(path) 75 76 77def DownloadAndUnpack(url, output_dir): 78 """Decompresses |url| into a cleared |output_dir|.""" 79 temp_name = tempfile.mktemp(prefix='mac_toolchain') 80 try: 81 print 'Downloading new toolchain.' 82 subprocess.check_call(['gsutil.py', 'cp', url, temp_name]) 83 if os.path.exists(output_dir): 84 print 'Deleting old toolchain.' 85 shutil.rmtree(output_dir) 86 EnsureDirExists(output_dir) 87 print 'Unpacking new toolchain.' 88 tarfile.open(mode='r:gz', name=temp_name).extractall(path=output_dir) 89 finally: 90 if os.path.exists(temp_name): 91 os.unlink(temp_name) 92 93 94def CanAccessToolchainBucket(): 95 """Checks whether the user has access to |TOOLCHAIN_URL|.""" 96 proc = subprocess.Popen(['gsutil.py', 'ls', TOOLCHAIN_URL], 97 stdout=subprocess.PIPE) 98 proc.communicate() 99 return proc.returncode == 0 100 101def LoadPlist(path): 102 """Loads Plist at |path| and returns it as a dictionary.""" 103 fd, name = tempfile.mkstemp() 104 try: 105 subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) 106 with os.fdopen(fd, 'r') as f: 107 return plistlib.readPlist(f) 108 finally: 109 os.unlink(name) 110 111 112def AcceptLicense(): 113 """Use xcodebuild to accept new toolchain license if necessary. Don't accept 114 the license if a newer license has already been accepted. This only works if 115 xcodebuild and xcode-select are passwordless in sudoers.""" 116 117 # Check old license 118 try: 119 target_license_plist_path = \ 120 os.path.join(TOOLCHAIN_BUILD_DIR, 121 *['Contents','Resources','LicenseInfo.plist']) 122 target_license_plist = LoadPlist(target_license_plist_path) 123 build_type = target_license_plist['licenseType'] 124 build_version = target_license_plist['licenseID'] 125 126 accepted_license_plist = LoadPlist( 127 '/Library/Preferences/com.apple.dt.Xcode.plist') 128 agreed_to_key = 'IDELast%sLicenseAgreedTo' % build_type 129 last_license_agreed_to = accepted_license_plist[agreed_to_key] 130 131 # Historically all Xcode build numbers have been in the format of AANNNN, so 132 # a simple string compare works. If Xcode's build numbers change this may 133 # need a more complex compare. 134 if build_version <= last_license_agreed_to: 135 # Don't accept the license of older toolchain builds, this will break the 136 # license of newer builds. 137 return 138 except (subprocess.CalledProcessError, KeyError): 139 # If there's never been a license of type |build_type| accepted, 140 # |target_license_plist_path| or |agreed_to_key| may not exist. 141 pass 142 143 print "Accepting license." 144 old_path = subprocess.Popen(['/usr/bin/xcode-select', '-p'], 145 stdout=subprocess.PIPE).communicate()[0].strip() 146 try: 147 build_dir = os.path.join(TOOLCHAIN_BUILD_DIR, 'Contents/Developer') 148 subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', build_dir]) 149 subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-license', 'accept']) 150 finally: 151 subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', old_path]) 152 153 154def UseLocalMacSDK(): 155 force_pull = os.environ.has_key('FORCE_MAC_TOOLCHAIN') 156 157 # Don't update the toolchain if there's already one installed outside of the 158 # expected location for a Chromium mac toolchain, unless |force_pull| is set. 159 proc = subprocess.Popen(['xcode-select', '-p'], stdout=subprocess.PIPE) 160 xcode_select_dir = proc.communicate()[0] 161 rc = proc.returncode 162 return (not force_pull and rc == 0 and 163 TOOLCHAIN_BUILD_DIR not in xcode_select_dir) 164 165 166def main(): 167 if sys.platform != 'darwin': 168 return 0 169 170 # TODO(justincohen): Add support for GN per crbug.com/570091 171 if UseLocalMacSDK(): 172 print 'Using local toolchain.' 173 return 0 174 175 toolchain_revision = os.environ.get('MAC_TOOLCHAIN_REVISION', 176 TOOLCHAIN_VERSION) 177 if ReadStampFile() == toolchain_revision: 178 print 'Toolchain (%s) is already up to date.' % toolchain_revision 179 AcceptLicense() 180 return 0 181 182 if not CanAccessToolchainBucket(): 183 print 'Cannot access toolchain bucket.' 184 return 0 185 186 # Reset the stamp file in case the build is unsuccessful. 187 WriteStampFile('') 188 189 toolchain_file = '%s.tgz' % toolchain_revision 190 toolchain_full_url = TOOLCHAIN_URL + toolchain_file 191 192 print 'Updating toolchain to %s...' % toolchain_revision 193 try: 194 toolchain_file = 'toolchain-%s.tgz' % toolchain_revision 195 toolchain_full_url = TOOLCHAIN_URL + toolchain_file 196 DownloadAndUnpack(toolchain_full_url, TOOLCHAIN_BUILD_DIR) 197 AcceptLicense() 198 199 print 'Toolchain %s unpacked.' % toolchain_revision 200 WriteStampFile(toolchain_revision) 201 return 0 202 except Exception as e: 203 print 'Failed to download toolchain %s.' % toolchain_file 204 print 'Exception %s' % e 205 print 'Exiting.' 206 return 1 207 208if __name__ == '__main__': 209 sys.exit(main()) 210