From 3c131618889eb42d8f052a806503489f5bfc79d6 Mon Sep 17 00:00:00 2001 From: bong-water-water-bong Date: Mon, 26 Oct 2026 06:42:45 -0300 Subject: [PATCH] os/v2: the appliance on an Ubuntu base, updated with apt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `os/mini` is the BusyBox image that ships today: everything in RAM, a reflash to update. `os/v2` is the same appliance - Lemonade Server by AMD on the 1bit engine, our kernel, Secure Boot through the enrolled MOK - on a minimal Ubuntu 26.04 root filesystem with systemd, where every piece we ship is a `.deb` from our own signed repository and `apt update && apt upgrade` is the update path. `os/mini` keeps shipping until v2 passes the same checks on hardware. Seven packages, built from what a release already produces (no binary is rebuilt): `1bit-engine`, `1bit-lemonade`, `1bit-kernel-image-`, `1bit-kernel-modules-`, `1bit-ryzenadj`, `1bit-appliance` and `1bit-archive-keyring`. The kernel version is in the package name, so an upgrade keeps the previous kernel installed - that is the rollback. `mkdebs.sh` refuses a stage whose EC module is unsigned or built for another kernel, and a kernel image with no signature; it localises the engine's libraries with `scripts/pack-elf.sh` (the same `pack_localize` a release tarball goes through), because a package that carries the build machine's RUNPATH is a package that does not load (#549). The live medium is the root filesystem as a squashfs with a tmpfs overlay: one initramfs-tools script (`scripts/init-bottom/0-1bit-live`) builds the overlay and hands `rootmnt` back through `/conf/param.conf`, so userspace has nothing custom at boot and a live boot cannot write the medium. `install.sh` is `1bit-install`, with the same refusal matrix as #539 and three partitions (ESP, `1BIT-ROOT`, `1BIT-DATA`) instead of a RAM image; it unpacks the squashfs, writes `fstab` by label, one grub entry per installed kernel, and leaves `/etc/1bit/live` off the installed disk so `1bit-first-boot.service` writes `/etc/1bit/installed` after making that machine's own SSH keys. Two VM tests, both judged from what the guest writes to its own disk rather than from QEMU: `vmtest-v2.sh` (the appliance, booted through OVMF so the secureboot row answers instead of failing for a QEMU choice) and `vmtest-install-v2.sh` (builds the medium with a test-only unit inside it, boots it with a target disk and a USB stick attached, runs the refusals, installs, then boots the installed disk alone). Bugs this found in the v2 path, all fixed here: the engine package could not load its own libraries (a dev-built stage carried the build tree's RUNPATH and never shipped libhrx/libloomc/libomp); `1bit` needed `libgomp1`/`libpcre2-8-0`, which minbase has not; lemond exits at once without `HOME` in a systemd unit; `ssh.service` enabled while `ssh.socket` was masked starts nothing at all on Ubuntu 26.04 (the socket is what listens, and it carries RequiredBy=ssh.service) - so the socket is what the package enables; minbase has no `sfdisk` (Ubuntu's lives in `fdisk`); `root=` in the live entry has to be the squashfs partition, or `data.mount` fights the initramfs for the data partition; and `1bit-appliance` never shipped the aggregate `/usr/share/licenses/1bit-os` that release-check reads. Known gap, named in `os/v2/README.md` rather than papered over: the ROCm userspace (HIP, rocBLAS, hipBLASLt, the HSA runtime) is the machine's, not the engine package's, and the engine's `Depends` do not name Ubuntu's packages for it yet - gfx1201 support against the distro's ROCm is a hardware question a VM cannot settle. `vmtest-v2` therefore asserts the package's own libraries resolve and lets only that set be missing, which is exactly the check that caught the first bug above. Evidence: `VMTEST-V2 PASS` (seven packages installed, banner, /data, 1bit --help, our kernel's vmlinuz and initrd in /boot, sshd on 22, apt from the repository, Lemonade 401 + GGUF + chat, release-check with no FAIL row but `sealed`, which needs a stage that carries the sealed NPU sets) and `VMTEST-V2-INSTALL PASS` (six refusals, install, installed disk boots alone: INSTALLED mode, first-boot marker and own SSH keys, /data on its own partition, Lemonade 401 + GGUF + chat, selftest INSTALLED). Transcripts: `~/wt/osapt-proto/v2-final.log` and `~/wt/osapt-proto/live-install.log`. ๐Ÿค– Generated with [Claude Code](https://claude.com/claude-code) --- os/README.md | 18 + os/v2/README.md | 143 ++++++++ os/v2/debian/1bit-appliance/conffiles | 3 + os/v2/debian/1bit-appliance/control | 14 + .../1bit-appliance/files/etc/1bit/power | 26 ++ .../files/etc/apt/sources.list.d/1bit.list | 4 + os/v2/debian/1bit-appliance/files/etc/issue | 8 + os/v2/debian/1bit-appliance/files/etc/motd | 14 + .../files/etc/profile.d/1bit.sh | 19 + .../files/etc/systemd/network/10-1bit.network | 21 ++ .../getty@tty1.service.d/1bit-autologin.conf | 19 + .../system/ssh.service.d/1bit-first-boot.conf | 19 + .../files/usr/lib/1bit/ensure-env.sh | 30 ++ .../files/usr/lib/1bit/first-boot.sh | 26 ++ .../lib/systemd/system-preset/90-1bit.preset | 20 + .../usr/lib/systemd/system/1bit-board.service | 31 ++ .../usr/lib/systemd/system/1bit-boost.service | 30 ++ .../systemd/system/1bit-first-boot.service | 29 ++ .../files/usr/lib/systemd/system/data.mount | 33 ++ .../files/usr/lib/tmpfiles.d/1bit.conf | 23 ++ os/v2/debian/1bit-appliance/postinst | 67 ++++ os/v2/debian/1bit-archive-keyring/control | 11 + os/v2/debian/1bit-engine/control | 19 + os/v2/debian/1bit-kernel-image/control | 16 + os/v2/debian/1bit-kernel-image/postinst | 23 ++ os/v2/debian/1bit-kernel-image/postrm | 23 ++ os/v2/debian/1bit-kernel-modules/control | 15 + os/v2/debian/1bit-kernel-modules/postinst | 23 ++ .../1bit-lemonade/1bit-lemonade.service | 41 +++ os/v2/debian/1bit-lemonade/control | 18 + os/v2/debian/1bit-ryzenadj/control | 15 + os/v2/diag-v2.sh | 96 +++++ os/v2/install.sh | 253 +++++++++++++ os/v2/mkdebs.sh | 345 ++++++++++++++++++ os/v2/mklive.sh | 209 +++++++++++ os/v2/mkrepo.sh | 59 +++ os/v2/mkrootfs.sh | 98 +++++ os/v2/pin.txt | 23 ++ os/v2/proto.sh | 73 ++++ os/v2/vmtest-install-v2.sh | 235 ++++++++++++ os/v2/vmtest-v2.sh | 206 +++++++++++ tools/copyright.py | 11 +- 42 files changed, 2407 insertions(+), 2 deletions(-) create mode 100644 os/v2/README.md create mode 100644 os/v2/debian/1bit-appliance/conffiles create mode 100644 os/v2/debian/1bit-appliance/control create mode 100644 os/v2/debian/1bit-appliance/files/etc/1bit/power create mode 100644 os/v2/debian/1bit-appliance/files/etc/apt/sources.list.d/1bit.list create mode 100644 os/v2/debian/1bit-appliance/files/etc/issue create mode 100644 os/v2/debian/1bit-appliance/files/etc/motd create mode 100644 os/v2/debian/1bit-appliance/files/etc/profile.d/1bit.sh create mode 100644 os/v2/debian/1bit-appliance/files/etc/systemd/network/10-1bit.network create mode 100644 os/v2/debian/1bit-appliance/files/etc/systemd/system/getty@tty1.service.d/1bit-autologin.conf create mode 100644 os/v2/debian/1bit-appliance/files/etc/systemd/system/ssh.service.d/1bit-first-boot.conf create mode 100755 os/v2/debian/1bit-appliance/files/usr/lib/1bit/ensure-env.sh create mode 100755 os/v2/debian/1bit-appliance/files/usr/lib/1bit/first-boot.sh create mode 100644 os/v2/debian/1bit-appliance/files/usr/lib/systemd/system-preset/90-1bit.preset create mode 100644 os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-board.service create mode 100644 os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-boost.service create mode 100644 os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-first-boot.service create mode 100644 os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/data.mount create mode 100644 os/v2/debian/1bit-appliance/files/usr/lib/tmpfiles.d/1bit.conf create mode 100755 os/v2/debian/1bit-appliance/postinst create mode 100644 os/v2/debian/1bit-archive-keyring/control create mode 100644 os/v2/debian/1bit-engine/control create mode 100644 os/v2/debian/1bit-kernel-image/control create mode 100755 os/v2/debian/1bit-kernel-image/postinst create mode 100755 os/v2/debian/1bit-kernel-image/postrm create mode 100644 os/v2/debian/1bit-kernel-modules/control create mode 100755 os/v2/debian/1bit-kernel-modules/postinst create mode 100644 os/v2/debian/1bit-lemonade/1bit-lemonade.service create mode 100644 os/v2/debian/1bit-lemonade/control create mode 100644 os/v2/debian/1bit-ryzenadj/control create mode 100755 os/v2/diag-v2.sh create mode 100644 os/v2/install.sh create mode 100755 os/v2/mkdebs.sh create mode 100755 os/v2/mklive.sh create mode 100755 os/v2/mkrepo.sh create mode 100755 os/v2/mkrootfs.sh create mode 100644 os/v2/pin.txt create mode 100755 os/v2/proto.sh create mode 100755 os/v2/vmtest-install-v2.sh create mode 100755 os/v2/vmtest-v2.sh diff --git a/os/README.md b/os/README.md index be2f337e..8473f55f 100644 --- a/os/README.md +++ b/os/README.md @@ -251,6 +251,24 @@ HSA runtime and its libraries complete, the PM4-emulation probe answered), Lemon running `onebit` models on HRX0 and the NPU, HRX kernels compiling into `/data/cache/hrx-jit` and being reused after a reboot, Lemonade's Vulkan backends on RADV, `llamacpp-hrx`, Lemonade's ROCm downloads for gfx1151, the Wi-Fi chip, and the speed of all of it. +## The v2 base (Ubuntu + apt) + +Everything above describes `os/mini`: one BusyBox image that runs from RAM, updated by reflashing the +stick. `os/v2/` is the same appliance on a minimal Ubuntu 26.04 root filesystem with systemd, where what +we ship are `.deb` packages from the signed repository on `1bit.gg/apt` and the update path is +`apt update && apt upgrade` - with the previous kernel and the cached debs as the rollback. The kernel, +the EC board driver and ryzenadj are packages too; the live medium is the root filesystem as a squashfs +with a tmpfs overlay, and `1bit-install` writes three partitions (ESP, `1BIT-ROOT`, `1BIT-DATA`) instead +of unpacking everything into a RAM image. + +```sh +os/v2/proto.sh # everything, in one command +os/v2/README.md # what each piece is, and what is proven +``` + +`os/mini` keeps shipping until v2 passes the same checks on hardware. Both images keep the same `/data` +layout and the same installer behaviour, so an owner's runbook does not change. + ## Next - A Loom kernel cache built ahead of time, so the first boot needs no compile. diff --git a/os/v2/README.md b/os/v2/README.md new file mode 100644 index 00000000..298793b6 --- /dev/null +++ b/os/v2/README.md @@ -0,0 +1,143 @@ + +# 1bit OS v2 โ€” the appliance on a minimal Ubuntu base, updated with `apt` + +`os/mini` builds the BusyBox image that ships today: everything in RAM, a reflash to update. v2 keeps the +same appliance โ€” Lemonade Server by AMD, running on the 1bit engine, our kernel, Secure Boot through the +enrolled MOK โ€” on a **minimal Ubuntu 26.04 root filesystem with systemd**, where everything we ship is a +`.deb` from our own signed repository and `apt update && apt upgrade` is the update path. + +The design and the reasons are in `~/briefs/TOSAPT-DESIGN.md`; this file is what is in the tree and how to +reproduce it. `os/mini` keeps shipping until v2 passes the same checks on hardware. + +## Build it + +```sh +# 1. the packages. A release build produces the two inputs: an engine stage (scripts/package-linux.sh's +# tarball, or a build tree with ENGINE_BUILD=) and a Lemonade prefix (bin/lemond, bin/resources). +# KERNEL_STAGE is the release kernel as it sits on the build machine: +# /boot/{vmlinuz,System.map,config}- and /lib/modules// +os/v2/mkdebs.sh v2026.43 [archive-keyring.gpg] + # KERNEL_STAGE= ENGINE_BUILD= + +# 2. the apt repository those debs are published from (signed: Release, Release.gpg, InRelease) +os/v2/mkrepo.sh [gnupg-home] [key-id] + +# 3. a root filesystem from one Ubuntu snapshot (pin.txt) that apt-installs 1bit-appliance from it +os/v2/mkrootfs.sh [ssh-key.pub] + +# 4. the live medium: one image to dd onto a stick +os/v2/mklive.sh [mok.priv mok.pem] + +# 5. the two tests (no hardware, no GPU: QEMU) +os/v2/vmtest-v2.sh [ssh-key] # the appliance, from a rootfs +os/v2/vmtest-install-v2.sh [mok.priv mok.pem] # live + install +``` + +`os/v2/proto.sh` runs 1..5 in one command against a throwaway signing key, which is what a lane without +the operator's key uses. `os/v2/diag-v2.sh` boots the rootfs and asks the guest what failed โ€” it is a +debugging tool, it judges nothing. + +`MIRROR=` overrides the pinned snapshot in `mkrootfs.sh` for one run. That exists for a +`snapshot.ubuntu.com` outage (it 503'd for an hour on 25 Oct); a release build is the pinned snapshot. + +## The packages + +| Package | What is in it | +|---|---| +| `1bit-engine` | `/opt/1bit`: the engine, the HRX `llama-server` and its libraries, the ds4 programs, the sealed NPU kernel sets, `1bit-selftest`, `1bit-release-check`, `/etc/1bit/engine.conf` | +| `1bit-lemonade` | `/opt/lemonade`: our build of Lemonade Server (`lemond`, the CLI, the web app) and its unit | +| `1bit-kernel-image-` | `/boot/{vmlinuz,System.map,config}-`; its postinst runs `update-initramfs`. The **kver is in the name**, so an upgrade keeps the previous kernel installed โ€” that is the rollback | +| `1bit-kernel-modules-` | every module for ``, including the out-of-tree EC board driver (MOK-signed) and its source under `/usr/src/1bit-ec-` | +| `1bit-ryzenadj` | `/usr/bin/ryzenadj`, built from the pinned v0.19.0 source, which ships beside it (LGPL-3.0) | +| `1bit-appliance` | the meta/config package: units, banner, autologin, network, `/data`, first boot, apt source, `/usr/share/licenses/1bit-os`. `Depends:` the six above | +| `1bit-archive-keyring` | the public half of the key that signs `1bit.gg/apt` | + +Two things the packages must get right, both learned the hard way and both checked by `mkdebs.sh`: + +- **The engine's own libraries are found beside it.** `scripts/pack-elf.sh`'s `pack_localize` runs over the + package (`ENGINE_BUILD`), so `libllama-server-impl.so`, `libhrx`, `libloomc`, `libomp` are in the package + and `RUNPATH` is `$ORIGIN`. A build-machine path in a package is a package that does not load (#549). +- **Signed, or Secure Boot refuses it.** `mkdebs.sh` fails if the EC module is unsigned or built for another + kernel, and if the kernel image carries no signature (`sbverify --list` โ†’ `signature 1`). + +## Trust and the boot chain + +``` +firmware (db) -> shim (Microsoft-signed) -> grubx64.efi (our MOK) -> vmlinuz (our MOK) -> initramfs-tools + \-> ec_su_axb35.ko (our MOK, module-signing OID) +``` + +`apt` trusts `1bit.gg/apt` through `signed-by=/usr/share/keyrings/1bit-archive-keyring.gpg` and nothing +else: no `apt-key`, no `[trusted=yes]`. An install copies the medium's own signed chain, so the machine's +MOK enrolment is never touched by installing. + +## The live medium, and what an install writes + +`mklive.sh` writes one disk image, in the same three partitions an installed box has: + +| | live medium | after `1bit-install /dev/nvme0n1 --yes` | +|---|---|---| +| 1 | 512 MB ESP `1BIT-OS`: shim, our grub, kernel + initramfs, `grub.cfg` | the same, with a `grub.cfg` for the disk | +| 2 | squashfs, `PARTLABEL=1BIT-LIVE` โ€” the whole root filesystem, read-only | ext4 `1BIT-ROOT` = `min(64 GB, max(24 GB, 25 %))`, the squashfs unpacked, `/boot`, `fstab` by label | +| 3 | ext4 `1BIT-DATA` โ€” models, backends, config, keys | the same content, copied from the medium | + +The live system is the squashfs with a tmpfs overlay: an initramfs-tools script +(`scripts/local-bottom/1bit-live`) mounts it, so userspace has nothing custom at boot and a live boot +cannot write the medium. `1bit-install` refuses before writing anything โ€” no `--yes`, a partition instead +of a disk, a removable device, the disk the live boot runs from, a disk with something mounted, a missing +GPT tool โ€” prints the disk and waits 10 s, exactly as the `os/mini` installer does (`#539`). + +## What is proven, and where + +`vmtest-v2.sh` runs the checks **inside** the guest (a test-only oneshot unit writes its transcript to +`/data` and powers the machine off); the host reads that transcript, so no port forwarding decides a +verdict. `vmtest-install-v2.sh` does the same for the live medium and then boots the installed disk alone. + +| What | How | +|---|---| +| all seven packages install and are the release's | `dpkg-query -W '1bit-*'` in the guest | +| the banner, `/data` on its own disk, `1bit-selftest` | guest transcript | +| the engine runs; `llama-server`'s own libraries resolve | `1bit --help`, `ldd` on the packaged `llama-server` | +| our kernel package produced `vmlinuz` **and** `initrd.img` | `/boot` in the guest | +| Lemonade: key generated, 401 without it, the GGUF listed, one chat answered | `curl` against `127.0.0.1:13305` | +| apt answers from the repository, and the shipped source is `1bit.gg/apt` with `signed-by` | `apt-get update`, `apt-cache policy`, the `.list` | +| `1bit-release-check`: every row passes except the sealed-NPU one (a dev stage carries no sealed sets) | its own output | +| the live medium boots the squashfs with the overlay; all six refusals; the install writes three partitions with the kernel in `/boot`, `fstab` by label and the live marker gone | `vmtest-install-v2.sh` pass 1 | +| the installed disk boots alone: INSTALLED banner, first-boot marker and own SSH keys, `/data` on its disk, Lemonade 401 + GGUF + chat, `1bit-selftest` reports INSTALLED | `vmtest-install-v2.sh` pass 2 | + +The VM boots **OVMF** (EFI, Secure Boot off) rather than a legacy BIOS, so `/sys/firmware/efi` exists and +the `secureboot` row answers; enrolling the MOK is the machine's step, not a VM's. + +## Known gaps + +- **The ROCm userspace is not packaged.** The engine's HIP path (`llama-server`, `ds4`) links + `libamdhip64`, rocBLAS, hipBLASLt, the HSA runtime and `libamd_comgr` from the SDK the build machine had. + Ubuntu 26.04 has packages for them (`libamdhip64-7`, `libhipblaslt1`, `librocblas*`), but gfx1201 + support against the distro's ROCm is a hardware question, so the engine's `Depends` do not name them yet + and a VM (no GPU) cannot settle it. The engine's *own* libraries are packaged and resolve โ€” that is what + `vmtest-v2` asserts. +- **A development stage has no sealed NPU sets**, so `1bit-release-check`'s `sealed` row fails by + construction in these runs (the VM has no NPU either). A release stage always carries them. +- The v2 rootfs in these runs came from the archive mirror, not the pinned snapshot, because + `snapshot.ubuntu.com` was 503. `pin.txt` is unchanged and is what a release build uses. + +## Licence + +The scripts here are Apache-2.0, like the rest of this repository. What the packages carry is in +`os/LICENSES.md`; each package ships its own licences under `/usr/share/doc//` and the aggregate +under `/usr/share/licenses/1bit-os/`. diff --git a/os/v2/debian/1bit-appliance/conffiles b/os/v2/debian/1bit-appliance/conffiles new file mode 100644 index 00000000..8eaac429 --- /dev/null +++ b/os/v2/debian/1bit-appliance/conffiles @@ -0,0 +1,3 @@ +/etc/1bit/power +/etc/issue +/etc/motd diff --git a/os/v2/debian/1bit-appliance/control b/os/v2/debian/1bit-appliance/control new file mode 100644 index 00000000..11ad644c --- /dev/null +++ b/os/v2/debian/1bit-appliance/control @@ -0,0 +1,14 @@ +Package: 1bit-appliance +Version: @VERSION@ +Architecture: amd64 +Maintainer: 1bit +Section: metapackages +Priority: optional +Depends: 1bit-engine (= @VERSION@), 1bit-lemonade (= @VERSION@), 1bit-archive-keyring, systemd, openssh-server, systemd-resolved, initramfs-tools +Replaces: base-files +Description: 1bit OS, Lemonade Server appliance - meta and configuration package + Turns a minimal Ubuntu into the appliance: the units that start + Lemonade and the engine, the console banner, autologin, the network + and /data mount, the fans and power profile, the first-boot setup + and the signed apt repository this box updates from. Lemonade + Server by AMD, running on the 1bit engine. diff --git a/os/v2/debian/1bit-appliance/files/etc/1bit/power b/os/v2/debian/1bit-appliance/files/etc/1bit/power new file mode 100644 index 00000000..f698d44f --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/1bit/power @@ -0,0 +1,26 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# 1bit OS power profile (read by 1bit-board from 1bit-board.service). Defaults are the development +# box's: the appliance is the same board and pad class. SKIP_POWER=1 leaves power alone entirely. +# The fan trip points are the first thing to revisit on a hotter unit. +MODE=performance +FAST_LIMIT=90000 +SLOW_LIMIT=90000 +SLOW_TIME=5 +TCTL_LIMIT=105000 +BOOST_TEMP=70 +BOOST_LEVEL=4 +LEVEL5_TEMP=82 +SKIP_POWER=0 diff --git a/os/v2/debian/1bit-appliance/files/etc/apt/sources.list.d/1bit.list b/os/v2/debian/1bit-appliance/files/etc/apt/sources.list.d/1bit.list new file mode 100644 index 00000000..07cd2605 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/apt/sources.list.d/1bit.list @@ -0,0 +1,4 @@ +# Our own signed repository: the engine, Lemonade, the kernel and modules, the board tools and the +# appliance meta package. Trust comes from the keyring file alone (signed-by), never from apt-key or +# [trusted=yes]. +deb [signed-by=/usr/share/keyrings/1bit-archive-keyring.gpg] https://1bit.gg/apt stable main diff --git a/os/v2/debian/1bit-appliance/files/etc/issue b/os/v2/debian/1bit-appliance/files/etc/issue new file mode 100644 index 00000000..8694186e --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/issue @@ -0,0 +1,8 @@ +1bit OS, Lemonade Server appliance +Lemonade Server by AMD, running on the 1bit engine + + Lemonade ................ http://:13305 (API key: /etc/1bit/lemonade.env) + 1bit-release-check ...... PASS/FAIL for this boot, one line per check + 1bit-selftest ........... prove the engine answers + 1bit-board status ....... fans and power, on the boards we know + apt update && apt upgrade the box updates from 1bit.gg/apt diff --git a/os/v2/debian/1bit-appliance/files/etc/motd b/os/v2/debian/1bit-appliance/files/etc/motd new file mode 100644 index 00000000..207cc0e7 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/motd @@ -0,0 +1,14 @@ +1bit OS, Lemonade Server appliance +Lemonade Server by AMD, running on the 1bit engine + +The console is root, on this machine. What is here, and what to run: + + 1bit-release-check PASS/FAIL for this boot (mode, kernel, Secure Boot, GPU, NPU, + Lemonade with its key, the catalog entries, a chat, the licences) + 1bit-selftest the engine answers, and where it ran + lemonade list the models; models go in /data/models + journalctl -u 1bit-lemonade -f Lemonade's log + apt update && apt upgrade update from 1bit.gg/apt (signed repository) + +The models, Lemonade's backends and configuration, and the SSH keys live on the 1BIT-DATA +partition, mounted at /data: they survive an update, a reinstall of the system does not touch them. diff --git a/os/v2/debian/1bit-appliance/files/etc/profile.d/1bit.sh b/os/v2/debian/1bit-appliance/files/etc/profile.d/1bit.sh new file mode 100644 index 00000000..e30e8255 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/profile.d/1bit.sh @@ -0,0 +1,19 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# PATH for the appliance's own commands, and nothing else that could surprise a login shell. +case ":$PATH:" in + *:/opt/1bit:*) ;; + *) PATH="/opt/1bit:/opt/lemonade/bin:$PATH"; export PATH ;; +esac diff --git a/os/v2/debian/1bit-appliance/files/etc/systemd/network/10-1bit.network b/os/v2/debian/1bit-appliance/files/etc/systemd/network/10-1bit.network new file mode 100644 index 00000000..dffeb911 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/systemd/network/10-1bit.network @@ -0,0 +1,21 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Every wired interface does DHCP: the appliance is reached over the network (Lemonade on 13305). +[Match] +Name=en* eth* + +[Network] +DHCP=yes +IPv6AcceptRA=yes diff --git a/os/v2/debian/1bit-appliance/files/etc/systemd/system/getty@tty1.service.d/1bit-autologin.conf b/os/v2/debian/1bit-appliance/files/etc/systemd/system/getty@tty1.service.d/1bit-autologin.conf new file mode 100644 index 00000000..83b53e78 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/systemd/system/getty@tty1.service.d/1bit-autologin.conf @@ -0,0 +1,19 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# The console is the appliance's face: autologin root on tty1, the banner above it. Same behaviour as +# the BusyBox image had, done the systemd way. +[Service] +ExecStart= +ExecStart=-/sbin/agetty --autologin root --noclear %I $TERM diff --git a/os/v2/debian/1bit-appliance/files/etc/systemd/system/ssh.service.d/1bit-first-boot.conf b/os/v2/debian/1bit-appliance/files/etc/systemd/system/ssh.service.d/1bit-first-boot.conf new file mode 100644 index 00000000..800c0e9f --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/etc/systemd/system/ssh.service.d/1bit-first-boot.conf @@ -0,0 +1,19 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# A fresh install has no SSH host keys yet: 1bit-first-boot generates them, so sshd must not start +# before it has. Ordering only - this drop-in never disables ssh. +[Unit] +Wants=1bit-first-boot.service +After=1bit-first-boot.service diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/1bit/ensure-env.sh b/os/v2/debian/1bit-appliance/files/usr/lib/1bit/ensure-env.sh new file mode 100755 index 00000000..f66a1c7e --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/1bit/ensure-env.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# First-boot state Lemonade cannot start without: the API key. Idempotent, so it runs as the service's +# ExecStartPre on every boot and the key never changes underneath the owner. +set -u +env=/etc/1bit/lemonade.env +[ -s "$env" ] && exit 0 +mkdir -p /etc/1bit +key=$(head -c 32 /dev/urandom | od -An -tx1 | tr -d ' \n') +umask 077 +{ + echo "# 1bit OS: Lemonade's environment. The API key is what release-check's lemonade row needs." + echo "LEMONADE_API_KEY=$key" + echo "LEMONADE_ADMIN_API_KEY=$key" +} > "$env" +chmod 600 "$env" +echo "1bit: wrote $env with a new LEMONADE_API_KEY" diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/1bit/first-boot.sh b/os/v2/debian/1bit-appliance/files/usr/lib/1bit/first-boot.sh new file mode 100755 index 00000000..7d6bf94b --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/1bit/first-boot.sh @@ -0,0 +1,26 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# First boot of an installed 1bit OS: own SSH host keys, and the marker release-check reads for the +# mode row. On a live boot /etc/1bit/live exists instead and this is skipped by the unit's condition. +set -u +mkdir -p /etc/1bit +if [ ! -s /etc/ssh/ssh_host_ed25519_key ]; then + ssh-keygen -q -t ed25519 -N '' -f /etc/ssh/ssh_host_ed25519_key +fi +# the live image's public key is not this machine's identity +: > /etc/1bit/installed +if [ -s /etc/1bit/live ]; then rm -f /etc/1bit/live; fi +echo "1bit: first boot on this machine (SSH host key generated, /etc/1bit/installed written)" diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system-preset/90-1bit.preset b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system-preset/90-1bit.preset new file mode 100644 index 00000000..081c1fda --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system-preset/90-1bit.preset @@ -0,0 +1,20 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# The appliance is the point of the image: these are on unless an operator disables them. +enable 1bit-lemonade.service +enable 1bit-first-boot.service +enable data.mount +enable 1bit-board.service +enable 1bit-boost.service diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-board.service b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-board.service new file mode 100644 index 00000000..49fb93c7 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-board.service @@ -0,0 +1,31 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Fans and the power profile, on the boards we know (the logic lives in /usr/sbin/1bit-board, shipped +# by the engine package; on any other board every command of it writes nothing). +[Unit] +Description=1bit OS: fans and power profile +ConditionPathExists=/usr/sbin/1bit-board +ConditionPathExists=!/etc/1bit/power-skip +After=local-fs.target + +[Service] +Type=oneshot +RemainAfterExit=yes +EnvironmentFile=-/etc/1bit/power +ExecStart=/usr/sbin/1bit-board curve +ExecStart=/usr/sbin/1bit-board power + +[Install] +WantedBy=multi-user.target diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-boost.service b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-boost.service new file mode 100644 index 00000000..8ceb2b24 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-boost.service @@ -0,0 +1,30 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# The predictive fan boost: one process, 0.5 s poll, releases to the EC curve when the machine is +# quiet. Restart=always, because a fan loop that dies quietly is how a board cooks. +[Unit] +Description=1bit OS: fan boost +ConditionPathExists=/usr/sbin/1bit-board +After=1bit-board.service +Requires=1bit-board.service + +[Service] +Type=simple +ExecStart=/usr/sbin/1bit-board boost +Restart=always +RestartSec=5 + +[Install] +WantedBy=multi-user.target diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-first-boot.service b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-first-boot.service new file mode 100644 index 00000000..3a83458e --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/1bit-first-boot.service @@ -0,0 +1,29 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Once, on the first boot of an installed machine: what a fresh install cannot carry - SSH host keys +# of its own (the live image's are public by definition) and the installed marker. +[Unit] +Description=1bit OS: first-boot setup +ConditionPathExists=!/etc/1bit/installed +After=local-fs.target +Wants=local-fs.target + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=/usr/lib/1bit/first-boot.sh + +[Install] +WantedBy=multi-user.target diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/data.mount b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/data.mount new file mode 100644 index 00000000..9935c4dd --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/systemd/system/data.mount @@ -0,0 +1,33 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# /data is the 1BIT-DATA partition on the machine's own disk: models, Lemonade's backends, its config +# and key, the NPU kernel sets, the SSH keys. Mounted by label so it is the same on a USB boot (the +# stick's own partition) and after an install; nofail so a machine whose data partition is missing +# still boots to a console that can say so. +[Unit] +Description=1bit OS: the data partition (1BIT-DATA) +Before=1bit-lemonade.service +DefaultDependencies=no +After=systemd-udev-settle.service local-fs-pre.target +Wants=local-fs-pre.target + +[Mount] +What=/dev/disk/by-label/1BIT-DATA +Where=/data +Type=ext4 +Options=defaults,nofail + +[Install] +WantedBy=local-fs.target diff --git a/os/v2/debian/1bit-appliance/files/usr/lib/tmpfiles.d/1bit.conf b/os/v2/debian/1bit-appliance/files/usr/lib/tmpfiles.d/1bit.conf new file mode 100644 index 00000000..8f4b39f1 --- /dev/null +++ b/os/v2/debian/1bit-appliance/files/usr/lib/tmpfiles.d/1bit.conf @@ -0,0 +1,23 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Directories on the data partition that must exist before Lemonade starts, even on a fresh stick. +d /data 0755 root root - - +d /data/lemonade 0755 root root - - +d /data/lemonade/config 0755 root root - - +d /data/lemonade/cache 0755 root root - - +d /data/models 0755 root root - - +d /data/cache 0755 root root - - +d /data/cache/hrx-jit 0755 root root - - +d /run/1bit 0755 root root - - diff --git a/os/v2/debian/1bit-appliance/postinst b/os/v2/debian/1bit-appliance/postinst new file mode 100755 index 00000000..6b12bc46 --- /dev/null +++ b/os/v2/debian/1bit-appliance/postinst @@ -0,0 +1,67 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# 1bit OS appliance: turn the units on. A minimal Ubuntu boots with almost nothing enabled, and the +# appliance is defined by what is enabled here. +# +# Two paths, because both happen: `deb-systemd-helper` (the Debian tool, honours each unit's [Install] +# section and works inside a chroot, which is how the image is built), and `systemctl` on a live +# system, so an already-running box picks the change up. +set -e + +# ssh: enable the *socket* when it exists. ssh.socket binds 22 and starts ssh.service per connection; +# enabling ssh.service as well makes it hold the port itself, and a connection then arrives at a socket +# nobody is accepting - a hang at banner exchange, measured in the VM. +set -- 1bit-lemonade.service 1bit-first-boot.service 1bit-board.service 1bit-boost.service \ + data.mount systemd-networkd.service systemd-networkd.socket systemd-resolved.service +# sshd on Ubuntu 26.04: ssh.socket is what listens on 22 and systemd hands each connection to +# ssh.service (the socket unit carries RequiredBy=ssh.service). So the socket is enabled and the service +# is not started by anything: enabling both makes a connection arrive at a service that is not accepting +# it (a client hangs at banner exchange, measured on the os/mini image), and masking the socket while +# enabling the service starts nothing at all - "Failed to start ssh.service: Unit ssh.socket is masked", +# measured in the v2 VM. One path, the distro's own. +set -- "$@" ssh.socket + +if command -v deb-systemd-helper >/dev/null 2>&1; then + deb-systemd-helper enable "$@" >/dev/null 2>&1 || true +fi +# In an image build nothing runs systemd, and deb-systemd-helper leaves some units behind (measured: +# systemd-networkd.service), which is how the appliance first came up with no network at all. The +# links are therefore written explicitly too - the same thing systemctl enable would do. +L=/etc/systemd/system +mkdir -p "$L/multi-user.target.wants" "$L/sockets.target.wants" "$L/local-fs.target.wants" +# ssh is in this list on purpose: in the image build deb-systemd-helper did not enable it (measured in +# the VM: "ssh.service ... disabled", so the appliance came up with nothing listening on 22). It is the +# *socket* that gets enabled, which is how Ubuntu 26.04 runs sshd. +for u in 1bit-lemonade 1bit-first-boot 1bit-board 1bit-boost systemd-networkd systemd-resolved; do + [ -e "/usr/lib/systemd/system/$u.service" ] || continue + ln -sf "/usr/lib/systemd/system/$u.service" "$L/multi-user.target.wants/$u.service" +done +for u in systemd-networkd systemd-networkd-resolve ssh; do + [ -e "/usr/lib/systemd/system/$u.socket" ] || continue + ln -sf "/usr/lib/systemd/system/$u.socket" "$L/sockets.target.wants/$u.socket" +done +[ -e /usr/lib/systemd/system/data.mount ] && + ln -sf /usr/lib/systemd/system/data.mount "$L/local-fs.target.wants/data.mount" +if [ -d /run/systemd/system ] && command -v systemctl >/dev/null 2>&1; then + systemctl daemon-reload || true + for u in "$@"; do systemctl enable "$u" >/dev/null 2>&1 || true; done + systemctl try-restart systemd-networkd.service >/dev/null 2>&1 || true + systemctl try-restart 1bit-lemonade.service >/dev/null 2>&1 || true +fi + +# the data partition's directories, so a fresh stick works before Lemonade's first start +command -v systemd-tmpfiles >/dev/null 2>&1 && systemd-tmpfiles --create /usr/lib/tmpfiles.d/1bit.conf 2>/dev/null || true +exit 0 diff --git a/os/v2/debian/1bit-archive-keyring/control b/os/v2/debian/1bit-archive-keyring/control new file mode 100644 index 00000000..5661e8f8 --- /dev/null +++ b/os/v2/debian/1bit-archive-keyring/control @@ -0,0 +1,11 @@ +Package: 1bit-archive-keyring +Version: @VERSION@ +Architecture: all +Maintainer: 1bit +Section: admin +Priority: optional +Description: signing key for the 1bit apt repository + The public half of the archive key that signs 1bit.gg/apt's + Release file. Every 1bit OS box trusts the repository through + this keyring and nothing else (apt's signed-by), so a package + this key did not sign is not installable. diff --git a/os/v2/debian/1bit-engine/control b/os/v2/debian/1bit-engine/control new file mode 100644 index 00000000..689aaed9 --- /dev/null +++ b/os/v2/debian/1bit-engine/control @@ -0,0 +1,19 @@ +Package: 1bit-engine +Version: @VERSION@ +Architecture: amd64 +Maintainer: 1bit +Section: admin +Priority: optional +Depends: libc6 (>= 2.39), libstdc++6, libgcc-s1, libgomp1, libpcre2-8-0 +Description: the 1bit engine and the server it runs models with + 1bit OS, Lemonade Server appliance: the engine binary, the HRX + llama-server and its libraries, the ds4 programs, the sealed NPU + kernel sets and the two commands that prove a boot (1bit-selftest, + 1bit-release-check). Installed under /opt/1bit, which is where the + engine looks once its build tree is gone. + . + The engine's own libraries are found beside it ($ORIGIN). The ROCm + userspace (HIP, rocBLAS, hipBLASLt, the HSA runtime) is the + machine's, not this package's: upstream ours came from the SDK the + build machine had, and on a box it has to come from packages + (see os/v2/README.md, "The ROCm userspace"). diff --git a/os/v2/debian/1bit-kernel-image/control b/os/v2/debian/1bit-kernel-image/control new file mode 100644 index 00000000..465ccdbf --- /dev/null +++ b/os/v2/debian/1bit-kernel-image/control @@ -0,0 +1,16 @@ +Package: 1bit-kernel-image-@KVER@ +Version: @VERSION@ +Architecture: amd64 +Maintainer: 1bit +Section: kernel +Priority: optional +Depends: 1bit-kernel-modules-@KVER@ (= @VERSION@), initramfs-tools, kmod +Provides: linux-image +Description: 1bit OS: our kernel for the appliance + Linux @KVER@ as this appliance runs it: amdgpu (the GPU and + /dev/kfd), amdxdna (the NPU), the board's EC and NPU support, built + and signed with the enrolled MOK, so shim under Secure Boot hands + over to it. The initramfs is generated by this package's postinst + from initramfs-tools, as on any Ubuntu box; the modules are in + 1bit-kernel-modules-@KVER@. The previous kernel version stays + installed on an upgrade, which is what makes the rollback work. diff --git a/os/v2/debian/1bit-kernel-image/postinst b/os/v2/debian/1bit-kernel-image/postinst new file mode 100755 index 00000000..0223b5f1 --- /dev/null +++ b/os/v2/debian/1bit-kernel-image/postinst @@ -0,0 +1,23 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# The initramfs is initramfs-tools' job, as on any Ubuntu box: this image is on / on 1BIT-ROOT, so the +# initramfs only has to find the root device (no engine, no Lemonade, no 103 MB limit any more). +set -e +kver=@KVER@ +if [ -x /usr/sbin/update-initramfs ]; then + update-initramfs -c -k "$kver" || true +fi +exit 0 diff --git a/os/v2/debian/1bit-kernel-image/postrm b/os/v2/debian/1bit-kernel-image/postrm new file mode 100755 index 00000000..746ea246 --- /dev/null +++ b/os/v2/debian/1bit-kernel-image/postrm @@ -0,0 +1,23 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Removing the kernel takes its initramfs with it; the running kernel is never the one being removed +# (apt keeps the previous version installed), so nothing here touches /boot/vmlinuz besides its own. +set -e +kver=@KVER@ +if [ -x /usr/sbin/update-initramfs ]; then + update-initramfs -d -k "$kver" || true +fi +exit 0 diff --git a/os/v2/debian/1bit-kernel-modules/control b/os/v2/debian/1bit-kernel-modules/control new file mode 100644 index 00000000..ee1914da --- /dev/null +++ b/os/v2/debian/1bit-kernel-modules/control @@ -0,0 +1,15 @@ +Package: 1bit-kernel-modules-@KVER@ +Version: @VERSION@ +Architecture: amd64 +Maintainer: 1bit +Section: kernel +Priority: optional +Depends: kmod +Description: 1bit OS: kernel modules for @KVER@ + Every module built for @KVER@ - amdgpu, amdxdna and the rest - plus + the out-of-tree board driver for this machine's embedded controller + (ec_su_axb35: fans, EC temperature, EC power mode), signed with the + enrolled MOK so Secure Boot loads it. The driver's source travels + with it under /usr/src/1bit-ec- (GPL-2.0). No DKMS: the kernel + version is fixed per release, so these are rebuilt with the kernel, + not on the box. diff --git a/os/v2/debian/1bit-kernel-modules/postinst b/os/v2/debian/1bit-kernel-modules/postinst new file mode 100755 index 00000000..daa29bfb --- /dev/null +++ b/os/v2/debian/1bit-kernel-modules/postinst @@ -0,0 +1,23 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# modules.dep & co for this kernel version, so a module is loadable the moment the package lands +# (initramfs-tools' own depmod run happens later, at the next initramfs build). +set -e +kver=@KVER@ +if command -v depmod >/dev/null 2>&1; then + depmod -a "$kver" || true +fi +exit 0 diff --git a/os/v2/debian/1bit-lemonade/1bit-lemonade.service b/os/v2/debian/1bit-lemonade/1bit-lemonade.service new file mode 100644 index 00000000..3008504b --- /dev/null +++ b/os/v2/debian/1bit-lemonade/1bit-lemonade.service @@ -0,0 +1,41 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Lemonade, our build: the appliance's server. Started by systemd, not by a custom init (os/v2). +[Unit] +Description=1bit OS: Lemonade Server (lemond) +Documentation=https://1bit.gg +After=network-online.target data.mount +Wants=network-online.target +RequiresMountsFor=/data + +[Service] +Type=simple +# lemond resolves its cache directory from HOME, and a systemd unit has none by default: without this +# it exits at once with "HOME is not set; cannot resolve Lemonade cache directory" (measured in the VM) +Environment=HOME=/data/lemonade +# the API key must exist before lemond reads its environment: a network-facing server without one is +# exactly what release-check's lemonade row fails on +ExecStartPre=/usr/lib/1bit/ensure-env.sh +# lemond takes its cache and config directories as positionals; /data is the 1BIT-DATA partition, so +# models, backends and the key survive an update +ExecStart=/opt/lemonade/bin/lemond --host 0.0.0.0 --port 13305 /data/lemonade/cache /data/lemonade/config +EnvironmentFile=-/etc/1bit/lemonade.env +EnvironmentFile=-/etc/1bit/libhsa.env +Restart=always +RestartSec=2 +LimitNOFILE=1048576 + +[Install] +WantedBy=multi-user.target diff --git a/os/v2/debian/1bit-lemonade/control b/os/v2/debian/1bit-lemonade/control new file mode 100644 index 00000000..0064f2c6 --- /dev/null +++ b/os/v2/debian/1bit-lemonade/control @@ -0,0 +1,18 @@ +Package: 1bit-lemonade +Version: @VERSION@ +Architecture: amd64 +Maintainer: 1bit +Section: admin +Priority: optional +Depends: libc6 (>= 2.39), libstdc++6, libgcc-s1, libssl3t64 | libssl3, zlib1g, libzstd1, + libcurl4t64 | libcurl4, libwebsockets19t64 | libwebsockets19, libcap2, libsystemd0, + libdrm-amdgpu1, libbrotli1 +Conflicts: lemonade-server +Replaces: lemonade-server +Provides: lemonade-server +Description: our build of Lemonade Server + 1bit OS, Lemonade Server appliance: lemond, the lemonade CLI and + the web app, built from the 1bit fork (the onebit recipe, the three + Qwen3 NPU catalog entries, the drop-in that points onebit at + /opt/1bit). This is our fork, not the upstream lemonade-server + package, and it conflicts with it. diff --git a/os/v2/debian/1bit-ryzenadj/control b/os/v2/debian/1bit-ryzenadj/control new file mode 100644 index 00000000..45e2a904 --- /dev/null +++ b/os/v2/debian/1bit-ryzenadj/control @@ -0,0 +1,15 @@ +Package: 1bit-ryzenadj +Version: @VERSION@ +Architecture: amd64 +Maintainer: 1bit +Section: admin +Priority: optional +Depends: libc6 (>= 2.39), libpci3, libudev1, zlib1g +Description: SMU power limits for this board (RyzenAdj v0.19.0) + The tool 1bit-board writes the power profile with: sustained, slow + and fast PPT limits and the die temperature limit through the SMU. + The EC power mode on its own gives the firmware's profile; the 90 W + sustained cap this appliance ships needs these limits written. + Built from the pinned source shipped in /usr/share/doc/1bit-ryzenadj + (v0.19.0, LGPL-3.0, Copyright Jiaxun Yang and the RyzenAdj + contributors), which is what LGPL-3.0 asks a binary to travel with. diff --git a/os/v2/diag-v2.sh b/os/v2/diag-v2.sh new file mode 100755 index 00000000..fa4c4cd9 --- /dev/null +++ b/os/v2/diag-v2.sh @@ -0,0 +1,96 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# diag-v2.sh -- boot the v2 rootfs and ASK the guest what failed. +# Same disks and guest as vmtest-v2.sh; the only difference is what the test unit runs. Nothing is +# judged here: this is for reading, when a vmtest-v2 assertion fails and the reason is inside the guest. +set -uo pipefail +rootfs=$(cd "$1" && pwd) seed=$(cd "$2" && pwd) +if [ "$(id -u)" != 0 ]; then exec sudo -n "$0" "$@"; fi +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT +kern=$(ls "$rootfs"/boot/vmlinuz-* 2>/dev/null | head -1) +initrd=$(ls "$rootfs"/boot/initrd.img-* 2>/dev/null | head -1) +echo "kernel: $(basename "$kern")" +install -d -m 755 "$rootfs/usr/lib/1bit" +cat > "$rootfs/usr/lib/1bit/vmtest.sh" <<'INGUEST' +#!/bin/sh +exec > /data/vmtest.txt 2>&1 +echo "== failed units" +systemctl --failed --no-pager --no-legend +echo "== lemonade unit" +systemctl status 1bit-lemonade.service --no-pager --lines=0 2>&1 | head -12 +journalctl -u 1bit-lemonade.service -n 40 --no-pager 2>&1 +echo "== ssh unit" +systemctl is-enabled ssh.service 2>&1 +systemctl show ssh.service -p ConditionResult -p ConditionTimestamp -p AssertResult -p ActiveState -p UnitFileState 2>&1 +systemctl show multi-user.target -p Wants 2>&1 | tr ' ' '\n' | grep -i ssh +systemctl start ssh.service 2>&1; sleep 3 +systemctl is-active ssh.service 2>&1 +awk 'NR>1 {print $2}' /proc/net/tcp 2>/dev/null | grep -ci ':0016$' +journalctl -b --no-pager 2>&1 | grep -iE 'ssh.service|sshd' | tail -12 +journalctl -b --no-pager 2>&1 | grep -ciE 'ordering cycle' +echo "== data" +ls -la /data /data/lemonade /data/lemonade/config /data/lemonade/cache 2>&1 +cat /data/lemonade/config/config.json 2>&1 +ls -l /data/lemonade/cache/bin 2>&1 | head -5 +echo "== lemonade binary" +ldd /opt/lemonade/bin/lemond 2>&1 | grep -E 'not found|=>' | head -25 +/opt/lemonade/bin/lemond --help 2>&1 | head -8 +echo "== engine binary" +ldd /opt/1bit/1bit 2>&1 | grep 'not found' | head +/opt/1bit/1bit --help 2>&1 | head -5 +/opt/1bit/hrx/llama/bin/llama-server --version 2>&1 | head -2 +echo "== network" +ip -br a 2>&1 +systemctl status systemd-networkd.service --no-pager --lines=0 2>&1 | head -6 +echo "== end" +poweroff -f +INGUEST +chmod 755 "$rootfs/usr/lib/1bit/vmtest.sh" +cat > "$rootfs/etc/systemd/system/1bit-vmtest.service" <<'UNIT' +[Unit] +Description=test-only: diagnostics +After=multi-user.target 1bit-lemonade.service 1bit-first-boot.service +Wants=1bit-lemonade.service +[Service] +Type=oneshot +ExecStartPre=/bin/sleep 10 +ExecStart=/usr/lib/1bit/vmtest.sh +[Install] +WantedBy=multi-user.target +UNIT +ln -sfn /etc/systemd/system/1bit-vmtest.service \ + "$rootfs/etc/systemd/system/multi-user.target.wants/1bit-vmtest.service" +rootimg=$work/rootfs.img; dataimg=$work/data.img +truncate -s "$(( $(du -sm "$rootfs" | cut -f1) + 2048 ))M" "$rootimg" +mkfs.ext4 -q -F -L 1BIT-ROOT -d "$rootfs" "$rootimg" +truncate -s 8G "$dataimg" +mkfs.ext4 -q -F -L 1BIT-DATA -d "$seed" "$dataimg" +qemu-system-x86_64 -enable-kvm -cpu host -m 8G -smp 8 -machine q35 \ + -kernel "$kern" -initrd "$initrd" \ + -append "root=/dev/vda rw console=ttyS0,115200 loglevel=4" \ + -drive file="$rootimg",if=virtio,format=raw,cache=writeback \ + -drive file="$dataimg",if=virtio,format=raw,cache=writeback \ + -nic user,model=virtio-net-pci \ + -display none -serial file:"$work/serial.log" -no-reboot & +vm=$! +trap 'kill $vm 2>/dev/null; rm -rf "$work"' EXIT +waited=0 +while kill -0 $vm 2>/dev/null && [ $waited -lt 420 ]; do sleep 5; waited=$((waited + 5)); done +kill $vm 2>/dev/null +mkdir -p "$work/data" +mount -o ro,loop,noload "$dataimg" "$work/data" 2>/dev/null && { + cat "$work/data/vmtest.txt" 2>/dev/null; umount "$work/data"; } diff --git a/os/v2/install.sh b/os/v2/install.sh new file mode 100644 index 00000000..7887e6c5 --- /dev/null +++ b/os/v2/install.sh @@ -0,0 +1,253 @@ +#!/bin/sh +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# 1bit-install /dev/nvme0n1 --yes +# +# Installs 1bit OS from this live boot onto ONE internal disk, in the layout an installed box has: +# +# partition 1 512 MB EFI system partition (1BIT-OS) - shim, our MOK-signed grub, and a grub.cfg +# that boots the disk's own /boot +# partition 2 ext4 1BIT-ROOT - the live squashfs unpacked, plus /boot (kernel + initramfs) and an +# fstab that mounts / and /data by label +# partition 3 ext4 1BIT-DATA - the stick's /data (models, Lemonade's backends and config, SSH keys) +# +# 1BIT-ROOT is min(64 GB, max(24 GB, 25 % of the disk)); 1BIT-DATA is the rest. +# +# It writes a disk, so every refusal is checked before anything is touched: no --yes, a partition +# instead of a whole disk, a removable device, the disk this live boot runs from, a device with +# anything mounted, or a missing GPT/filesystem tool. Then it prints the disk and waits 10 s. +# +# Exit: 0 installed, 1 usage, 2 refused (nothing written), 3 failed while writing. +set -u + +ESP_MB=512 +ESP_LABEL=1BIT-OS +ROOT_LABEL=1BIT-ROOT +DATA_LABEL=1BIT-DATA +# by absolute path, and checked before anything is written - the rule #539 learned the hard way on the +# BusyBox image (its shell ran its own applet for a name it knew, before PATH). This base has no busybox, +# but the installer names its tools the same way so the two images cannot drift. +SFDISK=/usr/sbin/sfdisk +MKE2FS=/usr/sbin/mke2fs +MKFS_VFAT=/usr/sbin/mkfs.vfat +UNSQUASHFS=/usr/bin/unsquashfs +EFIBOOTMGR=/usr/bin/efibootmgr +# the live squashfs is a partition of the live medium (PARTLABEL, not a filesystem label: squashfs has no +# label of its own), mounted read-only as the overlay's lower layer by the live initramfs +LIVE_SQ=/dev/disk/by-partlabel/1BIT-LIVE + +new_part() { # <1|2|3> -> the kernel's device name for that partition + case "${1##*/}" in + *[0-9]) echo "$1p$2" ;; # nvme0n1, mmcblk0: a digit glued to a number needs the p + *) echo "$1$2" ;; # sda, vda, hda + esac +} + +usage() { + sed -n '/^# 1bit-install \/dev/,/^# Exit:/p' "$0" 2>/dev/null | sed 's/^# \{0,1\}//' +} + +die() { printf '1bit-install: REFUSED: %s\n' "$*" >&2; exit 2; } + +DEV="" +YES=0 +for a in "$@"; do + case "$a" in + --yes|-y) YES=1 ;; + --help|-h) usage; exit 0 ;; + -*) printf '1bit-install: unknown option %s\n' "$a" >&2; exit 1 ;; + *) if [ -z "$DEV" ]; then DEV=$a; else printf '1bit-install: one target disk, not two\n' >&2; exit 1; fi ;; + esac +done + +if [ -z "$DEV" ]; then + printf '1bit-install: name the disk to install onto, and pass --yes when you mean it.\n\n' >&2 + usage >&2 + exit 1 +fi + +# --- refusals, before anything is written ------------------------------------------------------------ +[ -b "$DEV" ] || die "$DEV is not a block device" +name=${DEV#/dev/}; name=${name##*/} +sys=/sys/class/block/$name +[ -d "$sys" ] || die "$DEV is not a disk this kernel knows" +[ -e "$sys/partition" ] && die "$DEV is a partition; name the whole disk (e.g. /dev/nvme0n1, not /dev/nvme0n1p1)" +[ "$YES" = 1 ] || die "refusing to write $DEV without --yes (this destroys its contents)" +[ "$(cat "$sys/removable" 2>/dev/null || echo 0)" = 1 ] && + die "$DEV is a removable device; 1bit OS installs onto an internal disk, and the live stick is left as it is" + +# The disk this live boot is running from: /data is the stick's 1BIT-DATA partition, mounted by the live +# system. Ask /proc/mounts, never findfs: once a machine has been installed there are two 1BIT-DATA +# partitions and two 1BIT-OS ones (the stick's and the installed disk's) and findfs may answer with the +# wrong one - #539 measured exactly that, and it made a reinstall impossible. +live=$(awk '$2 == "/data" { print $1; exit }' /proc/mounts) +live_esp="" +if [ -n "$live" ]; then + lname=${live#/dev/}; lname=${lname##*/} + ldisk=$(basename "$(readlink -f "/sys/class/block/$lname/.." 2>/dev/null)" 2>/dev/null || true) + [ "$ldisk" = "$name" ] && die "$DEV carries this live boot's 1BIT-DATA partition; install onto a different disk" + [ -n "$ldisk" ] && live_esp=$(new_part "/dev/$ldisk" 1) +fi + +# anything mounted from this disk (any partition of it) means it is in use +der=$(awk -v d="/dev/$name" 'index($1, d) == 1 { print $1; exit }' /proc/mounts 2>/dev/null) +[ -n "${der:-}" ] && die "$DEV is in use: $der is mounted" + +for t in "$SFDISK" "$MKE2FS" "$MKFS_VFAT" "$UNSQUASHFS"; do + [ -x "$t" ] || die "$(basename "$t") is missing from this image; cannot lay out or fill a disk" +done +[ -b "$LIVE_SQ" ] || die "this boot has no live squashfs at $LIVE_SQ; it cannot install itself" + +# --- what it is about to destroy --------------------------------------------------------------------- +model=$(cat "$sys/device/model" 2>/dev/null | tr -s ' ' | sed 's/^ //;s/ $//') +[ -n "$model" ] || model="(no model reported)" +bytes=$(blockdev --getsize64 "$DEV" 2>/dev/null || echo 0) +gb=$((bytes / 1000000000)) +total_mb=$((bytes / 1000000)) +root_mb=$((total_mb / 4)) +[ "$root_mb" -lt 24576 ] && root_mb=24576 +[ "$root_mb" -gt 65536 ] && root_mb=65536 +[ "$total_mb" -gt $((ESP_MB + root_mb + 1024)) ] || + die "$DEV is ${gb} GB: too small for a ${ESP_MB} MB ESP, ${root_mb} MB root and a data partition" + +echo "1bit OS install" +echo " target : $DEV ($model, ${gb} GB)" +echo " layout : GPT - ${ESP_MB} MB EFI system partition ($ESP_LABEL), ${root_mb} MB ext4 ($ROOT_LABEL), the rest ext4 ($DATA_LABEL)" +echo " source : this live boot (its squashfs, its ESP, and its /data)" +echo " current contents of $DEV, about to be destroyed:" +$SFDISK -l "$DEV" 2>/dev/null | sed 's/^/ /' +echo +echo " Nothing is written for 10 seconds." +i=10 +while [ "$i" -gt 0 ]; do + printf '\r starting in %2d s ' "$i" + sleep 1 + i=$((i - 1)) +done +printf '\r writing now \n' + +# --- partition -------------------------------------------------------------------------------------- +esp=$(new_part "$DEV" 1) +root=$(new_part "$DEV" 2) +data=$(new_part "$DEV" 3) + +printf 'label: gpt\nstart=2048, size=%s, type=U, name="EFI"\nsize=%s, type=L, name="%s"\ntype=L, name="%s"\n' \ + $((ESP_MB * 2048)) $((root_mb * 2048)) "$ROOT_LABEL" "$DATA_LABEL" | + $SFDISK -q "$DEV" || { echo "1bit-install: sfdisk failed" >&2; exit 3; } + +i=0 +while [ ! -b "$esp" ] || [ ! -b "$root" ] || [ ! -b "$data" ]; do + i=$((i + 1)) + [ "$i" -gt 20 ] && { echo "1bit-install: the kernel never published $esp / $root / $data" >&2; exit 3; } + sleep 1 +done +sleep 1 +$SFDISK -l "$DEV" 2>/dev/null | sed 's/^/ /' + +$MKFS_VFAT -F 32 -n "$ESP_LABEL" "$esp" >/dev/null || { echo "1bit-install: mkfs.vfat failed" >&2; exit 3; } +$MKE2FS -q -t ext4 -L "$ROOT_LABEL" "$root" || { echo "1bit-install: mke2fs failed (root)" >&2; exit 3; } +$MKE2FS -q -t ext4 -L "$DATA_LABEL" "$data" || { echo "1bit-install: mke2fs failed (data)" >&2; exit 3; } + +T=/tmp/install +mkdir -p "$T/esp" "$T/root" "$T/data" "$T/live-esp" +mount -t vfat "$esp" "$T/esp" || { echo "1bit-install: cannot mount the new ESP" >&2; exit 3; } +mount -t ext4 "$root" "$T/root" || { echo "1bit-install: cannot mount the new root" >&2; exit 3; } +mount -t ext4 "$data" "$T/data" || { echo "1bit-install: cannot mount the new data partition" >&2; exit 3; } + +# --- the root filesystem: the live squashfs, unpacked ------------------------------------------------ +echo "1bit-install: unpacking the live system onto $root" +$UNSQUASHFS -f -d "$T/root" "$LIVE_SQ" >/dev/null || + { echo "1bit-install: unsquashfs failed" >&2; exit 3; } + +# an installed machine is not a live one: the marker the live image ships is what release-check reads, +# and 1bit-first-boot.service writes /etc/1bit/installed after it has made this machine's own SSH host +# keys. Writing that marker here would skip the first boot and leave the live image's public key as this +# machine's identity - so this removes one and writes the other, and never the other way round. +rm -f "$T/root/etc/1bit/live" +mkdir -p "$T/root/etc/1bit" +{ + echo "# Written by 1bit-install. Mounted by label, so the same fstab works on any disk." + echo "LABEL=$ROOT_LABEL / ext4 defaults,noatime 0 1" + echo "LABEL=$DATA_LABEL /data ext4 defaults,nofail 0 2" +} > "$T/root/etc/fstab" + +# what the box updates from: the repository and the key are packages, not files written here. Only +# assert that the source is the production one, so an install cannot silently come out with a test source. +[ -f "$T/root/etc/apt/sources.list.d/1bit.list" ] || + echo "1bit-install: WARNING - /etc/apt/sources.list.d/1bit.list is not in the live system" >&2 + +# --- the ESP: the live boot's own signed chain, plus a grub.cfg for the disk ------------------------- +if [ -n "$live_esp" ] && mount -t vfat -o ro "$live_esp" "$T/live-esp" 2>/dev/null; then + cp -a "$T/live-esp"/. "$T/esp"/ || { echo "1bit-install: copying the ESP failed" >&2; exit 3; } + umount "$T/live-esp" +else + echo "1bit-install: the live ESP is not mountable; the installed disk would not boot" >&2 + exit 3 +fi +# shim hands over to grubx64.efi beside it, which reads this file. The kernel and initramfs are on the +# root partition now (that is what a package-managed /boot means), so grub searches for the label and +# reads /boot from it - and the live boot's own copies of them, stale the moment apt upgrades the +# kernel, come off the ESP. One entry per installed kernel: that is the rollback apt kept installed. +rm -f "$T/esp/vmlinuz" "$T/esp/initrd.img" +{ + echo "# Written by 1bit-install: boot the installed 1BIT-ROOT, one entry per kernel in /boot." + echo "set default=0" + echo "set timeout=3" + emit() { # + echo "menuentry '1bit OS, Lemonade Server appliance (kernel $1)' {" + echo " search --no-floppy --label $ROOT_LABEL --set=root" + echo " linux /boot/vmlinuz-$1 root=LABEL=$ROOT_LABEL ro quiet" + echo " initrd /boot/initrd.img-$1" + echo "}" + } + for v in $(ls "$T/root"/boot/vmlinuz-* 2>/dev/null | sed 's|.*/vmlinuz-||' | sort -V -r); do + emit "$v" + done +} > "$T/esp/EFI/BOOT/grub.cfg" +sync + +# --- the data partition: the stick's own /data ------------------------------------------------------- +if grep -q " /data " /proc/mounts; then + echo "1bit-install: copying /data ($(du -sh /data 2>/dev/null | cut -f1)) onto the disk" + cp -a /data/. "$T/data/" || { echo "1bit-install: copying /data failed" >&2; exit 3; } +else + echo "1bit-install: no /data on this boot; the installed disk gets an empty data partition" + mkdir -p "$T/data/models" "$T/data/lemonade/config" "$T/data/lemonade/cache" "$T/data/cache/hrx-jit" +fi +sync + +umount "$T/esp" && umount "$T/root" && umount "$T/data" || + { echo "1bit-install: cannot flush the new partitions" >&2; exit 3; } + +# --- the firmware entry ------------------------------------------------------------------------------ +# The removable-media path (\\EFI\\BOOT\\BOOTX64.EFI) already boots this ESP; an explicit entry keeps +# the boot order stable after other media has been present. Not fatal: a VM's firmware may refuse it. +if [ -x "$EFIBOOTMGR" ] && [ -d /sys/firmware/efi ]; then + $EFIBOOTMGR -c -d "$DEV" -p 1 -L "1bit OS" -l '\EFI\BOOT\BOOTX64.EFI' >/dev/null 2>&1 && + echo "1bit-install: firmware boot entry '1bit OS' created" || + echo "1bit-install: WARNING - efibootmgr could not create an entry; the removable-media path still boots" +fi + +echo +echo "1bit OS is installed on $DEV." +echo " partition 1: $esp ${ESP_MB} MB, EFI system partition ($ESP_LABEL) - shim, grub, grub.cfg" +echo " partition 2: $root ${root_mb} MB, ext4 ($ROOT_LABEL) - the system, its packages and /boot" +echo " partition 3: $data ext4 ($DATA_LABEL) - the models, backends, configuration and keys" +echo +echo " Next: power off, remove the stick, and boot the machine from this disk." +echo " The first boot generates this machine's own SSH host keys and writes /etc/1bit/installed." +echo " Secure Boot needs no change: shim, grub and the kernel on this disk are the MOK-signed files the stick booted." +echo " Updates: apt update && apt upgrade. The previous kernel stays installed; apt can go back to it." diff --git a/os/v2/mkdebs.sh b/os/v2/mkdebs.sh new file mode 100755 index 00000000..b6475d54 --- /dev/null +++ b/os/v2/mkdebs.sh @@ -0,0 +1,345 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# mkdebs.sh [archive-keyring.gpg] +# +# Builds the appliance's packages from what a release build already produced - no binary is rebuilt +# here, dpkg-deb only wraps it: +# +# 1bit-engine the engine, its llama-server and libraries, the sealed NPU kernel sets, the +# two commands that prove a boot -> /opt/1bit +# 1bit-lemonade our build of Lemonade Server (lemond, CLI, web app) -> /opt/lemonade +# 1bit-appliance the meta/config package: units, banner, first boot, apt source +# 1bit-archive-keyring the public half of the key that signs 1bit.gg/apt +# +# With KERNEL_STAGE= set it also builds the three packages that only a box needs, from the +# release kernel build as it sits on the build machine (/boot/vmlinuz-, System.map-, +# config- and /lib/modules//): +# +# 1bit-kernel-image- vmlinuz + System.map + config, initramfs generated by its postinst +# 1bit-kernel-modules- every module for (amdgpu, amdxdna, the board's EC driver) and +# the EC driver's source under /usr/src/1bit-ec- +# 1bit-ryzenadj built here from os/licenses/ryzenadj/src (v0.19.0, LGPL-3.0), +# with its source beside it: the 90 W SMU cap needs it +# +# is either the release tarball (1bit--linux-x86_64.tar.zst from +# scripts/package-linux.sh) or an unpacked stage directory; is a build-lemonade.sh +# prefix (bin/lemond, bin/lemonade, bin/resources). is the release tag, with or without the +# leading v: v2026.43 becomes 2026.43-1. +set -euo pipefail +[ $# -ge 4 ] || { sed -n '22,45p' "$0"; exit 2; } +stage_in=$1 lemon=$2 version=${3#v} out=$4 keyring=${5:-} kstage=${KERNEL_STAGE:-} kver= +here=$(cd "$(dirname "$0")" && pwd) +src=$(cd "$here/../.." && pwd) +debver="$version-1" +[ -n "$version" ] || { echo "mkdebs: empty version"; exit 2; } +[ -x "$lemon/bin/lemond" ] || { echo "mkdebs: no $lemon/bin/lemond (a build-lemonade.sh prefix)"; exit 2; } +mkdir -p "$out" +work=$(mktemp -d) +trap 'rm -rf "$work"' EXIT + +# --- the kernel: only a box needs these, so a VM/rootfs build runs without a stage at all ---------- +# The version is read here and not where the packages are built, because 1bit-appliance depends on the +# three kernel packages by name and is packaged earlier in this script. +if [ -n "$kstage" ]; then + kmods=$(ls -d "$kstage"/lib/modules/*/ 2>/dev/null | head -1) + [ -n "$kmods" ] || { echo "mkdebs: $kstage carries no lib/modules/"; exit 2; } + kver=$(basename "$kmods") + for f in vmlinuz System.map config; do + [ -f "$kstage/boot/$f-$kver" ] || { echo "mkdebs: $kstage carries no boot/$f-$kver"; exit 2; } + done + # the EC driver is what gives the box fan control, and Secure Boot only loads it signed: check it + # against the kernel it was built for here, so a wrong pair fails the package build, not the boot + ec=$kstage/lib/modules/$kver/updates/ec_su_axb35.ko + [ -f "$ec" ] || { echo "mkdebs: no ec_su_axb35.ko in $kstage for $kver"; exit 2; } + [ "$(modinfo -F vermagic "$ec" 2>/dev/null | cut -d' ' -f1)" = "$kver" ] || { + echo "mkdebs: ec_su_axb35.ko is not built for $kver"; exit 1; } + [ -n "$(modinfo -F signer "$ec" 2>/dev/null)" ] || { + echo "mkdebs: ec_su_axb35.ko is unsigned: Secure Boot would refuse it"; exit 1; } + echo "== kernel: $kver, $(du -sh "$kstage/lib/modules/$kver" | cut -f1) of modules, EC driver signed by '$(modinfo -F signer "$ec" | cut -d: -f2)'" +fi + +# --- the engine stage: a tarball or a directory, never a fresh build -------------------------------- +case "$stage_in" in + *.tar.zst) + mkdir -p "$work/stage" + tar --zstd -xf "$stage_in" -C "$work/stage" + stage=$(find "$work/stage" -maxdepth 1 -mindepth 1 -type d | head -1) + ;; + *) + # a build directory: take the same surface scripts/package-linux.sh packs, not the build tree - + # the tree carries objects and CMakeFiles that have no business in a package (1.3 GB vs ~150 MB) + stage=$work/stage + mkdir -p "$stage" + cp -a "$stage_in/1bit" "$stage/" + put() { # : those programs plus every shared library beside them + local rel=$1 f; shift + [ -d "$stage_in/$rel" ] || return 0 + mkdir -p "$stage/$rel" + for f in "$@"; do [ -f "$stage_in/$rel/$f" ] && cp -a "$stage_in/$rel/$f" "$stage/$rel/"; done + find "$stage_in/$rel" -maxdepth 1 \( -name '*.so' -o -name '*.so.*' \) -exec cp -a {} "$stage/$rel/" \; + } + put hrx/llama/bin llama-server llama-bench + put ds4/hip ds4-server ds4 ds4-bench + # the sealed NPU sets: the artifact surface only (layout.json + layer1_*/layer1.elf) + if [ -d "$stage_in/npu/kernels" ]; then + mkdir -p "$stage/npu/kernels" + for set in "$stage_in"/npu/kernels/*/; do + sn=$(basename "$set") + [ -f "$set/layout.json" ] || continue + mkdir -p "$stage/npu/kernels/$sn" + cp -a "$set/layout.json" "$stage/npu/kernels/$sn/" + for layer in "$set"/layer1_*/; do + [ -f "$layer/layer1.elf" ] || continue + mkdir -p "$stage/npu/kernels/$sn/$(basename "$layer")" + cp -a "$layer/layer1.elf" "$stage/npu/kernels/$sn/$(basename "$layer")/" + done + done + fi + for d in scripts tools; do [ -d "$stage_in/$d" ] && cp -a "$stage_in/$d" "$stage/"; done + echo " stage: $(du -sh "$stage" | cut -f1) from $stage_in (1bit, llama-server, ds4, npu sets, scripts)" + ;; +esac +[ -x "$stage/1bit" ] || { echo "mkdebs: $stage has no 1bit binary"; exit 2; } +engine_version=$("$stage/1bit" version 2>/dev/null | head -1) +engine_version=${engine_version:-unknown} + +# a new package tree, with the Apache/Apache-2.0 notice files the release carries. @KVER@ is the +# kernel version for the three packages whose names carry it (Debian's own linux-image- rule: +# two kernels can be installed at once, which is what makes the previous one the rollback) +newpkg() { # -> sets $p + p="$work/pkg/$1" + mkdir -p "$p/DEBIAN" + sed -e "s/@VERSION@/$debver/g" -e "s/@KVER@/$kver/g" "$here/debian/$1/control" > "$p/DEBIAN/control" + chmod 644 "$p/DEBIAN/control" +} +# the file name apt expects to find in the pool: Package_Version_Arch.deb, read back out of the +# control file, so a package whose name carries the kernel version needs no second guess +builddeb() { + local name + name=$(sed -n 's/^Package: //p' "$p/DEBIAN/control") + dpkg-deb --build --root-owner-group "$p" "$out/${name}_${debver}_amd64.deb" >/dev/null +} +# and the maintainer scripts, if that package has any +withscripts() { # + local f + for f in "$here/debian/$1"/{postinst,preinst,prerm,postrm,conffiles}; do + [ -e "$f" ] || continue + sed -e "s/@VERSION@/$debver/g" -e "s/@KVER@/$kver/g" "$f" > "$p/DEBIAN/$(basename "$f")" + chmod 755 "$p/DEBIAN/$(basename "$f")" + done +} +doc() { # : licence files a package must carry + local d=$1/usr/share/doc/$2 + mkdir -p "$d" + for f in LICENSE NOTICE LICENSE-BINARY NOTICE-BINARY; do + if [ -e "$stage/$f" ]; then cp "$stage/$f" "$d/$f" + elif [ -e "$src/$f" ]; then cp "$src/$f" "$d/$f"; fi + done +} +# Every program and library finds its libraries beside itself. The release tarball gets this from +# scripts/package-linux.sh's pack_localize; a stage taken from a build (or from an older package) still +# carries the build machine's RUNPATH, and in the package that path is gone - which is exactly how a +# dev-built llama-server came to ship unable to load libllama-server-impl.so beside it (#549, measured +# in the v2 VM). Entries that are not the build machine's home (a packaged /opt) are kept. +localize() { # + local f rp kept + while IFS= read -r f; do + rp=$(patchelf --print-rpath "$f" 2>/dev/null) || continue + case ":$rp:" in *:/home/*) ;; *) continue ;; esac + kept=$(printf '%s' "$rp" | tr ':' '\n' | grep -v '^/home/' | grep -v '^$' | tr '\n' ':' | sed 's/:$//' || true) + patchelf --set-rpath "\$ORIGIN${kept:+:$kept}" "$f" + done < "$p/etc/1bit/engine.conf" <<'CONF' +# Paths the engine and Lemonade's onebit backend read. Static, because dpkg puts them here - the +# BusyBox image had to compute them at boot (os/mini/init.sh, os/mini/mkimage.sh). +ONEBIT_HRX_SERVER=/opt/1bit/hrx/llama/bin/llama-server +GGML_HRX_JIT_CACHE_DIR=/data/cache/hrx-jit +CONF +doc "$p" 1bit-engine +if [ -n "${ENGINE_BUILD:-}" ]; then + # the house localiser from scripts/pack-elf.sh, the one scripts/package-linux.sh uses: every program + # and library finds its libraries beside itself, the ones it loaded from elsewhere in the build tree + # (HRX's libhrx/libloomc, LLVM's libomp) are copied next to it, and RUNPATH becomes $ORIGIN (#549). + # The stage has the build's layout (mkdebs copies 1bit's surface relative to 1bit), which is what + # lets it map a packaged file back to the build it came from. + . "$src/scripts/pack-elf.sh" + pack_localize "$p/opt/1bit" "$(cd "$ENGINE_BUILD" && pwd)" + patchelf --remove-rpath "$p/opt/1bit/1bit" # the engine binary needs no search path at all +else + # a stage with no build tree behind it (a tarball, an older package): all that can be done is to + # stop the build machine's paths travelling; the check below catches a library left behind + localize "$p/opt/1bit" + echo "mkdebs: ENGINE_BUILD not given - RUNPATH rewritten to \$ORIGIN, libraries from a build tree NOT added" >&2 +fi +# the sealed NPU kernel sets must be the artifacts only: plaintext layer1.work/ would leak how the +# kernels are made, so the package build refuses anything else (the same rule mkimage.sh enforces) +if [ ! -d "$p/opt/1bit/npu/kernels" ]; then + # a release stage always carries the sealed sets (they are what makes the NPU entries run); a + # development stage on a machine without them can still package the rest, loudly + echo "mkdebs: WARNING - this stage carries no npu/kernels: the package ships without the sealed NPU sets" >&2 +fi +if [ -d "$p/opt/1bit/npu/kernels" ]; then + total=$(find "$p/opt/1bit/npu/kernels" -name layer1.elf | wc -l) + sealed=$(find "$p/opt/1bit/npu/kernels" -name layer1.elf -exec sh -c 'head -c 8 "$1" | grep -q 1BPKCRYP' _ {} \; -print | wc -l) + stray=$(find "$p/opt/1bit/npu/kernels" -mindepth 3 \( -name '*.mlir' -o -name '*.o' -o -name 'layer1.work' -o -name '*.pdi' \) | head -3) + [ "$total" -gt 0 ] || { echo "mkdebs: the stage carries no sealed NPU kernel sets"; exit 1; } + [ "$sealed" = "$total" ] || { echo "mkdebs: $sealed of $total kernels are sealed in the stage"; exit 1; } + [ -z "$stray" ] || { echo "mkdebs: the stage carries build artifacts, not just layout.json + layer1.elf:"; echo "$stray"; exit 1; } + echo " npu/kernels: $sealed sealed kernels" +fi +# the engine's llama-server must resolve its own libraries from the package, not from a build tree +if [ -x "$p/opt/1bit/hrx/llama/bin/llama-server" ]; then + miss=$(ldd "$p/opt/1bit/hrx/llama/bin/llama-server" 2>/dev/null | awk '/not found/ {print $1}' | tr '\n' ' ') + [ -z "$miss" ] || { echo "mkdebs: llama-server misses libraries: $miss"; exit 1; } +fi +dpkg-deb --build --root-owner-group "$p" "$out/1bit-engine_${debver}_amd64.deb" >/dev/null + +echo "== 1bit-lemonade" +newpkg 1bit-lemonade +mkdir -p "$p/opt/lemonade" +cp -a "$lemon/bin" "$p/opt/lemonade/" +localize "$p/opt/lemonade" +mkdir -p "$p/usr/lib/systemd/system" +# the unit travels with the server it starts (design ยง3.2), not with the meta package +install -m 644 "$here/debian/1bit-lemonade/1bit-lemonade.service" \ + "$p/usr/lib/systemd/system/1bit-lemonade.service" +mkdir -p "$p/usr/share/doc/1bit-lemonade" +# the credit line, verbatim, where the licences live: NOTICE travels with the package +cat > "$p/usr/share/doc/1bit-lemonade/NOTICE" <<'NOTICE' +1bit OS, Lemonade Server appliance + +Lemonade Server by AMD, running on the 1bit engine + +This package is our build of Lemonade Server (lemond, the lemonade CLI and the web app) from the +1bit fork, with the onebit recipe: the appliance's server, running the 1bit engine. +NOTICE +cp "$src/third_party/lemonade/LICENSE" "$p/usr/share/doc/1bit-lemonade/LICENSE" 2>/dev/null || true +# the HSA runtime the engine's llama-server loads, when the build prefix carries one: a package path, +# so /etc/1bit/libhsa is a file dpkg owns instead of something computed at boot (compare #542) +hsa=$(find "$lemon" -maxdepth 3 -name 'libhsa-runtime64.so*' 2>/dev/null | head -1) +if [ -n "$hsa" ]; then + mkdir -p "$p/opt/lemonade/lib" "$p/etc/1bit" + cp -L "$hsa" "$p/opt/lemonade/lib/libhsa-runtime64.so.1" + printf 'IREE_HAL_AMDGPU_LIBHSA_PATH=/opt/lemonade/lib/libhsa-runtime64.so.1\n' > "$p/etc/1bit/libhsa.env" + echo " HSA runtime: packed at /opt/lemonade/lib/libhsa-runtime64.so.1" +fi +dpkg-deb --build --root-owner-group "$p" "$out/1bit-lemonade_${debver}_amd64.deb" >/dev/null + +echo "== 1bit-appliance" +newpkg 1bit-appliance +withscripts 1bit-appliance +cp -a "$here/debian/1bit-appliance/files"/. "$p/" +# the aggregate licence set every 1bit OS image carries, in the one place 1bit-release-check looks +# (/usr/share/licenses/1bit-os): the binary licence pair, plus the component map (os/LICENSES.md) +mkdir -p "$p/usr/share/licenses/1bit-os" +for f in LICENSE NOTICE LICENSE-BINARY NOTICE-BINARY; do + [ -e "$src/$f" ] && cp "$src/$f" "$p/usr/share/licenses/1bit-os/" +done +cp "$src/os/LICENSES.md" "$p/usr/share/licenses/1bit-os/LICENSES.md" +# identity: release-check's identity row reads this, and it names exactly what was built +mkdir -p "$p/etc/1bit" +{ + echo "edition=1bit OS, Lemonade Server appliance" + echo "version=$version" + echo "engine=$engine_version" + echo "built=$(date -u +%FT%TZ)" +} > "$p/etc/1bit/release-identity" +# on a box the appliance is the kernel and the two board tools as well: with a kernel stage they are +# dependencies (apt installs the six together), without one this stays the build/VM package +if [ -n "$kver" ]; then + sed -i "s|^Depends: \(.*\)|Depends: \1, 1bit-kernel-image-$kver (= $debver), 1bit-kernel-modules-$kver (= $debver), 1bit-ryzenadj (= $debver)|" \ + "$p/DEBIAN/control" +fi +dpkg-deb --build --root-owner-group "$p" "$out/1bit-appliance_${debver}_amd64.deb" >/dev/null + +echo "== 1bit-archive-keyring" +newpkg 1bit-archive-keyring +mkdir -p "$p/usr/share/keyrings" +if [ -n "$keyring" ] && [ -s "$keyring" ]; then + install -m 644 "$keyring" "$p/usr/share/keyrings/1bit-archive-keyring.gpg" +else + echo "mkdebs: no archive keyring given, so 1bit-archive-keyring ships without a key" >&2 + : > "$p/usr/share/keyrings/1bit-archive-keyring.gpg" +fi +dpkg-deb --build --root-owner-group "$p" "$out/1bit-archive-keyring_${debver}_all.deb" >/dev/null + +# --- the three a box needs and a VM does not: the kernel and the two board tools -------------------- +if [ -n "$kstage" ]; then + echo "== 1bit-kernel-image-$kver" + newpkg 1bit-kernel-image + mkdir -p "$p/boot" + for f in vmlinuz System.map config; do install -m 644 "$kstage/boot/$f-$kver" "$p/boot/$f-$kver"; done + withscripts 1bit-kernel-image + mkdir -p "$p/usr/share/doc/1bit-kernel-image-$kver" + cp /usr/share/common-licenses/GPL-2 "$p/usr/share/doc/1bit-kernel-image-$kver/copyright" + # the signature the boot chain checks, named in the package: sbverify against the enrolled MOK + sbverify --list "$p/boot/vmlinuz-$kver" 2>/dev/null | grep -q 'signature 1' || { + echo "mkdebs: $kver's vmlinuz carries no signature: Secure Boot would refuse it"; exit 1; } + builddeb + + echo "== 1bit-kernel-modules-$kver" + newpkg 1bit-kernel-modules + mkdir -p "$p/lib/modules" "$p/usr/share/doc/1bit-kernel-modules-$kver" + # *.bak files are how the development box keeps the previous signed .ko; not part of a package + cp -a "$kstage/lib/modules/$kver" "$p/lib/modules/" + find "$p/lib/modules/$kver" -name '*.bak*' -delete + # GPL-2: the EC driver's binary travels with the source it was built from + mkdir -p "$p/usr/src/1bit-ec-$version" + cp -a "$src/os/licenses/ec_su_axb35/src"/. "$p/usr/src/1bit-ec-$version/" + cp "$src/os/licenses/ec_su_axb35/LICENSE" "$p/usr/src/1bit-ec-$version/LICENSE" + cp /usr/share/common-licenses/GPL-2 "$p/usr/share/doc/1bit-kernel-modules-$kver/copyright" + withscripts 1bit-kernel-modules + builddeb + + echo "== 1bit-ryzenadj" + newpkg 1bit-ryzenadj + rjsrc=$src/os/licenses/ryzenadj/src + [ -d "$rjsrc" ] || { echo "mkdebs: no $rjsrc (the pinned ryzenadj v0.19.0 source)"; exit 2; } + # the SMU limits go through /dev/mem, which needs no root-owned helper: a userspace build, as the + # pinned SOURCE says, and the source ships beside the binary (LGPL-3.0) + ( cd "$work" && cp -a "$rjsrc" rj && cmake -S rj -B rj/build -DCMAKE_BUILD_TYPE=Release >/dev/null \ + && cmake --build rj/build -j4 >/dev/null ) || { echo "mkdebs: the ryzenadj build failed (needs cmake, gcc, pkg-config, libpci-dev)"; exit 1; } + mkdir -p "$p/usr/bin" "$p/usr/share/doc/1bit-ryzenadj" + install -m 755 "$work/rj/build/ryzenadj" "$p/usr/bin/ryzenadj" + miss=$(ldd "$p/usr/bin/ryzenadj" 2>/dev/null | awk '/not found/ {print $1}' | tr '\n' ' ') + [ -z "$miss" ] || { echo "mkdebs: ryzenadj misses libraries: $miss"; exit 1; } + cp "$src/os/licenses/ryzenadj/LICENSE" "$p/usr/share/doc/1bit-ryzenadj/copyright" + ( cd "$rjsrc" && tar --xz -cf "$p/usr/share/doc/1bit-ryzenadj/source.tar.xz" . ) + builddeb +else + echo "mkdebs: KERNEL_STAGE not given - no 1bit-kernel-image/-modules/-ryzenadj (VM and rootfs builds need none)" >&2 +fi + +echo; ls -la "$out"/*.deb diff --git a/os/v2/mklive.sh b/os/v2/mklive.sh new file mode 100755 index 00000000..9e1a9a74 --- /dev/null +++ b/os/v2/mklive.sh @@ -0,0 +1,209 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# mklive.sh [mok-key.priv mok-cert.pem] +# +# Writes the live media: one disk image to dd onto a stick, in the same three partitions an installed +# box has, which is what makes 1bit-install a copy rather than a different layout. +# +# partition 1 512 MB EFI system partition, 1BIT-OS - shim, our MOK-signed grub, the kernel and the +# initramfs, and a grub.cfg for the live entry +# partition 2 squashfs, PARTLABEL=1BIT-LIVE - the whole root filesystem, read-only +# partition 3 ext4, 1BIT-DATA - models, Lemonade's backends and config, keys +# +# The live system is the squashfs with a tmpfs overlay (an initramfs-tools script, so nothing is custom +# in userspace): it runs from RAM, the stick is never written, and `apt` on a live boot cannot damage the +# medium. /data stays the stick's own partition, by label, exactly as on an installed box. +# +# Secure Boot: shim (Microsoft-signed) -> grubx64.efi and vmlinuz, both signed with the MOK that is +# enrolled on the machine, so an install copies the same signed chain and needs no enrolment change. +# Without a MOK key pair this still builds, says so loudly, and the image only boots with Secure Boot off. +set -euo pipefail +[ $# -ge 3 ] || { sed -n '/^# mklive.sh/,/^# Secure Boot/p' "$0" | sed 's/^# \{0,1\}//'; exit 2; } +if [ "$(id -u)" != 0 ]; then exec sudo -n "$0" "$@"; fi +rootfs=$(cd "$1" && pwd) seed=$(cd "$2" && pwd) out=$3 key=${4:-} cert=${5:-} +here=$(cd "$(dirname "$0")" && pwd) +ESP_MB=512 +PART_LIVE=1BIT-LIVE +DATA_LABEL=1BIT-DATA +work=$(mktemp -d) +loop= +# every step of the cleanup has to succeed-or-not without changing the runner's exit status: under +# `set -e` a failing command inside an EXIT trap aborts the trap and becomes the script's status, which +# is how a *successful* image build reported failure (the ESP is already unmounted on the happy path) +trap '{ [ -n "$loop" ] && losetup -d "$loop"; umount "$work/esp"; umount "$work/root/proc"; umount "$work/root/sys"; umount "$work/root/dev"; rm -rf "$work"; } >/dev/null 2>&1 || true' EXIT + +# the kernel this stick boots: our own when the rootfs carries it, otherwise the base's +kver=$(ls "$rootfs"/boot/vmlinuz-* 2>/dev/null | sed 's/.*vmlinuz-//' | sort -V | tail -1) +[ -n "$kver" ] || { echo "mklive: no kernel in $rootfs/boot"; exit 2; } +echo "== live tree (a copy: the rootfs this was built from stays as it was)" +cp -a "$rootfs" "$work/root" +rm -rf "$work/root/var/cache/apt/archives"/*.deb "$work/root/tmp"/* +# a test harness's leftovers are not part of a released medium: vmtest-v2.sh writes a test-only unit into +# the rootfs it boots, and a medium built from that rootfs must not carry it (measured: the installed +# disk ran the harness's unit instead of the install check, because the unit had been baked into rootfs6) +rm -f "$work/root"/usr/lib/1bit/vmtest*.sh \ + "$work/root"/usr/lib/systemd/system/1bit-vmtest*.service \ + "$work/root"/etc/systemd/system/1bit-vmtest*.service \ + "$work/root"/etc/systemd/system/multi-user.target.wants/1bit-vmtest*.service +# LIVE_EXTRA is the test harness's hook: a tree laid over the live system (a test-only unit), never part +# of a shipped image +[ -n "${LIVE_EXTRA:-}" ] && cp -a "$LIVE_EXTRA"/. "$work/root/" + +# the installer ships in the image, and the mode marker is the live image's (1bit-install removes it - +# 1bit-first-boot.service then writes /etc/1bit/installed after making this machine's own SSH keys) +install -m 755 "$here/install.sh" "$work/root/usr/bin/1bit-install" +mkdir -p "$work/root/etc/1bit" +: > "$work/root/etc/1bit/live" +# the overlay is built in the initramfs, so its modules have to be in there +mkdir -p "$work/root/etc/initramfs-tools" +grep -qx overlay "$work/root/etc/initramfs-tools/modules" 2>/dev/null || echo overlay >> "$work/root/etc/initramfs-tools/modules" +grep -qx squashfs "$work/root/etc/initramfs-tools/modules" 2>/dev/null || echo squashfs >> "$work/root/etc/initramfs-tools/modules" + +# --- the initramfs script that makes the squashfs the root ------------------------------------------ +# initramfs-tools sources /scripts/init-bottom/* after root= has been mounted and before the final +# run-init, so assigning rootmnt there is what redirects the switch_root. It has to be init-bottom and +# not local-bottom: local_bottom() only runs when a local-top/local-premount script ran, and nothing in +# this image does - measured, the overlay never got built and run-init found the data partition's empty +# root instead. The "0-" prefix sorts it before udev's init-bottom script, which moves /dev into the new +# root and so has to see the overlay already in place. +mkdir -p "$work/root/etc/initramfs-tools/scripts/init-bottom" +cat > "$work/root/etc/initramfs-tools/scripts/init-bottom/0-1bit-live" <<'LIVE' +# 1bit OS live boot: the initramfs has already mounted the squashfs at $rootmnt (root= is the +# 1BIT-LIVE partition, rootfstype=squashfs), so the overlay is that squashfs with a tmpfs upper. +# Nothing here writes the stick. +if grep -q '1bit.live=1' /proc/cmdline; then + echo "1bit live: building the overlay root" + [ -e "$rootmnt/etc/issue" ] || panic "1bit live: root= did not mount the 1BIT-LIVE squashfs (empty $rootmnt)" + mount -t tmpfs -o mode=0755 tmpfs /cow || panic "1bit live: no tmpfs for the overlay" + mkdir -p /cow/upper /cow/work /live-root + mount -t overlay -o "lowerdir=$rootmnt,upperdir=/cow/upper,workdir=/cow/work" overlay /live-root || + panic "1bit live: the overlay would not mount" + # A script in here cannot hand a variable back by assigning it: initramfs-tools' ORDER runs each + # script as a *command*, so its shell is the script's own. /conf/param.conf is the channel - ORDER + # sources it after every script - and it has to be used, or run-init walks off to the old rootmnt + # (measured: rootmnt=/live-root was silently ignored and run-init used /root). + printf 'rootmnt=/live-root\n' >> /conf/param.conf + echo "1bit live: squashfs=$([ -e "$rootmnt/etc/issue" ] && echo ok || echo empty) overlay=$([ -e /live-root/sbin/init ] && echo ok || echo empty) root=/live-root" +fi +LIVE +chmod 755 "$work/root/etc/initramfs-tools/scripts/init-bottom/0-1bit-live" +echo "== initramfs for $kver (overlay + squashfs in it)" +# initramfs-tools wants a live /proc and /dev to run inside a chroot; without them mkinitramfs fails and +# says so only on its own stderr, which is why its log is kept, not thrown away +for d in proc sys dev; do mount --bind "/$d" "$work/root/$d" || { echo "mklive: cannot bind /$d into the tree"; exit 1; }; done +if ! chroot "$work/root" update-initramfs -c -k "$kver" > "$work/initramfs.log" 2>&1; then + for d in proc sys dev; do umount "$work/root/$d"; done + echo "mklive: update-initramfs failed for $kver:"; tail -20 "$work/initramfs.log" | sed 's/^/ /' + exit 1 +fi +for d in proc sys dev; do umount "$work/root/$d"; done +[ -f "$work/root/boot/initrd.img-$kver" ] || { echo "mklive: no initrd for $kver"; exit 1; } + +echo "== squashfs" +mksquashfs "$work/root" "$work/live.squashfs" -comp zstd -noappend -no-progress -quiet +echo " live.squashfs $(du -h "$work/live.squashfs" | cut -f1)" + +echo "== the disk image" +sq_mb=$(( $(stat -c %s "$work/live.squashfs") / 1000000 + 64 )) +data_mb=$(( $(du -sm "$seed" | cut -f1) + 256 )) +total_mb=$(( ESP_MB + sq_mb + data_mb + 8 )) +rm -f "$out" +truncate -s "${total_mb}M" "$out" +sgdisk -Z "$out" >/dev/null +sgdisk -n "1:2048:+${ESP_MB}M" -t 1:ef00 -c 1:EFI \ + -n "2:0:+${sq_mb}M" -t 2:8300 -c "2:$PART_LIVE" \ + -n "3:0:0" -t 3:8300 -c "3:$DATA_LABEL" "$out" >/dev/null +loop=$(losetup --find --show --partscan "$out") +i=0 +while [ ! -b "${loop}p3" ] && [ "$i" -lt 20 ]; do sleep 1; i=$((i + 1)); done + +echo "== the ESP: shim, our grub, the kernel and the initramfs" +esp=$work/esp +mkdir -p "$esp" +mkfs.vfat -F 32 -n 1BIT-OS "${loop}p1" >/dev/null +mount "${loop}p1" "$esp" +# the directories go on the *mounted* filesystem: a mkdir before the mount is hidden by it (measured) +mkdir -p "$esp/EFI/BOOT" +cleanup_esp() { umount "$esp" 2>/dev/null || true; } +install -m 644 "$work/root/boot/vmlinuz-$kver" "$esp/vmlinuz" +install -m 644 "$work/root/boot/initrd.img-$kver" "$esp/initrd.img" + +# grub: one standalone binary with the modules the ESP needs; it reads grub.cfg from the ESP, which is +# what 1bit-install rewrites for the installed disk +cat > "$work/root/tmp/grub-embed.cfg" <<'EMBED' +search --no-floppy --file --set=root /EFI/BOOT/grub.cfg +configfile /EFI/BOOT/grub.cfg +EMBED +chroot "$work/root" grub-mkstandalone -O x86_64-efi -o /tmp/grubx64.efi \ + --modules='part_gpt part_msdos fat ext2 normal linux search search_fs_file search_label configfile echo ls' \ + 'boot/grub/grub.cfg=/tmp/grub-embed.cfg' >/dev/null 2>&1 || + { echo "mklive: grub-mkstandalone failed (grub-efi-amd64-bin in the rootfs?)"; exit 1; } +install -m 644 "$work/root/tmp/grubx64.efi" "$esp/EFI/BOOT/grubx64.efi" + +if [ -n "$key" ] && [ -n "$cert" ] && [ -s "$key" ] && [ -s "$cert" ]; then + for f in "$esp/vmlinuz" "$esp/EFI/BOOT/grubx64.efi"; do + sbsign --key "$key" --cert "$cert" --output "$f.signed" "$f" >/dev/null + mv "$f.signed" "$f" + sbverify --cert "$cert" "$f" >/dev/null || { echo "mklive: $f did not take the signature"; exit 1; } + done + echo " signed with $(openssl x509 -in "$cert" -noout -subject 2>/dev/null | sed 's/^subject=//')" +else + echo "mklive: WARNING - no MOK key pair: grub and the kernel are UNSIGNED, so this medium only boots" >&2 + echo "mklive: with Secure Boot disabled. Pass to sign them." >&2 +fi + +# shim first (Microsoft-signed), grub beside it: the chain Secure Boot walks. Ubuntu ships the second +# stage for MOK enrolment as plain mmx64.efi and only some versions add a .signed copy, so both are tried. +shim=/usr/lib/shim/shimx64.efi.signed +mmx=/usr/lib/shim/mmx64.efi +[ -f "$work/root/usr/lib/shim/mmx64.efi.signed" ] && mmx=/usr/lib/shim/mmx64.efi.signed +if [ -f "$work/root$shim" ] && [ -f "$work/root$mmx" ]; then + install -m 644 "$work/root$shim" "$esp/EFI/BOOT/BOOTX64.EFI" + install -m 644 "$work/root$mmx" "$esp/EFI/BOOT/mmx64.efi" +else + echo "mklive: WARNING - no shim in the rootfs (shim-signed): BOOTX64.EFI is grub itself" >&2 + install -m 644 "$esp/EFI/BOOT/grubx64.efi" "$esp/EFI/BOOT/BOOTX64.EFI" +fi + +cat > "$esp/EFI/BOOT/grub.cfg" </dev/null || echo squashfs) on ${loop}p2" + +echo "== the data partition: $(du -sh "$seed" | cut -f1) of models, backends and config" +mkfs.ext4 -q -F -L "$DATA_LABEL" -d "$seed" "${loop}p3" +losetup -d "$loop"; loop= + +sgdisk -p "$out" | sed 's/^/ /' +echo +echo "1bit OS v2 live image: $out ($(du -h "$out" | cut -f1))" +echo " write it with: dd if=$out of=/dev/sdX bs=4M conv=fsync status=progress" +echo " then boot the machine from it and run: 1bit-install /dev/nvme0n1 --yes" +sha256sum "$out" diff --git a/os/v2/mkrepo.sh b/os/v2/mkrepo.sh new file mode 100755 index 00000000..4d3331fd --- /dev/null +++ b/os/v2/mkrepo.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# mkrepo.sh [gnupg-home] [key-id] +# +# Lays the debs out as an apt repository and signs it: pool/main///, the +# Packages index, Release, and InRelease/Release.gpg from the archive key. With no key given the +# Release file is produced unsigned and the script says so - useful for a local run, never for +# publishing. The result is a directory of static files: publish it to 1bit.gg/apt as it is. +set -euo pipefail +[ $# -ge 2 ] || { sed -n '21,27p' "$0"; exit 2; } +debs=$1 repo=$2 gnupg=${3:-} keyid=${4:-} +[ -d "$debs" ] || { echo "mkrepo: no $debs"; exit 2; } +dist=$repo/dists/stable +mkdir -p "$dist/main/binary-amd64" "$repo/pool/main" +# each deb into pool/main///, which is what apt's index +# records as its location - a deb dropped anywhere else still installs, but the layout stays boring +for d in "$debs"/*.deb; do + [ -e "$d" ] || continue + pkg=$(dpkg-deb -f "$d" Package) + src=${pkg#1bit-} + src=${src:-appliance} + mkdir -p "$repo/pool/main/${src:0:1}/1bit-$src" + cp -f "$d" "$repo/pool/main/${src:0:1}/1bit-$src/" +done +( cd "$repo" && apt-ftparchive packages pool > "$dist/main/binary-amd64/Packages" ) +gzip -9 -kf "$dist/main/binary-amd64/Packages" +( cd "$repo" && apt-ftparchive \ + -o APT::FTPArchive::Release::Origin="1bit" \ + -o APT::FTPArchive::Release::Label="1bit OS" \ + -o APT::FTPArchive::Release::Suite="stable" \ + -o APT::FTPArchive::Release::Codename="stable" \ + -o APT::FTPArchive::Release::Architectures="amd64" \ + -o APT::FTPArchive::Release::Components="main" \ + -o APT::FTPArchive::Release::Description="1bit OS, Lemonade Server appliance - packages" \ + release "$dist" > "$dist/Release" ) +rm -f "$dist/Release.gpg" "$dist/InRelease" +if [ -n "$gnupg" ] && [ -n "$keyid" ]; then + [ -d "$gnupg" ] || { echo "mkrepo: no GNUPGHOME at $gnupg"; exit 2; } + GNUPGHOME=$gnupg gpg --batch --yes -u "$keyid" --detach-sign -o "$dist/Release.gpg" "$dist/Release" + GNUPGHOME=$gnupg gpg --batch --yes -u "$keyid" --clearsign -o "$dist/InRelease" "$dist/Release" + echo "mkrepo: signed with $keyid" +else + echo "mkrepo: WARNING - Release is UNSIGNED (no key given); fine for a local run, never publish it" >&2 +fi +echo "mkrepo: $(ls "$repo"/pool/main/*/*/*.deb | wc -l) packages, $(wc -c < "$dist/Release") bytes of Release" diff --git a/os/v2/mkrootfs.sh b/os/v2/mkrootfs.sh new file mode 100755 index 00000000..b3ec24e8 --- /dev/null +++ b/os/v2/mkrootfs.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# mkrootfs.sh [ssh-authorized-key.pub] +# +# Builds the appliance's root filesystem: a minimal Ubuntu from one snapshot (pin.txt) with +# mmdebstrap, and 1bit-appliance from the repository this script is handed - the same install an +# owner's box does from 1bit.gg/apt, only out of a directory. +# +# MIRROR= overrides the pinned snapshot for one run. That is for a snapshot.ubuntu.com outage, +# never for a release: the same packages from a moving archive are not the release pin.txt describes. +# +# Nothing of the appliance is unpacked by hand: the units, the banner, the /data mount, the first-boot +# step and the engine arrive as packages. The only additions are the two a test image needs - a way in +# over SSH (the real live image bakes its own key) and the hostname - both done in the customize hook. +# +# The repository goes in at /srv/1bit-apt and apt reads it as file://, so the signed-by chain is the +# same one https would use: an unsigned or wrongly signed Release file fails the build here. +set -euo pipefail +[ $# -ge 2 ] || { sed -n '21,32p' "$0"; exit 2; } +repo=$(cd "$1" && pwd) rootfs=$2 sshkey=${3:-} +here=$(cd "$(dirname "$0")" && pwd) +. "$here/pin.txt" +mirror=${MIRROR:-$mirror} +[ -d "$repo/dists/stable" ] || { echo "mkrootfs: $repo is not an apt repository (mkrepo.sh)"; exit 2; } + +# The repository is served over HTTP for the install: apt's file:// method drops to the _apt sandbox +# user, and a directory only the build user can traverse fails there in ways that look like a missing +# Release file. The owner's box fetches over https; the difference here is only the URL. +port=$((20000 + RANDOM % 20000)) +( cd "$repo" && exec python3 -m http.server "$port" --bind 127.0.0.1 >/dev/null 2>&1 ) & +httpsrv=$! +trap 'kill $httpsrv 2>/dev/null' EXIT +sleep 1 +curl -sf "http://127.0.0.1:$port/dists/stable/Release" >/dev/null || + { echo "mkrootfs: the local repository server did not answer on :$port"; exit 1; } + +# the trust anchor comes out of the package that carries it, so the keyring used to build is exactly +# the one the box will trust +keydeb=$(ls "$repo"/pool/main/*/*/1bit-archive-keyring_*.deb 2>/dev/null | head -1) +[ -n "$keydeb" ] || { echo "mkrootfs: no 1bit-archive-keyring deb in $repo"; exit 2; } +kr=$(mktemp) +dpkg-deb --fsys-tarfile "$keydeb" | tar -xO ./usr/share/keyrings/1bit-archive-keyring.gpg > "$kr" +[ -s "$kr" ] || { echo "mkrootfs: the keyring deb carries an empty keyring (mkrepo.sh signed?)"; exit 2; } + +# what a minimal appliance needs on top of minbase. The kernel is Ubuntu's here: it is the VM's, and +# our own 1bit-kernel-image takes its place on the machine (design ยง2) - and 1bit-appliance depends on +# it, so apt installs it too when the repository carries it. Mesa's RADV is there for the same reason it +# is in the os/mini image - Lemonade's vulkan backend has to run in the VM. grub, shim, efibootmgr and +# squashfs-tools are what mklive.sh needs to build the live medium out of this rootfs. +inc="systemd,systemd-sysv,dbus,systemd-resolved,openssh-server,initramfs-tools,linux-image-virtual,mesa-vulkan-drivers,libvulkan1,libgl1,e2fsprogs,dosfstools,fdisk,ca-certificates,wget,curl,zstd,gnupg,vim-tiny,less,procps,grub-efi-amd64-bin,shim-signed,efibootmgr,squashfs-tools,kmod" +# fdisk is not decoration: it is where Ubuntu's sfdisk lives, and 1bit-install lays out a GPT disk with it +# (minbase does not carry it - measured: rootfs5 had no /usr/sbin/sfdisk at all) +# the setup hook runs before the skeleton has /srv and sources.list.d, so it makes them +# The build-time source trusts the key through trusted.gpg.d: mmdebstrap's first apt run happens +# before any package exists, and there signed-by pointing at a file it cannot read yet is a puzzle +# without a payoff. The source the box actually keeps (from 1bit-appliance) uses signed-by and is +# asserted in the VM. +setup="mkdir -p \"\$1\"/usr/share/keyrings \"\$1\"/etc/apt/trusted.gpg.d \"\$1\"/etc/apt/sources.list.d && cp $kr \"\$1\"/usr/share/keyrings/1bit-archive-keyring.gpg && chmod 644 \"\$1\"/usr/share/keyrings/1bit-archive-keyring.gpg && cp $kr \"\$1\"/etc/apt/trusted.gpg.d/1bit-archive-keyring.gpg && chmod 644 \"\$1\"/etc/apt/trusted.gpg.d/1bit-archive-keyring.gpg && printf 'deb http://127.0.0.1:$port stable main\\n' > \"\$1\"/etc/apt/sources.list.d/00-1bit-build.list" +# the build-time source goes away; what stays is the production one from 1bit-appliance plus a +# test-only file:// source over the copy in /srv, so the VM can prove apt works without the internet +customize='rm -f /etc/apt/sources.list.d/00-1bit-build.list; mkdir -p /srv; cp -a /tmp/1bit-apt /srv/1bit-apt 2>/dev/null || true; chmod -R a+rX /srv/1bit-apt 2>/dev/null || true; printf "deb [signed-by=/usr/share/keyrings/1bit-archive-keyring.gpg] file:///srv/1bit-apt stable main\\n" > /etc/apt/sources.list.d/99-1bit-test.list; echo 1bit-os > /etc/hostname; mkdir -p /data; if [ -s /tmp/1bit-test-key.pub ]; then install -d -m 700 /root/.ssh; cp /tmp/1bit-test-key.pub /root/.ssh/authorized_keys; chmod 600 /root/.ssh/authorized_keys; fi; update-initramfs -u -k all >/dev/null 2>&1 || true; rm -rf /var/lib/apt/lists/* /tmp/1bit-test-key.pub' +setup="$setup && mkdir -p \"\$1\"/tmp && cp -a $repo \"\$1\"/tmp/1bit-apt" +if [ -n "$sshkey" ]; then + setup="$setup && cp $sshkey \"\$1\"/tmp/1bit-test-key.pub && chmod 644 \"\$1\"/tmp/1bit-test-key.pub" +fi + +# mmdebstrap needs to create the tree as root: in unprivileged mode it cannot write under a home +# directory that is not world-writable, which is how this host is set up +if [ "$(id -u)" = 0 ]; then + rm -rf "$rootfs"; MMD=(mmdebstrap) +else + # root, because a rootfs has /dev nodes and a home directory here is not world-writable + sudo -n rm -rf "$rootfs"; MMD=(sudo -n mmdebstrap) +fi +"${MMD[@]}" --variant=minbase --arch=amd64 \ + --components="$components" \ + --include="$inc,1bit-appliance" \ + --keyring="$kr" \ + --aptopt='Acquire::Retries "5"' \ + --setup-hook="$setup" \ + --chrooted-customize-hook="$customize" \ + resolute "$rootfs" "$mirror" +rm -f "$kr" +echo "== rootfs: $(du -sh "$rootfs" | cut -f1) at $rootfs" diff --git a/os/v2/pin.txt b/os/v2/pin.txt new file mode 100644 index 00000000..11ffca5f --- /dev/null +++ b/os/v2/pin.txt @@ -0,0 +1,23 @@ +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# Ubuntu snapshot the v2 root filesystem is built from. A release is reproducible from this file plus +# the engine and Lemonade builds it names: same timestamp, same packages, same binary inputs. +# +# The timestamp is a URL segment on snapshot.ubuntu.com, so every apt source below resolves to the +# archive exactly as it was at that moment (see os/v2/mkrootfs.sh). +snapshot=20261024T000000Z +mirror=https://snapshot.ubuntu.com/ubuntu/20261024T000000Z +suites="resolute resolute-updates resolute-security" +components="main universe" diff --git a/os/v2/proto.sh b/os/v2/proto.sh new file mode 100755 index 00000000..637efe57 --- /dev/null +++ b/os/v2/proto.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# proto.sh [backend-cache] [model.gguf] +# +# The whole v2 prototype in one command, on the build host and nowhere near a device: +# a throwaway archive key -> the four debs -> a signed apt repository -> an mmdebstrap rootfs that +# apt-installs 1bit-appliance from it -> that rootfs booted in QEMU as a machine -> VMTEST-V2. +# +# is a release stage (tarball or directory with 1bit and the backend dirs), and +# a build-lemonade.sh prefix. [backend-cache] is an existing Lemonade cache/bin +# directory to seed the data partition with (so the VM needs no network for Lemonade's backends), and +# [model.gguf] a small model for the chat row. +set -euo pipefail +[ $# -ge 3 ] || { sed -n '21,32p' "$0"; exit 2; } +w=$1 stage=$2 lemon=$3 backends=${4:-} model=${5:-$HOME/models/Qwen3-0.6B-Q4_K_M.gguf} +here=$(cd "$(dirname "$0")" && pwd) +version=v2026.43 +mkdir -p "$w" +w=$(cd "$w" && pwd) + +echo "== archive key (throwaway: the published repository is signed with the operator's key)" +if [ ! -s "$w/gnupg/pubring.kbx" ]; then + mkdir -p "$w/gnupg"; chmod 700 "$w/gnupg" + GNUPGHOME=$w/gnupg gpg --batch --quiet --passphrase '' --quick-gen-key \ + '1bit archive (prototype) ' rsa2048 sign never 2>/dev/null +fi +keyid=$(GNUPGHOME=$w/gnupg gpg --batch --with-colons --list-secret-keys 2>/dev/null | awk -F: '/^sec:/ {print $5; exit}') +[ -n "$keyid" ] || { echo "proto: no signing key"; exit 1; } +GNUPGHOME=$w/gnupg gpg --batch --yes --export "$keyid" > "$w/1bit-archive-keyring.gpg" +echo " key $keyid" + +echo "== the packages, from the existing stage (no binary is rebuilt)" +"$here/mkdebs.sh" "$stage" "$lemon" "$version" "$w/debs" "$w/1bit-archive-keyring.gpg" + +echo "== the repository, signed" +"$here/mkrepo.sh" "$w/debs" "$w/repo" "$w/gnupg" "$keyid" + +echo "== the data partition the VM boots with (models, Lemonade's config and backends)" +seed=$w/data +rm -rf "$seed"; mkdir -p "$seed/models" "$seed/lemonade/config" "$seed/lemonade/huggingface" "$seed/cache/hrx-jit" "$seed/npu" +[ -f "$model" ] && cp -L "$model" "$seed/models/" +printf '{\n "host": "0.0.0.0",\n "extra_models_dir": "/data/models",\n "models_dir": "/data/lemonade/huggingface"\n}\n' \ + > "$seed/lemonade/config/config.json" +if [ -n "$backends" ] && [ -d "$backends/llamacpp" ]; then + mkdir -p "$seed/lemonade/cache/bin" + cp -a "$backends/llamacpp" "$seed/lemonade/cache/bin/" + echo " backends: $(ls "$seed/lemonade/cache/bin/llamacpp" | tr '\n' ' ')" +else + echo " WARNING: no backend cache given - Lemonade will have no llama.cpp backend in the VM" >&2 +fi + +echo "== an SSH key for the test image (the real live image bakes its own)" +[ -s "$w/testkey" ] || ssh-keygen -q -t ed25519 -N '' -f "$w/testkey" + +echo "== the root filesystem (mmdebstrap, one Ubuntu snapshot) + apt install 1bit-appliance" +"$here/mkrootfs.sh" "$w/repo" "$w/rootfs" "$w/testkey.pub" + +echo "== boot it and check the appliance" +"$here/vmtest-v2.sh" "$w/rootfs" "$seed" "$w/testkey" diff --git a/os/v2/vmtest-install-v2.sh b/os/v2/vmtest-install-v2.sh new file mode 100755 index 00000000..30e5662f --- /dev/null +++ b/os/v2/vmtest-install-v2.sh @@ -0,0 +1,235 @@ +#!/usr/bin/env bash +# Copyright 2026 bong-water-water-bong +# SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# vmtest-install-v2.sh [mok.priv mok.pem] +# +# Builds the live medium out of (os/v2/mklive.sh), boots it as a machine with a second +# empty disk attached and a USB stick on the bus, runs 1bit-install's refusals and then a real install, +# and finally boots the disk it installed - alone, no live medium attached - to see that machine. +# +# Both halves are judged from what the guest wrote onto the disk it owns (the stick's, then the +# installed disk's /data), not from QEMU. Prints VMTEST-V2-INSTALL PASS or FAIL. +set -uo pipefail +[ $# -ge 3 ] || { sed -n '/^# vmtest-install-v2.sh/,/^# Both halves/p' "$0" | sed 's/^# \{0,1\}//'; exit 2; } +if [ "$(id -u)" != 0 ]; then exec sudo -n "$0" "$@"; fi +rootfs=$(cd "$1" && pwd) seed=$(cd "$2" && pwd) work=$3; mkdir -p "$work"; work=$(cd "$work" && pwd) +key=${4:-} cert=${5:-} +here=$(cd "$(dirname "$0")" && pwd) + +if [ ! -f /usr/share/OVMF/OVMF_CODE_4M.fd ]; then + echo "VMTEST-V2-INSTALL FAIL (no OVMF: the live medium must boot through real firmware, not -kernel)" + exit 1 +fi + +# --- the test-only unit that lives inside the live medium, and therefore also on the installed disk --- +extra=$(mktemp -d "$work/extra.XXXX"); trap 'rm -rf "$extra"' EXIT +install -d -m 755 "$extra/usr/lib/1bit" "$extra/usr/lib/systemd/system" \ + "$extra/etc/systemd/system/multi-user.target.wants" +cat > "$extra/usr/lib/1bit/vmtest-install.sh" <<'INGUEST' +#!/bin/sh +# Test-only. Two modes, decided by what this machine booted from: the live medium installs; the +# installed disk is then inspected. Everything goes to /data, which is the disk the mode is about. +if [ -e /etc/1bit/live ]; then + exec > /data/vmtest-install.txt 2>&1 + echo "== this boot" + grep -q '1bit.live=1' /proc/cmdline && echo "cmdline: live (squashfs + overlay)" || echo "cmdline: NOT live" + head -2 /etc/issue + findmnt -n -o SOURCE,FSTYPE / | head -1 + echo "== refusals" + r() { #