mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-02 20:12:07 +08:00
293 lines
9.2 KiB
Python
293 lines
9.2 KiB
Python
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import json
|
|
import lzma
|
|
import os
|
|
import shutil
|
|
import subprocess
|
|
import sys
|
|
import tempfile
|
|
import urllib.request
|
|
import urllib.parse
|
|
from pathlib import Path
|
|
|
|
|
|
REQUIRED_DIRS = [
|
|
("usr/local/lib", "/usr/local/lib"),
|
|
("usr/local/include", "/usr/local/include"),
|
|
("lib/aarch64-linux-gnu", "/lib/aarch64-linux-gnu"),
|
|
("usr/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu"),
|
|
("usr/include", "/usr/include"),
|
|
]
|
|
OPTIONAL_DIRS = [
|
|
("usr/lib/qt5/bin", "/usr/lib/qt5/bin"),
|
|
]
|
|
VENDOR_CANDIDATES = ["/system/vendor/lib64", "/vendor/lib64"]
|
|
OPTIONAL_INCLUDE_DIRS: list[str] = []
|
|
DEBUGFS_NOT_FOUND_MARKERS = (
|
|
"file not found by ext2_lookup",
|
|
"while trying to resolve filename",
|
|
"couldn't allocate",
|
|
"no such file",
|
|
"not found",
|
|
)
|
|
ANDROID_SPARSE_MAGIC = 0xED26FF3A
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
p = argparse.ArgumentParser(description="Extract TICI sysroot dirs from AGNOS system image")
|
|
p.add_argument("--manifest", default="system/hardware/tici/agnos.json", help="Path to AGNOS manifest JSON")
|
|
p.add_argument("--output-dir", required=True, help="Destination sysroot directory")
|
|
p.add_argument("--cache-dir", default=".cache/agnos", help="Cache directory for downloaded images")
|
|
p.add_argument("--url", default=None, help="Override AGNOS system image URL")
|
|
p.add_argument("--force-download", action="store_true", help="Redownload system image even if cached")
|
|
return p.parse_args()
|
|
|
|
|
|
def get_system_url(manifest_path: Path) -> str:
|
|
data = json.loads(manifest_path.read_text())
|
|
for entry in data:
|
|
if entry.get("name") != "system":
|
|
continue
|
|
if isinstance(entry.get("url"), str):
|
|
return entry["url"]
|
|
alt = entry.get("alt") if isinstance(entry, dict) else None
|
|
if isinstance(alt, dict) and isinstance(alt.get("url"), str):
|
|
return alt["url"]
|
|
raise RuntimeError(f"No system entry in manifest: {manifest_path}")
|
|
|
|
|
|
def download(url: str, dst: Path) -> None:
|
|
dst.parent.mkdir(parents=True, exist_ok=True)
|
|
tmp = dst.with_suffix(dst.suffix + ".part")
|
|
print(f"Downloading {url} -> {dst}", flush=True)
|
|
with urllib.request.urlopen(url) as src, open(tmp, "wb") as out:
|
|
shutil.copyfileobj(src, out, length=1024 * 1024)
|
|
tmp.replace(dst)
|
|
|
|
|
|
def download_and_prepare_image(url: str, cache_dir: Path, force_download: bool) -> Path:
|
|
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
compressed = cache_dir / "agnos_system.img.xz"
|
|
raw_image = cache_dir / "agnos_system.img"
|
|
ext4_image = cache_dir / "agnos_system.ext4.img"
|
|
|
|
if force_download:
|
|
compressed.unlink(missing_ok=True)
|
|
raw_image.unlink(missing_ok=True)
|
|
ext4_image.unlink(missing_ok=True)
|
|
|
|
# Prefer an already-cached raw image to avoid unnecessary multi-GB downloads.
|
|
if raw_image.exists():
|
|
return raw_image
|
|
|
|
parsed_url = urllib.parse.urlparse(url)
|
|
if parsed_url.path.endswith(".xz"):
|
|
if not compressed.exists():
|
|
download(url, compressed)
|
|
if not raw_image.exists():
|
|
print(f"Decompressing {compressed} -> {raw_image}", flush=True)
|
|
with lzma.open(compressed, "rb") as f_in, open(raw_image, "wb") as f_out:
|
|
shutil.copyfileobj(f_in, f_out, length=1024 * 1024)
|
|
else:
|
|
if not raw_image.exists():
|
|
download(url, raw_image)
|
|
|
|
return raw_image
|
|
|
|
|
|
def is_android_sparse(image_path: Path) -> bool:
|
|
with open(image_path, "rb") as f:
|
|
header = f.read(4)
|
|
if len(header) != 4:
|
|
return False
|
|
magic = int.from_bytes(header, "little")
|
|
return magic == ANDROID_SPARSE_MAGIC
|
|
|
|
|
|
def ensure_debugfs_readable_image(image_path: Path, cache_dir: Path) -> Path:
|
|
if not is_android_sparse(image_path):
|
|
return image_path
|
|
|
|
if shutil.which("simg2img") is None:
|
|
raise RuntimeError(
|
|
f"{image_path} is an Android sparse image, but simg2img is not installed."
|
|
)
|
|
|
|
converted = cache_dir / "agnos_system.ext4.img"
|
|
needs_convert = not converted.exists()
|
|
if converted.exists():
|
|
needs_convert = os.path.getmtime(converted) < os.path.getmtime(image_path)
|
|
|
|
if needs_convert:
|
|
print(f"Converting sparse image {image_path} -> {converted}", flush=True)
|
|
proc = subprocess.run(["simg2img", str(image_path), str(converted)], capture_output=True, text=True)
|
|
if proc.returncode != 0:
|
|
raise RuntimeError(f"simg2img failed:\n{proc.stdout}\n{proc.stderr}")
|
|
return converted
|
|
|
|
|
|
def _has_content(path: Path) -> bool:
|
|
if not path.exists():
|
|
return False
|
|
if path.is_file():
|
|
return path.stat().st_size > 0
|
|
return any(path.iterdir())
|
|
|
|
|
|
def _pick_extracted_root(tmp_path: Path, src_path: str) -> Path | None:
|
|
rel = Path(src_path.lstrip("/"))
|
|
candidates = [
|
|
tmp_path / rel,
|
|
tmp_path / rel.name,
|
|
tmp_path,
|
|
]
|
|
for c in candidates:
|
|
if _has_content(c):
|
|
return c
|
|
return None
|
|
|
|
|
|
def run_debugfs(image_path: Path, src_path: str, dst_path: Path) -> None:
|
|
attempts: list[str] = []
|
|
for candidate_src in (src_path, src_path.lstrip("/")):
|
|
if not candidate_src:
|
|
continue
|
|
with tempfile.TemporaryDirectory(prefix="sysroot_extract_") as tmp:
|
|
tmp_path = Path(tmp)
|
|
cmd = ["debugfs", "-R", f"rdump {candidate_src} {tmp_path}", str(image_path)]
|
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
out = f"{proc.stdout}\n{proc.stderr}".lower()
|
|
attempts.append(f"{' '.join(cmd)}\n{proc.stdout}\n{proc.stderr}")
|
|
|
|
if proc.returncode != 0:
|
|
continue
|
|
if any(marker in out for marker in DEBUGFS_NOT_FOUND_MARKERS):
|
|
continue
|
|
|
|
source_to_copy = _pick_extracted_root(tmp_path, candidate_src)
|
|
if source_to_copy is None:
|
|
continue
|
|
|
|
if dst_path.exists():
|
|
shutil.rmtree(dst_path)
|
|
dst_path.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copytree(source_to_copy, dst_path, symlinks=True, dirs_exist_ok=True)
|
|
return
|
|
|
|
joined_attempts = "\n---\n".join(attempts)
|
|
raise RuntimeError(f"debugfs could not extract '{src_path}'. Attempts:\n{joined_attempts}")
|
|
|
|
|
|
def is_non_empty_dir(path: Path) -> bool:
|
|
return path.is_dir() and any(path.iterdir())
|
|
|
|
|
|
def populate_optional_host_includes(output_dir: Path) -> None:
|
|
dst_root = output_dir / "usr/include"
|
|
if not dst_root.exists():
|
|
return
|
|
|
|
include_roots = [
|
|
Path("/usr/include/aarch64-linux-gnu"),
|
|
Path("/usr/include"),
|
|
Path("/usr/include/x86_64-linux-gnu"),
|
|
]
|
|
|
|
for include_dir in OPTIONAL_INCLUDE_DIRS:
|
|
dst = dst_root / include_dir
|
|
if dst.exists():
|
|
continue
|
|
for root in include_roots:
|
|
src = root / include_dir
|
|
if not src.is_dir():
|
|
continue
|
|
shutil.copytree(src, dst, symlinks=True)
|
|
break
|
|
|
|
openssl_dst = dst_root / "openssl"
|
|
for src_dir in (
|
|
Path("/usr/include/aarch64-linux-gnu/openssl"),
|
|
Path("/usr/include/x86_64-linux-gnu/openssl"),
|
|
):
|
|
if not src_dir.is_dir():
|
|
continue
|
|
openssl_dst.mkdir(parents=True, exist_ok=True)
|
|
shutil.copytree(src_dir, openssl_dst, symlinks=True, dirs_exist_ok=True)
|
|
break
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
if shutil.which("debugfs") is None:
|
|
raise RuntimeError("debugfs is required (install e2fsprogs)")
|
|
|
|
manifest_path = Path(args.manifest).resolve()
|
|
output_dir = Path(args.output_dir).resolve()
|
|
cache_dir = Path(args.cache_dir).resolve()
|
|
output_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
url = args.url or get_system_url(manifest_path)
|
|
image_path = download_and_prepare_image(url, cache_dir, args.force_download)
|
|
image_path = ensure_debugfs_readable_image(image_path, cache_dir)
|
|
|
|
for rel_dst, src_path in REQUIRED_DIRS:
|
|
dst = output_dir / rel_dst
|
|
print(f"Extracting {src_path} -> {dst}", flush=True)
|
|
run_debugfs(image_path, src_path, dst)
|
|
|
|
for rel_dst, src_path in OPTIONAL_DIRS:
|
|
dst = output_dir / rel_dst
|
|
try:
|
|
print(f"Extracting {src_path} -> {dst}", flush=True)
|
|
run_debugfs(image_path, src_path, dst)
|
|
except RuntimeError:
|
|
print(f"WARN: optional path not found in AGNOS image: {src_path}", flush=True)
|
|
|
|
vendor_ok = False
|
|
vendor_dst = output_dir / "system/vendor/lib64"
|
|
for src_path in VENDOR_CANDIDATES:
|
|
try:
|
|
print(f"Extracting {src_path} -> {vendor_dst}", flush=True)
|
|
run_debugfs(image_path, src_path, vendor_dst)
|
|
vendor_ok = True
|
|
break
|
|
except RuntimeError:
|
|
continue
|
|
|
|
if not vendor_ok:
|
|
print(
|
|
"WARN: vendor libs not found in AGNOS image at /system/vendor/lib64 or /vendor/lib64; "
|
|
"falling back to usr/lib/aarch64-linux-gnu",
|
|
flush=True,
|
|
)
|
|
if vendor_dst.is_symlink() or vendor_dst.is_file():
|
|
vendor_dst.unlink()
|
|
elif vendor_dst.exists():
|
|
shutil.rmtree(vendor_dst)
|
|
vendor_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
os.symlink("../../usr/lib/aarch64-linux-gnu", vendor_dst, target_is_directory=True)
|
|
|
|
populate_optional_host_includes(output_dir)
|
|
|
|
missing = []
|
|
for rel in ("usr/local/lib", "usr/local/include", "lib/aarch64-linux-gnu", "usr/lib/aarch64-linux-gnu", "usr/include", "system/vendor/lib64"):
|
|
if not is_non_empty_dir(output_dir / rel):
|
|
missing.append(rel)
|
|
if missing:
|
|
raise RuntimeError(f"Extracted sysroot is incomplete, missing content in: {', '.join(missing)}")
|
|
|
|
print(f"Sysroot ready: {output_dir}", flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
try:
|
|
raise SystemExit(main())
|
|
except KeyboardInterrupt:
|
|
raise
|
|
except Exception as exc:
|
|
print(f"ERROR: {exc}", file=sys.stderr)
|
|
raise SystemExit(1)
|