mirror of
https://gitlvb.teallvbs.xyz/IQ.Lvbs/IQ.Pilot.git
synced 2026-08-06 16:55:39 +08:00
IQ.Pilot Release Commit @ 4fcea4d
This commit is contained in:
@@ -0,0 +1,317 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
|
||||
_COLOR = sys.stdout.isatty() and os.environ.get("NO_COLOR") is None
|
||||
|
||||
|
||||
def _paint(text: str, code: str) -> str:
|
||||
return f"\033[{code}m{text}\033[0m" if _COLOR else text
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
ASSET_DIR = ROOT / "artifacts" / "runtime" / "boot_branding"
|
||||
BG_ASSET = ASSET_DIR / "bg.jpg"
|
||||
SPLASH_BMP_ASSET = ASSET_DIR / "splash_embedded.bmp"
|
||||
|
||||
BG_TARGET = Path("/usr/comma/bg.jpg")
|
||||
SPLASH_TARGET = Path("/dev/block/bootdevice/by-name/splash")
|
||||
SPLASH_BMP_OFFSET = 16384
|
||||
|
||||
PASSWD_PATH = Path("/etc/passwd")
|
||||
SHADOW_PATH = Path("/etc/shadow")
|
||||
IQ_USER = "iq"
|
||||
IQ_GECOS = "IQ.Pilot"
|
||||
SOURCE_USER = "comma"
|
||||
|
||||
STATE_DIR = Path("/data/iqpilot_boot_branding")
|
||||
BACKUP_DIR = STATE_DIR / "backups"
|
||||
META_PATH = BACKUP_DIR / "metadata.json"
|
||||
BG_BACKUP = BACKUP_DIR / "bg.jpg.orig"
|
||||
SPLASH_BACKUP = BACKUP_DIR / "splash.partition.orig.img"
|
||||
|
||||
|
||||
def sha256_bytes(data: bytes) -> str:
|
||||
return hashlib.sha256(data).hexdigest()
|
||||
|
||||
|
||||
def sha256_path(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
for chunk in iter(lambda: f.read(1024 * 1024), b""):
|
||||
h.update(chunk)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def run(cmd: list[str], capture_output: bool = False) -> str:
|
||||
res = subprocess.run(cmd, check=True, text=True, capture_output=capture_output)
|
||||
return res.stdout.strip() if capture_output else ""
|
||||
|
||||
|
||||
def get_root_mount_options() -> str:
|
||||
return run(["findmnt", "-no", "OPTIONS", "/"], capture_output=True)
|
||||
|
||||
|
||||
def remount_root(mode: str) -> None:
|
||||
run(["mount", "-o", mode, "/"])
|
||||
|
||||
|
||||
def splash_size() -> int:
|
||||
return int(run(["blockdev", "--getsize64", str(SPLASH_TARGET)], capture_output=True))
|
||||
|
||||
|
||||
def read_splash_bytes(offset: int, size: int) -> bytes:
|
||||
with SPLASH_TARGET.open("rb") as f:
|
||||
f.seek(offset)
|
||||
return f.read(size)
|
||||
|
||||
|
||||
def write_splash_bytes(offset: int, data: bytes) -> None:
|
||||
with SPLASH_TARGET.open("r+b", buffering=0) as f:
|
||||
f.seek(offset)
|
||||
f.write(data)
|
||||
f.flush()
|
||||
os.fsync(f.fileno())
|
||||
|
||||
|
||||
def backup_if_missing() -> None:
|
||||
BACKUP_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if not BG_BACKUP.exists():
|
||||
shutil.copy2(BG_TARGET, BG_BACKUP)
|
||||
|
||||
if not SPLASH_BACKUP.exists():
|
||||
with SPLASH_TARGET.open("rb") as src, SPLASH_BACKUP.open("wb") as dst:
|
||||
remaining = splash_size()
|
||||
while remaining > 0:
|
||||
chunk = src.read(min(1024 * 1024, remaining))
|
||||
if not chunk:
|
||||
break
|
||||
dst.write(chunk)
|
||||
remaining -= len(chunk)
|
||||
|
||||
if not META_PATH.exists():
|
||||
splash_backup_hash = sha256_path(SPLASH_BACKUP)
|
||||
meta = {
|
||||
"bg_backup_sha256": sha256_path(BG_BACKUP),
|
||||
"splash_backup_sha256": splash_backup_hash,
|
||||
"bg_asset_sha256": sha256_path(BG_ASSET) if BG_ASSET.exists() else None,
|
||||
"splash_asset_sha256": sha256_path(SPLASH_BMP_ASSET) if SPLASH_BMP_ASSET.exists() else None,
|
||||
"splash_bmp_offset": SPLASH_BMP_OFFSET,
|
||||
}
|
||||
META_PATH.write_text(json.dumps(meta, indent=2) + "\n", encoding="utf-8")
|
||||
|
||||
|
||||
def _line_for_user(text: str, user: str) -> str | None:
|
||||
prefix = f"{user}:"
|
||||
for line in text.splitlines():
|
||||
if line.startswith(prefix):
|
||||
return line
|
||||
return None
|
||||
|
||||
|
||||
def _expected_passwd_line(source_passwd_line: str) -> str:
|
||||
"""Build the iq passwd line by cloning comma's UID/GID/home/shell."""
|
||||
fields = source_passwd_line.split(":")
|
||||
if len(fields) < 7:
|
||||
raise ValueError(f"malformed passwd line for {SOURCE_USER!r}: {source_passwd_line!r}")
|
||||
_name, passwd_x, uid, gid, _gecos, home, shell = fields[:7]
|
||||
return f"{IQ_USER}:{passwd_x}:{uid}:{gid}:{IQ_GECOS}:{home}:{shell}"
|
||||
|
||||
|
||||
def _expected_shadow_line(days_since_epoch: int) -> str:
|
||||
return f"{IQ_USER}:!:{days_since_epoch}:0:99999:7:::"
|
||||
|
||||
|
||||
def _replace_or_append(text: str, user: str, new_line: str) -> str:
|
||||
prefix = f"{user}:"
|
||||
out: list[str] = []
|
||||
replaced = False
|
||||
for line in text.splitlines():
|
||||
if line.startswith(prefix):
|
||||
if not replaced:
|
||||
out.append(new_line)
|
||||
replaced = True
|
||||
continue
|
||||
out.append(line)
|
||||
if not replaced:
|
||||
out.append(new_line)
|
||||
return "\n".join(out) + "\n"
|
||||
|
||||
|
||||
def ensure_iq_user(
|
||||
passwd_path: Path | None = None,
|
||||
shadow_path: Path | None = None,
|
||||
remount: Callable[[str], None] | None = None,
|
||||
root_opts: str | None = None,
|
||||
now: Callable[[], float] | None = None,
|
||||
) -> bool:
|
||||
"""Ensure an `iq` user exists as a UID-1000 alias of `comma`.
|
||||
|
||||
Idempotent: returns False when /etc/passwd and /etc/shadow already have a
|
||||
correctly-shaped `iq` entry, True when either file was modified.
|
||||
"""
|
||||
passwd_path = passwd_path if passwd_path is not None else PASSWD_PATH
|
||||
shadow_path = shadow_path if shadow_path is not None else SHADOW_PATH
|
||||
remount = remount if remount is not None else remount_root
|
||||
now = now if now is not None else time.time
|
||||
|
||||
passwd_text = passwd_path.read_text(encoding="utf-8") if passwd_path.exists() else ""
|
||||
shadow_text = shadow_path.read_text(encoding="utf-8") if shadow_path.exists() else ""
|
||||
|
||||
source_line = _line_for_user(passwd_text, SOURCE_USER)
|
||||
if source_line is None:
|
||||
# Without a comma user to clone from we'd create a broken iq entry; bail.
|
||||
return False
|
||||
|
||||
expected_passwd = _expected_passwd_line(source_line)
|
||||
passwd_line = _line_for_user(passwd_text, IQ_USER)
|
||||
shadow_line = _line_for_user(shadow_text, IQ_USER)
|
||||
|
||||
passwd_ok = passwd_line == expected_passwd
|
||||
shadow_ok = shadow_line is not None and shadow_line.startswith(f"{IQ_USER}:!:")
|
||||
if passwd_ok and shadow_ok:
|
||||
return False
|
||||
|
||||
days = int(now() // 86400)
|
||||
new_passwd = _replace_or_append(passwd_text, IQ_USER, expected_passwd)
|
||||
new_shadow = _replace_or_append(shadow_text, IQ_USER, _expected_shadow_line(days))
|
||||
|
||||
opts = root_opts if root_opts is not None else get_root_mount_options()
|
||||
remounted = False
|
||||
try:
|
||||
remount("remount,rw")
|
||||
remounted = True
|
||||
if not passwd_ok:
|
||||
passwd_path.write_text(new_passwd, encoding="utf-8")
|
||||
if not shadow_ok:
|
||||
shadow_path.write_text(new_shadow, encoding="utf-8")
|
||||
finally:
|
||||
if remounted:
|
||||
remount(f"remount,{opts}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def apply_branding() -> bool:
|
||||
if not BG_ASSET.exists() or not SPLASH_BMP_ASSET.exists():
|
||||
return False
|
||||
|
||||
backup_if_missing()
|
||||
|
||||
bg_asset_bytes = BG_ASSET.read_bytes()
|
||||
splash_asset_bytes = SPLASH_BMP_ASSET.read_bytes()
|
||||
|
||||
current_bg_hash = sha256_path(BG_TARGET)
|
||||
current_splash_hash = sha256_bytes(read_splash_bytes(SPLASH_BMP_OFFSET, len(splash_asset_bytes)))
|
||||
|
||||
bg_asset_hash = sha256_bytes(bg_asset_bytes)
|
||||
splash_asset_hash = sha256_bytes(splash_asset_bytes)
|
||||
|
||||
if current_bg_hash == bg_asset_hash and current_splash_hash == splash_asset_hash:
|
||||
return False
|
||||
|
||||
root_opts = get_root_mount_options()
|
||||
remounted = False
|
||||
try:
|
||||
if current_bg_hash != bg_asset_hash:
|
||||
remount_root("remount,rw")
|
||||
remounted = True
|
||||
BG_TARGET.write_bytes(bg_asset_bytes)
|
||||
|
||||
if current_splash_hash != splash_asset_hash:
|
||||
write_splash_bytes(SPLASH_BMP_OFFSET, splash_asset_bytes)
|
||||
run(["sync"])
|
||||
finally:
|
||||
if remounted:
|
||||
remount_root(f"remount,{root_opts}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def restore_branding() -> bool:
|
||||
if not BG_BACKUP.exists() or not SPLASH_BACKUP.exists():
|
||||
return False
|
||||
|
||||
root_opts = get_root_mount_options()
|
||||
remounted = False
|
||||
try:
|
||||
remount_root("remount,rw")
|
||||
remounted = True
|
||||
shutil.copy2(BG_BACKUP, BG_TARGET)
|
||||
|
||||
with SPLASH_BACKUP.open("rb") as src, SPLASH_TARGET.open("r+b", buffering=0) as dst:
|
||||
while True:
|
||||
chunk = src.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
dst.write(chunk)
|
||||
dst.flush()
|
||||
os.fsync(dst.fileno())
|
||||
run(["sync"])
|
||||
finally:
|
||||
if remounted:
|
||||
remount_root(f"remount,{root_opts}")
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def status() -> dict[str, object]:
|
||||
result: dict[str, object] = {
|
||||
"bg_asset_exists": BG_ASSET.exists(),
|
||||
"splash_asset_exists": SPLASH_BMP_ASSET.exists(),
|
||||
"bg_backup_exists": BG_BACKUP.exists(),
|
||||
"splash_backup_exists": SPLASH_BACKUP.exists(),
|
||||
}
|
||||
if BG_ASSET.exists() and BG_TARGET.exists():
|
||||
result["bg_asset_sha256"] = sha256_path(BG_ASSET)
|
||||
result["bg_current_sha256"] = sha256_path(BG_TARGET)
|
||||
if SPLASH_BMP_ASSET.exists() and SPLASH_TARGET.exists():
|
||||
splash_asset_bytes = SPLASH_BMP_ASSET.read_bytes()
|
||||
result["splash_asset_sha256"] = sha256_bytes(splash_asset_bytes)
|
||||
result["splash_current_sha256"] = sha256_bytes(read_splash_bytes(SPLASH_BMP_OFFSET, len(splash_asset_bytes)))
|
||||
return result
|
||||
|
||||
|
||||
def main() -> int:
|
||||
if os.geteuid() != 0:
|
||||
raise SystemExit("apply_boot_branding.py must run as root")
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
group = parser.add_mutually_exclusive_group()
|
||||
group.add_argument("--restore", action="store_true")
|
||||
group.add_argument("--status", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.status:
|
||||
print(json.dumps(status(), indent=2))
|
||||
return 0
|
||||
|
||||
changed = restore_branding() if args.restore else apply_branding()
|
||||
|
||||
try:
|
||||
iq_changed = ensure_iq_user()
|
||||
except Exception as e: # noqa: BLE001
|
||||
iq_changed = False
|
||||
print(_paint(f"ensure_iq_user failed: {e}", "1;38;5;203"))
|
||||
|
||||
if changed or iq_changed:
|
||||
print(_paint("changed", "38;5;114"))
|
||||
else:
|
||||
print(_paint("no-change", "2;38;5;246"))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 78 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 6.7 MiB |
@@ -0,0 +1,381 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )/../.." >/dev/null && pwd )"
|
||||
INSTALL_ROOT="${ROOT_DIR}/.iqpilot"
|
||||
BUNDLES_ROOT="${INSTALL_ROOT}/bundles"
|
||||
VERIFY_SCRIPT="${ROOT_DIR}/artifacts/runtime/verify_proprietary_bundle.py"
|
||||
|
||||
manifest_hash() {
|
||||
local manifest_path="$1"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
sha256sum "${manifest_path}" | awk '{print $1}'
|
||||
else
|
||||
shasum -a 256 "${manifest_path}" | awk '{print $1}'
|
||||
fi
|
||||
}
|
||||
|
||||
NAVD_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_navd_private"
|
||||
HEPHA_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_hephaestusd_private"
|
||||
MODEL_SELECTOR_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_model_selector_private"
|
||||
ALC_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_alc_private"
|
||||
COMMANDER_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_commander_private"
|
||||
UPDATER_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_updater_private"
|
||||
VALHALLA_RUNTIME_BUNDLE="${ROOT_DIR}/artifacts/iqpilot_valhalla_runtime"
|
||||
BUNDLE_INSTALL_NAVD="${BUNDLES_ROOT}/iqpilot_navd_private"
|
||||
BUNDLE_INSTALL_HEPHA="${BUNDLES_ROOT}/iqpilot_hephaestusd_private"
|
||||
BUNDLE_INSTALL_MODEL_SELECTOR="${BUNDLES_ROOT}/iqpilot_model_selector_private"
|
||||
BUNDLE_INSTALL_ALC="${BUNDLES_ROOT}/iqpilot_alc_private"
|
||||
BUNDLE_INSTALL_COMMANDER="${BUNDLES_ROOT}/iqpilot_commander_private"
|
||||
BUNDLE_INSTALL_UPDATER="${BUNDLES_ROOT}/iqpilot_updater_private"
|
||||
BUNDLE_INSTALL_VALHALLA_RUNTIME="${BUNDLES_ROOT}/iqpilot_valhalla_runtime"
|
||||
NAVD_STATE_FILE="${INSTALL_ROOT}/.installed_navd_manifest.sha256"
|
||||
HEPHA_STATE_FILE="${INSTALL_ROOT}/.installed_hephaestusd_manifest.sha256"
|
||||
MODEL_SELECTOR_STATE_FILE="${INSTALL_ROOT}/.installed_model_selector_manifest.sha256"
|
||||
ALC_STATE_FILE="${INSTALL_ROOT}/.installed_alc_manifest.sha256"
|
||||
COMMANDER_STATE_FILE="${INSTALL_ROOT}/.installed_commander_manifest.sha256"
|
||||
UPDATER_STATE_FILE="${INSTALL_ROOT}/.installed_updater_manifest.sha256"
|
||||
VALHALLA_RUNTIME_STATE_FILE="${INSTALL_ROOT}/.installed_valhalla_runtime_manifest.sha256"
|
||||
NAVD_SENTINEL_BASE="${BUNDLE_INSTALL_NAVD}/python/iqpilot_private/navd/navd"
|
||||
HEPHA_SENTINEL_BASE="${BUNDLE_INSTALL_HEPHA}/python/iqpilot_private/konn3kt/hephaestus/hephaestusd"
|
||||
MODEL_SELECTOR_SENTINEL_BASE="${BUNDLE_INSTALL_MODEL_SELECTOR}/python/iqpilot_private/models/manager"
|
||||
ALC_SENTINEL_BASE="${BUNDLE_INSTALL_ALC}/python/iqpilot_private/konn3kt/iqlvbs/alc"
|
||||
COMMANDER_SENTINEL_BASE="${BUNDLE_INSTALL_COMMANDER}/python/iqpilot_private/konn3kt/iqlvbs/iqlvbs_commander"
|
||||
UPDATER_SENTINEL_BASE="${BUNDLE_INSTALL_UPDATER}/python/iqpilot_private/updater/git_remote"
|
||||
VALHALLA_RUNTIME_SENTINEL="${BUNDLE_INSTALL_VALHALLA_RUNTIME}/valhalla_runtime/bin/valhalla_service"
|
||||
|
||||
NAVD_HASH=""
|
||||
HEPHA_HASH=""
|
||||
MODEL_SELECTOR_HASH=""
|
||||
ALC_HASH=""
|
||||
COMMANDER_HASH=""
|
||||
UPDATER_HASH=""
|
||||
VALHALLA_RUNTIME_HASH=""
|
||||
NEED_INSTALL=0
|
||||
HAVE_BUNDLE=0
|
||||
|
||||
seed_hepha_ble_runtime() {
|
||||
if [ ! -f "${HEPHA_BUNDLE}/manifest.json" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
PYTHONPATH="${ROOT_DIR}" python3 <<'PY'
|
||||
from pathlib import Path
|
||||
import os
|
||||
import secrets
|
||||
|
||||
BLE_ENABLE_PARAM = "Konn3ktBleTransportEnabled"
|
||||
PRIMARY_AUTH_DIR = Path("/data/konn3kt_ble")
|
||||
FALLBACK_AUTH_DIR = Path("/data/openpilot/.konn3kt_ble")
|
||||
|
||||
|
||||
def ensure_dir(path: Path, mode: int) -> bool:
|
||||
try:
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
try:
|
||||
os.chmod(path, mode)
|
||||
except Exception:
|
||||
pass
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def write_if_missing(path: Path, contents: str, mode: int) -> None:
|
||||
if path.exists():
|
||||
return
|
||||
path.write_text(contents, encoding="utf-8")
|
||||
try:
|
||||
os.chmod(path, mode)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def seed_ble_enable_param() -> None:
|
||||
try:
|
||||
from openpilot.common.params import Params
|
||||
params = Params()
|
||||
if params.get(BLE_ENABLE_PARAM) is None:
|
||||
params.put_bool(BLE_ENABLE_PARAM, True)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
param_path = Path("/data/params/d") / BLE_ENABLE_PARAM
|
||||
if ensure_dir(param_path.parent, 0o755):
|
||||
try:
|
||||
if not param_path.exists():
|
||||
param_path.write_bytes(b"1")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def seed_ble_auth_artifacts() -> None:
|
||||
auth_dir = None
|
||||
for candidate in (PRIMARY_AUTH_DIR, FALLBACK_AUTH_DIR):
|
||||
if ensure_dir(candidate, 0o700):
|
||||
auth_dir = candidate
|
||||
break
|
||||
|
||||
if auth_dir is None:
|
||||
return
|
||||
|
||||
write_if_missing(auth_dir / "approved_clients.json", "{}\n", 0o600)
|
||||
write_if_missing(auth_dir / "install_credentials.json", "{}\n", 0o600)
|
||||
write_if_missing(auth_dir / "dev_secret", f"{secrets.token_hex(32)}\n", 0o600)
|
||||
|
||||
|
||||
seed_ble_enable_param()
|
||||
seed_ble_auth_artifacts()
|
||||
PY
|
||||
|
||||
if [ -d /usr/lib/python3/dist-packages/gi ]; then
|
||||
ln -sfn /usr/lib/python3/dist-packages/gi "${ROOT_DIR}/gi"
|
||||
fi
|
||||
}
|
||||
|
||||
module_present() {
|
||||
local base_path="$1"
|
||||
if [ -f "${base_path}.pyc" ]; then
|
||||
return 0
|
||||
fi
|
||||
if compgen -G "${base_path}".*.so >/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
apply_manifest_modes() {
|
||||
local bundle_root="$1"
|
||||
if [ ! -f "${bundle_root}/manifest.json" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
python3 - "${bundle_root}" <<'PY'
|
||||
from pathlib import Path
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
root = Path(sys.argv[1])
|
||||
manifest = json.loads((root / "manifest.json").read_text(encoding="utf-8"))
|
||||
for rel, meta in manifest.items():
|
||||
if rel in {"signatures", "runtime"} or not isinstance(meta, dict) or "mode" not in meta:
|
||||
continue
|
||||
path = root / rel
|
||||
if path.exists():
|
||||
os.chmod(path, int(meta["mode"]))
|
||||
PY
|
||||
}
|
||||
|
||||
installed_bundle_valid() {
|
||||
local bundle_root="$1"
|
||||
if [ ! -f "${bundle_root}/manifest.json" ]; then
|
||||
return 1
|
||||
fi
|
||||
apply_manifest_modes "${bundle_root}" || return 1
|
||||
python3 "${VERIFY_SCRIPT}" "${bundle_root}" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
if [ -f "${NAVD_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${NAVD_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
NAVD_HASH="$(manifest_hash "${NAVD_BUNDLE}/manifest.json")"
|
||||
NAVD_DST_HASH=""
|
||||
if [ -f "${NAVD_STATE_FILE}" ]; then
|
||||
NAVD_DST_HASH="$(cat "${NAVD_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if ! module_present "${NAVD_SENTINEL_BASE}" || [ "${NAVD_HASH}" != "${NAVD_DST_HASH}" ] || ! installed_bundle_valid "${BUNDLE_INSTALL_NAVD}"; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${HEPHA_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${HEPHA_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
HEPHA_HASH="$(manifest_hash "${HEPHA_BUNDLE}/manifest.json")"
|
||||
HEPHA_DST_HASH=""
|
||||
if [ -f "${HEPHA_STATE_FILE}" ]; then
|
||||
HEPHA_DST_HASH="$(cat "${HEPHA_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if ! module_present "${HEPHA_SENTINEL_BASE}" || [ "${HEPHA_HASH}" != "${HEPHA_DST_HASH}" ] || ! installed_bundle_valid "${BUNDLE_INSTALL_HEPHA}"; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${MODEL_SELECTOR_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${MODEL_SELECTOR_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
MODEL_SELECTOR_HASH="$(manifest_hash "${MODEL_SELECTOR_BUNDLE}/manifest.json")"
|
||||
MODEL_SELECTOR_DST_HASH=""
|
||||
if [ -f "${MODEL_SELECTOR_STATE_FILE}" ]; then
|
||||
MODEL_SELECTOR_DST_HASH="$(cat "${MODEL_SELECTOR_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if ! module_present "${MODEL_SELECTOR_SENTINEL_BASE}" || [ "${MODEL_SELECTOR_HASH}" != "${MODEL_SELECTOR_DST_HASH}" ] || ! installed_bundle_valid "${BUNDLE_INSTALL_MODEL_SELECTOR}"; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${ALC_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${ALC_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
ALC_HASH="$(manifest_hash "${ALC_BUNDLE}/manifest.json")"
|
||||
ALC_DST_HASH=""
|
||||
if [ -f "${ALC_STATE_FILE}" ]; then
|
||||
ALC_DST_HASH="$(cat "${ALC_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if ! module_present "${ALC_SENTINEL_BASE}" || [ "${ALC_HASH}" != "${ALC_DST_HASH}" ] || ! installed_bundle_valid "${BUNDLE_INSTALL_ALC}"; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${COMMANDER_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${COMMANDER_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
COMMANDER_HASH="$(manifest_hash "${COMMANDER_BUNDLE}/manifest.json")"
|
||||
COMMANDER_DST_HASH=""
|
||||
if [ -f "${COMMANDER_STATE_FILE}" ]; then
|
||||
COMMANDER_DST_HASH="$(cat "${COMMANDER_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if ! module_present "${COMMANDER_SENTINEL_BASE}" || [ "${COMMANDER_HASH}" != "${COMMANDER_DST_HASH}" ] || ! installed_bundle_valid "${BUNDLE_INSTALL_COMMANDER}"; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${UPDATER_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${UPDATER_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
UPDATER_HASH="$(manifest_hash "${UPDATER_BUNDLE}/manifest.json")"
|
||||
UPDATER_DST_HASH=""
|
||||
if [ -f "${UPDATER_STATE_FILE}" ]; then
|
||||
UPDATER_DST_HASH="$(cat "${UPDATER_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if ! module_present "${UPDATER_SENTINEL_BASE}" || [ "${UPDATER_HASH}" != "${UPDATER_DST_HASH}" ] || ! installed_bundle_valid "${BUNDLE_INSTALL_UPDATER}"; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ -f "${VALHALLA_RUNTIME_BUNDLE}/manifest.json" ]; then
|
||||
apply_manifest_modes "${VALHALLA_RUNTIME_BUNDLE}" || true
|
||||
HAVE_BUNDLE=1
|
||||
VALHALLA_RUNTIME_HASH="$(manifest_hash "${VALHALLA_RUNTIME_BUNDLE}/manifest.json")"
|
||||
VALHALLA_RUNTIME_DST_HASH=""
|
||||
if [ -f "${VALHALLA_RUNTIME_STATE_FILE}" ]; then
|
||||
VALHALLA_RUNTIME_DST_HASH="$(cat "${VALHALLA_RUNTIME_STATE_FILE}" 2>/dev/null || true)"
|
||||
fi
|
||||
if [ ! -x "${VALHALLA_RUNTIME_SENTINEL}" ] || [ "${VALHALLA_RUNTIME_HASH}" != "${VALHALLA_RUNTIME_DST_HASH}" ]; then
|
||||
NEED_INSTALL=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [ "${HAVE_BUNDLE}" -eq 0 ]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${NEED_INSTALL}" -eq 0 ]; then
|
||||
seed_hepha_ble_runtime || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
TMP_ROOT="${INSTALL_ROOT}.tmp.$$"
|
||||
echo "Installing bundled private artifacts..."
|
||||
rm -rf "${TMP_ROOT}"
|
||||
mkdir -p "${TMP_ROOT}"
|
||||
mkdir -p "${TMP_ROOT}/bundles"
|
||||
|
||||
if [ -d "${INSTALL_ROOT}" ]; then
|
||||
cp -a "${INSTALL_ROOT}"/. "${TMP_ROOT}/"
|
||||
fi
|
||||
|
||||
if [ -f "${NAVD_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${NAVD_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_navd_private"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_navd_private"
|
||||
cp -a "${NAVD_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_navd_private/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_navd_private"
|
||||
fi
|
||||
|
||||
if [ -f "${HEPHA_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${HEPHA_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_hephaestusd_private"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_hephaestusd_private"
|
||||
cp -a "${HEPHA_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_hephaestusd_private/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_hephaestusd_private"
|
||||
fi
|
||||
|
||||
if [ -f "${MODEL_SELECTOR_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${MODEL_SELECTOR_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_model_selector_private"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_model_selector_private"
|
||||
cp -a "${MODEL_SELECTOR_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_model_selector_private/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_model_selector_private"
|
||||
fi
|
||||
|
||||
if [ -f "${ALC_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${ALC_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_alc_private"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_alc_private"
|
||||
cp -a "${ALC_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_alc_private/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_alc_private"
|
||||
fi
|
||||
|
||||
if [ -f "${COMMANDER_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${COMMANDER_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_commander_private"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_commander_private"
|
||||
cp -a "${COMMANDER_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_commander_private/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_commander_private"
|
||||
fi
|
||||
|
||||
if [ -f "${UPDATER_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${UPDATER_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_updater_private"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_updater_private"
|
||||
cp -a "${UPDATER_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_updater_private/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_updater_private"
|
||||
fi
|
||||
|
||||
if [ -f "${VALHALLA_RUNTIME_BUNDLE}/manifest.json" ]; then
|
||||
python3 "${VERIFY_SCRIPT}" "${VALHALLA_RUNTIME_BUNDLE}"
|
||||
rm -rf "${TMP_ROOT}/bundles/iqpilot_valhalla_runtime"
|
||||
mkdir -p "${TMP_ROOT}/bundles/iqpilot_valhalla_runtime"
|
||||
cp -a "${VALHALLA_RUNTIME_BUNDLE}"/. "${TMP_ROOT}/bundles/iqpilot_valhalla_runtime/"
|
||||
apply_manifest_modes "${TMP_ROOT}/bundles/iqpilot_valhalla_runtime"
|
||||
fi
|
||||
|
||||
if [ -d "${INSTALL_ROOT}" ]; then
|
||||
rm -rf "${INSTALL_ROOT}.bak"
|
||||
mv "${INSTALL_ROOT}" "${INSTALL_ROOT}.bak"
|
||||
fi
|
||||
|
||||
mv "${TMP_ROOT}" "${INSTALL_ROOT}"
|
||||
|
||||
if [ -d "${INSTALL_ROOT}.bak" ]; then
|
||||
rm -rf "${INSTALL_ROOT}.bak"
|
||||
fi
|
||||
|
||||
if [ -n "${NAVD_HASH}" ]; then
|
||||
printf '%s\n' "${NAVD_HASH}" > "${NAVD_STATE_FILE}"
|
||||
fi
|
||||
if [ -n "${HEPHA_HASH}" ]; then
|
||||
printf '%s\n' "${HEPHA_HASH}" > "${HEPHA_STATE_FILE}"
|
||||
fi
|
||||
if [ -n "${MODEL_SELECTOR_HASH}" ]; then
|
||||
printf '%s\n' "${MODEL_SELECTOR_HASH}" > "${MODEL_SELECTOR_STATE_FILE}"
|
||||
fi
|
||||
if [ -n "${ALC_HASH}" ]; then
|
||||
printf '%s\n' "${ALC_HASH}" > "${ALC_STATE_FILE}"
|
||||
fi
|
||||
if [ -n "${COMMANDER_HASH}" ]; then
|
||||
printf '%s\n' "${COMMANDER_HASH}" > "${COMMANDER_STATE_FILE}"
|
||||
fi
|
||||
if [ -n "${UPDATER_HASH}" ]; then
|
||||
printf '%s\n' "${UPDATER_HASH}" > "${UPDATER_STATE_FILE}"
|
||||
fi
|
||||
if [ -n "${VALHALLA_RUNTIME_HASH}" ]; then
|
||||
printf '%s\n' "${VALHALLA_RUNTIME_HASH}" > "${VALHALLA_RUNTIME_STATE_FILE}"
|
||||
fi
|
||||
|
||||
seed_hepha_ble_runtime || true
|
||||
|
||||
echo "Private artifacts installed."
|
||||
@@ -0,0 +1,216 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from artifacts.runtime import apply_boot_branding as abb
|
||||
|
||||
|
||||
COMMA_HOME = "/home/comma"
|
||||
|
||||
EXISTING_PASSWD = (
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
"daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin\n"
|
||||
f"comma:x:1000:1000:comma:{COMMA_HOME}:/bin/bash\n"
|
||||
"nobody:x:65534:65534:nobody:/nonexistent:/usr/sbin/nologin\n"
|
||||
)
|
||||
|
||||
EXISTING_SHADOW = (
|
||||
"root:*:19000:0:99999:7:::\n"
|
||||
"daemon:*:19000:0:99999:7:::\n"
|
||||
"comma:$6$abc$xyz:19000:0:99999:7:::\n"
|
||||
"nobody:!:19000:0:99999:7:::\n"
|
||||
)
|
||||
|
||||
EXPECTED_PASSWD_LINE = f"iq:x:1000:1000:IQ.Pilot:{COMMA_HOME}:/bin/bash"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def etc(tmp_path: Path) -> tuple[Path, Path]:
|
||||
passwd = tmp_path / "passwd"
|
||||
shadow = tmp_path / "shadow"
|
||||
passwd.write_text(EXISTING_PASSWD, encoding="utf-8")
|
||||
shadow.write_text(EXISTING_SHADOW, encoding="utf-8")
|
||||
return passwd, shadow
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fake_remount():
|
||||
calls: list[str] = []
|
||||
|
||||
def _remount(mode: str) -> None:
|
||||
calls.append(mode)
|
||||
|
||||
_remount.calls = calls
|
||||
return _remount
|
||||
|
||||
|
||||
def _frozen_now() -> float:
|
||||
return 1779494400.0
|
||||
|
||||
|
||||
def test_fresh_install_adds_iq_user(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd,
|
||||
shadow_path=shadow,
|
||||
remount=fake_remount,
|
||||
root_opts="ro,relatime",
|
||||
now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
passwd_text = passwd.read_text(encoding="utf-8")
|
||||
shadow_text = shadow.read_text(encoding="utf-8")
|
||||
|
||||
assert EXPECTED_PASSWD_LINE in passwd_text.splitlines()
|
||||
assert any(l.startswith("iq:!:") and l.endswith(":0:99999:7:::") for l in shadow_text.splitlines())
|
||||
|
||||
for original_line in EXISTING_PASSWD.splitlines():
|
||||
assert original_line in passwd_text.splitlines()
|
||||
for original_line in EXISTING_SHADOW.splitlines():
|
||||
assert original_line in shadow_text.splitlines()
|
||||
|
||||
assert fake_remount.calls == ["remount,rw", "remount,ro,relatime"]
|
||||
|
||||
|
||||
def test_second_boot_is_noop(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
|
||||
abb.ensure_iq_user(passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now)
|
||||
passwd_after_first = passwd.read_text(encoding="utf-8")
|
||||
shadow_after_first = shadow.read_text(encoding="utf-8")
|
||||
fake_remount.calls.clear()
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
assert passwd.read_text(encoding="utf-8") == passwd_after_first
|
||||
assert shadow.read_text(encoding="utf-8") == shadow_after_first
|
||||
assert fake_remount.calls == [] # no remount when nothing to do
|
||||
|
||||
|
||||
def test_partial_state_self_heals(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
passwd.write_text(EXISTING_PASSWD + EXPECTED_PASSWD_LINE + "\n", encoding="utf-8")
|
||||
# shadow lacks iq
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
iq_passwd_lines = [l for l in passwd.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")]
|
||||
iq_shadow_lines = [l for l in shadow.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")]
|
||||
assert iq_passwd_lines == [EXPECTED_PASSWD_LINE] # not duplicated
|
||||
assert len(iq_shadow_lines) == 1
|
||||
assert iq_shadow_lines[0].startswith("iq:!:")
|
||||
|
||||
|
||||
def test_malformed_iq_passwd_line_replaced(etc, fake_remount):
|
||||
passwd, shadow = etc
|
||||
wrong = "iq:x:1000:1000:old:/root:/bin/sh"
|
||||
passwd.write_text(EXISTING_PASSWD + wrong + "\n", encoding="utf-8")
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
iq_lines = [l for l in passwd.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")]
|
||||
assert iq_lines == [EXPECTED_PASSWD_LINE]
|
||||
|
||||
|
||||
def test_remount_ro_restored_on_write_failure(tmp_path, fake_remount):
|
||||
passwd = tmp_path / "ro_passwd"
|
||||
passwd.write_text(EXISTING_PASSWD, encoding="utf-8")
|
||||
passwd.chmod(0o444) # force write to raise
|
||||
|
||||
shadow = tmp_path / "shadow"
|
||||
shadow.write_text(EXISTING_SHADOW, encoding="utf-8")
|
||||
|
||||
with pytest.raises(PermissionError):
|
||||
abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert fake_remount.calls == ["remount,rw", "remount,ro"]
|
||||
|
||||
|
||||
def test_does_not_alter_existing_branding_flow(monkeypatch, etc, fake_remount, capsys):
|
||||
passwd, shadow = etc
|
||||
|
||||
monkeypatch.setattr(abb, "PASSWD_PATH", passwd)
|
||||
monkeypatch.setattr(abb, "SHADOW_PATH", shadow)
|
||||
monkeypatch.setattr(abb, "remount_root", fake_remount)
|
||||
monkeypatch.setattr(abb, "get_root_mount_options", lambda: "ro,relatime")
|
||||
monkeypatch.setattr(abb.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(abb, "apply_branding", lambda: False) # no assets → branding no-op
|
||||
monkeypatch.setattr("sys.argv", ["apply_boot_branding.py"])
|
||||
|
||||
rc = abb.main()
|
||||
out = capsys.readouterr().out.strip().splitlines()
|
||||
|
||||
assert rc == 0
|
||||
assert out[-1] == "changed" # iq_changed flips combined result
|
||||
assert EXPECTED_PASSWD_LINE in passwd.read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
def test_iq_home_tracks_commas_actual_home(tmp_path, fake_remount):
|
||||
passwd = tmp_path / "passwd"
|
||||
shadow = tmp_path / "shadow"
|
||||
unusual_home = "/data/some/relocated/home/comma"
|
||||
passwd.write_text(
|
||||
"root:x:0:0:root:/root:/bin/bash\n"
|
||||
f"comma:x:1000:1000:comma:{unusual_home}:/bin/zsh\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
shadow.write_text("comma:$6$x$y:19000:0:99999:7:::\n", encoding="utf-8")
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is True
|
||||
iq_line = [l for l in passwd.read_text(encoding="utf-8").splitlines() if l.startswith("iq:")][0]
|
||||
# iq inherits comma's home, shell, UID, GID — not a hardcoded path
|
||||
assert iq_line == f"iq:x:1000:1000:IQ.Pilot:{unusual_home}:/bin/zsh"
|
||||
|
||||
|
||||
def test_skips_when_comma_user_absent(tmp_path, fake_remount):
|
||||
passwd = tmp_path / "passwd"
|
||||
shadow = tmp_path / "shadow"
|
||||
passwd.write_text("root:x:0:0:root:/root:/bin/bash\n", encoding="utf-8")
|
||||
shadow.write_text("root:*:19000:0:99999:7:::\n", encoding="utf-8")
|
||||
|
||||
changed = abb.ensure_iq_user(
|
||||
passwd_path=passwd, shadow_path=shadow, remount=fake_remount, root_opts="ro", now=_frozen_now,
|
||||
)
|
||||
|
||||
assert changed is False
|
||||
assert "iq:" not in passwd.read_text(encoding="utf-8")
|
||||
assert "iq:" not in shadow.read_text(encoding="utf-8")
|
||||
assert fake_remount.calls == []
|
||||
|
||||
|
||||
def test_ensure_iq_user_failure_is_swallowed_by_main(monkeypatch, capsys):
|
||||
def boom(*a, **kw):
|
||||
raise RuntimeError("simulated failure")
|
||||
|
||||
monkeypatch.setattr(abb, "ensure_iq_user", boom)
|
||||
monkeypatch.setattr(abb.os, "geteuid", lambda: 0)
|
||||
monkeypatch.setattr(abb, "apply_branding", lambda: True)
|
||||
monkeypatch.setattr("sys.argv", ["apply_boot_branding.py"])
|
||||
|
||||
rc = abb.main()
|
||||
out = capsys.readouterr().out
|
||||
|
||||
assert rc == 0
|
||||
assert "ensure_iq_user failed: simulated failure" in out
|
||||
assert "changed" in out # branding ran, so combined result is still "changed"
|
||||
@@ -0,0 +1,70 @@
|
||||
#!/usr/bin/env python3
|
||||
import argparse
|
||||
import hashlib
|
||||
import json
|
||||
import stat
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
p = argparse.ArgumentParser(description="Verify IQ Pilot proprietary bundle manifest")
|
||||
p.add_argument("bundle_root", help="Bundle directory containing manifest.json")
|
||||
return p.parse_args()
|
||||
|
||||
|
||||
def sha256(path: Path) -> str:
|
||||
h = hashlib.sha256()
|
||||
with path.open("rb") as f:
|
||||
while True:
|
||||
b = f.read(1024 * 1024)
|
||||
if not b:
|
||||
break
|
||||
h.update(b)
|
||||
return h.hexdigest()
|
||||
|
||||
|
||||
def main() -> None:
|
||||
args = parse_args()
|
||||
root = Path(args.bundle_root).resolve()
|
||||
manifest_path = root / "manifest.json"
|
||||
if not manifest_path.exists():
|
||||
raise SystemExit(f"manifest not found: {manifest_path}")
|
||||
|
||||
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
||||
failures: list[str] = []
|
||||
|
||||
for rel, meta in manifest.items():
|
||||
if rel in {"signatures", "runtime"}:
|
||||
continue
|
||||
f = root / rel
|
||||
if not f.exists():
|
||||
failures.append(f"missing: {rel}")
|
||||
continue
|
||||
|
||||
got = sha256(f)
|
||||
exp = str(meta["sha256"])
|
||||
if got != exp:
|
||||
failures.append(f"sha256 mismatch: {rel}")
|
||||
|
||||
got_size = f.stat().st_size
|
||||
exp_size = int(meta["size"])
|
||||
if got_size != exp_size:
|
||||
failures.append(f"size mismatch: {rel} ({got_size} != {exp_size})")
|
||||
|
||||
if "mode" in meta:
|
||||
got_mode = stat.S_IMODE(f.stat().st_mode)
|
||||
exp_mode = int(meta["mode"])
|
||||
if got_mode != exp_mode:
|
||||
failures.append(f"mode mismatch: {rel} ({got_mode:o} != {exp_mode:o})")
|
||||
|
||||
if failures:
|
||||
print("FAILED")
|
||||
for x in failures:
|
||||
print(x)
|
||||
raise SystemExit(1)
|
||||
|
||||
print("OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user