Home | History | Annotate | Download | only in build
      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())
    210