Ben Murdoch | 097c5b2 | 2016-05-18 11:27:45 +0100 | [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 | |
| 6 | """Download necessary mac toolchain files under certain conditions. If |
| 7 | xcode-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 |
| 11 | mac_toolchain_revision can be used instead. |
| 12 | |
| 13 | This 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 |
| 15 | accepted. |
| 16 | |
| 17 | Otherwise, user input would be required to complete the script. Perhaps future |
| 18 | versions can be modified to allow for user input on developer machines. |
| 19 | """ |
| 20 | |
| 21 | import os |
| 22 | import plistlib |
| 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. |
| 32 | TOOLCHAIN_REVISION = '5B1008' |
| 33 | TOOLCHAIN_SUB_REVISION = 2 |
| 34 | TOOLCHAIN_VERSION = '%s-%s' % (TOOLCHAIN_REVISION, TOOLCHAIN_SUB_REVISION) |
| 35 | |
| 36 | BASE_DIR = os.path.abspath(os.path.dirname(__file__)) |
| 37 | TOOLCHAIN_BUILD_DIR = os.path.join(BASE_DIR, 'mac_files', 'Xcode.app') |
| 38 | STAMP_FILE = os.path.join(BASE_DIR, 'mac_files', 'toolchain_build_revision') |
| 39 | TOOLCHAIN_URL = 'gs://chrome-mac-sdk/' |
| 40 | |
| 41 | |
| 42 | def GetToolchainDirectory(): |
| 43 | if sys.platform == 'darwin' and not UseLocalMacSDK(): |
| 44 | return TOOLCHAIN_BUILD_DIR |
| 45 | else: |
| 46 | return None |
| 47 | |
| 48 | |
| 49 | def SetToolchainEnvironment(): |
| 50 | mac_toolchain_dir = GetToolchainDirectory() |
| 51 | if mac_toolchain_dir: |
| 52 | os.environ['DEVELOPER_DIR'] = mac_toolchain_dir |
| 53 | |
| 54 | |
| 55 | def 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 | |
| 64 | def 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 | |
| 72 | def EnsureDirExists(path): |
| 73 | if not os.path.exists(path): |
| 74 | os.makedirs(path) |
| 75 | |
| 76 | |
| 77 | def 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 | |
| 94 | def 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 | |
| 101 | def 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 | |
| 112 | def 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 | |
| 154 | def 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 | |
| 166 | def 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 | |
| 208 | if __name__ == '__main__': |
| 209 | sys.exit(main()) |