Files
runi-dist/docs/prefix-bundles.md
T

360 lines
14 KiB
Markdown

<!-- Generated from the Runi source repository at v0.4.0. -->
<!-- Edit the original there, not this copy. -->
# Portable prefix bundles
## Status and objective
The first delivery is implemented: versioned export, inspection, full verification
and non-destructive transactional import are available from the CLI and shared
core. Replacement, optional components and GUI integration remain future phases.
The Wine prefix is the primary artefact, but a useful recovery package must also
record enough Runi configuration and runtime metadata to restore that prefix
on another Linux installation, including a Steam Deck, without assuming identical
filesystem paths.
The intended CLI is:
```console
runi bundle export bionic-commando-rearmed \
--output bionic-commando-rearmed.pfy
runi bundle inspect bionic-commando-rearmed.pfy
runi bundle verify bionic-commando-rearmed.pfy
runi bundle import bionic-commando-rearmed.pfy \
--exe "/path/to/bcr.exe"
```
## Bundle contents
A bundle has this logical layout:
```text
manifest.toml
checksums.sha256
game/config.toml
prefix/
notes/ # optional
cover/ # optional
recipe-state/ # optional, when applicable
```
`manifest.toml` records:
- bundle format version and creating Runi version;
- creation time, source operating system and architecture;
- game ID and UID;
- backend and runtime GameID;
- Proton name, source and build fingerprint;
- original executable path, for information only;
- included components;
- total uncompressed size and file count;
- external requirements and compatibility warnings.
`checksums.sha256` covers every payload file. It detects corruption but is not an
authenticity mechanism: signatures belong to a later phase if bundles are ever
published or exchanged through an untrusted channel.
The following are excluded by default:
- the game installation;
- Proton itself;
- logs and caches;
- installers or recipe assets without clear redistribution rights;
- saves stored outside the prefix.
These exclusions keep the bundle focused, avoid licensing problems and prevent
machine-local or disposable data from silently becoming portable state.
## Archive format
The payload must use TAR rather than ZIP. Wine prefixes contain symbolic links,
permissions and other Unix filesystem metadata which ZIP does not reliably round
trip.
The first format version uses:
- the `.pfy` extension;
- a versioned manifest independent of its container compression;
- TAR with gzip as the baseline available in Python 3.11's standard library.
Zstandard can be added later as another encoding without changing the logical
bundle format. Import must identify the actual format from its header, not trust
the filename alone.
## Current CLI
```console
runi bundle export <game-id> --output game.pfy
runi bundle inspect game.pfy
runi bundle verify game.pfy
runi bundle import game.pfy [--exe /new/path/game.exe] [--as new-id]
```
Export refuses an existing destination and an apparently active prefix. `--force`
only overrides the active-prefix check; it never overwrites a destination. Import
refuses existing registrations and prefixes. There is deliberately no `--replace`
until the preservation-and-rollback phase is implemented.
## Consistent export
Export is a snapshot operation, not an ordinary recursive copy:
1. Load the registered game and resolve its prefix.
2. Confirm that the prefix exists and passes basic structural checks.
3. Detect Wine/Proton processes using that prefix and refuse while it is active.
`--force` can explicitly override this consistency protection.
4. Estimate required space and create the archive as a temporary sibling of the
requested destination.
5. Archive filesystem entries without following symlinks.
6. Calculate the manifest and payload hashes.
7. Re-open and verify the completed temporary archive.
8. Flush it and atomically rename it to the final destination.
A failed or interrupted export must never appear under the requested final name.
Runi launches should take an advisory per-prefix lock; the process scan also
covers games started outside the current Runi process. This reduces races,
although a generic filesystem snapshot cannot guarantee application consistency
when an uncooperative external Wine process is still writing.
## Inspection and verification
`bundle inspect` reads only metadata and reports contents, requirements, sizes and
compatibility information without extracting the prefix.
`bundle verify` validates:
- container readability and format version;
- required manifest fields;
- unique, normalized archive paths;
- declared size and file-count limits;
- payload membership against the manifest;
- every SHA-256 digest.
Both commands must work without a registered game or installed Proton version.
## Transactional import
Import is split into four explicit stages:
```text
open -> validate -> stage -> publish
```
1. Open and fully validate the bundle before modifying managed state.
2. Extract into a newly-created staging directory inside the destination prefix
root, so the eventual rename remains on the same filesystem.
3. Validate the staged prefix and construct the destination configuration.
4. Atomically rename the staged prefix into place.
5. Persist the game configuration last.
On failure, only staging data is removed. Existing configuration and prefixes are
left untouched.
Archive extraction is implemented entry by entry. It rejects absolute paths, `..`
traversal, device nodes, FIFOs, duplicate paths, member paths descending through
symlinks, unsafe hardlinks, unexpected entry types and excessive expansion.
Symlink targets themselves are preserved without following them: Wine prefixes
legitimately contain external mappings such as `dosdevices/z:` pointing at `/`.
All directories and regular files are created before symlinks, so archive writes
cannot traverse those mappings during extraction.
The standard library's unrestricted `extractall()` is not an acceptable trust
boundary.
## Conflict and replacement policy
The default is non-destructive:
```text
existing registration -> abort
existing prefix -> abort
occupied game ID -> require --as <new-id>
```
An explicit future `--replace` mode must:
1. validate and stage the incoming bundle completely;
2. export or otherwise preserve the current registered state;
3. move the current prefix aside rather than delete it;
4. publish the incoming prefix and configuration;
5. roll back the old state if publication fails.
The safety snapshot's location and retention policy must be visible to the user.
No overwrite flag may turn validation off.
## Portability rules
Machine-specific absolute paths cannot be restored blindly:
- the prefix is imported under the destination machine's configured
`prefixes_root`;
- the imported `runtime.prefix` is cleared when the bundle represented a managed
prefix, allowing normal XDG resolution on the destination;
- the original executable path is treated only as a candidate;
- `--exe` supplies the new path when the original executable is unavailable;
- a default working directory tied to the old executable is recalculated;
- explicitly independent working directories require confirmation or replacement;
- the required Proton build is matched by name, source and fingerprint;
- a missing Proton build leaves a valid imported environment in a clear
"restored but not ready" state with concrete remediation instructions.
Import must not perform speculative rewrites inside the Wine registry. Any future
registry migration must be narrow, declarative and auditable because an incorrect
rewrite can destroy the state the bundle exists to preserve.
Game UID handling must be explicit. A normal recovery keeps the original UID. An
import using `--as` creates a distinct registration and therefore needs a new UID,
while retaining the source UID in import provenance rather than pretending both
registrations are the same object.
## Core architecture
This is a separate subsystem from `core/backup.py`. That existing module protects
individual files modified inside a game installation; it does not create or
restore complete environment snapshots.
Proposed package:
```text
src/runi/core/bundles/
__init__.py
models.py # BundleManifest and BundleComponent
archive.py # safe TAR writing and extraction
integrity.py # SHA-256 generation and verification
compatibility.py # destination readiness and Proton matching
exporter.py # snapshot orchestration
importer.py # staging, publication and rollback
src/runi/cli/commands/bundle.py
```
The core API should remain presentation-independent:
```python
export_bundle(game, destination, options, on_progress=None)
inspect_bundle(source)
verify_bundle(source, on_progress=None)
import_bundle(source, options, on_progress=None)
```
Core functions raise typed `RuniError` subclasses and never print, prompt or
exit. CLI and GUI adapters own user interaction. Long export, hash and extraction
operations report through the existing progress callback abstraction.
## Delivery phases
1. ✅ Versioned manifest, safe archive primitives, export, inspect and verify.
2. ✅ Transactional import with no replacement support.
3. ✅ Cross-machine executable override and Proton fingerprint/readiness reporting.
4. Explicit `--replace` with automatic preservation and rollback.
5. Required game notes plus optional custom cover and recipe-state components.
6. GUI integration and a visible bundle/history view.
7. Cryptographic signing only if an untrusted sharing channel creates that need.
The implemented first delivery remains deliberately narrow: a reliable prefix plus
configuration round trip through `export`, `verify` and non-destructive `import`.
## GUI design checkpoint (recorded, not approved for implementation)
Do not start the bundle GUI until its workflow and the remaining core contracts
below have been decided. The CLI implementation is not, by itself, a sufficient
UX specification for multi-gigabyte interactive operations.
### Export workflow
Decide whether export is exposed in the game context menu, Overview, or both; the
default filename and destination; which optional components can be selected; how
estimated size and free space are shown; and how an active prefix is handled. The
GUI should probably not expose the consistency-bypassing `--force` option in its
first version.
Prefix, game configuration and game notes remain mandatory for any bundle
presented as a complete per-game backup. Custom cover and recipe state are
candidates for explicit optional components once supported by the core. Until
the implemented bundle format carries notes, the UI and documentation must
describe it as a prefix/config portability bundle rather than a complete backup
of the game's Runi knowledge.
### Import workflow
Import belongs to the library/Add Game flow because the game may not be registered
yet. The proposed wizard is:
1. Select a `.pfy` file.
2. Verify structure and integrity.
3. Review game identity, creation date, size and required Proton build.
4. Select or confirm the executable on the destination machine.
5. Resolve the destination ID and paths.
6. Review the complete operation.
7. Import transactionally.
8. Show readiness and any missing requirements.
The initial GUI should resolve an occupied ID with only **Import as Copy** or
**Cancel**. It must not offer Replace until core replacement has automatic
preservation and rollback.
### Suggested entry points
- Game context menu and/or Overview: **Export Bundle…**
- Library **Add Game** menu: **Import Bundle…**
- A global utility action: **Inspect/Verify Bundle…**
Inspect/Verify does not need to appear inside a registered game's Overview because
it operates on an arbitrary bundle file, not on that registration.
### Progress and cancellation prerequisites
The current core reports phase-level progress only. Before GUI work, extend its
contract to provide file/byte totals and cooperative cancellation for hashing,
compression, verification and extraction. Cancellation must clean staging and
temporary archives, and the UI must prevent application shutdown during the small
atomic publication window.
All long-running work must use the existing background worker mechanism; no bundle
operation may block the Qt event loop.
### Completion and readiness view
After import, show the restored prefix, selected executable, backend, disk usage
and Proton state (available, missing or fingerprint mismatch). Useful next actions
are Run, Dry-run, Open Prefix Folder, Open Game Folder and Install/Select Proton.
Integrity and authenticity must use different language: a bundle whose internal
checksums pass is **integrity verified**, not a trusted or endorsed bundle. Invalid,
incompatible and unsigned/unknown-origin states must remain distinguishable.
### Deliberate first-GUI scope
The first GUI should read and write user-selected `.pfy` files only. A managed
bundle library/history is deferred because it introduces retention, cleanup,
removable-storage and storage-policy decisions.
Before implementation, close these prerequisites in order:
1. Detailed core progress.
2. Cooperative cancellation.
3. Required notes support and decisions for optional cover and recipe-state
components.
4. A richer import readiness/result model.
5. Final conflict and Import-as-Copy semantics.
6. An agreed import-wizard wireframe.
## Required tests
- Round-trip symlinks, permissions, hidden files, Unicode names and empty dirs.
- Export/import a synthetic prefix under different XDG roots.
- Detect a one-byte corruption, truncated archive and manifest mismatch.
- Reject traversal, absolute paths, escaping symlinks/hardlinks, device entries,
duplicate members and decompression/resource abuse.
- Leave no final archive after interrupted export.
- Leave no registration or final prefix after interrupted import.
- Preserve existing state on every conflict and failure path.
- Exercise ID, UID, config and prefix conflicts, including `--as` semantics.
- Report missing or fingerprint-mismatched Proton builds without corrupting the
imported environment.
- Confirm JSON CLI output and progress callbacks remain stable.
- Use the known-good Bionic Commando Rearmed prefix as a manual acceptance test,
including restoration onto a Steam Deck or an equivalent clean environment.