Skip to content

Latest commit

 

History

78 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Mint portable launcher scripts and manage Linux desktop integration from a declarative spec.

OpenSSF Best Practices License: MPL-2.0 Rust 1.85+ Status: alpha Mint: working Realign: working Provision: working Config: working

What it does

launch-scaffolder is a single Rust binary that generates, installs, and maintains launcher scripts for any hyperpolymath project (or any other project that adopts the hyperpolymath launcher standard). Native provisioning currently targets Linux; macOS and Windows integration remain planned. It turns launcher scripts into generated artefacts, not hand-edited files.

Inputs:

  • launcher-standard_praxis.deed — a declarative description of what a compliant launcher looks like (modes, file paths, permissions, integrity requirements, per-platform behaviour), written in the estate’s DEED v1.0.0 format. The canonical copy lives in the standards monorepo at standards/launcher/launcher-standard_praxis.deed. At runtime the loader resolves in this order:

    1. --standard <file> CLI flag

    2. $LAUNCH_SCAFFOLDER_STANDARD environment variable

    3. Every rung of the search ladder the standard declares for itself, in ascending :priority order — see (resolution) → standard-search inside the deed. A rung naming an unset $VAR is skipped, never expanded to an empty path.

    4. A baked-in copy compiled into the binary at build time (fallback only, and logged at info so the downgrade is visible)

  • <app>.launcher.a2ml — per-app config (name, display, URL, command, repo, icon, and an optional [exceptions] block).

Output:

  • <app>-launcher.sh — a fully spec-compliant cross-platform launcher (Linux / macOS / Windows-via-Git-Bash).

Why this exists

Without this tool, every hyperpolymath project maintains its own hand-written ~600-line bash launcher script that has to be kept in sync with the current launcher standard by hand. In practice, drift is guaranteed. The launcher standard evolves; the launchers don’t. Fixing a spec issue means editing 11+ files and hoping you caught them all.

With this tool:

  • The standard is a single file. Changing it is a one-line edit.

  • Every launcher is regenerated from the same standard + its own config.

  • Bulk realignment after a spec change is one command: launch-scaffolder realign *.launcher.a2ml.

  • Launchers become reproducible, auditable artefacts — not hand-edited drift.

  • “Not wasting tokens in future” — future AI sessions read one spec file, not 11 near-identical launcher scripts.

Commands

CLI subcommands, in the order you’re likely to use them:

# ─── mint ──────────────────────────────────────────────────────────────────
# Generate a launcher script from a config. Writes
# <config-parent>/<app-name>-launcher.sh by default.
launch-scaffolder mint examples/stapeln.launcher.fixture.a2ml
launch-scaffolder mint /path/to/burble.launcher.a2ml -o /tmp/out.sh
launch-scaffolder mint /path/to/burble.launcher.a2ml --stdout

# ─── realign ───────────────────────────────────────────────────────────────
# Bulk re-mint: walk the estate (default = /var/mnt/eclipse/repos) and
# re-render every live `<app>.launcher.a2ml` against the current standard
# + template. Idempotent — unchanged scripts stay unchanged.
launch-scaffolder realign                              # walk estate root
launch-scaffolder realign --search-root ~/my-projects  # narrow walk
launch-scaffolder realign /path/to/one.launcher.a2ml   # explicit configs
launch-scaffolder realign --dry-run                    # preview only
launch-scaffolder realign --check                      # CI mode: exit 1 on any diff
launch-scaffolder realign --keep-going                 # don't stop on errors

# ─── provision ─────────────────────────────────────────────────────────────
# Install (--integ) or uninstall (--disinteg) a launcher's desktop entry,
# icon, and launcher binary on the current system. Writes .desktop files
# directly from the Rust binary rather than going via the generated shell
# script. Bulk runs (--all) prompt before touching $HOME.
launch-scaffolder provision --integ    /path/to/burble.launcher.a2ml
launch-scaffolder provision --disinteg /path/to/burble.launcher.a2ml
launch-scaffolder provision --integ --all            # everything in the estate
launch-scaffolder provision --integ --all --no-confirm --force
launch-scaffolder provision --integ --all --dry-run  # preview only

