clone.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. #!/usr/bin/env python3
  2. # -*- coding: UTF-8 -*-
  3. # Copyright (c) 2023 The ungoogled-chromium Authors. All rights reserved.
  4. # Use of this source code is governed by a BSD-style license that can be
  5. # found in the LICENSE file.
  6. """
  7. Module for cloning the source tree.
  8. """
  9. import re
  10. import sys
  11. from argparse import ArgumentParser
  12. from os import environ, pathsep
  13. from pathlib import Path
  14. from shutil import copytree, copy, move
  15. from stat import S_IWRITE
  16. from subprocess import run
  17. from _common import add_common_params, get_chromium_version, get_logger
  18. from prune_binaries import CONTINGENT_PATHS
  19. # Config file for gclient
  20. # Instances of 'src' replaced with UC_OUT, which will be replaced with the output directory
  21. # third_party/angle/third_party/VK-GL-CTS/src is set to None since it's large and unused
  22. # target_* arguments set to match tarball rather than actual build target
  23. GC_CONFIG = """\
  24. solutions = [
  25. {
  26. "name": "UC_OUT",
  27. "url": "https://chromium.googlesource.com/chromium/src.git",
  28. "managed": False,
  29. "custom_deps": {
  30. "UC_OUT/third_party/angle/third_party/VK-GL-CTS/src": None,
  31. },
  32. "custom_vars": {
  33. "checkout_configuration": "small",
  34. },
  35. },
  36. ];
  37. target_os = ['unix'];
  38. target_os_only = True;
  39. target_cpu = ['x64'];
  40. target_cpu_only = True;
  41. """
  42. def clone(args): # pylint: disable=too-many-branches, too-many-statements
  43. """Clones, downloads, and generates the required sources"""
  44. get_logger().info('Setting up cloning environment')
  45. iswin = sys.platform.startswith('win')
  46. chromium_version = get_chromium_version()
  47. ucstaging = args.output / 'uc_staging'
  48. dtpath = ucstaging / 'depot_tools'
  49. gnpath = ucstaging / 'gn'
  50. environ['GCLIENT_FILE'] = str(ucstaging / '.gclient')
  51. environ['PATH'] += pathsep + str(dtpath)
  52. environ['PYTHONPATH'] = str(dtpath)
  53. # Prevent gclient from auto updating depot_tools
  54. environ['DEPOT_TOOLS_UPDATE'] = '0'
  55. # Don't generate pycache files
  56. environ['PYTHONDONTWRITEBYTECODE'] = '1'
  57. # Allow usage of system python
  58. environ['VPYTHON_BYPASS'] = 'manually managed python not supported by chrome operations'
  59. # Google has some regex strings that aren't escaped properly or set as raw
  60. environ["PYTHONWARNINGS"] = "ignore::SyntaxWarning"
  61. # depth=2 since generating LASTCHANGE and gpu_lists_version.h require at least two commits
  62. get_logger().info('Cloning chromium source: %s', chromium_version)
  63. if (args.output / '.git').exists():
  64. run(['git', 'fetch', 'origin', 'tag', chromium_version, '--depth=2'],
  65. cwd=args.output,
  66. check=True)
  67. run(['git', 'reset', '--hard', 'FETCH_HEAD'], cwd=args.output, check=True)
  68. run(['git', 'clean', '-ffdx', '-e', 'uc_staging'], cwd=args.output, check=True)
  69. else:
  70. run([
  71. 'git', 'clone', '-c', 'advice.detachedHead=false', '-b', chromium_version, '--depth=2',
  72. "https://chromium.googlesource.com/chromium/src",
  73. str(args.output)
  74. ],
  75. check=True)
  76. # Set up staging directory
  77. ucstaging.mkdir(exist_ok=True)
  78. get_logger().info('Cloning depot_tools')
  79. dt_commit = re.search(r"depot_tools\.git'\s*\+\s*'@'\s*\+\s*'([^']+)',",
  80. Path(args.output / 'DEPS').read_text()).group(1)
  81. if not dt_commit:
  82. get_logger().error('Unable to obtain commit for depot_tools checkout')
  83. sys.exit(1)
  84. if not dtpath.exists():
  85. dtpath.mkdir()
  86. run(['git', 'init', '-q'], cwd=dtpath, check=True)
  87. run([
  88. 'git', 'remote', 'add', 'origin',
  89. 'https://chromium.googlesource.com/chromium/tools/depot_tools'
  90. ],
  91. cwd=dtpath,
  92. check=True)
  93. run(['git', 'fetch', '--depth=1', 'origin', dt_commit], cwd=dtpath, check=True)
  94. run(['git', 'reset', '--hard', dt_commit], cwd=dtpath, check=True)
  95. run(['git', 'clean', '-ffdx'], cwd=dtpath, check=True)
  96. if iswin:
  97. (dtpath / 'git.bat').write_text('git')
  98. # Apply changes to gclient
  99. run(['git', 'apply'],
  100. input=Path(__file__).with_name('depot_tools.patch').read_text().replace(
  101. 'UC_OUT', str(args.output)).replace('UC_STAGING', str(ucstaging)),
  102. cwd=dtpath,
  103. check=True,
  104. universal_newlines=True)
  105. # gn requires full history to be able to generate last_commit_position.h
  106. get_logger().info('Cloning gn')
  107. if gnpath.exists():
  108. run(['git', 'fetch'], cwd=gnpath, check=True)
  109. run(['git', 'reset', '--hard', 'FETCH_HEAD'], cwd=gnpath, check=True)
  110. run(['git', 'clean', '-ffdx'], cwd=gnpath, check=True)
  111. else:
  112. run(['git', 'clone', "https://gn.googlesource.com/gn", str(gnpath)], check=True)
  113. get_logger().info('Running gsync')
  114. if args.custom_config:
  115. copy(args.custom_config, ucstaging / '.gclient').replace('UC_OUT', str(args.output))
  116. else:
  117. (ucstaging / '.gclient').write_text(GC_CONFIG.replace('UC_OUT', str(args.output)))
  118. gcpath = dtpath / 'gclient'
  119. if iswin:
  120. gcpath = gcpath.with_suffix('.bat')
  121. # -f, -D, and -R forces a hard reset on changes and deletes deps that have been removed
  122. run([str(gcpath), 'sync', '-f', '-D', '-R', '--no-history', '--nohooks'], check=True)
  123. # Follow tarball procedure:
  124. # https://source.chromium.org/chromium/chromium/tools/build/+/main:recipes/recipes/publish_tarball.py
  125. get_logger().info('Downloading node modules')
  126. run([
  127. sys.executable,
  128. str(dtpath / 'download_from_google_storage.py'), '--no_resume', '--extract', '--no_auth',
  129. '--bucket', 'chromium-nodejs', '-s',
  130. str(args.output / 'third_party' / 'node' / 'node_modules.tar.gz.sha1')
  131. ],
  132. check=True)
  133. get_logger().info('Downloading pgo profiles')
  134. run([
  135. sys.executable,
  136. str(args.output / 'tools' / 'update_pgo_profiles.py'), '--target=' + args.pgo, 'update',
  137. '--gs-url-base=chromium-optimization-profiles/pgo_profiles'
  138. ],
  139. check=True)
  140. # https://chromium-review.googlesource.com/c/chromium/tools/build/+/4380399
  141. run([
  142. sys.executable,
  143. str(args.output / 'v8' / 'tools' / 'builtins-pgo' / 'download_profiles.py'), 'download',
  144. '--depot-tools',
  145. str(dtpath)
  146. ],
  147. check=True)
  148. get_logger().info('Generating: DAWN_VERSION')
  149. run([
  150. sys.executable,
  151. str(args.output / 'build' / 'util' / 'lastchange.py'), '-s',
  152. str(args.output / 'third_party' / 'dawn'), '--revision',
  153. str(args.output / 'gpu' / 'webgpu' / 'DAWN_VERSION')
  154. ],
  155. check=True)
  156. get_logger().info('Generating: LASTCHANGE')
  157. run([
  158. sys.executable,
  159. str(args.output / 'build' / 'util' / 'lastchange.py'), '-o',
  160. str(args.output / 'build' / 'util' / 'LASTCHANGE')
  161. ],
  162. check=True)
  163. get_logger().info('Generating: gpu_lists_version.h')
  164. run([
  165. sys.executable,
  166. str(args.output / 'build' / 'util' / 'lastchange.py'), '-m', 'GPU_LISTS_VERSION',
  167. '--revision-id-only', '--header',
  168. str(args.output / 'gpu' / 'config' / 'gpu_lists_version.h')
  169. ],
  170. check=True)
  171. get_logger().info('Generating: skia_commit_hash.h')
  172. run([
  173. sys.executable,
  174. str(args.output / 'build' / 'util' / 'lastchange.py'), '-m', 'SKIA_COMMIT_HASH', '-s',
  175. str(args.output / 'third_party' / 'skia'), '--header',
  176. str(args.output / 'skia' / 'ext' / 'skia_commit_hash.h')
  177. ],
  178. check=True)
  179. get_logger().info('Generating: last_commit_position.h')
  180. run([sys.executable, str(gnpath / 'build' / 'gen.py')], check=True)
  181. for item in gnpath.iterdir():
  182. if not item.is_dir():
  183. copy(item, args.output / 'tools' / 'gn')
  184. elif item.name != '.git' and item.name != 'out':
  185. copytree(item, args.output / 'tools' / 'gn' / item.name)
  186. move(str(gnpath / 'out' / 'last_commit_position.h'),
  187. str(args.output / 'tools' / 'gn' / 'bootstrap'))
  188. get_logger().info('Removing uneeded files')
  189. # Match removals for the tarball:
  190. # https://source.chromium.org/chromium/chromium/tools/build/+/main:recipes/recipe_modules/chromium/resources/export_tarball.py
  191. remove_dirs = (
  192. (args.output / 'chrome' / 'test' / 'data'),
  193. (args.output / 'content' / 'test' / 'data'),
  194. (args.output / 'courgette' / 'testdata'),
  195. (args.output / 'extensions' / 'test' / 'data'),
  196. (args.output / 'media' / 'test' / 'data'),
  197. (args.output / 'native_client' / 'src' / 'trusted' / 'service_runtime' / 'testdata'),
  198. (args.output / 'third_party' / 'blink' / 'tools'),
  199. (args.output / 'third_party' / 'blink' / 'web_tests'),
  200. (args.output / 'third_party' / 'breakpad' / 'breakpad' / 'src' / 'processor' / 'testdata'),
  201. (args.output / 'third_party' / 'catapult' / 'tracing' / 'test_data'),
  202. (args.output / 'third_party' / 'hunspell' / 'tests'),
  203. (args.output / 'third_party' / 'hunspell_dictionaries'),
  204. (args.output / 'third_party' / 'jdk' / 'current'),
  205. (args.output / 'third_party' / 'jdk' / 'extras'),
  206. (args.output / 'third_party' / 'liblouis' / 'src' / 'tests' / 'braille-specs'),
  207. (args.output / 'third_party' / 'xdg-utils' / 'tests'),
  208. (args.output / 'v8' / 'test'),
  209. )
  210. keep_files = (
  211. (args.output / 'chrome' / 'test' / 'data' / 'webui' / 'i18n_process_css_test.html'),
  212. (args.output / 'chrome' / 'test' / 'data' / 'webui' / 'mojo' / 'foobar.mojom'),
  213. (args.output / 'chrome' / 'test' / 'data' / 'webui' / 'web_ui_test.mojom'),
  214. (args.output / 'v8' / 'test' / 'torque' / 'test-torque.tq'),
  215. )
  216. keep_suffix = ('.gn', '.gni', '.grd', '.gyp', '.isolate', '.pydeps')
  217. # Include Contingent Paths
  218. for cpath in CONTINGENT_PATHS:
  219. remove_dirs += (args.output / Path(cpath), )
  220. for remove_dir in remove_dirs:
  221. for path in sorted(remove_dir.rglob('*'), key=lambda l: len(str(l)), reverse=True):
  222. if path.is_file() and path not in keep_files and path.suffix not in keep_suffix:
  223. try:
  224. path.unlink()
  225. # read-only files can't be deleted on Windows
  226. # so remove the flag and try again.
  227. except PermissionError:
  228. path.chmod(S_IWRITE)
  229. path.unlink()
  230. elif path.is_dir() and not any(path.iterdir()):
  231. try:
  232. path.rmdir()
  233. except PermissionError:
  234. path.chmod(S_IWRITE)
  235. path.rmdir()
  236. for path in sorted(args.output.rglob('*'), key=lambda l: len(str(l)), reverse=True):
  237. if not path.is_symlink() and '.git' not in path.parts:
  238. if path.is_file() and ('out' in path.parts or path.name.startswith('ChangeLog')):
  239. try:
  240. path.unlink()
  241. except PermissionError:
  242. path.chmod(S_IWRITE)
  243. path.unlink()
  244. elif path.is_dir() and not any(path.iterdir()):
  245. try:
  246. path.rmdir()
  247. except PermissionError:
  248. path.chmod(S_IWRITE)
  249. path.rmdir()
  250. get_logger().info('Source cloning complete')
  251. def main():
  252. """CLI Entrypoint"""
  253. parser = ArgumentParser(description=__doc__)
  254. parser.add_argument('-o',
  255. '--output',
  256. type=Path,
  257. metavar='DIRECTORY',
  258. default='chromium',
  259. help='Output directory for the cloned sources. Default: %(default)s')
  260. parser.add_argument('-c',
  261. '--custom-config',
  262. type=Path,
  263. metavar='FILE',
  264. help='Supply a replacement for the default gclient config.')
  265. parser.add_argument('-p',
  266. '--pgo',
  267. default='linux',
  268. choices=('linux', 'mac', 'mac-arm', 'win32', 'win64'),
  269. help='Specifiy which pgo profile to download. Default: %(default)s')
  270. add_common_params(parser)
  271. args = parser.parse_args()
  272. clone(args)
  273. if __name__ == '__main__':
  274. main()