diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e984908f..8f1f1f0a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -13,6 +13,11 @@ on: reuse_package_run_id: description: Exact prior run supplying the unchanged hardware wheel (sdist is refreshed) required: false + verify_n61_kernel: + description: Build the N61 4 KiB kernel on an ARM64 runner (full compile, up to 6 hours) + required: false + type: boolean + default: false permissions: contents: read actions: read @@ -54,6 +59,7 @@ jobs: fi python -m pip install build python -m build "${args[@]}" + python ci/audit_packages.py dist/* - uses: actions/upload-artifact@v4 with: name: hardware-packages @@ -150,6 +156,45 @@ jobs: compression-level: 0 if-no-files-found: error retention-days: 2 + kernel-n61-check: + if: github.event_name == 'workflow_dispatch' && inputs.verify_n61_kernel == true + needs: native-smoke + runs-on: ubuntu-24.04-arm + container: debian:bookworm + timeout-minutes: 360 + defaults: + run: + shell: bash + env: + ORCHARD_BUILD_NATIVE: '1' + ORCHARD_BUILD_ENV_ID: debian-bookworm-arm64-llvm19 + steps: + - run: apt-get update && apt-get install -y git ca-certificates python3 python3-venv + - uses: actions/checkout@v4 + with: + persist-credentials: false + - uses: actions/download-artifact@v4 + with: + name: hardware-packages + path: out/packages + - name: Install the kernel toolchain and hardware wheel + run: | + bash kernel/install-toolchain + python3 -m venv .venv + .venv/bin/python -m pip install out/packages/*.whl + echo "$GITHUB_WORKSPACE/.venv/bin" >> "$GITHUB_PATH" + - name: Build the N61 kernel, modules and device trees + run: | + bash kernel/build n61 --work-dir "$PWD/out/n61-work" --output "$PWD/out/n61-kernel" + dpkg-query -W > out/n61-toolchain-packages.lock + tar -C out -czf out/n61-kernel-check.tar.gz n61-kernel n61-toolchain-packages.lock + - uses: actions/upload-artifact@v4 + with: + name: n61-kernel-check + path: out/n61-kernel-check.tar.gz + compression-level: 0 + if-no-files-found: error + retention-days: 2 tools: if: github.ref_type == 'tag' || github.event_name == 'workflow_dispatch' needs: native-smoke diff --git a/MANIFEST.in b/MANIFEST.in index 4e4f205a..82e36ffd 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -8,4 +8,6 @@ recursive-include docs *.md recursive-include .github/workflows *.yml prune boot/m1n1/.work prune boot/quiet/.work +prune kernel/.work +prune .work global-exclude __pycache__ *.py[cod] .DS_Store diff --git a/README.md b/README.md index d50b6508..d22f34f9 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,11 @@ Hardware support and image-building resources for Linux on Apple devices. This r Currently: iPad 6 Wi-Fi (`iPad7,5`, J71b), experimental. Boot is tethered and GPU acceleration is not available. +The iPhone 6 (`iPhone7,2`, N61AP) has an experimental 4 KiB RAM-boot recipe. +An earlier build reached a tethered desktop with touch. Its +[bring-up instructions](docs/n61-bringup.md) describe the sources and the +physical checks required for the reconciled build. + ## Use For installing and starting a published system, use the [Orchard CLI](https://github.com/OrchardLinux/cli). Its release requirements select compatible CLI and hardware wheels automatically. diff --git a/boot/initramfs/nfs/README.md b/boot/initramfs/nfs/README.md new file mode 100644 index 00000000..4a6cbc28 --- /dev/null +++ b/boot/initramfs/nfs/README.md @@ -0,0 +1,119 @@ +# NFS RAM startup + +This shared hardware runtime starts a systemd root over a dedicated USB link. +It owns configfs USB, the network link, root validation and the PID 1 handoff. +The distro builds its signed package tree and decides which desktop to start. +Host software supplies the interface and NFS export. This path performs no +internal-storage operation. + +The first consumer is experimental N61. Its existing 4 KiB kernel includes +ECM, NCM, ACM, configfs, the Apple USB PHY/controller and NFSv4.0 as built-ins. +The root still carries the matching `/usr/lib/modules/` tree for +other drivers. Starting a desktop needs a fresh RAM boot: the original N61 +diagnostic `/init` has a foreground shell loop without a PID 1 handoff hook. + +On 2026-10-28, N61 completed the PID 1 handoff over ECM to an Arch NFS root +running systemd 262.1. Systemd reported `running` with zero failed units. +The desktop user had a PAM/logind session and user D-Bus socket; labwc and +Quickshell produced a desktop ready marker. A `grim` capture showed the +Omarchy wallpaper and top bar. SSH worked over the same ECM link. +The initial NFS boot preceded physical touch qualification. A later complete +payload accepted taps and swipes. On 2026-10-29, the recorded reconciled +combination reached the desktop twice with distinct boot IDs. Touch worked on +both boots, and the first covered vertical edges and on-screen keyboard input. +Internal storage remains unqualified. Installation is disabled. + +## Root identity and staging + +Prepare the aarch64 distro root on a filesystem that preserves Linux file +names, numeric ownership and modes. Copy the exact built kernel module tree +into `/usr/lib/modules/`. Leave `/dev`, `/proc`, `/sys` and +`/run` as real directories. The root's regular, root-owned +`/etc/orchard/nfs-root.json` contains exactly: + +```json +{ + "schema": 1, + "kind": "orchard.nfs-root", + "profile": "n61", + "architecture": "aarch64", + "kernel_release": "7.3.0-rc1-orchard-apple-4k", + "root_id": "06106106-1061-4061-8061-061061061061", + "image_sha256": "<64 lowercase hex digits from the public image archive>", + "image_receipt_sha256": "<64 lowercase hex digits from its public receipt>" +} +``` + +Mint a fresh canonical UUID for each private root. The image digests bind it +to the distro's audited input. The RAM builder embeds the SHA-256 of these +exact JSON bytes, so host and runtime agree without a JSON interpreter in RAM. +The running kernel release and device-tree compatible must also match. The +runtime inspects the systemd ELF header before executing the new root. +The trusted host controls the writable export after this check. + +```sh +python -m orchard_hardware.boot.initramfs.nfs.stage \ + --receipt /path/to/root/etc/orchard/nfs-root.json \ + --busybox /path/to/static-aarch64-busybox \ + --output /path/to/new-ram-root \ + --server 10.61.0.1 --address 10.61.0.2/30 \ + --export /n61-root --port 32061 --function ecm --handoff manual \ + > /path/to/nfs-runtime-receipt.json +``` + +The output uses native `orchard build DIRECTORY` layout, with `init`, +`orchard.toml` and a `rootfs` overlay. Pass the same `--busybox` file to that +build, together with the existing N61 kernel, m1n1 and qualified Pongo inputs. +Keep the resulting RAM image and staging +receipt private because they select a specific writable root. The builder +requires a fresh output path and a static AArch64 BusyBox. ECM is the default; +NCM can be selected after the host's driver has been qualified. The generated +MAC addresses derive from the two IPv4 addresses and appear in the runtime +receipt. + +Use the pinned hardware BusyBox recipe, which supplies `awk`, `mount`, `stat`, +`sha256sum`, `od`, `timeout`, `setsid` and `switch_root`. Mount tracking reads +`/proc/self/mountinfo` with `awk`, including while `/proc` lives under +`/newroot`; the pinned BusyBox omits the `mountpoint` applet. Failure recovery +tracks the retired rescue process explicitly so its replacement starts even +while the old PID still exists. + +The N61 link reserves host `10.61.0.1`, device `10.61.0.2/30` and NFS TCP port +`32061`. A dedicated userspace NFS server can expose a Linux volume at the +NFSv4 pseudo path `/n61-root`, with `sec=sys`, writable access, numeric IDs and +root ownership preserved. The runtime explicitly requests NFSv4.0 over TCP, +so rpcbind and mountd ports are outside its protocol. Keep P105's existing +network, exports and host services unchanged. +Before arming handoff, verify through the NFS client that the root metadata +and systemd retain UID/GID `0:0`; NFSv4 owner mapping must preserve those IDs. + +## Handoff and rescue + +The default `--handoff manual` leaves the physical ACM shell running after +configuring USB. Create `/run/orchard/nfs-handoff` through that shell when the +host interface and export are ready. `--handoff auto` arms that request during +startup. PID 1 attempts the mount once with a 60-second initial timeout and +refuses a mismatched root. Network failure can still stall later I/O to a hard +NFS mount; the physical rescue tools remain in RAM. + +After validation, PID 1 moves `/dev`, `/proc`, `/sys` and `/run` into the new +root and calls BusyBox `switch_root`. The new PID 1 starts systemd. A transient +`orchard-acm-rescue.service` under `/run/systemd/system` runs the copied static +BusyBox and shell from `/run/orchard/rescue`. Its physical USB root access has +the same trusted-host scope as the RAM experiment. A runtime NetworkManager +configuration keeps `usb0` unmanaged so desktop services preserve the NFS +transport. Configfs keeps ownership of the existing composite gadget. + +The experimental USB PID is `05ac:4144`. Its descriptor publishes `PROFILE`, +the selected `ROOT` UUID and the live kernel `BOOT` UUID. This minimal runtime +does not read ECID from the ADT; it does not qualify as the installed recovery +protocol. The boot operator must retain the verified target's physical USB +location when selecting its ACM port. + +Before this path can present itself as normal Orchard recovery, adapt the +protected identity flow for N61: read and validate its live ADT, publish the +ECID and current boot UUID through the verified serial connection, and retain +that identity across the NFS handoff. Provision the host's public key through +that connection and pin the boot-scoped SSH host key received there. Recheck +the identity after handoff and exercise access retirement and failure recovery. +Until those checks pass, this remains a bring-up runtime. diff --git a/boot/initramfs/nfs/__init__.py b/boot/initramfs/nfs/__init__.py new file mode 100644 index 00000000..d5c7ecf9 --- /dev/null +++ b/boot/initramfs/nfs/__init__.py @@ -0,0 +1 @@ +"""Shared RAM startup for an explicitly identified NFS systemd root.""" diff --git a/boot/initramfs/nfs/acm-rescue b/boot/initramfs/nfs/acm-rescue new file mode 100644 index 00000000..365b5afa --- /dev/null +++ b/boot/initramfs/nfs/acm-rescue @@ -0,0 +1,16 @@ +#!/run/orchard/rescue/busybox sh +# Keep every executable and the shell history on the RAM mount. +BB=/run/orchard/rescue/busybox +export PATH=/run/orchard/rescue HOME=/run/orchard/rescue TERM=linux +export HISTFILE=/run/orchard/rescue/.ash_history +cd /run/orchard/rescue +while :; do + if [ -c /dev/ttyGS0 ]; then + "$BB" setsid "$BB" sh -i /dev/ttyGS0 2>&1 & + shell=$! + echo "$shell" > /run/orchard/rescue/shell.pid + wait "$shell" || true + "$BB" rm -f /run/orchard/rescue/shell.pid + fi + "$BB" sleep 1 +done diff --git a/boot/initramfs/nfs/handoff b/boot/initramfs/nfs/handoff new file mode 100644 index 00000000..32612c07 --- /dev/null +++ b/boot/initramfs/nfs/handoff @@ -0,0 +1,91 @@ +#!/bin/busybox sh +# Only PID 1 can retire the initial RAM filesystem and enter systemd. +set -eu +export PATH=/bin:/sbin:/usr/bin:/usr/sbin +[ "$$" -eq 1 ] || { echo 'NFS handoff requires PID 1' >&2; exit 1; } +. /etc/orchard/nfs.conf +rescue_retired=false +is_mounted() { + # Debian's static BusyBox omits mountpoint. /proc itself can be moved + # during handoff, so inspect whichever proc mount is still reachable. + if [ -r /proc/self/mountinfo ]; then + table=/proc/self/mountinfo + elif [ -r /newroot/proc/self/mountinfo ]; then + table=/newroot/proc/self/mountinfo + else + return 1 + fi + awk -v target="$1" '$5 == target { found = 1 } END { exit !found }' "$table" +} +failed() { + echo 'NFS handoff stopped; physical RAM rescue remains available.' + for name in run sys proc dev; do + if is_mounted "/newroot/$name" && ! is_mounted "/$name"; then + mount --move "/newroot/$name" "/$name" || true + fi + done + echo failed > /run/orchard/nfs-handoff-state + if [ "$rescue_retired" = true ] || ! kill -0 "$(cat /run/orchard/rescue.pid)" 2>/dev/null; then + # A retired child may still answer kill -0 while exiting or as a + # zombie. Its replacement must start even while that PID exists. + if [ "$rescue_retired" = true ]; then + kill -KILL "$(cat /run/orchard/rescue.pid)" 2>/dev/null || true + fi + /run/orchard/rescue/busybox sh /run/orchard/rescue/acm-rescue & + echo "$!" > /run/orchard/rescue.pid + fi + while :; do sleep 60; done +} +trap failed EXIT +[ ! -e /run/orchard/nfs-handoff-attempted ] +touch /run/orchard/nfs-handoff-attempted +/sbin/orchard-nfs-validate --board +options="vers=4.0,proto=tcp,port=$NFS_PORT,addr=$NFS_SERVER,hard,timeo=50,retrans=2" +timeout -s KILL 60 mount -t nfs4 -o "$options" "$NFS_SERVER:$NFS_EXPORT" /newroot +/sbin/orchard-nfs-validate /newroot +mkdir -p /run/systemd/system/sysinit.target.wants +cat > /run/systemd/system/orchard-acm-rescue.service <<'UNIT' +[Unit] +Description=Orchard physical USB RAM rescue +DefaultDependencies=no +Before=basic.target +IgnoreOnIsolate=yes + +[Service] +Type=simple +ExecStart=/run/orchard/rescue/busybox sh /run/orchard/rescue/acm-rescue +Restart=always +RestartSec=1 +TimeoutStopSec=2 +KillMode=control-group +UNIT +ln -s ../orchard-acm-rescue.service /run/systemd/system/sysinit.target.wants/orchard-acm-rescue.service +# Keep an NFS-rooted interface outside NetworkManager's DHCP/reconnection policy. +mkdir -p /run/NetworkManager/conf.d +cat > /run/NetworkManager/conf.d/90-orchard-nfs.conf <<'NETWORK' +[keyfile] +unmanaged-devices=interface-name:usb0 +NETWORK +cat > /run/orchard/rescue/systemd-init <<'INIT' +#!/run/orchard/rescue/busybox sh +export PATH=/usr/local/sbin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin +export HOME=/root USER=root LOGNAME=root TERM=linux +[ "$$" -eq 1 ] || exit 1 +echo /sbin/modprobe > /proc/sys/kernel/modprobe +exec /usr/lib/systemd/systemd +INIT +chmod 755 /run/orchard/rescue/systemd-init +echo ready > /run/orchard/nfs-handoff-state +# Stop the pre-handoff shell; systemd starts its RAM-owned replacement. +if [ -f /run/orchard/rescue.pid ]; then + rescue_retired=true + rescue=$(cat /run/orchard/rescue.pid) + kill "$rescue" 2>/dev/null || true + # The foreground shell was spawned in its own session. + if [ -f /run/orchard/rescue/shell.pid ]; then + shell=$(cat /run/orchard/rescue/shell.pid) + /bin/busybox kill -KILL "-$shell" 2>/dev/null || true + fi +fi +for name in dev proc sys run; do mount --move "/$name" "/newroot/$name"; done +exec /bin/busybox switch_root /newroot /run/orchard/rescue/systemd-init diff --git a/boot/initramfs/nfs/init b/boot/initramfs/nfs/init new file mode 100644 index 00000000..33e419c9 --- /dev/null +++ b/boot/initramfs/nfs/init @@ -0,0 +1,41 @@ +#!/bin/busybox sh +# Shared NFS RAM PID 1. Internal storage is outside this startup path. +set -eu +export PATH=/bin:/sbin:/usr/bin:/usr/sbin HOME=/root TERM=linux +/bin/busybox --install -s /bin +startup_failed() { + echo 'NFS startup stopped; LCD RAM rescue is available.' + while :; do /bin/busybox sh -i || true; /bin/busybox sleep 1; done +} +trap startup_failed EXIT +mount -t proc -o nosuid,nodev,noexec proc /proc +mount -t sysfs -o nosuid,nodev,noexec sysfs /sys +mount -t devtmpfs -o nosuid devtmpfs /dev +mount -t tmpfs -o mode=0755,nosuid,nodev tmpfs /run +mkdir -p /dev/pts /newroot /run/orchard/rescue +chmod 700 /run/orchard /run/orchard/rescue +mount -t devpts -o mode=0620,ptmxmode=0666 devpts /dev/pts +exec /dev/console 2>&1 +echo /bin/false > /proc/sys/kernel/modprobe +. /etc/orchard/nfs.conf +cp /bin/busybox /run/orchard/rescue/busybox +cp /sbin/orchard-acm-rescue /run/orchard/rescue/acm-rescue +chmod 755 /run/orchard/rescue/busybox /run/orchard/rescue/acm-rescue +/run/orchard/rescue/busybox --install -s /run/orchard/rescue +if ! /sbin/orchard-nfs-validate --board; then + echo 'NFS runtime identity rejected; leaving the LCD rescue shell.' + while :; do sh -i || true; sleep 1; done +fi +/sbin/orchard-nfs-usb +ip link set lo up +ip address add "$NFS_ADDRESS" dev usb0 +ip link set usb0 up +/run/orchard/rescue/busybox sh /run/orchard/rescue/acm-rescue & +echo "$!" > /run/orchard/rescue.pid +echo "$NFS_PROFILE: USB network ready; NFS root $NFS_ROOT_ID." +if [ "$NFS_HANDOFF" = auto ]; then + touch /run/orchard/nfs-handoff +fi +echo 'Create /run/orchard/nfs-handoff to start the configured NFS root.' +while [ ! -f /run/orchard/nfs-handoff ]; do sleep 1; done +exec /sbin/orchard-nfs-handoff diff --git a/boot/initramfs/nfs/stage.py b/boot/initramfs/nfs/stage.py new file mode 100644 index 00000000..c42a2f3f --- /dev/null +++ b/boot/initramfs/nfs/stage.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +"""Prepare a BusyBox-only RAM runtime bound to one NFS root receipt.""" +import argparse +import hashlib +import ipaddress +import json +from pathlib import Path +import re +import shlex +import shutil +import struct +import uuid + +from orchard_hardware import device + +HERE = Path(__file__).resolve().parent +FIELDS = {'schema', 'kind', 'profile', 'architecture', 'kernel_release', 'root_id', + 'image_sha256', 'image_receipt_sha256'} +SCRIPTS = {'init': 'init', 'usb-gadget': 'sbin/orchard-nfs-usb', + 'handoff': 'sbin/orchard-nfs-handoff', 'validate-root': 'sbin/orchard-nfs-validate', + 'acm-rescue': 'sbin/orchard-acm-rescue'} + + +def unique_object(pairs): + result = {} + for name, value in pairs: + if name in result: + raise ValueError('Duplicate root identity field: ' + name) + result[name] = value + return result + + +def identity(blob): + if len(blob) > 16384: + raise ValueError('Root identity is too large') + value = json.loads(blob, object_pairs_hook=unique_object) + if not isinstance(value, dict) or set(value) != FIELDS: + raise ValueError('Unexpected root identity fields') + if type(value['schema']) is not int or value['schema'] != 1 or value['kind'] != 'orchard.nfs-root': + raise ValueError('Unsupported NFS root identity') + if value['architecture'] != 'aarch64': + raise ValueError('NFS startup currently requires aarch64') + for key in ('image_sha256', 'image_receipt_sha256'): + if not isinstance(value[key], str) or not re.fullmatch('[0-9a-f]{64}', value[key]): + raise ValueError('Invalid root digest: ' + key) + root_id = value['root_id'] + if not isinstance(root_id, str) or str(uuid.UUID(root_id)) != root_id: + raise ValueError('Root ID must be a canonical UUID') + release = value['kernel_release'] + if not isinstance(release, str) or not re.fullmatch('[A-Za-z0-9][A-Za-z0-9._+-]{0,127}', release): + raise ValueError('Invalid kernel release') + description = device(value['profile']) + compatible = description['device']['dt_compatible'] + if (not compatible or any(not re.fullmatch('[a-z0-9,._+-]+', item) for item in compatible)): + raise ValueError('Invalid profile device-tree identity') + return value, compatible + + +def static_busybox(path): + blob = path.read_bytes() + if (len(blob) < 64 or blob[:6] != b'\x7fELF\x02\x01' or + struct.unpack_from(' 1024 or offset + size * count > len(blob): + raise ValueError('Invalid BusyBox program headers') + if any(struct.unpack_from(' "$g/idVendor" +echo 0x4144 > "$g/idProduct" +echo 0x0200 > "$g/bcdUSB" +mkdir -p "$g/strings/0x409" "$g/configs/c.1/strings/0x409" +echo Orchard > "$g/strings/0x409/manufacturer" +echo "Orchard $NFS_PROFILE NFS" > "$g/strings/0x409/product" +printf 'PROFILE:%s ROOT:%s BOOT:%s\n' "$NFS_PROFILE" "$NFS_ROOT_ID" \ + "$(cat /proc/sys/kernel/random/boot_id)" > "$g/strings/0x409/serialnumber" +echo 'USB network and physical rescue shell' > "$g/configs/c.1/strings/0x409/configuration" +# Descriptor power only; hardware charging remains a separate driver concern. +echo 100 > "$g/configs/c.1/MaxPower" +mkdir "$g/functions/$NFS_FUNCTION.usb0" "$g/functions/acm.usb0" +echo "$NFS_HOST_MAC" > "$g/functions/$NFS_FUNCTION.usb0/host_addr" +echo "$NFS_DEVICE_MAC" > "$g/functions/$NFS_FUNCTION.usb0/dev_addr" +ln -s "$g/functions/$NFS_FUNCTION.usb0" "$g/configs/c.1/" +ln -s "$g/functions/acm.usb0" "$g/configs/c.1/" +set -- /sys/class/udc/* +[ "$#" -eq 1 ] && [ -e "$1" ] +printf '%s\n' "${1##*/}" > "$g/UDC" diff --git a/boot/initramfs/nfs/validate-root b/boot/initramfs/nfs/validate-root new file mode 100644 index 00000000..eb8bb0ec --- /dev/null +++ b/boot/initramfs/nfs/validate-root @@ -0,0 +1,36 @@ +#!/bin/busybox sh +# This helper executes only RAM tools while inspecting the remote root. +set -eu +. /etc/orchard/nfs.conf +reject() { echo "NFS root rejected: $*" >&2; exit 1; } +[ "$(uname -m)" = "$NFS_ARCHITECTURE" ] || reject 'CPU ABI differs' +[ "$(uname -r)" = "$NFS_KERNEL_RELEASE" ] || reject 'kernel release differs' +matched=false +for compatible in $NFS_COMPATIBLE; do + if tr '\000' '\n' < /proc/device-tree/compatible | grep -Fqx "$compatible"; then matched=true; fi +done +[ "$matched" = true ] || reject 'device tree differs' +[ "${1:-}" != --board ] || exit 0 +root=${1:-/newroot} +for directory in etc etc/orchard usr usr/lib usr/lib/systemd usr/lib/modules \ + "usr/lib/modules/$NFS_KERNEL_RELEASE" dev proc sys run; do + [ -d "$root/$directory" ] && [ ! -L "$root/$directory" ] || reject "invalid $directory" +done +safe_file() { + [ -f "$1" ] && [ ! -L "$1" ] || reject 'missing regular root file' + [ "$(stat -c %u "$1")" = 0 ] || reject 'root file has another owner' + mode=$(stat -c %a "$1") + [ $((0$mode & 0022)) -eq 0 ] || reject 'root file permits group/other writes' +} +metadata=$root/etc/orchard/nfs-root.json +safe_file "$metadata" +set -- $(sha256sum "$metadata") +[ "$1" = "$NFS_IDENTITY_SHA256" ] || reject 'root receipt differs' +systemd=$root/usr/lib/systemd/systemd +safe_file "$systemd" +[ -x "$systemd" ] || reject 'systemd is not executable' +# ELF64, little endian, AArch64. Do not invoke a binary from the NFS root here. +header=$(dd if="$systemd" bs=1 count=6 2>/dev/null | od -An -v -tx1 | tr -d ' \n') +machine=$(dd if="$systemd" bs=1 skip=18 count=2 2>/dev/null | od -An -v -tx1 | tr -d ' \n') +[ "$header:$machine" = 7f454c460201:b700 ] || reject 'systemd ELF ABI differs' +safe_file "$root/usr/lib/modules/$NFS_KERNEL_RELEASE/modules.dep" diff --git a/boot/m1n1/build b/boot/m1n1/build index 76d0feb8..c1180e97 100755 --- a/boot/m1n1/build +++ b/boot/m1n1/build @@ -57,6 +57,19 @@ if "$resume"; then echo 'No matching source to resume' >&2; exit 1; } cmp -s "$applied" <(patch_list) || { echo 'm1n1 patch set changed; use a fresh build directory' >&2; exit 1; } + # Confirm the source still contains every layer without changing its real + # index or discarding unrelated edits. Later patches may overlap earlier + # ones, so unwind the series in reverse inside a disposable index. + ( + verification_dir=$(mktemp -d "${TMPDIR:-/tmp}/orchard-m1n1-resume.XXXXXX") + trap 'rm -rf "$verification_dir"' EXIT + export GIT_INDEX_FILE="$verification_dir/index" + git -C "$source" -c core.splitIndex=false read-tree HEAD + git -C "$source" -c core.splitIndex=false add --all -- . + for ((patch_index=${#patches[@]} - 1; patch_index >= 0; patch_index--)); do + git -C "$source" -c core.splitIndex=false apply --cached --reverse "${patches[patch_index]}" + done + ) else [[ ! -e "$work" ]] || { echo 'Build directory exists; use --resume' >&2; exit 1; } mkdir -p "$source" diff --git a/boot/m1n1/patches/orchard/0003-Forward-touchscreen-proximity-calibration.patch b/boot/m1n1/patches/orchard/0003-Forward-touchscreen-proximity-calibration.patch new file mode 100644 index 00000000..f2eae07e --- /dev/null +++ b/boot/m1n1/patches/orchard/0003-Forward-touchscreen-proximity-calibration.patch @@ -0,0 +1,20 @@ +Subject: [PATCH] kboot: forward proximity calibration for touchscreen aliases + +N61 Gen1 firmware optionally uses its per-unit proximity calibration in +addition to the required multi-touch calibration. Copy it through the +existing touchscreen0 alias path and keep the same missing/syscfg-reference +handling as the other calibration providers. The firmware container names +the property at runtime; no unit-specific bytes enter the shared recipe. + +--- +diff --git a/src/kboot.c b/src/kboot.c +--- a/src/kboot.c ++++ b/src/kboot.c +@@ -932,6 +932,7 @@ + {"firefly-p100-calibration", "apple,z2-firefly-p100-cal-blob"}, + {"ext-multi-touch-calibration", "apple,z2-ext-cal-blob"}, + {"pressure-effect-correction", "apple,z2-pressure-cal-blob"}, ++ {"prox-calibration", "apple,z2-prox-cal-blob"}, + }; + const char *path = fdt_get_alias(dt, "touchscreen0"); + if (path == NULL) diff --git a/ci/audit_packages.py b/ci/audit_packages.py new file mode 100644 index 00000000..85eb850b --- /dev/null +++ b/ci/audit_packages.py @@ -0,0 +1,107 @@ +#!/usr/bin/env python3 +"""Check public package resources and refuse build scratch or archive links.""" +import argparse +import json +from pathlib import Path, PurePosixPath +import stat +import tarfile +import zipfile + +ROOT = Path(__file__).resolve().parents[1] + + +def resources(source): + files = {source / name for name in ( + '__init__.py', 'buildenv.py', + 'kernel/Containerfile', 'kernel/install-toolchain', 'kernel/build', + 'kernel/build-modules', 'kernel/stage-modules.py', 'kernel/write-receipt.py', + 'kernel/series.py', 'kernel/sources.toml', 'kernel/upstream.lock', + 'boot/m1n1/Containerfile', 'boot/m1n1/install-toolchain', 'boot/m1n1/build', + 'boot/m1n1/series', 'boot/m1n1/sources.toml', 'boot/m1n1/upstream.lock')} + for pattern in ('features.toml', 'apple/*.py', 'image/*.py', 'launcher/*.py', + 'runtime/j71b/*.py', 'runtime/j71b/phase2-hardware', + 'devices/*/device.toml', 'devices/*/firmware.json', + 'devices/*/public-firmware/*', 'schemas/v1/*.json', + 'boot/__init__.py', 'boot/initramfs/*.py', + 'boot/initramfs/busybox/*.py', 'boot/initramfs/busybox/Containerfile', + 'boot/initramfs/recovery/*.json', 'boot/initramfs/recovery/Containerfile', + 'boot/initramfs/recovery/init', 'boot/initramfs/recovery/usb-gadget', + 'boot/initramfs/recovery/authorize-key', 'boot/initramfs/recovery/start-ssh', + 'boot/initramfs/recovery/capture-adt', 'boot/initramfs/recovery/udhcpd.conf', + 'boot/initramfs/recovery/upstream.lock', 'boot/initramfs/recovery/build-tools*', + 'boot/initramfs/recovery/install-toolchain', + 'boot/quiet/*.py', 'boot/quiet/*.patch', + 'kernel/series/*', 'kernel/configs/*/*', 'kernel/patches/**/*.patch', + 'kernel/devicetree/*/*', 'kernel/modules/*/*/*', + 'boot/m1n1/patches/**/*.patch', + 'docs/bring-up/*.md', + 'boot/initramfs/nfs/*'): + files.update(path for path in source.glob(pattern) if path.is_file()) + return sorted(files) + + +def member_name(name): + path = PurePosixPath(name) + if '\\' in name or path.is_absolute() or '..' in path.parts or not path.parts: + raise ValueError('Unsafe archive path: ' + name) + if any(part in ('.work', '__pycache__') for part in path.parts): + raise ValueError('Build scratch in public package: ' + name) + return path + + +def audit(archive, source=ROOT): + source = Path(source).resolve() + archive = Path(archive) + if archive.suffix == '.whl': + container = zipfile.ZipFile(archive) + entries = container.infolist() + names = [entry.filename for entry in entries] + sizes = {entry.filename: entry.file_size for entry in entries} + links = [entry.filename for entry in entries + if stat.S_ISLNK(entry.external_attr >> 16)] + prefix = 'orchard_hardware/' + read = container.read + else: + container = tarfile.open(archive, 'r:gz') + entries = container.getmembers() + names = [entry.name for entry in entries] + sizes = {entry.name: entry.size for entry in entries} + links = [entry.name for entry in entries if entry.issym() or entry.islnk()] + roots = {part for name in names for part in PurePosixPath(name).parts[:1]} + if len(roots) != 1: + container.close() + raise ValueError('Source package needs one enclosing directory') + prefix = next(iter(roots)) + '/' + + def read(name): + with container.extractfile(name) as stream: + return stream.read() + + with container: + for name in names: + member_name(name) + if links: + raise ValueError('Archive link in public package: ' + links[0]) + if len(names) != len(set(names)): + raise ValueError('Duplicate archive member') + required = resources(source) + for path in required: + name = prefix + path.relative_to(source).as_posix() + if name not in sizes: + raise ValueError('Missing package resource: ' + name) + if sizes[name] != path.stat().st_size or read(name) != path.read_bytes(): + raise ValueError('Changed package resource: ' + name) + return {'archive': str(archive), 'members': len(names), 'verified_resources': len(required)} + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('archives', nargs='+', type=Path) + parser.add_argument('--source', type=Path, default=ROOT) + args = parser.parse_args() + for archive in args.archives: + print(json.dumps(audit(archive, args.source), sort_keys=True)) + + +if __name__ == '__main__': + main() diff --git a/devices/n61/device.toml b/devices/n61/device.toml new file mode 100644 index 00000000..f5c92c83 --- /dev/null +++ b/devices/n61/device.toml @@ -0,0 +1,53 @@ +# iPhone 6 (N61AP): experimental A8 RAM-boot recipe. +schema = 1 +kind = "hardware.device" + +[device] +profile = "n61" +name = "iPhone 6" +apple_products = ["iPhone7,2"] +dt_compatible = ["apple,n61"] + +# Observed on N61AP in DFU; T7000 identifies itself as CPID 7000. +[match.dfu] +chip = 0x7000 +board = 0x06 + +[boot] +methods = ["checkm8-pongo"] + +[kernel] +platform = "apple-4k" +dtb = "t7000-n61" + +# The RAM shell uses public components; touch firmware is extracted privately. +[firmware] +recipe = "firmware.json" + +[support] +install = false +status = "experimental" +caveats = [ + "A recorded kernel, loader and private USB/NFS desktop combination reached the desktop twice using PongoOS 2.6.3-e98323f8 bundled with palera1n 2.4; other builds need their own physical checks.", + "Physical taps and swipes worked on both recorded boots; the first covered the panel's vertical edges and on-screen keyboard entry.", + "Bluetooth, Wi-Fi and internal storage are unqualified; installation remains disabled.", +] +wifi = false +usb_serial = true # The first N61 RAM boot reached the physical USB shell. +usb_network = true # ECM carried the NFS-rooted Arch desktop and SSH on N61. + +[features] +display = "working" # Linux framebuffer console was visible on the LCD. +touch = "working" # Physical evdev contacts/releases reached the Arch compositor. +wifi = "missing" +bluetooth = "missing" +storage = "missing" +battery = "untested" +charging = "missing" +backlight = "untested" +buttons = "untested" +audio = "missing" +camera = "missing" +cellular = "missing" +suspend = "missing" +gpu = "missing" diff --git a/devices/n61/firmware.json b/devices/n61/firmware.json new file mode 100644 index 00000000..6dea5e9b --- /dev/null +++ b/devices/n61/firmware.json @@ -0,0 +1,39 @@ +{ + "schema": 2, + "device": "iPhone7,2", + "ipsw": { + "version": "12.5.1", + "build": "16H22", + "url": "https://updates.cdn-apple.com/2021WinterFCS/fullrestores/001-90777/D1E46A9D-8C08-41DA-AA56-013C1D118A57/iPhone_4.7_12.5.1_16H22_Restore.ipsw", + "bytes": 3036811777, + "sha256": "ca18b1c4586630d50effe03af9879a7f2aefd0a9ce3d9c4208154e495b2d79f6" + }, + "inputs": { + "N61.mtprops": { + "ipsw": { + "rootfs": "usr/share/firmware/multitouch/N61.mtprops" + }, + "device": "/usr/share/firmware/multitouch/N61.mtprops", + "sha256": "b33e095df3040ccbcddaecec215d63c08c1f30235e47b8868c33d1bd2fb83f45" + } + }, + "outputs": [ + { + "destination": "lib/firmware/apple/n61-fwstart.z2fw", + "sha256": "51d2ef7b25c1d7e323fa703b531223312cb76f7183fbeb425f921dd6e5509294", + "steps": [ + { + "step": "z2fw-from-mtprops@3", + "input": "N61.mtprops", + "profile": "C1F59,1", + "packet": "Z2", + "touch_maximum": 1024, + "prox_maximum": 128, + "fll_mval": 6099, + "ref_clk_div": 2, + "clk32_clock_enable": 1 + } + ] + } + ] +} diff --git a/docs/bring-up/n61-touch.md b/docs/bring-up/n61-touch.md new file mode 100644 index 00000000..f64a6995 --- /dev/null +++ b/docs/bring-up/n61-touch.md @@ -0,0 +1,106 @@ +# iPhone 6 touchscreen bring-up + +N61 uses the native `apple_z2` input driver with a T7000 SPI controller profile, +PMGR touch clock and PMIC supply switches. The device selects the `apple-4k` +kernel. Its board description and driver patches live in +`kernel/patches/devices/t7000/`. + +The tethered N61 session on 2026-10-28 accepted ROM readiness, uploaded the +model firmware and returned a valid surface descriptor over SPI. Linux exposed +`iPhone 6 Touchscreen`, and Labwc opened its evdev endpoint. A physical test +then captured 996 input events over 117 synchronized reports. All ten contacts +ended with releases. The user entered calculator digits and resized its window +through the panel, while the host captured the results. GPIO3 returned high +after the report queue cleared. + +The complete native payload also passed a fresh tethered boot into Arch and the +Omarchy desktop that day. The standard module loaded automatically, and physical +input produced 11,668 events over 1,408 synchronized reports. Fifty contacts each +ended with a release. The user confirmed calculator input and swipes. This +capture reached x=0..747 and y=117..1150, with every slot inactive at the end. + +On 2026-10-29, a reconciled kernel and private firmware combination reached the +desktop on two tethered boots. The first capture recorded 25 contacts and 25 +releases, reaching y=26..1322 on the 0..1333 vertical axis. The owner entered +`n61` with the on-screen keyboard. Touch also worked on the second boot, with +18 contacts and 18 releases. The build identities are in the N61 bring-up record. + +The native touchscreen child is enabled after this qualification. The profile +retains experimental status with installation disabled. + +## Board controls + +| Resource | N61 connection | +|---|---| +| SPI2 | `0x20a088000`, size `0x4000`, AIC interrupt 154 | +| SPI input clock | 60 MHz | +| Touch transfer policy | Mode 0, 8-bit words, 500 kHz during bring-up | +| Chip select | GPIO185, active low | +| Reset | GPIO2, active low | +| Touch interrupt | GPIO3, falling edge | +| Touch clock | PMGR offset `0x1e000`, 24 MHz parent, requested rate 32768 Hz | +| Core supply | D2186 at I2C0 address `0x74`, register `0x0085`, bit 1 | +| Analog supply | Chestnut at I2C0 address `0x27`, register `0x05`, bit 4 | + +Chestnut enables dependency bits 0 through 2 before the touch supply. Shutdown +clears only the touch bit so the display keeps its shared supplies. The T7000 +clock uses the existing divider and gate operations. Startup enables analog and +core power, settles chip select, starts the clock, performs the pre-reset +exchanges and releases reset before checking synchronous ROM readiness. + +The T7000 SPI configuration uses a word-size index shifted by 15. For 8-bit +mode 0 it programs `0x4018`, then adds PIO enable for `0x4038`. T8010 keeps its +existing controller policy. The N61 readiness check accepts the ROM words +established by Apple's driver; transfer framing must reproduce those bytes +exactly. + +N61 interrupt reads use an EB data-phase request with its checksum in the final +two transfer bytes. A 61-byte read places that checksum at offsets 59 and 60. +The earlier fixed 16-byte placement left a report queued after a physical +contact. Correcting the native request restored contact and release events +while retaining the frame header and payload checks. + +## Firmware and calibration + +`devices/n61/firmware.json` pins `N61.mtprops` from iOS 12.5.1 build 16H22 and +uses the shared `z2fw-from-mtprops@3` converter. The output path is +`lib/firmware/apple/n61-fwstart.z2fw`, with SHA256 +`51d2ef7b25c1d7e323fa703b531223312cb76f7183fbeb425f921dd6e5509294`. + +The N61 personality resolves shared Apple XML integer references to reference +divider 2 and clock-32 enable 1. Preserve XML references when reading a kernel +personality; a flattened export can lose those values. + +The model firmware carries calibration records. During boot, m1n1 forwards +`multi-touch-calibration` and `prox-calibration` from this phone's ADT through +the board's `touchscreen0` alias. The driver consumes `apple,z2-cal-blob` and +`apple,z2-prox-cal-blob`, bounded to 1024 and 128 bytes. Gen1 calibration uses +the full word count in its packet header. + +Acquire firmware through the shared CLI with an explicit `n61` profile. Keep +the resulting private mixin and receipt together. The parent N61 NFS staging +flow verifies those inputs against the installed hardware recipe before +placing root-owned firmware in the private filesystem. Public source packages +contain the recipe and converter. Firmware and unit calibration stay in +private build inputs. + +## Development on an existing boot + +The optional module in `kernel/modules/n61/of-overlay/` applies an explicitly +prepared, private device-tree overlay to a matched 4 KiB N61 kernel. The +controller-only source in `kernel/devicetree/n61/spi2-development.dts` leaves +touch reset and power unchanged. A powered overlay needs the board controls, +firmware name and connected phone's calibration described above. + +Compile development modules from the exact native patch postimages using the +running kernel's source, configuration and symbol versions. Check the phone's +boot identity and keep the USB desktop and RAM rescue available during the +test. The helper corrects the parent of its own late D2186 child when the +dynamic OF notifier cannot locate the existing I2C client. A normal boot +populates that child beneath the PMIC directly. + +Retain raw readiness replies, firmware provenance and evdev captures with the +private test record. The recorded physical test reached x=28..746 and +y=452..892 within the advertised 750 by 1334 coordinate space. It established +horizontal motion across most of the panel and complete contact lifetimes. +The later complete boots extended that coverage as recorded above. diff --git a/docs/firmware-recipes.md b/docs/firmware-recipes.md index e1edc9aa..a2e97013 100644 --- a/docs/firmware-recipes.md +++ b/docs/firmware-recipes.md @@ -7,6 +7,13 @@ and which declarative steps turn it into an installed file. It contains no code: the steps come from a fixed, versioned vocabulary that the Orchard CLI implements. +An experimental RAM-boot profile can omit `[firmware]` when its descriptor +explicitly sets `support.install = false` and `support.status = "experimental"`. +Its public RAM components build without firmware acquisition. A request to +prepare firmware for that profile fails before extraction; adding peripheral +support requires a real recipe with pinned inputs and outputs. Profiles that +permit installation continue to require a firmware declaration. + ## Structure - `device`: the Apple product (`iPad7,5`). @@ -37,8 +44,31 @@ at boot. | `im4p-payload@1` | `input` | the payload of an IM4P container | | `z2fw-from-mtprops@1` | `input`, `profile`, `packet`, `load_address`, `hbpp_version` | Z2 touch firmware with calibration records | | `z2fw-from-mtprops@2` | `input`, `profile`, `packet` | Z2 touch firmware from an mtprops packet array (iPhone 7 Plus) | +| `z2fw-from-mtprops@3` | `input`, `profile`, `packet`, `touch_maximum`, `prox_maximum`, `fll_mval`, `ref_clk_div`, `clk32_clock_enable` | Legacy Gen1 Z2 touch firmware (iPhone 6), with full word counts, bounded calibration from the ADT and the board's kernel clock policy | | `z2fw-from-mtfw@1` | `profile`, `otp_preflight` | Z2 touch firmware from an MTFW HBPP item list (iPad 7); each calibration loads Apple's default, then its record | Before it extracts anything, the CLI checks that it implements every step a recipe uses. If not, it asks for an Orchard update. New devices that reuse known steps need only a hardware update. + +## Legacy Gen1 clock policy + +N61's iOS 12.5.1 (16H22) `N61.mtprops` selects `C1F59,1`. Its Z2 data +packet stores the full payload-word count and has no `Firmware Config`. +Step `z2fw-from-mtprops@3` follows the +[Corellium Gen1 bootstrap](https://github.com/corellium/projectsandcastle/blob/03db9c6ae04141eb940f3b9f56d446f50d57fadf/hx-touchd/mtfw/mtfw.c#L169-L219), +with calibration records at the original SysCfg load positions. Provider 9 +reads `prox-calibration`; provider 0 reads `multi-touch-calibration`. +N61 bounds them to the observed 128 and 1024 bytes respectively. + +The recipe takes clock values from the same IPSW's +`com.apple.driver.AppleMultitouchSPIN61` personality, +`AppleMultitouchSPI - C1,N61`: `fll-mval=6099`, `ref-clk-div-val=2`, +`clk32-clock-enable-val=1`. Resolve the source OSUnserialize XML references: +the latter fields use `IDREF="48"` and `IDREF="8"`, whose integer definitions +are `0x2` and `0x1`. A flattened plist can lose those referenced values. +The decompressed N61 kernel's SHA-256 is +`0145bc0eda01cd55e8b996d24ffc19c25712714326f893ba5085a5016d30a4ad`. +These fields are explicit recipe parameters, including zero-valued overrides. +Physical qualification of firmware loading and input events remains separate +from converting this shared firmware file. diff --git a/docs/n61-bringup.md b/docs/n61-bringup.md new file mode 100644 index 00000000..686ac21e --- /dev/null +++ b/docs/n61-bringup.md @@ -0,0 +1,78 @@ +# iPhone 6 RAM boot + +The `n61` profile describes the iPhone 6, `iPhone7,2` / N61AP, with an Apple +A8 T7000. Its DFU identity is chip `0x7000`, board `0x06`. The kernel platform +is `apple-4k`, selecting the upstream `t7000-n61` DTB and 4 KiB ARM64 pages. + +Build the public kernel components from this repository: + +```sh +./kernel/build n61 --output out/kernel/n61 +``` + +The platform references Orchard's shared patch layers. Kernel output contains +`Image.gz`, `n61.dtb`, `kernel.release`, the module tree and a `receipt.json` +that binds the files to the source tree, patches and configuration. Use the +complete output from one build. Installation is disabled. + +The `verify_n61_kernel` workflow-dispatch option runs the same LLVM 19 toolchain +recipe on an ARM64 GitHub runner. It performs a full kernel compile with a +six-hour timeout and retains its output and package inventory as a temporary +workflow artifact. + +The touch recipe names iOS 12.5.1 build 16H22 and its N61 multitouch input. +The paired CLI converts that private input to the Gen1 firmware stream. A +loader patch forwards the handset's touch and proximity calibration without +putting calibration bytes in the public recipe. See +[N61 touch](bring-up/n61-touch.md) for the startup sequence and test record. + +In an Orchard distro workspace with the paired CLI and hardware sources, +prepare the existing minimal experiment: + +```sh +./orchard build examples/minimal-boot --device n61 \ + --pongo /path/to/Pongo-2.6.3-e98323f8.bin +./orchard run examples/minimal-boot +``` + +The Pongo image used for the recorded N61 boots is the copy bundled with +palera1n 2.4, SHA-256 +`5475c5f701423858b34e92176c966a9d4b12950f38acb8d1c347f14d5b272655`. +Enter a fresh DFU session with the iPhone 6. The shared CLI requires one target +and tracks its chip, board, ECID and USB location through Pongo. It warns if +the connected device differs from the build's profile. `--yes` proceeds after +that warning, so verify the selected device before using it. The sample +`init` exposes a USB serial shell from RAM. Record the serial banner and page +size, then inspect CPU and storage inventories. Keep internal storage inactive +during qualification. The RAM shell grants root access to its physical USB +host. + +On 2026-10-29, the reconciled kernel and private NFS desktop reached the Arch +and Omarchy desktop on two tethered boots. The owner confirmed touch on both; +the first captured y=26..1322 on the 0..1333 axis and entered `n61` through +the on-screen keyboard. Systemd reported zero failed units on both boots. +The tested producer revisions were hardware `853659d` and CLI `8fed88e`. +The RAM payload SHA-256 was +`b2d49d60b3771947e11efcee4d1d7f91d126a333d7373bae02b4d1f341f6ec78`; +the desktop archive SHA-256 was +`a6e0626dc02d8aff535d6c0fa8f14cb54954700bfda5f267528f07cec3d6fa57`. +Those results apply to that combination. Bluetooth, Wi-Fi and internal storage +remain unqualified. Record later physical results with their exact inputs. + +## Internal storage gate + +The retained N61 ADT, checked against the saved device identity, describes +`/arm-io/ans` as `iop,s5l8960x`. Hoolock's generic storage selector uses its +ANS1 backend for that compatible. The backend has not been exercised on this +phone. Its current shutdown path frees the command buffer twice, and several +initialization failures leave the ANS power and allocation cleanup incomplete. +The read command also needs explicit bounds for its narrower LBA field and +verified device capacity. Repair and test these paths before any read or +shutdown experiment. + +Future installation follows Orchard's [boot contract](boot-contract.md): a +FAT32 XBOOTLDR partition holds BLS entries, and roots use PARTUUIDs. The CLI and +installer do not yet implement that contract. One inspected N61 on iOS 12.5.1 +had a single APFS container with separate mounted system, data and baseband +volumes. Inspect each device's layout before changing partitions. An iOS 12 +resize backend also requires separate qualification. diff --git a/kernel/README.md b/kernel/README.md index c58e10b2..b69cdb97 100644 --- a/kernel/README.md +++ b/kernel/README.md @@ -21,6 +21,11 @@ A second build of the same tree waits for the first. `--output` must name a new directory; it receives `Image.gz`, `.dtb`, `dtbs/`, `kernel.release` and `lib/modules/`. +For N61, `receipt.json` binds the output files to the platform, source trees, +hardware recipe, patches and configuration. The device-specific DTB must +match its copy under `dtbs/`. Consumers can verify the image, selected DTB and +complete module tree against this receipt. + The kernel has no device-specific NVMe fences. Protecting Apple's data is the installer's job: the recovery runtime keeps every NVMe disk and partition read-only and opens only its verified target while writing diff --git a/kernel/build b/kernel/build index 0934806e..d41b415b 100755 --- a/kernel/build +++ b/kernel/build @@ -138,6 +138,9 @@ else fi fi mkdir -p "$build_output" +recipe_snapshot="$work/recipe-snapshot.json" +python3 "$here/write-receipt.py" --capture --device "$device" --platform "$platform" \ + --patch-list "$applied" --recipe-file "$recipe_snapshot" cp "$config" "$build_output/.config" echo 'Preparing kernel build container...' python3 -m orchard_hardware.buildenv build --file "$here/Containerfile" \ @@ -176,4 +179,8 @@ fi cp "$build_output/arch/arm64/boot/Image.gz" "$dest/Image.gz" cp -R "$stage/module-root/lib" "$dest/lib" cp "$build_output/include/config/kernel.release" "$dest/kernel.release" +python3 "$here/write-receipt.py" --directory "$dest" --device "$device" \ + --platform "$platform" --dtb "$device_dtb" --source-commit "$commit" \ + --source-tree "$(snapshot)" --prepared-tree "$(cat "$prepared")" \ + --patch-list "$applied" --config "$config" --recipe-file "$recipe_snapshot" echo "Kernel output: $dest" diff --git a/kernel/build-modules b/kernel/build-modules index 05f96787..b09b5040 100755 --- a/kernel/build-modules +++ b/kernel/build-modules @@ -12,11 +12,27 @@ while (($#)); do *) echo "Unknown argument: $1" >&2; exit 2;; esac done -if [[ ! -d "$source" || ! -f "$build/Module.symvers" || ! -d "$modules" || +if [[ ! -d "$source" || ! -f "$build/Module.symvers" || + ! -f "$build/include/config/kernel.release" || ! -f "$build/.config" || ! -d "$modules" || -z "$output" || -z "$localversion" || -e "$output" ]]; then echo 'Usage: ./kernel/build-modules --source KERNEL_SOURCE --build KERNEL_OUTPUT --modules MODULE_SOURCES --localversion SUFFIX --output DIRECTORY' >&2 exit 2 fi +release=$(cat "$build/include/config/kernel.release") +if [[ -z "$release" || "$release" != *"$localversion" ]]; then + echo 'Kernel module SDK release does not match the requested platform' >&2 + exit 1 +fi +case "$localversion" in + -orchard-apple-4k) + grep -Fxq 'CONFIG_ARM64_4K_PAGES=y' "$build/.config" || { + echo 'Kernel module SDK page size does not match apple-4k' >&2; exit 1; + } ;; + -orchard-apple-16k) + grep -Fxq 'CONFIG_ARM64_16K_PAGES=y' "$build/.config" || { + echo 'Kernel module SDK page size does not match apple-16k' >&2; exit 1; + } ;; +esac source=$(cd "$source" && pwd) build=$(cd "$build" && pwd) mkdir -p "$output" diff --git a/kernel/configs/apple-4k/README.md b/kernel/configs/apple-4k/README.md index 8bbb5f84..9c93e24e 100644 --- a/kernel/configs/apple-4k/README.md +++ b/kernel/configs/apple-4k/README.md @@ -1,5 +1,8 @@ # Kernel configuration for A7, A8 and A8X +The following paragraph describes the initial 4 KiB baseline before the N61 +desktop settings listed below. + `config` is the `apple-16k` configuration with 4 KiB pages instead of 16 KiB. Apple's A7 (S5L8960X), A8 (T7000) and A8X (T7001) CPUs have no 16 KiB translation granule, so their devices cannot run the `apple-16k` kernel. The @@ -7,6 +10,9 @@ patch layers are the same (`../../series/apple-4k`); only the page size and what Kconfig derives from it differ (page shift, contiguous-PTE and max-order sizes, mmap randomization bits, THP swap). +For the N61 desktop, the `apple-4k` config also enables the NFS client, FUSE +and cgroup settings. + ## How the file is generated From `../apple-16k/config`, with `CONFIG_ARM64_4K_PAGES=y` and diff --git a/kernel/configs/apple-4k/config b/kernel/configs/apple-4k/config index 0bbcfdbe..bc691659 100644 --- a/kernel/configs/apple-4k/config +++ b/kernel/configs/apple-4k/config @@ -190,22 +190,25 @@ CONFIG_BLK_CGROUP=y CONFIG_CGROUP_WRITEBACK=y CONFIG_CGROUP_SCHED=y CONFIG_GROUP_SCHED_WEIGHT=y +CONFIG_GROUP_SCHED_BANDWIDTH=y CONFIG_FAIR_GROUP_SCHED=y -# CONFIG_CFS_BANDWIDTH is not set +CONFIG_CFS_BANDWIDTH=y # CONFIG_RT_GROUP_SCHED is not set CONFIG_SCHED_MM_CID=y CONFIG_CGROUP_PIDS=y # CONFIG_CGROUP_RDMA is not set # CONFIG_CGROUP_DMEM is not set -# CONFIG_CGROUP_FREEZER is not set +CONFIG_CGROUP_FREEZER=y CONFIG_CGROUP_HUGETLB=y -# CONFIG_CPUSETS is not set +CONFIG_CPUSETS=y +# CONFIG_CPUSETS_V1 is not set CONFIG_CGROUP_DEVICE=y CONFIG_CGROUP_CPUACCT=y CONFIG_CGROUP_PERF=y -# CONFIG_CGROUP_BPF is not set +CONFIG_CGROUP_BPF=y # CONFIG_CGROUP_MISC is not set # CONFIG_CGROUP_DEBUG is not set +CONFIG_SOCK_CGROUP_DATA=y CONFIG_NAMESPACES=y CONFIG_UTS_NS=y CONFIG_TIME_NS=y @@ -1052,6 +1055,7 @@ CONFIG_AF_UNIX_OOB=y # CONFIG_NET_KEY is not set # CONFIG_DIBS is not set # CONFIG_XDP_SOCKETS is not set +CONFIG_NET_HANDSHAKE=y CONFIG_INET=y CONFIG_IP_MULTICAST=y # CONFIG_IP_ADVANCED_ROUTER is not set @@ -1355,6 +1359,7 @@ CONFIG_XPS=y # CONFIG_CGROUP_NET_CLASSID is not set CONFIG_NET_RX_BUSY_POLL=y CONFIG_BQL=y +# CONFIG_BPF_STREAM_PARSER is not set CONFIG_NET_FLOW_LIMIT=y # @@ -2613,6 +2618,7 @@ CONFIG_GPIO_MACSMC=y # # GPIO Debugging utilities # +# CONFIG_GPIO_SLOPPY_LOGIC_ANALYZER is not set # CONFIG_GPIO_VIRTUSER is not set # end of GPIO Debugging utilities @@ -4811,7 +4817,6 @@ CONFIG_USB_F_NCM=y CONFIG_USB_F_ECM=y CONFIG_USB_F_EEM=y CONFIG_USB_F_SUBSET=y -CONFIG_USB_F_MASS_STORAGE=y CONFIG_USB_F_FS=y CONFIG_USB_CONFIGFS=y CONFIG_USB_CONFIGFS_SERIAL=y @@ -4852,9 +4857,7 @@ CONFIG_USB_FUNCTIONFS_ETH=y # CONFIG_USB_G_PRINTER is not set # CONFIG_USB_CDC_COMPOSITE is not set # CONFIG_USB_G_ACM_MS is not set -CONFIG_USB_G_MULTI=y -# CONFIG_USB_G_MULTI_RNDIS is not set -CONFIG_USB_G_MULTI_CDC=y +# CONFIG_USB_G_MULTI is not set # CONFIG_USB_G_HID is not set # CONFIG_USB_G_DBGP is not set # CONFIG_USB_G_WEBCAM is not set @@ -6095,7 +6098,7 @@ CONFIG_QFMT_V1=y CONFIG_QFMT_V2=y CONFIG_QUOTACTL=y CONFIG_AUTOFS_FS=y -CONFIG_FUSE_FS=m +CONFIG_FUSE_FS=y # CONFIG_CUSE is not set # CONFIG_VIRTIO_FS is not set CONFIG_FUSE_PASSTHROUGH=y @@ -6202,7 +6205,39 @@ CONFIG_PSTORE_RAM=y # CONFIG_PSTORE_BLK is not set # CONFIG_UFS_FS is not set # CONFIG_EROFS_FS is not set -# CONFIG_NETWORK_FILESYSTEMS is not set +CONFIG_NETWORK_FILESYSTEMS=y +CONFIG_NFS_FS=y +# CONFIG_NFS_V2 is not set +CONFIG_NFS_V3=y +# CONFIG_NFS_V3_ACL is not set +CONFIG_NFS_V4=y +# CONFIG_NFS_SWAP is not set +CONFIG_NFS_V4_0=y +# CONFIG_NFS_V4_2 is not set +CONFIG_PNFS_FILE_LAYOUT=y +CONFIG_PNFS_BLOCK=y +CONFIG_PNFS_FLEXFILE_LAYOUT=y +CONFIG_NFS_V4_1_IMPLEMENTATION_ID_DOMAIN="kernel.org" +# CONFIG_NFS_V4_1_MIGRATION is not set +# CONFIG_NFS_FSCACHE is not set +# CONFIG_NFS_USE_LEGACY_DNS is not set +CONFIG_NFS_USE_KERNEL_DNS=y +CONFIG_NFS_DISABLE_UDP_SUPPORT=y +# CONFIG_NFSD is not set +CONFIG_GRACE_PERIOD=y +CONFIG_LOCKD=y +CONFIG_LOCKD_V4=y +CONFIG_NFS_COMMON=y +CONFIG_SUNRPC=y +CONFIG_SUNRPC_GSS=y +CONFIG_SUNRPC_BACKCHANNEL=y +CONFIG_RPCSEC_GSS_KRB5=y +# CONFIG_SUNRPC_DEBUG is not set +# CONFIG_CEPH_FS is not set +# CONFIG_CIFS is not set +# CONFIG_SMB_SERVER is not set +# CONFIG_CODA_FS is not set +# CONFIG_AFS_FS is not set CONFIG_NLS=y CONFIG_NLS_DEFAULT="iso8859-1" CONFIG_NLS_CODEPAGE_437=y @@ -6369,7 +6404,7 @@ CONFIG_CRYPTO_NULL=y # CONFIG_CRYPTO_PCRYPT is not set CONFIG_CRYPTO_CRYPTD=y CONFIG_CRYPTO_AUTHENC=y -# CONFIG_CRYPTO_KRB5ENC is not set +CONFIG_CRYPTO_KRB5ENC=y # CONFIG_CRYPTO_BENCHMARK is not set # end of Crypto core or helper @@ -6392,7 +6427,7 @@ CONFIG_CRYPTO_AES=y # CONFIG_CRYPTO_ANUBIS is not set # CONFIG_CRYPTO_ARIA is not set # CONFIG_CRYPTO_BLOWFISH is not set -# CONFIG_CRYPTO_CAMELLIA is not set +CONFIG_CRYPTO_CAMELLIA=y # CONFIG_CRYPTO_CAST5 is not set # CONFIG_CRYPTO_CAST6 is not set # CONFIG_CRYPTO_DES is not set @@ -6527,7 +6562,8 @@ CONFIG_SYSTEM_TRUSTED_KEYS="" # CONFIG_SYSTEM_BLACKLIST_KEYRING is not set # end of Certificates for signature checking -# CONFIG_CRYPTO_KRB5 is not set +CONFIG_CRYPTO_KRB5=y +# CONFIG_CRYPTO_KRB5_SELFTESTS is not set CONFIG_BINARY_PRINTF=y # @@ -6686,6 +6722,7 @@ CONFIG_SBITMAP=y CONFIG_GENERIC_IOREMAP=y CONFIG_GENERIC_LIB_DEVMEM_IS_ALLOWED=y +CONFIG_UNION_FIND=y # # Kernel hacking diff --git a/kernel/devicetree/n61/spi2-development.dts b/kernel/devicetree/n61/spi2-development.dts new file mode 100644 index 00000000..d72f6ce6 --- /dev/null +++ b/kernel/devicetree/n61/spi2-development.dts @@ -0,0 +1,50 @@ +// SPDX-License-Identifier: GPL-2.0-only OR MIT +/* N61 SPI2 PIO diagnostic. Reset, touch power and touch IRQ stay unchanged. */ +/dts-v1/; +/plugin/; + +/ { + compatible = "apple,n61"; + + fragment@0 { + target-path = "/"; + __overlay__ { + n61_spi2_clk: n61-spi2-clock { + compatible = "fixed-clock"; + #clock-cells = <0>; + clock-frequency = <60000000>; + }; + }; + }; + + fragment@1 { + target-path = "/soc"; + __overlay__ { + #address-cells = <2>; + #size-cells = <2>; + + spi@20a088000 { + /* The qualified running kernel knows this legacy backend. */ + compatible = "apple,t8010-spi", "apple,spi"; + reg = <0x2 0x0a088000 0 0x4000>; + interrupt-parent = <&aic>; + interrupts = <0 154 4>; + clocks = <&n61_spi2_clk>; + power-domains = <&ps_spi2>; + #address-cells = <1>; + #size-cells = <0>; + cs-gpios = <&pinctrl 185 1>; + status = "okay"; + + touchscreen@0 { + compatible = "apple,n61-touchscreen"; + reg = <0>; + spi-max-frequency = <500000>; + spi-cs-setup-delay-ns = <1000000>; + spi-cs-hold-delay-ns = <1000000>; + spi-cs-inactive-delay-ns = <1000000>; + }; + }; + }; + }; +}; diff --git a/kernel/modules/n61/of-overlay/Makefile b/kernel/modules/n61/of-overlay/Makefile new file mode 100644 index 00000000..05bcf8ae --- /dev/null +++ b/kernel/modules/n61/of-overlay/Makefile @@ -0,0 +1 @@ +obj-m += orchard_n61_overlay.o diff --git a/kernel/modules/n61/of-overlay/README.md b/kernel/modules/n61/of-overlay/README.md new file mode 100644 index 00000000..d46582b3 --- /dev/null +++ b/kernel/modules/n61/of-overlay/README.md @@ -0,0 +1,26 @@ +# N61 development overlay + +This explicitly loaded module lets a running N61 4 KiB kernel test its touch +device tree while preserving the USB desktop and RAM rescue session. It refuses +other boards, a different page size, and an existing SPI2 node. It has no autoload +alias. Native board support belongs in the N61 device tree. + +Build with the matching kernel source and a private copy of its module SDK, +using the shared `kernel/build-modules` recipe. Prepare one private, reviewed +DTBO at `/usr/lib/firmware/apple/n61-touch-development.dtbo`; it may contain +the connected phone's calibration. Check its digest and the phone's board and +boot identities before loading the module. Keep that file outside public +images and source control. + +The module applies one bounded FDT overlay through the kernel's normal overlay +API. Unloading removes its own overlay. A failed application or removal needs +inspection before further hardware changes. Preserve the existing boot image +and source receipts throughout development. + +For a powered test, the helper checks the newly added D2186 touch supply child +and attaches it beneath the existing I2C PMIC before retrying its probe. The +kernel's dynamic OF platform notifier otherwise gives this late child a +platform parent without the PMIC regmap. The helper accepts only its own exact +N61 child path and compatible, and refuses to move an already bound child. +It leaves existing PMIC children in place. Normal native DT population creates +the touch supply beneath the I2C PMIC during boot. diff --git a/kernel/modules/n61/of-overlay/orchard_n61_overlay.c b/kernel/modules/n61/of-overlay/orchard_n61_overlay.c new file mode 100644 index 00000000..abacc82f --- /dev/null +++ b/kernel/modules/n61/of-overlay/orchard_n61_overlay.c @@ -0,0 +1,149 @@ +// SPDX-License-Identifier: GPL-2.0-only +/* Explicit N61 development overlay; board support belongs in the native DTB. */ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define N61_OVERLAY "apple/n61-touch-development.dtbo" +#define N61_SPI_PATH "/soc/spi@20a088000" +#define N61_CORE_PATH "/soc/i2c@20a110000/pmic@74/regulator-touch-core" +#define N61_OVERLAY_MAX_SIZE (64 * 1024) + +static const struct firmware *n61_overlay; +static int n61_overlay_id; + +/* A live OF child notification cannot locate an I2C client as its parent. */ +static int n61_overlay_attach_core(void) +{ + struct device_node *node, *parent; + struct platform_device *child; + struct i2c_client *pmic; + int error = 0; + + node = of_find_node_by_path(N61_CORE_PATH); + if (!node) + return 0; + if (!of_device_is_compatible(node, "apple,d2186-touch-core-regulator")) { + error = -EINVAL; + goto put_node; + } + parent = of_get_parent(node); + pmic = of_find_i2c_device_by_node(parent); + of_node_put(parent); + if (!pmic) { + error = -ENODEV; + goto put_node; + } + if (!dev_get_regmap(&pmic->dev, NULL)) { + error = -ENODEV; + goto put_pmic; + } + child = of_find_device_by_node(node); + if (!child) { + error = -ENODEV; + goto put_pmic; + } + device_lock(&child->dev); + if (child->dev.parent != &pmic->dev) { + if (child->dev.driver) + error = -EBUSY; + else + error = device_move(&child->dev, &pmic->dev, + DPM_ORDER_DEV_AFTER_PARENT); + } + device_unlock(&child->dev); + if (!error) { + error = device_attach(&child->dev); + if (error == 1) + error = 0; + else if (!error) + error = -ENODEV; + } + put_device(&child->dev); +put_pmic: + put_device(&pmic->dev); +put_node: + of_node_put(node); + return error; +} + +static int __init orchard_n61_overlay_init(void) +{ + struct device_node *existing; + int error; + + if (PAGE_SIZE != 4096 || !of_machine_is_compatible("apple,n61")) + return -ENODEV; + + existing = of_find_node_by_path(N61_SPI_PATH); + if (existing) { + of_node_put(existing); + return -EEXIST; + } + existing = of_find_node_by_path(N61_CORE_PATH); + if (existing) { + of_node_put(existing); + return -EEXIST; + } + + error = request_firmware_direct(&n61_overlay, N61_OVERLAY, NULL); + if (error) + return error; + if (n61_overlay->size < 40 || + n61_overlay->size > N61_OVERLAY_MAX_SIZE || + get_unaligned_be32(n61_overlay->data) != 0xd00dfeed || + get_unaligned_be32(n61_overlay->data + 4) != n61_overlay->size) { + error = -EINVAL; + goto release; + } + + error = of_overlay_fdt_apply(n61_overlay->data, n61_overlay->size, + &n61_overlay_id, NULL); + if (!error) + error = n61_overlay_attach_core(); + if (error) { + if (n61_overlay_id) { + int cleanup = of_overlay_remove(&n61_overlay_id); + + if (cleanup) { + pr_err("N61 overlay cleanup failed: %d; inspect before retry\n", + cleanup); + /* Retain backing bytes if a partial overlay remains. */ + return error; + } + } + goto release; + } + + pr_info("N61 development touch overlay applied: %d\n", n61_overlay_id); + return 0; + +release: + release_firmware(n61_overlay); + n61_overlay = NULL; + return error; +} + +static void __exit orchard_n61_overlay_exit(void) +{ + int error = of_overlay_remove(&n61_overlay_id); + + if (error) { + pr_err("N61 overlay removal failed: %d; inspect before retry\n", error); + return; + } + release_firmware(n61_overlay); +} + +module_init(orchard_n61_overlay_init); +module_exit(orchard_n61_overlay_exit); +MODULE_LICENSE("GPL"); +MODULE_DESCRIPTION("Explicit N61 touch development overlay for a matched 4K kernel"); diff --git a/kernel/patches/devices/t7000/0001-spi-apple-add-T7000-legacy-controller.patch b/kernel/patches/devices/t7000/0001-spi-apple-add-T7000-legacy-controller.patch new file mode 100644 index 00000000..1556a215 --- /dev/null +++ b/kernel/patches/devices/t7000/0001-spi-apple-add-T7000-legacy-controller.patch @@ -0,0 +1,136 @@ +Subject: [PATCH] spi: apple: add the T7000 legacy FIFO configuration + +T7000 spi-version1 shares the legacy register/FIFO layout. Its config +uses base0x4000, independent CPHA/CPOL bits1/2 and a word-size index shifted +by15. N61 mode0/8-bit programs0x4018, then0x4038 with PIO enabled. + +Keep the T8010 descriptor path behind its hardware flag. T7000 uses its +own mode/word config for every transfer size and does not request DMA. +This preserves the current T8010 config function and transport behavior. + +Evidence: exact N61 16H22 kernel AppleSamsungSPI config at +0xfffffff007e8ba0c-0xfffffff007e8ba98; version1 constants at +0xfffffff00704c420 are [15,0x4000,0x2000,16]. N61 tuple byte10=1 leaves +endian bit0x2000 clear. Native startup verifies unshifted ROM words before +any firmware upload. + +--- +diff --git a/Documentation/devicetree/bindings/spi/apple,spi.yaml b/Documentation/devicetree/bindings/spi/apple,spi.yaml +--- a/Documentation/devicetree/bindings/spi/apple,spi.yaml ++++ b/Documentation/devicetree/bindings/spi/apple,spi.yaml +@@ -21,6 +21,7 @@ + - const: apple,t8103-spi + - items: + - enum: ++ - apple,t7000-spi + - apple,t8010-spi + - apple,t8103-spi + - apple,t8112-spi +diff --git a/drivers/spi/spi-apple.c b/drivers/spi/spi-apple.c +--- a/drivers/spi/spi-apple.c ++++ b/drivers/spi/spi-apple.c +@@ -44,6 +44,7 @@ + #define APPLE_SPI_CFG_FIFO_THRESH_8B 0 + + #define APPLE_SPI_CFG_T8010_BASE 0x0010401e ++#define APPLE_SPI_CFG_T7000_BASE 0x00004018 + #define APPLE_SPI_CFG_T8010_PIOEN BIT(5) + #define APPLE_SPI_CFG_T8010_16BIT BIT(15) + +@@ -118,6 +119,7 @@ + + struct apple_spi_hw { + bool legacy_status; ++ bool legacy_descriptor; + unsigned int min_clk_div; + u32 default_word_delay; + }; +@@ -179,7 +181,8 @@ + reg_write(spi, APPLE_SPI_IF_XFER, ~0); + reg_write(spi, APPLE_SPI_IF_FIFO, ~0); + reg_write(spi, APPLE_SPI_WORD_DELAY, spi->word_delay); +- reg_write(spi, APPLE_SPI_CFG, APPLE_SPI_CFG_T8010_BASE); ++ reg_write(spi, APPLE_SPI_CFG, spi->hw->legacy_descriptor ? ++ APPLE_SPI_CFG_T8010_BASE : APPLE_SPI_CFG_T7000_BASE); + return; + } + +@@ -326,6 +329,21 @@ + return ret; + } + ++/* SPI version 1 uses a word-size index shifted by 15, separate from mode. */ ++static u32 apple_spi_t7000_config(struct spi_device *device, ++ unsigned int bits_per_word) ++{ ++ u32 cfg = APPLE_SPI_CFG_T7000_BASE; ++ ++ if (device->mode & SPI_CPHA) ++ cfg |= APPLE_SPI_CFG_CPHA; ++ if (device->mode & SPI_CPOL) ++ cfg |= APPLE_SPI_CFG_CPOL; ++ if (bits_per_word == 16) ++ cfg |= BIT(15); ++ return cfg; ++} ++ + static u32 apple_spi_t8010_config(struct spi_device *device, + unsigned int bits_per_word, + bool use_descriptor) +@@ -663,15 +681,23 @@ + return ret; + speed_hz = clk_get_rate(spi->clk) / div; + +- use_descriptor = apple_spi_t8010_uses_descriptor(t, words); ++ use_descriptor = spi->hw->legacy_descriptor && ++ apple_spi_t8010_uses_descriptor(t, words); + reg_write(spi, APPLE_SPI_CLKDIV, div); + reg_write(spi, APPLE_SPI_WORD_DELAY, + apple_spi_t8010_word_delay(spi, device, use_descriptor)); + apple_spi_t8010_prepare_xfer(spi, use_descriptor); + +- cfg = apple_spi_t8010_config(device, bits_per_word, use_descriptor); ++ cfg = spi->hw->legacy_descriptor ? ++ apple_spi_t8010_config(device, bits_per_word, use_descriptor) : ++ apple_spi_t7000_config(device, bits_per_word); + if (use_descriptor) + cfg &= ~(APPLE_SPI_T8010_DESC_CFG_DEFAULT | BIT(0)); ++ if (!spi->hw->legacy_descriptor) ++ dev_dbg(&device->dev, ++ "T7000 PIO cfg=%#x div=%u word-delay=%u words=%u bpw=%u\n", ++ (u32)(cfg | APPLE_SPI_CFG_T8010_PIOEN), div, spi->word_delay, ++ words, bits_per_word); + + reg_write(spi, APPLE_SPI_RXCNT, words); + reg_write(spi, APPLE_SPI_TXCNT, words); +@@ -984,7 +1010,7 @@ + ctlr->flags |= SPI_CONTROLLER_GPIO_SS; + ctlr->auto_runtime_pm = true; + +- if (spi->hw->legacy_status) { ++ if (spi->hw->legacy_descriptor) { + ctlr->dma_tx = dma_request_chan(&pdev->dev, "tx"); + if (IS_ERR(ctlr->dma_tx)) { + ret = PTR_ERR(ctlr->dma_tx); +@@ -1019,7 +1045,13 @@ + + static const struct apple_spi_hw apple_spi_hw_t8010 = { + .legacy_status = true, ++ .legacy_descriptor = true, + .min_clk_div = 2, ++}; ++ ++static const struct apple_spi_hw apple_spi_hw_t7000 = { ++ .legacy_status = true, ++ .min_clk_div = 1, + }; + + static const struct apple_spi_hw apple_spi_hw_default = { +@@ -1027,6 +1059,7 @@ + }; + + static const struct of_device_id apple_spi_of_match[] = { ++ { .compatible = "apple,t7000-spi", .data = &apple_spi_hw_t7000, }, + { .compatible = "apple,t8010-spi", .data = &apple_spi_hw_t8010, }, + { .compatible = "apple,t8103-spi", .data = &apple_spi_hw_default, }, + { .compatible = "apple,spi", .data = &apple_spi_hw_default, }, diff --git a/kernel/patches/devices/t7000/0002-arm64-dts-apple-describe-N61-SPI2.patch b/kernel/patches/devices/t7000/0002-arm64-dts-apple-describe-N61-SPI2.patch new file mode 100644 index 00000000..fd773155 --- /dev/null +++ b/kernel/patches/devices/t7000/0002-arm64-dts-apple-describe-N61-SPI2.patch @@ -0,0 +1,66 @@ +Subject: [PATCH] arm64: dts: apple: describe the N61 SPI2 bus + +Describe the T7000 SPI2 register range, interrupt, PMGR domain and +60 MHz input clock, then enable its GPIO chip select on N61. The +controller remains disabled for other T7000 boards. Its data/clock pins +retain the bootloader configuration until their mux is independently +verified. No touchscreen child, touch clock or supply is instantiated. + +These values agree between the N61 ADT and the public dump at: +https://github.com/SoMainline/adt_collection/blob/768d800c46140c9ba39a372d097fa49f7197a66d/a8/n61.adt + +--- +diff --git a/arch/arm64/boot/dts/apple/t7000.dtsi b/arch/arm64/boot/dts/apple/t7000.dtsi +--- a/arch/arm64/boot/dts/apple/t7000.dtsi ++++ b/arch/arm64/boot/dts/apple/t7000.dtsi +@@ -24,6 +24,14 @@ + #clock-cells = <0>; + clock-frequency = <24000000>; + clock-output-names = "clkref"; ++ }; ++ ++ spi2_clk: clock-spi2 { ++ compatible = "fixed-clock"; ++ #clock-cells = <0>; ++ /* ADT clock ID 0x12e: clock-frequencies[46]. */ ++ clock-frequency = <60000000>; ++ clock-output-names = "spi2"; + }; + + cpus { +@@ -201,6 +209,18 @@ + status = "disabled"; + }; + ++ spi2: spi@20a088000 { ++ compatible = "apple,t7000-spi", "apple,spi"; ++ reg = <0x2 0x0a088000 0x0 0x4000>; ++ interrupt-parent = <&aic>; ++ interrupts = ; ++ clocks = <&spi2_clk>; ++ power-domains = <&ps_spi2>; ++ #address-cells = <1>; ++ #size-cells = <0>; ++ status = "disabled"; ++ }; ++ + bus@20c000000 { + compatible = "apple,t7000-usb-complex", "apple,s5l8960x-usb-complex", "simple-pm-bus"; + #address-cells = <2>; +diff --git a/arch/arm64/boot/dts/apple/t7000-n61.dts b/arch/arm64/boot/dts/apple/t7000-n61.dts +--- a/arch/arm64/boot/dts/apple/t7000-n61.dts ++++ b/arch/arm64/boot/dts/apple/t7000-n61.dts +@@ -11,4 +11,13 @@ + / { + compatible = "apple,n61", "apple,t7000", "apple,arm-platform"; + model = "Apple iPhone 6"; ++ ++ aliases { ++ spi2 = &spi2; ++ }; + }; ++ ++&spi2 { ++ cs-gpios = <&pinctrl 185 GPIO_ACTIVE_LOW>; ++ status = "okay"; ++}; diff --git a/kernel/patches/devices/t7000/0003-input-apple-z2-add-N61-Gen1-support.patch b/kernel/patches/devices/t7000/0003-input-apple-z2-add-N61-Gen1-support.patch new file mode 100644 index 00000000..4d149106 --- /dev/null +++ b/kernel/patches/devices/t7000/0003-input-apple-z2-add-N61-Gen1-support.patch @@ -0,0 +1,1007 @@ +Subject: [PATCH] input: apple-z2: add the N61 legacy Gen1 path + +N61.mtprops C1F59,1 carries a legacy Z2 data-form Constructed Firmware +packet. Preserve its 8-bit wire order, split long writes under one chip +select and append proximity calibration provider 9 without renumbering +existing providers. The N61 header encodes the full payload word count, +before HBPP 0x104. Existing board boot sequences keep their current paths. The shared J71b +drain and IRQ policy stays separate from N61 framing and timing. + +Gen1 long reads repeat the request and parse direct signed-coordinate +reports. Read the D9 sensor bounds before enabling contacts. The N61 +runtime setup follows Corellium's Gen1 wake/D9/9d/bf/af sequence. + +N61 owns explicit analog/core supply references and the touch clock during +startup. A separate opt-in preserved-power path leaves external resources +unchanged and must first be qualified by a ROM response. + +Protocol reference: +https://github.com/corellium/projectsandcastle/tree/03db9c6ae04141eb940f3b9f56d446f50d57fadf/hx-touchd + +--- +diff --git a/Documentation/devicetree/bindings/input/touchscreen/apple,z2-multitouch.yaml b/Documentation/devicetree/bindings/input/touchscreen/apple,z2-multitouch.yaml +index 78c212785ff9652c0ecc9659f31d2336df314e47..8dcf64c1459c3a1583891895240300aeba2b77a4 100644 +--- a/Documentation/devicetree/bindings/input/touchscreen/apple,z2-multitouch.yaml ++++ b/Documentation/devicetree/bindings/input/touchscreen/apple,z2-multitouch.yaml +@@ -21,6 +21,7 @@ properties: + - apple,j172-touchscreen + - apple,d11-touchscreen + - apple,d111-touchscreen ++ - apple,n61-touchscreen + + interrupts: + maxItems: 1 +@@ -53,6 +54,22 @@ properties: + clocks: + maxItems: 1 + ++ ana-supply: ++ description: N61 analog touchscreen supply from the Chestnut touch LDO ++ ++ apple,boot-power-preserved: ++ type: boolean ++ description: ++ The boot environment keeps the N61 touch supplies and 32.768 kHz ++ clock active. Set only after a ROM response qualifies that state; ++ the driver owns reset and leaves those external resources unchanged. ++ This excludes explicit supply and clock ownership by the driver. ++ ++ apple,z2-prox-cal-blob: ++ $ref: /schemas/types.yaml#/definitions/uint8-array ++ maxItems: 128 ++ description: Optional N61 proximity calibration supplied by the bootloader ++ + apple,z2-bpw16-min-len: + $ref: /schemas/types.yaml#/definitions/uint32 + description: +@@ -72,7 +89,9 @@ properties: + $ref: /schemas/types.yaml#/definitions/uint8-array + maxItems: 4096 + description: +- Calibration blob supplied by the bootloader ++ Calibration blob supplied by the bootloader. N61 requires this ++ per-unit data at driver probe; it can be absent from a static DTB ++ before the bootloader adds it. + + apple,z2-orb-gap-cal-blob: + $ref: /schemas/types.yaml#/definitions/uint8-array +@@ -184,6 +203,37 @@ allOf: + - spi-cpol + - apple,z2-no-init-ack + ++ - if: ++ properties: ++ compatible: ++ const: apple,n61-touchscreen ++ then: ++ properties: ++ apple,z2-cal-blob: ++ minItems: 1 ++ maxItems: 1024 ++ power-ana-gpios: false ++ power-ldo-gpios: false ++ display-sync-gpios: false ++ display-sync1-gpios: false ++ spi-cpha: false ++ spi-cpol: false ++ oneOf: ++ - properties: ++ apple,boot-power-preserved: ++ const: true ++ ana-supply: false ++ core-supply: false ++ clocks: false ++ required: ++ - apple,boot-power-preserved ++ - properties: ++ apple,boot-power-preserved: false ++ required: ++ - ana-supply ++ - core-supply ++ - clocks ++ + unevaluatedProperties: false + + examples: +diff --git a/drivers/input/touchscreen/apple_z2.c b/drivers/input/touchscreen/apple_z2.c +index b1e1a457989378ae6217d9caf6ad464730570d7d..6bf895084ae1d4290796624c23d6ec435c00df9d 100644 +--- a/drivers/input/touchscreen/apple_z2.c ++++ b/drivers/input/touchscreen/apple_z2.c +@@ -25,6 +25,8 @@ + #include + #include + ++#include "apple_z2_n61.h" ++ + #define APPLE_Z2_NUM_FINGERS_OFFSET 16 + #define APPLE_Z2_FINGERS_OFFSET 24 + #define APPLE_Z2_TOUCH_STARTED 3 +@@ -75,6 +77,7 @@ + #define PRESSURE_CAL_PROP_NAME "apple,z2-pressure-cal-blob" + #define FIREFLY_P100_CAL_PROP_NAME "apple,z2-firefly-p100-cal-blob" + #define EXT_CAL_PROP_NAME "apple,z2-ext-cal-blob" ++#define PROX_CAL_PROP_NAME "apple,z2-prox-cal-blob" + + #define APPLE_Z2_FW_CONFIG_WORDS 11 + #define APPLE_Z2_FW_CONFIG_MIN_DMA BIT(0) +@@ -103,6 +106,7 @@ enum apple_z2_variant { + APPLE_Z2_VARIANT_J71B, + APPLE_Z2_VARIANT_D11, + APPLE_Z2_VARIANT_D111, ++ APPLE_Z2_VARIANT_N61, + }; + + /** +@@ -152,6 +156,7 @@ struct apple_z2 { + bool j172_sync_pinctrl_active; + bool hv_enabled; + bool core_enabled; ++ bool n61_boot_power_preserved; + enum apple_z2_protocol_state protocol_state; + unsigned int bpw16_min_len; + unsigned int dma_min_len; +@@ -181,12 +186,22 @@ static bool apple_z2_is_j71b(const struct apple_z2 *z2) + return z2->variant == APPLE_Z2_VARIANT_J71B; + } + +-/* The generation-1 byte-stream runtime protocol of the iPad 6. */ ++static bool apple_z2_is_n61(const struct apple_z2 *z2) ++{ ++ return z2->variant == APPLE_Z2_VARIANT_N61; ++} ++ ++/* The J71b drain and IRQ policy remains independent of N61 data framing. */ + static bool apple_z2_is_gen1(const struct apple_z2 *z2) + { + return apple_z2_is_j71b(z2); + } + ++static bool apple_z2_uses_gen1_data(const struct apple_z2 *z2) ++{ ++ return apple_z2_is_gen1(z2) || apple_z2_is_n61(z2); ++} ++ + static bool apple_z2_is_d11(const struct apple_z2 *z2) + { + return z2->variant == APPLE_Z2_VARIANT_D11; +@@ -248,6 +263,7 @@ enum apple_z2_calibration_provider { + APPLE_Z2_CAL_PRESSURE, + APPLE_Z2_CAL_FIREFLY_P100, + APPLE_Z2_CAL_EXT_MULTI_TOUCH, ++ APPLE_Z2_CAL_PROX = 9, + }; + + static void apple_z2_apply_z2_delays(struct apple_z2 *z2); +@@ -430,7 +446,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2, + int slot_valid; + struct apple_z2_finger *fingers; + +- if (msg_len >= APPLE_Z2_J172_HEADER_SIZE && ++ if (!apple_z2_is_n61(z2) && msg_len >= APPLE_Z2_J172_HEADER_SIZE && + msg[0] == APPLE_Z2_HID_REPORT_ID && msg[3] == 4) { + apple_z2_parse_j172_touches(z2, msg, msg_len); + return; +@@ -453,7 +469,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2, + msg_len, nfingers); + return; + } +- if (apple_z2_is_iphone7_plus(z2) && nfingers && ++ if ((apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) && nfingers && + !z2->surface_descriptor_valid) { + dev_warn_ratelimited(&z2->spidev->dev, + "iPhone 7 Plus touch packet without surface descriptor\n"); +@@ -471,7 +487,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2, + input_mt_slot(z2->input_dev, slot); + if (!input_mt_report_slot_state(z2->input_dev, MT_TOOL_FINGER, slot_valid)) + continue; +- if (apple_z2_is_iphone7_plus(z2)) { ++ if (apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) { + abs_x = (s16)le16_to_cpu(fingers[i].abs_x); + abs_y = (s16)le16_to_cpu(fingers[i].abs_y); + coords_valid = apple_z2_scale_iphone7_coord(abs_x, +@@ -503,7 +519,7 @@ static void apple_z2_parse_touches(struct apple_z2 *z2, + input_report_abs(z2->input_dev, ABS_MT_WIDTH_MINOR, + le16_to_cpu(fingers[i].tool_minor)); + orientation = (s16)le16_to_cpu(fingers[i].orientation); +- if (apple_z2_is_iphone7_plus(z2)) ++ if (apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) + orientation = (s16)(0x4000 - orientation); + input_report_abs(z2->input_dev, ABS_MT_ORIENTATION, orientation); + input_report_abs(z2->input_dev, ABS_MT_TOUCH_MAJOR, +@@ -521,8 +537,8 @@ static void apple_z2_dispatch_frame(struct apple_z2 *z2, const u8 *payload, + const u8 *report; + u16 report_len; + +- /* iPhone 7 Plus Gen2 packets carry the legacy report header directly. */ +- if (apple_z2_is_iphone7_plus(z2)) { ++ /* N61 Gen1 and iPhone 7 Plus carry the legacy report header directly. */ ++ if (apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) { + if (payload_len < APPLE_Z2_FINGERS_OFFSET) + return; + +@@ -605,7 +621,7 @@ static int apple_z2_gen1_exchange(struct apple_z2 *z2, u8 *buf, size_t len) + + if (!len || len > APPLE_Z2_RX_BUF_SIZE) + return -EMSGSIZE; +- if (apple_z2_is_j71b(z2)) ++ if (apple_z2_uses_gen1_data(z2)) + xfer.speed_hz = 500000; + tx = kmemdup(buf, len, GFP_KERNEL); + if (!tx) +@@ -657,16 +673,25 @@ static int apple_z2_read_packet(struct apple_z2 *z2) + if (error) + return error; + apple_z2_post_z2_xfer_delay(z2); ++ if (apple_z2_is_n61(z2)) { ++ dev_dbg_ratelimited(&z2->spidev->dev, ++ "N61 EB phase0 tag=%u rx=%*ph\n", counter, ++ APPLE_Z2_CMD_SIZE, z2->rx_buf); ++ if (!apple_z2_z2_checksum_valid(z2->rx_buf, APPLE_Z2_CMD_SIZE)) ++ return -EPROTO; ++ } + + if (z2->rx_buf[0] != APPLE_Z2_REPLY_INTERRUPT_DATA) + return 0; + + payload_len = get_unaligned_le16(z2->rx_buf + 1); +- if (apple_z2_is_gen1(z2) && !payload_len) { ++ if (apple_z2_uses_gen1_data(z2) && !payload_len) { + z2->runtime_queue_empty = true; ++ if (apple_z2_is_n61(z2)) ++ z2->index_parity = !z2->index_parity; + return 0; + } +- pkt_len = apple_z2_is_gen1(z2) ? ++ pkt_len = apple_z2_uses_gen1_data(z2) ? + max_t(size_t, 16, (size_t)payload_len + 5) : + (payload_len + 8) & 0xfffffffc; + if (pkt_len > APPLE_Z2_RX_BUF_SIZE) { +@@ -678,14 +703,22 @@ static int apple_z2_read_packet(struct apple_z2 *z2) + wire_len = max_t(size_t, wire_len, + APPLE_Z2_GEN2_MIN_RESULT_SIZE); + +- if (apple_z2_uses_ipad_protocol(z2) || apple_z2_is_iphone7_plus(z2)) { +- if (apple_z2_is_gen1(z2)) { ++ if (apple_z2_uses_ipad_protocol(z2) || apple_z2_is_iphone7_plus(z2) || ++ apple_z2_is_n61(z2)) { ++ if (apple_z2_uses_gen1_data(z2)) { + /* Data phase acknowledges the queued report. */ +- memset(z2->rx_buf, 0, wire_len); +- z2->rx_buf[0] = APPLE_Z2_CMD_READ_INTERRUPT_DATA; +- z2->rx_buf[1] = counter; +- z2->rx_buf[2] = 1; +- apple_z2_z2_checksum(z2->rx_buf, APPLE_Z2_CMD_SIZE); ++ if (apple_z2_is_n61(z2)) { ++ error = apple_z2_n61_interrupt_request(z2->rx_buf, ++ wire_len, counter); ++ if (error) ++ return error; ++ } else { ++ memset(z2->rx_buf, 0, wire_len); ++ z2->rx_buf[0] = APPLE_Z2_CMD_READ_INTERRUPT_DATA; ++ z2->rx_buf[1] = counter; ++ z2->rx_buf[2] = 1; ++ apple_z2_z2_checksum(z2->rx_buf, APPLE_Z2_CMD_SIZE); ++ } + error = apple_z2_gen1_exchange(z2, z2->rx_buf, wire_len); + } else { + memset(z2->rx_buf, 0xa5, wire_len); +@@ -701,12 +734,18 @@ static int apple_z2_read_packet(struct apple_z2 *z2) + return error; + if (apple_z2_is_iphone7_plus(z2)) + apple_z2_post_z2_xfer_delay(z2); ++ if (apple_z2_is_n61(z2)) ++ dev_dbg_ratelimited(&z2->spidev->dev, ++ "N61 EB data tag=%u len=%zu head=%*ph tail=%*ph\n", ++ counter, wire_len, APPLE_Z2_CMD_SIZE, z2->rx_buf, ++ APPLE_Z2_CMD_SIZE, ++ z2->rx_buf + wire_len - APPLE_Z2_CMD_SIZE); + + /* + * Generation-1 firmware answers no-data (0) or a checksummed control + * reply while service is pending. Neither is a frame; keep the tag. + */ +- if (apple_z2_is_gen1(z2) && ++ if (apple_z2_uses_gen1_data(z2) && + (!z2->rx_buf[0] || + (wire_len >= 16 && z2->rx_buf[0] == APPLE_Z2_CMD_LAST && + apple_z2_z2_checksum_valid(z2->rx_buf, 16)))) { +@@ -787,7 +826,7 @@ static void apple_z2_set_spi_delay(struct spi_delay *delay, + static unsigned int apple_z2_effective_cs_delay_us(struct apple_z2 *z2) + { + /* J172 firmware expresses this field in ns; Linux SPI uses us here. */ +- if (apple_z2_is_gen1(z2)) ++ if (apple_z2_uses_gen1_data(z2)) + return z2->info->cs_setup_us; + return apple_z2_uses_ipad_protocol(z2) ? 10 : z2->z2_cs_delay_us; + } +@@ -799,7 +838,7 @@ static void apple_z2_apply_z2_delays(struct apple_z2 *z2) + unsigned int inactive_us = apple_z2_uses_ipad_protocol(z2) ? 10 : + z2->z2_inter_packet_delay_us; + +- if (apple_z2_is_gen1(z2)) { ++ if (apple_z2_uses_gen1_data(z2)) { + hold_us = z2->info->cs_hold_us; + inactive_us = z2->info->cs_inactive_us; + } +@@ -1031,6 +1070,13 @@ static int apple_z2_read_report_locked(struct apple_z2 *z2, u8 report, + return error; + + if (len <= 11) { ++ if (apple_z2_is_n61(z2)) { ++ apple_z2_n61_short_read_request(z2->rx_buf, report, len); ++ error = apple_z2_gen1_exchange(z2, z2->rx_buf, APPLE_Z2_CMD_SIZE); ++ if (!error) ++ apple_z2_post_z2_xfer_delay(z2); ++ return error; ++ } + snprintf(last_tag, sizeof(last_tag), "%s-last", tag); + error = apple_z2_z2_cmd_locked(z2, APPLE_Z2_CMD_LAST, 0, + last_tag); +@@ -1042,14 +1088,18 @@ static int apple_z2_read_report_locked(struct apple_z2 *z2, u8 report, + + data_len = len + 5; + apple_z2_apply_z2_delays(z2); +- if (apple_z2_is_gen1(z2)) { ++ if (apple_z2_uses_gen1_data(z2)) { + /* Long read request repeated with phase 1 and a checksum. */ +- memset(z2->rx_buf, 0, data_len); +- z2->rx_buf[0] = APPLE_Z2_CMD_CTRL_READ_LONG; +- z2->rx_buf[1] = report; +- z2->rx_buf[2] = 1; +- put_unaligned_le16(len, z2->rx_buf + 3); +- apple_z2_z2_checksum(z2->rx_buf, data_len); ++ if (apple_z2_is_n61(z2)) { ++ apple_z2_n61_long_read_request(z2->rx_buf, report, len); ++ } else { ++ memset(z2->rx_buf, 0, data_len); ++ z2->rx_buf[0] = APPLE_Z2_CMD_CTRL_READ_LONG; ++ z2->rx_buf[1] = report; ++ z2->rx_buf[2] = 1; ++ put_unaligned_le16(len, z2->rx_buf + 3); ++ apple_z2_z2_checksum(z2->rx_buf, data_len); ++ } + error = apple_z2_gen1_exchange(z2, z2->rx_buf, data_len); + } else { + memset(z2->rx_buf, 0xa5, data_len); +@@ -1083,7 +1133,7 @@ static int apple_z2_store_surface_descriptor(struct apple_z2 *z2, u16 len) + if (len != APPLE_Z2_SURFACE_DESCRIPTOR_SIZE || + z2->rx_buf[0] != APPLE_Z2_CMD_CTRL_READ_LONG || + z2->rx_buf[1] != APPLE_Z2_REPORT_SURFACE || +- z2->rx_buf[2] > (apple_z2_is_gen1(z2) ? 1 : 0) || ++ z2->rx_buf[2] > (apple_z2_uses_gen1_data(z2) ? 1 : 0) || + !apple_z2_z2_checksum_valid(z2->rx_buf, len + 5)) + return -EPROTO; + +@@ -1164,6 +1214,40 @@ static int apple_z2_iphone7_plus_init_locked(struct apple_z2 *z2) + return 0; + } + ++static int apple_z2_n61_init_locked(struct apple_z2 *z2) ++{ ++ static const u8 rep9d[] = { 1, 0, 0, 0, 0, 0, 0, 0 }; ++ static const u8 repbf[] = { 0x9d, 0x81, 0x09, 0 }; ++ static const u8 repaf[] = { 0 }; ++ u16 length; ++ u8 status; ++ int error; ++ ++ error = apple_z2_z2_cmd_locked(z2, APPLE_Z2_CMD_WAKE, 0, "n61-wake"); ++ if (error) ++ return error; ++ error = apple_z2_read_report_info_locked(z2, APPLE_Z2_REPORT_SURFACE, ++ &status, &length, "n61-d9"); ++ if (error) ++ return error; ++ if (status || length != APPLE_Z2_SURFACE_DESCRIPTOR_SIZE) ++ return -EPROTO; ++ error = apple_z2_read_report_locked(z2, APPLE_Z2_REPORT_SURFACE, ++ length, "n61-d9-read"); ++ if (error) ++ return error; ++ error = apple_z2_store_surface_descriptor(z2, length); ++ if (error) ++ return error; ++ error = apple_z2_write_report_locked(z2, 0x9d, rep9d, sizeof(rep9d), "n61-9d"); ++ if (error) ++ return error; ++ error = apple_z2_write_report_locked(z2, 0xbf, repbf, sizeof(repbf), "n61-bf"); ++ if (error) ++ return error; ++ return apple_z2_write_report_locked(z2, 0xaf, repaf, sizeof(repaf), "n61-af"); ++} ++ + static int apple_z2_j172_init_reads_locked(struct apple_z2 *z2) + { + u8 status; +@@ -1493,9 +1577,14 @@ static void apple_z2_platform_power_off(struct apple_z2 *z2) + { + int error; + ++ if (apple_z2_is_n61(z2) && z2->n61_boot_power_preserved) { ++ z2->platform_powered = false; ++ return; ++ } ++ + if (apple_z2_is_gen1(z2)) + apple_z2_gen1_restore_irq_type(z2); +- if (apple_z2_is_iphone7_plus(z2)) ++ if (apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) + gpiod_set_value_cansleep(z2->reset_gpio, 1); + + if (z2->clk_enabled) { +@@ -1512,7 +1601,7 @@ static void apple_z2_platform_power_off(struct apple_z2 *z2) + return; + } + +- if (apple_z2_is_iphone7_plus(z2)) { ++ if (apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) { + if (z2->core_enabled) { + error = regulator_disable(z2->core_supply); + if (error) { +@@ -1651,6 +1740,40 @@ err_power_off: + return error; + } + ++static int apple_z2_n61_power_on(struct apple_z2 *z2) ++{ ++ int error; ++ ++ gpiod_set_value_cansleep(z2->reset_gpio, 1); ++ if (z2->n61_boot_power_preserved) { ++ z2->platform_powered = true; ++ return 0; ++ } ++ ++ /* Chestnut prepares its shared boost/Sage rails before the touch LDO. */ ++ error = regulator_enable(z2->hv_supply); ++ if (error) ++ return error; ++ z2->hv_enabled = true; ++ error = regulator_enable(z2->core_supply); ++ if (error) ++ goto err_power_off; ++ z2->core_enabled = true; ++ /* The N61 enable-CS helper settles for 5 ms before clock startup. */ ++ usleep_range(5000, 6000); ++ error = apple_z2_enable_clock(z2); ++ if (error) ++ goto err_power_off; ++ usleep_range(15000, 16000); ++ z2->platform_powered = true; ++ z2->protocol_state = APPLE_Z2_STATE_POWERED; ++ return 0; ++ ++err_power_off: ++ apple_z2_platform_power_off(z2); ++ return error; ++} ++ + static int apple_z2_platform_power_on(struct apple_z2 *z2) + { + int error; +@@ -1715,6 +1838,8 @@ static const char *apple_z2_calibration_property(u32 provider) + return FIREFLY_P100_CAL_PROP_NAME; + case APPLE_Z2_CAL_EXT_MULTI_TOUCH: + return EXT_CAL_PROP_NAME; ++ case APPLE_Z2_CAL_PROX: ++ return PROX_CAL_PROP_NAME; + default: + return NULL; + } +@@ -1848,6 +1973,87 @@ static const u8 *apple_z2_build_iphone7_cal(struct apple_z2 *z2, + return no_free_ptr(blob_data); + } + ++static const u8 *apple_z2_build_n61_cal(struct apple_z2 *z2, ++ const char *property, u32 address, ++ u32 maximum, u32 provider, size_t *size) ++{ ++ struct device *dev = &z2->spidev->dev; ++ u8 *cal __free(kfree) = NULL; ++ u8 *packet __free(kfree) = NULL; ++ int length, error; ++ ++ if (!apple_z2_n61_calibration_target(address, maximum, provider)) ++ return ERR_PTR(-EINVAL); ++ if (!device_property_present(dev, property)) ++ return provider == APPLE_Z2_CAL_MULTI_TOUCH ? ERR_PTR(-ENOENT) : NULL; ++ length = device_property_count_u8(dev, property); ++ if (length <= 0 || length > maximum) ++ return ERR_PTR(-EINVAL); ++ cal = kmalloc(length, GFP_KERNEL); ++ if (!cal) ++ return ERR_PTR(-ENOMEM); ++ error = device_property_read_u8_array(dev, property, cal, length); ++ if (error) ++ return ERR_PTR(error); ++ packet = kzalloc(16 + round_up(length, 4), GFP_KERNEL); ++ if (!packet) ++ return ERR_PTR(-ENOMEM); ++ error = apple_z2_n61_calibration_packet(packet, 16 + round_up(length, 4), ++ cal, length, address); ++ if (error < 0) ++ return ERR_PTR(error); ++ *size = error; ++ return no_free_ptr(packet); ++} ++ ++/* Gen1 byte streams keep CS asserted across bounded PIO chunks. */ ++static int apple_z2_send_n61_blob(struct apple_z2 *z2, const u8 *data, ++ u32 size, bool init) ++{ ++ struct spi_transfer chunks[APPLE_Z2_N61_MAX_CHUNKS] = { }; ++ struct spi_transfer ack = { ++ .tx_buf = z2->tx_buf, ++ .rx_buf = z2->rx_buf, ++ .len = 2, ++ .bits_per_word = 8, ++ .speed_hz = 500000, ++ }; ++ unsigned int count, i; ++ u32 offset = 0; ++ int error; ++ ++ if (!size || size > APPLE_Z2_N61_MAX_BLOB || ++ (init && !apple_z2_n61_init_valid(data, size))) ++ return -EINVAL; ++ count = DIV_ROUND_UP(size, APPLE_Z2_N61_CHUNK); ++ for (i = 0; i < count; i++) { ++ chunks[i].tx_buf = data + offset; ++ chunks[i].len = min_t(u32, size - offset, APPLE_Z2_N61_CHUNK); ++ chunks[i].bits_per_word = 8; ++ chunks[i].speed_hz = 500000; ++ offset += chunks[i].len; ++ } ++ if (init) { ++ memset(z2->rx_buf, 0, size); ++ chunks[0].rx_buf = z2->rx_buf; ++ } ++ apple_z2_apply_z2_delays(z2); ++ error = spi_sync_transfer(z2->spidev, chunks, count); ++ if (error) ++ return error; ++ if (init) { ++ dev_dbg(&z2->spidev->dev, "Gen1 INIT len=%u rx=%*ph\n", ++ size, size, z2->rx_buf); ++ return 0; ++ } ++ z2->tx_buf[0] = 0x1a; ++ z2->tx_buf[1] = 0xa1; ++ error = spi_sync_transfer(z2->spidev, &ack, 1); ++ if (!error) ++ apple_z2_post_ack_delay(z2); ++ return error; ++} ++ + static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data, + u32 size, bool init, bool defer_irq_wait) + { +@@ -1864,6 +2070,9 @@ static int apple_z2_send_firmware_blob(struct apple_z2 *z2, const u8 *data, + bool ready; + int error; + ++ if (apple_z2_is_n61(z2)) ++ return apple_z2_send_n61_blob(z2, data, size, init); ++ + /* + * iPhone 7 Plus firmware stores HBPP byte streams in 8-bit wire order. + * SmartIO may still DMA these transfers, but using 16-bit SPI words would +@@ -2108,6 +2317,7 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + u32 timeout_ms; + bool defer_irq_wait; + bool init; ++ bool n61_touch_calibration = false; + int error; + + error = request_firmware(&fw, z2->fw_name, &z2->spidev->dev); +@@ -2153,6 +2363,8 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + break; + } + case LOAD_COMMAND_RAW_XFER: ++ if (apple_z2_is_n61(z2)) ++ return -EINVAL; + if (size > fw->size - fw_idx) + return -EINVAL; + error = apple_z2_send_firmware_raw_xfer(z2, fw->data + fw_idx, size); +@@ -2161,6 +2373,8 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + fw_idx += size; + break; + case LOAD_COMMAND_SET_CONFIG: ++ if (apple_z2_is_n61(z2)) ++ return -EINVAL; + if (size > fw->size - fw_idx) + return -EINVAL; + error = apple_z2_apply_fw_config(z2, fw->data + fw_idx, size); +@@ -2171,6 +2385,9 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + case LOAD_COMMAND_SEND_CALIBRATION: { + const u8 *data __free(kfree) = NULL; + ++ if (apple_z2_is_n61(z2)) ++ return -EINVAL; ++ + address = size; + data = apple_z2_build_cal_blob(z2, CAL_PROP_NAME, address, + U32_MAX, &size, false); +@@ -2189,7 +2406,8 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + u32 max_size; + u32 provider; + +- if (!apple_z2_is_iphone7_plus(z2) && !apple_z2_uses_ipad_protocol(z2)) ++ if (!apple_z2_is_iphone7_plus(z2) && !apple_z2_uses_ipad_protocol(z2) && ++ !apple_z2_is_n61(z2)) + return -EINVAL; + if (size != sizeof(struct apple_z2_fw_calibration) || + size > fw->size - fw_idx) +@@ -2203,8 +2421,15 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + return -EINVAL; + fw_idx += size; + +- data = apple_z2_build_iphone7_cal(z2, property, address, +- max_size, &size); ++ if (apple_z2_is_n61(z2)) { ++ data = apple_z2_build_n61_cal(z2, property, address, ++ max_size, provider, &size); ++ if (!IS_ERR_OR_NULL(data) && provider == APPLE_Z2_CAL_MULTI_TOUCH) ++ n61_touch_calibration = true; ++ } else { ++ data = apple_z2_build_iphone7_cal(z2, property, address, ++ max_size, &size); ++ } + if (IS_ERR(data)) + return PTR_ERR(data); + if (!data) +@@ -2216,6 +2441,8 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + break; + } + case LOAD_COMMAND_WAIT_IRQ: ++ if (apple_z2_is_n61(z2)) ++ return -EINVAL; + if (size != sizeof(__le32) || size > fw->size - fw_idx) + return -EINVAL; + timeout_ms = get_unaligned_le32(fw->data + fw_idx); +@@ -2242,6 +2469,24 @@ static int apple_z2_upload_firmware(struct apple_z2 *z2) + fw_idx = round_up(fw_idx, 4); + } + ++ if (apple_z2_is_n61(z2)) { ++ if (!n61_touch_calibration) ++ return -ENOENT; ++ msleep(50); ++ mutex_lock(&z2->io_lock); ++ error = apple_z2_n61_init_locked(z2); ++ if (!error) { ++ z2->booted = true; ++ z2->protocol_state = APPLE_Z2_STATE_REPORTS_READY; ++ } ++ mutex_unlock(&z2->io_lock); ++ if (!error && z2->upload_irq_masked) { ++ z2->upload_irq_masked = false; ++ enable_irq(z2->spidev->irq); ++ } ++ return error; ++ } ++ + if (apple_z2_is_iphone7_plus(z2)) { + msleep(50); + mutex_lock(&z2->io_lock); +@@ -2386,11 +2631,67 @@ static int apple_z2_iphone7_plus_post_boot_preamble(struct apple_z2 *z2) + return spi_sync_transfer(z2->spidev, &xfer, 1); + } + ++static int apple_z2_n61_boot(struct apple_z2 *z2) ++{ ++ static const u8 preamble[] = { 0x19, 0xc1 }; ++ static const u8 check[] = { 0x1a, 0xa1, 0x18, 0xe1, 0x18, 0xe1, ++ 0x18, 0xe1, 0x18, 0xe1, 0x18, 0xe1, ++ 0x18, 0xe1, 0x18, 0xe1 }; ++ unsigned int attempt; ++ int error; ++ ++ z2->booted = false; ++ apple_z2_reset_protocol(z2); ++ error = apple_z2_n61_power_on(z2); ++ if (error) ++ return error; ++ /* AppleMultitouchN1SPI exchanges the preamble while reset is held. */ ++ error = apple_z2_send_n61_blob(z2, preamble, sizeof(preamble), true); ++ if (error) ++ goto err_power; ++ usleep_range(10000, 11000); ++ /* N61 reset-deassert preflight clocks one complete CHECK exchange. */ ++ error = apple_z2_send_n61_blob(z2, check, sizeof(check), true); ++ if (error) ++ goto err_power; ++ gpiod_set_value_cansleep(z2->reset_gpio, 0); ++ /* N61 reset-deassert-delay is 15 ms. Readiness is synchronous. */ ++ usleep_range(15000, 16000); ++ for (attempt = 0; attempt < APPLE_Z2_N61_READY_ATTEMPTS; attempt++) { ++ error = apple_z2_send_n61_blob(z2, check, sizeof(check), true); ++ if (error) ++ goto err_power; ++ if (apple_z2_n61_rom_ready(z2->rx_buf, sizeof(check))) ++ break; ++ if (attempt + 1 < APPLE_Z2_N61_READY_ATTEMPTS) ++ usleep_range(5000, 6000); ++ } ++ if (attempt == APPLE_Z2_N61_READY_ATTEMPTS) { ++ error = -ETIMEDOUT; ++ goto err_power; ++ } ++ /* IRQF_NO_AUTOEN keeps runtime IRQs masked until reports are ready. */ ++ z2->upload_irq_masked = true; ++ z2->protocol_state = APPLE_Z2_STATE_HBPP_READY; ++ error = apple_z2_upload_firmware(z2); ++ if (!error) ++ return 0; ++ z2->upload_irq_masked = false; ++err_power: ++ gpiod_set_value_cansleep(z2->reset_gpio, 1); ++ apple_z2_platform_power_off(z2); ++ apple_z2_reset_protocol(z2); ++ return error; ++} ++ + static int apple_z2_boot(struct apple_z2 *z2) + { + bool irq_enabled = false; + int error; + ++ if (apple_z2_is_n61(z2)) ++ return apple_z2_n61_boot(z2); ++ + z2->booted = false; + apple_z2_reset_protocol(z2); + error = apple_z2_platform_power_on(z2); +@@ -2515,6 +2816,14 @@ static const struct apple_z2_chip_info apple_z2_d111_info = { + .variant = APPLE_Z2_VARIANT_D111, + }; + ++static const struct apple_z2_chip_info apple_z2_n61_info = { ++ .name = "iPhone 6 Touchscreen", ++ .variant = APPLE_Z2_VARIANT_N61, ++ .cs_setup_us = 1000, ++ .cs_hold_us = 1000, ++ .cs_inactive_us = 1000, ++}; ++ + static int apple_z2_probe(struct spi_device *spi) + { + struct device *dev = &spi->dev; +@@ -2543,6 +2852,39 @@ static int apple_z2_probe(struct spi_device *spi) + z2->spidev = spi; + z2->info = info; + z2->variant = info->variant; ++ if (apple_z2_is_n61(z2)) { ++ z2->n61_boot_power_preserved = ++ device_property_read_bool(dev, "apple,boot-power-preserved"); ++ if (z2->n61_boot_power_preserved) { ++ if (device_property_present(dev, "ana-supply") || ++ device_property_present(dev, "core-supply") || ++ device_property_present(dev, "clocks")) ++ return dev_err_probe(dev, -EINVAL, ++ "preserved N61 power cannot claim supplies or clock\n"); ++ } else if (!device_property_present(dev, "ana-supply") || ++ !device_property_present(dev, "core-supply") || ++ !device_property_present(dev, "clocks")) { ++ return dev_err_probe(dev, -EINVAL, ++ "N61 requires explicit analog/core supplies and clock\n"); ++ } ++ if (spi->mode & (SPI_CPHA | SPI_CPOL) || spi->bits_per_word != 8) ++ return dev_err_probe(dev, -EINVAL, "N61 requires mode 0 and 8-bit SPI\n"); ++ if (device_property_present(dev, "power-ana-gpios") || ++ device_property_present(dev, "power-ldo-gpios") || ++ device_property_present(dev, "display-sync-gpios") || ++ device_property_present(dev, "display-sync1-gpios")) ++ return dev_err_probe(dev, -EINVAL, "N61 uses PMIC supplies\n"); ++ cal_size = device_property_count_u8(dev, CAL_PROP_NAME); ++ if (cal_size <= 0 || cal_size > APPLE_Z2_N61_TOUCH_MAX) ++ return dev_err_probe(dev, -EINVAL, "invalid N61 touch calibration\n"); ++ if (device_property_present(dev, PROX_CAL_PROP_NAME)) { ++ cal_size = device_property_count_u8(dev, PROX_CAL_PROP_NAME); ++ if (cal_size <= 0 || cal_size > APPLE_Z2_N61_PROX_MAX) ++ return dev_err_probe(dev, -EINVAL, "invalid N61 prox calibration\n"); ++ } ++ z2->z2_inter_packet_delay_us = 1000; ++ z2->z2_cs_delay_us = 1000; ++ } + z2->boot_timeout_ms = 20; + if (apple_z2_is_iphone7_plus(z2)) { + z2->z2_inter_packet_delay_us = 1000; +@@ -2566,8 +2908,9 @@ static int apple_z2_probe(struct spi_device *spi) + if (IS_ERR(z2->reset_gpio)) + return dev_err_probe(dev, PTR_ERR(z2->reset_gpio), + "unable to get reset GPIO\n"); +- if (apple_z2_is_iphone7_plus(z2)) { +- z2->hv_supply = devm_regulator_get(dev, "hv"); ++ if (apple_z2_is_iphone7_plus(z2) || ++ (apple_z2_is_n61(z2) && !z2->n61_boot_power_preserved)) { ++ z2->hv_supply = devm_regulator_get(dev, apple_z2_is_n61(z2) ? "ana" : "hv"); + if (IS_ERR(z2->hv_supply)) + return dev_err_probe(dev, PTR_ERR(z2->hv_supply), + "unable to get HV supply\n"); +@@ -2599,7 +2942,10 @@ static int apple_z2_probe(struct spi_device *spi) + return dev_err_probe(dev, PTR_ERR(z2->display_sync1_gpio), + "unable to get display-sync1 GPIO\n"); + +- z2->clk = devm_clk_get_optional(dev, NULL); ++ if (apple_z2_is_n61(z2) && !z2->n61_boot_power_preserved) ++ z2->clk = devm_clk_get(dev, NULL); ++ else ++ z2->clk = devm_clk_get_optional(dev, NULL); + if (IS_ERR(z2->clk)) + return dev_err_probe(dev, PTR_ERR(z2->clk), + "unable to get clock\n"); +@@ -2658,7 +3004,7 @@ static int apple_z2_probe(struct spi_device *spi) + + if (apple_z2_uses_ipad_protocol(z2)) { + slots = APPLE_Z2_J172_MAX_CONTACTS; +- } else if (apple_z2_is_iphone7_plus(z2)) { ++ } else if (apple_z2_is_iphone7_plus(z2) || apple_z2_is_n61(z2)) { + slots = 10; + if (apple_z2_is_d11(z2)) + mt_flags |= INPUT_MT_DROP_UNUSED; +@@ -2715,6 +3061,7 @@ static const struct of_device_id apple_z2_of_match[] = { + { .compatible = "apple,j172-touchscreen", .data = &apple_z2_j172_info }, + { .compatible = "apple,d11-touchscreen", .data = &apple_z2_d11_info }, + { .compatible = "apple,d111-touchscreen", .data = &apple_z2_d111_info }, ++ { .compatible = "apple,n61-touchscreen", .data = &apple_z2_n61_info }, + {} + }; + MODULE_DEVICE_TABLE(of, apple_z2_of_match); +@@ -2725,6 +3072,7 @@ static struct spi_device_id apple_z2_of_id[] = { + { .name = "j172-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_j172_info }, + { .name = "d11-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d11_info }, + { .name = "d111-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_d111_info }, ++ { .name = "n61-touchscreen", .driver_data = (kernel_ulong_t)&apple_z2_n61_info }, + {} + }; + MODULE_DEVICE_TABLE(spi, apple_z2_of_id); +diff --git a/drivers/input/touchscreen/apple_z2_n61.h b/drivers/input/touchscreen/apple_z2_n61.h +new file mode 100644 +index 0000000000000000000000000000000000000000..73cfa3c0e37a482d8004455c26d6ecea64e761b3 +--- /dev/null ++++ b/drivers/input/touchscreen/apple_z2_n61.h +@@ -0,0 +1,142 @@ ++/* SPDX-License-Identifier: GPL-2.0 */ ++#ifndef _APPLE_Z2_N61_H ++#define _APPLE_Z2_N61_H ++ ++#include ++#include ++#include ++#include ++ ++#define APPLE_Z2_N61_TOUCH_MAX 1024 ++#define APPLE_Z2_N61_PROX_MAX 128 ++#define APPLE_Z2_N61_CHUNK 16384 ++#define APPLE_Z2_N61_MAX_CHUNKS 4 ++#define APPLE_Z2_N61_MAX_BLOB (APPLE_Z2_N61_CHUNK * APPLE_Z2_N61_MAX_CHUNKS) ++#define APPLE_Z2_N61_READY_ATTEMPTS 50 ++ ++/* N1 EB includes phase 1 in the sum, then puts that sum at the wire tail. */ ++static inline int apple_z2_n61_interrupt_request(u8 *out, size_t size, u8 tag) ++{ ++ if (size < 16 || size > 8192 || (tag != 1 && tag != 2)) ++ return -EINVAL; ++ memset(out, 0, size); ++ out[0] = 0xeb; ++ out[1] = tag; ++ out[2] = 1; ++ put_unaligned_le16(0xeb + tag + 1, out + size - 2); ++ return 0; ++} ++ ++/* Provider IDs are the stable Z2FW calibration ABI; append new providers. */ ++static inline bool apple_z2_n61_calibration_target(u32 address, u32 maximum, ++ u32 provider) ++{ ++ if (!maximum) ++ return false; ++ if (provider == 0) ++ return address == 0x10009000 && maximum <= APPLE_Z2_N61_TOUCH_MAX; ++ if (provider == 9) ++ return address == 0x10009600 && maximum <= APPLE_Z2_N61_PROX_MAX; ++ return false; ++} ++ ++static inline bool apple_z2_n61_init_valid(const u8 *data, size_t size) ++{ ++ static const u8 execute[] = { 0x1d, 0x53, 0x34, 0, 0x10, 0, ++ 0, 1, 0, 0, 0, 0x45 }; ++ size_t i; ++ ++ if (size == 2) ++ return data[0] == 0x19 && data[1] == 0xc1; ++ if (size == sizeof(execute)) ++ return !memcmp(data, execute, sizeof(execute)); ++ if (size != 16 || data[0] != 0x1a || data[1] != 0xa1) ++ return false; ++ for (i = 2; i < size; i += 2) ++ if (data[i] != 0x18 || data[i + 1] != 0xe1) ++ return false; ++ return true; ++} ++ ++/* Apple N1 retains the phase-0 checksum when it sends the phase-1 request. */ ++static inline void apple_z2_n61_long_read_request(u8 *out, u8 report, u16 size) ++{ ++ u16 checksum = 0xe7 + report + (size & 0xff) + (size >> 8); ++ ++ memset(out, 0, size + 5); ++ out[0] = 0xe7; ++ out[1] = report; ++ out[2] = 1; ++ put_unaligned_le16(size, out + 3); ++ put_unaligned_le16(checksum, out + size + 3); ++} ++ ++static inline void apple_z2_n61_short_read_request(u8 *out, u8 report, u16 size) ++{ ++ u16 checksum = 0xe6 + report + (size & 0xff) + (size >> 8); ++ ++ memset(out, 0, 16); ++ out[0] = 0xe6; ++ out[1] = report; ++ out[2] = 1; ++ put_unaligned_le16(size, out + 3); ++ put_unaligned_le16(checksum, out + 14); ++} ++ ++static inline bool apple_z2_n61_rom_word_valid(u16 word) ++{ ++ switch (word) { ++ case 0x18e1: ++ case 0x1aa1: ++ case 0x1f01: ++ case 0x4879: ++ case 0x4969: ++ case 0x4ad1: ++ case 0x4bc1: ++ return true; ++ default: ++ return false; ++ } ++} ++ ++static inline bool apple_z2_n61_rom_ready(const u8 *data, size_t size) ++{ ++ return size >= 4 && ++ apple_z2_n61_rom_word_valid(get_unaligned_be16(data)) && ++ apple_z2_n61_rom_word_valid(get_unaligned_be16(data + 2)); ++} ++ ++/* Legacy HBPP carries a full word count and swaps bytes within payload pairs. */ ++static inline int apple_z2_n61_calibration_packet(u8 *out, size_t capacity, ++ const u8 *cal, size_t size, ++ u32 address) ++{ ++ size_t padded = (size + 3) & ~(size_t)3; ++ u32 checksum = 0; ++ u16 header_checksum = 0; ++ size_t i; ++ ++ if (!size || size > APPLE_Z2_N61_TOUCH_MAX || ++ (address == 0x10009600 && size > APPLE_Z2_N61_PROX_MAX) || ++ (address != 0x10009000 && address != 0x10009600)) ++ return -EINVAL; ++ if (capacity < 16 + padded) ++ return -EMSGSIZE; ++ memset(out, 0, 16 + padded); ++ put_unaligned_be16(0x18e1, out); ++ put_unaligned_be16(0x3001, out + 2); ++ put_unaligned_be16(padded / 4, out + 4); ++ put_unaligned_be16(address, out + 6); ++ put_unaligned_be16(address >> 16, out + 8); ++ for (i = 4; i < 10; i++) ++ header_checksum += out[i]; ++ put_unaligned_be16(header_checksum, out + 10); ++ for (i = 0; i < size; i++) { ++ out[12 + (i ^ 1)] = cal[i]; ++ checksum += cal[i]; ++ } ++ put_unaligned_be16(checksum, out + 12 + padded); ++ put_unaligned_be16(checksum >> 16, out + 14 + padded); ++ return 16 + padded; ++} ++#endif diff --git a/kernel/patches/devices/t7000/0004-arm64-apple-wire-N61-touch-power.patch b/kernel/patches/devices/t7000/0004-arm64-apple-wire-N61-touch-power.patch new file mode 100644 index 00000000..a829d619 --- /dev/null +++ b/kernel/patches/devices/t7000/0004-arm64-apple-wire-N61-touch-power.patch @@ -0,0 +1,266 @@ +Subject: [PATCH] arm64: apple: wire N61 touch clock and PMIC supplies + +T7000 TOUCH uses the existing PMGR divider/gate layout at offset 0x1e000 +with a 24 MHz parent and 32768 Hz target. Add the T7000 match without +changing the T8010 clock operations. + +N61 D2186 LDO14 is register 0x0085 bit 1. Chestnut LDO2 is register 0x05 +bit 4 and requires boost/Sage0 dependency bits 0..2. The touch regulator +preserves those shared display bits when it disables LDO2. + +Describe the N61 reset, IRQ and board supplies with its touchscreen child +enabled after physical power, ROM readiness and touch report qualification. +Per-unit calibration enters through the native loader's touchscreen alias. + +Evidence: N61 16H22 xnu-4903.272.3~3/RELEASE_ARM64_T7000, kernel SHA256 +0145bc0eda01cd55e8b996d24ffc19c25712714326f893ba5085a5016d30a4ad; +AppleT7000PMGR TOUCH dispatcher/setup, AppleD2186PMU pmuL selector14, +and AppleChestnutDisplayPMU dpLE output2/options3. No vendor firmware or +unit calibration bytes are included in this patch. + +--- +diff --git a/drivers/clk/clk-apple-t8010-touch.c b/drivers/clk/clk-apple-t8010-touch.c +--- a/drivers/clk/clk-apple-t8010-touch.c ++++ b/drivers/clk/clk-apple-t8010-touch.c +@@ -1,6 +1,6 @@ + // SPDX-License-Identifier: GPL-2.0-only OR MIT + /* +- * Apple T8010 touchscreen clock driver ++ * Apple T7000/T8010 touchscreen clock driver + * + * Copyright (C) Corellium LLC + * Copyright (C) Paul Praschl +@@ -187,6 +187,7 @@ + } + + static const struct of_device_id apple_t8010_touch_clk_of_match[] = { ++ { .compatible = "apple,t7000-touch-clock" }, + { .compatible = "apple,t8010-touch-clock" }, + { } + }; +@@ -201,6 +202,6 @@ + }; + module_platform_driver(apple_t8010_touch_clk_driver); + +-MODULE_DESCRIPTION("Apple T8010 touchscreen clock driver"); ++MODULE_DESCRIPTION("Apple T7000/T8010 touchscreen clock driver"); + MODULE_AUTHOR("Paul Praschl"); + MODULE_LICENSE("Dual MIT/GPL"); +diff --git a/drivers/clk/Kconfig b/drivers/clk/Kconfig +--- a/drivers/clk/Kconfig ++++ b/drivers/clk/Kconfig +@@ -67,12 +67,12 @@ + generators of audio clocks. + + config COMMON_CLK_APPLE_T8010_TOUCH +- bool "Clock driver for Apple T8010 touchscreens" ++ bool "Clock driver for Apple T7000/T8010 touchscreens" + depends on ARCH_APPLE || COMPILE_TEST + default ARCH_APPLE + help + This driver supports the PMGR clock gate and divider used by +- touchscreen controllers on A10/T8010 Apple devices. The clock ++ touchscreen controllers on A8/T7000 and A10/T8010 Apple devices. The clock + is required for firmware upload and runtime touch reports on + iPad models using this controller. + +diff --git a/drivers/regulator/apple-pmic-touch-regulator.c b/drivers/regulator/apple-pmic-touch-regulator.c +--- a/drivers/regulator/apple-pmic-touch-regulator.c ++++ b/drivers/regulator/apple-pmic-touch-regulator.c +@@ -17,6 +17,58 @@ + .enable = regulator_enable_regmap, + .disable = regulator_disable_regmap, + .is_enabled = regulator_is_enabled_regmap, ++}; ++ ++static int apple_chestnut_touch_ldo_enable(struct regulator_dev *rdev) ++{ ++ struct regmap *regmap = rdev_get_regmap(rdev); ++ int error; ++ ++ /* N61 LDO2 needs the shared boost and Sage0 display rails. */ ++ error = regmap_update_bits(regmap, 0x05, 0x07, 0x07); ++ if (error) ++ return error; ++ return regulator_enable_regmap(rdev); ++} ++ ++static int apple_chestnut_touch_ldo_is_enabled(struct regulator_dev *rdev) ++{ ++ unsigned int value; ++ int error; ++ ++ error = regmap_read(rdev_get_regmap(rdev), 0x05, &value); ++ if (error) ++ return error; ++ return (value & 0x17) == 0x17; ++} ++ ++static const struct regulator_ops apple_chestnut_touch_ldo_ops = { ++ .enable = apple_chestnut_touch_ldo_enable, ++ /* Disable owns only bit 4. Shared display rails remain enabled. */ ++ .disable = regulator_disable_regmap, ++ .is_enabled = apple_chestnut_touch_ldo_is_enabled, ++}; ++ ++static const struct regulator_desc apple_d2186_touch_core = { ++ .name = "d2186-touch-core", ++ .ops = &apple_pmic_touch_regulator_ops, ++ .type = REGULATOR_VOLTAGE, ++ .owner = THIS_MODULE, ++ .enable_reg = 0x85, ++ .enable_mask = 0x02, ++ .enable_val = 0x02, ++ .disable_val = 0, ++}; ++ ++static const struct regulator_desc apple_chestnut_touch_ldo = { ++ .name = "chestnut-touch-ldo", ++ .ops = &apple_chestnut_touch_ldo_ops, ++ .type = REGULATOR_VOLTAGE, ++ .owner = THIS_MODULE, ++ .enable_reg = 0x05, ++ .enable_mask = 0x10, ++ .enable_val = 0x10, ++ .disable_val = 0, + }; + + static const struct regulator_desc apple_adelyn_touch_core = { +@@ -73,6 +125,14 @@ + + static const struct of_device_id apple_pmic_touch_regulator_of_match[] = { + { ++ .compatible = "apple,d2186-touch-core-regulator", ++ .data = &apple_d2186_touch_core, ++ }, ++ { ++ .compatible = "apple,chestnut-touch-ldo-regulator", ++ .data = &apple_chestnut_touch_ldo, ++ }, ++ { + .compatible = "apple,adelyn-touch-core-regulator", + .data = &apple_adelyn_touch_core, + }, +diff --git a/Documentation/devicetree/bindings/clock/apple,t8010-touch-clock.yaml b/Documentation/devicetree/bindings/clock/apple,t8010-touch-clock.yaml +--- a/Documentation/devicetree/bindings/clock/apple,t8010-touch-clock.yaml ++++ b/Documentation/devicetree/bindings/clock/apple,t8010-touch-clock.yaml +@@ -4,18 +4,20 @@ + $id: http://devicetree.org/schemas/clock/apple,t8010-touch-clock.yaml# + $schema: http://devicetree.org/meta-schemas/core.yaml# + +-title: Apple T8010 touchscreen clock controller ++title: Apple T7000/T8010 touchscreen clock controller + + maintainers: + - Paul Praschl + + description: +- The T8010 PMGR contains a gate and integer divider for the 24 MHz clock +- consumed by the touchscreen controller. ++ The T7000 and T8010 PMGR contain a gate and integer divider for the ++ reference clock consumed by the touchscreen controller. + + properties: + compatible: +- const: apple,t8010-touch-clock ++ enum: ++ - apple,t7000-touch-clock ++ - apple,t8010-touch-clock + + reg: + maxItems: 1 +diff --git a/Documentation/devicetree/bindings/regulator/apple,pmic-touch-regulator.yaml b/Documentation/devicetree/bindings/regulator/apple,pmic-touch-regulator.yaml +--- a/Documentation/devicetree/bindings/regulator/apple,pmic-touch-regulator.yaml ++++ b/Documentation/devicetree/bindings/regulator/apple,pmic-touch-regulator.yaml +@@ -10,7 +10,7 @@ + - Paul Praschl + + description: +- Fixed touchscreen supply switches provided by the Adelyn and Chestnut ++ Fixed touchscreen supply switches provided by the D2186, Adelyn and Chestnut + PMICs. The compatible identifies both the PMIC and the physical supply; + register layout is an implementation detail of the regulator driver. + +@@ -22,6 +22,8 @@ + enum: + - apple,adelyn-touch-core-regulator + - apple,chestnut-touch-hv-regulator ++ - apple,d2186-touch-core-regulator ++ - apple,chestnut-touch-ldo-regulator + + required: + - compatible +diff --git a/arch/arm64/boot/dts/apple/t7000-pmgr.dtsi b/arch/arm64/boot/dts/apple/t7000-pmgr.dtsi +--- a/arch/arm64/boot/dts/apple/t7000-pmgr.dtsi ++++ b/arch/arm64/boot/dts/apple/t7000-pmgr.dtsi +@@ -5,6 +5,16 @@ + * Copyright (c) 2024, Nick Chan + */ + &pmgr { ++ touchclk: clock-controller@1e000 { ++ compatible = "apple,t7000-touch-clock"; ++ reg = <0x1e000 0x4>; ++ #clock-cells = <0>; ++ clocks = <&clkref>; ++ clock-output-names = "touchclk"; ++ clock-frequency = <32768>; ++ status = "disabled"; ++ }; ++ + ps_cpu0: power-controller@20000 { + compatible = "apple,t7000-pmgr-pwrstate", "apple,pmgr-pwrstate"; + reg = <0x20000 4>; +diff --git a/arch/arm64/boot/dts/apple/t7000-n61.dts b/arch/arm64/boot/dts/apple/t7000-n61.dts +--- a/arch/arm64/boot/dts/apple/t7000-n61.dts ++++ b/arch/arm64/boot/dts/apple/t7000-n61.dts +@@ -14,10 +14,50 @@ + + aliases { + spi2 = &spi2; ++ touchscreen0 = &touchscreen; + }; + }; + + &spi2 { + cs-gpios = <&pinctrl 185 GPIO_ACTIVE_LOW>; + status = "okay"; ++ ++ touchscreen: touchscreen@0 { ++ compatible = "apple,n61-touchscreen"; ++ reg = <0>; ++ spi-max-frequency = <500000>; ++ interrupts-extended = <&pinctrl 3 IRQ_TYPE_EDGE_FALLING>; ++ reset-gpios = <&pinctrl 2 GPIO_ACTIVE_LOW>; ++ ana-supply = <&touch_ana>; ++ core-supply = <&touch_core>; ++ clocks = <&touchclk>; ++ firmware-name = "apple/n61-fwstart.z2fw"; ++ touchscreen-size-x = <750>; ++ touchscreen-size-y = <1334>; ++ /* Physical taps and swipes passed the experimental N61 NFS boot. */ ++ status = "okay"; ++ }; + }; ++ ++&touchclk { ++ status = "okay"; ++}; ++ ++&i2c0 { ++ pmic@74 { ++ touch_core: regulator-touch-core { ++ compatible = "apple,d2186-touch-core-regulator"; ++ regulator-name = "n61-touch-core"; ++ }; ++ }; ++ ++ display-pmu@27 { ++ compatible = "apple,chestnut-pmic"; ++ reg = <0x27>; ++ ++ touch_ana: regulator-touch-ana { ++ compatible = "apple,chestnut-touch-ldo-regulator"; ++ regulator-name = "n61-touch-ana"; ++ }; ++ }; ++}; diff --git a/kernel/patches/devices/t7000/README.md b/kernel/patches/devices/t7000/README.md new file mode 100644 index 00000000..74928b5e --- /dev/null +++ b/kernel/patches/devices/t7000/README.md @@ -0,0 +1,53 @@ +# N61 touch development + +The T7000 patches describe SPI2 at `0x20a088000` with the legacy PIO backend, +a 60 MHz source, AIC interrupt 154 and the existing `ps_spi2` power domain. +Its SPI version 1 configuration starts at `0x4018` for N61 mode 0 and 8-bit +words, then becomes `0x4038` during PIO. T7000 uses its own word-size index +shifted by 15 and skips the T8010 descriptor path. The T8010 configuration +and branch selection keep their existing behavior. +N61 uses GPIO185 for SPI chip select while GPIO2 holds touch reset. Per-unit +touch and proximity calibration pass through the native m1n1 touchscreen +alias. The firmware recipe builds `apple/n61-fwstart.z2fw` from the pinned +N61 `C1F59,1` data-form firmware in iOS 12.5.1 build 16H22. + +The native clock driver reuses the PMGR gate/divider operations at T7000 +offset `0x1e000`. It divides the 24 MHz source by 732 for the 32768 Hz +target. D2186 supplies the core through register `0x0085` bit `0x02`. +Chestnut register `0x05` bit `0x10` controls the analog supply; its enable +prepares shared boost/Sage bits `0x07`. Disabling touch preserves those +shared display bits and every unrelated bit in each register. + +The N61 driver enables analog power before core power and keeps reset +asserted through clock startup. After the 15 ms clock wait, it exchanges +`19c1`, waits 10 ms and sends the 16-byte CHECK preflight before releasing +reset. A bounded synchronous CHECK validates both ROM words before upload; +runtime IRQs stay masked until report setup finishes. The firmware stream +preserves 8-bit wire order and holds chip select +across 16 KiB chunks. Provider 0 supplies required touch calibration; +optional proximity uses appended provider 9. Runtime long reads retain the +N61 phase-0 transmit checksum in the phase-1 E6/E7 request. +EB includes phase 1 in its request checksum and writes that checksum at +the complete transfer's tail. A checksum-valid empty phase-0 read advances +the N61 request tag. + +The exact Apple T7000 kernel used to trace the clock, PMIC and startup +operations has SHA256 +`0145bc0eda01cd55e8b996d24ffc19c25712714326f893ba5085a5016d30a4ad`. +The Gen1 firmware container follows the +[pinned Corellium implementation](https://github.com/corellium/projectsandcastle/tree/03db9c6ae04141eb940f3b9f56d446f50d57fadf/hx-touchd), +with the N61 clock values and startup behavior taken from that exact Apple +kernel. Private ADT/calibration bytes stay outside these patches. + +Native N61 startup, firmware upload and touch reports passed physical +qualification during one experimental USB/NFS-rooted Arch session. Taps +and swipes produced evdev contact/release events and reached the compositor. +The N61 touch child is enabled with `ana-supply`, `core-supply` and `clocks`. +The separately gated preserved-power path requires a prior ROM response +and excludes those resource references. Repeated cold boots and return to +iOS await qualification; installation remains disabled. + +`tests/test_n61_spi.py` checks the board/controller contract. +`tests/test_n61_touch.py` compiles the native packet and regulator code with +synthetic data to check wire bytes, transfer bounds, failed transfers and +the shared display bits across every possible starting register byte. diff --git a/kernel/write-receipt.py b/kernel/write-receipt.py new file mode 100644 index 00000000..f7ced85e --- /dev/null +++ b/kernel/write-receipt.py @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 +"""Bind a kernel image, its device trees and module tree to build inputs.""" +import argparse +import hashlib +import json +from pathlib import Path, PurePosixPath +import re +try: + import tomllib +except ImportError: + import tomli as tomllib + + +def digest(path): + if path.is_symlink() or not path.is_file(): + raise ValueError('Expected a regular file: ' + str(path)) + sha = hashlib.sha256() + with path.open('rb') as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b''): + sha.update(chunk) + return {'bytes': path.stat().st_size, 'sha256': sha.hexdigest()} + + +def recipe_snapshot(root, device, platform, patch_list): + """Describe the exact public hardware inputs used by this kernel build.""" + root = Path(root) + paths = { + '__init__.py', 'buildenv.py', 'kernel/Containerfile', 'kernel/install-toolchain', + 'kernel/build', 'kernel/build-modules', 'kernel/stage-modules.py', + 'kernel/write-receipt.py', 'kernel/upstream.lock', 'kernel/sources.toml', + 'kernel/series.py', 'kernel/series/' + platform, + 'kernel/configs/' + platform + '/config', + } + descriptor = root / 'devices' / device / 'device.toml' + if descriptor.is_file(): + paths.add('devices/' + device + '/device.toml') + firmware_name = tomllib.loads(descriptor.read_text()).get('firmware', {}).get('recipe') + if firmware_name: + firmware_path = PurePosixPath(firmware_name) + if (firmware_path.is_absolute() or '..' in firmware_path.parts or + '\\' in firmware_name or not firmware_path.parts): + raise ValueError('Invalid firmware recipe path') + paths.add('devices/' + device + '/' + firmware_path.as_posix()) + for profile in {platform, device}: + module_dir = root / 'kernel/modules' / profile + if module_dir.exists(): + for path in module_dir.rglob('*'): + if path.is_symlink(): + raise ValueError('Linked recipe resource refused: ' + str(path)) + if path.is_file(): + paths.add(path.relative_to(root).as_posix()) + for line in Path(patch_list).read_text().splitlines(): + blob, separator, relative = line.partition(' ') + path = PurePosixPath(relative) + if (not separator or not re.fullmatch(r'[0-9a-f]{40}', blob) or + path.is_absolute() or '..' in path.parts or not path.parts): + raise ValueError('Invalid applied patch entry') + full = root / 'kernel' / Path(*path.parts) + if full.is_symlink() or not full.is_file(): + raise ValueError('Missing or linked patch: ' + relative) + contents = full.read_bytes() + actual = hashlib.sha1(b'blob ' + str(len(contents)).encode() + b'\0' + contents).hexdigest() + if actual != blob: + raise ValueError('Patch changed during build: ' + relative) + paths.add('kernel/' + relative) + files = {name: digest(root / name) for name in sorted(paths)} + encoded = json.dumps(files, sort_keys=True, separators=(',', ':')).encode() + return {'sha256': hashlib.sha256(encoded).hexdigest(), 'files': files} + + +def write_receipt(directory, device, platform, dtb, source_commit, source_tree, + prepared_tree, patch_list, config, recipe_root, recipe_file): + if not re.fullmatch(r'[a-z0-9][a-z0-9_-]*', device): + raise ValueError('Invalid device or platform name') + if not re.fullmatch(r'[a-z0-9][a-z0-9_-]*', platform): + raise ValueError('Invalid platform name') + if dtb != '-' and not re.fullmatch(r'[a-z0-9][a-z0-9_.-]*', dtb): + raise ValueError('Invalid DTB name') + for value in (source_commit, source_tree, prepared_tree): + if not re.fullmatch(r'[0-9a-f]{40}', value): + raise ValueError('Invalid source identity') + directory = Path(directory) + output = directory / 'receipt.json' + if output.exists() or output.is_symlink(): + raise ValueError('Receipt already exists') + if directory.is_symlink() or not directory.is_dir(): + raise ValueError('Expected a kernel output directory') + release_path = directory / 'kernel.release' + digest(release_path) + release = release_path.read_text().strip() + if not release.endswith('-orchard-' + platform) or '/' in release: + raise ValueError('Kernel release does not match the platform') + module_dir = directory / 'lib/modules' / release + if not module_dir.is_dir() or module_dir.is_symlink(): + raise ValueError('Missing kernel module tree') + if not any(module_dir.rglob('*.ko')): + raise ValueError('Missing kernel modules') + required = ['Image.gz', 'kernel.release'] + if dtb != '-': + required += [device + '.dtb', 'dtbs/' + dtb + '.dtb'] + if digest(directory / required[-2]) != digest(directory / required[-1]): + raise ValueError('Device DTB does not match the selected platform DTB') + files = {} + for name in required: + digest(directory / name) + captured = json.loads(Path(recipe_file).read_text()) + if captured != recipe_snapshot(recipe_root, device, platform, patch_list): + raise ValueError('Hardware recipe changed during build') + for path in sorted(directory.rglob('*')): + if path.is_symlink(): + raise ValueError('Linked kernel output refused: ' + str(path)) + if path.is_file(): + files[path.relative_to(directory).as_posix()] = digest(path) + elif not path.is_dir(): + raise ValueError('Unsupported kernel output: ' + str(path)) + receipt = { + 'schema': 1, + 'device': device if dtb != '-' else None, + 'platform': platform, + 'dtb': dtb if dtb != '-' else None, + 'kernel_release': release, + 'source': { + 'commit': source_commit, + 'tree': source_tree, + 'prepared_tree': prepared_tree, + 'patch_list_sha256': digest(Path(patch_list))['sha256'], + 'config_sha256': digest(Path(config))['sha256'], + }, + 'hardware_recipe': captured, + 'files': files, + } + with output.open('x') as stream: + json.dump(receipt, stream, indent=2, sort_keys=True) + stream.write('\n') + return receipt + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--capture', action='store_true') + parser.add_argument('--directory', type=Path) + parser.add_argument('--device', required=True) + parser.add_argument('--platform', required=True) + parser.add_argument('--dtb') + parser.add_argument('--source-commit') + parser.add_argument('--source-tree') + parser.add_argument('--prepared-tree') + parser.add_argument('--patch-list', type=Path, required=True) + parser.add_argument('--config', type=Path) + parser.add_argument('--recipe-root', type=Path, default=Path(__file__).resolve().parent.parent) + parser.add_argument('--recipe-file', type=Path, required=True) + args = parser.parse_args() + if args.capture: + captured = recipe_snapshot(args.recipe_root, args.device, args.platform, args.patch_list) + args.recipe_file.write_text(json.dumps(captured, indent=2, sort_keys=True) + '\n') + return + if not all((args.directory, args.dtb, args.source_commit, args.source_tree, + args.prepared_tree, args.config)): + parser.error('output, device tree, source and configuration are required') + write_receipt(args.directory, args.device, args.platform, args.dtb, + args.source_commit, args.source_tree, args.prepared_tree, + args.patch_list, args.config, args.recipe_root, args.recipe_file) + + +if __name__ == '__main__': + main() diff --git a/pyproject.toml b/pyproject.toml index e012081b..9b580025 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ Issues = "https://github.com/OrchardLinux/hardware/issues" [tool.setuptools] include-package-data = false -packages = ["orchard_hardware", "orchard_hardware.image", "orchard_hardware.apple", "orchard_hardware.boot", "orchard_hardware.boot.initramfs"] +packages = ["orchard_hardware", "orchard_hardware.image", "orchard_hardware.apple", "orchard_hardware.boot", "orchard_hardware.boot.initramfs", "orchard_hardware.boot.initramfs.nfs"] package-dir = {orchard_hardware = "."} [tool.setuptools.package-data] @@ -32,10 +32,14 @@ orchard_hardware = [ "boot/initramfs/recovery/capture-adt", "boot/initramfs/recovery/udhcpd.conf", "boot/initramfs/recovery/upstream.lock", "boot/initramfs/recovery/build-tools*", "boot/initramfs/recovery/install-toolchain", + "boot/initramfs/nfs/init", "boot/initramfs/nfs/usb-gadget", "boot/initramfs/nfs/handoff", + "boot/initramfs/nfs/validate-root", "boot/initramfs/nfs/acm-rescue", "boot/initramfs/nfs/README.md", "boot/initramfs/busybox/*.py", "boot/initramfs/busybox/Containerfile", "boot/quiet/*.py", "boot/quiet/*.patch", "boot/m1n1/build", "boot/m1n1/install-toolchain", "boot/m1n1/Containerfile", "boot/m1n1/upstream.lock", "boot/m1n1/series", "boot/m1n1/sources.toml", "boot/m1n1/patches/*/*.patch", - "kernel/Containerfile", "kernel/install-toolchain", "kernel/build", "kernel/build-modules", "kernel/stage-modules.py", - "kernel/upstream.lock", "kernel/configs/*/*", "kernel/devicetree/*/*", - "kernel/patches/*/*.patch", "kernel/modules/*/*/*" + "kernel/Containerfile", "kernel/install-toolchain", "kernel/build", "kernel/build-modules", "kernel/stage-modules.py", "kernel/write-receipt.py", + "kernel/upstream.lock", "kernel/sources.toml", "kernel/series.py", "kernel/series/*", + "kernel/configs/*/*", "kernel/devicetree/*/*", + "kernel/patches/*/*.patch", "kernel/patches/*/*/*.patch", "kernel/modules/*/*/*", + "docs/bring-up/*.md" ] diff --git a/tests/test_ci_efficiency.py b/tests/test_ci_efficiency.py index 5b7fe4eb..bca28e4d 100644 --- a/tests/test_ci_efficiency.py +++ b/tests/test_ci_efficiency.py @@ -37,6 +37,17 @@ def test_apple_build_has_exact_input_cache_and_one_compiler_invocation(self): self.assertIn("hashFiles('boot/quiet/build-pongo.py', 'boot/quiet/pongo.patch')", body) self.assertIn('xcrun --sdk iphoneos --show-sdk-build-version', body) + def test_n61_kernel_check_is_opt_in_and_separate_from_release_assets(self): + workflow = (ROOT / '.github/workflows/build.yml').read_text() + body = job(workflow, 'kernel-n61-check') + self.assertIn("github.event_name == 'workflow_dispatch' && inputs.verify_n61_kernel == true", body) + self.assertRegex(workflow, r'verify_n61_kernel:\n(?:.*\n){3}\s+default: false') + self.assertIn('timeout-minutes: 360', body) + self.assertIn('bash kernel/install-toolchain', body) + self.assertIn('bash kernel/build n61', body) + self.assertIn('name: n61-kernel-check', body) + self.assertIn('pattern: hardware-*', job(workflow, 'assets')) + def test_shared_toolchain_package_lists_can_be_queried_without_installing(self): recipes = [('kernel/install-toolchain', 'kmod'), ('boot/m1n1/install-toolchain', 'clang-19'), diff --git a/tests/test_kernel_cache_transition.py b/tests/test_kernel_cache_transition.py new file mode 100644 index 00000000..3156fa86 --- /dev/null +++ b/tests/test_kernel_cache_transition.py @@ -0,0 +1,101 @@ +"""Exercise incremental kernel cache transitions with a small Git fixture.""" +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class KernelCacheTransition(unittest.TestCase): + def setUp(self): + if shutil.which('git') is None or shutil.which('bash') is None: + self.skipTest('Git and bash are required') + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + root = Path(self.temporary.name) + self.hardware = root / 'hardware' + kernel = self.hardware / 'kernel' + kernel.mkdir(parents=True) + shutil.copyfile(ROOT / 'kernel/build', kernel / 'build') + shutil.copyfile(ROOT / 'kernel/series.py', kernel / 'series.py') + (kernel / 'write-receipt.py').write_text('raise SystemExit(42)\n') + config = kernel / 'configs/apple-4k/config' + config.parent.mkdir(parents=True) + config.write_text('config\n') + series = kernel / 'series/apple-4k' + series.parent.mkdir() + series.write_text('patches/devices\n') + descriptor = self.hardware / 'devices/n61/device.toml' + descriptor.parent.mkdir(parents=True) + descriptor.write_text('[kernel]\nplatform = "apple-4k"\ndtb = "t7000-n61"\n') + self.patch = kernel / 'patches/devices/0001.patch' + self.patch.parent.mkdir(parents=True) + self.patch.write_text(self.diff('old')) + + self.work = root / 'work' + self.source = self.work / 'source' + self.source.mkdir(parents=True) + self.git('init', '-q') + self.git('remote', 'add', 'origin', 'https://github.com/torvalds/linux.git') + (self.source / 'driver.txt').write_text('base\n') + (self.source / 'editable.txt').write_text('original\n') + self.git('add', '.') + self.git('-c', 'user.name=Orchard test', '-c', 'user.email=test@example.test', + 'commit', '-qm', 'kernel fixture') + commit = self.git('rev-parse', 'HEAD').strip() + (kernel / 'upstream.lock').write_text( + 'url=https://github.com/torvalds/linux.git\ncommit=' + commit + '\n') + self.git('apply', str(self.patch)) + self.git('add', '.') + tree = self.git('write-tree').strip() + self.git('reset', '-q', 'HEAD') + (self.work / 'prepared-tree').write_text(tree + '\n') + blob = subprocess.check_output(['git', 'hash-object', str(self.patch)], text=True).strip() + (self.work / 'applied-patches').write_text(blob + ' patches/devices/0001.patch\n') + self.output = self.work / 'output' + self.output.mkdir() + (self.output / 'compiler-output.o').write_bytes(b'original compiler output') + + @staticmethod + def diff(value): + return ('diff --git a/driver.txt b/driver.txt\n' + '--- a/driver.txt\n+++ b/driver.txt\n@@ -1 +1 @@\n' + '-base\n+' + value + '\n') + + def git(self, *args): + return subprocess.check_output(['git', '-C', str(self.source), *args], text=True) + + def run_build(self): + env = dict(os.environ, ORCHARD_BUILD_NATIVE='1') + return subprocess.run(['bash', str(self.hardware / 'kernel/build'), 'n61', + '--work-dir', str(self.work), '--output', + str(self.hardware / 'result')], env=env, + capture_output=True, text=True) + + def test_patch_change_keeps_other_source_and_compiler_output(self): + self.patch.write_text(self.diff('new')) + result = self.run_build() + # The fake receipt writer stops the test after the cache transition. + self.assertNotEqual(result.returncode, 0) + self.assertIn('applying only the difference', result.stdout) + self.assertEqual((self.source / 'driver.txt').read_text(), 'new\n') + self.assertEqual((self.source / 'editable.txt').read_text(), 'original\n') + self.assertEqual((self.output / 'compiler-output.o').read_bytes(), b'original compiler output') + + def test_patch_change_preserves_hand_edits_and_refuses_transition(self): + self.patch.write_text(self.diff('new')) + (self.source / 'editable.txt').write_text('manual change\n') + result = self.run_build() + self.assertNotEqual(result.returncode, 0) + self.assertIn('changes that are not in the patch series', result.stderr) + self.assertEqual((self.source / 'driver.txt').read_text(), 'old\n') + self.assertEqual((self.source / 'editable.txt').read_text(), 'manual change\n') + self.assertEqual((self.output / 'compiler-output.o').read_bytes(), b'original compiler output') + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_kernel_receipt.py b/tests/test_kernel_receipt.py new file mode 100644 index 00000000..906d03cd --- /dev/null +++ b/tests/test_kernel_receipt.py @@ -0,0 +1,111 @@ +"""The output receipt binds the image, device tree and complete module tree.""" +import hashlib +import importlib.util +import json +from pathlib import Path +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('kernel_receipt', ROOT / 'kernel/write-receipt.py') +producer = importlib.util.module_from_spec(spec) +spec.loader.exec_module(producer) + + +class KernelReceipt(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + self.root = Path(self.temporary.name) + self.output = self.root / 'kernel' + self.release = '7.3.0-rc1-orchard-apple-4k' + self.files = { + 'Image.gz': b'image', + 'kernel.release': (self.release + '\n').encode(), + 'n61.dtb': b'device tree', + 'dtbs/t7000-n61.dtb': b'device tree', + 'dtbs/t7001-j81.dtb': b'other device tree', + f'lib/modules/{self.release}/kernel/driver.ko': b'module', + f'lib/modules/{self.release}/modules.dep': b'kernel/driver.ko:\n', + } + for name, contents in self.files.items(): + path = self.output / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(contents) + self.patch_list = self.root / 'applied-patches' + self.recipe = self.root / 'hardware' + for name in ('__init__.py', 'buildenv.py', 'kernel/Containerfile', + 'kernel/install-toolchain', 'kernel/build', 'kernel/build-modules', + 'kernel/stage-modules.py', 'kernel/write-receipt.py', + 'kernel/upstream.lock', 'kernel/sources.toml', 'kernel/series.py', + 'kernel/series/apple-4k', 'devices/n61/firmware.json'): + path = self.recipe / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(name) + descriptor = self.recipe / 'devices/n61/device.toml' + descriptor.write_text('[firmware]\nrecipe = "firmware.json"\n') + self.device_module = self.recipe / 'kernel/modules/n61/of-overlay/Makefile' + self.device_module.parent.mkdir(parents=True) + self.device_module.write_text('obj-m += overlay.o\n') + patch = self.recipe / 'kernel/patches/test.patch' + patch.parent.mkdir(parents=True, exist_ok=True) + patch.write_bytes(b'example patch\n') + blob = hashlib.sha1(b'blob ' + str(patch.stat().st_size).encode() + b'\0' + patch.read_bytes()).hexdigest() + self.patch_list.write_text(blob + ' patches/test.patch\n') + self.config = self.recipe / 'kernel/configs/apple-4k/config' + self.config.parent.mkdir(parents=True, exist_ok=True) + self.config.write_text('CONFIG_ARM64_4K_PAGES=y\n') + self.recipe_file = self.root / 'recipe.json' + snapshot = producer.recipe_snapshot(self.recipe, 'n61', 'apple-4k', self.patch_list) + self.recipe_file.write_text(json.dumps(snapshot)) + + def produce(self, **overrides): + args = dict(directory=self.output, device='n61', platform='apple-4k', + dtb='t7000-n61', source_commit='a' * 40, source_tree='b' * 40, + prepared_tree='c' * 40, patch_list=self.patch_list, config=self.config, + recipe_root=self.recipe, recipe_file=self.recipe_file) + args.update(overrides) + return producer.write_receipt(**args) + + def test_receipt_covers_each_output_and_source_state(self): + receipt = self.produce() + self.assertEqual(json.loads((self.output / 'receipt.json').read_text()), receipt) + self.assertEqual((receipt['device'], receipt['platform'], receipt['dtb']), + ('n61', 'apple-4k', 't7000-n61')) + self.assertEqual(receipt['kernel_release'], self.release) + self.assertEqual(receipt['source']['tree'], 'b' * 40) + self.assertEqual(receipt['source']['prepared_tree'], 'c' * 40) + self.assertIn('devices/n61/firmware.json', receipt['hardware_recipe']['files']) + self.assertIn('kernel/patches/test.patch', receipt['hardware_recipe']['files']) + self.assertIn('kernel/modules/n61/of-overlay/Makefile', receipt['hardware_recipe']['files']) + self.assertEqual(set(receipt['files']), set(self.files)) + for name, contents in self.files.items(): + with self.subTest(name=name): + self.assertEqual(receipt['files'][name], { + 'bytes': len(contents), 'sha256': hashlib.sha256(contents).hexdigest()}) + + def test_rejects_mismatched_platform_or_device_tree(self): + with self.assertRaisesRegex(ValueError, 'platform'): + self.produce(platform='apple-16k') + (self.output / 'n61.dtb').write_bytes(b'different') + with self.assertRaisesRegex(ValueError, 'Device DTB'): + self.produce() + + def test_rejects_missing_modules_and_linked_files(self): + (self.output / f'lib/modules/{self.release}/kernel/driver.ko').unlink() + with self.assertRaisesRegex(ValueError, 'Missing kernel modules'): + self.produce() + module = self.output / f'lib/modules/{self.release}/kernel/driver.ko' + module.symlink_to(self.output / 'Image.gz') + with self.assertRaisesRegex(ValueError, 'Linked kernel output'): + self.produce() + + def test_rejects_recipe_changes_after_capture(self): + self.device_module.write_text('obj-m += different.o\n') + with self.assertRaisesRegex(ValueError, 'Hardware recipe changed'): + self.produce() + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_m1n1_resume.py b/tests/test_m1n1_resume.py new file mode 100644 index 00000000..2781a555 --- /dev/null +++ b/tests/test_m1n1_resume.py @@ -0,0 +1,131 @@ +"""Verify an overlapping loader patch stack without changing its checkout.""" +import difflib +import json +import os +from pathlib import Path +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] + + +class M1n1Resume(unittest.TestCase): + def setUp(self): + if shutil.which('git') is None or shutil.which('bash') is None: + self.skipTest('Git and Bash are required for loader resume checks') + temporary = tempfile.TemporaryDirectory(prefix='orchard-m1n1-resume-test-') + self.addCleanup(temporary.cleanup) + self.root = Path(temporary.name) + self.source = self.root / 'work/source' + (self.source / 'src').mkdir(parents=True) + self.env = dict(os.environ, GIT_CONFIG_GLOBAL=os.devnull, + GIT_CONFIG_NOSYSTEM='1', GIT_AUTHOR_NAME='Orchard test', + GIT_AUTHOR_EMAIL='test@example.invalid', + GIT_COMMITTER_NAME='Orchard test', + GIT_COMMITTER_EMAIL='test@example.invalid') + self.git('init', '--quiet') + base = 'before\ncalibration = touch;\nafter\n' + first = 'before\ncalibration = touch_blob;\nafter\n' + second = 'before\ncalibration = touch_and_prox_blobs;\nafter\n' + (self.source / 'src/kboot.c').write_text(base) + (self.source / 'tracked.txt').write_text('committed\n') + (self.source / '.gitignore').write_text('build/\n') + self.git('add', '--all') + self.git('commit', '--quiet', '-m', 'Fixture upstream') + commit = self.git('rev-parse', 'HEAD').stdout.strip() + loader = self.root / 'loader' + patches = loader / 'patches' + (patches / 'hoolock').mkdir(parents=True) + (patches / 'orchard').mkdir() + (loader / 'series').write_text('patches/hoolock\npatches/orchard\n') + self.build = loader / 'build' + shutil.copyfile(ROOT / 'boot/m1n1/build', self.build) + (loader / 'upstream.lock').write_text( + 'url=https://example.invalid/unused.git\ncommit=' + commit + '\n') + self.first_patch = patches / 'hoolock/0001-calibration.patch' + self.first_patch.write_text(''.join(difflib.unified_diff( + base.splitlines(True), first.splitlines(True), + fromfile='a/src/kboot.c', tofile='b/src/kboot.c')) + + ''.join(difflib.unified_diff([], ['touch calibration\n'], + fromfile='/dev/null', tofile='b/src/calibration.c'))) + second_patch = patches / 'orchard/0002-proximity.patch' + second_patch.write_text(''.join(difflib.unified_diff( + first.splitlines(True), second.splitlines(True), + fromfile='a/src/kboot.c', tofile='b/src/kboot.c'))) + for patch in (self.first_patch, second_patch): + self.git('apply', str(patch)) + applied = [] + for patch in (self.first_patch, second_patch): + blob = subprocess.check_output(['git', 'hash-object', str(patch)], text=True).strip() + applied.append(blob + ' ' + patch.relative_to(loader).as_posix() + '\n') + (self.source.parent / 'applied-patches').write_text(''.join(applied)) + + # The real index deliberately differs from both HEAD and the work file. + (self.source / 'tracked.txt').write_text('staged\n') + self.git('add', 'tracked.txt') + (self.source / 'tracked.txt').write_text('working\n') + (self.source / 'untracked.txt').write_text('keep this work\n') + (self.source / 'build').mkdir() + (self.source / 'build/m1n1.bin').write_bytes(b'fixture loader\0') + self.output = self.root / 'out/m1n1.bin' + self.log = self.root / 'buildenv.jsonl' + temporary_indexes = self.root / 'temporary-indexes' + temporary_indexes.mkdir() + mocks = self.root / 'mocks' + package = mocks / 'orchard_hardware' + package.mkdir(parents=True) + (package / '__init__.py').write_text('') + (package / 'buildenv.py').write_text( + 'import json, os, sys\n' + 'with open(os.environ["ORCHARD_TEST_BUILDENV_LOG"], "a") as log:\n' + ' log.write(json.dumps(sys.argv[1:]) + "\\n")\n') + self.env.update(PYTHONPATH=str(mocks), TMPDIR=str(temporary_indexes), + ORCHARD_TEST_BUILDENV_LOG=str(self.log)) + + def git(self, *args, check=True): + return subprocess.run(['git', '-C', str(self.source), *args], + check=check, capture_output=True, text=True, + env=self.env) + + def snapshot(self): + files = {str(path.relative_to(self.source)): path.read_bytes() + for path in self.source.rglob('*') + if path.is_file() and '.git' not in path.relative_to(self.source).parts} + return (files, (self.source / '.git/index').read_bytes()) + + def resume(self): + return subprocess.run(['bash', str(self.build), 'n61', '--resume', + '--work-dir', str(self.source.parent), + '--output', str(self.output)], + capture_output=True, text=True, env=self.env) + + def test_overlapping_stack_resumes_and_preserves_real_index_and_files(self): + independent = self.git('apply', '--reverse', '--check', str(self.first_patch), check=False) + self.assertNotEqual(independent.returncode, 0) + before = self.snapshot() + completed = self.resume() + self.assertEqual(completed.returncode, 0, completed.stderr) + self.assertEqual(self.snapshot(), before) + self.assertEqual(self.git('show', ':tracked.txt').stdout, 'staged\n') + self.assertEqual(self.output.read_bytes(), b'fixture loader\0') + calls = [json.loads(line) for line in self.log.read_text().splitlines()] + self.assertEqual([call[0] for call in calls], ['build', 'run']) + self.assertEqual(list(Path(self.env['TMPDIR']).glob('orchard-m1n1-resume.*')), []) + + def test_incomplete_earlier_patch_refuses_build_and_discards_temporary_index(self): + # Reversing patch 2 can succeed before patch 1 discovers this loss. + (self.source / 'src/calibration.c').unlink() + before = self.snapshot() + completed = self.resume() + self.assertNotEqual(completed.returncode, 0) + self.assertIn('src/calibration.c', completed.stderr) + self.assertEqual(self.snapshot(), before) + self.assertFalse(self.output.exists()) + self.assertFalse(self.log.exists()) + self.assertEqual(list(Path(self.env['TMPDIR']).glob('orchard-m1n1-resume.*')), []) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_module_sdk_platform.py b/tests/test_module_sdk_platform.py new file mode 100644 index 00000000..9c426a3f --- /dev/null +++ b/tests/test_module_sdk_platform.py @@ -0,0 +1,67 @@ +"""External modules use the SDK for the selected kernel platform.""" +import os +from pathlib import Path +import subprocess +import tempfile +import unittest + + +ROOT = Path(__file__).resolve().parents[1] + + +class ModuleSDKPlatform(unittest.TestCase): + def setUp(self): + self.temporary = tempfile.TemporaryDirectory() + self.addCleanup(self.temporary.cleanup) + root = Path(self.temporary.name) + self.source = root / 'source' + self.source.mkdir() + self.build = root / 'build' + (self.build / 'include/config').mkdir(parents=True) + (self.build / 'Module.symvers').write_text('') + self.release = self.build / 'include/config/kernel.release' + self.config = self.build / '.config' + self.modules = root / 'modules' + (self.modules / 'example').mkdir(parents=True) + (self.modules / 'example/Makefile').write_text('obj-m += example.o\n') + self.output = root / 'external' + mock = root / 'mock/orchard_hardware' + mock.mkdir(parents=True) + (mock / '__init__.py').write_text('') + (mock / 'buildenv.py').write_text('print("fake compiler invoked")\n') + self.env = dict(os.environ, PYTHONPATH=str(root / 'mock')) + + def build_modules(self, suffix): + return subprocess.run(['bash', str(ROOT / 'kernel/build-modules'), + '--source', str(self.source), '--build', str(self.build), + '--modules', str(self.modules), '--localversion', suffix, + '--output', str(self.output)], env=self.env, + capture_output=True, text=True) + + def test_4k_sdk_is_accepted_for_4k_modules(self): + self.release.write_text('7.3.0-rc1-orchard-apple-4k\n') + self.config.write_text('CONFIG_ARM64_4K_PAGES=y\n') + result = self.build_modules('-orchard-apple-4k') + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual((self.output / 'example/Makefile').read_text(), 'obj-m += example.o\n') + + def test_16k_sdk_is_accepted_for_existing_platform_modules(self): + self.release.write_text('7.3.0-rc1-orchard-apple-16k\n') + self.config.write_text('CONFIG_ARM64_16K_PAGES=y\n') + result = self.build_modules('-orchard-apple-16k') + self.assertEqual(result.returncode, 0, result.stderr) + + def test_release_or_page_size_mismatch_stops_before_compilation(self): + for release, config in (('7.3.0-rc1-orchard-apple-16k', 'CONFIG_ARM64_4K_PAGES=y'), + ('7.3.0-rc1-orchard-apple-4k', 'CONFIG_ARM64_16K_PAGES=y')): + with self.subTest(release=release, config=config): + self.release.write_text(release + '\n') + self.config.write_text(config + '\n') + result = self.build_modules('-orchard-apple-4k') + self.assertNotEqual(result.returncode, 0) + self.assertNotIn('fake compiler invoked', result.stdout) + self.assertFalse(self.output.exists()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_n61_data.py b/tests/test_n61_data.py new file mode 100644 index 00000000..7b563dac --- /dev/null +++ b/tests/test_n61_data.py @@ -0,0 +1,145 @@ +"""N61 identity, private touch firmware and experimental descriptor contracts.""" +import importlib.util +import json +from pathlib import Path +import re +import unittest + +HARDWARE = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('n61_hardware_data', HARDWARE / '__init__.py') +data = importlib.util.module_from_spec(spec) +spec.loader.exec_module(data) + + +class N61Data(unittest.TestCase): + def setUp(self): + self.device = data.device('n61') + + def test_board_identity_and_platform(self): + self.assertIn('n61', data.profiles()) + self.assertEqual(self.device['device']['apple_products'], ['iPhone7,2']) + self.assertEqual(self.device['device']['dt_compatible'], ['apple,n61']) + self.assertEqual(self.device['match']['dfu'], {'chip': 0x7000, 'board': 0x06}) + self.assertEqual(self.device['kernel'], {'platform': 'apple-4k', 'dtb': 't7000-n61'}) + self.assertEqual(self.device['boot'], {'methods': ['checkm8-pongo']}) + + def test_observed_ram_nfs_and_touch_keep_installation_experimental(self): + self.assertIs(self.device['support']['install'], False) + self.assertEqual(self.device['support']['status'], 'experimental') + for name in ('usb_serial', 'usb_network'): + self.assertIs(self.device['support'][name], True) + self.assertIs(self.device['support']['wifi'], False) + expected = dict.fromkeys(data.features(), 'missing') + expected.update(display='working', touch='working', battery='untested', backlight='untested', buttons='untested') + self.assertEqual(self.device['features'], expected) + caveats = ' '.join(self.device['support']['caveats']) + self.assertIn('other builds need their own physical checks', caveats) + self.assertIn('PongoOS 2.6.3-e98323f8', caveats) + self.assertIn('Physical taps and swipes worked on both recorded boots', caveats) + self.assertIn('vertical edges and on-screen keyboard entry', caveats) + self.assertIn('Bluetooth, Wi-Fi and internal storage are unqualified', caveats) + + def test_touch_recipe_selects_the_pinned_n61_gen1_profile(self): + path = data.firmware_path('n61') + self.assertEqual(path, HARDWARE / 'devices/n61/firmware.json') + recipe = json.loads(path.read_text()) + self.assertEqual((recipe['schema'], recipe['device']), (2, 'iPhone7,2')) + output, = recipe['outputs'] + self.assertEqual(output['destination'], 'lib/firmware/apple/n61-fwstart.z2fw') + self.assertEqual(output['sha256'], '51d2ef7b25c1d7e323fa703b531223312cb76f7183fbeb425f921dd6e5509294') + self.assertEqual(output['steps'], [{'step': 'z2fw-from-mtprops@3', 'input': 'N61.mtprops', + 'profile': 'C1F59,1', 'packet': 'Z2', 'touch_maximum': 1024, 'prox_maximum': 128, + 'fll_mval': 6099, 'ref_clk_div': 2, 'clk32_clock_enable': 1}]) + self.assertFalse(self.device['support']['install']) + + + +class DescriptorSchema(unittest.TestCase): + def setUp(self): + try: + from jsonschema import Draft202012Validator + except ImportError: + self.skipTest('jsonschema is not a hardware dependency; the CLI validates descriptors') + schema = json.loads((HARDWARE / 'schemas/v1/device.schema.json').read_text()) + Draft202012Validator.check_schema(schema) + self.validator = Draft202012Validator(schema) + + def test_all_packaged_descriptors_validate(self): + for profile in data.profiles(): + with self.subTest(profile=profile): + self.validator.validate(data.device(profile)) + + def test_firmware_is_required_for_every_descriptor(self): + for profile in data.profiles(): + with self.subTest(profile=profile): + candidate = data.device(profile) + del candidate['firmware'] + self.assertFalse(self.validator.is_valid(candidate)) + + def test_firmware_declaration_still_requires_a_recipe(self): + candidate = data.device('n61') + candidate['firmware'] = {} + self.assertFalse(self.validator.is_valid(candidate)) + candidate['firmware'] = {'recipe': ''} + self.assertFalse(self.validator.is_valid(candidate)) + candidate['firmware'] = None + self.assertFalse(self.validator.is_valid(candidate)) + candidate = data.device('j71b') + del candidate['firmware'] + self.assertFalse(self.validator.is_valid(candidate)) + + +class Apple4kKernelConfig(unittest.TestCase): + @staticmethod + def config(platform): + settings = {} + for line in (HARDWARE / 'kernel/configs' / platform / 'config').read_text().splitlines(): + if line.startswith('CONFIG_'): + name, value = line.split('=', 1) + settings[name] = value + else: + disabled = re.fullmatch(r'# (CONFIG_\w+) is not set', line) + if disabled: + settings[disabled[1]] = 'n' + return settings + + def test_a8_granule_and_existing_platform_stay_distinct(self): + for platform, granule, shift in (('apple-4k', '4K', '12'), ('apple-16k', '16K', '14')): + with self.subTest(platform=platform): + settings = self.config(platform) + self.assertEqual(settings['CONFIG_ARM64_' + granule + '_PAGES'], 'y') + self.assertEqual(settings['CONFIG_PAGE_SHIFT'], shift) + for other in {'4K', '16K', '64K'} - {granule}: + self.assertEqual(settings['CONFIG_ARM64_' + other + '_PAGES'], 'n') + + def test_ram_console_drivers_are_built_in(self): + settings = self.config('apple-4k') + for name in ('CONFIG_APPLE_USBCOMPLEX', 'CONFIG_PHY_APPLE_AUSB', 'CONFIG_USB_DWC2', + 'CONFIG_USB_GADGET', 'CONFIG_USB_CONFIGFS', 'CONFIG_USB_CONFIGFS_ACM', + 'CONFIG_DRM_SIMPLEDRM'): + with self.subTest(option=name): + self.assertEqual(settings[name], 'y') + self.assertEqual(settings['CONFIG_MTD_RAW_NAND'], 'n') + self.assertEqual(settings['CONFIG_USB_CONFIGFS_MASS_STORAGE'], 'n') + self.assertEqual(settings['CONFIG_USB_G_MULTI'], 'n') + + def test_nfs_desktop_prerequisites_are_built_in(self): + settings = self.config('apple-4k') + for name in ('CONFIG_NFS_FS', 'CONFIG_NFS_V3', 'CONFIG_NFS_V4', 'CONFIG_FUSE_FS', + 'CONFIG_CGROUP_FREEZER', 'CONFIG_CGROUP_BPF', 'CONFIG_CPUSETS', + 'CONFIG_CFS_BANDWIDTH'): + with self.subTest(option=name): + self.assertEqual(settings[name], 'y') + self.assertEqual(settings['CONFIG_LOCALVERSION_AUTO'], 'n') + + def test_both_platforms_reuse_the_same_ordered_patch_layers(self): + spec = importlib.util.spec_from_file_location('n61_kernel_series', HARDWARE / 'kernel/series.py') + series = importlib.util.module_from_spec(spec) + spec.loader.exec_module(series) + self.assertEqual(series.resolve('n61'), ('apple-4k', 't7000-n61')) + self.assertEqual(series.patches('apple-4k'), series.patches('apple-16k')) + self.assertTrue(series.patches('apple-4k')) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_n61_spi.py b/tests/test_n61_spi.py new file mode 100644 index 00000000..5a9728db --- /dev/null +++ b/tests/test_n61_spi.py @@ -0,0 +1,204 @@ +"""N61 SPI wiring, independent word/mode fields and PIO contracts.""" +import importlib.util +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +PATCHES = ROOT / 'kernel/patches/devices/t7000' + + +def changes(name): + """Return the post-image fragments carried by each unified-diff file.""" + files, current = {}, None + for line in (PATCHES / name).read_text().splitlines(): + if line.startswith('+++ b/'): + current = line[6:] + files[current] = [] + elif current is not None and line[:1] in (' ', '+'): + files[current].append(line[1:]) + return {path: '\n'.join(lines) for path, lines in files.items()} + + +class N61SPI(unittest.TestCase): + def test_binding_selects_legacy_backend_before_the_generic_fallback(self): + files = changes('0001-spi-apple-add-T7000-legacy-controller.patch') + binding = files['Documentation/devicetree/bindings/spi/apple,spi.yaml'] + self.assertIn('apple,t7000-spi', binding) + controller = files['drivers/spi/spi-apple.c'] + matches = dict(re.findall(r'compatible = "([^"]+)".*?data = &(\w+)', controller)) + self.assertEqual(matches['apple,t7000-spi'], 'apple_spi_hw_t7000') + self.assertNotEqual(matches['apple,t7000-spi'], matches['apple,t8010-spi']) + self.assertNotEqual(matches['apple,t7000-spi'], matches['apple,t8103-spi']) + + def test_soc_bus_matches_n61_adt_address_clock_irq_and_power(self): + files = changes('0002-arm64-dts-apple-describe-N61-SPI2.patch') + soc = files['arch/arm64/boot/dts/apple/t7000.dtsi'] + clock = re.search(r'spi2_clk: clock-spi2 \{(.*?)\n\t\};', soc, re.S)[1] + self.assertIn('compatible = "fixed-clock"', clock) + self.assertRegex(clock, r'clock-frequency\s*=\s*<60000000>') + bus = re.search(r'spi2: spi@20a088000 \{(.*?)\n\t\t\};', soc, re.S)[1] + for setting in ('compatible = "apple,t7000-spi", "apple,spi"', + 'reg = <0x2 0x0a088000 0x0 0x4000>', + 'interrupts = ', + 'clocks = <&spi2_clk>', 'power-domains = <&ps_spi2>', + 'status = "disabled"'): + with self.subTest(setting=setting): + self.assertIn(setting, bus) + + def test_controller_layer_adds_cs_without_touch_or_dma_wiring(self): + files = changes('0002-arm64-dts-apple-describe-N61-SPI2.patch') + self.assertEqual(set(files), {'arch/arm64/boot/dts/apple/t7000.dtsi', + 'arch/arm64/boot/dts/apple/t7000-n61.dts'}) + board = files['arch/arm64/boot/dts/apple/t7000-n61.dts'] + bus = re.search(r'&spi2 \{(.*?)\n\};', board, re.S)[1] + self.assertIn('cs-gpios = <&pinctrl 185 GPIO_ACTIVE_LOW>', bus) + self.assertIn('status = "okay"', bus) + self.assertIn('spi2 = &spi2', board) + # Controller wiring is separate from the qualified board power and + # touchscreen layer, so it can be reviewed without driving supplies. + for source in files.values(): + for forbidden in ('dmas =', 'dma-names =', 'touchscreen@', 'reset-gpios =', + 'core-supply =', 'hv-supply =', 'pinmux =', 'firmware-name ='): + with self.subTest(forbidden=forbidden): + self.assertNotIn(forbidden, source) + + def test_shared_native_series_carries_the_ordered_n61_layers(self): + spec = importlib.util.spec_from_file_location('n61_spi_series', ROOT / 'kernel/series.py') + series = importlib.util.module_from_spec(spec) + spec.loader.exec_module(series) + expected = sorted(PATCHES.glob('*.patch')) + for platform in ('apple-4k', 'apple-16k'): + found = series.patches(platform) + self.assertEqual([path for path in found if path.parent == PATCHES], expected) + + +class N61SPIConfig(unittest.TestCase): + @classmethod + def setUpClass(cls): + compiler = shutil.which('cc') or shutil.which('clang') + if compiler is None: + raise unittest.SkipTest('A host C compiler is required for SPI config checks') + cls.temporary = tempfile.TemporaryDirectory() + cls.addClassCleanup(cls.temporary.cleanup) + root = Path(cls.temporary.name) + controller = changes('0001-spi-apple-add-T7000-legacy-controller.patch')['drivers/spi/spi-apple.c'] + + def block(label): + start = controller.index(label) + opening = controller.index('{', start) + depth = 0 + for index in range(opening, len(controller)): + depth += controller[index] == '{' + depth -= controller[index] == '}' + if not depth: + return controller[start:index + 1] + raise ValueError('Incomplete native SPI block') + + start = controller.index('\tuse_descriptor = spi->hw->legacy_descriptor') + end = controller.index('\tif (!spi->hw->legacy_descriptor)', start) + selection = controller[start:end] + program = r''' +#include +#include +#include +#include +typedef uint32_t u32; +#define BIT(n) (1U<<(n)) +#define SPI_CPHA 1 +#define SPI_CPOL 2 +#define APPLE_SPI_CFG_CPHA BIT(1) +#define APPLE_SPI_CFG_CPOL BIT(2) +#define APPLE_SPI_CFG_T7000_BASE 0x4018 +#define APPLE_SPI_T8010_DESC_CFG_DEFAULT 0x2000 +#define APPLE_SPI_CLKDIV 0x30 +#define APPLE_SPI_WORD_DELAY 0x38 +struct spi_device { unsigned mode; }; +struct spi_transfer { unsigned unused; }; +struct apple_spi_hw { + bool legacy_status,legacy_descriptor; unsigned min_clk_div; u32 default_word_delay; +}; +struct apple_spi { const struct apple_spi_hw *hw; u32 word_delay; }; +static unsigned descriptor_calls,old_config_calls,prepared_descriptor; +static bool apple_spi_t8010_uses_descriptor(struct spi_transfer *t,unsigned words) +{ (void)t; descriptor_calls++; return words<=16; } +static u32 apple_spi_t8010_config(struct spi_device *d,unsigned bits,bool descriptor) +{ (void)d; (void)bits; old_config_calls++; return descriptor?0x601f:0x104018; } +static void reg_write(struct apple_spi *s,unsigned reg,u32 value) +{ (void)s; assert((reg==0x30 && value==120)||(reg==0x38 && value==0)); } +static u32 apple_spi_t8010_word_delay(struct apple_spi *s,struct spi_device *d,bool descriptor) +{ (void)d; (void)descriptor; return s->word_delay; } +static void apple_spi_t8010_prepare_xfer(struct apple_spi *s,bool descriptor) +{ (void)s; prepared_descriptor=descriptor; } +''' + program += block('static const struct apple_spi_hw apple_spi_hw_t8010') + ';\n' + program += block('static const struct apple_spi_hw apple_spi_hw_t7000') + ';\n' + program += block('static u32 apple_spi_t7000_config(') + program += r''' +static u32 select_config(struct apple_spi *spi,struct spi_device *device, + struct spi_transfer *t,unsigned words,unsigned bits_per_word) +{ + bool use_descriptor; + u32 cfg,div=120; +''' + program += selection + '\nreturn cfg;\n}\n' + program += r''' +int main(int argc,char **argv) +{ + struct apple_spi spi={&apple_spi_hw_t7000,0}; + struct spi_device device={0}; + struct spi_transfer transfer={0}; + unsigned mode,bits,index; + static const unsigned counts[]={1,2,8,16,17,16384,32768}; + assert(argc==2); + if(!strcmp(argv[1],"t7000")) { + assert(spi.hw->legacy_status && !spi.hw->legacy_descriptor); + for(mode=0;mode<4;mode++) for(bits=8;bits<=16;bits+=8) { + device.mode=mode; + for(index=0;indexlegacy_status && spi.hw->legacy_descriptor); + assert(select_config(&spi,&device,&transfer,16,8)==0x401e); + assert(prepared_descriptor && descriptor_calls==1 && old_config_calls==1); + assert(select_config(&spi,&device,&transfer,17,8)==0x104018); + assert(!prepared_descriptor && descriptor_calls==2 && old_config_calls==2); + return 0; + } + return 2; +} +''' + source = root / 'test.c' + source.write_text(program) + cls.binary = root / 'n61-spi-config' + subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-O2', + str(source), '-o', str(cls.binary)], check=True, + capture_output=True, text=True) + cls.controller = controller + + def test_t7000_mode_word_size_and_every_length_use_its_native_fifo_config(self): + subprocess.run([self.binary, 't7000'], check=True) + + def test_t8010_keeps_its_existing_descriptor_and_fifo_branches(self): + subprocess.run([self.binary, 't8010'], check=True) + + def test_dma_allocation_remains_gated_to_the_descriptor_backend(self): + self.assertIn('if (spi->hw->legacy_descriptor) {\n\t\tctlr->dma_tx =', self.controller) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_n61_touch.py b/tests/test_n61_touch.py new file mode 100644 index 00000000..97ee82d9 --- /dev/null +++ b/tests/test_n61_touch.py @@ -0,0 +1,608 @@ +"""Execute N61 packet and SPI transport code with synthetic calibration.""" +from pathlib import Path +import re +import shutil +import subprocess +import tempfile +import unittest + +ROOT = Path(__file__).resolve().parents[1] +PATCH = ROOT / 'kernel/patches/devices/t7000/0003-input-apple-z2-add-N61-Gen1-support.patch' + + +def postimages(patch=PATCH): + files, current = {}, None + for line in patch.read_text().splitlines(): + if line.startswith('+++ b/'): + current = line[6:] + files[current] = [] + elif current is not None and line[:1] in (' ', '+'): + files[current].append(line[1:]) + return {name: '\n'.join(lines) for name, lines in files.items()} + + +def function(source, label): + start = source.index(label) + opening = source.index('{', start) + depth = 0 + for position in range(opening, len(source)): + depth += source[position] == '{' + depth -= source[position] == '}' + if not depth: + return source[start:position + 1] + raise ValueError('Incomplete C function') + + +class N61TouchPackets(unittest.TestCase): + @classmethod + def setUpClass(cls): + compiler = shutil.which('cc') or shutil.which('clang') + if compiler is None: + raise unittest.SkipTest('A host C compiler is required for N61 wire tests') + cls.temporary = tempfile.TemporaryDirectory() + cls.addClassCleanup(cls.temporary.cleanup) + root = Path(cls.temporary.name) + files = postimages() + (root / 'apple_z2_n61.h').write_text(files['drivers/input/touchscreen/apple_z2_n61.h']) + linux = root / 'linux' + linux.mkdir() + (linux / 'types.h').write_text('#include \n#include \n#include \ntypedef uint8_t u8; typedef uint16_t u16; typedef uint32_t u32;\n') + (linux / 'errno.h').write_text('#include \n') + (linux / 'string.h').write_text('#include \n') + (linux / 'unaligned.h').write_text('static inline void put_unaligned_be16(unsigned n, void *p) { unsigned char *b=p; b[0]=n>>8; b[1]=n; }\nstatic inline void put_unaligned_le16(unsigned n, void *p) { unsigned char *b=p; b[0]=n; b[1]=n>>8; }\nstatic inline unsigned get_unaligned_be16(const void *p) { const unsigned char *b=p; return (b[0]<<8)|b[1]; }\n') + transport = function(files['drivers/input/touchscreen/apple_z2.c'], + 'static int apple_z2_send_n61_blob(') + program = r''' +#include +#include +#include +#include "apple_z2_n61.h" +#define DIV_ROUND_UP(n,d) (((n)+(d)-1)/(d)) +#define min_t(t,a,b) ((t)((a)<(b)?(a):(b))) +#define dev_dbg(...) ((void)0) +struct spi_device { int unused; }; +struct spi_transfer { + const void *tx_buf; void *rx_buf; u32 len; + unsigned bits_per_word, speed_hz, cs_change; +}; +struct apple_z2 { + struct spi_device *spidev; u8 *tx_buf; u8 *rx_buf; + int index_parity; bool runtime_queue_empty, n61; +}; +static bool apple_z2_is_n61(struct apple_z2 *z) { return z->n61; } +static bool apple_z2_uses_gen1_data(struct apple_z2 *z) { (void)z; return true; } +static unsigned calls, first_count, first_total, acknowledgements, fail_call; +static int spi_sync_transfer(struct spi_device *dev, struct spi_transfer *x, unsigned count) +{ + unsigned i, total=0; + (void)dev; + calls++; + for(i=0;i +#include +enum { APPLE_Z2_VARIANT_J71B, APPLE_Z2_VARIANT_N61, APPLE_Z2_VARIANT_OTHER }; +struct apple_z2 { int variant; }; +struct apple_z2_chip_info { + const char *name; int variant; unsigned cs_setup_us, cs_hold_us, cs_inactive_us; +}; +static bool apple_z2_is_j71b(const struct apple_z2 *z) { return z->variant == APPLE_Z2_VARIANT_J71B; } +''' + for label in ('static bool apple_z2_is_n61(', 'static bool apple_z2_is_gen1(', + 'static bool apple_z2_uses_gen1_data('): + code += function(n61, label) + '\n' + code += function(n61, 'static const struct apple_z2_chip_info apple_z2_n61_info') + ';\n' + code += function(j71b, 'static const struct apple_z2_chip_info apple_z2_j71b_info') + ';\n' + code += r''' +int main(void) { + struct apple_z2 j = { APPLE_Z2_VARIANT_J71B }, n = { APPLE_Z2_VARIANT_N61 }; + struct apple_z2 other = { APPLE_Z2_VARIANT_OTHER }; + assert(apple_z2_is_gen1(&j) && !apple_z2_is_gen1(&n)); + assert(apple_z2_uses_gen1_data(&j) && apple_z2_uses_gen1_data(&n)); + assert(!apple_z2_uses_gen1_data(&other)); + assert(apple_z2_j71b_info.cs_setup_us == 1000 && apple_z2_j71b_info.cs_hold_us == 0); + assert(apple_z2_j71b_info.cs_inactive_us == 1000); + assert(apple_z2_n61_info.cs_setup_us == 1000 && apple_z2_n61_info.cs_hold_us == 1000); + assert(apple_z2_n61_info.cs_inactive_us == 1000); + return 0; +} +''' + with tempfile.TemporaryDirectory() as temporary: + source = Path(temporary) / 'shared.c' + binary = Path(temporary) / 'shared' + source.write_text(code) + subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', + str(source), '-o', str(binary)], check=True, capture_output=True) + subprocess.run([binary], check=True) + + +class N61TouchPower(unittest.TestCase): + @classmethod + def setUpClass(cls): + compiler = shutil.which('cc') or shutil.which('clang') + if compiler is None: + raise unittest.SkipTest('A host C compiler is required for N61 power tests') + cls.temporary = tempfile.TemporaryDirectory() + cls.addClassCleanup(cls.temporary.cleanup) + root = Path(cls.temporary.name) + patch = ROOT / 'kernel/patches/devices/t7000/0004-arm64-apple-wire-N61-touch-power.patch' + cls.files = postimages(patch) + regulator = cls.files['drivers/regulator/apple-pmic-touch-regulator.c'] + actual = '\n'.join([ + function(regulator, 'static int apple_chestnut_touch_ldo_enable('), + function(regulator, 'static int apple_chestnut_touch_ldo_is_enabled('), + function(regulator, 'static const struct regulator_ops apple_chestnut_touch_ldo_ops') + ';', + function(regulator, 'static const struct regulator_desc apple_d2186_touch_core') + ';', + function(regulator, 'static const struct regulator_desc apple_chestnut_touch_ldo =') + ';', + ]) + program = r''' +#include +#include +#include +#include +#define THIS_MODULE 0 +#define REGULATOR_VOLTAGE 1 +struct regulator_dev; +struct regulator_ops { + int (*enable)(struct regulator_dev *); + int (*disable)(struct regulator_dev *); + int (*is_enabled)(struct regulator_dev *); +}; +struct regulator_desc { + const char *name; const struct regulator_ops *ops; + int type, owner; unsigned enable_reg, enable_mask, enable_val, disable_val; +}; +struct regmap { unsigned value, calls, fail, reg; }; +struct regulator_dev { struct regmap *map; const struct regulator_desc *desc; }; +static struct regmap *rdev_get_regmap(struct regulator_dev *r) { return r->map; } +static int regmap_update_bits(struct regmap *r, unsigned reg, unsigned mask, unsigned value) +{ + r->calls++; + r->reg=reg; + if(r->calls==r->fail) return -EIO; + r->value=(r->value&~mask)|(value&mask); + return 0; +} +static int regmap_read(struct regmap *r, unsigned reg, unsigned *value) +{ + r->reg=reg; + if(r->fail) return -EIO; + *value=r->value; + return 0; +} +static int regulator_enable_regmap(struct regulator_dev *r) +{ return regmap_update_bits(r->map,r->desc->enable_reg,r->desc->enable_mask,r->desc->enable_val); } +static int regulator_disable_regmap(struct regulator_dev *r) +{ return regmap_update_bits(r->map,r->desc->enable_reg,r->desc->enable_mask,r->desc->disable_val); } +static int regulator_is_enabled_regmap(struct regulator_dev *r) +{ return (r->map->value&r->desc->enable_mask)==r->desc->enable_val; } +static const struct regulator_ops apple_pmic_touch_regulator_ops={ + regulator_enable_regmap,regulator_disable_regmap,regulator_is_enabled_regmap +}; +''' + program += actual + program += r''' +int main(int argc, char **argv) +{ + unsigned initial; + struct regmap map={0}; + struct regulator_dev r={&map,&apple_chestnut_touch_ldo}; + assert(argc==2); + if(!strcmp(argv[1],"chestnut")) { + for(initial=0;initial<256;initial++) { + map=(struct regmap){.value=initial}; + assert(r.desc->ops->enable(&r)==0); + assert(map.value==(initial|0x17) && map.reg==5); + assert(r.desc->ops->is_enabled(&r)==1); + assert(r.desc->ops->disable(&r)==0); + assert(map.value==((initial|7)&~0x10)); + assert(r.desc->ops->is_enabled(&r)==0); + } + map=(struct regmap){.value=0x0f}; + assert(r.desc->ops->enable(&r)==0 && map.value==0x1f); + assert(r.desc->ops->disable(&r)==0 && map.value==0x0f); + return 0; + } + if(!strcmp(argv[1],"failures")) { + map=(struct regmap){.value=0x0f,.fail=1}; + assert(r.desc->ops->enable(&r)==-EIO && map.calls==1 && map.value==0x0f); + map=(struct regmap){.value=0x0f,.fail=2}; + assert(r.desc->ops->enable(&r)==-EIO && map.calls==2 && map.value==0x0f); + map=(struct regmap){.value=0xff,.fail=1}; + assert(r.desc->ops->is_enabled(&r)==-EIO); + return 0; + } + if(!strcmp(argv[1],"core")) { + r.desc=&apple_d2186_touch_core; + for(initial=0;initial<256;initial++) { + map=(struct regmap){.value=initial}; + assert(r.desc->ops->enable(&r)==0 && map.value==(initial|2) && map.reg==0x85); + assert(r.desc->ops->disable(&r)==0 && map.value==(initial&~2U)); + } + return 0; + } + return 2; +} +''' + source = root / 'test.c' + source.write_text(program) + cls.binary = root / 'n61-power-test' + subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-O2', + str(source), '-o', str(cls.binary)], check=True, + capture_output=True, text=True) + + def test_chestnut_preserves_every_unowned_bit_and_lcd_baseline(self): + subprocess.run([self.binary, 'chestnut'], check=True) + + def test_regmap_failures_stop_before_touch_enable(self): + subprocess.run([self.binary, 'failures'], check=True) + + def test_d2186_owns_only_ldo14_at_the_exact_register(self): + subprocess.run([self.binary, 'core'], check=True) + + def test_qualified_n61_uses_explicit_providers_and_enables_touch(self): + board = self.files['arch/arm64/boot/dts/apple/t7000-n61.dts'] + self.assertIn('ana-supply = <&touch_ana>', board) + self.assertIn('core-supply = <&touch_core>', board) + touch = re.search(r'touchscreen: touchscreen@0 \{(.*?)\n\t\};', board, re.S)[1] + self.assertIn('status = "okay"', touch) + self.assertIn('reg = <0x27>', board) + clock = self.files['arch/arm64/boot/dts/apple/t7000-pmgr.dtsi'] + self.assertIn('reg = <0x1e000 0x4>', clock) + self.assertIn('clocks = <&clkref>', clock) + self.assertIn('clock-frequency = <32768>', clock) + self.assertIn('status = "disabled"', clock) + + +class N61Startup(unittest.TestCase): + @classmethod + def setUpClass(cls): + compiler = shutil.which('cc') or shutil.which('clang') + if compiler is None: + raise unittest.SkipTest('A host C compiler is required for N61 startup tests') + cls.temporary = tempfile.TemporaryDirectory() + cls.addClassCleanup(cls.temporary.cleanup) + root = Path(cls.temporary.name) + driver = postimages()['drivers/input/touchscreen/apple_z2.c'] + program = r''' +#include +#include +#include +#include +#include +#include +typedef uint8_t u8; +typedef uint16_t u16; +typedef uint32_t u32; +#define APPLE_Z2_STATE_POWERED 1 +#define APPLE_Z2_STATE_HBPP_READY 2 +#define APPLE_Z2_N61_READY_ATTEMPTS 50 +struct spi_device { int irq; }; +struct apple_z2 { + struct spi_device *spidev; int reset_gpio, boot_irq; + void *hv_supply, *core_supply; + bool n61_boot_power_preserved, platform_powered, hv_enabled, core_enabled; + bool clk_enabled, booted, upload_irq_masked; + u8 *rx_buf; + int protocol_state; +}; +static char events[256]; +static unsigned used, fault, irq_depth, reset_asserted, supply_calls; +static void event(char c) { assert(used=min && max<=min+1000); + if(min==5000) event('5'); + else if(min==15000) event('F'); + else if(min==10000) event('T'); + else assert(0); +} +static int apple_z2_enable_clock(struct apple_z2 *z) +{ + assert(z->hv_enabled && z->core_enabled && reset_asserted); + event('K'); + if(fault==3) return -EIO; + z->clk_enabled=true; + return 0; +} +static void apple_z2_platform_power_off(struct apple_z2 *z) +{ + assert(reset_asserted); + event('O'); + z->platform_powered=false; + if(!z->n61_boot_power_preserved) z->clk_enabled=z->hv_enabled=z->core_enabled=false; +} +static void apple_z2_reset_protocol(struct apple_z2 *z) { z->protocol_state=0; } +static void enable_irq(int irq) { (void)irq; assert(irq_depth==1); irq_depth--; event('E'); } +static u16 get_unaligned_be16(const void *p) { const u8 *b=p; return (b[0]<<8)|b[1]; } +static int apple_z2_send_n61_blob(struct apple_z2 *z, const u8 *data, u32 size, bool init) +{ + unsigned i; + assert(init && (z->clk_enabled||z->n61_boot_power_preserved)); + if(size==2) { + assert(reset_asserted); + assert(data[0]==0x19 && data[1]==0xc1); + event('P'); + return fault==4?-EIO:0; + } + assert(size==16 && data[0]==0x1a && data[1]==0xa1); + for(i=2;i<16;i+=2) assert(data[i]==0x18 && data[i+1]==0xe1); + if(reset_asserted) { event('Q'); return fault==5?-EIO:0; } + event('B'); + if(fault==8) return -EIO; + memset(z->rx_buf,0,16); + if(fault!=6) { z->rx_buf[0]=0x18; z->rx_buf[1]=0xe1; z->rx_buf[2]=0x1a; z->rx_buf[3]=0xa1; } + return 0; +} +static int apple_z2_upload_firmware(struct apple_z2 *z) +{ + assert(irq_depth==1 && z->upload_irq_masked); + event('U'); + if(fault==7) return -EIO; + z->booted=true; + z->upload_irq_masked=false; + enable_irq(z->spidev->irq); + return 0; +} +''' + header = postimages()['drivers/input/touchscreen/apple_z2_n61.h'] + program += function(header, 'static inline bool apple_z2_n61_rom_word_valid(') + program += '\n' + function(header, 'static inline bool apple_z2_n61_rom_ready(') + program += function(driver, 'static int apple_z2_n61_power_on(') + program += '\n' + function(driver, 'static int apple_z2_n61_boot(') + program += r''' +int main(int argc, char **argv) +{ + struct spi_device spi={3}; + u8 rx[16]={0}; + struct apple_z2 z; + unsigned failure; + assert(argc==2); + if(!strcmp(argv[1],"sequence")) { + z=(struct apple_z2){.spidev=&spi,.hv_supply=(void *)1,.core_supply=(void *)2,.rx_buf=rx}; + irq_depth=1; + assert(apple_z2_n61_boot(&z)==0); + assert(!strcmp(events,"RAC5KFPTQrFBUE")); + assert(z.booted && z.platform_powered && !irq_depth && !reset_asserted); + return 0; + } + if(!strcmp(argv[1],"failures")) { + for(failure=1;failure<=8;failure++) { + used=0; events[0]=0; irq_depth=1; fault=failure; + z=(struct apple_z2){.spidev=&spi,.hv_supply=(void *)1,.core_supply=(void *)2,.rx_buf=rx}; + assert(apple_z2_n61_boot(&z)==(failure==6?-ETIMEDOUT:-EIO)); + assert(irq_depth==1 && reset_asserted); + assert(!z.hv_enabled && !z.core_enabled && !z.clk_enabled && !z.booted); + assert(!z.upload_irq_masked && !z.platform_powered && !z.protocol_state); + } + return 0; + } + if(!strcmp(argv[1],"preserved")) { + z=(struct apple_z2){.spidev=&spi,.n61_boot_power_preserved=true,.rx_buf=rx}; + irq_depth=1; + assert(apple_z2_n61_boot(&z)==0 && !supply_calls); + assert(!strcmp(events,"RPTQrFBUE")); + assert(!z.clk_enabled && !z.hv_enabled && !z.core_enabled); + return 0; + } + return 2; +} +''' + source = root / 'test.c' + source.write_text(program) + cls.binary = root / 'n61-startup-test' + subprocess.run([compiler, '-std=c11', '-Wall', '-Wextra', '-Werror', '-O2', + str(source), '-o', str(cls.binary)], check=True, + capture_output=True, text=True) + + def test_apple_power_preamble_and_reset_order(self): + subprocess.run([self.binary, 'sequence'], check=True) + + def test_failure_keeps_reset_asserted_and_balances_irq_masking(self): + subprocess.run([self.binary, 'failures'], check=True) + + def test_preserved_power_does_not_claim_clock_or_supply_references(self): + subprocess.run([self.binary, 'preserved'], check=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_nfs_runtime.py b/tests/test_nfs_runtime.py new file mode 100644 index 00000000..f09ee6c0 --- /dev/null +++ b/tests/test_nfs_runtime.py @@ -0,0 +1,260 @@ +"""Validate host staging and refusal paths without mounts or device access.""" +import copy +import hashlib +import json +import os +from pathlib import Path +import shutil +import struct +import subprocess +import sys +import tempfile +import unittest + +from orchard_hardware.boot.initramfs.nfs import stage + +HERE = Path(stage.__file__).resolve().parent + + +def elf(machine=183, interpreter=False): + blob = bytearray(120) + blob[:6] = b'\x7fELF\x02\x01' + struct.pack_into('> "$TEST_START_LOG"\n') + busybox.chmod(0o755) + start_log, kill_log = self.base / 'starts', self.base / 'signals' + env = dict(os.environ, TEST_START_LOG=str(start_log), TEST_KILL_LOG=str(kill_log)) + mocks = ''' +is_mounted() { return 1; } +# Every PID probe succeeds, including the retired child/zombie case. +kill() { printf '%s\\n' "$*" >> "$TEST_KILL_LOG"; return 0; } +# End the failure loop after any replacement process has recorded its start. +sleep() { wait; exit 0; } +''' + for retired in ('false', 'true'): + start_log.write_text('') + kill_log.write_text('') + script = 'set -eu\nrescue_retired=' + retired + '\n' + mocks + failure + '\nfailed\n' + result = subprocess.run(['/bin/sh', '-c', script], env=env, + capture_output=True, text=True, timeout=5) + with self.subTest(retired=retired): + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(start_log.read_text(), 'started\n' if retired == 'true' else '') + if retired == 'true': + self.assertIn('-KILL 42', kill_log.read_text()) + else: + self.assertEqual(kill_log.read_text().strip(), '-0 42') + + def test_all_runtime_scripts_parse_as_posix_shell(self): + for source in stage.SCRIPTS: + with self.subTest(script=source): + subprocess.run(['sh', '-n', str(HERE / source)], check=True) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_package_archives.py b/tests/test_package_archives.py new file mode 100644 index 00000000..92d6cb50 --- /dev/null +++ b/tests/test_package_archives.py @@ -0,0 +1,117 @@ +"""Archive audits catch missing inputs and private scratch without extracting files.""" +import importlib.util +import io +from pathlib import Path +import stat +import tarfile +import tempfile +import unittest +import zipfile + +ROOT = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('audit_packages', ROOT / 'ci/audit_packages.py') +packages = importlib.util.module_from_spec(spec) +spec.loader.exec_module(packages) + + +class PackageArchives(unittest.TestCase): + def fixture(self, root): + source = root / 'source' + for name in ('__init__.py', 'buildenv.py', 'kernel/Containerfile', + 'kernel/install-toolchain', 'kernel/build', 'kernel/build-modules', + 'kernel/stage-modules.py', 'kernel/write-receipt.py', + 'kernel/series.py', 'kernel/sources.toml', 'kernel/upstream.lock', + 'boot/m1n1/Containerfile', 'boot/m1n1/install-toolchain', + 'boot/m1n1/build', 'boot/m1n1/series', 'boot/m1n1/sources.toml', + 'boot/m1n1/upstream.lock', + 'features.toml', 'image/runtime.py', 'boot/quiet/m1n1.patch', + 'boot/initramfs/recovery/init', + 'devices/n61/device.toml', 'kernel/series/apple-4k', + 'kernel/configs/apple-4k/config', 'kernel/patches/drivers/input/test.patch', + 'boot/m1n1/patches/prox.patch', 'kernel/devicetree/n61/touch.dtso', + 'kernel/modules/n61/of-overlay/helper.c', 'docs/bring-up/n61-touch.md'): + path = source / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(name.encode()) + return source + + def wheel(self, root, source, *, extra=None, omit=None, changed=None): + wheel = root / 'hardware.whl' + with zipfile.ZipFile(wheel, 'w') as archive: + for path in packages.resources(source): + relative = path.relative_to(source).as_posix() + if relative != omit: + archive.writestr('orchard_hardware/' + relative, + b'changed' if relative == changed else path.read_bytes()) + if extra: + archive.writestr(extra, b'private') + return wheel + + def test_wheel_resources_are_checked_by_content(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.fixture(root) + self.assertEqual(packages.audit(self.wheel(root, source), source)['verified_resources'], 29) + for options, error in (({'omit': 'kernel/series.py'}, 'Missing'), + ({'changed': 'kernel/configs/apple-4k/config'}, 'Changed')): + with self.subTest(options=options), self.assertRaisesRegex(ValueError, error): + packages.audit(self.wheel(root, source, **options), source) + + def test_development_sources_and_loader_patches_are_required(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.fixture(root) + for name in ('boot/m1n1/patches/prox.patch', + 'kernel/devicetree/n61/touch.dtso', + 'kernel/modules/n61/of-overlay/helper.c', + 'docs/bring-up/n61-touch.md'): + with self.subTest(name=name), self.assertRaisesRegex(ValueError, 'Missing'): + packages.audit(self.wheel(root, source, omit=name), source) + + def test_existing_runtime_and_image_recipes_are_required(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.fixture(root) + for name in ('features.toml', 'image/runtime.py', 'boot/quiet/m1n1.patch', + 'boot/initramfs/recovery/init'): + with self.subTest(name=name), self.assertRaisesRegex(ValueError, 'Missing'): + packages.audit(self.wheel(root, source, omit=name), source) + + def test_wheel_refuses_scratch_and_links(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.fixture(root) + for name in ('orchard_hardware/.work/private.bin', + 'orchard_hardware/kernel/.work/private.md'): + with self.subTest(name=name), self.assertRaisesRegex(ValueError, 'Build scratch'): + packages.audit(self.wheel(root, source, extra=name), source) + wheel = self.wheel(root, source) + with zipfile.ZipFile(wheel, 'a') as archive: + link = zipfile.ZipInfo('orchard_hardware/link') + link.external_attr = (stat.S_IFLNK | 0o777) << 16 + archive.writestr(link, '/private/owner-input') + with self.assertRaisesRegex(ValueError, 'Archive link'): + packages.audit(wheel, source) + + def test_sdist_retains_resources_and_refuses_root_or_kernel_scratch(self): + with tempfile.TemporaryDirectory() as temporary: + root = Path(temporary) + source = self.fixture(root) + archive = root / 'hardware.tar.gz' + for extra in (None, '.work/private.md', 'kernel/.work/private.md'): + with tarfile.open(archive, 'w:gz') as output: + for path in packages.resources(source): + output.add(path, arcname='hardware-1/' + path.relative_to(source).as_posix()) + if extra: + entry = tarfile.TarInfo('hardware-1/' + extra) + entry.size = 7 + output.addfile(entry, io.BytesIO(b'private')) + if extra: + with self.subTest(extra=extra), self.assertRaisesRegex(ValueError, 'Build scratch'): + packages.audit(archive, source) + else: + self.assertEqual(packages.audit(archive, source)['verified_resources'], 29) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_package_resources.py b/tests/test_package_resources.py new file mode 100644 index 00000000..2ce126f1 --- /dev/null +++ b/tests/test_package_resources.py @@ -0,0 +1,52 @@ +"""Installed hardware retains the recipes used by the shared kernel producer.""" +from pathlib import Path +import subprocess +import sys +import unittest + +import orchard_hardware + +SOURCE = Path(__file__).resolve().parents[1] +PACKAGE = Path(orchard_hardware.__file__).resolve().parent + + +class PackageResources(unittest.TestCase): + def test_nfs_runtime_is_available_from_the_installed_package(self): + for source in (SOURCE / 'boot/initramfs/nfs').iterdir(): + if source.is_file(): + relative = source.relative_to(SOURCE) + with self.subTest(resource=str(relative)): + packaged = PACKAGE / relative + self.assertTrue(packaged.is_file(), str(packaged)) + self.assertEqual(packaged.read_bytes(), source.read_bytes()) + + def test_platform_recipes_and_patch_layers_match_the_source(self): + files = [SOURCE / name for name in ('kernel/series.py', 'kernel/sources.toml', + 'kernel/upstream.lock')] + for relative, pattern in (('kernel/series', '*'), ('kernel/configs', '*/config'), + ('kernel/patches', '**/*.patch')): + files.extend(path for path in (SOURCE / relative).glob(pattern) if path.is_file()) + for source in files: + relative = source.relative_to(SOURCE) + with self.subTest(resource=str(relative)): + packaged = PACKAGE / relative + self.assertTrue(packaged.is_file(), str(packaged)) + self.assertEqual(packaged.read_bytes(), source.read_bytes()) + + def test_n61_recipe_resolves_inside_the_installed_package(self): + self.assertIn('n61', orchard_hardware.profiles()) + resolver = PACKAGE / 'kernel/series.py' + resolved = subprocess.check_output([sys.executable, str(resolver), '--resolve', 'n61'], + text=True).strip() + self.assertEqual(resolved, 'apple-4k t7000-n61') + patches = subprocess.check_output([sys.executable, str(resolver), 'n61'], text=True).splitlines() + self.assertTrue(patches) + for name in patches: + path = Path(name).resolve() + with self.subTest(patch=name): + self.assertIn(PACKAGE, path.parents) + self.assertTrue(path.is_file()) + + +if __name__ == '__main__': + unittest.main()