Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
9802724
feat(apm): open the NeXT label Rhapsody nests in Apple_Rhapsody_UFS
danifunker Sep 10, 2026
b0d8819
feat(rbformats): read NeXT magneto-optical media (.od)
danifunker Sep 10, 2026
d6fd792
fix(sun): find the backup slice by geometry when the label has no VTOC
danifunker Sep 10, 2026
8d814b7
feat(backup): back up Sun- and NeXT-labeled disks as a single-file CHD
danifunker Sep 10, 2026
85b3ed4
feat(backup): compact a Rhapsody slice without losing its NeXT label
danifunker Sep 10, 2026
2682541
feat(next-mo): edit a NeXT magneto-optical image in place
danifunker Sep 10, 2026
6e49c64
test(regress): cases for the NeXT MO / SunOS 4.1.3 / Rhapsody drop
danifunker Sep 10, 2026
ee8d4e1
fix(fs): count the Rhapsody label's bytes in every partition-level size
danifunker Sep 10, 2026
9cd5249
feat(backup): back up SGI volume-header and SGI-disk-label disks
danifunker Sep 10, 2026
a92f934
test(regress): backup/restore cases for the two SGI schemes
danifunker Sep 10, 2026
ac34203
feat(backup): back up Amiga RDB disks, and stop making ones that cann…
danifunker Sep 10, 2026
30d4f9b
feat(backup): back up Atari AHDI disks
danifunker Sep 10, 2026
e7449c3
docs: resume prompt for restoring the CHD single-file invariant
danifunker Sep 11, 2026
c728a42
feat(backup): back up a partitionless volume as one whole-disk CHD
danifunker Sep 11, 2026
26a36ff
fix(backup): refuse what the whole-disk CHD layout cannot assemble, a…
danifunker Sep 11, 2026
99e5f06
feat(restore): rewrite disk labels on restore, and make an X68k disk …
danifunker Sep 11, 2026
050e518
feat(export): rewrite disk labels on resize at backup and export time…
danifunker Sep 11, 2026
2f331d5
test(backup): a superfloppy CHD backup is one container, not one member
danifunker Sep 12, 2026
2305fef
fix(os): reach the Linux syscalls through libc, so the host transpile…
danifunker Sep 12, 2026
3029ec0
ci(sol9): clear the build-script artifacts before retrying, so the re…
danifunker Sep 12, 2026
f14e4db
ci(appimage): pin anylinux.c to the same commit as the script that do…
danifunker Sep 12, 2026
36fcd03
ci(appimage): follow anylinux.c to its new repo, pinned to sharun 3.2.1
danifunker Sep 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,12 @@ jobs:
ANYLINUX_REF: 2affcd69e3b3fccab4507dbdbaed5d1a04bedfa9
# Pinned appimagetool release; quick-sharun honours APPIMAGETOOL_LINK.
APPIMAGETOOL_VERSION: "0.3.0"
# anylinux.c moved out of Anylinux-AppImages into its own repo on
# 2026-09-10 (Anylinux-sharun commit a84aa4b, "move and build helper
# libraries to this repository"). quick-sharun.sh at the ref above still
# defaults to the old `refs/heads/main` path, which now 404s -- so point
# it at the new home, pinned to a release tag rather than a branch.
ANYLINUX_SHARUN_VERSION: "3.2.1"
# See build-windows: keep APP_VERSION consistent across Test and Build.
RELEASE_VERSION: ${{ needs.generate-version.outputs.version }}
steps:
Expand Down Expand Up @@ -590,6 +596,14 @@ jobs:
export VERSION
# Pin appimagetool (quick-sharun downloads via this URL).
export APPIMAGETOOL_LINK="https://github.com/pkgforge-dev/appimagetool/releases/download/${APPIMAGETOOL_VERSION}/appimagetool-${{ matrix.sharun_arch }}-linux"
# anylinux.c now lives in Anylinux-sharun (see the env note). The
# script's default still points at the old repo's main branch, which
# 404s -- and its five retries just repeat a request that cannot
# succeed ("Failed to download 5 times!"), which reads like a network
# blip rather than a moved file.
export ANYLINUX_LIB_SOURCE="https://raw.githubusercontent.com/pkgforge-dev/Anylinux-sharun/${ANYLINUX_SHARUN_VERSION}/lib/anylinux.c"
curl -fsI "$ANYLINUX_LIB_SOURCE" >/dev/null \
|| { echo "::error::anylinux.c unreachable at Anylinux-sharun ${ANYLINUX_SHARUN_VERSION}; upstream may have moved it again"; exit 1; }
# GITHUB_REPOSITORY is set by Actions; appimagetool auto-emits zsync.