# ─── standard ──────────────────────────────────────────────────────────────
launch-scaffolder standard show
launch-scaffolder standard validate

# ─── config ────────────────────────────────────────────────────────────────
# Read/validate either metadata dialect. `set` can edit legacy A2ML blocks;
# current DEED blocks are read-only and should be changed at the source config.
launch-scaffolder config get      ./stapeln-launcher.sh version
launch-scaffolder config get      ./stapeln-launcher.sh standards-compliance
launch-scaffolder config validate ./stapeln-launcher.sh
# Legacy launcher only: current @launcher-deed blocks are intentionally read-only.
launch-scaffolder config set      ./legacy-launcher.sh version 0.2.0
# For current launchers, edit the source config and re-mint.

Managed integration ownership

Both the native Rust provisioner and generated-shell fallback refuse to replace or remove an existing unmarked launcher or desktop entry, even with --force. Each desktop/launcher file is checked independently; installed icons carry a sidecar ownership marker. Move or inspect a conflicting file rather than expecting the tool to claim it automatically. Writes use same-filesystem atomic replacement.

Generated script → binary delegation

Generated <app>-launcher.sh scripts carry their own --integ / --disinteg arms (the original, pure-bash implementation). They now also embed the absolute path of their source config as CONFIG_FILE=… and, on invocation, fast-path back to launch-scaffolder provision --integ "$CONFIG_FILE" when the binary is on PATH. If the binary isn’t present, the in-script shell fallback runs instead. This means: the Rust binary is authoritative when available, without making itself a hard dependency for integrated launchers.

Architecture

launch-scaffolder/
├── Cargo.toml                    # Workspace root
├── crates/
│   ├── launcher-common/          # Shared library — all real logic
│   │   └── src/
│   │       ├── lib.rs            # Public API
│   │       ├── deed.rs           # DEED v1.0.0 parser (estate format)
│   │       ├── standard.rs       # Parse launcher-standard_praxis.deed
│   │       │                     #   + LauncherStandard::resolve (flag →
│   │       │                     #     env → the deed's own :priority
│   │       │                     #     ladder → baked fallback)
│   │       ├── config.rs         # Parse <app>.launcher.a2ml
│   │       ├── template.rs       # Render via Tera (embeds CONFIG_FILE)
│   │       ├── discovery.rs      # Walk + prune + fixture-suffix filter
│   │       │                     #   (shared by realign + provision)
│   │       ├── integration.rs    # Native Rust .desktop writer,
│   │       │                     #   icon/launcher install, gio,
│   │       │                     #   update-desktop-database
│   │       ├── metadata_block.rs # Reads DEED and legacy metadata; edits legacy blocks
│   │       ├── platform.rs       # Planned platform helpers (not wired yet)
│   │       ├── integrity.rs      # Planned integrity manifests (not wired yet)
│   │       └── exceptions.rs     # Planned exception merge (not wired yet)
│   └── launcher/                 # Thin CLI binary
│       └── src/
│           ├── main.rs           # clap dispatch
│           ├── cmd_mint.rs       # ✓ working
│           ├── cmd_realign.rs    # ✓ working
│           ├── cmd_provision.rs  # ✓ working (native Rust; option b)
│           ├── cmd_config.rs     # ✓ working
│           └── cmd_standard.rs   # standard show / validate
├── standards/
│   └── launcher-standard_praxis.deed  # Vendored/baked standard fallback
├── templates/
│   └── launcher.sh.tera          # Bash launcher template rendered by Tera
├── examples/
│   ├── README.adoc               # Fixture-vs-live naming convention
│   └── stapeln.launcher.fixture.a2ml  # Worked example (fixture suffix!)
├── docs/
│   ├── launcher-exceptions-2026-04-10.adoc
│   ├── compliance-audit-2026-04-10.adoc
│   ├── branch-protection-remediation-2026-04-10.adoc
│   └── ruleset-audit-2026-04-10/  # Audit artefacts (read-only record)
└── crates/launcher-common/tests/  # Parser, renderer, fixture and integration tests

