diff --git a/docs/linux-vm.md b/docs/linux-vm.md index 144dc82..b1988a0 100644 --- a/docs/linux-vm.md +++ b/docs/linux-vm.md @@ -208,3 +208,19 @@ VM gate. References: [Debian Cloud images](https://wiki.debian.org/Cloud), [Debian cloud image comparison](https://wiki.debian.org/Cloud/SystemsComparison), and the [QEMU invocation manual](https://www.qemu.org/docs/master/system/invocation.html). + +## Package-only pinned image + +The source-provisioned VM above remains the development and diagnostic gate. +The first package-only image recipe is in +[`images/debian-13/README.md`](../images/debian-13/README.md). It builds five +native Debian packages outside the image, verifies the already-evidenced Debian +13.6 ARM64 generic image by filename, byte size, SHA-256, and SHA-512, resolves +runtime dependencies only against an immutable Debian snapshot, and installs +the packages into a fresh QCOW2 without copying the repository or a compiler. + +This is currently an implemented, statically checked recipe rather than a +completed image gate. It must not replace the source-provisioned VM evidence +until a Debian 13 builder has produced the packages, the exact assembled image +has booted through `sos-session.target`, and the package lifecycle and +boot-session verifiers have passed against that artifact. diff --git a/docs/progress.md b/docs/progress.md index 7a19df9..9326ea0 100644 --- a/docs/progress.md +++ b/docs/progress.md @@ -8997,3 +8997,159 @@ SHA-256 run its separate one-sideload Core 1 no-Zygote readiness, exact Pi authority, credential-clear, leak/crash/AVC, manifest, and soak gate. No Core hardware claim is made here. + +## 2026-08-18 — Native Debian packages and pinned reference-image recipe + +**Goal:** Replace source-tree mutation as the Linux release boundary with +versioned Debian packages, then define an immutable-input Debian 13 image that +contains no source checkout or compiler. + +**Changed:** Added deterministic builders for `sos-runtime`, `sos-agent`, +`sos-desktop-session`, `sos-appliance-session`, and `sos-image-config`. +The agent package carries the already pinned Node 24.18.0 runtime privately; +shared-library dependencies come from `dpkg-shlibdeps`; package timestamps use +`SOURCE_DATE_EPOCH`; and one layout renderer converts the development +`/usr/local` installation into package-owned `/usr` paths. Added first-boot +secret/revision initialization and a Debian 13 image recipe locked to the +previously evidenced ARM64 generic image and the `20260809T000000Z` Debian +archive snapshot. The image assembler installs only local SOS packages, +resolves dependencies from that snapshot, removes cloned identity, and emits an +adjacent input/output hash manifest. Added a Podman/Docker builder based on the +multi-architecture Debian 13.6 slim manifest digest +`sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258`; +it uses the same Debian snapshot, Rust 1.95.0, and verified Node 24.18.0 while +isolating Cargo targets, npm caches, and Linux agent staging from the host +checkout. + +**Evidence:** `bash -n` and ShellCheck passed over the new builders, +initializer, renderer, host test, and modified login launchers; +`./tests/linux-packaging-host-test.sh` returned +`linux_packaging_host_test_passed architecture=arm64 +snapshot=20260809T000000Z`; and `git diff --check` passed. These static checks +cover rendered paths, all five package boundaries, lock syntax, and the +prohibition on source installer/Cargo/npm use inside the image. The Debian +builder manifest digest was queried from Docker Registry for +`debian:13.6-slim`; its recorded platform manifests are +`sha256:38a76d01668772e381ad2826d876627c89e7133e2f8a0f5d567306798b0f2a16` +for AMD64 and +`sha256:c94f5ddd41327aa2d4a7cfba7889056c02936182fd76a513fec6160c97181fc0` +for ARM64. + +Podman 6.1.0 built final ARM64 builder image +`f7a4b4c86aa9f9671f50ec2bc4d79260e3efef30851d95657bc50759b45533b1` +in an AppleHV VM configured with 8 CPUs, 12 GiB RAM, and a 100 GiB disk. +The package build used source revision +`e05f91bb6f0b0a9299b914138d6cd0966b9c82d5-dirty`, source epoch +`1787036064`, and two Cargo jobs. The locked release build completed in +3 minutes 52 seconds; npm installed/audited 102 packages with zero +vulnerabilities; and `artifacts/linux-packages/` contains 46,185,773 logical +bytes: + +- `sos-runtime_0.1.0_arm64.deb`: 7,716,860 bytes, SHA-256 + `be708f2c486ba35e10bd1f894920b7719f2931018bbeb09f4a8614a596e5ec78`; +- `sos-agent_0.1.0_arm64.deb`: 38,459,692 bytes, SHA-256 + `5e47d86a734d8f2695b99924e95195a8d6569b5bd2c0fa5b7427f7ee709bfba6`; +- `sos-desktop-session_0.1.0_all.deb`: 3,824 bytes, SHA-256 + `a76db9f14efe276d9e540af8489768cb7a2fbe6476a8f185337e3895b8734999`; +- `sos-appliance-session_0.1.0_all.deb`: 2,832 bytes, SHA-256 + `63055a8b9164b5da7b026278244debd48cecd3fe7d73025f895e5a632bfaa767`; +- `sos-image-config_0.1.0_all.deb`: 2,080 bytes, SHA-256 + `20ac338e4afa0715a4dc0bb20595d225654c17cb0e11f184012a3cc229247527`; +- `SHA256SUMS`: 485 bytes, SHA-256 + `2f98fa9ca9d9887017f5695a3448beb422b24efb09f48d13c3371bbfaf67535c`. + +Host `shasum -a 256 -c` independently accepted every package. A fresh +digest-pinned Debian 13.6 slim container resolved and installed the package +closure from snapshot `20260809T000000Z`; all six native executables had no +missing `ldd` dependency; `systemd-analyze verify` accepted the session, agent, +target, and image-initializer units; package ownership resolved to the intended +five packages; and private Node reported `v24.18.0`. The first-boot initializer +created revision +`31f8e1d31b6e2c91a8a0b0829e5f29934440c64ed8f535bb86d81a5a836c49e5`, +a 64-byte shell token, and retained the same revision on a second invocation. +A second same-input package build was byte-for-byte identical for all five +`.deb` files and `SHA256SUMS`; its duplicate output was removed. This proves +same-builder package reproducibility, not yet independent-builder +reproducibility. The clean install and initialization procedure is retained as +`SOS_CONTAINER_ENGINE=podman ./tools/test-linux-packages-container`. + +Two useful failures preceded PASS. The first container run could not resolve +the Delta worktree's external Git administration directory; the wrapper now +captures revision, dirty state, and source epoch on the host. The first Rust +attempt then ran in the user's initial 2 GiB Podman VM and `rustc` compiling +`wayland-protocols` terminated with signal 9. Kernel OOM evidence was +unavailable after the VM stopped, but resizing that same VM to 12 GiB and +limiting Cargo to two jobs eliminated the failure. Neither failed attempt left +partial packages. + +**Decision:** Accept the ARM64 container build, package ownership/dependency +closure, clean install, initializer idempotence, and same-builder package +reproducibility. Continue with the package-only image architecture. This is a +package PASS, not an image boot, physical-hardware, update, or rollback claim; +keep the source-provisioned Debian VM as the accepted runtime gate until the +new image passes independently. + +**Open risks / next gate:** Build the exact ARM64 QCOW2 from these package +hashes, record its byte size and SHA-256, boot it, and adapt the boot-session +verifier so it proves package ownership, machine-identity regeneration, +first-boot initialization, upgrade/reinstall behavior, and SOS readiness +without a guest source tree. Independent-builder package reproduction, +byte-for-byte QCOW2 reproducibility, rollback/A-B layout, signed repository +metadata, AMD64 base-image identity, physical Linux hardware, and installer +media remain open. + +## 2026-08-18 — Pinned ARM64 live-ISO assembly attempt + +**Goal:** Assemble the five verified Debian packages into ARM64 UEFI live media +that the owner can boot in a VM, while retaining the package hashes and pinned +`20260809T000000Z` Debian snapshot as the only software inputs. + +**Changed:** Added a digest-pinned Debian 13.6 live-build container, a +package-only live-build recipe, and a Podman/Docker host wrapper. The recipe +selects an ARM64 GRUB EFI hybrid ISO, installs the SOS package set, enables the +SOS system session, removes cloned machine identity, and emits an ISO structure +inspection plus an atomic input/output manifest. Snapshot APT calls now disable +only `Valid-Until` enforcement, disable the absent `trixie-updates` suite, use +bounded 30-second HTTP/HTTPS attempts with five retries and no HTTP pipelining, +and omit live-build's generic hardware-firmware bundles because virtio VM +devices do not require them. + +**Evidence:** Syntax checks, ShellCheck, +`./tests/linux-packaging-host-test.sh`, and `git diff --check` pass. The first +full build reached chroot archive setup but rejected the pinned security and +updates metadata after its `Valid-Until` timestamp. Passing +`Acquire::Check-Valid-Until=false` through live-build's own `--apt-options` and +disabling updates allowed the next run to pass that phase, validate all five +local package hashes, and begin package installation. That run was interrupted +before an artifact was produced. A subsequent owner-run attempt selected 163 +packages and requested 568 MB, including 522 MB from the snapshot. Repeated +runtime samples showed the container alive but unchanged at 127.9 MB network +input and 787.8 MB block output, with APT's HTTP method waiting and no counter +movement over the sampled intervals. Dependency inspection showed live-build's +default firmware handling had expanded the VM image with broad AMD, Intel, +Nvidia, Wi-Fi, and device firmware; that default is now disabled. + +The revised detached build used builder image +`92ffa6cbdef931bfef2921f3d1fffd3a1cc7b900533fa52bf5480964f2a273c5`, +validated the five package hashes, and completed live-build's binary stage. +`xorriso` reported a successfully written 236,153-sector hybrid ISO in the +temporary build volume, followed by `P: Build completed successfully`. The +container and recorded host wrapper had already exited when inspected, however, +and neither the container entrypoint's `linux_live_iso_built` marker nor the host +wrapper's `linux_iso_container_build_passed` marker appears in the closed log. +The host output directory is empty and the temporary volume has been removed, +so the built bytes, size, and SHA-256 were not retained. No bootable evidence +artifact exists from these attempts. + +**Decision:** Continue with package-only ARM64 live media, bounded snapshot +fetches, and no generic firmware bundle. The expiry failure is fixed, but ISO +assembly remains unproven until a clean build completes and its manifest and +structure inspection pass. + +**Open risks / next gate:** Diagnose why the successful `lb build` did not +continue through the entrypoint's output-copy and inspection statements, then +rerun the revised recipe. Record the final ISO path, byte size, SHA-256, builder +identity, package closure, and elapsed build time; then boot it as ARM64 UEFI +media and verify SOS readiness, graphics/input, package ownership, first-boot +identity and secret generation, and absence of relevant service failures. A +container build or ISO structure check alone is not boot evidence. diff --git a/images/debian-13/README.md b/images/debian-13/README.md new file mode 100644 index 0000000..33810b9 --- /dev/null +++ b/images/debian-13/README.md @@ -0,0 +1,129 @@ +# Pinned Debian 13 SOS reference image + +This recipe creates a package-only development appliance image. It is a +reference image, not yet the SOS production distribution or an installer ISO. +The image assembler never copies a source tree, runs Cargo/npm, or invokes the +source installer inside the image. + +## Inputs + +- the exact official Debian 13 ARM64 `generic` QCOW2 named and hashed in + `base-images.lock`; +- one same-version build of each SOS package produced by + `tools/build-linux-packages`; +- the immutable Debian archive timestamp in `base-images.lock`; +- this recipe revision. + +The first lock deliberately contains only the Debian 13.6 ARM64 artifact that +has existing VM evidence. Package building also supports Debian `amd64`, but an +AMD64 image must not be claimed as pinned until its official filename, size, +SHA-256, and SHA-512 are added and independently checked. + +## Build packages + +The preferred host entry point uses Podman or Docker: + +```sh +./tools/build-linux-packages-container 0.1.0 +``` + +It builds the environment from the digest-pinned Debian 13.6 slim image, +switches APT to the same immutable Debian snapshot as the image recipe, +installs Rust 1.95.0 and Node 24.18.0, bind-mounts the repository, and invokes +the native package builder as the host UID. Cargo targets and package-manager +caches stay below ignored `.cache/linux-package-builder`; Linux `node_modules` +are built in a temporary staging tree rather than the host source directory. + +Set `SOS_CONTAINER_ENGINE=podman` or `SOS_CONTAINER_ENGINE=docker` to select an +engine explicitly. `SOS_LINUX_PACKAGE_PLATFORM=linux/arm64` or +`linux/amd64` selects the package architecture when the engine supports that +platform; cross-architecture builds may require emulation and are not a +substitute for the architecture's boot gate. + +The wrapper defaults to two Cargo jobs because release LTO exceeded the +12 GiB Podman VM when Cargo used unrestricted parallelism. Override this only +when the builder has measured headroom: + +```sh +SOS_CARGO_BUILD_JOBS=1 \ + ./tools/build-linux-packages-container 0.1.0 +``` + +The lower-level command remains available when already running inside a clean +Debian 13 builder: + +```sh +./tools/build-linux-packages 0.1.0 +``` + +The command creates five `.deb` files and `SHA256SUMS` under the ignored +`artifacts/linux-packages` directory: + +- `sos-runtime`; +- `sos-agent`, including Node 24.18.0 as a private runtime; +- `sos-desktop-session`; +- `sos-appliance-session`; +- `sos-image-config`. + +The native package builder rejects non-Debian-13 environments, derives native +shared-library dependencies with `dpkg-shlibdeps`, verifies the Node archive +digest, and normalizes package timestamps with `SOURCE_DATE_EPOCH`. + +Verify the package dependency closure, installed paths, systemd units, private +Node runtime, package ownership, and idempotent first-boot initialization in a +fresh pinned Debian container: + +```sh +SOS_CONTAINER_ENGINE=podman ./tools/test-linux-packages-container +``` + +## Assemble a live ISO + +For a VM-visible boot gate, build an ARM64 Debian Live hybrid ISO from the same +five packages: + +```sh +SOS_CONTAINER_ENGINE=podman ./tools/build-linux-iso-container +``` + +The live builder runs privileged inside the Podman VM because Debian +`live-build` uses chroots and mounts. Its build tree is an ephemeral Podman +volume; only the ISO, El Torito/filesystem inspection report, and deterministic +input/output manifest are copied to ignored `artifacts/linux-iso`. The ISO uses +GRUB EFI, boots the live system into `sos-session.target`, carries no source +tree or compiler, and generates its machine identity, shell token, and initial +experience revision at boot. + +## Assemble the image + +On a Linux host with QEMU tools and libguestfs: + +```sh +./images/debian-13/build \ + /path/to/debian-13-generic-arm64.qcow2 \ + artifacts/linux-packages +``` + +The builder verifies both base-image digests and its byte size before writing +anything. `virt-customize` switches APT to the immutable Debian snapshot, +installs only the five local SOS packages plus dependencies from that snapshot, +selects `sos-session.target`, removes package caches and device identity, and +closes the image. The adjacent `.manifest` records the recipe revision, package +hashes, base identity, output byte size, and output SHA-256. + +The first boot generates a unique shell token and machine identity, creates the +service-owned state directories, and installs the stock immutable experience +revision before the compositor session starts. Authentication credentials and +SSH host keys are never cloned into the image. + +## Current confidence boundary + +The recipe is functionally pinned: its base, archive timestamp, SOS package +payloads, and policy are immutable inputs. Byte-for-byte image reproduction is +not yet claimed because QCOW2/libguestfs allocation and filesystem metadata +have not been normalized and compared across two independent builders. + +The ARM64 package build, clean-container install, first-boot initializer, and +same-builder byte reproducibility checks have passed. The next gate is to +assemble the image, boot the exact artifact in QEMU, and adapt the boot-session +verifier to consume the installed packages without a guest source tree. diff --git a/images/debian-13/base-images.lock b/images/debian-13/base-images.lock new file mode 100644 index 0000000..9db3701 --- /dev/null +++ b/images/debian-13/base-images.lock @@ -0,0 +1,9 @@ +# Immutable base accepted by the first SOS Debian 13 image recipe. +# Add another architecture only after recording the official artifact's exact +# filename and both independently verified digests. +SOS_IMAGE_ARCH=arm64 +SOS_IMAGE_BASE_FILENAME=debian-13-generic-arm64.qcow2 +SOS_IMAGE_BASE_SIZE=428736512 +SOS_IMAGE_BASE_SHA256=0e68f071dec0215f5d8c7e6f51898213951a6c1a4859f1b980fb4d479255e2bc +SOS_IMAGE_BASE_SHA512=e8ed94e83edded072c66b8871beff8243e0b846ac53980847e2ae44c6d47a8a55579181390b6c85939e85e2a821014ae87e9684930c0509a045212753c8d7916 +SOS_IMAGE_DEBIAN_SNAPSHOT=20260809T000000Z diff --git a/images/debian-13/build b/images/debian-13/build new file mode 100755 index 0000000..f968921 --- /dev/null +++ b/images/debian-13/build @@ -0,0 +1,121 @@ +#!/usr/bin/env bash + +set -euo pipefail + +image_recipe_root="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +image_repo_root="$(cd "$image_recipe_root/../.." && pwd)" + +image_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -ge 2 && "$#" -le 3 ]] || image_fail \ + "usage: images/debian-13/build [output-qcow2]" +image_base="$1" +image_packages="$2" +image_output="${3:-$image_repo_root/artifacts/linux-image/sos-debian13-arm64.qcow2}" +image_manifest="$image_output.manifest" + +[[ "$(uname -s)" == Linux ]] || image_fail "image assembly requires Linux with libguestfs" +for image_command in dpkg-deb git qemu-img sha256sum sha512sum stat virt-customize; do + command -v "$image_command" >/dev/null 2>&1 || \ + image_fail "required command not found: $image_command" +done +[[ -f "$image_base" ]] || image_fail "base image not found: $image_base" +[[ -d "$image_packages" ]] || image_fail "package directory not found: $image_packages" +[[ ! -e "$image_output" && ! -e "$image_manifest" ]] || \ + image_fail "output already exists: $image_output" + +# shellcheck source=/dev/null +source "$image_recipe_root/base-images.lock" +[[ "$(basename "$image_base")" == "$SOS_IMAGE_BASE_FILENAME" ]] || \ + image_fail "expected base filename $SOS_IMAGE_BASE_FILENAME" +[[ "$(stat -c '%s' "$image_base")" == "$SOS_IMAGE_BASE_SIZE" ]] || \ + image_fail "base image byte size does not match lock" +[[ "$(sha256sum "$image_base" | awk '{print $1}')" == "$SOS_IMAGE_BASE_SHA256" ]] || \ + image_fail "base image SHA-256 does not match lock" +[[ "$(sha512sum "$image_base" | awk '{print $1}')" == "$SOS_IMAGE_BASE_SHA512" ]] || \ + image_fail "base image SHA-512 does not match lock" + +image_work="$(mktemp -d -t sos-image.XXXXXX)" +image_incomplete=true +image_manifest_tmp="" +image_cleanup() { + rm -rf -- "$image_work" + [[ -z "$image_manifest_tmp" ]] || rm -f -- "$image_manifest_tmp" + if [[ "$image_incomplete" == true ]]; then + rm -f -- "$image_output" "$image_manifest" + fi +} +trap image_cleanup EXIT +mkdir -p "$image_work/sos-image-bundle/packages" "$(dirname "$image_output")" +install -m 0755 "$image_recipe_root/configure-root" \ + "$image_work/sos-image-bundle/configure-root" + +image_version="" +for image_name in \ + sos-runtime \ + sos-agent \ + sos-desktop-session \ + sos-appliance-session \ + sos-image-config; do + image_matches=("$image_packages"/"${image_name}_"*.deb) + [[ "${#image_matches[@]}" -eq 1 && -f "${image_matches[0]}" ]] || \ + image_fail "expected exactly one package for $image_name" + image_package="${image_matches[0]}" + [[ "$(dpkg-deb --field "$image_package" Package)" == "$image_name" ]] || \ + image_fail "package metadata mismatch: $image_package" + image_package_version="$(dpkg-deb --field "$image_package" Version)" + if [[ -z "$image_version" ]]; then + image_version="$image_package_version" + fi + [[ "$image_package_version" == "$image_version" ]] || \ + image_fail "SOS package versions do not agree" + image_package_arch="$(dpkg-deb --field "$image_package" Architecture)" + [[ "$image_package_arch" == all || "$image_package_arch" == "$SOS_IMAGE_ARCH" ]] || \ + image_fail "package architecture does not match $SOS_IMAGE_ARCH: $image_package" + install -m 0644 "$image_package" "$image_work/sos-image-bundle/packages/" +done + +qemu-img convert -f qcow2 -O qcow2 "$image_base" "$image_output" +virt-customize \ + --format qcow2 \ + --add "$image_output" \ + --network \ + --copy-in "$image_work/sos-image-bundle:/tmp" \ + --run-command "/tmp/sos-image-bundle/configure-root $SOS_IMAGE_DEBIAN_SNAPSHOT" +qemu-img check "$image_output" + +image_revision="$(git -C "$image_repo_root" rev-parse HEAD)" +if [[ -n "$(git -C "$image_repo_root" status --short)" ]]; then + image_revision="$image_revision-dirty" +fi +image_manifest_tmp="$(mktemp "$(dirname "$image_manifest")/.sos-image-manifest.XXXXXX")" +{ + printf 'format=sos-linux-image-manifest-v1\n' + printf 'recipe_revision=%s\n' "$image_revision" + printf 'package_version=%s\n' "$image_version" + printf 'architecture=%s\n' "$SOS_IMAGE_ARCH" + printf 'debian_snapshot=%s\n' "$SOS_IMAGE_DEBIAN_SNAPSHOT" + printf 'base_path=%s\n' "$image_base" + printf 'base_size=%s\n' "$SOS_IMAGE_BASE_SIZE" + printf 'base_sha256=%s\n' "$SOS_IMAGE_BASE_SHA256" + printf 'base_sha512=%s\n' "$SOS_IMAGE_BASE_SHA512" + for image_package in "$image_work"/sos-image-bundle/packages/*.deb; do + printf 'package=%s size=%s sha256=%s\n' \ + "$(basename "$image_package")" \ + "$(stat -c '%s' "$image_package")" \ + "$(sha256sum "$image_package" | awk '{print $1}')" + done + printf 'image_path=%s\n' "$image_output" + printf 'image_size=%s\n' "$(stat -c '%s' "$image_output")" + printf 'image_sha256=%s\n' "$(sha256sum "$image_output" | awk '{print $1}')" +} >"$image_manifest_tmp" +chmod 0644 "$image_manifest_tmp" +mv -- "$image_manifest_tmp" "$image_manifest" +image_manifest_tmp="" +image_incomplete=false + +printf 'linux_image_built version=%s output=%s manifest=%s\n' \ + "$image_version" "$image_output" "$image_manifest" diff --git a/images/debian-13/configure-root b/images/debian-13/configure-root new file mode 100755 index 0000000..42372a1 --- /dev/null +++ b/images/debian-13/configure-root @@ -0,0 +1,73 @@ +#!/usr/bin/env bash + +set -euo pipefail + +configure_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -eq 1 ]] || configure_fail "usage: configure-root " +configure_snapshot="$1" +[[ "$configure_snapshot" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || \ + configure_fail "invalid Debian snapshot identifier: $configure_snapshot" +[[ -r /etc/os-release ]] || configure_fail "missing /etc/os-release" +# shellcheck source=/dev/null +source /etc/os-release +[[ "${ID:-}" == debian && "${VERSION_ID:-}" == 13 ]] || \ + configure_fail "expected Debian 13, found ${ID:-unknown} ${VERSION_ID:-unknown}" + +rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources +cat >/etc/apt/sources.list </etc/apt/apt.conf.d/99sos-snapshot <<'APT' +Acquire::Check-Valid-Until "false"; +APT + +configure_policy_created=false +if [[ ! -e /usr/sbin/policy-rc.d ]]; then + printf '#!/bin/sh\nexit 101\n' >/usr/sbin/policy-rc.d + chmod 0755 /usr/sbin/policy-rc.d + configure_policy_created=true +fi +configure_cleanup() { + if [[ "$configure_policy_created" == true ]]; then + rm -f /usr/sbin/policy-rc.d + fi +} +trap configure_cleanup EXIT + +apt-get update +DEBIAN_FRONTEND=noninteractive apt-get install -y \ + /tmp/sos-image-bundle/packages/*.deb + +for configure_package in \ + sos-runtime \ + sos-agent \ + sos-desktop-session \ + sos-appliance-session \ + sos-image-config; do + [[ "$(dpkg-query -W -f='${db:Status-Abbrev}' "$configure_package")" == "ii " ]] || \ + configure_fail "package did not install cleanly: $configure_package" +done +for configure_forbidden in cargo rustc npm cc c++ gcc g++ make; do + ! command -v "$configure_forbidden" >/dev/null 2>&1 || \ + configure_fail "development command remains in image: $configure_forbidden" +done +[[ ! -e /root/sos && ! -e /home/sos/sos ]] || \ + configure_fail "source checkout remains in image" + +systemctl enable sos-agent.target +systemctl set-default sos-session.target + +apt-get clean +rm -rf /var/lib/apt/lists/* /tmp/sos-image-bundle +if command -v cloud-init >/dev/null 2>&1; then + cloud-init clean --logs --seed +fi +rm -f /etc/ssh/ssh_host_* /var/lib/dbus/machine-id +: >/etc/machine-id + +printf 'sos_image_root_configured snapshot=%s\n' "$configure_snapshot" diff --git a/packaging/debian-live/Containerfile b/packaging/debian-live/Containerfile new file mode 100644 index 0000000..c58b9fb --- /dev/null +++ b/packaging/debian-live/Containerfile @@ -0,0 +1,36 @@ +ARG DEBIAN_LIVE_IMAGE=debian:13.6-slim@sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258 +FROM ${DEBIAN_LIVE_IMAGE} + +ARG DEBIAN_FRONTEND=noninteractive +ARG DEBIAN_SNAPSHOT=20260809T000000Z + +RUN rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources \ + && printf '%s\n' \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT} trixie main contrib non-free-firmware" \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT} trixie-security main contrib non-free-firmware" \ + > /etc/apt/sources.list \ + && printf '%s\n' \ + 'Acquire::Check-Valid-Until "false";' \ + 'Acquire::Retries "3";' \ + > /etc/apt/apt.conf.d/99sos-snapshot \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + ca-certificates \ + debootstrap \ + dosfstools \ + dpkg-dev \ + file \ + grub-common \ + grub-efi-arm64-bin \ + live-build \ + mtools \ + rsync \ + squashfs-tools \ + xorriso \ + && rm -rf /var/lib/apt/lists/* + +COPY build-live-iso /usr/local/bin/sos-build-live-iso +RUN chmod 0755 /usr/local/bin/sos-build-live-iso + +WORKDIR /build +ENTRYPOINT ["/usr/local/bin/sos-build-live-iso"] diff --git a/packaging/debian-live/build-live-iso b/packaging/debian-live/build-live-iso new file mode 100755 index 0000000..192f60f --- /dev/null +++ b/packaging/debian-live/build-live-iso @@ -0,0 +1,123 @@ +#!/usr/bin/env bash + +set -euo pipefail + +live_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +live_snapshot="${SOS_DEBIAN_SNAPSHOT:?SOS_DEBIAN_SNAPSHOT is required}" +live_packages="${SOS_PACKAGE_DIRECTORY:-/packages}" +live_output="${SOS_ISO_OUTPUT_DIRECTORY:-/output}" +[[ "$live_snapshot" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] || \ + live_fail "invalid Debian snapshot: $live_snapshot" +[[ -d "$live_packages" ]] || live_fail "package directory not found: $live_packages" +[[ -d "$live_output" ]] || live_fail "output directory not found: $live_output" + +cd "$live_packages" +sha256sum --check SHA256SUMS +for live_package_name in \ + sos-runtime \ + sos-agent \ + sos-desktop-session \ + sos-appliance-session \ + sos-image-config; do + live_package_matches=("${live_package_name}_"*.deb) + [[ "${#live_package_matches[@]}" -eq 1 && -f "${live_package_matches[0]}" ]] || \ + live_fail "expected exactly one package for $live_package_name" + live_package_arch="$(dpkg-deb --field "${live_package_matches[0]}" Architecture)" + [[ "$live_package_arch" == arm64 || "$live_package_arch" == all ]] || \ + live_fail "package is not ARM64-compatible: ${live_package_matches[0]}" +done + +rm -rf /build/* +cd /build +lb config \ + --mode debian \ + --architectures arm64 \ + --distribution trixie \ + --archive-areas "main contrib non-free-firmware" \ + --mirror-bootstrap "http://snapshot.debian.org/archive/debian/$live_snapshot/" \ + --mirror-chroot "http://snapshot.debian.org/archive/debian/$live_snapshot/" \ + --mirror-chroot-security "http://snapshot.debian.org/archive/debian-security/$live_snapshot/" \ + --mirror-binary "http://snapshot.debian.org/archive/debian/$live_snapshot/" \ + --mirror-binary-security "http://snapshot.debian.org/archive/debian-security/$live_snapshot/" \ + --binary-images iso-hybrid \ + --bootloaders grub-efi \ + --debian-installer none \ + --apt-options "--yes -o Acquire::Check-Valid-Until=false -o Acquire::Retries=5 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30" \ + --apt-pipeline 0 \ + --apt-recommends false \ + --apt-secure true \ + --firmware-binary false \ + --firmware-chroot false \ + --updates false \ + --iso-volume SOS_DEBIAN13 \ + --image-name sos-debian13-live-arm64 \ + --bootappend-live "boot=live components hostname=sos-debian13 username=sos quiet" + +mkdir -p \ + config/apt \ + config/hooks/live \ + config/package-lists \ + config/packages.chroot +cat >config/apt/apt.conf.chroot <<'APT' +Acquire::Check-Valid-Until "false"; +Acquire::Retries "3"; +APT +cat >config/package-lists/sos.list.chroot <<'PACKAGES' +busybox +ca-certificates +dbus +initramfs-tools +kmod +libgl1-mesa-dri +libpam-systemd +linux-image-arm64 +live-boot +live-config +live-config-systemd +mesa-vulkan-drivers +network-manager +systemd-sysv +udev +util-linux +PACKAGES +cat >config/hooks/live/0100-sos-image.hook.chroot <<'HOOK' +#!/bin/sh +set -eu +systemctl enable sos-agent.target +systemctl set-default sos-session.target +rm -f /etc/ssh/ssh_host_* /var/lib/dbus/machine-id +: >/etc/machine-id +HOOK +chmod 0755 config/hooks/live/0100-sos-image.hook.chroot +install -m 0644 "$live_packages"/*.deb config/packages.chroot/ + +lb build + +live_iso_matches=(sos-debian13-live-arm64*.hybrid.iso) +[[ "${#live_iso_matches[@]}" -eq 1 && -f "${live_iso_matches[0]}" ]] || \ + live_fail "live-build did not produce exactly one hybrid ISO" +live_iso="${live_iso_matches[0]}" +live_output_iso="$live_output/sos-debian13-live-arm64.iso" +live_output_inspection="$live_output/sos-debian13-live-arm64.iso.inspection.txt" +[[ ! -e "$live_output_iso" && ! -e "$live_output_inspection" ]] || \ + live_fail "ISO output already exists" + +live_output_tmp="$live_output/.sos-debian13-live-arm64.iso.$$" +install -m 0644 "$live_iso" "$live_output_tmp" +mv -- "$live_output_tmp" "$live_output_iso" +{ + file "$live_output_iso" + xorriso -indev "$live_output_iso" -report_el_torito plain + xorriso -indev "$live_output_iso" -find / -name 'vmlinuz*' -print + xorriso -indev "$live_output_iso" -find / -name 'initrd*' -print + xorriso -indev "$live_output_iso" -find / -name 'filesystem.squashfs' -print +} >"$live_output_inspection" 2>&1 + +printf 'linux_live_iso_built path=%s size=%s sha256=%s\n' \ + "$live_output_iso" \ + "$(stat -c '%s' "$live_output_iso")" \ + "$(sha256sum "$live_output_iso" | awk '{print $1}')" diff --git a/packaging/debian/Containerfile b/packaging/debian/Containerfile new file mode 100644 index 0000000..cb25f7b --- /dev/null +++ b/packaging/debian/Containerfile @@ -0,0 +1,91 @@ +ARG DEBIAN_BUILDER_IMAGE=debian:13.6-slim@sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258 +FROM ${DEBIAN_BUILDER_IMAGE} + +ARG DEBIAN_FRONTEND=noninteractive +ARG DEBIAN_SNAPSHOT=20260809T000000Z +ARG NODE_VERSION=24.18.0 +ARG NODE_ARM64_SHA256=58c9520501f6ae2b52d5b210444e24b9d0c029a58c5011b797bc1fe7105886f6 +ARG NODE_AMD64_SHA256=55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742 + +RUN rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources \ + && printf '%s\n' \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/${DEBIAN_SNAPSHOT} trixie main contrib non-free-firmware" \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/${DEBIAN_SNAPSHOT} trixie-security main contrib non-free-firmware" \ + > /etc/apt/sources.list \ + && printf '%s\n' \ + 'Acquire::Check-Valid-Until "false";' \ + 'Acquire::Retries "3";' \ + > /etc/apt/apt.conf.d/99sos-snapshot \ + && apt-get update \ + && apt-get install -y --no-install-recommends \ + build-essential \ + ca-certificates \ + clang \ + cmake \ + curl \ + dpkg-dev \ + elfutils \ + g++ \ + gcc \ + git \ + jq \ + libasound2-dev \ + libfontconfig-dev \ + libgbm-dev \ + libgit2-dev \ + libglib2.0-dev \ + libinput-dev \ + libseat-dev \ + libsqlite3-dev \ + libssl-dev \ + libudev-dev \ + libva-dev \ + libvulkan1 \ + libwayland-dev \ + libx11-xcb-dev \ + libxkbcommon-x11-dev \ + libzstd-dev \ + lld \ + llvm \ + make \ + mesa-vulkan-drivers \ + musl-dev \ + musl-tools \ + pkgconf \ + rustup \ + xz-utils \ + && rm -rf /var/lib/apt/lists/* + +ENV RUSTUP_HOME=/opt/rustup +ENV PATH=/opt/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin +RUN mkdir -p /opt/cargo "${RUSTUP_HOME}" \ + && CARGO_HOME=/opt/cargo rustup set profile minimal \ + && CARGO_HOME=/opt/cargo rustup toolchain install 1.95.0 \ + && CARGO_HOME=/opt/cargo rustup default 1.95.0 \ + && chmod -R a+rX /opt/cargo "${RUSTUP_HOME}" + +RUN set -eux; \ + debian_arch="$(dpkg --print-architecture)"; \ + case "${debian_arch}" in \ + arm64) node_arch=arm64; node_sha256="${NODE_ARM64_SHA256}" ;; \ + amd64) node_arch=x64; node_sha256="${NODE_AMD64_SHA256}" ;; \ + *) printf 'unsupported Debian architecture: %s\n' "${debian_arch}" >&2; exit 1 ;; \ + esac; \ + node_archive="/tmp/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz"; \ + curl --fail --location --silent --show-error \ + "https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-${node_arch}.tar.xz" \ + --output "${node_archive}"; \ + printf '%s %s\n' "${node_sha256}" "${node_archive}" | sha256sum --check --status; \ + tar -xJf "${node_archive}" -C /usr/local --strip-components=1; \ + rm -f "${node_archive}"; \ + node --version; \ + npm --version; \ + rustc --version; \ + cargo --version + +COPY container-entrypoint /usr/local/bin/sos-linux-package-builder-entrypoint +RUN chmod 0755 /usr/local/bin/sos-linux-package-builder-entrypoint + +ENV SOS_WORKSPACE=/workspace +WORKDIR /workspace +ENTRYPOINT ["/usr/local/bin/sos-linux-package-builder-entrypoint"] diff --git a/packaging/debian/container-entrypoint b/packaging/debian/container-entrypoint new file mode 100755 index 0000000..80e989b --- /dev/null +++ b/packaging/debian/container-entrypoint @@ -0,0 +1,17 @@ +#!/usr/bin/env bash + +set -euo pipefail + +builder_workspace="${SOS_WORKSPACE:-/workspace}" +builder_arch="$(dpkg --print-architecture)" +export HOME="${SOS_BUILDER_HOME:-/tmp/sos-linux-package-builder-home}" +export CARGO_HOME="${SOS_CARGO_HOME:-$builder_workspace/.cache/linux-package-builder/cargo}" +export CARGO_TARGET_DIR="${SOS_CARGO_TARGET_DIR:-$builder_workspace/.cache/linux-package-builder/target/$builder_arch}" +export CARGO_BUILD_JOBS="${SOS_CARGO_BUILD_JOBS:-2}" +export NPM_CONFIG_CACHE="${SOS_NPM_CACHE:-$builder_workspace/.cache/linux-package-builder/npm}" +export RUSTUP_HOME=/opt/rustup +export PATH="/opt/cargo/bin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" + +mkdir -p "$HOME" "$CARGO_HOME" "$CARGO_TARGET_DIR" "$NPM_CONFIG_CACHE" + +exec "$@" diff --git a/packaging/image/sos-image-initialize b/packaging/image/sos-image-initialize new file mode 100755 index 0000000..16c1a5e --- /dev/null +++ b/packaging/image/sos-image-initialize @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +initialize_fail() { + printf 'sos_image_initialize_failed error=%s\n' "$*" >&2 + exit 1 +} + +[[ "$(id -u)" -eq 0 ]] || initialize_fail "image initialization requires root" +for initialize_user in sos-compositor sos-provider sos-supervisor sos-host sos-agent; do + id "$initialize_user" >/dev/null 2>&1 || initialize_fail "missing system user: $initialize_user" +done +getent group sos-ipc >/dev/null 2>&1 || initialize_fail "missing system group: sos-ipc" + +install -d -o root -g root -m 0755 /etc/sos +install -d -o sos-compositor -g sos-ipc -m 0750 /var/lib/sos +install -d -o sos-provider -g sos-ipc -m 0750 /var/lib/sos/provider +install -d -o sos-supervisor -g sos-ipc -m 0750 \ + /var/lib/sos/revisions \ + /var/lib/sos/staging + +if [[ ! -s /etc/sos/shell-token ]]; then + initialize_token_tmp="/etc/sos/.shell-token.$$" + umask 077 + od -An -N32 -tx1 /dev/urandom | tr -d ' \n' >"$initialize_token_tmp" + chown root:root "$initialize_token_tmp" + chmod 0400 "$initialize_token_tmp" + mv -n -- "$initialize_token_tmp" /etc/sos/shell-token + rm -f -- "$initialize_token_tmp" +fi +chown root:root /etc/sos/shell-token +chmod 0400 /etc/sos/shell-token + +initialize_supervisor=/usr/lib/sos/sos-revision-supervisor +initialize_source=/usr/share/sos/experiences/default.luau +[[ -x "$initialize_supervisor" ]] || initialize_fail "missing revision supervisor" +[[ -r "$initialize_source" ]] || initialize_fail "missing default experience" + +initialize_current="$(runuser -u sos-supervisor -- \ + "$initialize_supervisor" status --root /var/lib/sos/revisions)" +if [[ "$initialize_current" == none ]]; then + initialize_state=/var/lib/sos/staging/initial-state.json + printf '{}\n' >"$initialize_state" + chown sos-supervisor:sos-ipc "$initialize_state" + chmod 0640 "$initialize_state" + initialize_revision="$(runuser -u sos-supervisor -- \ + "$initialize_supervisor" install \ + --root /var/lib/sos/revisions \ + --source "$initialize_source" \ + --state "$initialize_state" \ + --schema 1 \ + --api 3)" + runuser -u sos-supervisor -- \ + "$initialize_supervisor" bootstrap \ + --root /var/lib/sos/revisions \ + --revision "$initialize_revision" >/dev/null + rm -f -- "$initialize_state" +else + initialize_revision="$initialize_current" +fi + +printf 'sos_image_initialized revision_id=%s\n' "$initialize_revision" diff --git a/packaging/image/sos-image-initialize.service b/packaging/image/sos-image-initialize.service new file mode 100644 index 0000000..c36b455 --- /dev/null +++ b/packaging/image/sos-image-initialize.service @@ -0,0 +1,22 @@ +[Unit] +Description=Initialize SOS appliance identity and boot revision +Documentation=file:/usr/share/doc/sos/linux-stable-host.md +After=local-fs.target systemd-sysusers.service +Before=sos-session.service +RequiresMountsFor=/etc/sos /var/lib/sos + +[Service] +Type=oneshot +ExecStart=/usr/lib/sos/sos-image-initialize +RemainAfterExit=yes +NoNewPrivileges=yes +PrivateTmp=yes +ProtectHome=yes +ProtectKernelTunables=yes +ProtectKernelModules=yes +ProtectKernelLogs=yes +ProtectControlGroups=yes +RestrictSUIDSGID=yes +LockPersonality=yes +RestrictAddressFamilies=AF_UNIX +ReadWritePaths=/etc/sos /var/lib/sos diff --git a/packaging/image/sos-session-image.conf b/packaging/image/sos-session-image.conf new file mode 100644 index 0000000..81306cb --- /dev/null +++ b/packaging/image/sos-session-image.conf @@ -0,0 +1,3 @@ +[Unit] +Requires=sos-image-initialize.service +After=sos-image-initialize.service diff --git a/packaging/libexec/sos-agent-login b/packaging/libexec/sos-agent-login index bd7956a..7034bf0 100755 --- a/packaging/libexec/sos-agent-login +++ b/packaging/libexec/sos-agent-login @@ -18,13 +18,16 @@ sos_agent_login_state_dir="$sos_agent_login_state_home/sos/agent" sos_agent_login_credentials="$sos_agent_login_state_dir/auth.json" sos_agent_login_config="$sos_agent_login_state_dir/config.env" sos_agent_login_main="${SOS_AGENT_MAIN:-/usr/local/libexec/sos-agent/dist/agent-runner.cjs}" +sos_agent_login_node="${SOS_NODE_BIN:-$(command -v node || true)}" sos_agent_login_provider="${SOS_AGENT_PROVIDER:-openai-codex}" sos_agent_login_model="${SOS_AGENT_MODEL:-gpt-5.6-sol}" -[[ "$sos_agent_login_state_home" == /* && "$sos_agent_login_main" == /* ]] || \ +[[ "$sos_agent_login_state_home" == /* \ + && "$sos_agent_login_main" == /* \ + && "$sos_agent_login_node" == /* ]] || \ sos_agent_login_fail "agent state and program paths must be absolute" [[ -r "$sos_agent_login_main" ]] || sos_agent_login_fail "missing resident agent: $sos_agent_login_main" -command -v node >/dev/null 2>&1 || sos_agent_login_fail "Node.js is not installed" +[[ -x "$sos_agent_login_node" ]] || sos_agent_login_fail "missing Node.js runtime: $sos_agent_login_node" [[ "$sos_agent_login_provider" == openai-codex ]] || \ sos_agent_login_fail "selectable-session device login currently requires openai-codex" [[ "$sos_agent_login_model" =~ ^[A-Za-z0-9._:/-]+$ ]] || \ @@ -38,7 +41,7 @@ if [[ "${1:-}" == --if-needed && -s "$sos_agent_login_credentials" && -s "$sos_a exit 0 fi -node "$sos_agent_login_main" login \ +"$sos_agent_login_node" "$sos_agent_login_main" login \ --provider "$sos_agent_login_provider" \ --credentials "$sos_agent_login_credentials" \ --device-code diff --git a/packaging/libexec/sos-login-session b/packaging/libexec/sos-login-session index 8985826..3c0121e 100755 --- a/packaging/libexec/sos-login-session +++ b/packaging/libexec/sos-login-session @@ -31,6 +31,7 @@ sos_login_supervisor_bin="$sos_login_bin_dir/sos-revision-supervisor" sos_login_host_bin="$sos_login_bin_dir/sos-experience-host" sos_login_authoring_bin="$sos_login_bin_dir/sos-agent-authoring" sos_login_agent_main="/usr/local/libexec/sos-agent/dist/agent-runner.cjs" +sos_login_node="${SOS_NODE_BIN:-$(command -v node || true)}" for sos_login_executable in \ "$sos_login_session_bin" \ "$sos_login_compositor_bin" \ @@ -87,7 +88,7 @@ sos_login_session_pid="" sos_login_authoring_pid="" sos_login_agent_pid="" # ShellCheck cannot infer that EXIT invokes this callback. -# shellcheck disable=SC2317 +# shellcheck disable=SC2317,SC2329 sos_login_cleanup() { for sos_login_cleanup_pid in \ "$sos_login_agent_pid" \ @@ -108,7 +109,7 @@ sos_login_cleanup() { fi } # ShellCheck cannot infer that the signal traps invoke this callback. -# shellcheck disable=SC2317 +# shellcheck disable=SC2317,SC2329 sos_login_forward_stop() { for sos_login_stop_pid in \ "$sos_login_agent_pid" \ @@ -165,7 +166,7 @@ sos_login_wait_for_socket "$sos_login_supervisor_socket" \ sos_login_fail "missing agent authoring broker: $sos_login_authoring_bin" [[ -r "$sos_login_agent_main" ]] || \ sos_login_fail "missing resident agent: $sos_login_agent_main" -command -v node >/dev/null 2>&1 || sos_login_fail "Node.js is not installed" +[[ -x "$sos_login_node" ]] || sos_login_fail "missing Node.js runtime: $sos_login_node" set -a # This file is written by sos-agent-login in the current user's private state directory. # shellcheck source=/dev/null @@ -187,7 +188,7 @@ sos_login_authoring_pid=$! sos_login_wait_for_socket "$sos_login_agent_authoring_socket" \ "$sos_login_authoring_pid" "SOS agent authoring broker" -node "$sos_login_agent_main" serve \ +"$sos_login_node" "$sos_login_agent_main" serve \ --socket "$sos_login_agent_socket" \ --authoring-socket "$sos_login_agent_authoring_socket" \ --state "$sos_login_agent_state_dir/messages.json" \ diff --git a/tests/linux-packages-container-test.sh b/tests/linux-packages-container-test.sh new file mode 100755 index 0000000..230f746 --- /dev/null +++ b/tests/linux-packages-container-test.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash + +set -euo pipefail + +package_test_snapshot="${SOS_DEBIAN_SNAPSHOT:?SOS_DEBIAN_SNAPSHOT is required}" +package_test_directory="${SOS_PACKAGE_DIRECTORY:-/packages}" +[[ "$package_test_snapshot" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] +[[ -d "$package_test_directory" ]] + +rm -f /etc/apt/sources.list.d/*.list /etc/apt/sources.list.d/*.sources +printf '%s\n' \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian/$package_test_snapshot trixie main contrib non-free-firmware" \ + "deb [check-valid-until=no] http://snapshot.debian.org/archive/debian-security/$package_test_snapshot trixie-security main contrib non-free-firmware" \ + >/etc/apt/sources.list +printf 'Acquire::Check-Valid-Until "false";\n' >/etc/apt/apt.conf.d/99sos-snapshot + +apt-get update >/tmp/sos-package-apt-update.log +DEBIAN_FRONTEND=noninteractive apt-get install -y \ + "$package_test_directory"/*.deb >/tmp/sos-package-apt-install.log + +package_test_version="" +for package_test_name in \ + sos-runtime \ + sos-agent \ + sos-desktop-session \ + sos-appliance-session \ + sos-image-config; do + package_test_status="$(dpkg-query -W -f='${db:Status-Abbrev}' "$package_test_name")" + package_test_actual_version="$(dpkg-query -W -f='${Version}' "$package_test_name")" + package_test_architecture="$(dpkg-query -W -f='${Architecture}' "$package_test_name")" + [[ "$package_test_status" == "ii " ]] + if [[ -z "$package_test_version" ]]; then + package_test_version="$package_test_actual_version" + fi + [[ "$package_test_actual_version" == "$package_test_version" ]] + printf 'installed package=%s version=%s architecture=%s\n' \ + "$package_test_name" "$package_test_actual_version" "$package_test_architecture" +done + +for package_test_binary in \ + sos-compositor \ + sos-experience-host \ + sos-provider-state-service \ + sos-revision-supervisor \ + sos-linux-session \ + sos-agent-authoring; do + [[ -x "/usr/lib/sos/$package_test_binary" ]] + if ldd "/usr/lib/sos/$package_test_binary" | grep -q 'not found'; then + printf 'unresolved shared library: %s\n' "$package_test_binary" >&2 + exit 1 + fi +done + +[[ "$(/usr/lib/sos-agent/node/bin/node --version)" == v24.18.0 ]] +[[ -x /usr/lib/sos/sos-login-session ]] +[[ -x /usr/lib/sos/sos-agent-login ]] +[[ -f /usr/share/wayland-sessions/sos.desktop ]] +if grep -R -q '/usr/local/libexec/sos\|/usr/local/bin/node' \ + /usr/lib/systemd/system/sos-*.service \ + /usr/lib/sos/sos-login-session \ + /usr/lib/sos/sos-agent-login \ + /usr/share/wayland-sessions/sos.desktop; then + printf 'installed packages retain development installation paths\n' >&2 + exit 1 +fi + +systemd-analyze verify \ + /usr/lib/systemd/system/sos-session.service \ + /usr/lib/systemd/system/sos-session.target \ + /usr/lib/systemd/system/sos-agent-authoring.service \ + /usr/lib/systemd/system/sos-agent.service \ + /usr/lib/systemd/system/sos-agent.target \ + /usr/lib/systemd/system/sos-image-initialize.service + +/usr/lib/sos/sos-image-initialize +package_test_revision="$(runuser -u sos-supervisor -- \ + /usr/lib/sos/sos-revision-supervisor status --root /var/lib/sos/revisions)" +[[ "$package_test_revision" =~ ^[0-9a-f]{64}$ ]] +[[ "$(wc -c /tmp/sos-image-initialize-second.log +[[ "$(runuser -u sos-supervisor -- \ + /usr/lib/sos/sos-revision-supervisor status --root /var/lib/sos/revisions)" \ + == "$package_test_revision" ]] + +dpkg -S \ + /usr/lib/sos/sos-compositor \ + /usr/lib/sos-agent/node/bin/node \ + /usr/lib/systemd/system/sos-session.service \ + /usr/lib/systemd/system/sos-image-initialize.service \ + /usr/share/wayland-sessions/sos.desktop + +printf 'linux_package_install_test_passed version=%s revision_id=%s node=%s\n' \ + "$package_test_version" \ + "$package_test_revision" \ + "$(/usr/lib/sos-agent/node/bin/node --version)" diff --git a/tests/linux-packaging-host-test.sh b/tests/linux-packaging-host-test.sh new file mode 100755 index 0000000..0ccd507 --- /dev/null +++ b/tests/linux-packaging-host-test.sh @@ -0,0 +1,102 @@ +#!/usr/bin/env bash + +set -euo pipefail + +test_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +test_work="$(mktemp -d -t sos-packaging-test.XXXXXX)" +trap 'rm -rf -- "$test_work"' EXIT + +"$test_repo_root/tools/render-linux-packaging" \ + "$test_work/rendered" \ + /usr/lib/sos \ + /usr/lib/sos-agent \ + /usr/lib/sos-agent/node/bin/node >/dev/null + +grep -q 'ExecStart=/usr/lib/sos/sos-linux-session' \ + "$test_work/rendered/systemd/sos-session.service" +grep -q 'ExecStart=/usr/lib/sos-agent/node/bin/node /usr/lib/sos-agent/dist/agent-runner.cjs' \ + "$test_work/rendered/systemd/sos-agent.service" +grep -q 'Exec=/usr/lib/sos/sos-login-session' \ + "$test_work/rendered/wayland-sessions/sos.desktop" +# These are intentionally literal shell expressions in the rendered file. +# shellcheck disable=SC2016 +grep -q 'sos_login_node="${SOS_NODE_BIN:-/usr/lib/sos-agent/node/bin/node}"' \ + "$test_work/rendered/libexec/sos-login-session" +# shellcheck disable=SC2016 +grep -q 'sos_agent_login_node="${SOS_NODE_BIN:-/usr/lib/sos-agent/node/bin/node}"' \ + "$test_work/rendered/libexec/sos-agent-login" +if grep -R -q '/usr/local/libexec/sos\|/usr/local/bin/node' "$test_work/rendered"; then + printf 'rendered packaging retained source-install paths\n' >&2 + exit 1 +fi + +# shellcheck source=/dev/null +source "$test_repo_root/images/debian-13/base-images.lock" +[[ "$SOS_IMAGE_ARCH" == arm64 ]] +[[ "$SOS_IMAGE_BASE_SIZE" =~ ^[0-9]+$ ]] +[[ "$SOS_IMAGE_BASE_SHA256" =~ ^[0-9a-f]{64}$ ]] +[[ "$SOS_IMAGE_BASE_SHA512" =~ ^[0-9a-f]{128}$ ]] +[[ "$SOS_IMAGE_DEBIAN_SNAPSHOT" =~ ^[0-9]{8}T[0-9]{6}Z$ ]] +grep -q '^ARG DEBIAN_BUILDER_IMAGE=debian:13\.6-slim@sha256:[0-9a-f]\{64\}$' \ + "$test_repo_root/packaging/debian/Containerfile" +grep -q "^ARG DEBIAN_SNAPSHOT=$SOS_IMAGE_DEBIAN_SNAPSHOT$" \ + "$test_repo_root/packaging/debian/Containerfile" +grep -q '^ENV RUSTUP_HOME=/opt/rustup$' \ + "$test_repo_root/packaging/debian/Containerfile" +grep -q 'rustup toolchain install 1\.95\.0' \ + "$test_repo_root/packaging/debian/Containerfile" + +for test_package in \ + sos-runtime \ + sos-agent \ + sos-desktop-session \ + sos-appliance-session \ + sos-image-config; do + grep -q "package_build_one $test_package " \ + "$test_repo_root/tools/build-linux-packages" +done +if grep -q 'install-linux-login-session\|cargo build\|npm run build' \ + "$test_repo_root/images/debian-13/configure-root"; then + printf 'image root configuration invokes a source build or installer\n' >&2 + exit 1 +fi +grep -q 'CARGO_TARGET_DIR=.*linux-package-builder/target' \ + "$test_repo_root/packaging/debian/container-entrypoint" +grep -q 'CARGO_BUILD_JOBS=.*SOS_CARGO_BUILD_JOBS:-2' \ + "$test_repo_root/packaging/debian/container-entrypoint" +grep -q -- '--env "SOS_SOURCE_REVISION=' \ + "$test_repo_root/tools/build-linux-packages-container" +grep -q -- '--env "SOURCE_DATE_EPOCH=' \ + "$test_repo_root/tools/build-linux-packages-container" +grep -q 'linux_package_install_test_passed' \ + "$test_repo_root/tests/linux-packages-container-test.sh" +grep -q "package_test_base_image='debian:13\\.6-slim@sha256:[0-9a-f]\\{64\\}'" \ + "$test_repo_root/tools/test-linux-packages-container" +grep -q '^ARG DEBIAN_LIVE_IMAGE=debian:13\.6-slim@sha256:[0-9a-f]\{64\}$' \ + "$test_repo_root/packaging/debian-live/Containerfile" +grep -q "^ARG DEBIAN_SNAPSHOT=$SOS_IMAGE_DEBIAN_SNAPSHOT$" \ + "$test_repo_root/packaging/debian-live/Containerfile" +grep -q -- '--binary-images iso-hybrid' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +grep -q -- '--bootloaders grub-efi' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +grep -q -- '--apt-options "--yes -o Acquire::Check-Valid-Until=false -o Acquire::Retries=5 -o Acquire::http::Timeout=30 -o Acquire::https::Timeout=30"' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +grep -q -- '--apt-pipeline 0' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +grep -q -- '--firmware-binary false' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +grep -q -- '--firmware-chroot false' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +grep -q -- '--updates false' \ + "$test_repo_root/packaging/debian-live/build-live-iso" +if grep -q 'cargo build\|npm run build\|install-linux-login-session' \ + "$test_repo_root/packaging/debian-live/build-live-iso"; then + printf 'live ISO recipe invokes a source build or installer\n' >&2 + exit 1 +fi +grep -q 'output directory is not empty' \ + "$test_repo_root/tools/build-linux-iso-container" + +printf 'linux_packaging_host_test_passed architecture=%s snapshot=%s\n' \ + "$SOS_IMAGE_ARCH" "$SOS_IMAGE_DEBIAN_SNAPSHOT" diff --git a/tools/build-linux-iso-container b/tools/build-linux-iso-container new file mode 100755 index 0000000..aae1e1f --- /dev/null +++ b/tools/build-linux-iso-container @@ -0,0 +1,143 @@ +#!/usr/bin/env bash + +set -euo pipefail + +iso_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +iso_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -le 2 ]] || iso_fail \ + "usage: tools/build-linux-iso-container [package-directory] [output-directory]" +iso_packages="${1:-$iso_repo_root/artifacts/linux-packages}" +iso_output="${2:-$iso_repo_root/artifacts/linux-iso}" +[[ "$iso_packages" == /* ]] || iso_packages="$iso_repo_root/$iso_packages" +[[ "$iso_output" == /* ]] || iso_output="$iso_repo_root/$iso_output" +[[ -d "$iso_packages" ]] || iso_fail "package directory not found: $iso_packages" +iso_output_created=false +if [[ -e "$iso_output" ]]; then + [[ -d "$iso_output" ]] || iso_fail "output exists and is not a directory: $iso_output" + shopt -s nullglob dotglob + iso_output_entries=("$iso_output"/*) + shopt -u nullglob dotglob + [[ "${#iso_output_entries[@]}" -eq 0 ]] || \ + iso_fail "output directory is not empty: $iso_output" +else + iso_output_created=true +fi + +iso_engine="${SOS_CONTAINER_ENGINE:-}" +if [[ -z "$iso_engine" ]]; then + if command -v podman >/dev/null 2>&1; then + iso_engine=podman + elif command -v docker >/dev/null 2>&1; then + iso_engine=docker + else + iso_fail "Podman or Docker is required" + fi +fi +command -v "$iso_engine" >/dev/null 2>&1 || \ + iso_fail "container engine not found: $iso_engine" + +# shellcheck source=/dev/null +source "$iso_repo_root/images/debian-13/base-images.lock" +iso_builder_image="${SOS_LINUX_ISO_BUILDER_IMAGE:-sos-linux-iso-builder:debian13.6}" +iso_volume="sos-linux-iso-build-$$" +iso_incomplete=true +iso_cleanup() { + "$iso_engine" volume rm --force "$iso_volume" >/dev/null 2>&1 || true + if [[ "$iso_incomplete" == true ]]; then + rm -f -- \ + "$iso_output"/.sos-debian13-live-arm64.iso.* \ + "$iso_output"/sos-debian13-live-arm64.iso \ + "$iso_output"/sos-debian13-live-arm64.iso.inspection.txt \ + "$iso_output"/sos-debian13-live-arm64.iso.manifest + if [[ "$iso_output_created" == true ]]; then + rmdir -- "$iso_output" 2>/dev/null || true + fi + fi +} +trap iso_cleanup EXIT +if [[ "$iso_output_created" == true ]]; then + mkdir -p "$iso_output" +fi + +printf 'Building pinned Debian 13 ARM64 live-ISO environment (%s).\n' \ + "$iso_builder_image" +"$iso_engine" build \ + --pull \ + --tag "$iso_builder_image" \ + --file "$iso_repo_root/packaging/debian-live/Containerfile" \ + "$iso_repo_root/packaging/debian-live" +"$iso_engine" volume create "$iso_volume" >/dev/null + +iso_run_args=( + run + --rm + --privileged + --volume "$iso_volume:/build" + --volume "$iso_packages:/packages:ro" + --volume "$iso_output:/output" + --env "SOS_DEBIAN_SNAPSHOT=$SOS_IMAGE_DEBIAN_SNAPSHOT" + "$iso_builder_image" +) +if [[ "$iso_engine" == podman ]]; then + iso_run_args=( + run + --rm + --privileged + --security-opt label=disable + --volume "$iso_volume:/build" + --volume "$iso_packages:/packages:ro" + --volume "$iso_output:/output" + --env "SOS_DEBIAN_SNAPSHOT=$SOS_IMAGE_DEBIAN_SNAPSHOT" + "$iso_builder_image" + ) +fi +"$iso_engine" "${iso_run_args[@]}" + +iso_path="$iso_output/sos-debian13-live-arm64.iso" +iso_inspection="$iso_path.inspection.txt" +[[ -f "$iso_path" && -s "$iso_path" ]] || iso_fail "ISO was not produced" +[[ -f "$iso_inspection" && -s "$iso_inspection" ]] || \ + iso_fail "ISO inspection was not produced" + +iso_size="$(stat -f '%z' "$iso_path" 2>/dev/null || stat -c '%s' "$iso_path")" +iso_sha256="$(shasum -a 256 "$iso_path" | awk '{print $1}')" +iso_inspection_size="$(stat -f '%z' "$iso_inspection" 2>/dev/null || stat -c '%s' "$iso_inspection")" +iso_inspection_sha256="$(shasum -a 256 "$iso_inspection" | awk '{print $1}')" +iso_builder_id="$("$iso_engine" image inspect "$iso_builder_image" --format '{{.Id}}')" +iso_revision="$(git -C "$iso_repo_root" rev-parse HEAD)" +if [[ -n "$(git -C "$iso_repo_root" status --short)" ]]; then + iso_revision="$iso_revision-dirty" +fi +iso_manifest="$iso_path.manifest" +iso_manifest_tmp="$iso_output/.sos-debian13-live-arm64.iso.manifest.$$" +{ + printf 'format=sos-linux-live-iso-manifest-v1\n' + printf 'recipe_revision=%s\n' "$iso_revision" + printf 'architecture=arm64\n' + printf 'debian_snapshot=%s\n' "$SOS_IMAGE_DEBIAN_SNAPSHOT" + printf 'builder_image=%s\n' "$iso_builder_image" + printf 'builder_image_id=%s\n' "$iso_builder_id" + for iso_package in "$iso_packages"/*.deb; do + printf 'package=%s size=%s sha256=%s\n' \ + "$(basename "$iso_package")" \ + "$(stat -f '%z' "$iso_package" 2>/dev/null || stat -c '%s' "$iso_package")" \ + "$(shasum -a 256 "$iso_package" | awk '{print $1}')" + done + printf 'iso_path=%s\n' "$iso_path" + printf 'iso_size=%s\n' "$iso_size" + printf 'iso_sha256=%s\n' "$iso_sha256" + printf 'inspection_path=%s\n' "$iso_inspection" + printf 'inspection_size=%s\n' "$iso_inspection_size" + printf 'inspection_sha256=%s\n' "$iso_inspection_sha256" +} >"$iso_manifest_tmp" +chmod 0644 "$iso_manifest_tmp" +mv -- "$iso_manifest_tmp" "$iso_manifest" +iso_incomplete=false + +printf 'linux_iso_container_build_passed path=%s size=%s sha256=%s manifest=%s\n' \ + "$iso_path" "$iso_size" "$iso_sha256" "$iso_manifest" diff --git a/tools/build-linux-packages b/tools/build-linux-packages new file mode 100755 index 0000000..5dd74d2 --- /dev/null +++ b/tools/build-linux-packages @@ -0,0 +1,301 @@ +#!/usr/bin/env bash + +set -euo pipefail + +package_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$package_repo_root" + +package_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -ge 1 && "$#" -le 2 ]] || package_fail \ + "usage: tools/build-linux-packages [output-directory]" +package_version="$1" +package_output="${2:-$package_repo_root/artifacts/linux-packages}" +[[ "$package_version" =~ ^[0-9][0-9A-Za-z.+~-]*$ ]] || \ + package_fail "invalid Debian package version: $package_version" +[[ ! -e "$package_output" ]] || package_fail "output already exists: $package_output" + +[[ -r /etc/os-release ]] || package_fail "package builds require Debian 13" +# shellcheck source=/dev/null +source /etc/os-release +[[ "${ID:-}" == debian && "${VERSION_ID:-}" == 13 ]] || \ + package_fail "package builds require Debian 13, found ${ID:-unknown} ${VERSION_ID:-unknown}" + +for package_command in \ + cargo curl dpkg dpkg-deb dpkg-shlibdeps node npm sha256sum tar; do + command -v "$package_command" >/dev/null 2>&1 || \ + package_fail "required command not found: $package_command" +done +package_node_build_version="$(node --version | sed 's/^v//')" +[[ "$(printf '%s\n' 22.19.0 "$package_node_build_version" | sort -V | sed -n '1p')" == 22.19.0 ]] || \ + package_fail "agent package build requires Node 22.19.0 or newer" + +package_arch="$(dpkg --print-architecture)" +package_node_version=24.18.0 +case "$package_arch" in + arm64) + package_node_arch=arm64 + package_node_sha256=58c9520501f6ae2b52d5b210444e24b9d0c029a58c5011b797bc1fe7105886f6 + ;; + amd64) + package_node_arch=x64 + package_node_sha256=55aa7153f9d88f28d765fcdad5ae6945b5c0f98a36881703817e4c450fa76742 + ;; + *) + package_fail "unsupported Debian architecture: $package_arch" + ;; +esac + +if [[ -n "${SOS_SOURCE_REVISION:-}" ]]; then + package_revision="$SOS_SOURCE_REVISION" + [[ "$package_revision" =~ ^[0-9a-f]{40}(-dirty)?$ ]] || \ + package_fail "SOS_SOURCE_REVISION must be a full Git revision with optional -dirty suffix" +else + command -v git >/dev/null 2>&1 || \ + package_fail "git is required when SOS_SOURCE_REVISION is not supplied" + package_revision="$(git rev-parse HEAD)" + if [[ -n "$(git status --short)" ]]; then + package_revision="$package_revision-dirty" + fi +fi +if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then + package_source_epoch="$SOURCE_DATE_EPOCH" +else + command -v git >/dev/null 2>&1 || \ + package_fail "git is required when SOURCE_DATE_EPOCH is not supplied" + package_source_epoch="$(git show -s --format=%ct HEAD)" +fi +[[ "$package_source_epoch" =~ ^[0-9]+$ ]] || package_fail "SOURCE_DATE_EPOCH must be an integer" +export LC_ALL=C +export TZ=UTC +export SOURCE_DATE_EPOCH="$package_source_epoch" + +package_work="$(mktemp -d -t sos-packages.XXXXXX)" +package_incomplete=true +package_cleanup() { + rm -rf -- "$package_work" + if [[ "$package_incomplete" == true ]]; then + rm -rf -- "$package_output" + fi +} +trap package_cleanup EXIT +mkdir -p "$package_output" +package_target_dir="${CARGO_TARGET_DIR:-$package_repo_root/target}" +package_agent_source="$package_work/sos-agent-source" +mkdir -p "$package_agent_source" +cp -a \ + services/sos-agent/package-lock.json \ + services/sos-agent/package.json \ + services/sos-agent/scripts \ + services/sos-agent/src \ + services/sos-agent/test \ + services/sos-agent/tsconfig.json \ + "$package_agent_source/" + +printf 'Building SOS release binaries for Debian %s/%s.\n' "$VERSION_ID" "$package_arch" +cargo build --locked --release \ + -p sos-compositor --features direct-backend --bin sos-compositor \ + -p sos-experience --features linux-host --bin sos-experience-host \ + -p revision-supervisor --bin sos-revision-supervisor \ + -p provider-state-service --bin sos-provider-state-service \ + -p sos-linux-session --bin sos-linux-session \ + -p sos-linux-session --bin sos-agent-authoring +( + cd "$package_agent_source" + npm ci --ignore-scripts + npm run build +) + +package_node_archive="$package_work/node.tar.xz" +curl --fail --location --silent --show-error \ + "https://nodejs.org/dist/v$package_node_version/node-v$package_node_version-linux-$package_node_arch.tar.xz" \ + --output "$package_node_archive" +printf '%s %s\n' "$package_node_sha256" "$package_node_archive" \ + | sha256sum --check --status +mkdir -p "$package_work/node" +tar -xJf "$package_node_archive" -C "$package_work/node" --strip-components=1 + +"$package_repo_root/tools/render-linux-packaging" \ + "$package_work/rendered" \ + /usr/lib/sos \ + /usr/lib/sos-agent \ + /usr/lib/sos-agent/node/bin/node + +package_runtime_stage="$package_work/sos-runtime" +install -d -m 0755 \ + "$package_runtime_stage/usr/lib/sos" \ + "$package_runtime_stage/usr/share/sos/experiences" \ + "$package_runtime_stage/usr/share/doc/sos" +for package_binary in \ + sos-compositor \ + sos-experience-host \ + sos-provider-state-service \ + sos-revision-supervisor \ + sos-linux-session \ + sos-agent-authoring; do + install -m 0755 "$package_target_dir/release/$package_binary" \ + "$package_runtime_stage/usr/lib/sos/$package_binary" +done +for package_experience in default.luau daily-flow.luau timeflow.luau; do + install -m 0644 "experiences/$package_experience" \ + "$package_runtime_stage/usr/share/sos/experiences/$package_experience" +done +install -m 0644 docs/experience-api.md "$package_runtime_stage/usr/share/doc/sos/experience-api.md" +install -m 0644 docs/linux-stable-host.md "$package_runtime_stage/usr/share/doc/sos/linux-stable-host.md" +printf 'package_version=%s\nsource_revision=%s\nsource_date_epoch=%s\n' \ + "$package_version" "$package_revision" "$package_source_epoch" \ + >"$package_runtime_stage/usr/share/doc/sos/build-info" + +package_shlibs="$package_work/shlibs" +mkdir -p "$package_shlibs/debian" +cat >"$package_shlibs/debian/control" <<'CONTROL' +Source: sos-runtime +Section: misc +Priority: optional +Maintainer: SOS Project + +Package: sos-runtime +Architecture: any +Description: SOS native Linux runtime +CONTROL +package_shlib_args=() +for package_binary in "$package_runtime_stage"/usr/lib/sos/*; do + package_shlib_args+=("-e$package_binary") +done +package_runtime_depends="$( + cd "$package_shlibs" + dpkg-shlibdeps -O "${package_shlib_args[@]}" +)" +package_runtime_depends="${package_runtime_depends#shlibs:Depends=}" +[[ -n "$package_runtime_depends" ]] || package_fail "dpkg-shlibdeps returned no runtime dependencies" +package_runtime_depends="$package_runtime_depends, bash, coreutils, util-linux" + +package_agent_stage="$package_work/sos-agent" +install -d -m 0755 \ + "$package_agent_stage/usr/lib/sos-agent" \ + "$package_agent_stage/usr/lib/sos-agent/node/bin" \ + "$package_agent_stage/usr/share/doc/sos" +cp -a "$package_agent_source/dist" "$package_agent_source/node_modules" \ + "$package_agent_stage/usr/lib/sos-agent/" +install -m 0644 "$package_agent_source/package.json" \ + "$package_agent_stage/usr/lib/sos-agent/package.json" +install -m 0755 "$package_work/node/bin/node" \ + "$package_agent_stage/usr/lib/sos-agent/node/bin/node" +install -m 0644 "$package_work/node/LICENSE" \ + "$package_agent_stage/usr/share/doc/sos/node-license" +install -m 0644 docs/sos-agent.md "$package_agent_stage/usr/share/doc/sos/sos-agent.md" + +package_desktop_stage="$package_work/sos-desktop-session" +install -d -m 0755 \ + "$package_desktop_stage/usr/lib/sos" \ + "$package_desktop_stage/usr/share/wayland-sessions" +install -m 0755 "$package_work/rendered/libexec/sos-login-session" \ + "$package_desktop_stage/usr/lib/sos/sos-login-session" +install -m 0755 "$package_work/rendered/libexec/sos-agent-login" \ + "$package_desktop_stage/usr/lib/sos/sos-agent-login" +install -m 0644 "$package_work/rendered/wayland-sessions/sos.desktop" \ + "$package_desktop_stage/usr/share/wayland-sessions/sos.desktop" + +package_appliance_stage="$package_work/sos-appliance-session" +install -d -m 0755 \ + "$package_appliance_stage/usr/lib/systemd/system" \ + "$package_appliance_stage/usr/lib/sysusers.d" +for package_unit in \ + sos-agent-authoring.service \ + sos-agent.service \ + sos-agent.target \ + sos-session.service \ + sos-session.target; do + install -m 0644 "$package_work/rendered/systemd/$package_unit" \ + "$package_appliance_stage/usr/lib/systemd/system/$package_unit" +done +install -m 0644 packaging/sysusers.d/sos.conf \ + "$package_appliance_stage/usr/lib/sysusers.d/sos.conf" +mkdir -p "$package_appliance_stage/DEBIAN" +cat >"$package_appliance_stage/DEBIAN/postinst" <<'POSTINST' +#!/bin/sh +set -e +systemd-sysusers /usr/lib/sysusers.d/sos.conf +if [ -d /run/systemd/system ]; then + systemctl daemon-reload +fi +POSTINST +chmod 0755 "$package_appliance_stage/DEBIAN/postinst" +cat >"$package_appliance_stage/DEBIAN/postrm" <<'POSTRM' +#!/bin/sh +set -e +if [ -d /run/systemd/system ]; then + systemctl daemon-reload +fi +POSTRM +chmod 0755 "$package_appliance_stage/DEBIAN/postrm" + +package_image_stage="$package_work/sos-image-config" +install -d -m 0755 \ + "$package_image_stage/usr/lib/sos" \ + "$package_image_stage/usr/lib/systemd/system" \ + "$package_image_stage/usr/lib/systemd/system/sos-session.service.d" +install -m 0755 packaging/image/sos-image-initialize \ + "$package_image_stage/usr/lib/sos/sos-image-initialize" +install -m 0644 packaging/image/sos-image-initialize.service \ + "$package_image_stage/usr/lib/systemd/system/sos-image-initialize.service" +install -m 0644 packaging/image/sos-session-image.conf \ + "$package_image_stage/usr/lib/systemd/system/sos-session.service.d/10-image-initialize.conf" + +package_build_one() { + local package_name="$1" + local package_architecture="$2" + local package_depends="$3" + local package_description="$4" + local package_stage="$package_work/$package_name" + local package_installed_size + package_installed_size="$(du -sk "$package_stage" | awk '{print $1}')" + mkdir -p "$package_stage/DEBIAN" + cat >"$package_stage/DEBIAN/control" < +Installed-Size: $package_installed_size +Depends: $package_depends +Description: $package_description + Built from SOS revision $package_revision for the pinned Debian 13 platform. +CONTROL + find "$package_stage" -print0 \ + | xargs -0 touch --no-dereference --date="@$package_source_epoch" + dpkg-deb --root-owner-group -Zxz -z9 --build \ + "$package_stage" \ + "$package_output/${package_name}_${package_version}_${package_architecture}.deb" +} + +package_build_one sos-runtime "$package_arch" \ + "$package_runtime_depends" \ + "SOS compositor, experience host, authority, and revision runtime" +package_build_one sos-agent "$package_arch" \ + "sos-runtime (= $package_version), ca-certificates, libc6, libstdc++6" \ + "SOS resident authoring agent with pinned private Node runtime" +package_build_one sos-desktop-session all \ + "sos-runtime (= $package_version), sos-agent (= $package_version), bash, coreutils" \ + "Selectable SOS Wayland desktop session" +package_build_one sos-appliance-session all \ + "sos-runtime (= $package_version), sos-agent (= $package_version), systemd, init-system-helpers" \ + "System users and boot-owned SOS appliance units" +package_build_one sos-image-config all \ + "sos-appliance-session (= $package_version), util-linux" \ + "First-boot policy for the pinned SOS reference image" + +( + cd "$package_output" + for package_deb in *.deb; do + printf '%s %s\n' "$(sha256sum "$package_deb" | awk '{print $1}')" "$package_deb" + done +) >"$package_output/SHA256SUMS" +package_incomplete=false + +printf 'linux_packages_built version=%s architecture=%s output=%s\n' \ + "$package_version" "$package_arch" "$package_output" diff --git a/tools/build-linux-packages-container b/tools/build-linux-packages-container new file mode 100755 index 0000000..973f047 --- /dev/null +++ b/tools/build-linux-packages-container @@ -0,0 +1,90 @@ +#!/usr/bin/env bash + +set -euo pipefail + +container_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +container_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -ge 1 && "$#" -le 2 ]] || container_fail \ + "usage: tools/build-linux-packages-container [repo-relative-output]" +container_version="$1" +container_output="${2:-artifacts/linux-packages}" +[[ "$container_output" != /* && "$container_output" != .. && "$container_output" != ../* \ + && "$container_output" != */../* && "$container_output" != */.. ]] || \ + container_fail "output must stay within the repository: $container_output" +[[ ! -e "$container_repo_root/$container_output" ]] || \ + container_fail "output already exists: $container_output" + +container_source_revision="$(git -C "$container_repo_root" rev-parse HEAD)" +if [[ -n "$(git -C "$container_repo_root" status --short)" ]]; then + container_source_revision="$container_source_revision-dirty" +fi +container_source_epoch="${SOURCE_DATE_EPOCH:-$(git -C "$container_repo_root" show -s --format=%ct HEAD)}" +container_cargo_jobs="${SOS_CARGO_BUILD_JOBS:-2}" +[[ "$container_cargo_jobs" =~ ^[1-9][0-9]*$ ]] || \ + container_fail "SOS_CARGO_BUILD_JOBS must be a positive integer" + +container_engine="${SOS_CONTAINER_ENGINE:-}" +if [[ -z "$container_engine" ]]; then + if command -v podman >/dev/null 2>&1; then + container_engine=podman + elif command -v docker >/dev/null 2>&1; then + container_engine=docker + else + container_fail "Podman or Docker is required" + fi +fi +command -v "$container_engine" >/dev/null 2>&1 || \ + container_fail "container engine not found: $container_engine" + +container_image="${SOS_LINUX_PACKAGE_BUILDER_IMAGE:-sos-linux-package-builder:debian13.6}" +container_platform="${SOS_LINUX_PACKAGE_PLATFORM:-}" +container_build_args=( + build + --pull + --tag "$container_image" + --file "$container_repo_root/packaging/debian/Containerfile" +) +if [[ -n "$container_platform" ]]; then + container_build_args+=(--platform "$container_platform") +fi +container_build_args+=("$container_repo_root/packaging/debian") + +printf 'Building pinned Debian 13 package environment (%s).\n' "$container_image" +"$container_engine" "${container_build_args[@]}" + +container_run_args=( + run + --rm + --workdir /workspace + --volume "$container_repo_root:/workspace" +) +if [[ -n "$container_platform" ]]; then + container_run_args+=(--platform "$container_platform") +fi +if [[ "$container_engine" == podman ]]; then + container_run_args+=(--security-opt label=disable --userns=keep-id) +else + container_run_args+=(--user "$(id -u):$(id -g)") +fi +if [[ -n "${SOURCE_DATE_EPOCH:-}" ]]; then + container_source_epoch="$SOURCE_DATE_EPOCH" +fi +container_run_args+=( + --env "SOS_SOURCE_REVISION=$container_source_revision" + --env "SOURCE_DATE_EPOCH=$container_source_epoch" + --env "SOS_CARGO_BUILD_JOBS=$container_cargo_jobs" +) +container_run_args+=( + "$container_image" + ./tools/build-linux-packages + "$container_version" + "$container_output" +) + +printf 'Building SOS Debian packages in the container.\n' +"$container_engine" "${container_run_args[@]}" diff --git a/tools/render-linux-packaging b/tools/render-linux-packaging new file mode 100755 index 0000000..a3e6506 --- /dev/null +++ b/tools/render-linux-packaging @@ -0,0 +1,72 @@ +#!/usr/bin/env bash + +set -euo pipefail + +render_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +render_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -eq 4 ]] || render_fail \ + "usage: tools/render-linux-packaging " + +render_output="$1" +render_sos_lib_dir="$2" +render_agent_dir="$3" +render_node_bin="$4" + +for render_path in "$render_sos_lib_dir" "$render_agent_dir" "$render_node_bin"; do + [[ "$render_path" =~ ^/[A-Za-z0-9._/+:-]+$ ]] || \ + render_fail "installation paths must be absolute and shell-safe: $render_path" +done +[[ ! -e "$render_output" ]] || render_fail "output already exists: $render_output" + +mkdir -p \ + "$render_output/libexec" \ + "$render_output/systemd" \ + "$render_output/wayland-sessions" + +render_one() { + local render_source="$1" + local render_destination="$2" + sed \ + -e "s|/usr/local/libexec/sos-agent|$render_agent_dir|g" \ + -e "s|/usr/local/libexec/sos|$render_sos_lib_dir|g" \ + -e "s|/usr/local/bin/node|$render_node_bin|g" \ + -e "s|^sos_login_node=.*$|sos_login_node=\"\${SOS_NODE_BIN:-$render_node_bin}\"|" \ + -e "s|^sos_agent_login_node=.*$|sos_agent_login_node=\"\${SOS_NODE_BIN:-$render_node_bin}\"|" \ + "$render_source" >"$render_destination" + if [[ -x "$render_source" ]]; then + chmod 0755 "$render_destination" + else + chmod 0644 "$render_destination" + fi +} + +for render_name in sos-agent-login sos-login-session; do + render_one \ + "$render_repo_root/packaging/libexec/$render_name" \ + "$render_output/libexec/$render_name" +done +for render_name in \ + sos-agent-authoring.service \ + sos-agent.service \ + sos-agent.target \ + sos-session.service \ + sos-session.target; do + render_one \ + "$render_repo_root/packaging/systemd/$render_name" \ + "$render_output/systemd/$render_name" +done +render_one \ + "$render_repo_root/packaging/wayland-sessions/sos.desktop" \ + "$render_output/wayland-sessions/sos.desktop" + +if grep -R -n '/usr/local/libexec/sos\|/usr/local/bin/node' "$render_output"; then + render_fail "rendered package files retain source-install paths" +fi + +printf 'linux_packaging_rendered output=%s sos_lib=%s agent_dir=%s node=%s\n' \ + "$render_output" "$render_sos_lib_dir" "$render_agent_dir" "$render_node_bin" diff --git a/tools/test-linux-packages-container b/tools/test-linux-packages-container new file mode 100755 index 0000000..1e56b8d --- /dev/null +++ b/tools/test-linux-packages-container @@ -0,0 +1,63 @@ +#!/usr/bin/env bash + +set -euo pipefail + +package_test_repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +package_test_directory="${1:-$package_test_repo_root/artifacts/linux-packages}" +[[ "$package_test_directory" == /* ]] || \ + package_test_directory="$package_test_repo_root/$package_test_directory" + +package_test_fail() { + printf 'error: %s\n' "$*" >&2 + exit 1 +} + +[[ "$#" -le 1 ]] || package_test_fail \ + "usage: tools/test-linux-packages-container [package-directory]" +[[ -d "$package_test_directory" ]] || \ + package_test_fail "package directory not found: $package_test_directory" +for package_test_name in \ + sos-runtime \ + sos-agent \ + sos-desktop-session \ + sos-appliance-session \ + sos-image-config; do + package_test_matches=("$package_test_directory"/"${package_test_name}_"*.deb) + [[ "${#package_test_matches[@]}" -eq 1 && -f "${package_test_matches[0]}" ]] || \ + package_test_fail "expected exactly one package for $package_test_name" +done + +package_test_engine="${SOS_CONTAINER_ENGINE:-}" +if [[ -z "$package_test_engine" ]]; then + if command -v podman >/dev/null 2>&1; then + package_test_engine=podman + elif command -v docker >/dev/null 2>&1; then + package_test_engine=docker + else + package_test_fail "Podman or Docker is required" + fi +fi +command -v "$package_test_engine" >/dev/null 2>&1 || \ + package_test_fail "container engine not found: $package_test_engine" + +# shellcheck source=/dev/null +source "$package_test_repo_root/images/debian-13/base-images.lock" +package_test_base_image='debian:13.6-slim@sha256:3a39a0592364683e6bab97937b72cad5a8fa6dcbbee90edb3bb48c7f8e94f258' +package_test_run_args=( + run + --rm + --volume "$package_test_directory:/packages:ro" + --volume "$package_test_repo_root/tests/linux-packages-container-test.sh:/package-test:ro" + --env "SOS_DEBIAN_SNAPSHOT=$SOS_IMAGE_DEBIAN_SNAPSHOT" + "$package_test_base_image" + /package-test +) +if [[ "$package_test_engine" == podman ]]; then + package_test_run_args=(run --rm --security-opt label=disable \ + --volume "$package_test_directory:/packages:ro" \ + --volume "$package_test_repo_root/tests/linux-packages-container-test.sh:/package-test:ro" \ + --env "SOS_DEBIAN_SNAPSHOT=$SOS_IMAGE_DEBIAN_SNAPSHOT" \ + "$package_test_base_image" /package-test) +fi + +"$package_test_engine" "${package_test_run_args[@]}"