Files
baketnk--frame-yap/docs/packaging.md
T

15 KiB

Release packaging and idempotent user-local installer

No GitHub release is published. Native-only local artifacts have been installed and reinstalled on Frame. The installer never pretends an ASR runtime is included when it is not; see third-party notes.

Producer

Default builds stay offline. Producers explicitly provision native dependencies, build with FRAMEYAP_NATIVE=ON, and stage this layout (regular files, no links):

bin/frameyap
bin/install.sh                    # managed installer helper when required by layout
lib/*                             # explicitly supplied compatible native libraries
assets/actions.json               # plus binding JSON and backends/redux.json
fonts/font.ttf
python/frameyap/*.py
scripts/model-status.py           # offline CLI verifier; also backend-service.py, fetch-model.py
licenses/THIRD_PARTY_NOTICES.txt
model/*                           # optional pinned public weights + attribution
runtime/bin/python3               # ONLY for an authorized bundled-runtime artifact

The stage copies the self-contained installer into bin/ and CMake installs scripts/model-status.py alongside the backend manifests. The package allowlist permits scripts/; installed frameyap --list-models expects the verifier at ../scripts/model-status.py. No actual staged archive has been audited/tested from a clean account for publication; exercise the entire producer pipeline and the installed CLI before treating the payload layout as release-ready.

For the current external-runtime package, scripts/stage-native.py --help documents explicit inputs. The old scripts/stage-native-poc.py is retained as a deprecated migration wrapper for that command, not the documented or shipped staging interface. The stage invokes cmake --install on an existing native build, copies SDL/OpenVR and a font. It does not build, download, run the app, or copy an ASR runtime. The native app relies on Frame's system Vulkan loader/driver, Wayland, libxcb, FreeType, libstdc++ and glibc. Check the actual staged ARM64 binaries' NEEDED, GLIBC_*/GLIBCXX_* symbol versions and ELF interpreter; then test on a clean target. No compatible libc floor is yet established. SDL/OpenVR resolve inside its own lib/, not a producer prefix. ARM64/glibc packaging is not a claim of compatibility with arbitrary Linux.

python3 scripts/package-release.py --stage /path/to/stage --output /existing/output \
  --version 0.1.202609241627 --arch linux-aarch64 \
  --model-revision fad622f25f303105c20d70e201bcc477c88b620c --external-runtime

Use the actual binary's numeric MAJOR.MINOR.YYYYMMDDHHMM UTC version (currently 0.1), not this illustrative value, for --version and filename; tag a vetted clean tree as vVERSION. CMake generates this stamp at configuration time (SOURCE_DATE_EPOCH may supply it); -DFRAMEYAP_VERSION=0.1.YYYYMMDDHHMM can pin it. Development --version may print a separate git HASH line, with (uncommitted changes) only if dirty: that line is not part of the version, tag or archive name. The installer rejects a reused tag with different contents; historic local versions may remain selectable for rollback, not as new releases.

--external-runtime refuses a runtime directory and records runtime: external-authorized-python in release.json. The installer explicitly reports that ASR is not supplied. Without that flag, a complete compatible isolated CPU Python runtime is required in the archive. Staging validation is not an inference test.

The producer refuses overwrites and emits frameyap-VERSION-linux-aarch64.tar.gz plus .sha256 containing HASH FILENAME. Archive extraction rejects traversal, links/special files, duplicate members, oversized metadata/payloads and invalid layout. Checksums detect corruption, not a malicious/compromised publisher; authenticate release metadata independently. No packaging/installation model fetch.

Consumer

Bootstrap for a binary archive: Linux ARM64/glibc, Python 3.12+ (installer bootstrap, not the inference runtime), curl for network release downloads, sha256sum and tar for archive handling. No compiler, sudo, Steam store AppID or engine checkout in binary mode. No minimum glibc floor has been certified; preflight alone cannot guarantee compatibility. Download/inspect a pinned installer before running it. Current local artifact route:

sh install.sh --archive /path/to/frameyap-VERSION-linux-aarch64.tar.gz \
  --sha256 64_HEX_DIGIT_HASH --version VERSION

