Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions docs/BUILDS.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,3 +231,23 @@ recommends Zero 2 W and Pi 3/4/5, and advises against Pi 1, Pi 2 and original
Zero hosts because their processing capacity can cause print stalls. Klipper
also requires double-precision floating-point hardware on low-end hosts. A
phobos software-float executable does not remove those Klipper requirements.

## Moonraker ZIP packages

`tools/release/prepare.py` also wraps each verified source-build artifact in a
native update-manager ZIP. Its stable, platform-specific name is
`phobos-<target-suffix>.zip` (for the Adventurer 5M/Cortex-A7:
`phobos-arm-linux-musleabihf.zip`). The version remains in the release URL and
`release_info.json`, not in the installed executable's filename.

The flat archive contains `phobos` (0755), `release_info.json`, `.version`, and
`LICENSE`. Metadata carries `project_owner`, `project_name`, `version`, and
`asset_name`; that last field lets Phobos select the same architecture on its
next update. The binary bytes are unchanged. ZIP timestamps and permissions are
fixed, and the combined SHA256SUMS covers both raw and ZIP assets.

Extract the verified ZIP into a dedicated application directory such as
`/data/phobos`, never `/data` itself: Moonraker replaces the configured application
directory during a ZIP update. Forge-X's optional updater entry uses that dedicated
directory and an explicit asset name. Selecting/running Phobos as the API daemon
is a separate operation; installing or updating the package does not switch it.
15 changes: 15 additions & 0 deletions docs/PROVENANCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -2530,3 +2530,18 @@ or production-install claim was added. No printer was contacted, no VM or
foreign executable was started, and no image or live database was opened or
changed. Existing releases were not retagged or overwritten. Graphify is absent
from PATH, so the optional graph update was skipped.

### 2026-10-29 — native ZIP release packaging

Added deterministic, platform-specific Moonraker ZIP packages to the verified
release assembler. ZIP payloads retain the source-built executable bytes and
include executable permissions, GPLv3 licensing, version/repository metadata and
a stable architecture-specific `asset_name` used by Phobos's own ZIP updater.
The release manifest now covers raw binaries and ZIPs. Packaging tests check
metadata, permissions, deterministic output and refusal to overwrite artifacts.

For the v0.3.0 Forge-X package, the retained Cortex-A7 source-build output was
checked against the existing published SHA256SUMS (binary SHA256
`0069056fcff9baf8c1edc612869180e863883eba2808b7805a88b1a84a2f30ef`).
The ZIP repackages that exact published payload. A redundant fresh tag rebuild
reached the build helper's 600-second timeout; it was not used as package evidence.
36 changes: 36 additions & 0 deletions tools/release/prepare.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@
import argparse
import csv
import hashlib
import json
import stat
import zipfile
import re
import shutil
import subprocess
Expand Down Expand Up @@ -50,6 +53,35 @@ def absolute_link(match: re.Match[str]) -> str:
return re.sub(r"\]\(([^)]+)\)", absolute_link, notes.read_text())


def package_zip(root: Path, binary: Path, output: Path, version: str, repository: str) -> Path:
"""Wrap an already verified source-build artifact for Moonraker's ZIP updater."""
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version):
raise ValueError("invalid package version")
if (not re.fullmatch(r"[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+", repository)
or any(part in (".", "..") for part in repository.split("/"))):
raise ValueError("invalid repository name")
if not re.fullmatch(rf"phobos-v{re.escape(version)}-[A-Za-z0-9_.+-]+", binary.name):
raise ValueError("invalid binary asset name")
owner, project = repository.split("/")
name = "phobos-" + binary.name.removeprefix(f"phobos-v{version}-") + ".zip"
metadata = {"project_owner": owner, "project_name": project, "version": "v" + version, "asset_name": name}
destination = output / name
entries = {
"phobos": (binary.read_bytes(), 0o755),
"release_info.json": ((json.dumps(metadata, indent=2) + "\n").encode(), 0o644),
".version": (("v" + version + "\n").encode(), 0o644),
"LICENSE": ((root / "LICENSE").read_bytes(), 0o644),
}
with zipfile.ZipFile(destination, "x", compression=zipfile.ZIP_DEFLATED, compresslevel=9) as archive:
for name, (content, mode) in entries.items():
entry = zipfile.ZipInfo(name, date_time=(1980, 1, 1, 0, 0, 0))
entry.create_system = 3
entry.external_attr = (stat.S_IFREG | mode) << 16
entry.compress_type = zipfile.ZIP_DEFLATED
archive.writestr(entry, content)
return destination


