Build

Install and compile Sqyre from source. For action types and screenshots, see Features. For using the GUI, see Docs.

The sections below are synced from the Sqyre application repository so build steps stay in one place.

Run

After building, launch ./bin/sqyre (Linux) or ./bin/sqyre.exe (Windows). For creating and running macros, see Docs.

Developing (extended)

Developing Sqyre

Open the repository in the dev container (.devcontainer/). It includes Rust 1.96, clang (pipewire bindgen), X11 link deps, AppImage packaging tools (appimage-builder, squashfs-tools), flatpak + flatpak-builder, Trunk + wasm32-unknown-unknown (for make wasm), JDK 17 + Android SDK/NDK + Gradle + cargo-ndk (for make android), and the Docker CLI (host daemon via socket) so make windows, AppImage Docker fallbacks, and Flatpak Docker fallbacks work inside the container. Native Flatpak builds still need host user namespaces / bwrap; otherwise use the privileged Flatpak builder image via Docker.

Nested docker run -v mounts use the host path via LOCAL_WORKSPACE_FOLDER (${localWorkspaceFolder}). Rebuild the container after pulling that change so the env var is set.

If Cursor reports “container is not running” during attach, stale containers are usually the cause — remove them (docker ps -a → docker rm -f <id>) and Rebuild Container. Large target/ trees are excluded from file watchers (see .devcontainer/devcontainer.json). Prefer make clean-sweep to keep target/ (and target-dhat/ when present) under ~20 GiB; use cargo clean only if you need a full cold rebuild.

shutdownAction is set to none so Cursor does not stop the container when an Agents window closes or a secondary attach ends (a known Cursor Dev Containers bug). Stop it yourself when finished: docker stop <id> or Docker Desktop. After pulling this change, Rebuild Container once so the setting takes effect.

Host permissions / SELinux / git

On Fedora, Bazzite, and other SELinux-enforcing hosts, a plain bind mount of the repo often yields Permission denied on files and broken git (“dubious ownership”) inside the container. The devcontainer sets --security-opt=label=disable, remaps the vscode user to the host UID (updateRemoteUserUID), marks /workspace as a git safe.directory, and re-owns writable caches (target/, .cache/, cargo home) on each attach. Nested make windows / AppImage / Flatpak Docker binds use the :z SELinux label.

After pulling these changes, Rebuild Container (not just reopen). If target/ or .cache/ were created as root on another machine, delete them on the host or let post-start.sh chown them once they are unwritable.

From the repo root:

make            # ./bin/sqyre (debug)
make release    # fmt + check, then ./bin/sqyre (release)
make run        # cargo run -p sqyre-app
make check      # fmt --check + clippy (-D warnings) + cargo deny
make machete    # unused crate deps
make test       # cargo nextest (falls back to cargo test)
make bench      # criterion: match, vision (no OCR), serialize (not in CI)
make wasm-check # cargo check -p sqyre-app --target wasm32-unknown-unknown --no-default-features
make coverage   # llvm-cov HTML + lcov under target/coverage/
make coverage-floors  # line-% gates for pure crates (needs cargo-llvm-cov)
make clean-sweep # cargo-sweep target/ (+ target-dhat/) down to 20GB (SWEEP_MAXSIZE=…)
make docs-media # regenerate docs/images screenshots
make appimage   # fmt + check, then bin/*.AppImage (Linux)
make flatpak    # fmt + check, then bin/com.sqyre.app.flatpak (Linux; Docker fallback if needed)
make windows    # fmt + check, then bin/sqyre.exe (Docker MinGW cross / native on Windows)
make macos      # fmt + check, then bin/sqyre (macOS host)
make wasm       # fmt + check, then bin/wasm/ GUI-only browser editor (Trunk)
make android-check  # cargo ndk check (editor + runtime) for arm64-v8a (devcontainer)
make android    # fmt + check, then bin/sqyre-debug.apk (devcontainer; see ANDROID.md)
make android-emulator  # boot an x86_64 emulator (needs /dev/kvm), install and launch the APK
make ocr-models # fetch the ocrs ONNX models embedded into the binary at compile time
make release-bundle  # portable bin/sqyre-bundle/ (dist/LTO shipping; no check gate — see scripts/linux/packaging/PACKAGING.md)
make dev            # fast prototype of release-bundle → bin/sqyre-dev/ ([profile.proto], no LTO)
make release-bundle-dhat  # same + dhat-heap → bin/sqyre-bundle-dhat/ (leak hunts; not for shipping)

