makepkg-source-roller.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  1. from collections import OrderedDict
  2. from importlib.util import spec_from_loader, module_from_spec
  3. from importlib.machinery import SourceFileLoader
  4. from tempfile import NamedTemporaryFile
  5. from heapq import heappush
  6. import sys
  7. import requests
  8. import base64
  9. import re
  10. import os
  11. def eprint(*args, **kwargs):
  12. print(*args, file=sys.stderr, **kwargs)
  13. def fetch_deps(url, rev):
  14. # Get the DEPS file from the given URL and revision
  15. if "googlesource.com" in url:
  16. response = requests.get(f"{url}/+/{rev}/DEPS?format=text")
  17. response.raise_for_status()
  18. return base64.b64decode(response.text).decode("utf-8")
  19. elif url.startswith("https://github.com/"):
  20. if url.endswith(".git"):
  21. url = url[: -len(".git")]
  22. response = requests.get(f"{url}/raw/{rev}/DEPS")
  23. response.raise_for_status()
  24. return response.text
  25. else:
  26. raise Exception(f"Unimplemented for URL {url}")
  27. class Str:
  28. def __init__(self, s):
  29. self.inner = s
  30. def __str__(self):
  31. return self.inner
  32. ignored_dep_prefix = [
  33. # MacOS specific
  34. "src/third_party/squirrel.mac",
  35. # Unnecessary parts
  36. "src/docs/website",
  37. ]
  38. def parse_deps(path, prefix="", is_src=False, vars=None, reverse_map=None):
  39. """
  40. path: Path to the DEPS file
  41. prefix: Prefix to add when using recursedeps
  42. is_src: Whether the current DEPS file is the one from "src" repo
  43. vars: Override variables when generating gclient gn args file
  44. reverse_map: Map from url to path. Used for de-duplication
  45. """
  46. spec = spec_from_loader("deps", SourceFileLoader("deps", path))
  47. deps_module = module_from_spec(spec)
  48. def var_substitute(var_name):
  49. return deps_module.vars[var_name]
  50. deps_module.Var = var_substitute
  51. deps_module.Str = Str
  52. spec.loader.exec_module(deps_module)
  53. for k in (
  54. "checkout_win",
  55. "checkout_mac",
  56. "checkout_ios",
  57. "checkout_chromeos",
  58. "checkout_fuchsia",
  59. "checkout_android",
  60. "checkout_cxx_debugging_extension_deps",
  61. ):
  62. deps_module.vars[k] = False
  63. deps_module.vars["checkout_linux"] = True
  64. deps_module.vars["build_with_chromium"] = True
  65. deps_module.vars["host_os"] = "linux"
  66. use_relative_paths = (
  67. hasattr(deps_module, "use_relative_paths") and deps_module.use_relative_paths
  68. )
  69. def url_and_revision(raw_url):
  70. url = raw_url.format(**deps_module.vars)
  71. url, rev = url.rsplit("@", 1)
  72. if '.googlesource.com/' in url and not url.endswith(".git"):
  73. # Unify url format by adding .git suffix (for de-duplication)
  74. url += ".git"
  75. return (url, rev)
  76. def format_path(dep_name):
  77. return dep_name if not use_relative_paths else f"{prefix}/{dep_name}"
  78. real_deps = OrderedDict()
  79. cipd_deps = {}
  80. reverse_map = reverse_map or {}
  81. def add_dep(dep_name, raw_url):
  82. path = format_path(dep_name)
  83. for ignored_prefix in ignored_dep_prefix:
  84. if path.startswith(ignored_prefix):
  85. eprint(f"Ignoring {path}")
  86. return
  87. url, rev = url_and_revision(raw_url)
  88. real_deps[path] = (url, rev)
  89. # Add to reverse map for de-duplication, use a heap to make sure the shortest path is chosen
  90. heappush(reverse_map.setdefault(url, []), (len(path), path))
  91. for dep_name, dep_value in deps_module.deps.items():
  92. if isinstance(dep_value, dict):
  93. if "dep_type" in dep_value:
  94. if dep_value["dep_type"] == "cipd":
  95. cipd_deps[format_path(dep_name)] = dep_value["packages"]
  96. else:
  97. raise Exception(f"Unknown DEP {dep_name} = {dep_value}")
  98. else:
  99. if "condition" in dep_value and not eval(
  100. dep_value["condition"], vars, deps_module.vars
  101. ):
  102. eprint(
  103. f"Skipping {format_path(dep_name)} because of unmet condition {dep_value['condition']}"
  104. )
  105. continue
  106. add_dep(dep_name, dep_value["url"])
  107. elif isinstance(dep_value, str):
  108. add_dep(dep_name, dep_value)
  109. else:
  110. raise Exception(f"Unknown DEP {dep_name} = {dep_value}")
  111. gclient_gn_args = {}
  112. vars = vars or {}
  113. if is_src and hasattr(deps_module, "gclient_gn_args"):
  114. for arg in deps_module.gclient_gn_args:
  115. # electron vars overwrites chromium vars
  116. gclient_gn_args[arg] = (deps_module.vars | vars).get(arg)
  117. if hasattr(deps_module, "recursedeps"):
  118. for dep in deps_module.recursedeps:
  119. if dep not in real_deps:
  120. eprint(f"Skipping recursive DEP {dep} as it's not found in deps dict")
  121. continue
  122. eprint(f"Fetching recursedep {dep}")
  123. deps_text = fetch_deps(*real_deps[dep])
  124. with NamedTemporaryFile(mode="w", delete=True) as f:
  125. f.write(deps_text)
  126. f.flush()
  127. dep_deps, dep_gclient_gn_args, dep_cipd_deps, _ = parse_deps(
  128. f.name,
  129. format_path(dep),
  130. dep == "src",
  131. deps_module.vars | vars,
  132. reverse_map,
  133. )
  134. real_deps.update(dep_deps)
  135. gclient_gn_args.update(dep_gclient_gn_args)
  136. cipd_deps.update(dep_cipd_deps)
  137. return real_deps, gclient_gn_args, cipd_deps, reverse_map
  138. repos_with_changed_url = {
  139. "https://chromium.googlesource.com/chromium/llvm-project/compiler-rt/lib/fuzzer.git",
  140. "https://chromium.googlesource.com/external/github.com/protocolbuffers/protobuf.git",
  141. }
  142. def get_source_path(path, url, pkgname, reverse_map):
  143. """returns the source path and whether it's deduplicated or not"""
  144. deduplicated = False
  145. if len(reverse_map[url]) > 1:
  146. # Deduplicate, choose the shortest path
  147. shortest = reverse_map[url][0][1]
  148. if path != shortest:
  149. eprint(f"Deduplicate: {path} -> {shortest}")
  150. deduplicated = True
  151. path = shortest
  152. flattened = path.replace("/", "_")
  153. result = re.sub("^src", "chromium-mirror", flattened)
  154. if url in repos_with_changed_url:
  155. # To make makepkg happy when using SRCDEST
  156. result += f"_{pkgname}"
  157. return result, deduplicated
  158. def generate_fragment(rev):
  159. if "." in rev:
  160. # Treat revisions that contain dot as tags
  161. return f"tag={rev}"
  162. else:
  163. return f"commit={rev}"
  164. preferred_url_map = {
  165. # Replace with github mirror
  166. "https://chromium.googlesource.com/chromium/src.git": "https://github.com/chromium/chromium.git",
  167. }
  168. def get_preferred_url(url):
  169. preferred_url = preferred_url_map.get(url)
  170. return preferred_url or url
  171. def generate_source_list(deps, indent, extra_sources, pkgname, reverse_map):
  172. for path, (url, rev) in deps.items():
  173. source_path, deduplicated = get_source_path(path, url, pkgname, reverse_map)
  174. if deduplicated:
  175. # Skip the duplicated source
  176. continue
  177. yield f"{indent}{source_path}::git+{get_preferred_url(url)}#{generate_fragment(rev)}"
  178. for s in extra_sources:
  179. yield f"{indent}{s}"
  180. def generate_managed_scripts(deps, extra_cmds, pkgname, reverse_map):
  181. script = """#!/usr/bin/env rbash
  182. set -e
  183. # Generated file. Do not modify by hand.
  184. # Usage: script <CARCH>
  185. place_subproject_into_tree () {
  186. # place_subproject_into_tree flattened_path path should_copy
  187. local parent_dir="$(dirname "$2")"
  188. if [[ -n "$parent_dir" ]]; then
  189. mkdir -p "$parent_dir"
  190. fi
  191. # Remove the target dir
  192. rm -rf "$2"
  193. if [[ "$3" == "true" ]]; then
  194. cp -r "$1" "$2"
  195. else
  196. mv -v "$1" "$2"
  197. fi
  198. }
  199. CARCH="$1"
  200. case "$CARCH" in
  201. x86_64)
  202. _go_arch=amd64;;
  203. *)
  204. _go_arch="$CARCH";;
  205. esac
  206. """
  207. for path, (url, rev) in deps.items():
  208. source_path, deduplicated = get_source_path(path, url, pkgname, reverse_map)
  209. if deduplicated:
  210. shortest = reverse_map[url][0][1]
  211. script += f"place_subproject_into_tree {shortest} {path} true\n"
  212. script += f"git -C {path} checkout --detach {rev}\n"
  213. else:
  214. script += f"place_subproject_into_tree {source_path} {path} false\n"
  215. # Additional Commands
  216. script += "\n".join(extra_cmds)
  217. filename = "prepare-electron-source-tree.sh"
  218. with open(filename, "w") as f:
  219. f.write(script)
  220. return filename
  221. def update_pkgbuild(real_deps, reverse_map, extra_sources):
  222. with open("PKGBUILD", "r") as f:
  223. pkgbuild = f.read()
  224. res = re.search(
  225. "([ \t]*)# BEGIN managed sources\n((.|\n)*)([ \t]*)# END managed sources",
  226. pkgbuild,
  227. re.MULTILINE,
  228. )
  229. if res is None:
  230. raise Exception("managed sources not found")
  231. indent = res.group(1)
  232. span = res.span(2)
  233. pkgbuild = (
  234. pkgbuild[: span[0]]
  235. + "\n".join(
  236. generate_source_list(real_deps, indent, extra_sources, pkgname, reverse_map)
  237. )
  238. + "\n"
  239. + indent
  240. + pkgbuild[span[1] :]
  241. )
  242. with open("PKGBUILD", "w") as f:
  243. f.write(pkgbuild)
  244. def pyobj_to_gn_arg(k, v):
  245. if isinstance(v, Str):
  246. return f'{k} = "{v.inner}"'
  247. elif isinstance(v, str):
  248. return f'{k} = "{v}"'
  249. elif isinstance(v, bool):
  250. return f"{k} = {'true' if v else 'false'}"
  251. else:
  252. raise Exception(f"Cannot convert {k}={v} ({type(v)})to gn arg")
  253. def generate_gclient_args(args):
  254. """
  255. Writes gclient_args.gni
  256. Returns command to copy it
  257. """
  258. with open("gclient_args.gni", "w") as f:
  259. f.writelines(pyobj_to_gn_arg(k, v) + "\n" for k, v in args.items())
  260. return "cp gclient_args.gni src/build/config/gclient_args.gni"
  261. def cipd_path_substitute(cipd_path):
  262. # Assume PKGBUILD provides _go_arch variable
  263. return cipd_path.replace("${{platform}}", "linux-${_go_arch}").replace(
  264. "${{arch}}", "${_go_arch}"
  265. )
  266. def generate_cipd_cmds(cipd_deps, enabled_deps):
  267. for dep, is_optional in enabled_deps:
  268. packages = cipd_deps.get(dep)
  269. if packages is None:
  270. if is_optional:
  271. continue
  272. else:
  273. raise f"cipd dependency {dep} not found"
  274. for package in packages:
  275. yield f"cipd install {cipd_path_substitute(package['package'])} {package['version']} -root {dep}"
  276. if __name__ == "__main__":
  277. if len(sys.argv) != 4:
  278. eprint(f"Usage: {sys.argv[0]} ACTION PATH_OR_ELECTRON_VERSION PKGNAME")
  279. sys.exit(1)
  280. action = sys.argv[1]
  281. deps_path = sys.argv[2]
  282. pkgname = sys.argv[3]
  283. assert action in ("print", "update", "generate")
  284. if not os.path.exists(deps_path):
  285. # Get it from web
  286. response = requests.get(
  287. f"https://github.com/electron/electron/raw/{deps_path}/DEPS"
  288. )
  289. response.raise_for_status()
  290. deps_text = response.text
  291. with NamedTemporaryFile(mode="w", delete=True) as f:
  292. f.write(deps_text)
  293. f.flush()
  294. git_deps, gargs, cipd_deps, reverse_map = parse_deps(f.name)
  295. else:
  296. git_deps, gargs, cipd_deps, reverse_map = parse_deps(deps_path)
  297. if action == "print":
  298. for name, value in git_deps.items():
  299. print(f"git: {name} = {value}")
  300. for name, value in cipd_deps.items():
  301. print(f"cipd: {name} = {value}")
  302. elif action == "update":
  303. update_pkgbuild(git_deps, reverse_map, [])
  304. elif action == "generate":
  305. garg_cmd = generate_gclient_args(gargs)
  306. # cipd dependencies are usually binary blobs. Only add the necessary parts.
  307. cipd_cmds = generate_cipd_cmds(
  308. cipd_deps,
  309. [
  310. # (dependency path, is_optional)
  311. (
  312. "src/third_party/screen-ai/linux",
  313. True,
  314. ), # only for new electron versions (probably >= 29)
  315. # The esbuild version 0.14.13 is not compatible with the system one
  316. ("src/third_party/devtools-frontend/src/third_party/esbuild", False),
  317. ],
  318. )
  319. managed_script = generate_managed_scripts(
  320. git_deps, [garg_cmd] + list(cipd_cmds), pkgname, reverse_map
  321. )
  322. print("Done")