From-source Flatpak packaging
ai.storyteller.lightcraft.yml builds LightCraft and lightcraft-cli from this
repository inside the Flatpak SDK. Flatpak builders have no network access, so
cargo-sources.json lists every crate and git source with a fixed checksum or
commit. Regenerate it after changing Cargo.lock:
./packaging/linux/flatpak/generate-cargo-sources.py
flatpak-builder --user --install --force-clean build-flatpak packaging/linux/flatpak/ai.storyteller.lightcraft.yml
flatpak run ai.storyteller.lightcraft
Regeneration wrapper
generate-cargo-sources.py uses the upstream
flatpak-cargo-generator.py
pinned to commit 9d86d336a39b9af38b86f114b6ce5820fe260f91 with SHA-256
0a2db6be87d75910facef28ab46d4d6460802e8419ab850d0caa6a364d26b380. It downloads
the script, verifies that hash, applies the two reviewed patches below, and
invokes it with the current Python interpreter. The upstream script is not
vendored; it is MIT-licensed
(upstream licence).
The patched script's hash is also checked before execution.
Requirements: Python 3.9+ with aiohttp and tomlkit (PyYAML is optional and
only needed for upstream's YAML output), plus git and network access to fetch
the generator and git dependencies. Crate archive URLs and checksums are read
from Cargo.lock; Flatpak downloads those archives when materializing the sources. The generator clones git
repositories into $XDG_CACHE_HOME/lightcraft-flatpak-cargo by default, or the
directory passed with --cache-dir; the wrapper refuses a cache inside the
checkout or CARGO_TARGET_DIR. cargo-sources.json is written to a temporary
file and atomically renamed only after the generator succeeds, so a failed run
leaves the previous inventory untouched.
The regenerated inventory keeps the temporary file’s owner-only permissions (0600 on POSIX). Run the generator and Flatpak builder as the same user; a cross-user packaging workflow should explicitly install the inventory with its required ownership and permissions.
Generation is capped at 30 minutes and the generator's stdin is /dev/null, so
an interactive credential prompt fails instead of hanging. On POSIX systems
(Linux is the supported Flatpak packaging platform) the generator starts in its
own session; at the cap, the wrapper kills that generator's process group,
including git clone/fetch descendants, and reaps its leader, so no orphan keeps
writing into the cache. On Windows only the direct generator process is
terminated; its git descendants are not tree-killed, and the timeout is
otherwise the same. The timeout path runs before the atomic rename, so the
existing inventory and the temporary-file cleanup behave exactly like any other
generation failure.
Because the generator no longer shares the wrapper's terminal session, the
wrapper also handles Ctrl-C (SIGINT) and SIGTERM while it runs: the signal
is recorded, the generator's process group is killed and reaped like the timeout
path, the temporary file and script directory are removed, and only then is the
signal replayed with the handler that was installed before generation started
(KeyboardInterrupt for SIGINT, the previous or default action for
SIGTERM). A cancelled flatpak-builder run or an interactive Ctrl-C therefore
leaves neither a detached generator or git clone nor a modified inventory. The
temporary handlers are installed before the generator is spawned, so a signal
that arrives while the child is being created cannot strand the new session
either. A cancellation delivered while those handlers are installed is recorded
once (the first signal wins) and replayed after cleanup with the caller's
restored handler. Signals that cannot be handled by this process at all
(SIGKILL, an OOM kill, or a wrapper crash) cannot be cleaned up: the generator
runs in its own session, so it may survive the wrapper, and any temporary output
or script directory it had created remains. A hard termination between creating
the temporary directory or output file and installing the handlers, or after
run_generator returns while the output is being validated and renamed, can
likewise leave those temporary paths behind (the complete temporary output in
the latter case), because the default signal action performs no cleanup.
Why two patches
- Keep pinned revisions distinct. Upstream keys
[source]replacement entries by the canonical repository URL and then addsrev/tag/branch. Two different revisions of the same repository therefore overwrite each other. The patch keys the entry by the full source URL without its#fragment, sohttps://github.com/storytold/craft-libs?rev=...remains a separate source for each craft-libs revision. - Include
[patch.unused].Cargo.lockrecords patched packages that are not in the resolved graph underpatch.unused. Upstream iterates onlypackage, which omits crates such asegui_demo_lib;--lockedoffline dependency resolution then fails because Cargo still loads those patched manifests. The patch also walkspatch.unusedentries.
No third patch is needed: upstream's git_repo_name() already appends the
first seven characters of the commit, so the two craft-libs checkouts get
different destinations.
Offline validation performed
The committed inventory was generated from the current Cargo.lock and then
validated without contacting the network: every git/archive/shell/inline entry
was materialized as Flatpak would, crate archives were checked against their
recorded SHA-256 values, and the generated Cargo config was pointed at the
materialized vendor directory. With an isolated CARGO_HOME and
CARGO_NET_OFFLINE=true, cargo --config <generated-config> metadata --offline --locked --format-version 1 exited 0. The two craft-libs revisions
(82a24b006eefa036b43c09a67fb9a3fbd17ffe63 and
25da1314d443171b4adeaad2cd9454b273f35c6f) are present as separate git sources
and [source] entries. The checked-in cargo-sources.json has SHA-256
c7f515d1178bb79b2cabdfd419ad375a04ff5d84ba9d08b1b34970a460cd0f2c.
This validates the source inventory and offline metadata resolution only. A
full flatpak-builder build against the org.freedesktop.Sdk 25.08 runtime and
Rust extension is still required before release; it was not performed here.
Run the wrapper failure-path and cancellation regression tests (the real
process-group cases, including Ctrl-C/SIGTERM cancellation of a live
generator session, run on Linux only and are skipped elsewhere) with:
python3 -B packaging/linux/flatpak/test_generate_cargo_sources.py
The cancellation and timeout tests check process liveness and a frozen
heartbeat before their teardown kills anything. With
LIGHTCRAFT_FLATPAK_TEST_BREAK_KILL=1 the group kill is a deliberate no-op and
those tests must fail; run that variant to confirm they do not silently pass on
a broken kill path.