mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-09-11 02:33:51 +08:00
Agnos 19.6.10
This commit is contained in:
@@ -2,12 +2,45 @@
|
||||
set -euo pipefail
|
||||
|
||||
HOST="${1:-comma@192.168.3.110}"
|
||||
IMAGE="${2:-/Users/dominickthompson/Desktop/system8.img.xz}"
|
||||
IMAGE="${2:-/Users/dominickthompson/Desktop/system17.img.xz}"
|
||||
METADATA="${3:-${IMAGE}.metadata.json}"
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/../.." && pwd)"
|
||||
SSH_KEY="${SSH_KEY:-${REPO_ROOT}/system/hardware/tici/id_rsa}"
|
||||
SSH_OPTS=(-i "$SSH_KEY" -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
|
||||
SSH_OPTS=(-o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null)
|
||||
if [[ -n "${SSH_KEY:-}" ]]; then
|
||||
SSH_OPTS=(-i "$SSH_KEY" -o IdentitiesOnly=yes "${SSH_OPTS[@]}")
|
||||
fi
|
||||
|
||||
for required_path in "$IMAGE" "$METADATA"; do
|
||||
[[ -f "$required_path" ]] || { echo "missing file: $required_path" >&2; exit 1; }
|
||||
done
|
||||
|
||||
metadata_value() {
|
||||
python3 - "$METADATA" "$1" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
metadata = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
||||
value = metadata[sys.argv[2]]
|
||||
if isinstance(value, (dict, list)):
|
||||
raise SystemExit(f"metadata field {sys.argv[2]} is not scalar")
|
||||
print(value)
|
||||
PY
|
||||
}
|
||||
|
||||
BASE_VERSION="$(metadata_value base_version)"
|
||||
EXPECTED_VERSION="$(metadata_value target_version)"
|
||||
RAW_HASH="$(metadata_value raw_sha256)"
|
||||
RAW_SIZE="$(metadata_value raw_size)"
|
||||
EXPECTED_XZ_HASH="$(metadata_value xz_sha256)"
|
||||
ACTUAL_XZ_HASH="$(shasum -a 256 "$IMAGE" | awk '{print $1}')"
|
||||
|
||||
[[ "$ACTUAL_XZ_HASH" == "$EXPECTED_XZ_HASH" ]] || {
|
||||
echo "compressed image hash mismatch: got $ACTUAL_XZ_HASH, expected $EXPECTED_XZ_HASH" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
SESSION="local_agnos_flash"
|
||||
REMOTE_DIR="/data/local_agnos_flash"
|
||||
@@ -15,40 +48,49 @@ REMOTE_MANIFEST="${REMOTE_DIR}/agnos-local-system.json"
|
||||
REMOTE_RUNNER="${REMOTE_DIR}/run_flash.sh"
|
||||
REMOTE_AGNOS="${REMOTE_DIR}/agnos.py"
|
||||
PORT="8989"
|
||||
|
||||
EXPECTED_VERSION="12.8.28"
|
||||
RAW_HASH="4c01245932068aedfceb41cb1aab1f7f044f6659aa2fe2de558f99e2d3aa5793"
|
||||
RAW_SIZE="5368709120"
|
||||
|
||||
if [[ ! -f "$IMAGE" ]]; then
|
||||
echo "missing image: $IMAGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
IMAGE_NAME="$(basename "$IMAGE")"
|
||||
REMOTE_IMAGE="${REMOTE_DIR}/${IMAGE_NAME}"
|
||||
|
||||
INSTALLED_VERSION="$(ssh "${SSH_OPTS[@]}" "$HOST" 'tr -d "\r\n" </VERSION')"
|
||||
case "$INSTALLED_VERSION" in
|
||||
19.6|19.6.*) ;;
|
||||
*)
|
||||
echo "refusing flash: device is on incompatible AGNOS $INSTALLED_VERSION; candidate is based on upstream $BASE_VERSION" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
echo "[CHECK] Device AGNOS: $INSTALLED_VERSION"
|
||||
echo "[CHECK] Candidate AGNOS: $EXPECTED_VERSION"
|
||||
echo "[CHECK] Candidate XZ hash: $ACTUAL_XZ_HASH"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$HOST" "mkdir -p '$REMOTE_DIR'"
|
||||
scp "${SSH_OPTS[@]}" "$IMAGE" "$HOST:$REMOTE_IMAGE"
|
||||
|
||||
LOCAL_AGNOS="$(mktemp "${TMPDIR:-/tmp}/agnos-local.XXXXXX.py")"
|
||||
trap 'rm -f "$LOCAL_AGNOS"' EXIT
|
||||
python3 - "$REPO_ROOT/system/hardware/tici/agnos.py" "$LOCAL_AGNOS" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
src, dst = map(Path, sys.argv[1:])
|
||||
data = src.read_text(encoding="utf-8")
|
||||
needle = "import openpilot.system.updated.casync.casync as casync"
|
||||
if data.count(needle) != 1:
|
||||
raise SystemExit("could not isolate the unused casync dependency in agnos.py")
|
||||
data = data.replace(
|
||||
"import openpilot.system.updated.casync.casync as casync",
|
||||
"""class _UnusedCasync:
|
||||
needle,
|
||||
'''class _UnusedCasync:
|
||||
ChunkReader = object
|
||||
ChunkDict = object
|
||||
|
||||
def __getattr__(self, name):
|
||||
raise RuntimeError("casync support is unavailable in local AGNOS flash runner")
|
||||
casync = _UnusedCasync()""",
|
||||
casync = _UnusedCasync()''',
|
||||
)
|
||||
dst.write_text(data, encoding="utf-8")
|
||||
PY
|
||||
scp "${SSH_OPTS[@]}" "$LOCAL_AGNOS" "$HOST:$REMOTE_AGNOS"
|
||||
rm -f "$LOCAL_AGNOS"
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$HOST" "cat > '$REMOTE_MANIFEST'" <<MANIFEST
|
||||
[
|
||||
@@ -73,40 +115,33 @@ set -euo pipefail
|
||||
: "${REMOTE_MANIFEST:?}"
|
||||
: "${REMOTE_AGNOS:?}"
|
||||
: "${PORT:?}"
|
||||
: "${IMAGE_NAME:?}"
|
||||
: "${EXPECTED_VERSION:?}"
|
||||
|
||||
exec > >(tee -a "${REMOTE_DIR}/flash.log") 2>&1
|
||||
|
||||
echo "[STEP] Local AGNOS system flash"
|
||||
echo "[CHECK] Installed AGNOS: $(cat /VERSION 2>/dev/null || echo unknown)"
|
||||
echo "[CHECK] Target AGNOS: ${EXPECTED_VERSION}"
|
||||
echo "[CHECK] Active slot: $(abctl --boot_slot)"
|
||||
df -h /data
|
||||
|
||||
if [[ ! -f "$REMOTE_AGNOS" ]]; then
|
||||
echo "[ERROR] $REMOTE_AGNOS not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -x /usr/local/venv/bin/python3 ]]; then
|
||||
PYTHON_BIN="/usr/local/venv/bin/python3"
|
||||
else
|
||||
PYTHON_BIN="python3"
|
||||
fi
|
||||
PYTHON_BIN="/usr/local/venv/bin/python3"
|
||||
[[ -x "$PYTHON_BIN" ]] || { echo "[ERROR] managed Python is unavailable" >&2; exit 1; }
|
||||
|
||||
pkill -f "http.server ${PORT}.*${REMOTE_DIR}" >/dev/null 2>&1 || true
|
||||
"$PYTHON_BIN" -m http.server "$PORT" --bind 127.0.0.1 --directory "$REMOTE_DIR" >"${REMOTE_DIR}/http.log" 2>&1 &
|
||||
http_pid="$!"
|
||||
trap 'kill "$http_pid" >/dev/null 2>&1 || true' EXIT
|
||||
|
||||
http_ready=0
|
||||
for _ in $(seq 1 20); do
|
||||
if "$PYTHON_BIN" - "${PORT}" "${IMAGE_NAME}" <<'PY'
|
||||
if "$PYTHON_BIN" - "$REMOTE_MANIFEST" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
import urllib.request
|
||||
from pathlib import Path
|
||||
|
||||
port, image_name = sys.argv[1], sys.argv[2]
|
||||
with urllib.request.urlopen(f"http://127.0.0.1:{port}/{image_name}", timeout=2) as resp:
|
||||
resp.read(1)
|
||||
for entry in json.loads(Path(sys.argv[1]).read_text(encoding="utf-8")):
|
||||
with urllib.request.urlopen(entry["url"], timeout=2) as response:
|
||||
response.read(1)
|
||||
PY
|
||||
then
|
||||
http_ready=1
|
||||
@@ -115,24 +150,23 @@ PY
|
||||
sleep 0.25
|
||||
done
|
||||
|
||||
if [[ "$http_ready" != "1" ]]; then
|
||||
echo "[ERROR] Local image HTTP server did not become ready" >&2
|
||||
[[ "${http_ready:-0}" == "1" ]] || {
|
||||
echo "[ERROR] local image server did not become ready" >&2
|
||||
cat "${REMOTE_DIR}/http.log" >&2 || true
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[FLASH] Flashing local system image to inactive AGNOS slot"
|
||||
echo "[FLASH] Writing and verifying the candidate in the inactive system slot"
|
||||
PYTHONPATH="$(dirname "$REMOTE_AGNOS")" "$PYTHON_BIN" "$REMOTE_AGNOS" --swap "$REMOTE_MANIFEST"
|
||||
|
||||
echo "[DONE] AGNOS flashed and slot swapped"
|
||||
echo "[REBOOT] Rebooting now"
|
||||
echo "[DONE] Candidate written, verified, and selected"
|
||||
sudo reboot
|
||||
REMOTE_RUNNER
|
||||
|
||||
ssh "${SSH_OPTS[@]}" "$HOST" "tmux kill-session -t '$SESSION' >/dev/null 2>&1 || true"
|
||||
ssh "${SSH_OPTS[@]}" "$HOST" "rm -f '$REMOTE_DIR/flash.log' '$REMOTE_DIR/http.log'"
|
||||
ssh "${SSH_OPTS[@]}" "$HOST" \
|
||||
"tmux new-session -d -s '$SESSION' \"REMOTE_DIR='$REMOTE_DIR' REMOTE_MANIFEST='$REMOTE_MANIFEST' REMOTE_AGNOS='$REMOTE_AGNOS' PORT='$PORT' IMAGE_NAME='$IMAGE_NAME' EXPECTED_VERSION='$EXPECTED_VERSION' bash '$REMOTE_RUNNER'\""
|
||||
"tmux new-session -d -s '$SESSION' \"REMOTE_DIR='$REMOTE_DIR' REMOTE_MANIFEST='$REMOTE_MANIFEST' REMOTE_AGNOS='$REMOTE_AGNOS' PORT='$PORT' EXPECTED_VERSION='$EXPECTED_VERSION' bash '$REMOTE_RUNNER'\""
|
||||
|
||||
echo "Started remote tmux session: $SESSION"
|
||||
echo "Watch it with: ssh $HOST 'tmux attach -t $SESSION'"
|
||||
echo "After reboot, run tools/agnos/validate_agnos_runtime.sh $EXPECTED_VERSION on the device."
|
||||
|
||||
Regular → Executable
+955
-1290
File diff suppressed because it is too large
Load Diff
@@ -1,106 +1,238 @@
|
||||
import importlib.util
|
||||
import json
|
||||
import os
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
import runpy
|
||||
|
||||
import pytest
|
||||
|
||||
from tools.agnos.patch_system_reset_image import (
|
||||
AMDGPU_FIRMWARE_SHA256,
|
||||
COMMA_SH_DISPLAY_WAIT_PATCH_MARKER,
|
||||
comma_sh_has_expected_display_wait,
|
||||
find_default_reference_manifest,
|
||||
format_debugfs_mode,
|
||||
patch_comma_sh_display_wait,
|
||||
patch_setup_branding_script,
|
||||
sha256_zstd_payload,
|
||||
)
|
||||
|
||||
def _load_patch_module():
|
||||
path = Path(__file__).resolve().parent / "patch_system_reset_image.py"
|
||||
spec = importlib.util.spec_from_file_location("patch_system_reset_image_under_test", path)
|
||||
assert spec is not None and spec.loader is not None
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
return module
|
||||
|
||||
|
||||
ORIGINAL_DISPLAY_WAIT = b'''#!/usr/bin/env bash
|
||||
echo "waiting for magic"
|
||||
for i in {1..200}; do
|
||||
if systemctl is-active --quiet magic && [ -S /tmp/drmfd.sock ]; then
|
||||
break
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
patch_image = _load_patch_module()
|
||||
|
||||
if systemctl is-active --quiet magic && [ -S /tmp/drmfd.sock ]; then
|
||||
echo "magic ready after ${SECONDS}s"
|
||||
else
|
||||
echo "timed out waiting for magic, ${SECONDS}s"
|
||||
fi
|
||||
|
||||
exec /data/continue.sh
|
||||
def test_only_version_runtime_packages_and_factory_install_payloads_are_mutable():
|
||||
assert patch_image.ALLOWED_IMAGE_MUTATIONS == {
|
||||
patch_image.VERSION_PATH_IN_IMAGE,
|
||||
*patch_image.STAR_PILOT_DEPENDENCY_PATHS,
|
||||
*patch_image.LEGACY_RUNTIME_LIBRARY_PATHS,
|
||||
patch_image.SETUP_PATH_IN_IMAGE,
|
||||
patch_image.INSTALLER_PATH_IN_IMAGE,
|
||||
}
|
||||
assert set(patch_image.C3_DEPENDENCY_PATHS) < set(patch_image.STAR_PILOT_DEPENDENCY_PATHS)
|
||||
assert len(patch_image.LEGACY_RUNTIME_LIBRARY_PATHS) == 10
|
||||
assert len(patch_image.LEGACY_RUNTIME_LIBRARY_PATHS) == len(set(patch_image.LEGACY_RUNTIME_LIBRARY_PATHS))
|
||||
|
||||
|
||||
def test_upstream_profile_covers_factory_reset_and_runtime_paths():
|
||||
assert patch_image.UPSTREAM_VERSION == "19.6"
|
||||
assert patch_image.UPSTREAM_RAW_SHA256 == "5b6ce7965904a157fd3a134ccfcb854f9ca5c1cc2a26b7cb80a4fa4e1cc4aaa3"
|
||||
assert set(patch_image.UPSTREAM_REQUIRED_VENV_PATHS) == {"capnp", "numpy", "Crypto", "tqdm", "raylib"}
|
||||
assert set(patch_image.REQUIRED_VENV_PATHS) == {
|
||||
"crcmod", "serial", "kaitaistruct", "cv2", "mapbox_earcut", "jsonrpc", "xattr", "onnx",
|
||||
"aiohttp", "pyaudio", "capnp", "numpy", "Crypto", "tqdm", "raylib",
|
||||
}
|
||||
assert patch_image.CANDIDATE_SITE_PACKAGES_COUNT == (
|
||||
patch_image.UPSTREAM_SITE_PACKAGES_COUNT + len(patch_image.STAR_PILOT_DEPENDENCY_PATHS)
|
||||
)
|
||||
assert len(patch_image.STAR_PILOT_DEPENDENCY_PATHS) == len(set(patch_image.STAR_PILOT_DEPENDENCY_PATHS))
|
||||
assert set(patch_image.PROTECTED_PAYLOAD_HASHES) >= {
|
||||
"/etc/NetworkManager/NetworkManager.conf",
|
||||
"/lib/systemd/system/NetworkManager.service",
|
||||
"/usr/comma/updater",
|
||||
"/usr/comma/reset",
|
||||
"/usr/comma/comma.sh",
|
||||
"/usr/comma/magic.py",
|
||||
}
|
||||
assert set(patch_image.UPSTREAM_FACTORY_INSTALL_HASHES) == {"/usr/comma/installer", "/usr/comma/setup"}
|
||||
assert set(patch_image.PROTECTED_PAYLOAD_HASHES).isdisjoint(patch_image.UPSTREAM_FACTORY_INSTALL_HASHES)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["19.6.1", "19.6.5", "19.6.99"])
|
||||
def test_target_version_accepts_starpilot_revision(version):
|
||||
assert patch_image.validate_target_version(version) == version
|
||||
|
||||
|
||||
@pytest.mark.parametrize("version", ["19.6", "19.6.0", "19.7.1", "20.6.1", "latest"])
|
||||
def test_target_version_rejects_non_revision(version):
|
||||
with pytest.raises(RuntimeError):
|
||||
patch_image.validate_target_version(version)
|
||||
|
||||
|
||||
def test_write_version_fails_closed_if_allowlist_changes(tmp_path, monkeypatch):
|
||||
monkeypatch.setattr(patch_image, "ALLOWED_IMAGE_MUTATIONS", frozenset({"/VERSION", "/usr/comma/setup"}))
|
||||
with pytest.raises(RuntimeError, match="allowlist"):
|
||||
patch_image.write_version("debugfs", tmp_path / "system.img", tmp_path / "VERSION")
|
||||
|
||||
|
||||
def test_add_path_to_image_preserves_runtime_library_symlink(tmp_path, monkeypatch):
|
||||
target = tmp_path / "libavformat.so.58.29.100"
|
||||
target.write_bytes(b"ELF")
|
||||
source = tmp_path / "libavformat.so.58"
|
||||
source.symlink_to(target.name)
|
||||
calls = []
|
||||
|
||||
monkeypatch.setattr(patch_image, "image_path_exists", lambda *_args: False)
|
||||
monkeypatch.setattr(patch_image, "ensure_image_directory", lambda *_args: None)
|
||||
|
||||
def fake_run_debugfs(_debugfs, _image, request, *, write=False):
|
||||
calls.append((request, write))
|
||||
if request.startswith("stat "):
|
||||
return 'Inode: 1 Type: symlink\nFast link dest: "libavformat.so.58.29.100"'
|
||||
return ""
|
||||
|
||||
monkeypatch.setattr(patch_image, "run_debugfs", fake_run_debugfs)
|
||||
patch_image.add_path_to_image("debugfs", tmp_path / "system.img", source, "/usr/local/lib/libavformat.so.58")
|
||||
|
||||
assert ("symlink /usr/local/lib/libavformat.so.58 libavformat.so.58.29.100", True) in calls
|
||||
assert not any(request.startswith("write ") for request, _write in calls)
|
||||
|
||||
|
||||
def _setup_source(member: str) -> str:
|
||||
connectivity = (
|
||||
'request = urllib.request.Request(OPENPILOT_URL, method="HEAD")'
|
||||
if member.endswith("mici_setup.py")
|
||||
else "urllib.request.urlopen(OPENPILOT_URL, timeout=2.0)"
|
||||
)
|
||||
labels = (
|
||||
'LargerSlider("slide to install\\nopenpilot", use_openpilot_callback)\n'
|
||||
'BigPillButton("install openpilot", green=True)\n'
|
||||
'set_text("install openpilot" if not custom_software else "choose software")'
|
||||
if member.endswith("mici_setup.py")
|
||||
else 'ButtonRadio("openpilot", self.checkmark)'
|
||||
)
|
||||
if member.endswith("mici_setup.py"):
|
||||
not_elf = 'self._download_failed_reason = "No custom software found at this URL: " + self.download_url.replace("https://", "", 1)'
|
||||
http_error = 'self._download_failed_reason = "http"'
|
||||
generic_error = 'self._download_failed_reason = "Invalid URL: " + self.download_url.replace("https://", "", 1)'
|
||||
else:
|
||||
not_elf = 'self.download_failed(self.download_url, "No custom software found at this URL.")'
|
||||
http_error = 'self.download_failed(self.download_url, "http")'
|
||||
generic_error = (
|
||||
'error_msg = "Ensure the entered URL is valid, and the device\'s internet connection is good."\n'
|
||||
' self.download_failed(self.download_url, error_msg)'
|
||||
)
|
||||
return f'''OPENPILOT_URL = "https://openpilot.comma.ai"
|
||||
{connectivity}
|
||||
{labels}
|
||||
def download(self, url: str):
|
||||
# autocomplete incomplete URLs
|
||||
if re.match("^([^/.]+)/([^/]+)$", url):
|
||||
url = f"https://installer.comma.ai/{{url}}"
|
||||
|
||||
parsed = urlparse(url, scheme='https')
|
||||
self.download_url = (urlparse(f"https://{{url}}") if not parsed.netloc else parsed).geturl()
|
||||
|
||||
try:
|
||||
import tempfile
|
||||
|
||||
headers = {{"User-Agent": "test"}}
|
||||
req = urllib.request.Request(self.download_url, headers=headers)
|
||||
|
||||
with open(tmpfile, 'wb') as f, urllib.request.urlopen(req, timeout=30) as response:
|
||||
total_size = int(response.headers.get('content-length', 0))
|
||||
is_elf = True
|
||||
if not is_elf:
|
||||
{not_elf}
|
||||
with open(INSTALLER_URL_PATH, "w") as f:
|
||||
f.write(self.download_url)
|
||||
except urllib.error.HTTPError as e:
|
||||
{http_error}
|
||||
except Exception:
|
||||
{generic_error}
|
||||
'''
|
||||
|
||||
|
||||
def test_patch_comma_sh_display_wait_uses_available_display_service():
|
||||
patched = patch_comma_sh_display_wait(ORIGINAL_DISPLAY_WAIT)
|
||||
def test_patch_setup_zipapp_preserves_prefix_and_custom_flow(tmp_path):
|
||||
source = tmp_path / "setup"
|
||||
source.write_bytes(b"#!/usr/bin/env python3\n")
|
||||
with zipfile.ZipFile(source, "a") as setup_zip:
|
||||
for member in patch_image.SETUP_SOURCE_MEMBERS:
|
||||
setup_zip.writestr(member, _setup_source(member))
|
||||
cache_member = str(Path(member).parent / "__pycache__" / f"{Path(member).stem}.cpython-312.pyc")
|
||||
setup_zip.writestr(cache_member, b"stale")
|
||||
setup_zip.writestr("unchanged.txt", b"upstream")
|
||||
os.chmod(source, 0o755)
|
||||
|
||||
assert COMMA_SH_DISPLAY_WAIT_PATCH_MARKER.encode() in patched
|
||||
assert b"systemctl cat magic.service" in patched
|
||||
assert b"systemctl is-active --quiet magic" in patched
|
||||
assert b"systemctl is-active --quiet weston-ready" in patched
|
||||
assert b"[ -S /var/tmp/weston/wayland-0 ]" in patched
|
||||
assert comma_sh_has_expected_display_wait(patched)
|
||||
assert patch_comma_sh_display_wait(patched) == patched
|
||||
destination = tmp_path / "setup.patched"
|
||||
patch_image.patch_setup_zipapp(source, destination)
|
||||
|
||||
assert destination.read_bytes().startswith(b"#!/usr/bin/env python3\n")
|
||||
assert destination.stat().st_mode & 0o777 == 0o755
|
||||
with zipfile.ZipFile(destination) as setup_zip:
|
||||
assert setup_zip.read("unchanged.txt") == b"upstream"
|
||||
assert not any(patch_image.is_setup_cache_member(name) for name in setup_zip.namelist())
|
||||
mici = setup_zip.read(patch_image.SETUP_SOURCE_MEMBERS[0]).decode()
|
||||
tici = setup_zip.read(patch_image.SETUP_SOURCE_MEMBERS[1]).decode()
|
||||
assert 'OPENPILOT_URL = "file:///usr/comma/installer"' in mici
|
||||
assert 'LargerSlider("slide to install\\nStarPilot"' in mici
|
||||
assert "urllib.request.Request(CONNECTIVITY_URL" in mici
|
||||
assert 'ButtonRadio("StarPilot"' in tici
|
||||
assert "urllib.request.urlopen(CONNECTIVITY_URL" in tici
|
||||
for setup_source in (mici, tici):
|
||||
assert "patch_bundled_installer(tmpfile, *self.bundled_installer_target)" in setup_source
|
||||
assert "install_bundled_installer(*bundled_target, self.installer_url)" in setup_source
|
||||
assert 'self.bundled_installer_target = (("firestar5683", "StarPilot") if url == OPENPILOT_URL else None)' in setup_source
|
||||
assert "self.installer_url = (" in setup_source
|
||||
assert "url = OPENPILOT_URL" in setup_source
|
||||
assert "f.write(self.installer_url)" in setup_source
|
||||
assert 'open("/usr/comma/installer", "rb")' in setup_source
|
||||
assert "self.download_url == OPENPILOT_URL" in setup_source
|
||||
assert 'url = f"https://installer.comma.ai/{url}"' not in setup_source
|
||||
|
||||
|
||||
def test_patch_comma_sh_display_wait_rejects_unknown_layout():
|
||||
with pytest.raises(RuntimeError, match="display readiness wait"):
|
||||
patch_comma_sh_display_wait(b"#!/usr/bin/env bash\nexec /data/continue.sh\n")
|
||||
def test_patch_installer_binary_keeps_elf_layout_and_targets_starpilot(tmp_path):
|
||||
source = tmp_path / "installer"
|
||||
source.write_bytes(
|
||||
b"\x7fELF" +
|
||||
b"https://github.com/commaai/openpilot.git?" + b" " * 64 + b"\0" +
|
||||
b"release3?" + b" " * 64 + b"\0tail"
|
||||
)
|
||||
os.chmod(source, 0o755)
|
||||
destination = tmp_path / "installer.patched"
|
||||
|
||||
patch_image.patch_installer_binary(source, destination)
|
||||
|
||||
assert destination.stat().st_size == source.stat().st_size
|
||||
assert destination.stat().st_mode & 0o777 == 0o755
|
||||
data = destination.read_bytes()
|
||||
assert data.count(b"https://github.com/firestar5683/openpilot.git?") == 1
|
||||
assert data.count(b"StarPilot?") == 1
|
||||
assert b"commaai/openpilot" not in data
|
||||
|
||||
|
||||
@pytest.mark.parametrize("slider_text", ["slide to use", "slide to install"])
|
||||
def test_patch_mici_setup_branding_handles_old_and_new_labels(slider_text):
|
||||
original = f'''OPENPILOT_URL = "https://openpilot.comma.ai"
|
||||
self._openpilot_slider = LargerSlider("{slider_text}\\nopenpilot", callback)
|
||||
self._continue_button = BigPillButton("install openpilot", green=True)
|
||||
self._continue_button.set_text("install openpilot" if not custom_software else "choose software")
|
||||
'''.encode()
|
||||
|
||||
patched = patch_setup_branding_script(original, "openpilot/system/ui/mici_setup.py")
|
||||
|
||||
assert b"installer.comma.ai/firestar5683/StarPilot" in patched
|
||||
assert f"{slider_text}\\nstarpilot".encode() in patched
|
||||
assert b"install StarPilot" in patched
|
||||
assert b"install openpilot" not in patched
|
||||
def test_update_manifest_changes_only_system_entry():
|
||||
original = [
|
||||
{"name": "boot", "url": "custom-boot", "hash": "boot-hash"},
|
||||
{"name": "system", "url": "old", "hash": "old", "alt": {"url": "old-alt"}},
|
||||
]
|
||||
updated = patch_image.update_manifest_system_entry(original, "hosted", "new-hash", 123)
|
||||
assert updated[0] == original[0]
|
||||
assert updated[1] == {
|
||||
"name": "system",
|
||||
"url": "hosted",
|
||||
"hash": "new-hash",
|
||||
"hash_raw": "new-hash",
|
||||
"size": 123,
|
||||
"sparse": False,
|
||||
"full_check": False,
|
||||
"has_ab": True,
|
||||
"ondevice_hash": "new-hash",
|
||||
}
|
||||
assert json.dumps(original)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("mode", "expected"), [
|
||||
("100775", "0100775"),
|
||||
("100644", "0100644"),
|
||||
("040755", "040755"),
|
||||
("120777", "0120777"),
|
||||
])
|
||||
def test_format_debugfs_mode(mode, expected):
|
||||
assert format_debugfs_mode(mode) == expected
|
||||
|
||||
|
||||
def test_external_gpu_firmware_matches_tinygrad_requirements():
|
||||
firmware_metadata = Path(__file__).resolve().parents[2] / "tinygrad/runtime/autogen/am/fw.py"
|
||||
hashes = runpy.run_path(firmware_metadata)["hashes"]
|
||||
expected = {filename.removesuffix(".zst"): digest for filename, digest in AMDGPU_FIRMWARE_SHA256.items()}
|
||||
assert all(hashes[filename] == digest for filename, digest in expected.items())
|
||||
|
||||
|
||||
def test_zstd_payload_hash(tmp_path):
|
||||
import hashlib
|
||||
import zstandard
|
||||
|
||||
payload = b"external GPU firmware payload"
|
||||
compressed = tmp_path / "firmware.bin.zst"
|
||||
compressed.write_bytes(zstandard.ZstdCompressor().compress(payload))
|
||||
|
||||
assert sha256_zstd_payload(compressed) == hashlib.sha256(payload).hexdigest()
|
||||
|
||||
|
||||
def test_default_reference_manifest_uses_sibling_openpilot(tmp_path):
|
||||
primary = tmp_path / "starpilot/system/hardware/tici/agnos.json"
|
||||
reference = tmp_path / "openpilot/openpilot/system/hardware/tici/agnos.json"
|
||||
primary.parent.mkdir(parents=True)
|
||||
reference.parent.mkdir(parents=True)
|
||||
primary.write_text("[]")
|
||||
reference.write_text("[]")
|
||||
|
||||
assert find_default_reference_manifest(primary) == reference.resolve()
|
||||
def test_protected_payload_validation_reports_any_drift():
|
||||
patch_image.validate_protected_payloads(dict(patch_image.PROTECTED_PAYLOAD_HASHES))
|
||||
changed = dict(patch_image.PROTECTED_PAYLOAD_HASHES)
|
||||
changed["/usr/comma/reset"] = "0" * 64
|
||||
with pytest.raises(RuntimeError, match="/usr/comma/reset"):
|
||||
patch_image.validate_protected_payloads(changed)
|
||||
|
||||
Executable
+90
@@ -0,0 +1,90 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
EXPECTED_VERSION="${1:-}"
|
||||
REPO_ROOT="${2:-/data/openpilot}"
|
||||
PYTHON_BIN="/usr/local/venv/bin/python3"
|
||||
SITE_PACKAGES="/usr/local/venv/lib/python3.12/site-packages"
|
||||
|
||||
fail() {
|
||||
echo "[FAIL] $*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ -x "$PYTHON_BIN" ]] || fail "managed Python is missing: $PYTHON_BIN"
|
||||
[[ -d "$SITE_PACKAGES" ]] || fail "site-packages is missing: $SITE_PACKAGES"
|
||||
|
||||
actual_version="$(tr -d '\r\n' </VERSION)"
|
||||
if [[ -n "$EXPECTED_VERSION" && "$actual_version" != "$EXPECTED_VERSION" ]]; then
|
||||
fail "AGNOS version is $actual_version, expected $EXPECTED_VERSION"
|
||||
fi
|
||||
|
||||
site_count="$(find "$SITE_PACKAGES" -mindepth 1 -maxdepth 1 -print | wc -l | tr -d ' ')"
|
||||
[[ "$site_count" == "253" ]] || fail "managed venv has $site_count site-packages entries, expected upstream 213 plus 40 additive StarPilot dependencies"
|
||||
|
||||
echo "[CHECK] AGNOS version: $actual_version"
|
||||
echo "[CHECK] managed venv entries: $site_count"
|
||||
|
||||
"$PYTHON_BIN" -c 'import aiohttp, capnp, crcmod, Crypto, cv2, jsonrpc, kaitaistruct, mapbox_earcut, numpy, onnx, pyaudio, raylib, serial, tqdm, xattr; print("[CHECK] runtime imports: ok")'
|
||||
|
||||
[[ "$(readlink /usr/local/lib/libavformat.so.58)" == "libavformat.so.58.29.100" ]]
|
||||
[[ "$(readlink /usr/local/lib/libavcodec.so.58)" == "libavcodec.so.58.54.100" ]]
|
||||
[[ "$(readlink /usr/local/lib/libavutil.so.56)" == "libavutil.so.56.31.100" ]]
|
||||
[[ "$(readlink /usr/local/lib/libswresample.so.3)" == "libswresample.so.3.5.100" ]]
|
||||
echo "[CHECK] legacy prebuilt runtime links: ok"
|
||||
|
||||
"$PYTHON_BIN" - <<'PY'
|
||||
from pathlib import Path
|
||||
import zipfile
|
||||
|
||||
with zipfile.ZipFile("/usr/comma/setup") as setup:
|
||||
for member in ("openpilot/system/ui/mici_setup.py", "openpilot/system/ui/tici_setup.py"):
|
||||
source = setup.read(member).decode("utf-8")
|
||||
assert 'OPENPILOT_URL = "file:///usr/comma/installer"' in source, member
|
||||
assert 'CONNECTIVITY_URL = "https://openpilot.comma.ai"' in source, member
|
||||
assert "StarPilot" in source, member
|
||||
assert "install_bundled_installer(*bundled_target, self.installer_url)" in source, member
|
||||
assert 'self.bundled_installer_target = (("firestar5683", "StarPilot") if url == OPENPILOT_URL else None)' in source, member
|
||||
|
||||
installer = Path("/usr/comma/installer").read_bytes()
|
||||
assert installer.startswith(b"\x7fELF")
|
||||
assert installer.count(b"https://github.com/firestar5683/openpilot.git?") == 1
|
||||
assert installer.count(b"StarPilot?") == 1
|
||||
print("[CHECK] factory StarPilot setup/installer: ok")
|
||||
PY
|
||||
|
||||
if [[ -d "$REPO_ROOT" ]]; then
|
||||
(
|
||||
cd "$REPO_ROOT"
|
||||
"$PYTHON_BIN" -c 'from openpilot.system.manager import manager; print("[CHECK] manager import: ok")'
|
||||
if [[ -f openpilot/selfdrive/pandad/pandad_api_impl.so ]]; then
|
||||
"$PYTHON_BIN" -c 'import openpilot.selfdrive.pandad.pandad_api_impl; print("[CHECK] legacy prebuilt native import: ok")'
|
||||
fi
|
||||
)
|
||||
else
|
||||
echo "[CHECK] manager import: deferred until software is installed"
|
||||
fi
|
||||
|
||||
sha256sum --check --strict <<'HASHES'
|
||||
779db62d2d4c5f8ce504c5d1f2994d34a9f35296d5efb7f3a48cb1e8a0d4778e /etc/NetworkManager/NetworkManager.conf
|
||||
45e653e2f709c027fad41f2d86b70e008b72c6bf4d34590b4765ebe8fe3ea948 /etc/NetworkManager/conf.d/10-globally-managed-devices.conf
|
||||
fb33a80bf8c78b3af004d4b294c47a0139e37742c1d0d5a6a7663c7d1f4a2b48 /lib/systemd/system/NetworkManager.service
|
||||
9df4edbeb5849de03f9c2d691d04646af84a3ef74c2f33be8e73d9281daebe99 /usr/comma/updater
|
||||
97ed6413515d0674442c42ae6e20baccf66dd6bb4ec382ee4cf0cc5ebe84e739 /usr/comma/reset
|
||||
c4416e66b127b31c17d08e6723ad46d12af7683e56626512d66c708b5f347ac9 /usr/comma/magic.py
|
||||
8f6c84e5799c0025f645997ce8e2cbc99ab0232f4be14ee2d95f609ca90c6e02 /usr/local/lib/libcapnp-1.0.2.so
|
||||
c548c10b875b8841637017503ac73b1d466a36926df59fc5382221c021738d90 /usr/local/lib/libkj-1.0.2.so
|
||||
7bf17a186267d1642049929cbda3d34c4c160f727e7d819985b49f5fb07ed05b /usr/local/lib/libavformat.so.58.29.100
|
||||
252b85381ab652736dbea984ad2cab158b43f93641b043475834e3fd0bccd697 /usr/local/lib/libavcodec.so.58.54.100
|
||||
3730dde66fe502d769e06e5faf001a3dc1c30091993d606989f3e077ba2e579a /usr/local/lib/libavutil.so.56.31.100
|
||||
41b0a5e7807506779f2163d23d5120949efaeeaf96fb9b69758551c9af7c7c1a /usr/local/lib/libswresample.so.3.5.100
|
||||
370d154aaf7e1e9ee433c069348ae885036a9d8cc4c8babfbfae12c8d5b3f2e8 /usr/comma/installer
|
||||
934f74ab4b2ac06048418c2857be3a041e192ec03c09979987691c23c91353bd /usr/comma/setup_keys
|
||||
6565acac9eb8167931f6ad50c62254b64f037d21a7944456bd8ec328c7f4af0b /usr/comma/setup
|
||||
bcba2b336cf0ca852786f8a58bbce407e0e9fe952c26fc5d903f6d9a34b44b4f /usr/comma/comma.sh
|
||||
HASHES
|
||||
|
||||
[[ "$(systemctl is-enabled NetworkManager)" == "enabled" ]] || fail "NetworkManager is not enabled"
|
||||
[[ "$(systemctl is-active NetworkManager)" == "active" ]] || fail "NetworkManager is not active"
|
||||
|
||||
echo "[PASS] AGNOS runtime, manager, recovery payloads, and networking validated"
|
||||
@@ -16,6 +16,7 @@ from pathlib import Path
|
||||
REQUIRED_DIRS = [
|
||||
("usr/local/lib", "/usr/local/lib"),
|
||||
("usr/local/include", "/usr/local/include"),
|
||||
("usr/local/venv/lib/python3.12/site-packages/raylib/install", "/usr/local/venv/lib/python3.12/site-packages/raylib/install"),
|
||||
("lib/aarch64-linux-gnu", "/lib/aarch64-linux-gnu"),
|
||||
("usr/lib/aarch64-linux-gnu", "/usr/lib/aarch64-linux-gnu"),
|
||||
("usr/include", "/usr/include"),
|
||||
|
||||
Reference in New Issue
Block a user