Release builds (make release, make release-bundle, make release-bundle-dhat, make dev) need ≥4 GiB container RAM on a cold target/; if rustc is SIGKILL’d, raise Docker memory or set CARGO_BUILD_JOBS=1. Make defaults CARGO_BUILD_JOBS to 16 on hosts with ≥16 CPUs, otherwise 4; an explicit CARGO_BUILD_JOBS in the environment or on the command line wins. With a warm target/ cache, make release-bundle / make dev are much faster and lighter than before (they no longer run clippy/deny first). Prefer make dev for day-to-day bundled iteration; it uses [profile.proto] (release opts, no LTO), [profile.release] uses thin LTO (default codegen-units), and release-bundle uses [profile.dist] (codegen-units = 1). Do not set RUSTFLAGS='-C target-cpu=native' for AppImage/Windows shipping artifacts. release-bundle-dhat uses a separate target-dhat/ so it does not overwrite the normal release binary.

Run make help for the full target list. Workspace layout: RUST.md.

Build caches (all gitignored):

PathRole
target/Incremental compile artifacts (host + docker bind-mount; Windows under target/x86_64-pc-windows-gnu/). Cap with make clean-sweep (~20 GB).
target-dhat/Separate release artifacts for make release-bundle-dhat (avoids clobbering normal target/release). Also swept by make clean-sweep.
.cargo-home/Optional workspace-local cargo/rustup install
.cache/cargo/Cargo registry/git cache used by CI and docker AppImage / Windows builds
.cache/sccache/sccache disk fallback in CI containers (primary cache is the GitHub Actions backend)
.cache/sccache-windows/sccache rustc cache for make windows (Linux/CI bind mount; GHA backend used in CI)
Docker volumes sqyre-windows-*Windows cross cargo/target/sccache when the repo is on a Docker Desktop Windows path
Dev container volume sqyre-cargo-homePersistent /home/vscode/.cargo in the container

make appimage via Docker reuses CARGO_HOME when Make exports .cargo-home, otherwise .cache/cargo. make windows defaults to CARGO_INCREMENTAL=1; on Docker Desktop it stores cargo caches in Linux volumes (bind-mounted target/ on a Windows host path is very slow). CI uses SQYRE_WINDOWS_SCCACHE=1 instead. See scripts/windows/PACKAGING.md.


Make targets

TargetOutput
all / sqyrebin/sqyre (debug) — default
releasebin/sqyre (release)
check-fmtcargo fmt --all -- --check
fmtcargo fmt --all (write)
clippycargo clippy --workspace --all-targets (-D warnings)
denycargo deny check (licenses / advisories / bans / sources)
machetecargo machete (unused dependencies)
checkcheck-fmt + clippy + deny (CI quality gates)
testcargo nextest run --workspace (falls back to cargo test)
smokeDebug bin/sqyre --version (no display)
benchCriterion benches for sqyre-match, sqyre-vision, sqyre-serialize (local only; not CI)
wasm-checkcargo check of the GUI-only WASM editor (no Trunk)
coveragellvm-cov nextest → HTML + lcov.info + summary.json under target/coverage/ (no % gate)
coverage-floorsLine-coverage floors for pure crates (sqyre-domain, sqyre-varref, path_confine, migrate, sqyre-serialize, sqyre-validate, sqyre-persist, sqyre-executor; see scripts/coverage-floors.json)
clean-sweepcargo-sweep --maxsize 20GB on target/ (and target-dhat/ if present); override with SWEEP_MAXSIZE=
runcargo run -p sqyre-app
docs-mediaRegenerate images/sqyre/ screenshots
appimagebin/Sqyre-*.AppImage
flatpakbin/com.sqyre.app.flatpak
release-bundlebin/sqyre-bundle/ (portable Linux; dist/LTO; no check gate)
devbin/sqyre-dev/ (same layout; --release / no LTO; fast prototyping)
release-bundle-dhatbin/sqyre-bundle-dhat/ (same + dhat-heap; local leak hunts only)
windowsbin/sqyre.exe (Docker MinGW cross on Linux; native on Windows)
macosbin/sqyre (release; macOS host only)
wasmGUI-only browser editor → bin/wasm/ (Trunk; no Run/capture/OCR)
android-checkcargo ndk check of sqyre-app (editor and native-runtime,overlay-buttons) and the Android backends for arm64-v8a
androidSideload APK → bin/sqyre-debug.apk (ANDROID_FEATURES default native-runtime,overlay-buttons, empty = editor only; ANDROID_PROFILE, ANDROID_ABIS; ANDROID.md)
android-emulator / android-emulator-headless / android-emulator-stopx86_64 API 35 emulator (window or headless) that installs and launches bin/sqyre-debug.apk; needs /dev/kvm and an APK that includes x86_64, which make android builds by default ( ANDROID.md)
ocr-modelsocrs detection/recognition ONNX models via scripts/download-ocr-models.sh

