初始化奇妙小屏幕控制器项目
This commit is contained in:
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
from datetime import datetime, timezone
|
||||
import gzip
|
||||
import hashlib
|
||||
import json
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import sys
|
||||
import tarfile
|
||||
|
||||
SOURCE_ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(SOURCE_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(SOURCE_ROOT))
|
||||
|
||||
from scripts.fat16_image import Fat16Image
|
||||
from scripts.image_config import PRODUCT_ID, create_config_file, read_config_file, validate_config
|
||||
from scripts.debian_closure import IMAGE_DEBIAN_ROOTS, verify_local_closure
|
||||
from scripts.kernel_artifact import KernelArtifactInfo, verify_kernel_dependency, verify_source_dependency
|
||||
from scripts.boot_space import DEFAULT_SAFETY_BYTES, calculate_budget, candidate_sizes_from_dependency
|
||||
|
||||
|
||||
EXCLUDED_PARTS = {".venv", "data", "__pycache__", ".pytest_cache", "node_modules"}
|
||||
NATIVE_BUILD_OUTPUTS = {
|
||||
"app/display/native/libh618_hub75.so",
|
||||
"app/display/native/hub75_benchmark",
|
||||
"app/display/native/hub75_native_test",
|
||||
"app/display/native/hub75_safeoff",
|
||||
}
|
||||
IMAGE_FORMAT_VERSION = 3
|
||||
BUNDLE_IMAGE_PATH = "/opt/matrix-image-bootstrap/app/MSCBOOT.TGZ"
|
||||
|
||||
|
||||
def _allowed_source(path: Path, root: Path) -> bool:
|
||||
relative = path.relative_to(root)
|
||||
if any(part in EXCLUDED_PARTS for part in relative.parts):
|
||||
return False
|
||||
if relative.as_posix() in NATIVE_BUILD_OUTPUTS:
|
||||
return False
|
||||
return path.suffix not in {".pyc", ".pyo"}
|
||||
|
||||
|
||||
def _add_file(tar: tarfile.TarFile, path: Path, name: str) -> None:
|
||||
info = tar.gettarinfo(str(path), arcname=name)
|
||||
info.uid = info.gid = 0
|
||||
info.uname = info.gname = "root"
|
||||
info.mtime = 0
|
||||
info.mode = 0o755 if path.suffix == ".sh" else 0o644
|
||||
with path.open("rb") as handle:
|
||||
tar.addfile(info, handle)
|
||||
|
||||
|
||||
def build_bundle(
|
||||
source: Path,
|
||||
wheelhouse: Path,
|
||||
debian: Path,
|
||||
output: Path,
|
||||
system_dependencies: Path | None = None,
|
||||
) -> None:
|
||||
with output.open("wb") as raw:
|
||||
with gzip.GzipFile(filename="", mode="wb", fileobj=raw, mtime=0) as compressed:
|
||||
with tarfile.open(fileobj=compressed, mode="w", format=tarfile.PAX_FORMAT) as tar:
|
||||
for path in sorted(source.rglob("*"), key=lambda item: item.relative_to(source).as_posix()):
|
||||
if path.is_file() and _allowed_source(path, source):
|
||||
_add_file(tar, path, f"project/核桃派软件源代码/{path.relative_to(source).as_posix()}")
|
||||
for root, archive_root in (
|
||||
(wheelhouse, "project/离线依赖/其他依赖/aarch64-py311"),
|
||||
(debian, "project/离线依赖/其他依赖/debian12-aarch64"),
|
||||
):
|
||||
for path in sorted(root.rglob("*"), key=lambda item: item.relative_to(root).as_posix()):
|
||||
if path.is_file():
|
||||
_add_file(tar, path, f"{archive_root}/{path.relative_to(root).as_posix()}")
|
||||
if system_dependencies is not None:
|
||||
for path in sorted(system_dependencies.rglob("*"), key=lambda item: item.relative_to(system_dependencies).as_posix()):
|
||||
if path.is_file():
|
||||
_add_file(
|
||||
tar,
|
||||
path,
|
||||
f"project/离线依赖/其他依赖/frp/0.71.0/linux-arm64/{path.relative_to(system_dependencies).as_posix()}",
|
||||
)
|
||||
|
||||
|
||||
def sha256_file(path: Path) -> str:
|
||||
digest = hashlib.sha256()
|
||||
with path.open("rb") as handle:
|
||||
for chunk in iter(lambda: handle.read(4 * 1024 * 1024), b""):
|
||||
digest.update(chunk)
|
||||
return digest.hexdigest()
|
||||
|
||||
|
||||
def image_metadata(
|
||||
version: str,
|
||||
bundle_bytes: int,
|
||||
bundle_sha256: str,
|
||||
kernel: KernelArtifactInfo,
|
||||
boot_required_bytes: int,
|
||||
boot_safety_bytes: int = DEFAULT_SAFETY_BYTES,
|
||||
) -> dict:
|
||||
return {
|
||||
"format_version": IMAGE_FORMAT_VERSION,
|
||||
"product": PRODUCT_ID,
|
||||
"software_version": version,
|
||||
"bundle_path": BUNDLE_IMAGE_PATH,
|
||||
"bundle_bytes": bundle_bytes,
|
||||
"bundle_sha256": bundle_sha256,
|
||||
"kernel_release": kernel.kernel_release,
|
||||
"kernel_artifact": kernel.artifact,
|
||||
"kernel_artifact_bytes": kernel.artifact_bytes,
|
||||
"kernel_artifact_sha256": kernel.artifact_sha256,
|
||||
"kernel_unpacked_file_bytes": kernel.unpacked_file_bytes,
|
||||
"boot_required_bytes": boot_required_bytes,
|
||||
"boot_safety_bytes": boot_safety_bytes,
|
||||
}
|
||||
|
||||
|
||||
def build_image(
|
||||
base_image: Path,
|
||||
source: Path,
|
||||
wheelhouse: Path,
|
||||
debian: Path,
|
||||
kernel: Path,
|
||||
kernel_source: Path,
|
||||
config: dict | bytes,
|
||||
output: Path,
|
||||
bootstrap_bundle: Path,
|
||||
system_dependencies: Path | None = None,
|
||||
) -> dict:
|
||||
base_image = Path(base_image).resolve()
|
||||
source = Path(source).resolve()
|
||||
wheelhouse = Path(wheelhouse).resolve()
|
||||
debian = Path(debian).resolve()
|
||||
kernel = Path(kernel).resolve()
|
||||
kernel_source = Path(kernel_source).resolve()
|
||||
output = Path(output).resolve()
|
||||
bootstrap_bundle = Path(bootstrap_bundle).resolve()
|
||||
system_dependencies = Path(system_dependencies).resolve() if system_dependencies is not None else None
|
||||
if system_dependencies is None:
|
||||
raise ValueError("the verified frpc system dependency bundle is required")
|
||||
if isinstance(config, bytes):
|
||||
config_bytes = config
|
||||
config, _, _ = read_config_file(config_bytes)
|
||||
config = validate_config(config)
|
||||
else:
|
||||
config = validate_config(config)
|
||||
config_bytes = create_config_file(config)
|
||||
version = config["software_version"]
|
||||
if (source / "VERSION").read_text(encoding="utf-8").strip() != version:
|
||||
raise ValueError("image configuration version does not match source VERSION")
|
||||
for manifest in (
|
||||
wheelhouse / "SHA256SUMS",
|
||||
debian / "SHA256SUMS",
|
||||
debian / "BASE_IMAGE_PACKAGES.tsv",
|
||||
system_dependencies / "SHA256SUMS",
|
||||
):
|
||||
if not manifest.is_file():
|
||||
raise FileNotFoundError(manifest)
|
||||
verify_local_closure(debian, debian / "BASE_IMAGE_PACKAGES.tsv", list(IMAGE_DEBIAN_ROOTS))
|
||||
kernel_info = verify_kernel_dependency(kernel)
|
||||
verify_source_dependency(kernel_source)
|
||||
if output.exists() or bootstrap_bundle.exists():
|
||||
raise FileExistsError(output if output.exists() else bootstrap_bundle)
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
bootstrap_bundle.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
try:
|
||||
build_bundle(source, wheelhouse, debian, bootstrap_bundle, system_dependencies)
|
||||
bundle_digest = sha256_file(bootstrap_bundle)
|
||||
shutil.copyfile(base_image, output)
|
||||
except BaseException:
|
||||
output.unlink(missing_ok=True)
|
||||
bootstrap_bundle.unlink(missing_ok=True)
|
||||
raise
|
||||
try:
|
||||
firstboot = (source / "scripts" / "image_firstboot.sh").read_bytes()
|
||||
with Fat16Image(output, writable=True) as image:
|
||||
image.write_file("MSCCFG.BIN", config_bytes, contiguous=True)
|
||||
image.write_file("MSCINIT", firstboot)
|
||||
with Fat16Image(output) as image:
|
||||
boot_budget = calculate_budget(
|
||||
candidate_sizes=candidate_sizes_from_dependency(kernel),
|
||||
boot_cmd=image.read_file("BOOT.CMD"),
|
||||
boot_scr=image.read_file("BOOT.SCR"),
|
||||
cluster_bytes=image.cluster_bytes,
|
||||
available_bytes=image.free_bytes(),
|
||||
)
|
||||
if boot_budget.available_bytes < boot_budget.total_bytes:
|
||||
raise RuntimeError(
|
||||
"FAT boot partition lacks safe candidate-kernel space: "
|
||||
f"available={boot_budget.available_bytes}, required={boot_budget.required_bytes}, "
|
||||
f"safety={boot_budget.safety_bytes}, total={boot_budget.total_bytes}"
|
||||
)
|
||||
meta = image_metadata(
|
||||
version,
|
||||
bootstrap_bundle.stat().st_size,
|
||||
bundle_digest,
|
||||
kernel_info,
|
||||
boot_budget.required_bytes,
|
||||
boot_budget.safety_bytes,
|
||||
)
|
||||
rendered_meta = (
|
||||
json.dumps(meta, ensure_ascii=False, sort_keys=True, indent=2) + "\n"
|
||||
).encode("utf-8")
|
||||
with Fat16Image(output, writable=True) as image:
|
||||
image.write_file("MSCMETA.JSN", rendered_meta)
|
||||
with Fat16Image(output) as image:
|
||||
if image.read_file("MSCCFG.BIN") != config_bytes:
|
||||
raise RuntimeError("image configuration read-back failed")
|
||||
if image.find("MSCBOOT.TGZ") is not None:
|
||||
raise RuntimeError("FAT boot partition unexpectedly contains the application bundle")
|
||||
if image.read_file("MSCMETA.JSN") != rendered_meta:
|
||||
raise RuntimeError("image metadata read-back failed")
|
||||
if image.read_file("MSCINIT") != firstboot:
|
||||
raise RuntimeError("image first-boot program read-back failed")
|
||||
final_budget = calculate_budget(
|
||||
candidate_sizes=candidate_sizes_from_dependency(kernel),
|
||||
boot_cmd=image.read_file("BOOT.CMD"),
|
||||
boot_scr=image.read_file("BOOT.SCR"),
|
||||
cluster_bytes=image.cluster_bytes,
|
||||
available_bytes=image.free_bytes(),
|
||||
)
|
||||
if final_budget.available_bytes < final_budget.total_bytes:
|
||||
raise RuntimeError("final FAT boot partition no longer satisfies the recorded space budget")
|
||||
except BaseException:
|
||||
output.unlink(missing_ok=True)
|
||||
bootstrap_bundle.unlink(missing_ok=True)
|
||||
raise
|
||||
return {
|
||||
**meta,
|
||||
"created_at": datetime.now(timezone.utc).astimezone().isoformat(timespec="seconds"),
|
||||
"image_bytes": output.stat().st_size,
|
||||
"image_sha256": sha256_file(output),
|
||||
}
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser(description="Build a Rufus-writable WalnutPi controller image")
|
||||
parser.add_argument("--base-image", type=Path, required=True)
|
||||
parser.add_argument("--source", type=Path, required=True)
|
||||
parser.add_argument("--wheelhouse", type=Path, required=True)
|
||||
parser.add_argument("--debian", type=Path, required=True)
|
||||
parser.add_argument("--kernel", type=Path, required=True)
|
||||
parser.add_argument("--kernel-source", type=Path, required=True)
|
||||
parser.add_argument("--config", type=Path, required=True)
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
parser.add_argument("--bootstrap-bundle-output", type=Path, required=True)
|
||||
parser.add_argument("--frpc-bundle", type=Path, required=True)
|
||||
args = parser.parse_args()
|
||||
config = json.loads(args.config.read_text(encoding="utf-8"))
|
||||
info = build_image(
|
||||
args.base_image,
|
||||
args.source,
|
||||
args.wheelhouse,
|
||||
args.debian,
|
||||
args.kernel,
|
||||
args.kernel_source,
|
||||
config,
|
||||
args.output,
|
||||
args.bootstrap_bundle_output,
|
||||
args.frpc_bundle,
|
||||
)
|
||||
print(json.dumps(info, ensure_ascii=False, indent=2))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Reference in New Issue
Block a user