download_kernel_archives.py 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721
  1. import argparse
  2. import base64
  3. import json
  4. import os
  5. import shutil
  6. import subprocess
  7. import tempfile
  8. import threading
  9. import time
  10. import urllib.parse
  11. import urllib.request
  12. import xml.etree.ElementTree as ET
  13. from concurrent.futures import ThreadPoolExecutor
  14. from typing import Any
  15. from urllib.error import HTTPError
  16. from urllib.request import Request
  17. from urllib.request import urlopen
  18. import hashlib
  19. def parse_args() -> argparse.Namespace:
  20. parser = argparse.ArgumentParser()
  21. parser.add_argument("--android-version", required=True)
  22. parser.add_argument("--kernel-version", required=True)
  23. parser.add_argument("--os-patch-level", required=True)
  24. return parser.parse_args()
  25. def write_github_output(name: str, value: str) -> None:
  26. path = os.environ.get("GITHUB_OUTPUT")
  27. if not path:
  28. return
  29. with open(path, "a", encoding="utf-8") as f:
  30. f.write(f"{name}={value}\n")
  31. def main() -> int:
  32. args = parse_args()
  33. formatted_branch = f"{args.android_version}-{args.kernel_version}-{args.os_patch_level}"
  34. manifest_branch = f"common-{formatted_branch}"
  35. manifest_ref = f"refs/heads/{manifest_branch}"
  36. manifest_base = f"https://android.googlesource.com/kernel/manifest/+/{manifest_ref}/"
  37. manifest_origin = f"{urllib.parse.urlparse(manifest_base).scheme}://{urllib.parse.urlparse(manifest_base).netloc}"
  38. target_repo = os.environ.get("GITHUB_REPOSITORY", "")
  39. github_token = os.environ.get("GITHUB_TOKEN", "")
  40. toolchain_map = {
  41. "clang/host/linux-x86": "clang",
  42. "prebuilts/rust": "rust",
  43. "prebuilts/clang-tools": "clang-tools",
  44. "platform/prebuilts/build-tools": "build-tools",
  45. "kernel/prebuilts/build-tools": "kernel-build-tools",
  46. "platform/prebuilts/bazel/linux-x86_64": "bazel-linux-x86_64",
  47. "platform/prebuilts/jdk/jdk11": "jdk11",
  48. "toolchain/prebuilts/ndk/r23": "ndk-r23",
  49. "platform/prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.17-4.8": "gcc-glibc2.17-4.8",
  50. }
  51. release_assets_lock = threading.Lock()
  52. release_assets_cache: list[dict[str, Any]] | None = None
  53. release_info_lock = threading.Lock()
  54. release_info_cache: dict[str, Any] | None = None
  55. upload_locks_guard = threading.Lock()
  56. upload_locks: dict[tuple[str, str], threading.Lock] = {}
  57. def is_deprecated_branch(branch: str) -> bool:
  58. try:
  59. out = subprocess.check_output(
  60. ["git", "ls-remote", "https://android.googlesource.com/kernel/common", branch],
  61. text=True,
  62. stderr=subprocess.STDOUT,
  63. timeout=60,
  64. )
  65. return "deprecated" in out
  66. except Exception:
  67. return False
  68. deprecated = is_deprecated_branch(formatted_branch)
  69. write_github_output("deprecated", str(deprecated).lower())
  70. print(f"Manifest branch: {manifest_branch}")
  71. print(f"Deprecated: {deprecated}")
  72. def fetch_gitiles_file_text(file_name: str) -> str:
  73. url = f"{manifest_base}{file_name}?format=TEXT"
  74. req = Request(url, headers={"User-Agent": "actions-download-kernel"})
  75. with urlopen(req, timeout=60) as resp:
  76. data = resp.read()
  77. decoded = base64.b64decode(data)
  78. text = decoded.decode("utf-8", errors="replace")
  79. if deprecated:
  80. text = text.replace(f"\"{formatted_branch}\"", f"\"deprecated/{formatted_branch}\"")
  81. return text
  82. def load_manifest_with_includes(entry_file: str) -> tuple[ET.Element, list[tuple[str, ET.Element]]]:
  83. seen_files: set[str] = set()
  84. to_process = [entry_file]
  85. combined = ET.Element("manifest")
  86. combined_default: ET.Element | None = None
  87. combined_remotes: dict[str, ET.Element] = {}
  88. projects: list[ET.Element] = []
  89. link_copy: list[tuple[str, ET.Element]] = []
  90. while to_process:
  91. file_name = to_process.pop()
  92. if file_name in seen_files:
  93. continue
  94. seen_files.add(file_name)
  95. xml_text = fetch_gitiles_file_text(file_name)
  96. root = ET.fromstring(xml_text)
  97. for include in root.findall("include"):
  98. inc_name = include.get("name")
  99. if inc_name:
  100. to_process.append(inc_name)
  101. for remote in root.findall("remote"):
  102. remote_name = remote.get("name")
  103. if remote_name and remote_name not in combined_remotes:
  104. combined_remotes[remote_name] = remote
  105. if combined_default is None:
  106. d = root.find("default")
  107. if d is not None:
  108. combined_default = d
  109. for project in root.findall("project"):
  110. projects.append(project)
  111. path = project.get("path", project.get("name") or "")
  112. for child in list(project):
  113. if child.tag in ("linkfile", "copyfile"):
  114. link_copy.append((path, child))
  115. for r in combined_remotes.values():
  116. combined.append(r)
  117. if combined_default is not None:
  118. combined.append(combined_default)
  119. for p in projects:
  120. combined.append(p)
  121. return combined, link_copy
  122. manifest_root, link_copy = load_manifest_with_includes("default.xml")
  123. def resolve_remote_fetch(fetch: str) -> str:
  124. fetch = fetch.strip()
  125. if fetch.startswith("https://") or fetch.startswith("http://"):
  126. return fetch.rstrip("/")
  127. base = f"{manifest_origin}/"
  128. resolved = urllib.parse.urljoin(base, f"{fetch.rstrip('/')}/")
  129. return resolved.rstrip("/")
  130. remotes = {
  131. r.get("name"): resolve_remote_fetch(r.get("fetch") or "")
  132. for r in manifest_root.findall("remote")
  133. if r.get("name") and r.get("fetch") is not None
  134. }
  135. default = manifest_root.find("default")
  136. def_remote = default.get("remote") if default is not None else None
  137. def_rev = default.get("revision") if default is not None else None
  138. def get_toolchain_label(project_name: str) -> str | None:
  139. for key, value in toolchain_map.items():
  140. if key in project_name:
  141. return value
  142. return None
  143. def github_api_get_json(url: str) -> object:
  144. headers = {
  145. "Accept": "application/vnd.github.v3+json",
  146. "User-Agent": "actions-download-kernel",
  147. }
  148. if github_token:
  149. headers["Authorization"] = f"token {github_token}"
  150. req = Request(url, headers=headers)
  151. with urlopen(req, timeout=60) as resp:
  152. data = resp.read()
  153. return json.loads(data.decode("utf-8", errors="replace"))
  154. def github_api_post_json(url: str, payload: dict[str, Any]) -> object:
  155. headers = {
  156. "Accept": "application/vnd.github.v3+json",
  157. "User-Agent": "actions-download-kernel",
  158. "Content-Type": "application/json",
  159. }
  160. if github_token:
  161. headers["Authorization"] = f"token {github_token}"
  162. data = json.dumps(payload).encode("utf-8")
  163. req = Request(url, headers=headers, data=data, method="POST")
  164. with urlopen(req, timeout=60) as resp:
  165. body = resp.read()
  166. return json.loads(body.decode("utf-8", errors="replace"))
  167. def get_toolchain_release_assets() -> list[dict[str, Any]]:
  168. nonlocal release_assets_cache
  169. with release_assets_lock:
  170. if release_assets_cache is not None:
  171. return release_assets_cache
  172. if not target_repo:
  173. with release_assets_lock:
  174. release_assets_cache = []
  175. return release_assets_cache
  176. assets: list[dict[str, Any]] = []
  177. url = f"https://api.github.com/repos/{target_repo}/releases?per_page=100"
  178. try:
  179. releases = github_api_get_json(url)
  180. if isinstance(releases, list):
  181. release = next(
  182. (
  183. r
  184. for r in releases
  185. if isinstance(r, dict)
  186. and (r.get("name") == "Toolchains Mirror Cache" or r.get("tag_name") == "toolchain-cache")
  187. ),
  188. None,
  189. )
  190. if isinstance(release, dict):
  191. raw_assets = release.get("assets", [])
  192. assets = raw_assets if isinstance(raw_assets, list) else []
  193. except Exception:
  194. assets = []
  195. if not assets and github_token:
  196. release = get_or_create_toolchain_release()
  197. if isinstance(release, dict):
  198. raw_assets = release.get("assets", [])
  199. assets = raw_assets if isinstance(raw_assets, list) else []
  200. with release_assets_lock:
  201. release_assets_cache = assets
  202. return release_assets_cache
  203. def get_or_create_toolchain_release() -> dict[str, Any] | None:
  204. nonlocal release_info_cache, release_assets_cache
  205. with release_info_lock:
  206. if release_info_cache is not None:
  207. return release_info_cache
  208. if not target_repo or not github_token:
  209. release_info_cache = None
  210. return release_info_cache
  211. try:
  212. url = f"https://api.github.com/repos/{target_repo}/releases/tags/toolchain-cache"
  213. release = github_api_get_json(url)
  214. if isinstance(release, dict):
  215. release_info_cache = release
  216. assets = release.get("assets", [])
  217. with release_assets_lock:
  218. release_assets_cache = assets if isinstance(assets, list) else []
  219. return release_info_cache
  220. except Exception:
  221. pass
  222. try:
  223. create_url = f"https://api.github.com/repos/{target_repo}/releases"
  224. release = github_api_post_json(
  225. create_url,
  226. {
  227. "tag_name": "toolchain-cache",
  228. "name": "Toolchains Mirror Cache",
  229. "draft": False,
  230. "prerelease": False,
  231. },
  232. )
  233. if isinstance(release, dict):
  234. release_info_cache = release
  235. assets = release.get("assets", [])
  236. with release_assets_lock:
  237. release_assets_cache = assets if isinstance(assets, list) else []
  238. return release_info_cache
  239. except Exception:
  240. try:
  241. url = f"https://api.github.com/repos/{target_repo}/releases/tags/toolchain-cache"
  242. release = github_api_get_json(url)
  243. if isinstance(release, dict):
  244. release_info_cache = release
  245. assets = release.get("assets", [])
  246. with release_assets_lock:
  247. release_assets_cache = assets if isinstance(assets, list) else []
  248. return release_info_cache
  249. except Exception:
  250. release_info_cache = None
  251. return release_info_cache
  252. release_info_cache = None
  253. return release_info_cache
  254. def download_github_release_asset(api_url: str, out_path: str) -> None:
  255. headers = {
  256. "Accept": "application/octet-stream",
  257. "User-Agent": "actions-download-kernel",
  258. }
  259. if github_token:
  260. headers["Authorization"] = f"token {github_token}"
  261. req = Request(api_url, headers=headers)
  262. with urlopen(req, timeout=120) as resp, open(out_path, "wb") as f:
  263. shutil.copyfileobj(resp, f, length=1024 * 1024)
  264. def run_curl_download(url: str, out_path: str) -> None:
  265. subprocess.run(
  266. ["curl", "-LfsS", "--retry", "5", "--connect-timeout", "30", "-o", out_path, url],
  267. check=True,
  268. text=True,
  269. )
  270. def extract_tar_gz(archive_path: str, dest_dir: str, strip_components: int) -> None:
  271. cmd = ["tar", "-xzf", archive_path, "-C", dest_dir]
  272. if strip_components > 0:
  273. cmd.extend(["--strip-components", str(strip_components)])
  274. subprocess.run(cmd, check=True, text=True)
  275. def upload_release_asset(upload_url_template: str, file_path: str, asset_name: str) -> None:
  276. upload_url = upload_url_template.split("{", 1)[0]
  277. url = f"{upload_url}?name={urllib.parse.quote(asset_name, safe='')}"
  278. tmp_fd, tmp_resp = tempfile.mkstemp(prefix="gh-release-upload-", suffix=".json")
  279. os.close(tmp_fd)
  280. cmd = [
  281. "curl",
  282. "-LsS",
  283. "-X",
  284. "POST",
  285. "-H",
  286. "Accept: application/vnd.github.v3+json",
  287. "-H",
  288. "Content-Type: application/octet-stream",
  289. "-H",
  290. "User-Agent: actions-download-kernel",
  291. "-o",
  292. tmp_resp,
  293. "-w",
  294. "%{http_code}",
  295. ]
  296. if github_token:
  297. cmd.extend(["-H", f"Authorization: token {github_token}"])
  298. cmd.extend(["--data-binary", f"@{file_path}", url])
  299. try:
  300. res = subprocess.run(cmd, check=True, text=True, capture_output=True)
  301. code = (res.stdout or "").strip()
  302. if code in {"200", "201"}:
  303. return
  304. if code == "422":
  305. return
  306. body = ""
  307. try:
  308. with open(tmp_resp, "r", encoding="utf-8", errors="replace") as f:
  309. body = f.read()
  310. except Exception:
  311. body = ""
  312. raise RuntimeError(f"Release asset upload failed (HTTP {code}): {body}")
  313. finally:
  314. if os.path.exists(tmp_resp):
  315. os.remove(tmp_resp)
  316. def build_release_asset_index() -> dict[str, dict[str, Any]]:
  317. assets = get_toolchain_release_assets()
  318. index: dict[str, dict[str, Any]] = {}
  319. for a in assets:
  320. if isinstance(a, dict) and isinstance(a.get("name"), str):
  321. index[a["name"]] = a
  322. return index
  323. def sha256_file(path: str) -> str:
  324. h = hashlib.sha256()
  325. with open(path, "rb") as f:
  326. for chunk in iter(lambda: f.read(1024 * 1024), b""):
  327. h.update(chunk)
  328. return h.hexdigest()
  329. def ensure_toolchain_cached(label: str, rev: str, src_dir: str) -> None:
  330. nonlocal release_assets_cache
  331. if not github_token or not target_repo:
  332. return
  333. cache_key = (label, rev)
  334. with upload_locks_guard:
  335. lock = upload_locks.get(cache_key)
  336. if lock is None:
  337. lock = threading.Lock()
  338. upload_locks[cache_key] = lock
  339. with lock:
  340. try:
  341. base_filename = f"{label}-{rev}.tar.gz"
  342. meta_name = f"{label}-{rev}.cache.json"
  343. asset_index = build_release_asset_index()
  344. if meta_name in asset_index:
  345. return
  346. release = get_or_create_toolchain_release()
  347. if not release:
  348. return
  349. upload_url_template = release.get("upload_url")
  350. if not isinstance(upload_url_template, str) or not upload_url_template:
  351. return
  352. tmp_dir = tempfile.mkdtemp(prefix="toolchain-cache-")
  353. try:
  354. archive_path = os.path.join(tmp_dir, base_filename)
  355. subprocess.run(
  356. ["tar", "-I", "gzip -1", "-cf", archive_path, "-C", src_dir, "."],
  357. check=True,
  358. text=True,
  359. )
  360. size = os.path.getsize(archive_path)
  361. max_part = 1900 * 1024 * 1024
  362. meta: dict[str, Any] = {
  363. "format": 1,
  364. "label": label,
  365. "rev": rev,
  366. "archive": base_filename,
  367. "parts": [],
  368. }
  369. if size <= max_part:
  370. upload_release_asset(upload_url_template, archive_path, base_filename)
  371. meta["parts"].append(
  372. {
  373. "name": base_filename,
  374. "size": size,
  375. "sha256": sha256_file(archive_path),
  376. }
  377. )
  378. else:
  379. subprocess.run(
  380. ["split", "-b", str(max_part), "-d", "-a", "2", archive_path, f"{archive_path}.part"],
  381. check=True,
  382. text=True,
  383. )
  384. for name in sorted(os.listdir(tmp_dir)):
  385. if name.startswith(f"{base_filename}.part"):
  386. part_path = os.path.join(tmp_dir, name)
  387. upload_release_asset(upload_url_template, part_path, name)
  388. meta["parts"].append(
  389. {
  390. "name": name,
  391. "size": os.path.getsize(part_path),
  392. "sha256": sha256_file(part_path),
  393. }
  394. )
  395. meta_path = os.path.join(tmp_dir, meta_name)
  396. with open(meta_path, "w", encoding="utf-8") as f:
  397. json.dump(meta, f, separators=(",", ":"))
  398. upload_release_asset(upload_url_template, meta_path, meta_name)
  399. with release_assets_lock:
  400. release_assets_cache = None
  401. finally:
  402. shutil.rmtree(tmp_dir, ignore_errors=True)
  403. except Exception:
  404. return
  405. def try_extract_toolchain_cache(project_name: str, rev: str, dest_dir: str, strip_components: int) -> bool:
  406. label = get_toolchain_label(project_name)
  407. if not label:
  408. return False
  409. asset_index = build_release_asset_index()
  410. base_filename = f"{label}-{rev}.tar.gz"
  411. meta_name = f"{label}-{rev}.cache.json"
  412. matching: list[tuple[str, str]] = []
  413. if meta_name in asset_index and isinstance(asset_index[meta_name].get("url"), str):
  414. tmp_fd, tmp_meta = tempfile.mkstemp(prefix="toolchain-cache-", suffix=".json")
  415. os.close(tmp_fd)
  416. try:
  417. download_github_release_asset(asset_index[meta_name]["url"], tmp_meta)
  418. with open(tmp_meta, "r", encoding="utf-8", errors="replace") as f:
  419. meta = json.load(f)
  420. parts = meta.get("parts", [])
  421. if not isinstance(parts, list) or not parts:
  422. return False
  423. for p in parts:
  424. if not isinstance(p, dict):
  425. continue
  426. name = p.get("name")
  427. if isinstance(name, str) and name in asset_index and isinstance(asset_index[name].get("url"), str):
  428. matching.append((name, asset_index[name]["url"]))
  429. if not matching:
  430. return False
  431. finally:
  432. if os.path.exists(tmp_meta):
  433. os.remove(tmp_meta)
  434. else:
  435. for asset_name, a in asset_index.items():
  436. asset_url = a.get("url")
  437. if isinstance(asset_url, str) and asset_name.startswith(base_filename):
  438. matching.append((asset_name, asset_url))
  439. if not matching:
  440. return False
  441. matching.sort(key=lambda x: x[0])
  442. part_paths: list[str] = []
  443. try:
  444. for asset_name, asset_url in matching:
  445. part_path = os.path.abspath(asset_name)
  446. download_github_release_asset(asset_url, part_path)
  447. part_paths.append(part_path)
  448. if len(part_paths) == 1 and part_paths[0].endswith(".tar.gz"):
  449. extract_tar_gz(part_paths[0], dest_dir, strip_components)
  450. else:
  451. tmp_fd, tmp_path = tempfile.mkstemp(prefix="toolchain-", suffix=".tar.gz")
  452. os.close(tmp_fd)
  453. try:
  454. with open(tmp_path, "wb") as out_f:
  455. for p in part_paths:
  456. with open(p, "rb") as in_f:
  457. shutil.copyfileobj(in_f, out_f, length=1024 * 1024)
  458. extract_tar_gz(tmp_path, dest_dir, strip_components)
  459. finally:
  460. if os.path.exists(tmp_path):
  461. os.remove(tmp_path)
  462. return True
  463. except Exception:
  464. return False
  465. finally:
  466. for p in part_paths:
  467. if os.path.exists(p):
  468. os.remove(p)
  469. def url_quote_rev(rev: str) -> str:
  470. return urllib.parse.quote(rev, safe="")
  471. def googlesource_archive_urls(repo_url: str, rev: str) -> list[str]:
  472. qrev = url_quote_rev(rev)
  473. if rev.startswith("refs/"):
  474. return [f"{repo_url}/+archive/{qrev}.tar.gz"]
  475. return [
  476. f"{repo_url}/+archive/{url_quote_rev(f'refs/heads/{rev}')}.tar.gz",
  477. f"{repo_url}/+archive/{url_quote_rev(f'refs/tags/{rev}')}.tar.gz",
  478. f"{repo_url}/+archive/{qrev}.tar.gz",
  479. ]
  480. def github_archive_urls(repo_url: str, rev: str) -> list[str]:
  481. if rev.startswith("refs/"):
  482. return [f"{repo_url}/archive/{rev}.tar.gz"]
  483. return [
  484. f"{repo_url}/archive/refs/heads/{rev}.tar.gz",
  485. f"{repo_url}/archive/refs/tags/{rev}.tar.gz",
  486. f"{repo_url}/archive/{rev}.tar.gz",
  487. ]
  488. def gitlab_archive_urls(repo_url: str, rev: str, repo_basename: str) -> list[str]:
  489. qrev = urllib.parse.quote(rev, safe="")
  490. return [f"{repo_url}/-/archive/{qrev}/{repo_basename}-{qrev}.tar.gz"]
  491. def build_project_urls(base_url: str, name: str, rev: str) -> tuple[list[str], int]:
  492. repo_url = f"{base_url}/{name}"
  493. if "github.com" in base_url:
  494. return github_archive_urls(repo_url, rev), 1
  495. if "googlesource.com" in base_url or "android.googlesource.com" in base_url:
  496. return googlesource_archive_urls(repo_url, rev), 0
  497. if "git.codelinaro.org" in base_url or "gitlab" in base_url:
  498. repo_basename = name.rstrip("/").split("/")[-1]
  499. return gitlab_archive_urls(repo_url, rev, repo_basename), 1
  500. return [], 0
  501. def sync_project(task: tuple[str, str, str, str]) -> dict[str, Any]:
  502. name, path, rev, base_url = task
  503. start_time = time.time()
  504. dest_dir = os.path.abspath(path if path not in ("./", ".") else ".")
  505. os.makedirs(dest_dir, exist_ok=True)
  506. try:
  507. urls, strip_components = build_project_urls(base_url, name, rev)
  508. if not urls:
  509. return {
  510. "ok": False,
  511. "name": name,
  512. "path": path,
  513. "rev": rev,
  514. "base_url": base_url,
  515. "error": "no supported remote",
  516. "urls": [],
  517. }
  518. if try_extract_toolchain_cache(name, rev, dest_dir, strip_components):
  519. duration = time.time() - start_time
  520. print(f"Synced {name} -> {path} ({duration:.2f}s)")
  521. return {"ok": True}
  522. tmp_fd, tmp_path = tempfile.mkstemp(prefix="kernel-src-", suffix=".tar.gz")
  523. os.close(tmp_fd)
  524. try:
  525. last_err: str | None = None
  526. for url in urls:
  527. try:
  528. run_curl_download(url, tmp_path)
  529. extract_tar_gz(tmp_path, dest_dir, strip_components)
  530. label = get_toolchain_label(name)
  531. if label:
  532. ensure_toolchain_cached(label, rev, dest_dir)
  533. duration = time.time() - start_time
  534. print(f"Synced {name} -> {path} ({duration:.2f}s)")
  535. return {"ok": True}
  536. except subprocess.CalledProcessError as e:
  537. last_err = str(e)
  538. continue
  539. return {
  540. "ok": False,
  541. "name": name,
  542. "path": path,
  543. "rev": rev,
  544. "base_url": base_url,
  545. "error": last_err or "download/extract failed",
  546. "urls": urls,
  547. }
  548. finally:
  549. if os.path.exists(tmp_path):
  550. os.remove(tmp_path)
  551. except Exception as e:
  552. return {
  553. "ok": False,
  554. "name": name,
  555. "path": path,
  556. "rev": rev,
  557. "base_url": base_url,
  558. "error": f"unexpected exception: {e}",
  559. "urls": [],
  560. }
  561. def ensure_build_tools_python(kernel_root: str) -> None:
  562. build_tools_dir = os.path.join(kernel_root, "prebuilts", "build-tools")
  563. expected = os.path.join(build_tools_dir, "path", "linux-x86", "python3")
  564. if os.path.exists(expected):
  565. return
  566. os.makedirs(os.path.dirname(expected), exist_ok=True)
  567. candidate: str | None = None
  568. for root_dir, _, files in os.walk(build_tools_dir):
  569. if "python3" in files:
  570. candidate = os.path.join(root_dir, "python3")
  571. break
  572. if candidate is None:
  573. candidate = shutil.which("python3") or "/usr/bin/python3"
  574. try:
  575. if os.path.lexists(expected):
  576. os.remove(expected)
  577. rel_target = os.path.relpath(candidate, os.path.dirname(expected))
  578. os.symlink(rel_target, expected)
  579. except Exception:
  580. return
  581. sync_tasks: list[tuple[str, str, str, str]] = []
  582. for project in manifest_root.findall("project"):
  583. name = project.get("name")
  584. if not name:
  585. continue
  586. path = project.get("path", name)
  587. remote_name = project.get("remote", def_remote)
  588. if not remote_name:
  589. continue
  590. base_url = remotes.get(remote_name)
  591. if not base_url:
  592. continue
  593. rev = project.get("revision", def_rev)
  594. if not rev:
  595. continue
  596. sync_tasks.append((name, path, rev, base_url))
  597. max_workers = min(32, (os.cpu_count() or 2) * 2)
  598. with ThreadPoolExecutor(max_workers=max_workers) as executor:
  599. results: list[dict[str, Any]] = list(executor.map(sync_project, sync_tasks))
  600. failed = [r for r in results if not r.get("ok")]
  601. if failed:
  602. for r in failed[:25]:
  603. print(
  604. f"[FAIL] {r.get('name')} -> {r.get('path')} rev={r.get('rev')} remote={r.get('base_url')} err={r.get('error')}"
  605. )
  606. for u in (r.get("urls") or [])[:5]:
  607. print(f" url={u}")
  608. return 1
  609. ensure_build_tools_python(os.getcwd())
  610. for project_path, child in link_copy:
  611. src_rel = child.get("src")
  612. dest_rel = child.get("dest")
  613. if not src_rel or not dest_rel:
  614. continue
  615. src_path = os.path.join(os.getcwd(), project_path, src_rel)
  616. dest_path = os.path.join(os.getcwd(), dest_rel)
  617. os.makedirs(os.path.dirname(dest_path), exist_ok=True)
  618. if child.tag == "linkfile":
  619. if os.path.lexists(dest_path):
  620. os.remove(dest_path)
  621. rel_target = os.path.relpath(src_path, os.path.dirname(dest_path))
  622. os.symlink(rel_target, dest_path)
  623. elif child.tag == "copyfile":
  624. shutil.copy2(src_path, dest_path)
  625. return 0
  626. if __name__ == "__main__":
  627. raise SystemExit(main())