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