Fixture-vs-live naming convention

To distinguish test fixtures from live, estate-owned launcher configs, file-name suffixes carry the distinction — directory names do not:

Purpose File-name suffix Picked up by estate walks?

Live per-app config

<app>.launcher.a2ml

Yes

Fixture / worked example

<app>.launcher.fixture.a2ml

No

The discovery code in `launch-scaffolder-common

discovery
is_live_config` enforces this rule. Fixture files can live anywhere — including inside a consumer repo’s examples/ directory — without being swept up by realign or provision --all. See examples/README.adoc for the full contributor-facing version of the rule.

Why Rust/SPARK

Per the hyperpolymath language policy (canonical policy):

  • Rust is the preferred language for CLI tools — zero-dep binary, fast cold start, strong types, excellent ecosystem (clap, tera, sha2, anyhow).

  • “Rust” always means “Rust with SPARK integration as the default stance”. This project is Rust-primary and designed in that direction, but it has no SPARK/FFI implementation today; the integrity module is still a placeholder.

Where a launcher keeps its state

A generated launcher writes two files: a pid file and a log. Unless the config says otherwise ([runtime] pid-file / log-file) they land under the invoking user’s own XDG directories, never in shared, world-writable space:

File Default Overridden by

pid

$XDG_RUNTIME_DIR, else $XDG_STATE_HOME, else ~/.local/state — under launch-scaffolder/<app>/server.pid

[runtime] pid-file

log

$XDG_STATE_HOME, else ~/.local/state — under launch-scaffolder/<app>/server.log

[runtime] log-file

The log goes to the state directory rather than the runtime directory because it has to survive a logout, which $XDG_RUNTIME_DIR does not promise. The unique per-app default directories are created with mode 0700. Explicit override paths are never chmodded; the generated launcher refuses a state directory that is not user-owned or is group/world-writable, so a legacy /tmp override cannot change /tmp permissions or expose a PID file to other users.

