blob: 28cbd84d29918f0df80ef21141ccf79888bd5c05 [file] [log] [blame]
Ben Murdoch097c5b22016-05-18 11:27:45 +01001#!/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())