mkdir -p "$OUTPATH"
Expand Down Expand Up @@ -1608,6 +1622,13 @@ jobs:
RELEASE_VERSION="$VER" scripts/build-sol9.sh vendor
if ! RELEASE_VERSION="$VER" scripts/build-sol9.sh sol9; then
echo "::warning::sol9 stage failed; retrying once (minicargo build-script race)"
# The losing worker leaves the build-script marker behind with an
# empty OUT_DIR, so a bare retry trusts the marker, skips re-running
# the script and dies on output that was never written ("Unable to
# open .../private.rs") -- a different error than the one it is
# retrying. Drop every build-script artifact so they genuinely re-run;
# they are seconds each, and the expensive crate compiles stay.
rm -rf "$SOL9_OUT"/build_* "$SOL9_OUT"/host/build_*
RELEASE_VERSION="$VER" scripts/build-sol9.sh sol9
fi
RELEASE_VERSION="$VER" scripts/build-sol9.sh dist
Expand Down
14 changes: 11 additions & 3 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,14 +94,22 @@ Each backup is a folder. Two layouts depending on the chosen output:
**Single-file CHD** (CHD output):
- `metadata.json` with `layout: "single-file-chd"` and per-partition
`offset_in_disk` byte ranges instead of per-file references
- `mbr.json` / `gpt.json` / `apm.json` - parsed partition-table sidecar (raw
bytes live inside the CHD)
- `mbr.json` / `gpt.json` / `apm.json` (or `sun.json`, `next.json`,
`sgi.json`, `sgi_dklabel.json`, `rdb.json`, `ahdi.json`, `x68k.json`) -
parsed partition-table sidecar (raw bytes live inside the CHD)
- `<backup-name>.chd` - one disk image with table at sector 0, partitions
at their declared offsets, gaps zero-filled. `chdman info` opens it,
MAME loads it.
- A partitionless volume (floppy, `.hfv`, bare `.hdf`) is one body from
byte 0 with no table and no sidecar; the CHD's logical size is the
source size, never the packed extent.

CHD output never produces per-partition CHDs — the single-file layout is
the only CHD shape rusty-backup writes.
the only CHD shape rusty-backup writes. A CHD holds a whole disk, so a
source the layout cannot assemble must be refused, not downgraded. The
disk-label schemes (Sun, NeXT, SGI, SGI-DkLabel, RDB, AHDI, X68k) are
backed up whole with their head region verbatim and resized on restore by
`partition::restore_patch`; see `docs/backup_partition_schemes.md`.

### Key Design Patterns

Expand Down
19 changes: 10 additions & 9 deletions README.md

Large diffs are not rendered by default.

367 changes: 367 additions & 0 deletions docs/RESUME-chd-single-file-invariant.md

Large diffs are not rendered by default.

71 changes: 71 additions & 0 deletions docs/backup_partition_schemes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
# Backup and restore, by partition scheme

The rule that decides every shape below: **a CHD holds a whole disk.**
`backup --format chd` writes exactly one `<name>.chd` whose logical bytes are
the disk, or it refuses. It never writes `partition-N.chd`. A partitionless
volume (a floppy, a BasiliskII `.hfv`, a bare Amiga `.hdf`) is the one case
where the partition *is* the disk, so its CHD is the volume, sized to the
source, with no table and no sidecar.

Two layouts exist. **Per-partition** (`zstd`, `gzip`, `lz4`, `raw`, `vhd`)
stores one body per partition plus a table sidecar, and restore rebuilds the
table from the sidecar. **Single-file CHD** stores the disk image itself, and
restore is a byte copy unless a size changes.

## The disk-label schemes

Sun, NeXT, SGI volume header, SGI disk label, Amiga RDB, Atari AHDI and Sharp
X68k are backed up **whole**: everything before the first partition (label
copies, the RDSK/PART/FSHD/LSEG chain, the IPL and its table, boot blocks)
rides verbatim inside the CHD. Nothing on the restore side has to understand
the label to put the disk back.