Set CARGO_FLAGS for extra cargo args. Set RELEASE_VERSION (or write a VERSION file) before make appimage / make flatpak / make release / make windows to stamp package names and embed SQYRE_VERSION in the binary for auto-update checks (Flatpak disables in-app self-replace — use flatpak update). Local builds without either default to 0.0.0-dev (update checks disabled).

OCR models (assets/ocr-models/)

OCR runs on ocrs (pure Rust, RTen runtime) with two ONNX models — text-detection.onnx + text-recognition.onnx (~12 MB). They are embedded into the binary at compile time via include_bytes!, so assets/ocr-models/ must be populated before any target that compiles sqyre-vision — make targets that need it fetch them automatically (file prerequisite on assets/ocr-models/*.onnx; make ocr-models runs the download script directly, SHA-256 verified).

SQYRE_OCR_MODELS=<dir> overrides the embedded pair with a directory containing both files — for testing different weights without a rebuild.

WASM editor (make wasm)

Browser-only macro editor (import/export db.yaml). Does not run automation. The dev container already has Trunk and the wasm32-unknown-unknown target — rebuild the container after pulling those Dockerfile changes, then:

make wasm-check    # cargo check wasm32 editor (no Trunk; also run on Linux CI)
make wasm          # → bin/wasm/index.html  (deployable; use this, not trunk serve's dist)
cd crates/sqyre-app && env -u NO_COLOR trunk serve   # local preview + reload only

Serve the release output with any static file server (python3 -m http.server from bin/wasm/, etc.). Do not copy dist/ from a running trunk serve — that injects an unreplaced autoreload WebSocket stub and floods the console. On a bare host (no container), install once:

rustup target add wasm32-unknown-unknown
cargo install --locked trunk

Uses --no-default-features (no global hotkey hooks). Native make / make release are unchanged.

CI and GitHub Releases

Push/PR to main runs a check job (fmt/clippy/deny/machete, doctests, make smoke, make wasm-check, and a cargo-sources.json ↔ Cargo.lock drift check), a parallel test job running instrumented coverage (make coverage + floors), an android-check job on the Android image, and a Windows job that tests OS-agnostic crates (sqyre-domain, sqyre-varref, sqyre-serialize, sqyre-validate, sqyre-persist) — not a GitHub Release. Capture, hotkeys, and GPU UI tests stay Linux-only. Criterion benches (make bench) are local-only.

Releases come from .github/workflows/main.yml on schedule or manual dispatch only:

TriggerWhen
CronDaily at 23:00 UTC (0 23 * * *)
ManualActions → Build and Release → Run workflow, or gh workflow run "Build and Release" --ref main

The gate job sets should_release=true only when there is no prior v* tag, or the released ref (main on schedule, the dispatched ref on manual runs) has changed since the latest v* tag excluding docs/** and *.md. It also fails fast if crates/sqyre-update/update_pubkey.hex is unconfigured, before any build job spins up. Docs-only / markdown-only changes do not publish. If nothing releasable changed, release jobs are skipped.

Release build jobs set SQYRE_SKIP_GATE=1 so make android / make wasm skip release-gate (fmt + check) — the check job already gated the exact same commit, and release itself requires check to succeed.

Tag shape: vYYYY.MM.DD (UTC date). If that tag already exists, CI uses vYYYY.MM.DD.HHMM.

Artifacts: Linux binary + AppImage + Flatpak (com.sqyre.app.flatpak), Windows .exe (MinGW cross via scripts/windows/), and the WASM editor zip (make wasm). make macos stays native; MSI/DMG packaging is not shipped yet.

Shipped Linux AppImage/Windows builds embed SQYRE_VERSION so the in-app updater can compare against GitHub Releases (local 0.0.0-dev builds skip update checks). Flatpak installs use flatpak update instead of in-app self-replace.

Signed updates: Releases must publish SHA256SUMS and SHA256SUMS.sig (Ed25519 over a version-bound manifest — sqyre-update-manifest-v1\n<version>\n<SHA256SUMS bytes> — so an older signed manifest cannot be replayed under a newer tag). The client verifies the signature with the public key in crates/sqyre-update/update_pubkey.hex before trusting hashes.

To configure signing (maintainer, once):

  1. Generate a keypair (32-byte seed as hex), e.g. with Python nacl / cryptography, or any Ed25519 tool that can emit raw keys.
  2. Write the public key as 64 lowercase hex characters into crates/sqyre-update/update_pubkey.hex and commit it.
  3. Store the private seed hex as GitHub Actions secret SQYRE_UPDATE_SIGNING_KEY (never commit it). build-linux compiles sign_update_sums in the build container and uploads it as an artifact; the release job downloads and runs it — no second checkout/toolchain.

Until update_pubkey.hex is configured (not UNCONFIGURED), auto-update checks fail closed on signature setup.

CI caches (shared where possible):

CacheShared byNotes
Cargo registry (.cache/cargo)All Linux jobsOne key on Cargo.lock; warmed once per run via cargo-registry
Cargo target/Per triplelinux / windows-gnu / wasm32 / android — not cross-shareable
sccacheAll containerized jobsGitHub Actions cache backend: per-compilation content-addressed blobs, no save steps; .cache/sccache is only the in-container disk fallback
Linux build image (base stage)check, test, build-linux (+AppImage), build-wasmSlim: no Android toolchain. Content-hash tag on GHCR + Buildx layers
Android build image (android stage)check-android, build-androidbase + SDK/NDK/Gradle/emulator/scrcpy; builds FROM base inline in one Buildx invocation — no dependency on the pushed base image, so it runs in parallel with linux-image
Windows cross imagebuild-windows (+ local make windows)Content-hash tag on GHCR + Buildx layers
Flatpak runtimes + builder statebuild-flatpakWeekly-epoch key on the manifest — bounded staleness instead of a multi-GB re-save every release
OCR models / Trunk tools / AppImage runtime / Gradle / rustup toolchainsAcross runsKeyed on the inputs that pin their versions

Runnable images are tagged ghcr.io/<owner>/<repo>-linux-build:<dockerfile-hash>, …-android-build:<dockerfile-hash>, and …-windows-cross:<scripts-hash> (also :latest). .devcontainer/Dockerfile is multi-stage — docker build without --target produces the full android image (what the devcontainer uses); CI builds --target base or --target android explicitly.


Native dependencies

ResourcePurpose
.devcontainer/DockerfileMulti-stage: base = Rust + AppImage/Flatpak tools + Trunk/wasm32; android = base + SDK/NDK/Gradle/emulator/scrcpy (devcontainer image)
.devcontainer/devcontainer.jsonDocker-outside-of-Docker (CLI + host socket) for make windows / packaging fallbacks
scripts/windows/DockerfileMinGW cross image for make windows on Linux
crates/sqyre-app/assets/icons/Brand icons (embedded SVG)
assets/ocr-models/ocrs ONNX models embedded at compile time (make ocr-models, gitignored)

OCR models are embedded at compile time from assets/ocr-models/; SQYRE_OCR_MODELS overrides them with a directory (see above).


Manual setup (without dev container)

Prefer the container when possible. Needs Rust ≥ 1.96, clang, and X11 libs (libx11-dev, libxtst-dev, …). See RUST.md.

make            # or: cargo build -p sqyre-app
./bin/sqyre

For AppImage on the host, also install appimage-builder, patchelf, and squashfs-tools. For Flatpak, install flatpak, flatpak-builder, and ostree (and ensure user namespaces / bwrap work), or use the Docker fallback in scripts/linux/packaging/flatpak/build-flatpak.sh.


Tests

make test
# or: cargo test

Headless CI uses Null* backends / stub hotkeys where hooks are unavailable.

Coverage

make coverage instruments the full workspace (nextest when available), runs tests once, and writes HTML + LCOV + JSON under target/coverage/ with no percentage gate. Requires cargo-llvm-cov and llvm-tools-preview (preinstalled in the dev container). CI uses this instead of a separate make test so the suite is not compiled twice. Normal local make test does not need these tools.

make coverage-floors fails when line coverage drops below the floors in scripts/coverage-floors.json:

TargetScopeDefault floor
sqyre-domainwhole crate83%
sqyre-varrefwhole crate93%
path_confinesqyre-executor path confinement90%
migratesqyre-persist db.yaml migration83%
sqyre-serializewhole crate93%
sqyre-validatewhole crate84%
sqyre-persistwhole crate77%
sqyre-executorwhole crate85%

OS-specific crates (sqyre-capture, etc.) are intentionally not gated. By default floors run a smaller instrumented pass over pure crates only. After make coverage, pass COVERAGE_REPORT_JSON=target/coverage/summary.json to reuse that report (no second instrumented build) — CI does this. Set COVERAGE_FLOORS=1 when running make coverage to run the floor check automatically after the report.

README screenshots

In-memory egui goldens live under images/sqyre/ (test: cargo test -p sqyre-app --test docs_screenshots):

AssetSurface
main-window.pngMacro tree + Macros sidebar
add-action-picker.pngAdd Action catalog
data-editor.pngData Editor → Coordinates / Points
settings.pngSettings → Appearance (sidebar layout)
command-palette.pngCtrl+K command palette
make docs-media
# or: SQYRE_UPDATE_SCREENSHOTS=1 ./scripts/generate-docs-media.sh

Needs wgpu (lavapipe in the dev container / CI image).


Packaging

See scripts/linux/packaging/PACKAGING.md for AppImage and Flatpak builds.