mac_toolchain.py 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
  1. #!/usr/bin/env python3
  2. # Copyright 2018 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. If should_use_hermetic_xcode.py emits "1", and the current toolchain is out of
  7. date:
  8. * Downloads the hermetic mac toolchain
  9. * Requires CIPD authentication. Run `cipd auth-login`, use Google account.
  10. * Accepts the license.
  11. * If xcode-select and xcodebuild are not passwordless in sudoers, requires
  12. user interaction.
  13. * Downloads standalone binaries from [a possibly different version of Xcode].
  14. The toolchain version can be overridden by setting MAC_TOOLCHAIN_REVISION with
  15. the full revision, e.g. 9A235.
  16. """
  17. import argparse
  18. import os
  19. import pkg_resources
  20. import platform
  21. import plistlib
  22. import shutil
  23. import subprocess
  24. import sys
  25. def LoadPList(path):
  26. """Loads Plist at |path| and returns it as a dictionary."""
  27. with open(path, 'rb') as f:
  28. return plistlib.load(f)
  29. # This contains binaries from Xcode 13.4.1 13F100, along with the macOS 12 SDK
  30. # (12.3 21E226). To build these packages, see comments in
  31. # build/xcode_binaries.yaml
  32. MAC_BINARIES_LABEL = 'infra_internal/ios/xcode/xcode_binaries/mac-amd64'
  33. MAC_BINARIES_TAG = 'OzUNvLYw4Z-9XcbsXRKaDWo3rbJtcD1B7BbGPqEQ8a0C'
  34. # The toolchain will not be downloaded if the minimum OS version is not met. 19
  35. # is the Darwin major version number for macOS 10.15. Xcode 13.3 13E113 only
  36. # claims support for running on macOS 12.0 and newer, but some bots are still
  37. # running older OS versions. 10.15.4, the macOS minimum through Xcode 12.4,
  38. # still seems to work.
  39. MAC_MINIMUM_OS_VERSION = [19, 4]
  40. BASE_DIR = os.path.abspath(os.path.dirname(__file__))
  41. TOOLCHAIN_ROOT = os.path.join(BASE_DIR, 'mac_files')
  42. TOOLCHAIN_BUILD_DIR = os.path.join(TOOLCHAIN_ROOT, 'Xcode.app')
  43. # Always integrity-check the entire SDK. Mac SDK packages are complex and often
  44. # hit edge cases in cipd (eg https://crbug.com/1033987,
  45. # https://crbug.com/915278), and generally when this happens it requires manual
  46. # intervention to fix.
  47. # Note the trailing \n!
  48. PARANOID_MODE = '$ParanoidMode CheckIntegrity\n'
  49. def PlatformMeetsHermeticXcodeRequirements():
  50. if sys.platform != 'darwin':
  51. return True
  52. needed = MAC_MINIMUM_OS_VERSION
  53. major_version = [int(v) for v in platform.release().split('.')[:len(needed)]]
  54. return major_version >= needed
  55. def _UseHermeticToolchain():
  56. current_dir = os.path.dirname(os.path.realpath(__file__))
  57. script_path = os.path.join(current_dir, 'mac/should_use_hermetic_xcode.py')
  58. proc = subprocess.Popen([script_path, 'mac'], stdout=subprocess.PIPE)
  59. return '1' in proc.stdout.readline().decode()
  60. def RequestCipdAuthentication():
  61. """Requests that the user authenticate to access Xcode CIPD packages."""
  62. print('Access to Xcode CIPD package requires authentication.')
  63. print('-----------------------------------------------------------------')
  64. print()
  65. print('You appear to be a Googler.')
  66. print()
  67. print('I\'m sorry for the hassle, but you may need to do a one-time manual')
  68. print('authentication. Please run:')
  69. print()
  70. print(' cipd auth-login')
  71. print()
  72. print('and follow the instructions.')
  73. print()
  74. print('NOTE: Use your google.com credentials, not chromium.org.')
  75. print()
  76. print('-----------------------------------------------------------------')
  77. print()
  78. sys.stdout.flush()
  79. def PrintError(message):
  80. # Flush buffers to ensure correct output ordering.
  81. sys.stdout.flush()
  82. sys.stderr.write(message + '\n')
  83. sys.stderr.flush()
  84. def InstallXcodeBinaries():
  85. """Installs the Xcode binaries needed to build Chrome and accepts the license.
  86. This is the replacement for InstallXcode that installs a trimmed down version
  87. of Xcode that is OS-version agnostic.
  88. """
  89. # First make sure the directory exists. It will serve as the cipd root. This
  90. # also ensures that there will be no conflicts of cipd root.
  91. binaries_root = os.path.join(TOOLCHAIN_ROOT, 'xcode_binaries')
  92. if not os.path.exists(binaries_root):
  93. os.makedirs(binaries_root)
  94. # 'cipd ensure' is idempotent.
  95. args = ['cipd', 'ensure', '-root', binaries_root, '-ensure-file', '-']
  96. p = subprocess.Popen(args,
  97. universal_newlines=True,
  98. stdin=subprocess.PIPE,
  99. stdout=subprocess.PIPE,
  100. stderr=subprocess.PIPE)
  101. stdout, stderr = p.communicate(input=PARANOID_MODE + MAC_BINARIES_LABEL +
  102. ' ' + MAC_BINARIES_TAG)
  103. if p.returncode != 0:
  104. print(stdout)
  105. print(stderr)
  106. RequestCipdAuthentication()
  107. return 1
  108. if sys.platform != 'darwin':
  109. return 0
  110. # Accept the license for this version of Xcode if it's newer than the
  111. # currently accepted version.
  112. cipd_xcode_version_plist_path = os.path.join(binaries_root,
  113. 'Contents/version.plist')
  114. cipd_xcode_version_plist = LoadPList(cipd_xcode_version_plist_path)
  115. cipd_xcode_version = cipd_xcode_version_plist['CFBundleShortVersionString']
  116. cipd_license_path = os.path.join(binaries_root,
  117. 'Contents/Resources/LicenseInfo.plist')
  118. cipd_license_plist = LoadPList(cipd_license_path)
  119. cipd_license_version = cipd_license_plist['licenseID']
  120. should_overwrite_license = True
  121. current_license_path = '/Library/Preferences/com.apple.dt.Xcode.plist'
  122. if os.path.exists(current_license_path):
  123. current_license_plist = LoadPList(current_license_path)
  124. xcode_version = current_license_plist.get(
  125. 'IDEXcodeVersionForAgreedToGMLicense')
  126. if (xcode_version is not None and pkg_resources.parse_version(xcode_version)
  127. >= pkg_resources.parse_version(cipd_xcode_version)):
  128. should_overwrite_license = False
  129. if not should_overwrite_license:
  130. return 0
  131. # Use puppet's sudoers script to accept the license if its available.
  132. license_accept_script = '/usr/local/bin/xcode_accept_license.sh'
  133. if os.path.exists(license_accept_script):
  134. args = [
  135. 'sudo', license_accept_script, cipd_xcode_version, cipd_license_version
  136. ]
  137. subprocess.check_call(args)
  138. return 0
  139. # Otherwise manually accept the license. This will prompt for sudo.
  140. print('Accepting new Xcode license. Requires sudo.')
  141. sys.stdout.flush()
  142. args = [
  143. 'sudo', 'defaults', 'write', current_license_path,
  144. 'IDEXcodeVersionForAgreedToGMLicense', cipd_xcode_version
  145. ]
  146. subprocess.check_call(args)
  147. args = [
  148. 'sudo', 'defaults', 'write', current_license_path,
  149. 'IDELastGMLicenseAgreedTo', cipd_license_version
  150. ]
  151. subprocess.check_call(args)
  152. args = ['sudo', 'plutil', '-convert', 'xml1', current_license_path]
  153. subprocess.check_call(args)
  154. return 0
  155. def main():
  156. if not _UseHermeticToolchain():
  157. print('Skipping Mac toolchain installation for mac')
  158. return 0
  159. parser = argparse.ArgumentParser(description='Download hermetic Xcode.')
  160. args = parser.parse_args()
  161. if not PlatformMeetsHermeticXcodeRequirements():
  162. print('OS version does not support toolchain.')
  163. return 0
  164. return InstallXcodeBinaries()
  165. if __name__ == '__main__':
  166. sys.exit(main())