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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 16 additions & 2 deletions BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 \
Expand All @@ -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

Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
41 changes: 34 additions & 7 deletions audit.py
Original file line number Diff line number Diff line change
Expand Up @@ -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('<H', data, 18)[0] == target['machine']
return True


def check_elf_pages(data):
"""The real J71b kernel uses 16 KiB pages; the container kernel may not."""
if data[:6] != b'\x7fELF\x02\x01':
Expand Down Expand Up @@ -53,15 +67,22 @@ def audit(directory):
manifest = tomllib.loads((directory / 'release.toml').read_text())
assert manifest['schema'] == 2 and manifest['distro'] == 'minimal-linux'
record = json.loads((directory / 'rootfs.json').read_text())
arch = record.get('architecture', 'aarch64')
rootfile = directory / 'rootfs.tar.gz'
assert rootfile.stat().st_size == record['bytes'] and sha(rootfile) == record['sha256']
# The checks read the tree the image was made from; orchard.image.root_image made it, and checks it.
for row in (manifest['root'], manifest['root']['map'], manifest['logo'], manifest['pongo'], manifest['m1n1'],
manifest['install_tools']):
if arch == 'aarch64':
assert 'pongo' in manifest and 'm1n1' in manifest and 'logo' in manifest
files = [manifest['logo'], manifest['pongo'], manifest['m1n1']]
else: # a board's Cascadia loader each; no Pongo, m1n1 or logo
assert manifest['boot_method'] == 'recovery-autogo' and manifest['cascadia']
assert not {'pongo', 'm1n1', 'logo'} & manifest.keys()
files = list(manifest['cascadia'].values())
for row in (manifest['root'], manifest['root']['map'], *files, manifest['install_tools']):
path = directory / row['file']
assert path.stat().st_size == row['bytes'] and sha(path) == row['sha256'], row['file']
entries = {}
size = elf_loads = 0
size = elf_loads = elf_files = 0
with tarfile.open(rootfile, 'r|gz') as archive:
for m in archive:
assert m.name not in entries
Expand All @@ -74,13 +95,16 @@ def audit(directory):
'usr/bin/sudo', 'usr/libexec/omarchy-', 'usr/lib/modules/'))
data = archive.extractfile(m).read() if m.isfile() else None
if data is not None:
elf_loads += check_elf_pages(data)
elf_files += check_elf_arch(data, arch)
if arch == 'aarch64':
elf_loads += check_elf_pages(data)
entries[m.name] = (data, m.mode, m.linkname)
size += m.size
assert len(entries) == record['members'] and size == record['expanded_bytes']
assert entries['lib'][2] == 'usr/lib'
assert entries['usr/sbin/init'][2] == '/bin/busybox'
assert entries['usr/lib/ld-musl-aarch64.so.1'][0].startswith(b'\x7fELF')
assert entries[ARCHITECTURES[arch]['loader']][0].startswith(b'\x7fELF')
assert entries['etc/apk/arch'][0].strip() == arch.encode()
assert 'usr/lib/libc.so.6' not in entries
assert entries['etc/shadow'][0].startswith(b'root:!:')
assert 'root/.ssh/authorized_keys' not in entries
Expand All @@ -89,7 +113,9 @@ def audit(directory):
# Every boot is reachable over USB: a serial console login and session SSH.
assert b'ttyGS0::respawn:/sbin/getty' in entries['etc/inittab'][0]
assert 'usr/share/orchard/splash' in entries, 'the boot splash for orchard launch'
for name in ('usr/sbin/dropbear', 'etc/init.d/orchard-ssh', 'etc/init.d/orchard-usb',
# resize2fs and python3: what the install finish step runs in the root.
for name in ('usr/sbin/resize2fs', 'usr/bin/python3',
'usr/sbin/dropbear', 'usr/libexec/minimal-linux/busybox', 'etc/init.d/orchard-ssh', 'etc/init.d/orchard-usb',
'etc/runlevels/default/orchard-ssh', 'etc/runlevels/default/orchard-usb',
'etc/init.d/orchard-splash', 'etc/orchard/udhcpd.conf'):
assert name in entries, name
Expand All @@ -106,7 +132,8 @@ def audit(directory):
# The installer refuses a layer file the root already has.
assert not (layer.keys() & {n for n, (data, _, _) in entries.items() if data is not None})
platforms[name] = {'boards': sorted(description['boards'])}
return {'root_sha256': record['sha256'], 'root_bytes': record['bytes'],
return {'architecture': arch, 'elf_files': elf_files,
'root_sha256': record['sha256'], 'root_bytes': record['bytes'],
'expanded_bytes': size, 'root_members': len(entries), 'platforms': platforms,
'elf_16k_compatible_load_segments': elf_loads,
'signed_regulatory_database_present': True,
Expand Down
Loading
Loading