| Scheme | Per-partition layouts | Label rewrite (restore, backup-time resize, raw / VHD export) | Unit the patcher rounds to |
|---|---|---|---|
| Sun | refused | slices rewritten, XOR checksum restamped | cylinder (`ntrks * nsect`) |
| NeXT | refused | all four label copies rewritten | `d_secsize` (1024) past the front porch |
| SGI volume header | refused | slots rewritten, checksum recomputed | 512-byte block |
| SGI disk label | refused | eight slots rewritten in the label's own word order | 512-byte block |
| Amiga RDB | refused | RDSK + PART blocks overlaid, driver chain untouched (raw / VHD export has its own older path) | cylinder, per partition |
| Atari AHDI | refused | root-sector entries rewritten, 0x1234 word-sum restamped | 512-byte sector |
| Sharp X68k | allowed (`zstd` etc.) | table entries rewritten, sector-size aware | logical sector (256 / 512 / 1024) |

The per-partition layouts are refused for the label schemes because their
sidecar is a *parsed* table, and re-serializing it loses what the head
carries (the RDB driver chain and bad-block list most visibly). X68k is the
exception: its per-partition restore rebuilds the table from `x68k.json` and
zero-fills the IPL region, which is fine for a MiSTer data disk and wrong for
a real SCSI disk whose `X68SCSI1` signature selects the sector size. Use CHD
for a faithful copy.

**The rewrite** is `partition::restore_patch`, and every path that changes a
size goes through it. On restore, the head region is read out of the CHD,
rewritten, and the bodies are copied to wherever the label now says. A CHD
backup or export with a resize rewrites the head *before* the bodies are
staged, so the CHD carries the new layout and keeps the drive's size. A raw or
VHD export with size overrides copies the bodies to the new layout and writes
the rewritten head after them. Because each scheme counts in its own unit,
the patcher may round a partition up and shift the ones after it; it returns
the layout it actually wrote, and the body copy and filesystem resize follow
that. A filesystem `resize_filesystem_for` cannot shrink (UFS on a real Sun or
NeXT disk) is refused before anything is written. The head is written *after*
the bodies, so a label that lives inside its first slice (SunOS at cylinder 0)
still lands.

## What cannot be backed up

- **`.dsd`** (double-sided Acorn DFS): two sides track-interleaved in one
file, which the reader de-interleaves into two volumes. No restore could put
them back, so `backup` refuses it in every format. Copy the file; `ls`,
`get`, `put` and `convert` keep working on it.
- **An X68k SASI disk with a partition off a 512-byte boundary** is refused
for CHD only; the per-partition layouts still work.

## Compacted bodies in a single-file CHD

A packed FAT/NTFS/exFAT body sits shrunk inside its full partition in the CHD
(the tail is zeros, which the CHD compresses away). An as-is restore grows the
volume back to its partition, on every table, so the two layouts restore the
same disk. For NTFS that includes the backup boot sector the packed stream
stops short of. `--sector-by-sector` copies the source bytes instead, and
restores byte-identical.
28 changes: 28 additions & 0 deletions docs/build-ppc-mrustc.md
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,34 @@ bin/mrustc hello.rs -o output-1.74.0-powerpc-apple-darwin/hello \
ssh $PPC_HOST './ppc-xbuild/output-1.74.0-powerpc-apple-darwin/hello'
```

### No nix

`rb-cli-ppc/Cargo.toml` declares no `nix`: lockfiles are target-agnostic, so
nix 0.31's `libc >= 0.2.186` floor would drag the whole graph onto a libc
mrustc cannot lower.

The catch is that nothing under `../src` may then name it. `src/os/linux.rs` is
`#[cfg(target_os = "linux")]`, and the PowerPC target is macOS, so it never
reaches the PowerPC build - but the `hostc` / `host` stages transpile for *this*
machine, and on a Linux host that cfg is true. With no extern crate, mrustc
resolves `use nix::...` as a local path and the engine fails to compile:

```
src/os/linux.rs:8:5-37 error:0: Cannot find component 2 of crate::os::linux::nix::mount::umount2
```

Those stages had only ever been run on an Apple Silicon host (`HOST_ARCH`
defaults to `aarch64`), where linux.rs is not compiled, so the host path had
never worked on Linux.

