#!/usr/bin/env python # Copyright 2016 The Chromium Authors. All rights reserved. # Use of this source code is governed by a BSD-style license that can be # found in the LICENSE file. """ If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of date: * Downloads the hermetic mac toolchain * Requires gsutil to be configured. * Accepts the license. * If xcode-select and xcodebuild are not passwordless in sudoers, requires user interaction. The toolchain version can be overridden by setting IOS_TOOLCHAIN_REVISION or MAC_TOOLCHAIN_REVISION with the full revision, e.g. 9A235-1. """ from distutils.version import LooseVersion import os import platform import plistlib import shutil import subprocess import sys import tarfile import time import tempfile import urllib2 # This can be changed after running /build/package_mac_toolchain.py. MAC_TOOLCHAIN_VERSION = '8E2002' MAC_TOOLCHAIN_SUB_REVISION = 3 MAC_TOOLCHAIN_VERSION = '%s-%s' % (MAC_TOOLCHAIN_VERSION, MAC_TOOLCHAIN_SUB_REVISION) # The toolchain will not be downloaded if the minimum OS version is not met. # 16 is the major version number for macOS 10.12. MAC_MINIMUM_OS_VERSION = 16 IOS_TOOLCHAIN_VERSION = '9A235' IOS_TOOLCHAIN_SUB_REVISION = 1 IOS_TOOLCHAIN_VERSION = '%s-%s' % (IOS_TOOLCHAIN_VERSION, IOS_TOOLCHAIN_SUB_REVISION) # Absolute path to src/ directory. REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Absolute path to a file with gclient solutions. GCLIENT_CONFIG = os.path.join(os.path.dirname(REPO_ROOT), '.gclient') BASE_DIR = os.path.abspath(os.path.dirname(__file__)) TOOLCHAIN_BUILD_DIR = os.path.join(BASE_DIR, '%s_files', 'Xcode.app') STAMP_FILE = os.path.join(BASE_DIR, '%s_files', 'toolchain_build_revision') TOOLCHAIN_URL = 'gs://chrome-mac-sdk/' def PlatformMeetsHermeticXcodeRequirements(target_os): if target_os == 'ios': return True return int(platform.release().split('.')[0]) >= MAC_MINIMUM_OS_VERSION def GetPlatforms(): target_os = set(['mac']) try: env = {} execfile(GCLIENT_CONFIG, env, env) target_os |= set(env.get('target_os', target_os)) except: pass return target_os def ReadStampFile(target_os): """Return the contents of the stamp file, or '' if it doesn't exist.""" try: with open(STAMP_FILE % target_os, 'r') as f: return f.read().rstrip() except IOError: return '' def WriteStampFile(target_os, s): """Write s to the stamp file.""" EnsureDirExists(os.path.dirname(STAMP_FILE % target_os)) with open(STAMP_FILE % target_os, 'w') as f: f.write(s) f.write('\n') def EnsureDirExists(path): if not os.path.exists(path): os.makedirs(path) def DownloadAndUnpack(url, output_dir): """Decompresses |url| into a cleared |output_dir|.""" temp_name = tempfile.mktemp(prefix='mac_toolchain') try: print 'Downloading new toolchain.' subprocess.check_call(['gsutil.py', 'cp', url, temp_name]) if os.path.exists(output_dir): print 'Deleting old toolchain.' shutil.rmtree(output_dir) EnsureDirExists(output_dir) print 'Unpacking new toolchain.' tarfile.open(mode='r:gz', name=temp_name).extractall(path=output_dir) finally: if os.path.exists(temp_name): os.unlink(temp_name) def CanAccessToolchainBucket(): """Checks whether the user has access to |TOOLCHAIN_URL|.""" proc = subprocess.Popen(['gsutil.py', 'ls', TOOLCHAIN_URL], stdout=subprocess.PIPE) proc.communicate() return proc.returncode == 0 def LoadPlist(path): """Loads Plist at |path| and returns it as a dictionary.""" fd, name = tempfile.mkstemp() try: subprocess.check_call(['plutil', '-convert', 'xml1', '-o', name, path]) with os.fdopen(fd, 'r') as f: return plistlib.readPlist(f) finally: os.unlink(name) def FinalizeUnpack(output_dir, target_os): """Use xcodebuild to accept new toolchain license and run first launch installers if necessary. Don't accept the license if a newer license has already been accepted. This only works if xcodebuild and xcode-select are passwordless in sudoers.""" # Check old license try: target_license_plist_path = os.path.join( output_dir, 'Contents','Resources','LicenseInfo.plist') target_license_plist = LoadPlist(target_license_plist_path) build_type = target_license_plist['licenseType'] build_version = target_license_plist['licenseID'] accepted_license_plist = LoadPlist( '/Library/Preferences/com.apple.dt.Xcode.plist') agreed_to_key = 'IDELast%sLicenseAgreedTo' % build_type last_license_agreed_to = accepted_license_plist[agreed_to_key] # Historically all Xcode build numbers have been in the format of AANNNN, so # a simple string compare works. If Xcode's build numbers change this may # need a more complex compare. if build_version <= last_license_agreed_to: # Don't accept the license of older toolchain builds, this will break the # license of newer builds. return except (subprocess.CalledProcessError, KeyError): # If there's never been a license of type |build_type| accepted, # |target_license_plist_path| or |agreed_to_key| may not exist. pass print "Accepting license." target_version_plist_path = os.path.join( output_dir, 'Contents','version.plist') target_version_plist = LoadPlist(target_version_plist_path) short_version_string = target_version_plist['CFBundleShortVersionString'] old_path = subprocess.Popen(['/usr/bin/xcode-select', '-p'], stdout=subprocess.PIPE).communicate()[0].strip() try: build_dir = os.path.join(output_dir, 'Contents/Developer') subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', build_dir]) subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-license', 'accept']) if target_os == 'ios' and \ LooseVersion(short_version_string) >= LooseVersion("9.0"): print "Installing packages." subprocess.check_call(['sudo', '/usr/bin/xcodebuild', '-runFirstLaunch']) finally: subprocess.check_call(['sudo', '/usr/bin/xcode-select', '-s', old_path]) def _UseHermeticToolchain(target_os): current_dir = os.path.dirname(os.path.realpath(__file__)) script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py') proc = subprocess.Popen([script_path, target_os], stdout=subprocess.PIPE) return '1' in proc.stdout.readline() def RequestGsAuthentication(): """Requests that the user authenticate to be able to access gs://. """ print 'Access to ' + TOOLCHAIN_URL + ' not configured.' print '-----------------------------------------------------------------' print print 'You appear to be a Googler.' print print 'I\'m sorry for the hassle, but you need to do a one-time manual' print 'authentication. Please run:' print print ' download_from_google_storage --config' print print 'and follow the instructions.' print print 'NOTE 1: Use your google.com credentials, not chromium.org.' print 'NOTE 2: Enter 0 when asked for a "project-id".' print print '-----------------------------------------------------------------' print sys.stdout.flush() sys.exit(1) def DownloadHermeticBuild(target_os, toolchain_version, toolchain_filename): if not _UseHermeticToolchain(target_os): return 0 toolchain_output_path = TOOLCHAIN_BUILD_DIR % target_os if ReadStampFile(target_os) == toolchain_version: FinalizeUnpack(toolchain_output_path, target_os) return 0 if not CanAccessToolchainBucket(): RequestGsAuthentication() return 1 # Reset the stamp file in case the build is unsuccessful. WriteStampFile(target_os, '') toolchain_file = '%s.tgz' % toolchain_version toolchain_full_url = TOOLCHAIN_URL + toolchain_file print 'Updating toolchain to %s...' % toolchain_version try: toolchain_file = toolchain_filename % toolchain_version toolchain_full_url = TOOLCHAIN_URL + toolchain_file DownloadAndUnpack(toolchain_full_url, toolchain_output_path) FinalizeUnpack(toolchain_output_path, target_os) print 'Toolchain %s unpacked.' % toolchain_version WriteStampFile(target_os, toolchain_version) return 0 except Exception as e: print 'Failed to download toolchain %s.' % toolchain_file print 'Exception %s' % e print 'Exiting.' return 1 def main(): if sys.platform != 'darwin': return 0 for target_os in GetPlatforms(): if not PlatformMeetsHermeticXcodeRequirements(target_os): print 'OS version does not support toolchain.' continue if target_os == 'ios': toolchain_version = os.environ.get('IOS_TOOLCHAIN_REVISION', IOS_TOOLCHAIN_VERSION) toolchain_filename = 'ios-toolchain-%s.tgz' else: toolchain_version = os.environ.get('MAC_TOOLCHAIN_REVISION', MAC_TOOLCHAIN_VERSION) toolchain_filename = 'toolchain-%s.tgz' return_value = DownloadHermeticBuild( target_os, toolchain_version, toolchain_filename) if return_value: return return_value return 0 if __name__ == '__main__': sys.exit(main())