def assemble(root: Path, source: Path, output: Path, repository: str) -> None:
if not output.resolve().is_relative_to((root / ".scratch").resolve()):
raise ValueError("output must be inside project .scratch")
Expand Down Expand Up @@ -91,6 +123,10 @@ def assemble(root: Path, source: Path, output: Path, repository: str) -> None:
raise ValueError(f"asset checksum mismatch: {profile}")
shutil.copyfile(directory / asset, output / asset)
checksums.append(f"{actual} {asset}\n")
zipped = package_zip(root, output / asset, output, version, repository)
with zipped.open("rb") as stream:
zipped_hash = hashlib.file_digest(stream, "sha256").hexdigest()
checksums.append(f"{zipped_hash} {zipped.name}\n")
metadata.append(entry)
(output / "SHA256SUMS").write_text("".join(checksums))
with (output / "profiles.tsv").open("w") as stream:
Expand Down
30 changes: 29 additions & 1 deletion tools/release/tests/test_prepare.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import hashlib
import json
import zipfile
import subprocess
from pathlib import Path

Expand Down Expand Up @@ -33,7 +35,7 @@ def test_assemble_copies_every_asset_and_combines_manifests(tmp_path: Path) -> N
root, source = fixture_artifacts(tmp_path)
output = tmp_path / "output"
prepare.assemble(root, source, output, "maci0/phobos")
assert len((output / "SHA256SUMS").read_text().splitlines()) == 8
assert len((output / "SHA256SUMS").read_text().splitlines()) == 16
assert len((output / "profiles.tsv").read_text().splitlines()) == 9
version = prepare.package_version(root)
assert (output / f"phobos-v{version}-armv7-soft").read_bytes() == b"armv7-soft\n"
Expand Down Expand Up @@ -112,3 +114,29 @@ def test_notes_reject_invalid_repository_and_missing_target(tmp_path: Path) -> N
notes.write_text("[missing](../../missing.md)\n")
with pytest.raises(ValueError, match="target is missing"):
prepare.release_notes(tmp_path, "1.2.3", "maci0/phobos")


def test_zip_package_is_deterministic_executable_and_moonraker_compatible(tmp_path: Path) -> None:
root = prepare.project_root(Path(__file__).resolve())
version = prepare.package_version(root)
binary = tmp_path / f"phobos-v{version}-arm-linux-musleabihf"
binary.write_bytes(b"verified source-build fixture")
first = tmp_path / "first"
second = tmp_path / "second"
first.mkdir()
second.mkdir()
a = prepare.package_zip(root, binary, first, version, "maci0/phobos")
b = prepare.package_zip(root, binary, second, version, "maci0/phobos")
assert a.read_bytes() == b.read_bytes()
with zipfile.ZipFile(a) as archive:
assert archive.testzip() is None
assert set(archive.namelist()) == {"phobos", "release_info.json", ".version", "LICENSE"}
assert archive.read("phobos") == binary.read_bytes()
assert (archive.getinfo("phobos").external_attr >> 16) & 0o777 == 0o755
assert json.loads(archive.read("release_info.json")) == {
"project_owner": "maci0", "project_name": "phobos", "version": f"v{version}",
"asset_name": "phobos-arm-linux-musleabihf.zip"
}
assert archive.read(".version") == f"v{version}\n".encode()
with pytest.raises(FileExistsError):
prepare.package_zip(root, binary, first, version, "maci0/phobos")
Loading