# Internal storage and machine settings [Documentation index](README.md) · [Boot images](developer/boot.md) · [Recovery](recovery.md) Internal storage contains boot files, an independent recovery system and persistent machine settings. The operating system itself lives on a removable SYSTEM cartridge. This guide covers preparing the internal disk and updating its settings from recovery. For the complete first-install sequence, including cabling, EEPROM and a first bay map without DATA, use [the real-hardware setup guide](hardware-setup.md). ## Build the complete disk image Build the component images first, in this order: ```sh make kernel make initramfs make boot-volume BOOT_MODE=production make recovery make internal-image ``` The result is `out/fds-internal.img`, a **complete GPT disk image**. Its versioned build directory contains `layout.json`, source payload checksums, and readback verification. This differs from `out/fds-boot.img` (a raw FAT partition) and `out/fds-recovery.img` (a raw EROFS partition). | Partition | Format | Default allocation | Purpose | | --- | --- | --- | --- | | 1: FDS_BOOT | FAT32, EFI system type | 512 MiB | Pi firmware, kernel, DTBs, configuration and initramfs | | 2: FDS_RECOVERY | EROFS | 1 GiB | Complete independent maintenance system | | 3: FDS_INTERNAL | ext4 | 256 MiB | Machine settings and explicitly saved diagnostics | The builder pads the recovery partition without changing its EROFS contents, aligns partitions to 1 MiB, writes both GPT copies, verifies each payload, and checks the table independently with `sfdisk`. Internal ext4 is fully initialized before deployment. It contains root-owned `config/` and private `diagnostics/`. Unused capacity beyond this initial layout is not automatically expanded. For a 2 GiB recovery allocation or larger settings partition: ```sh ./image/build-internal --recovery-mib 2048 --internal-mib 512 ``` ## Install the internal disk Use an NVMe enclosure or another Linux machine that can access the target drive while the Pi is off. First verify a downloaded release using a separately trusted public key as described in [Release signatures](releases.md). Its complete disk is named `fds-internal-0.1.0.img`; a local build uses `out/fds-internal.img`. Use the complete disk image, not the separate BOOT or RECOVERY payload. List disks before and after connecting the intended drive: ```sh lsblk -d -o NAME,PATH,MODEL,SERIAL,SIZE,TRAN,LOG-SEC ls -l /dev/disk/by-id/ ``` Match the physical model, serial and capacity. Choose a persistent **whole-disk** `/dev/disk/by-id/` path without a `-partN` suffix; never guess a `/dev/sdX` name. Unmount every target partition and disable any swap on it. Do not select the workstation's system disk. The current images require 512-byte logical sectors and a disk at least as large as the image; 4 KiB logical-sector media is not supported by this layout. Open Bash (`bash`), replace both paths below, then run the block. It checks the selected disk again, requires a typed confirmation, writes and reads back the complete image, and relocates the backup GPT when the disk is larger than the image. It does not expand any partition or filesystem. ```bash ( set -euo pipefail fds_image="$PWD/out/fds-internal.img" fds_disk=/dev/disk/by-id/REPLACE_WITH_THE_TARGET_DISK [[ -f "$fds_image" && -s "$fds_image" && -b "$fds_disk" ]] [[ $(lsblk -dnro TYPE "$fds_disk") == disk ]] [[ $(sudo blockdev --getss "$fds_disk") == 512 ]] fds_bytes=$(stat -Lc %s "$fds_image") (( $(sudo blockdev --getsize64 "$fds_disk") >= fds_bytes )) lsblk -p -o NAME,TYPE,MODEL,SERIAL,SIZE,MOUNTPOINTS "$fds_disk" if lsblk -nrpo MOUNTPOINTS "$fds_disk" | grep '[^[:space:]]' >/dev/null; then echo 'Target has mounted filesystems or swap; stop and release them first.' >&2 exit 1 fi if lsblk -nrpo TYPE "$fds_disk" | grep -Ev '^(disk|part)$' >/dev/null; then echo 'Target has device-mapper or other active descendants; stop.' >&2 exit 1 fi read -r -p "Type ERASE $fds_disk to erase this disk: " fds_confirmation [[ "$fds_confirmation" == "ERASE $fds_disk" ]] sudo dd if="$fds_image" of="$fds_disk" bs=4M conv=fsync status=progress sudo blockdev --flushbufs "$fds_disk" sudo cmp -n "$fds_bytes" "$fds_image" "$fds_disk" sudo sfdisk --lock=yes --relocate gpt-bak-std "$fds_disk" sudo sfdisk --verify "$fds_disk" sudo blockdev --flushbufs "$fds_disk" ) ``` Every command must succeed. The byte comparison occurs before relocating GPT, because relocation intentionally changes disk-table headers. GNU `dd`'s `fsync` flushes output before it returns; `sfdisk`'s `gpt-bak-std` moves the backup header to the end of the target. See the [GNU dd manual](https://www.gnu.org/s/coreutils/manual/html_node/dd-invocation.html) and [sfdisk manual](https://man7.org/linux/man-pages/man8/sfdisk.8.html). These commands use the already documented coreutils, diffutils and util-linux host tools; they do not add a target daemon. Confirm that the disk shows `FDS_BOOT`, `FDS_RECOVERY` and `FDS_INTERNAL` in that order using `lsblk -o NAME,PARTLABEL,FSTYPE,MOUNTPOINTS`. Unmount anything the desktop automatically mounted, then safely disconnect the enclosure and install the NVMe in the powered-off Pi. Follow [EEPROM preparation](eeprom.md) for the separately reviewed NVMe boot settings. Prepare the **first SYSTEM cartridge on the workstation** using the same guarded write block, with its two path assignments changed to the chosen SYSTEM image and a different, empty USB cartridge disk. For a local build select `out/fds-system-cli.img` or `out/fds-system-development.img`; release files are `fds-system-cli-0.1.0.img` and `fds-system-development-0.1.0.img`. They are alternative complete GPT images, each containing one `FDS_SYSTEM` partition. Readback and backup-GPT relocation apply to this disk too. Check its partition label, safely disconnect it, and insert exactly one SYSTEM cartridge in the Pi before normal boot. Subsequent cartridge creation and updates can use FDS's confirmed [media workflow](developer/media-tools.md) after bay calibration. ## Configure the machine before building Copy `config/machine/` to your own directory. It contains three files: - `machine.toml`: `format = 1` and a short human-readable `name`. - `bays.toml`: the measured controller/port map described in [Cartridges and bay calibration](cartridges.md). - `hardware-catalog.toml`: optional USB identification names using the same schema as the base catalog. Entries are data and cannot run commands. The supplied bay map is empty. Record actual controller/port identities for each physical slot before installing settings. USB 2 and USB 3 companion ports need explicit aliases for the same bay. ```sh cp -a config/machine out/my-machine # Edit the three files in out/my-machine using your editor. make internal-image MACHINE_CONFIG=out/my-machine ``` The builder compiles a native host copy of `fds` and uses the same strict parser as the target system. You can also validate or pack settings yourself: ```sh cargo build --locked --offline --release --target x86_64-unknown-linux-gnu -p fds-cli ./target/x86_64-unknown-linux-gnu/release/fds machine validate out/my-machine ./target/x86_64-unknown-linux-gnu/release/fds machine pack out/my-machine out/my-machine.json ``` The three source files become one atomic `config/machine.json` document on FDS_INTERNAL. Names, lengths, bay aliases, catalog fields and unknown keys are validated before use. Do not put passwords or private keys in this configuration: its active snapshot is readable by the local FDS user. ## What happens at boot The `machine-config` native s6 oneshot precedes `cartridged`. It does **not** precede the console or Dasung controller. It accepts exactly one non-removable NVMe disk with the three named partitions in the order above. USB lookalikes are ignored; multiple eligible NVMe disks are rejected instead of choosing by name. The settings partition must be clean ext4 with the expected label. Loading uses `ro,noload,nosuid,nodev,noexec` in a private mount namespace, validates the entire settings bundle, copies it into `/run/fds/machine/`, and unmounts. No filesystem repair, journal replay, cache compilation or persistent write occurs during normal boot. The cartridge service uses that snapshot throughout this boot, including after a service restart. If internal storage is missing, unclean, invalid or ambiguous, the service records an explanation and uses the immutable image's `/etc/fds/` defaults. The console still opens. Check the actual source before treating bays as calibrated: ```sh fds machine status fds --json machine status fds machine export /tmp/current-machine ``` Export creates a new directory with the three editable source files. It never overwrites an existing directory. The status source is `internal_nvme` or `image_defaults`. A temporarily unavailable NVMe is not adopted later in the same boot; resolve the issue and reboot to load its settings. This prevents changing bay identities underneath active cartridge operations. ## Update settings from recovery Bring the edited source directory on a DATA cartridge. At the local root `RECOVERY#` console, identify its bay with `fds bays`; healthy DATA is mounted read-only under `/run/fds/media/NN`. For example, if it is in BAY 02: ```sh fds machine validate /run/fds/media/02/my-machine fds machine install /run/fds/media/02/my-machine fds reboot ``` Installation is restricted to root in the recovery image. It writes the complete validated bundle atomically, saves the old bytes as `config/previous.json`, flushes and unmounts the internal filesystem, and reports success only after those steps. A reboot activates the new settings. The currently running bay map is unchanged, so active media does not move to a different bay mid-operation. Keep a copy of your previous source directory on DATA or the build host to reinstall it if the new calibration is wrong. An invalid existing JSON document can be replaced this way. Wrong ownership, symlinks, an unclean filesystem or a damaged directory require offline filesystem maintenance first. Recovery does not automatically repair internal NVMe; its `fds recovery repair` command is deliberately limited to DATA cartridges. ## Save and retrieve diagnostics Logs and boot records stay in RAM by default. Root may explicitly save a file of up to 16 MiB. Saved names cannot contain paths, and existing names are refused. These operations mount internal ext4 only for the operation and then unmount it. For example, from recovery: ```sh fds --json boot-profile >/tmp/boot.json fds machine store boot-first.json /tmp/boot.json fds --json bays >/tmp/cartridge-inventory.json fds machine store cartridges-first.json /tmp/cartridge-inventory.json bash /usr/share/fds/capture-hardware /tmp/hardware-capture tar -C /tmp -czf /tmp/hardware-capture.tar.gz hardware-capture fds machine store hardware-first.tar.gz /tmp/hardware-capture.tar.gz fds machine fetch boot-first.json /tmp/retrieved-boot.json ``` Use distinct names for subsequent sessions. A failed flush or unmount is an error, not a successful save. These are machine diagnostics; do not use this facility as ordinary user storage. The saved cartridge inventory is a persistent diagnostic snapshot, including metadata already inspected during this boot. Retrieve it with `fds machine fetch cartridges-first.json /tmp/saved-inventory.json`. The daemon's live metadata cache remains volatile and is rebuilt from currently attached devices; saved snapshots are never used to authorize media actions.