After an actual vetted release exists, use a real numeric version (for example, sh install.sh --mode binary --version 0.1.202609241627 --yes); the installer will retrieve tag v0.1.202609241627 and its versioned checksum; no latest or moving-branch lookup. A pipe invocation is supported, never prompts on stdin, and must also pin a real published tag. There is no functional public download command yet.

--without-model omits any model files in the selected archive, never deletes an existing current model on rerun, and records the choice. Same digest/version/choice is idempotent and repairs missing managed wrappers. Different digest or model choice for the same version is refused. External model provisioning is always deliberate: the local installer accepts sh install.sh --install-model --backend redux --yes (optional --model-dir /absolute/path) to explicitly download and verify files from the installed pinned manifest, not an ASR runtime. Inspect the model/size first with sh install.sh --install-model --backend redux --print-plan --json. --expected-manifest-sha256 HASH additionally binds consent to the exact raw installed redux.json bytes: under the model lock a mismatch fails before a model directory is created or any network request. In the locally implemented Models UI, Install displays source, rounded size, license text, attribution and the fingerprint; only the second Confirm Install click passes that fingerprint through the backend helper to the installer. Installation alone does not provision a CPU Python runtime; no in-panel flow has been accepted on Frame. In the source tree, python3 scripts/model-status.py --list-models or --check-model redux --model-dir /absolute/model hash-checks local files; the native frameyap --list-models / --check-model entry points use the adjacent installed verifier script; verify this in a staged archive before release. Manifest schema, source, size/hash and attribution live in assets/backends/redux.json and are validated by python/frameyap/model_files.py.

For an explicit local source install, e.g.:

sh install.sh --mode source --source /absolute/source --openvr-root /absolute/sdk \
  --openvr-library /absolute/libopenvr_api.so --openvr-license /absolute/openvr/LICENSE \
  --sdl-library /absolute/libSDL3.so.0 --sdl-license /absolute/sdl/LICENSE \
  --version 0.1.202609241627

It checks tools and native dependencies, builds/stages/packages in a private workspace, then installs the result. Unlike binary mode, this requires a C++ compiler, CMake, SDK, SDL3, Wayland/scanner, libxcb, FreeType, Vulkan development files and producer-supplied licenses; it still does not install Python ASR packages. --print-plan performs a read-only plan, --json gives machine-readable results/errors (model installs also stream file events), and --yes authorizes network downloads. Bare TTY invocation can guide choices and prints equivalent flags; non-TTY runs require explicit arguments and never prompt. --autolaunch/--no-autolaunch are explicit OpenVR registration choices, off by default; do not pass either during an inert install if a running SteamVR session must remain untouched.

The installed launcher defaults to --run and forwards explicit run flags (including --backend ID, --model-store /absolute/store and --manifest-dir /absolute/manifests) to the binary. These overrides select local metadata/model paths, not a runtime download; the latter two require absolute paths without dot segments. --backend selects for that run unless changed in the panel; a saved config.json backend is otherwise used. For a native-only package, supply FRAMEYAP_PYTHON=/absolute/authorized/python and FRAMEYAP_MODEL=/absolute/model, or override --python/--model on an explicit --run. For menu launches, create $XDG_CONFIG_HOME/frameyap/paths.conf (default ~/.config/frameyap/paths.conf):

python=/absolute/path/to/independently-authorized/runtime/bin/python3
model=/absolute/path/to/pinned/local/model

The launcher reads these as literal absolute paths, not shell code, and does not follow a symlink to the config file. Environment variables override either setting for an explicit launch. This paths.conf survives upgrade/uninstall; the installer does not populate it or bundle an unauthorized runtime. No pip/bootstrap/model download or fallback is invoked by the launcher. Without paths, the native-only artifact cannot perform voice inference; its default runtime and model locations do not exist. A new installer accepts only the exact previous managed launcher bytes for migration; a modified launcher is refused. The current managed launcher leaves --font unset so config.json can choose it.

Without any ASR runtime, these checks work directly through the installed launcher:

~/.local/bin/frameyap --check-input
~/.local/bin/frameyap --check-overlay --head
~/.local/bin/frameyap --check-controls --head

These modes cannot record or type. GAMESCOPE_SOCKET or --socket selects the input backend; default is GAMESCOPE_WAYLAND_DISPLAY, then gamescope-0.

Lifecycle

