diff --git a/audit.py b/audit.py index 8c9f6d8..823c5be 100644 --- a/audit.py +++ b/audit.py @@ -95,7 +95,7 @@ def audit(directory): payload = stream.read(row['bytes']) assert len(payload) == row['bytes'] and hashlib.sha256(payload).hexdigest() == row['sha256'] components[row['name']] = payload - profile = Path(orchard_hardware.__file__).parent / 'boot/initramfs/j71b/recovery/boot.json' + profile = Path(orchard_hardware.__file__).parent / 'boot/initramfs/recovery/boot.json' assert components['bootargs'] == json.loads(profile.read_text())['bootargs'].encode(), 'Recovery must preserve the tested standard boot profile' recovery = newc(gzip.decompress(components['initramfs'])) for name in ('usr/lib/firmware/regulatory.db', 'usr/lib/firmware/regulatory.db.p7s'): @@ -107,8 +107,7 @@ def audit(directory): for name, (data, mode, link) in entries.items(): if data is not None and (name.startswith(('usr/libexec/orchard-j71b/', 'usr/lib/modules/', 'usr/lib/firmware/', 'usr/share/licenses/orchard-regdb/')) or - name in ('usr/bin/orchard-device', 'usr/libexec/j71b-check-platform', - 'usr/libexec/j71b-charge-lease', 'etc/orchard/public.SHA256SUMS')): + name in ('usr/bin/orchard-device', 'etc/orchard/public.SHA256SUMS')): assert recovery[name][0] == data and stat.S_IMODE(recovery[name][1]) == mode shared += 1 for data, mode in recovery.values(): diff --git a/build.py b/build.py index b96e82b..a751d8f 100644 --- a/build.py +++ b/build.py @@ -63,8 +63,8 @@ def root_archive(base, base_receipt, components, output, version): for name, target in (('start-ssh', 'usr/sbin/recovery-start-ssh'), ('authorize-key', 'usr/sbin/authorize-key'), ('capture-adt', 'usr/sbin/capture-adt')): - additions[target] = ((hardware / 'boot/initramfs/j71b/recovery' / name).read_bytes(), 0o755) - additions['etc/udhcpd.conf'] = ((hardware / 'boot/initramfs/j71b/recovery/udhcpd.conf').read_bytes(), 0o644) + additions[target] = ((hardware / 'boot/initramfs/recovery' / name).read_bytes(), 0o755) + additions['etc/udhcpd.conf'] = ((hardware / 'boot/initramfs/recovery/udhcpd.conf').read_bytes(), 0o644) additions.update({ 'etc/orchard/release.json': (json.dumps({'protocol': 1, 'release': version, 'profile': 'j71b', 'kernel_release': kernel_release}).encode(), 0o644), @@ -132,7 +132,7 @@ def parents(name): member.mode, member.size = mode, len(data) add(member, data) for service in ('orchard-session', 'orchard-platform', 'orchard-diagnostics', - 'orchard-charge', 'orchard-dhcp', 'orchard-buttons', 'orchard-ready'): + 'orchard-dhcp', 'orchard-buttons', 'orchard-ready'): name = 'etc/runlevels/default/' + service parents(name) member = tarfile.TarInfo(name) @@ -155,7 +155,7 @@ def parents(name): def manifests(output, version, root): document = ('schema = 1\nkind = "orchard.install"\ndistro = "minimal-linux"\n' 'name = "Minimal Alpine"\nversion = ' + json.dumps(version) + '\n' - 'device = "j71b"\nboot_method = "checkm8-pongo"\nmin_orchard = "0.1.10"\nprotocol = 1\n' + 'device = "j71b"\nboot_method = "checkm8-pongo"\nmin_orchard = "0.1.11"\nprotocol = 1\n' '\n[install]\ndefault_root = "/minimal"\n') for role, name in (('root', 'rootfs.tar.gz'), ('boot', 'recovery.bin'), ('pongo', 'pongo.bin')): p = output / name diff --git a/cli b/cli index 4f96e58..20c67e1 160000 --- a/cli +++ b/cli @@ -1 +1 @@ -Subproject commit 4f96e5805975d00ec4b89fe4064e8c1acbb1bcc4 +Subproject commit 20c67e132c69a7737c8bd52af50d89c58dce8267 diff --git a/hardware b/hardware index d60da6b..2de374f 160000 --- a/hardware +++ b/hardware @@ -1 +1 @@ -Subproject commit d60da6b31a07f84a76fe24df27fcd398e60363ee +Subproject commit 2de374f8ef9dd1b77b7bb42a6425a57df539d7f0 diff --git a/image/j71b/image_policy.py b/image/j71b/image_policy.py index 86df7d5..ced717d 100644 --- a/image/j71b/image_policy.py +++ b/image/j71b/image_policy.py @@ -19,7 +19,7 @@ def stop_diagnostics(): CONFIG = Path('/etc/orchard/release.json') INSTALLED = Path('/.orchard-install.json') STARTED = Path('/run/openrc/started') -SERVICES = ('orchard-platform', 'orchard-diagnostics', 'orchard-charge', 'orchard-dhcp', 'orchard-buttons') +SERVICES = ('orchard-platform', 'orchard-diagnostics', 'orchard-dhcp', 'orchard-buttons') def process(pid, executable): @@ -39,7 +39,6 @@ def ready(*, ssh_revoked=False): if not (ssh_revoked and name == 'orchard-diagnostics')) and process(1, '/usr/bin/busybox') and (ssh_revoked or process(record['diagnostics_pid'], DIAGNOSTICS_EXECUTABLE)) and - process(record['charge_pid'], '/usr/bin/busybox') and process(record['dhcp_pid'], '/usr/libexec/minimal-linux/busybox') and process(record['buttons_pid'], str(Path('/usr/bin/python3').resolve())) and json.loads(INSTALLED.read_text())['release'] == record['release']) @@ -55,7 +54,7 @@ def validate_handoff(root): raise ValueError('Alpine PID 1 must be BusyBox init') for name in ('usr/bin/busybox', 'usr/bin/python3', 'usr/sbin/openrc', 'usr/sbin/openrc-run', 'usr/sbin/dropbear', - 'usr/libexec/j71b-check-platform', 'usr/libexec/j71b-charge-lease', + 'usr/libexec/minimal-check-platform', 'usr/libexec/minimal-ready', 'usr/libexec/minimal-clear-splash', 'usr/libexec/minimal-console', 'usr/libexec/minimal-buttons', 'usr/libexec/minimal-screen-toggle', 'usr/libexec/minimal-brightness-display', diff --git a/system/overlay/etc/init.d/orchard-charge b/system/overlay/etc/init.d/orchard-charge deleted file mode 100644 index dc80760..0000000 --- a/system/overlay/etc/init.d/orchard-charge +++ /dev/null @@ -1,6 +0,0 @@ -#!/sbin/openrc-run -description="Maintain the tested hardware charging lease" -command="/usr/libexec/j71b-charge-lease" -command_background=true -pidfile="/run/orchard-charge.pid" -depend() { need orchard-platform; } diff --git a/system/overlay/etc/init.d/orchard-platform b/system/overlay/etc/init.d/orchard-platform index fac8889..e216d89 100644 --- a/system/overlay/etc/init.d/orchard-platform +++ b/system/overlay/etc/init.d/orchard-platform @@ -1,8 +1,8 @@ #!/sbin/openrc-run -description="Verify preserved J71b hardware state and installed payload" +description="Verify the hardware and the installed hardware files" depend() { need orchard-session; } start() { ebegin "Checking Orchard hardware" - /usr/libexec/j71b-check-platform >/run/orchard-platform.log 2>&1 + /usr/libexec/minimal-check-platform >/run/orchard-platform.log 2>&1 eend $? } diff --git a/system/overlay/etc/init.d/orchard-ready b/system/overlay/etc/init.d/orchard-ready index 597e49d..2f155e0 100644 --- a/system/overlay/etc/init.d/orchard-ready +++ b/system/overlay/etc/init.d/orchard-ready @@ -1,6 +1,6 @@ #!/sbin/openrc-run description="Publish headless readiness only after required OpenRC services" -depend() { need orchard-platform orchard-diagnostics orchard-charge orchard-dhcp orchard-buttons; } +depend() { need orchard-platform orchard-diagnostics orchard-dhcp orchard-buttons; } start() { ebegin "Checking minimal Linux readiness" /usr/libexec/minimal-ready diff --git a/system/overlay/usr/libexec/minimal-check-platform b/system/overlay/usr/libexec/minimal-check-platform new file mode 100755 index 0000000..288f7ae --- /dev/null +++ b/system/overlay/usr/libexec/minimal-check-platform @@ -0,0 +1,15 @@ +#!/bin/sh +# Ready only on the hardware this system was installed for. No bring-up gates: +# the installed kernel and device, and the installed hardware files. Wi-Fi, +# Bluetooth and touch are reported, not required: experimental boards may +# lack some of them. +set -eu +config=/etc/orchard/release.json +field() { python3 -c 'import json, sys; print(json.load(open(sys.argv[1]))[sys.argv[2]])' "$config" "$1"; } +[ "$(uname -r)" = "$(field kernel_release)" ] +[ "$(tr '\000' '\n' < /proc/device-tree/compatible | head -1)" = "apple,$(field profile)" ] +state() { if eval "$2"; then echo "$1: ready"; else echo "$1: missing"; fi; } +state wifi '[ -d /sys/class/net/wlan0 ]' +state bluetooth '[ -d /sys/class/bluetooth/hci0 ]' +state touch 'grep -q "^N: Name=\".*Touchscreen\"" /proc/bus/input/devices' +sha256sum -c /usr/share/ipadlinux/hardware.SHA256SUMS >/dev/null diff --git a/system/overlay/usr/libexec/minimal-ready b/system/overlay/usr/libexec/minimal-ready index 4b201c9..ba10a54 100644 --- a/system/overlay/usr/libexec/minimal-ready +++ b/system/overlay/usr/libexec/minimal-ready @@ -16,7 +16,6 @@ while True: record = {'boot_id': policy.BOOT_ID.read_text().strip(), 'release': json.loads(policy.CONFIG.read_text())['release'], 'diagnostics_pid': int(Path('/run/recovery/dropbear.pid').read_text()), - 'charge_pid': int(Path('/run/orchard-charge.pid').read_text()), 'dhcp_pid': int(Path('/run/orchard-dhcp.pid').read_text()), 'buttons_pid': int(Path('/run/orchard-buttons.pid').read_text())} temporary = policy.RECORD.with_suffix('.pending') diff --git a/tests/native-image.py b/tests/native-image.py index 49f6ea2..033702e 100644 --- a/tests/native-image.py +++ b/tests/native-image.py @@ -38,7 +38,7 @@ # Exercise the actual platform checker's checksum command with Alpine's tools, # without bypassing its real hardware preconditions in the shipped checker. -checksum = Path('/usr/libexec/j71b-check-platform').read_text().splitlines()[-1] +checksum = Path('/usr/libexec/minimal-check-platform').read_text().splitlines()[-1] assert checksum == 'sha256sum -c /usr/share/ipadlinux/hardware.SHA256SUMS >/dev/null' Path('/tmp/checksum-data').write_bytes(b'known data') Path('/tmp/checksums').write_text(hashlib.sha256(b'known data').hexdigest() + ' /tmp/checksum-data\n') diff --git a/tests/native-init.py b/tests/native-init.py index 04ed34f..c17e154 100644 --- a/tests/native-init.py +++ b/tests/native-init.py @@ -45,11 +45,10 @@ from pathlib import Path assert str(Path('/proc/1/exe').readlink()) == '/usr/bin/busybox' assert Path('/proc/1/cmdline').read_bytes().split(b'\0')[0] in (b'/sbin/init', b'init') -assert subprocess.run(['/usr/libexec/j71b-check-platform'], capture_output=True).returncode != 0 +assert subprocess.run(['/usr/libexec/minimal-check-platform'], capture_output=True).returncode != 0 status=json.loads(subprocess.check_output(['/usr/bin/orchard-device','status']))['result'] assert status['ready'] is False assert not Path('/run/minimal-linux-ready.json').exists() -assert not Path('/run/openrc/started/orchard-charge').exists() assert not Path('/run/openrc/started/orchard-ready').exists() print(json.dumps({'busybox_pid1':True,'openrc_booted':True,'diagnostics_started':True, 'dhcp_started':True,'dhcp_process_alive_and_udp67_bound':True,'hardware_gate_failed_as_expected':True,'ready':False, diff --git a/tests/test_policy.py b/tests/test_policy.py index 1eed77d..3cf8ce8 100644 --- a/tests/test_policy.py +++ b/tests/test_policy.py @@ -28,7 +28,7 @@ def setUp(self): for service in policy.SERVICES: (policy.STARTED / service).touch() self.record = {'release': 'test', 'boot_id': 'current-boot', - 'diagnostics_pid': 2, 'charge_pid': 3, 'dhcp_pid': 4, 'buttons_pid': 5} + 'diagnostics_pid': 2, 'dhcp_pid': 4, 'buttons_pid': 5} self.save() # Tests run on macOS as an unprivileged developer. Root metadata and # live Linux executables are checked separately in the native image. @@ -44,14 +44,14 @@ def save(self): def test_current_boot_with_all_services_is_ready(self): self.assertTrue(policy.ready()) - self.assertEqual(self.process.call_count, 5) + self.assertEqual(self.process.call_count, 4) def test_revoked_ssh_does_not_remove_hardware_readiness(self): (policy.STARTED / 'orchard-diagnostics').unlink() self.process.side_effect = lambda pid, exe: pid != 2 self.assertFalse(policy.ready()) self.assertTrue(policy.ready(ssh_revoked=True)) - (policy.STARTED / 'orchard-charge').unlink() + (policy.STARTED / 'orchard-dhcp').unlink() self.assertFalse(policy.ready(ssh_revoked=True)) def test_prior_boot_is_not_ready(self): @@ -69,7 +69,7 @@ def test_different_installed_root_is_not_ready(self): self.assertFalse(policy.ready()) def test_missing_service_is_not_ready(self): - (policy.STARTED / 'orchard-charge').unlink() + (policy.STARTED / 'orchard-dhcp').unlink() self.assertFalse(policy.ready()) def test_dead_or_different_process_is_not_ready(self): diff --git a/uv.lock b/uv.lock index e3bfe8f..cfdacab 100644 --- a/uv.lock +++ b/uv.lock @@ -74,6 +74,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/41/45/1a4ed80516f02155c51f51e8cedb3c1902296743db0bbc66608a0db2814f/jsonschema_specifications-2025.9.1-py3-none-any.whl", hash = "sha256:98802fee3a11ee76ecaca44429fda8a41bff98b00a0f2838151b113f210cc6fe", size = 18437, upload-time = "2025-09-08T01:34:57.871Z" }, ] +[[package]] +name = "minimal-linux-dev" +version = "0.0.0" +source = { virtual = "." } +dependencies = [ + { name = "orchard-cli" }, + { name = "orchard-hardware" }, +] + +[package.metadata] +requires-dist = [ + { name = "orchard-cli", editable = "cli" }, + { name = "orchard-hardware", editable = "hardware" }, +] + [[package]] name = "mypy" version = "1.18.2" @@ -134,24 +149,9 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "minimal-linux-dev" -version = "0.0.0" -source = { virtual = "." } -dependencies = [ - { name = "orchard-cli" }, - { name = "orchard-hardware" }, -] - -[package.metadata] -requires-dist = [ - { name = "orchard-cli", editable = "cli" }, - { name = "orchard-hardware", editable = "hardware" }, -] - [[package]] name = "orchard-cli" -version = "0.1.10" +version = "0.1.11" source = { editable = "cli" } dependencies = [ { name = "jsonschema", version = "4.25.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.10'" }, @@ -177,7 +177,7 @@ dev = [{ name = "mypy", specifier = "==1.18.2" }] [[package]] name = "orchard-hardware" -version = "0.1.10" +version = "0.1.11" source = { editable = "hardware" } dependencies = [ { name = "tomli", marker = "python_full_version < '3.11'" },