`src/os/linux.rs` therefore reaches its seven POSIX calls - `umount2` plus
`geteuid` / `getuid` / `getgid` / `umask` in the elevation path - through `libc`
directly, in its private `sys` module. That is what nix did anyway: its
`umount2` is `libc::umount2` plus errno handling, and `MntFlags` takes its bits
from `libc`. One implementation serves every manifest, with no feature gate or
stub to drift. `nix` survives only in `src/main.rs` (the GUI binary, not part of
this crate); **a `use nix::` anywhere under `src/` breaks `hostc` again.**

## rb-cli-ppc deviations (mrustc workarounds)

`rb-cli-ppc/Cargo.toml` carries the manifest-level deviations; the vendored
Expand Down
4 changes: 3 additions & 1 deletion docs/full_MiSTer_support_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,9 @@ support the disk types (floppy / hard disk / CD-ROM) of the outstanding cores.
SECTOR mode), MSA (Atari ST), EDSK/DSK (Amstrad CPC / PCW / Einstein /
etc.), `.d88` (Sharp X68000 / PC-88 / PC-98 / MSX / FM-7), Acorn `.hdf`
(bare + Arculator-wrapped), Apple-II `.do` / `.po` / `.dsk` sector-order,
gzip-wrapped Amiga `.adz` / `.hdz`.
gzip-wrapped Amiga `.adz` / `.hdz`, NeXT magneto-optical `.od` (raw MO media
— 1296-byte sectors with a cross-interleaved Reed-Solomon(36,32) code plus the
`od` driver's spare-sector map, both decoded transparently).
- **Raw / superfloppy** (partitionless) images are handled.
- **Optical / CD-ROM:** rip a physical CD/DVD drive to ISO or BIN/CUE
(`optical rip`), list drives (`optical drives`), convert ISO <-> BIN/CUE <->
Expand Down
8 changes: 8 additions & 0 deletions docs/solaris9-open-items.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,14 @@ the TUI, and read-only device enumeration. See `docs/build-sol9-mrustc.md` for t
commit and seed failed once and passed on re-run. The CI job retries the stage once,
which works because the build is incremental, but the race is upstream in minicargo
and that retry is a mitigation rather than a fix.
**2026-09-12:** the retry used to fail on a *different* error than the one it was
retrying. The losing worker leaves the build-script marker behind with an empty
`OUT_DIR`, so the second attempt trusted the marker, skipped re-running the script
and died with `Unable to open .../private.rs`. The retry now clears
`$SOL9_OUT/{,host/}build_*` first, so the scripts genuinely re-run. Upstream, the
two minicargo commits on the `ppc-build-2026-09` branch (`0d3211be`, `debcce0e`)
address the scheduling side, but CI builds from a prebuilt seed that predates them,
so a reseeded toolchain is what would actually retire this item.
- [ ] **The seed pins an mrustc commit implicitly.** `scripts/pack-sol9-seed.sh` packs
whatever `bin/mrustc` and the stdlib outputs happen to be, with nothing recording which
commit built them. Stamping that into the seed would make a stale one obvious.
Expand Down
15 changes: 10 additions & 5 deletions rb-cli-ppc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -204,11 +204,16 @@ crc = "=3.3.0"
# fs/resource_fork.rs.
libc = "=0.2.155"

# DEVIATION (see header): no `nix`. It is a linux-only dependency (used only by
# src/os/linux.rs, never compiled for powerpc-apple-darwin), but Cargo lockfiles
# are target-agnostic, so nix 0.31's `libc >= 0.2.186` requirement would drag the
# whole graph onto a libc mrustc cannot lower. Dropping it is what lets the pin
# below work.
# DEVIATION (see header): no `nix`. Cargo lockfiles are target-agnostic, so nix
# 0.31's `libc >= 0.2.186` requirement would drag the whole graph onto a libc
# mrustc cannot lower. Dropping it is what lets the pin below work.
#
# Nothing under ../src needs it: src/os/linux.rs reaches umount2 / geteuid /
# getuid / getgid / umask through `libc` directly (its `sys` module), which is
# the same syscalls nix wrapped. Only the GUI binary src/main.rs still uses nix,
# and that is not part of this crate. Keep it that way: a `use nix::` anywhere
# in ../src breaks the `hostc` transpile proof on a Linux host, where
# target_os = "linux" selects that module and the extern crate is absent.

# DEVIATION (see header): the objc2-* stack that rb-cli-vintage declares here is
# absent. It is reachable only through src/os/macos.rs, which the `os-stub`
Expand Down
Loading
Loading