Install root: $XDG_DATA_HOME/frameyap (default ~/.local/share/frameyap); launcher: ~/.local/bin/frameyap; desktop entry: $XDG_DATA_HOME/applications/frameyap.desktop (default ~/.local/share/applications/frameyap.desktop). The desktop entry points to the user-local launcher; the installer never edits Steam's library or registers a Steam shortcut. Install/upgrade creates or checks $XDG_CONFIG_HOME/frameyap/config.json (default ~/.config/frameyap/config.json), filling missing properties or repairing invalid JSON/values while preserving valid customization. Before each repair it saves an exact-byte config.json.backup-* next to the original; valid config is left untouched. Symlinks and oversized config files are refused, and uninstall leaves both the config and backups in place. paths.conf is not modified. The launcher passes a stable install-root lock identity and sets PYTHONDONTWRITEBYTECODE=1. Runtime/check/registration modes and installer share an exclusive nonblocking .lock; no upgrade/rollback/uninstall kills a running app or any other process.

Selection of a completed current is atomic; previous is retained. sh install.sh --rollback switches to the prior validated version. Foreign/modified wrappers, untracked install files and inconsistent ownership metadata are refused. Same-user malicious concurrent filesystem mutation is outside the current threat model.

The generated frameyap.vrmanifest uses local.frameyap.overlay, not a store AppID. Linux ARM requires binary_path_linux_arm; both Linux fields are written. Installation does not initialize OpenVR, register, autostart, record or type. With SteamVR ready, explicitly register:

~/.local/bin/frameyap --register "$HOME/.local/share/frameyap/frameyap.vrmanifest"
# Substitute XDG_DATA_HOME if customized. Add --autostart only if deliberately wanted.

Registration checks actual OpenVR installed state rather than trusting Add's return alone. Repeated registration/removal were exercised on Frame, autolaunch off. Actual menu launch and cold-runtime behavior remain separate acceptance checks.

SteamVR dashboard launcher check (opt-in on Frame)

Registration creates an OpenVR app entry, not a Steam store/library shortcut. On one Frame, registration succeeded with autolaunch off but no FrameYap entry was visible in the first checked dashboard menu. The user then found FrameYap under Steam's Non-Steam section and reported that selecting it opened the panel and Quit closed it. The VR server logged a --run overlay connection and exit; no owned process remained. This establishes a basic menu-driven launch on that device, not that OpenVR registration alone creates a Steam shortcut or that the new desktop entry was the sole discovery path. The manifest points to ~/.local/bin/frameyap, which starts --run if launched. A native-only install without configured model/runtime cannot transcribe; it should show Unavailable (possibly after a brief Warming transition), rather than record or infer. Only perform this check on an unconfigured native-only installation: verify that current/runtime/bin/python3 and current/model are absent and no user-local paths are configured; do not press Record, Type or Type + Enter. The installer also provides a desktop entry; where Steam's UI supports adding a non-Steam app, the user may select that entry or browse to the installed launcher. Shortcut discovery/persistence after a normal restart is not yet verified. Do not hand-edit Steam's shortcut database.

  1. With SteamVR running, register using the command above (do not pass --autostart). Leave any other apps and sessions alone.
  2. In the headset, check Steam's Non-Steam section or the + application picker for FrameYap. If absent, stop and report the menu checked; registration alone does not add a Steam library shortcut. If present, select it, check the FrameYap panel's status, and use Quit to close.
  3. Report separately whether the menu entry was visible, the click started an app, the panel was visible, and Quit worked. A registration success or CLI --check-overlay success alone does not establish menu-driven launch.

This check does not validate microphone capture, transcription, controller input, text delivery or cold SteamVR startup. With a configured inference runtime, the menu launch attempts to load the model; defer that test unless you intend to load the model. If an environment/configuration unexpectedly supplies a runtime/model, do not perform this inert launcher check.

Before uninstall, explicitly frameyap --unregister /absolute/manifest/path and verify success, then run sh install.sh --uninstall --unregistered. The latter is an acknowledgement, not a hidden SteamVR edit. Only owned files are removed; config stays, models move to saved-models/VERSION, conflicts/untracked files abort.

Offline tests: python3 -m unittest discover -s tests -p test_installer.py. They use temporary homes/local fixtures. When editing the Python helper, run python3 scripts/sync-installer.py; tests enforce embedded installer parity.