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

14 KiB

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:

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:

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

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:

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:

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:

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:

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.