diff --git a/BUILD.md b/BUILD.md index 1421e80..2196332 100644 --- a/BUILD.md +++ b/BUILD.md @@ -23,6 +23,20 @@ change; the release is assembled and audited every time. `orchard run` boots its recovery on the attached device without installing, and `orchard install` installs it. The rest of this page is what that entry runs, step by step. +### 32-bit (armv7) + +A release has one root of one architecture. `orchard build --platform apple-armv7` +(or `tools/build.py --arch armv7`) builds the armv7 root and its own release, for +orchard's 32-bit platforms: the same packages, services and configuration as the +arm64 root, with Alpine's dropbear and udhcpd (`busybox-extras`) in place of +orchard's static arm64 tools. `system/armv7/` builds it in the same pinned arm64 +Alpine image: the native `apk --arch armv7` unpacks the signed ARMv7 packages into +`/armroot` without running their scripts, and the recipe does as files what those +scripts would (applet links, accounts, `/etc/shells`), then merges `/usr` with the +arm64 `merge-usr --root`. Nothing runs ARMv7 code, so no emulation is needed. The +release takes the boards, loaders and layer from orchard's 32-bit index +(`platforms-armv7.toml`, beside the unchanged arm64 `platforms.toml`). + ## Local setup Requires uv and a native ARM64 Podman environment. Run commands from this @@ -51,7 +65,7 @@ the few lines that find it and run its commands. ```sh export ORCHARD=/path/to/orchard PYTHONPATH=/path/to/orchard/src python3 boot/build-artwork.py --output out/artwork -python3 build_base.py --output out/base/base.tar.gz +python3 build_base.py --output out/base/base.tar.gz # --arch armv7: the 32-bit root's base # What orchard supplies finished: kernels and boards per platform, m1n1, Pongo, tools. python3 -m orchard.inputs --output out/orchard-inputs.json --platform apple-16k python3 pin_components.py --loader M1N1 --splash out/artwork/splash --logo out/artwork/logo.bin \ @@ -75,7 +89,7 @@ signature or trust service. Use fresh output paths; producers refuse overwriting. The official Alpine OCI digest is pinned in `system/Containerfile`; APK repositories retain signature checking. The base records package versions in `/etc/alpine-packages.lock`. Its archive hash and all selected hardware hashes are recorded separately from the install contract. The APK mirror is not an immutable snapshot: future base rebuilds may select newer signed package revisions and must get new receipts. -The producer composes Alpine-owned policy into one public gzip root for every platform (no kernel or modules: orchard's platform layer brings those at install, and orchard writes the boot entry), seals a recovery per platform to the root's receipt, adds orchard's layer per platform, and emits `release.toml` plus the index that pins it. Public build receipts omit local machine paths. No private firmware, SSH keys, ADT or machine IDs are embedded. +The producer composes Alpine-owned policy into one public gzip root for every platform of its architecture (no kernel or modules: orchard's platform layer brings those at install, and orchard writes the boot entry), seals a recovery per platform to the root's receipt, adds orchard's layer per platform, and emits `release.toml` plus the index that pins it. Public build receipts omit local machine paths. No private firmware, SSH keys, ADT or machine IDs are embedded. ## Native isolated checks diff --git a/README.md b/README.md index d6afd38..225862e 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A small Alpine-based Linux image for Apple devices, demonstrating the Orchard release format without a desktop or systemd requirement. -A release covers every device orchard describes: one root image, and orchard's kernel, device trees and modules per platform. How well a device works is orchard's device data, which the installer shows; today the iPad 6 Wi-Fi (`iPad7,5`, J71b) is supported, the others are experimental. Boot is tethered and requires a host computer. The installer sets the password of its one user, `tablet`; the console on tty1 and the USB serial console take it. +A release covers every device orchard describes for its architecture: one root image, and orchard's kernel, device trees and modules per platform. 64-bit devices take the arm64 release, Apple's 32-bit devices (apple-armv7) an armv7 release of the same system. How well a device works is orchard's device data, which the installer shows; today the iPad 6 Wi-Fi (`iPad7,5`, J71b) is supported, the others are experimental. Boot is tethered and requires a host computer. The installer sets the password of its one user, `tablet`; the console on tty1 and the USB serial console take it. ## Local development diff --git a/audit.py b/audit.py index 9a2bd55..e77bb6a 100644 --- a/audit.py +++ b/audit.py @@ -13,6 +13,20 @@ HARDWARE = None # the orchard snapshot; main() sets it, else orchard_root() finds it +# Per root architecture: its ELF class and machine, and the musl loader. +ARCHITECTURES = {'aarch64': {'class': 2, 'machine': 183, 'loader': 'usr/lib/ld-musl-aarch64.so.1'}, + 'armv7': {'class': 1, 'machine': 40, 'loader': 'usr/lib/ld-musl-armhf.so.1'}} + + +def check_elf_arch(data, arch): + """An ELF file of the root is of the root's architecture: no arm64 tool in an armv7 root, or back.""" + if data[:4] != b'\x7fELF': + return False + target = ARCHITECTURES[arch] + assert data[4] == target['class'] and struct.unpack_from('> "$root/etc/shells" +for list in "$root"/etc/busybox-paths.d/*; do + case "$list" in *busybox-extras) target=/bin/busybox-extras ;; *) target=/bin/busybox ;; esac + while read -r applet; do + applet=${applet#/} + [ -n "$applet" ] || continue + if [ -e "$root/$applet" ] || [ -L "$root/$applet" ]; then continue; fi + mkdir -p "$root/$(dirname "$applet")" + ln -s "$target" "$root/$applet" + done < "$list" +done +groupadd --root "$root" --system klogd +useradd --root "$root" --system --no-create-home --home-dir /dev/null --shell /sbin/nologin \ + --gid klogd --comment klogd klogd +# The one regular user; orchard's installer sets its password (chpasswd). +useradd --root "$root" --create-home --uid 1000 --user-group --shell /bin/bash tablet +merge-usr --root "$root" --dryrun +merge-usr --root "$root" +apk --root "$root" info -v > "$root/etc/alpine-packages.lock" +rm -f "$root/etc/machine-id" "$root/var/lib/dbus/machine-id" +test "$(cat "$root/etc/apk/arch")" = armv7 +test -x "$root/usr/sbin/resize2fs" -a -e "$root/usr/bin/python3" +test "$(readlink "$root/lib")" = usr/lib +test "$(readlink "$root/bin")" = usr/bin +test "$(readlink "$root/sbin")" = usr/sbin +test -x "$root/usr/bin/busybox" -a -L "$root/usr/bin/ls" -a -L "$root/usr/sbin/getty" +test -e "$root/usr/lib/ld-musl-armhf.so.1" +test ! -e "$root/usr/lib/systemd/systemd" diff --git a/system/install-base b/system/install-base index 481ba33..ce2ac5f 100644 --- a/system/install-base +++ b/system/install-base @@ -1,6 +1,6 @@ #!/bin/sh set -eu -apk add --no-cache openrc python3 bash kmod util-linux e2fsprogs openssh-client-default merge-usr \ +apk add --no-cache openrc python3 bash kmod util-linux e2fsprogs e2fsprogs-extra openssh-client-default merge-usr \ shadow openssl # The one regular user; orchard's installer sets its password (chpasswd). useradd --create-home --uid 1000 --user-group --shell /bin/bash tablet @@ -12,4 +12,5 @@ rm -f /etc/machine-id /var/lib/dbus/machine-id test "$(readlink /lib)" = usr/lib test "$(readlink /bin)" = usr/bin test "$(readlink /sbin)" = usr/sbin +test -x /usr/sbin/resize2fs # the install finish step grows the root test ! -e /usr/lib/systemd/systemd diff --git a/system/overlay/usr/libexec/minimal-buttons b/system/overlay/usr/libexec/minimal-buttons index 331a2f5..f6b35de 100644 --- a/system/overlay/usr/libexec/minimal-buttons +++ b/system/overlay/usr/libexec/minimal-buttons @@ -12,15 +12,17 @@ import stat import struct import subprocess -EVENT = struct.Struct(' xv or yo + y > yv or stride < xv * 4 or stride % 4): diff --git a/tests/test_armv7_root.py b/tests/test_armv7_root.py new file mode 100644 index 0000000..7b9a4bc --- /dev/null +++ b/tests/test_armv7_root.py @@ -0,0 +1,169 @@ +"""The armv7 root: built by arm64 tools only, released on its own from orchard's 32-bit index. No builds.""" +import gzip +import importlib.util +import io +import json +from pathlib import Path +import struct +import subprocess +import sys +import tarfile +import tempfile +import tomllib +import unittest + +PROJECT = Path(__file__).resolve().parents[1] +sys.path.insert(0, str(PROJECT)) +import snapshot # noqa: E402 + + +def load(name, path): + spec = importlib.util.spec_from_file_location(name, PROJECT / path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +build = load('minimal_build_armv7', 'build.py') +audit = load('minimal_audit_armv7', 'audit.py') +base = load('minimal_base_armv7', 'build_base.py') + + +def elf(arch): + """The first bytes of an executable of ARCH.""" + header = bytearray(64) + header[:4] = b'\x7fELF' + header[4] = audit.ARCHITECTURES[arch]['class'] + header[5:7] = b'\x01\x01' + struct.pack_into(' 1: parser.error('--platform all builds every platform and combines with no other') - # The release builder builds every platform when it is given none. - selected = [part for name in args.platform if name != 'all' for part in ('--platform', name)] + known = snapshot.platforms(orchard) + named = [name for name in args.platform if name != 'all'] + unknown = sorted(set(named) - set(known)) + if unknown: + parser.error(f'unknown platform {", ".join(unknown)}; orchard describes {", ".join(known)}') + architectures = {known[name] for name in named} | ({args.arch} if args.arch else set()) + if len(architectures) > 1: + parser.error('one release has one root: choose platforms of one architecture') + arch = architectures.pop() if architectures else 'aarch64' + if not named and arch != 'aarch64': + named = sorted(name for name, value in known.items() if value == arch) + # Without names, orchard's inputs cover every arm64 platform. + selected = [part for name in named for part in ('--platform', name)] - base = snapshot.component('base', INPUTS['base'], - lambda path, work: python('build_base.py', '--output', path), artifact='base.tar.gz') + if arch == 'aarch64': + base = snapshot.component('base', INPUTS['base'], + lambda path, work: python('build_base.py', '--output', path), artifact='base.tar.gz') + else: + base = snapshot.component(f'base-{arch}', INPUTS[f'base-{arch}'], + lambda path, work: python('build_base.py', '--arch', arch, '--output', path), + artifact='base.tar.gz') artwork = snapshot.component('artwork', INPUTS['artwork'], lambda path, work: python('boot/build-artwork.py', '--output', path), artifact='artwork') @@ -57,15 +77,18 @@ def main(): resolved.parent.mkdir(parents=True, exist_ok=True) with tempfile.TemporaryDirectory(prefix='.inputs-', dir=PROJECT / 'out') as temporary: snapshot.command(orchard, 'inputs', '--output', resolved, *selected) - shared = json.loads(resolved.read_text())['shared'] - tools = {name: Path(path) for name, path in shared['tools'].items()} inputs = Path(temporary) / 'public' - python('pin_components.py', '--loader', shared['m1n1'], '--splash', artwork / 'splash', - '--logo', artwork / 'logo.bin', '--pongo', shared['pongo'], - '--busybox', tools['busybox'], '--dropbear', tools['dropbear'], '--dropbearkey', tools['dropbearkey'], - '--wpa-supplicant', tools['wpa_supplicant'], '--wpa-cli', tools['wpa_cli'], '--iw', tools['iw'], - '--output', inputs) - python('build.py', '--base', base, '--base-receipt', base.with_name(base.name + '.json'), + pinned = ['--splash', artwork / 'splash', '--logo', artwork / 'logo.bin'] + if arch == 'aarch64': + # orchard's static arm64 tools; an armv7 root has Alpine's packages of them. + shared = json.loads(resolved.read_text())['shared'] + tools = {name: Path(path) for name, path in shared['tools'].items()} + pinned += ['--loader', shared['m1n1'], '--pongo', shared['pongo'], + '--busybox', tools['busybox'], '--dropbear', tools['dropbear'], + '--dropbearkey', tools['dropbearkey'], '--wpa-supplicant', tools['wpa_supplicant'], + '--wpa-cli', tools['wpa_cli'], '--iw', tools['iw']] + python('pin_components.py', *pinned, '--output', inputs) + python('build.py', '--arch', arch, '--base', base, '--base-receipt', base.with_name(base.name + '.json'), '--inputs', inputs / 'inputs.json', '--orchard-inputs', resolved, '--orchard', orchard, '--version', args.version, '--output', args.output) python('audit.py', args.output, '--orchard', orchard)