360 lines
14 KiB
Markdown
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.
|