Before 2026-09-25 both defaults were /tmp/<app>-server.{pid,log}: world-writable, and predictable from nothing but the app name, so any local user could create or symlink the path before the launcher’s first run and influence what it later killed or removed (issue #48). Launchers minted before that date keep their old paths until they are re-minted — set the two keys explicitly, or re-mint, to move them. The worked example now leaves those overrides unset so it demonstrates the secure defaults.

Why a declarative format for inputs

  • A launcher carries a metadata block in its own header, so a generated script can be parsed back by this tool to recover its config and be re-minted. That round-trip is the reason the inputs are declarative rather than a pile of CLI flags.

  • The standard is a praxis DEED (launcher-standard_praxis.deed, DEED v1.0.0) as of the D73-C ruling — see ADR-010 in .machine_readable/6a2/META.a2ml.

  • The per-app config (<app>.launcher.a2ml) and the emitted @a2ml-metadata launcher header are still on the retired A2ML naming and are parsed as TOML. They are deliberately not converted here: the config conversion is tracked as standards#960, and changing the emitted header is a wire-format change between mint and realign that would strand every launcher already on disk. A2ML is retired estate-wide — nothing new should be built on it, and the earlier plan to migrate to a2ml-rs no longer applies.

Status

Alpha. The five CLI subcommands (mint, realign, provision, config, standard) are implemented; platform/integrity/exception-library modules and non-Linux integration remain planned. Last reviewed 2026-09-26.

Implemented:

  • ✓ Cargo workspace layout (two crates: launch-scaffolder-common
    launch-scaffolder)

  • ✓ deed module — parses the estate’s DEED v1.0.0 format

  • ✓ standard module — parses launcher-standard_praxis.deed; LauncherStandard
    resolve
    walks flag → env → the deed’s own :priority ladder → baked

  • ✓ config module — parses <app>.launcher.a2ml (TOML) with runtime-kind validation

  • ✓ template module — Tera renderer with full context; embeds CONFIG_FILE for in-script delegation

  • ✓ discovery module — shared walk + prune + fixture-suffix filter

  • ✓ integration module — native Rust .desktop writer, icon/launcher install, best-effort gio + update-desktop-database (Linux only; macOS/Windows return IntegError
    UnsupportedPlatform
    )

  • ✓ metadata_block module — reads current @launcher-deed and legacy @a2ml-metadata blocks; safe scalar editing remains legacy-only

  • ✓ templates/launcher.sh.tera — parameterised over runtime_kind ∈ {server-url, process, remote}; --integ / --disinteg fast-path to launch-scaffolder provision when on PATH

  • ✓ mint subcommand — positional config, -o/--out, --stdout, --no-chmod

  • ✓ realign subcommand — estate walk with --search-root override, --dry-run, --check (CI), --keep-going, walk-error tolerant

  • ✓ provision subcommand — --integ / --disinteg with --all, --force, --no-confirm, --dry-run; bulk mode prompts before touching $HOME

  • ✓ config subcommand — get / set / validate; set safely edits legacy blocks and refuses current DEED blocks

  • ✓ standard subcommand — show / semantic validate

  • ✓ Generated launcher implements standard --version output and routes --browser / --web to --auto

  • ✓ Unit and integration tests run by locked Rust CI; launcher-artifact CI enforces a 59-test floor, a current golden mint, and ShellCheck

  • ✓ 7 launchers fully managed: aerie, burble, game-server-admin, nqc, panll, project-wharf, stapeln

  • ✓ 5 declared exceptions documented in docs/launcher-exceptions-2026-04-10.adoc

  • ✓ Fixture-vs-live file-naming convention documented and enforced

  • ✓ All 7 managed launchers regenerated with template delegation arms (2026-04-10)

Remaining work (see .machine_readable/6a2/STATE.a2ml for the current checkpoint):

  • ✓ Mint output is currency-locked against the committed DEED fixture in unit tests and CI; seven-consumer golden coverage remains future work

  • ❏ macOS and Windows integration backends in launch-scaffolder-common::integration

  • ❏ SPARK integration hook for a future implemented integrity API via Zig FFI

  • ✓ standard subcommand — show and semantic validate

  • ❏ platform, integrity and exceptions library APIs — currently placeholders and explicitly not used by the CLI

  • ❏ Cross-platform CI (Linux/macOS/Windows matrix); current CI runs on Linux

  • ❏ Migration of the 5 declared exceptions once the template grows custom-mode hooks

Build

# Standard cargo workflow
cargo build            # debug build
cargo build --release  # optimized, stripped, single-file binary
cargo test             # run tests
cargo run -- --help    # invoke the binary

# Or via Justfile
just build
just test
just install           # cargo install --path crates/launcher

License

This project is licensed under the Mozilla Public License, v. 2.0. See the LICENSE file for details.

SPDX-License-Identifier: CC-BY-SA-4.0

Author

Jonathan D.A. Jewell (hyperpolymath)
j.d.a.jewell@open.ac.uk

Relationship to other hyperpolymath projects

Each consumer repo owns its own <app>.launcher.a2ml config at its repository root. launch-scaffolder mint writes the generated <app>-launcher.sh next to it. The pre-2026-04-10 pattern of pooling launchers under /var/mnt/eclipse/repos/.desktop-tools/ is deprecated; ~/Desktop/Shortcuts/*.desktop files have been repointed at the new per-repo paths.

Scaffolder-managed consumers (as of 2026-04-10):

  • stapeln — Visual Container Stack Designer (server-url, port 4010)

  • burble — WebRTC voice + control plane (server-url, port 4020)

  • aerie — network diagnostic suite (process)

  • game-server-admin — multi-game server orchestration (process)

  • nqc — NextGen Query Client, inside nextgen-databases/ (process)

  • panll — panels framework (server-url, port 8000)

  • project-wharf — container/workload staging (process)

Declared exceptions (still hand-written; see docs/launcher-exceptions-2026-04-10.adoc for reasoning and migration triggers):

  • hypatia, invariant-path, opsm, ambientops, idaptik

The bulk-realignment goal: once realign is implemented, one command will re-mint every managed launcher in the estate against the current standard.

Releases

Sponsor this project

Packages

Used by

Contributors

Languages