gitvolt-app runs on the Tauri CEF runtime, which is a Chromium build. Unbundled runs (just dev,
just build-app / just run-app) need Chromium's root-owned setuid chrome-sandbox helper —
Ubuntu restricts unprivileged user namespaces, so without it Chromium's sandbox can't start and
the app either won't launch or (never do this) has to run with the sandbox disabled.
Packages: the .deb payload itself ships chrome-sandbox as root:root, mode 4755 (the Tauri
bundler writes the tar entry that way; there is no post-install script involved), so a packaged
.deb needs no setup, and neither do the Arch package and the .rpm built from it. There is no
AppImage build. The app uses SandboxPolicy::Required on Linux: it refuses to start rather than run unsandboxed.
Symptom: an unbundled run without a usable setuid helper opens no window and exits with code 133.
The only message is Chromium's FATAL:...] No usable sandbox!, and GitVolt itself prints nothing
more useful. Fix: install the helper once (see "Recommended" below), or fix the copy next to the
binary (see "Fallback").
Symptom: the app dies at once with FATAL:sandbox/linux/services/credentials.cc:131] Check failed: . : Permission denied (13) and "Trace/breakpoint trap", and the kernel log has an
AppArmor DENIED operation="userns_create" ("failed to find unprivileged_userns profile").
Cause: with /proc/sys/kernel/apparmor_restrict_unprivileged_userns at 1 but Ubuntu's
unprivileged_userns AppArmor profile not loaded, creating a user namespace fails with EACCES,
an errno Chromium's namespace-sandbox probe aborts on instead of falling back to the setuid helper
(where the profile is loaded, the probe fails gracefully). GitVolt now passes
--disable-namespace-sandbox whenever that restriction is on and the helper Chromium will use (next to
the binary, else CHROME_DEVEL_SANDBOX, as below) is usable, so Chromium goes straight to the helper; it logs
"AppArmor restricts unprivileged user namespaces: using the setuid sandbox helper". The sandbox
stays on.
A local dev build is different: every time the CEF build script runs (first build, clean
build, CEF upgrade) it copies a fresh, non-setuid chrome-sandbox from the CEF distribution next
to the binary (target/debug/ or target/release/). There are two ways to deal with that.
Chromium also looks for the helper at $CHROME_DEVEL_SANDBOX, but only when there is no
chrome-sandbox next to the executable: a helper next to the binary always wins, and if it isn't
root-owned setuid Chromium aborts ("The SUID sandbox helper binary was found, but is not
configured correctly"). Verified against the CEF 152 build in use: with the build's copy moved
away, Chromium reads CHROME_DEVEL_SANDBOX; with it present, the variable is ignored.
One time, after any build that has produced target/release/chrome-sandbox (e.g. just build-app):
sudo install -D -o root -g root -m 4755 target/release/chrome-sandbox /usr/local/lib/gitvolt/chrome-sandboxFrom then on just dev and just run-app see /usr/local/lib/gitvolt/chrome-sandbox, export
CHROME_DEVEL_SANDBOX pointing at it, and delete the build's non-setuid copy next to the binary
before launching. The sandbox stays on; only the helper's location changes.
- If a
just devrun still aborts with "found, but is not configured correctly", that build just re-ran the CEF build script and copied a fresh helper back; runjust devagain. - Re-run the
sudo installafter a CEF upgrade, so the helper matches the Chromium it serves. - To go back to the per-build flow below,
sudo rm /usr/local/lib/gitvolt/chrome-sandbox.
Without the installed helper, redo this after each rebuild, for whichever target directory you just built:
# after `just build-app` (target/release):
sudo chown root:root target/release/chrome-sandbox && sudo chmod 4755 target/release/chrome-sandbox
# after `just dev` or a plain `cargo build -p gitvolt-app` (target/debug):
sudo chown root:root target/debug/chrome-sandbox && sudo chmod 4755 target/debug/chrome-sandboxNever set the runtime's sandbox policy to Disabled as a default or in committed code.
crates/gitvolt-app/src/lib.rs sets SandboxPolicy::Required on Linux, so a missing helper
stops the app instead of silently dropping the sandbox, which Auto would do. If you need a one-off unsandboxed run for a specific
measurement (e.g. reading /proc/<pid>/smaps_rollup for PSS, which is unreadable for a sandboxed,
non-dumpable renderer), that's a manual, temporary override you make yourself, not something to
land in the repo.
libgtk-4-devandpatchelf(sudo apt install libgtk-4-dev patchelf) — the CEF runtime uses GTK 4, not WebKitGTK.- The v3-alpha
cargo tauriCLI, pinned to match the vendored runtime:cargo install tauri-cli --version =3.0.0-alpha.5 --locked(justfile'sdev/build-apprecipes check for exactlytauri-cli 3.0.0-alpha.5and tell you the command if it's missing or the wrong version). - The first build downloads the CEF distribution (~320 MB compressed) into
~/.cache/tauri-cef; after that, rebuilds are incremental. - The WebAssembly toolchain, which
just testneeds (itstest-demostep builds the demo's wasm viajust demo-wasm) as well asjust e2e-demoand the website demo build: the WebAssembly target, the wasm-bindgen CLI pinned to thewasm-bindgencrate's version, and cargo-about (licence notices):rustup target add wasm32-unknown-unknown,cargo install wasm-bindgen-cli --version 0.2.129 --lockedandcargo install cargo-about --locked(just demo-wasmchecks the first two and prints these commands when one is missing).
The window names itself gitvolt (X11 WM_CLASS, set in crates/gitvolt-app/src/desktop.rs)
and carries a 256 px window icon (_NET_WM_ICON, Tauri's default window icon: the first PNG in
tauri.conf.json's bundle.icon). An X11 taskbar can show that, but Wayland docks need the
.desktop file to show the icon: Wayland has no window-icon property, so GNOME and KDE match
the window to a .desktop entry by its class/app id (StartupWMClass=gitvolt) and show that
entry's Icon=. (The CEF window runs through XWayland, where a shell may fall back to
_NET_WM_ICON for an unmatched window, but pinning, the app grid and a reliable dock icon all
need the entry.) An unbundled build has no entry until you install one.
From the main checkout, after just build-app:
just install-desktop # ~/.local/share/applications/gitvolt.desktop + hicolor icons
just uninstall-desktop # removes them againBoth honour XDG_DATA_HOME (default ~/.local/share). The entry runs just run-app in this
checkout. Re-run just install-desktop after regenerating the icons; GNOME may need you to log
out and back in before it picks up a changed icon. The .deb installs its own entry (GitVolt.desktop);
run just uninstall-desktop before installing it so the local entry can't shadow it.
just e2e builds the harness, then runs the Playwright projects on one worker: chromium, then
chromium-budget (the tests tagged @budget, which assert a latency budget, run last so a
loaded run doesn't trip them), then webkit. Arguments go to Playwright:
just e2e --project=chromium --project=chromium-budget, just e2e e2e/diff.spec.ts,
just e2e -g '<part of a test title>'.
- The UI is a production build, not the Vite dev server:
vite build --mode e2eintoui/dist-e2e, served byvite preview. The build is redone only when its inputs change (ui/src, the build config, the lockfile:ui/e2e/build-ui.mjs), so a one-spec run starts in seconds. Thee2emode keeps the test hooks (window.__gb) that the release bundle compiles out.GITVOLT_E2E_DEV=1tests the dev server instead (unminified, for debugging a spec);GITVOLT_E2E_REBUILD=1forces a fresh build. - Traces are off. Recording one for every test (to keep the failures') took about half the
run's CPU time. A failure still leaves a screenshot under
ui/test-results/. To get its trace, rerun the spec withGITVOLT_E2E_TRACE=1and open it withnpx playwright show-trace. - Fixtures are built once per run by
gitvolt-harness fixtureand copied for each test that asks for a fresh one (freshFixtureinui/e2e/fixtures.ts):cp -a, the copy's absolute paths repointed, its index refreshed. expect.poll(fromui/e2e/test.ts) retries every 100 ms instead of Playwright's back-off to once a second.
just e2e starts the gitvolt-harness WebSocket server and the UI server on fixed ports
(7433 and 1420 by default). Two just e2e runs on those same ports collide, so if you're running
the suite from more than one git worktree at once, give each worktree a
distinct GITVOLT_E2E_PORT_BASE:
# worktree A
just e2e
# worktree B (concurrently)
GITVOLT_E2E_PORT_BASE=7600 just e2e
# worktree C (concurrently)
GITVOLT_E2E_PORT_BASE=7700 just e2eSetting GITVOLT_E2E_PORT_BASE=N runs the harness on port N and the UI on port N+1; leave it
unset to keep the defaults (7433 / 1420). Pick bases far enough apart that N and N+1 don't
overlap another worktree's pair -- e.g. 7500, 7600, 7700. This only affects just e2e; just dev
and the packaged app are unaffected and always use 1420.
just demo-build builds ui/demo.html into site/dist/demo/ (GITVOLT_DEMO_DATA=<folder> for
other data than site/demo-data/); just test-demo runs its vitest suites, the parity suite
included; just e2e-demo its Playwright smoke test (its own config, port
GITVOLT_E2E_PORT_BASE + 2, 1422 by default). just demo-golden re-records the core's answers the
parity suite compares with, after a change to them or to the basic and long_history fixtures.
just demo-data rebuilds the demo's repositories into site/demo-data/ (see docs/playground.md);
a change to playground/media_repos.rs needs both just demo-data and just demo-golden.
The website itself is site/, its own npm project (Vite, plain TypeScript and CSS; run
cd site && npm ci once). just site-build builds the landing page into site/dist/ and checks
it (nothing left unfilled, the first screen's weight); just demo-build adds the demo under
site/dist/demo/; just site-dev serves the page at http://localhost:1430 with the last demo
build at /demo/; just test-site runs its unit tests and just e2e-site its Playwright checks
(port GITVOLT_E2E_PORT_BASE + 3, 1423 by default). The features and the GitKraken table are
generated from README.md and the downloads from site/data/release.json at build time.
just site-shots retakes the screenshots in site/shots/ from the demo build (headless Chromium,
port base + 4; needs Pillow): on demand only, then commit them. The four review, undo,
rebase and stack shots wait for the writes, rebase and forge phases: until their demo
methods exist, the page shows a placeholder card in their place
(ui/src/demo/coverage.ts lists the methods still pending). just site-pages builds the whole
website the way its deploy does and checks it (docs/website.md).
- Debug info. Dev and test builds keep line tables only for our crates (backtraces and panic
locations still give file:line) and none for dependencies (
[profile.dev]inCargo.toml): a worktree'starget/is about a third the size, and links are cheaper. Run a debugger on a build withCARGO_PROFILE_DEV_DEBUG=trueif you need full debug info. - Leave
CARGO_INCREMENTALunset. Incremental builds are the default for dev builds; with them a one-line change in core rebuilds the test binaries in about a third of the time aCARGO_INCREMENTAL=0build takes, and clippy reruns in seconds. Never set it to1: sccache (below) refuses to run at all when it's1. Unset, incremental crates simply bypass sccache. - Release builds are incremental too (
[profile.release] incremental = true; still opt-level 3), sojust packageafter a small change rebuilds in under a minute instead of re-optimising all of core.just packageandjust build-appunsetCARGO_INCREMENTALfor their build so a caller's=0can't undo that. - One harness test binary. The harness's integration tests are modules of
tests/it/:cargo test -p gitvolt-harness --test it forge_stacks::runs one old file's tests. - cargo-nextest (
cargo install --locked cargo-nextest, or the prebuilt binary from nexte.st into~/.cargo/bin) runs the workspace's tests in under half the time ofcargo testhere: every test is its own process, and all the test binaries run at once instead of one after the other.just test-rustuses it when it's installed; its settings are in.config/nextest.toml. There are no doctests, so nothing is skipped. - Vitest keeps its default pool (forks) and worker count.
just test-ui-changedruns only the files related to what this branch changed;just test-uistays the full gate. - sccache and mold are optional and not configured by this repo: set them up yourself in
~/.cargo/config.toml. sccache caches dependency builds across worktrees, though about a third of them still miss in a new worktree (their cache key includes the worktree's own paths), and it never caches our own incremental crates, build scripts, proc macros or test binaries. mold links a test binary in about 0.3 s, where GNU ld took about 10 s.
How GitVolt works inside (the request API, reads and writes, askpass, the watcher, background fetch, where settings and logs live, the harness) is in ARCHITECTURE.md. A few things that matter while developing:
- One instance per config dir.
just devorjust run-appwith your own config while an installed GitVolt runs only focuses that one and exits. PointXDG_CONFIG_HOME,XDG_CACHE_HOMEandXDG_DATA_HOMEat throwaway dirs, or setGITVOLT_MULTI_INSTANCE=1. - Open Repository (Ctrl+O, or the automatic tab of an empty profile): Recent (pinned first,
filterable), "Your repos" (a scan of the profile's default repos folder; a banner offers to set
one), Open folder… and Clone. Any folder inside a repository opens that repository; a linked
worktree opens as its own tab. Open folder… needs
xdg-desktop-portaland a desktop backend (GNOME:xdg-desktop-portal-gnome; KDE:-kde), which standard desktops already run. - Command palette: Ctrl+P. Prefixes narrow it to one group:
>actions,@branches and tags,/files at HEAD,#settings (opens the Settings dialog on that setting). The Settings dialog itself is Ctrl+, (or the hamburger's File menu). - Fetch feedback: a user's Fetch always says how it went: a short "Fetched: …" toast, or an 8 s "Fetch failed: …" toast with git's message and an "Activity log" link. One still running after 2 s shows "Fetching … N%" with Cancel in the status bar. Background fetch errors go to the status bar's bell.
- Activity log and Debug: open the activity log from the bell's "Activity log" submenu or Help
→ Activity log (in dev builds, also
window.__gb.activity()in devtools). Help → Debug… opens the same modal on its Commands tab (git's command log) or Actions tab.
Everything lives in /tmp/gb-askpass and runs in a throwaway GitVolt instance. Neither the
setup nor that instance reads your git config:
GIT_CONFIG_GLOBAL=/dev/nullandGIT_CONFIG_NOSYSTEM=1keep out~/.gitconfig, so nocredential.helper store, no signing and no password-manager ssh agent.- Throwaway
XDG_*dirs give a fresh default profile, with no extra gitconfig and none of your tabs or recent repos.
Nothing here touches ~/.ssh, your ssh agent, ~/.git-credentials, ~/.config/gitvolt or a
real repository. (This holds unless your shell's startup files set GIT_CONFIG_GLOBAL
themselves: GitVolt runs git with your login shell's environment.)
scripts/fake-ssh stands in for ssh. It asks for a key passphrase the way OpenSSH decides
(through SSH_ASKPASS), then serves a local bare repo, so no sshd or key is needed. It needs a
release build (just build-app).
1. Set up a throwaway repo whose remote is "ssh". Paste this as is: it runs in a subshell, so your shell keeps its own directory and environment.
(
set -e
export GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1
GB=~/repos/gitvolt # your GitVolt checkout
T=/tmp/gb-askpass
id="-c user.name=gb-test -c user.email=gb-test@example.invalid"
rm -rf $T && mkdir -p $T
git init -q --bare -b main $T/origin.git
git init -q -b main $T/seed
git -C $T/seed $id commit -q --allow-empty -m first
git -C $T/seed push -q $T/origin.git main
git clone -q $T/origin.git $T/work
git -C $T/work remote set-url origin ssh://fake$T/origin.git
git -C $T/work config ssh.variant simple
git -C $T/work config core.sshCommand "$GB/scripts/fake-ssh --passphrase testpass"
git -C $T/work config credential.helper "" # belt and braces: no helper, even if one slipped in
git -C $T/seed $id commit -q --allow-empty -m "new upstream commit"
git -C $T/seed push -q $T/origin.git main
)2. Start the throwaway instance on that repo. Your usual GitVolt can stay open.
env GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 \
XDG_CONFIG_HOME=/tmp/gb-askpass/xdg/config XDG_CACHE_HOME=/tmp/gb-askpass/xdg/cache XDG_DATA_HOME=/tmp/gb-askpass/xdg/data \
just --justfile ~/repos/gitvolt/justfile run-app /tmp/gb-askpass/work &Its git sees no credential helper at all. To check, run this; it prints only the repo's own
empty credential.helper line (config, never credentials):
GIT_CONFIG_GLOBAL=/dev/null GIT_CONFIG_NOSYSTEM=1 git -C /tmp/gb-askpass/work config --show-origin --get-all credential.helper3. SSH key passphrase. The first background fetch can't prompt, so the status bar says "Fetch skipped: authentication required"; that's expected. Click Fetch:
- The modal asks "Enter passphrase for key '/tmp/gb-askpass-key'". Type
testpass: the toast says "Fetched: remote changes", and "new upstream commit" shows onorigin/main. - Click Fetch again and type anything else: the toast says "Fetch failed: Authentication failed (fake: Permission denied (publickey).)". Its Activity log link lists every fetch and how it went.
- Press Esc in the modal: the fetch is cancelled quietly (no toast).
4. A dead agent, and a stuck remote. A dead agent is what a closed password manager's ssh agent looks like to an ssh remote:
git -C /tmp/gb-askpass/work config core.sshCommand "$HOME/repos/gitvolt/scripts/fake-ssh --dead-agent"Fetch shows "Fetch failed: Authentication failed (fake: Permission denied (publickey).)". For a
stuck remote, use --hang instead: Fetch spins, and after 2 s the status bar shows "Fetching
work…" with Cancel, which stops it.
5. HTTPS username and password. Start an always-401 server (its PID goes in a file, so any shell can stop it), then point the repo at it:
python3 -c 'import http.server as h
class H(h.BaseHTTPRequestHandler):
def do_GET(s): s.send_response(401); s.send_header("WWW-Authenticate", "Basic realm=gitvolt-test"); s.send_header("Content-Length", "0"); s.end_headers()
def log_message(s, *a): pass
h.HTTPServer(("127.0.0.1", 8765), H).serve_forever()' & echo $! > /tmp/gb-askpass/server.pid
git -C /tmp/gb-askpass/work config --unset core.sshCommand
git -C /tmp/gb-askpass/work remote set-url origin http://127.0.0.1:8765/x.gitIn the throwaway instance, Fetch asks "Username for 'http://127.0.0.1:8765'", then the
password, then fails with "Fetch failed: Authentication failed (…)". No credential helper is
consulted, so whatever you type is never stored, and ~/.git-credentials is never opened.
6. Clean up: quit the throwaway instance, then run
kill "$(cat /tmp/gb-askpass/server.pid)"; rm -rf /tmp/gb-askpass.
A real ssh key with a passphrase isn't covered here. It would be
ssh-keygen -t ed25519 -N testpass -f /tmp/gb-askpass-key, then
core.sshCommand = ssh -i /tmp/gb-askpass-key -o IdentitiesOnly=yes -o IdentityAgent=none. But it
needs an sshd that accepts that key: ssh only asks for the passphrase after the server accepts
the public key.
Why the throwaway instance and GIT_CONFIG_GLOBAL=/dev/null rather than just a repo-local
credential.helper "":
- That reset does work against
~/.gitconfig(git 2.53, checked). - But GitVolt passes the active profile's extra gitconfig as
-c include.path=…. That is read after the repo's config, so a helper set there comes back. - With such a helper, git still calls it to
getand, after a 401, toerase;storerewrites~/.git-credentialson erase. - A fresh profile has no extra gitconfig, and
/dev/nullhas no helper.
-
Builds the
.debintotarget/release/bundle/deb/.scripts/fix-deb.shthen rewrites itsDepends: dpkg-shlibdeps results pluslibgtk-4-1andgit (>= 1:2.40), without thelibgtk-3-0the alpha CLI always adds (upstream draft:docs/upstream/tauri-cli-cef-gtk-depends.md).scripts/check-deb.shverifies that, thatchrome-sandboxis root:root 4755, and that the package carries the control fields, icons, desktop entry and AppStream metainfo below, and the license notices and DEP-5copyrightfile in/usr/share/doc/gitvolt/. It needsappstreamclianddesktop-file-validate(sudo apt install appstream desktop-file-utils). -
fix-deb.shalso writes the Debian form of the version: a SemVer pre-release's-becomes~(0.1.0-alpha.1→0.1.0~alpha.1), which sorts before0.1.0. -
GITVOLT_RELEASE_VERSION=<version> just packagebuilds the plain, unstamped version, as the release workflow does; it must equaltauri.conf.json's version. See releasing.md. -
Before building, it generates the third-party license notices (
scripts/licenses.sh, needs cargo-about), and fails on a dependency whose license isn't on the allow-list. See licensing.md. -
just package-debbuilds and checks the.debalone;just packageis that, then the Arch package and the RPM side by side. -
Repeat builds skip what hasn't changed since the last one, each keyed on a hash of its inputs:
- the license notices (about 50 s;
target/licenses.key); - the UI build (about 20 s;
ui/scripts/build-if-changed.mjs,target/ui-build/dist.key); fix-deb.sh'sdpkg-shlibdeps(about 20 s;target/package-cache/shlibdeps/, keyed on the ELF files' linkage, the paths and the installed packages).
scripts/cef-bundler-dist.shalso keeps its directory when it's already right, socef-dll-sysand what links CEF don't rebuild. - the license notices (about 50 s;
-
Local packages are compressed fast: zstd 3 for the
.deb, the Arch package and the RPM (seconds each, about 20% larger). Installing a zstd.debneeds dpkg 1.21.18 or later (Debian 12, Ubuntu 22.04). A release build (GITVOLT_RELEASE_VERSIONset) compresses as the release ships: gzip 6 for the.deb, zstd -19 for the other two (about 75 s each).GITVOLT_PACKAGE_COMPRESSION=releaseorfastpicks one either way (scripts/package-compression.sh). -
Forced full rebuild:
GITVOLT_REBUILD=1 just packageignores those caches and regenerates the notices, the UI and thedpkg-shlibdepsresult (just licensesalways regenerates the notices too).GITVOLT_REBUILD=1 GITVOLT_PACKAGE_COMPRESSION=releasebuilds the way the release workflow does, version stamp aside. -
After a Rust change, a warm
just package-debtakes 35-40 s on the dev machine andjust packageabout 50 s (5.5 min before these caches): mostly the Rust build and link, then Tauri's copying and compressing (6 s),check-deb.sh(2 s), and the Arch package and the RPM (8 s). -
scripts/test-fix-deb.shandscripts/test-check-deb.sh(part ofjust test) checkfix-deb.shandcheck-deb.shon synthetic packages. The first needsdpkg-dev, the secondappstreamanddesktop-file-utils; without them, they skip and exit 0.scripts/test-package-meta.shchecks the metainfo and copyright generator, and validates the real metainfo whenappstreamcliis installed.
Double-clicking a .deb opens it in the desktop's software centre. None of them reads the
package's AppStream file before it's installed:
- GNOME Software (50): its
dpkgplugin runsdpkg-deb -WforPackage,Version,License,Installed-Size,HomepageandDescription(plugins/dpkg/gs-plugin-dpkg.c); itspackagekitplugin asks PackageKit'sGetDetailsLocal, whose apt backend always reports the license asunknown(backends/apt/apt-job.cpp,emitPackageDetail), and takes the app id from the shortest.desktopfile name in the package (file_to_app_get_files_cb). An app without an icon then gets the genericsystem-component-application(lib/gs-plugin-job-file-to-app.c). It shows the name as the package name,gitvolt. - Ubuntu's App Center: PackageKit's
GetDetailsLocalonly (lib/deb/local_deb_model.dart): the package name as the title, the summary and description,Homepage, and the license, which isunknownthere too. Its local.debpage has no icon at all (AppTitleBar.fromLocalDeb). - KDE Discover: PackageKit's
GetDetailsLocalandGetFilesLocal(LocalFilePKResource.cpp); the file name as the name, the genericapplications-othericon.
So for that page the control fields are what count: fix-deb.sh adds License: MIT (not a
Debian field, but GNOME Software reads it), and tauri.conf.json sets Homepage and the long
Description. Once installed, the software centres read
/usr/share/metainfo/com.gitvolt.desktop.metainfo.xml (the id is tauri.conf.json's
identifier):
- It's generated by
scripts/package-meta.pyfromcrates/gitvolt-app/linux/com.gitvolt.desktop.metainfo.xml.in: the releases (version and date) from CHANGELOG.md's dated sections, and the README screenshot by its raw GitHub URL at the newest release's tag. - Its
<pkgname>is the package's name,gitvoltin the.deb, the RPM and the Arch package (fix-deb.shrenames Tauri'sgit-volt). App Center and Discover match an installed component to its package by it, and GNOME Software finds the package to remove that way, since no distribution catalog lists GitVolt. - Its
<replaces>names GitBolt's id,dev.gitbolt.desktop(the app's name before 0.7.0), so software centres carry the listing over. - The desktop entry keeps its name,
GitVolt.desktop(linux/GitVolt.desktop.hbs), so existing installs' dock pins and menu entries carry over; the metainfo's<launchable>names it. - The icons are installed under the app id too (
com.gitvolt.desktop.pngat 32 to 512 px, and.svg), which the desktop entry'sIcon=and the metainfo name. Tauri's owngitvolt.pngset stays: its bundler always installs thebundle.iconPNGs under the binary's name. check-deb.shvalidates it (appstreamcli validate --pedantic --no-net) and checks it against the package: file name, launchable, icon, pkgname, license, and the newest release against the package's version.- The package is
gitvolt(about 146 MiB). It installs/usr/share/GitVolt/(the binary and CEF,chrome-sandboxroot:root 4755) and/usr/bin/gitvoltas a symlink to it. - Install and check:
- Quit any running GitVolt that uses your config. With the single-instance guard, a running one would just be focused instead of the installed app starting.
- Run
just uninstall-desktop, so the dev entry doesn't duplicate or shadow the.deb'sGitVolt.desktop. - Run
sudo apt install ./target/release/bundle/deb/GitVolt_*_amd64.deb, then launch GitVolt from the app menu. The file name carries a build stamp (GitVolt_0.1.0+<UTC time>.<commit>_amd64.deb, e.g.0.1.0+202610051325.ab4dbf9e), andjust packagedeletes older packages first, so the glob matches only the new one. - Run
GITVOLT_PID=<browser pid> scripts/check-sandbox.sh, which checks that every renderer has seccomp and its own PID namespace. ThenGITVOLT_PID=<browser pid> just bench. The browser process is the/usr/share/GitVolt/gitvoltprocess without a--type=argument.
- Remove it:
sudo apt remove gitvolt. Thenjust install-desktopbrings the dev entry back.
just packageends withjust package-arch, so one build gives both packages. It turns the.debintotarget/release/bundle/arch/GitVolt-<ver>-1-x86_64.pkg.tar.zstwithout makepkg (scripts/package-arch.sh; the Python helpers are inscripts/arch-pkg.py).- The payload is the
.deb's data member, path for path and mode for mode; the script checks that, and thatchrome-sandboxis root:root 4755. The one addition is/usr/share/licenses/gitvolt, a symlink to the license notices in/usr/share/doc/gitvolt, and the one change is the AppStream metainfo's<pkgname>, which becomesgitvolt. licenselines: MIT, then the licenses in the Rust and UI notices' summaries, CEF and Chromium'sBSD-3-Clause, and the spell-check dictionary'sLicenseRef-SCOWL(arch-pkg.py licenses).pkgveris the.debversion with+turned into.(0.1.0+202610051325.ab4dbf9e→0.1.0.202610051325.ab4dbf9e-1), and a pre-release's~dropped (0.1.0~alpha.1→0.1.0alpha.1-1): pacman'svercmpwould sort0.1.0.alpha.1after0.1.0. Every entry gets the build stamp's time, and so doesbuilddate;$SOURCE_DATE_EPOCHoverrides it (a release build has no stamp: the workflow sets it to the commit time, and a local one falls back to the.deb's mtime).dependlines come from the.deb'sDependsthrough theDEB_TO_ARCHtable inscripts/arch-pkg.py. A Debian name the table doesn't know fails the build: add a row..MTREEis written in Python in the format makepkg's bsdtar uses (no bsdtar on Ubuntu).
- The payload is the
scripts/test-package-arch.sh(part ofjust test) checks the version and dependency mapping, mtree escaping, and a synthetic.debthrough the whole script.just check-arch-pkginstalls the package in a throwawayarchlinux:latestDocker container (--rm; it needs Docker and the network, sojust packagedoesn't run it). It runspacman -Syu, thenpacman -U, and checkspacman -Qkk gitvolt, the license notices, the setuidchrome-sandbox, the AppStream metainfo (appstreamcli validate), desktop entry and icons,lddon the binary and the CEF libraries, and a 20 s headless launch underxvfb-run.packaging/arch/PKGBUILDis a template for a futuregitvolt-binAUR package that repackages the release.deb. The build doesn't use it.- Install on Arch:
sudo pacman -U GitVolt-<ver>-1-x86_64.pkg.tar.zst. Remove:sudo pacman -R gitvolt.
just packagealso runsjust package-rpm, which turns the.debintotarget/release/bundle/rpm/GitVolt-<version>-1.x86_64.rpmfor Fedora and openSUSE, without rpmbuild (scripts/package-rpm.sh;scripts/rpm-pkg.pywrites the package: the lead, the signature header with its digests, the header, and a zstd-compressed cpio payload, as rpmbuild does). It reads the package back and checks it against the.debbefore keeping it.- The payload is the
.deb's, path for path and mode for mode (chrome-sandboxroot:root 4755), with the Arch package's two changes (the/usr/share/licenses/gitvoltsymlink, and<pkgname>gitvolt</pkgname>),install-kindsayingrpm, and without the system's directories (/usr,/usr/bin, the icon theme's): the package owns only/usr/share/GitVoltand/usr/share/doc/gitvolt. Fedora's/usr/binis mode 555, and owning a directory with another mode is a file conflict. A.debdirectory in neither list fails the build (OWNED_DIRSandSYSTEM_DIRSinrpm-pkg.py). - The license notices are
%licensefiles: dnf installs them even withtsflags=nodocs. Requiresare what rpmbuild would generate: the libraries the ELF files need and the payload doesn't bring, with their symbol versions (libnss3.so()(64bit),libc.so.6(GLIBC_2.34)(64bit), fromreadelf), so Fedora's and openSUSE's own package names don't matter. From the.deb'sDependsit addsgit-core >= 2.40and GTK 4's library (DEB_TO_RPM); another Debian name that isn't alib*one fails the build.Versionis the.deb's (0.1.0~alpha.1,0.1.0+202610051325.ab4dbf9e): rpm sorts~before anything, as dpkg does, and+as a separator.Releaseis 1. The file name keeps the SemVer form of the.deb's (GitVolt-0.1.0-alpha.1-1.x86_64.rpm). Times as in the Arch package.- It isn't signed (rpm reports
Signature: (none)); dnf says it skipped the OpenPGP check of a local file, and zypper wants--allow-unsigned-rpm.
- The payload is the
scripts/test-package-rpm.sh(part ofjust test) checks the version and dependency mapping and a synthetic.debthrough the whole script;GITVOLT_TEST_DOCKER=1adds rpm's own checks of the result infedora:latest.just check-rpm-pkginstalls the package in a throwawayfedora:latestDocker container:rpm -K,dnf install(every Requires resolved from Fedora's repositories),rpm -V, the setuidchrome-sandbox, the license notices,install-kind, the AppStream metainfo, desktop entry and icons,ldd, the same 20 s headless launch as Arch's, anddnf removeleaving nothing behind.- The release's
Requirescome from the Ubuntu 24.04 runner's libraries (glibc 2.39), so it needs Fedora 40 or later; openSUSE Tumbleweed installs it too. RHEL 9 (glibc 2.34) and openSUSE Leap 15 can't. - Install on Fedora:
sudo dnf install ./GitVolt-<version>-1.x86_64.rpm; on openSUSE,sudo zypper install --allow-unsigned-rpm ./GitVolt-<version>-1.x86_64.rpm. Remove:sudo dnf remove gitvolt(sudo zypper remove gitvolt).
A small, non-gating check of the two performance budgets that need a live app: idle CPU and
memory. It runs
scripts/measure-idle.sh 60 and scripts/measure-mem.sh against a running instance and prints
each next to its budget (idle CPU < 1.5%, memory with 12 tabs < 650 MB). It never fails on a
number.
# the browser process: a gitvolt process without --type= (CEF's helpers re-run the same binary)
GITVOLT_PID=$(for p in $(pgrep -x gitvolt); do grep -qa -- --type= /proc/$p/cmdline || echo $p; done | head -1)
echo "$GITVOLT_PID"; GITVOLT_PID=$GITVOLT_PID just bench
pgrep -nx gitvolt alone isn't enough: it returns the newest gitvolt process, which is usually a
renderer.
GITVOLT_PIDis the instance's browser (main) process; it scopes the scripts to that instance and its CEF children. Start the instance yourself (just run-app, with any throwaway XDG dirs you like), open the tabs to measure, and leave the window focused and idle for the 60 s.- The other performance budgets (latencies) are asserted by
just e2e(menu-perf.spec, thetabs.specswitch) and the opt-in, read-onlyreal-repo.spectimings. These are single timings: there's no repo generator and no statistics.
The app builds, runs and packages on Windows (x64). Needs Visual Studio's C++ Build Tools
with its Ninja on PATH (…\Common7\IDE\CommonExtensions\Microsoft\CMake\Ninja), CMake, Git for
Windows and Node.
- CEF:
cef-dll-sys's build script downloads the Windows distribution intoCEF_PATH(cargo tauriuses%LOCALAPPDATA%\tauri-cef) and copies its runtime files next to the binary. - The UI:
cd ui && npm ci && npm run build. - Debug build with the bundled UI:
cargo build -p gitvolt-app --features tauri/custom-protocol(without the feature, a debug build loads the Vite dev server,npm run dev). - Sandboxed: a plain debug
gitvolt.exeruns Chromium unsandboxed, and a release one refuses to start (SECURITY.md). The sandbox needs the app as a DLL under CEF's bootstrap, named alike:(cargo rustc -p gitvolt-app --lib --crate-type cdylib --features tauri/custom-protocol copy %CEF_PATH%\154.0.34\cef_windows_x86_64\bootstrapc.exe target\debug\gitvolt_app.exe target\debug\gitvolt_app.exe
bootstrapc.exekeeps a console;bootstrap.exeis the windowed host.) - A throwaway instance, driven from another machine: in debug builds,
GITVOLT_DEV_DIRS=<absolute dir>puts the config, cache, data and Chromium profile folders under that folder (Windows' Known Folders ignore the environment; setTEMPandTMPtoo for the single-instance and askpass files), andGITVOLT_DEV_CDP_PORT=<port>opens Chromium's DevTools protocol on127.0.0.1:<port>. Started over SSH, the app runs in the SSH session's own window station, not on the desktop;ssh -L 9333:127.0.0.1:9333 <host>and Playwright'schromium.connectOverCDP('http://127.0.0.1:9333')then reach its page. Release builds have neither variable. - The installers:
just package-windows(orpowershell -File scripts\package-windows.ps1) builds the NSIS per-user installer and the per-machine MSI intotarget\release\bundle\windows\, from a release build with the sandboxed layout above (GitVolt.exeis CEF'sbootstrap.exe,GitVolt.dllthe app). Besides the above it needs Python 3 (PYTHONpicks one), cargo-about 0.9.2 (cargo install cargo-about --version 0.9.2 --locked --features cli) and the .NET SDK (WiX is a .NET tool); NSIS, WiX and rcedit are downloaded intotarget\windows-toolson first use. docs/releasing.md has the details, the versions and the signing hook. - The title bar on GitHub's runners:
.github/workflows/windows-titlebar.yml(on demand: the Actions tab,gh workflow run windows-titlebar.yml --ref <branch>, or thewindowslabel on a pull request) builds the release app as release.yml does, thenui/scripts/win-titlebar.ps1drives its title bar with real input on Windows Server 2025 and 2022 (Windows 11's and 10's shells): the frame and its edges, moves, resizes from every border, the buttons, the window menu, Aero Snap, shake, restarts, the system title bar setting, and 150% scaling when the session can switch to it.reuse-rundrives an earlier run's build.
The app builds and runs on macOS (Apple Silicon checked, macOS 12 or later), but only as
GitVolt.app: CEF loads its framework and starts its helper processes from inside the bundle, so
a bare target/debug/GitVolt can't start. Needs Xcode's command line tools, CMake, Ninja, Node
and the pinned Tauri CLI (cargo install tauri-cli --version =3.0.0-alpha.5 --locked).
- The bundle:
cd crates/gitvolt-app && cargo tauri build --debug --bundles app(or without--debug) makestarget/<profile>/bundle/macos/GitVolt.app, with the UI embedded. The Tauri CLI does the whole layout (tauri.macos.conf.jsonadds totauri.conf.json):Contents/MacOS/GitVolt, the app (mainBinaryName, so the helpers are named after it);Contents/Frameworks/Chromium Embedded Framework.framework, copied from the distributioncef-dll-sysdownloaded intoCEF_PATH(~/Library/Caches/tauri-cefby default);Contents/Frameworks/GitVolt Helper.appand its(GPU),(Renderer),(Plugin)and(Alerts)siblings: one small helper executable, which the bundler compiles against the app's owncefcrate (target/tauri-cef-helper) and copies into each, with itsInfo.plist;Contents/Resources/dictionaries/en-US-10-2.bdic, the spell-check dictionary;- an ad-hoc signature (
signingIdentity: "-"), inside out, with the hardened runtime and the entitlements CEF needs (JIT, unsigned executable memory, no library validation);just package-macossigns with an identity instead whenGITVOLT_MACOS_SIGNING_IDENTITYnames one (docs/releasing.md, "macOS signing"); Info.plistwith the folder document type (packaging/macos/Info.plist, merged in).
- The package:
just package-macosbuilds the release bundle andtarget/release/bundle/dmg/GitVolt_<version>_aarch64.dmg, then checks both (scripts/package-macos.sh; docs/releasing.md, "macOS disk image"). It needs cargo-about too, for the license notices. - Folders from Finder: macOS hands folders to the app as Apple Events (Open With, a folder
dropped on the Dock icon,
open -a <bundle> <folder>), which arrive as Tauri'sRunEvent::Openedand open in a tab like a later launch's path. A launch argument still works when the binary is started directly. - The sandbox: the helpers enter Chromium's Seatbelt sandbox, ad-hoc or self-signed; a
release build asks for it outright (
SandboxPolicy::Required, SECURITY.md). - A throwaway instance driven over CDP (debug builds only), as on Windows:
GITVOLT_DEV_DIRS=<absolute dir> GITVOLT_DEV_CDP_PORT=9333 GitVolt.app/Contents/MacOS/GitVolt <repo>, thennode ui/scripts/cdp-smoke.mjs 9333 <out dir> <repo>(Playwright'sconnectOverCDP) opens the graph, a commit, its diff and a fetch, with a screenshot of each. The single-instance and askpass sockets are in$TMPDIR. - No Mac at hand:
.github/workflows/mac-port.yml(on demand: run it from the Actions tab, orgh workflow run mac-port.yml --ref <branch>) runs the core, forge and harness tests, a workspace check, and jobapp: it builds the debug bundle, launches it on the runner's own screen, drives it withcdp-smoke.mjs(a second launch andopen -aof a folder included), lists the bundle, its signatures and each process's sandbox state, and uploads the screenshots (macos-app). Jobpackagebuilds the release.dmgwithjust package-macos, copies its app to/Applications, launches it withopen -aon a folder and opens a second one, checks the tabs (in the profile, since a release build refuses CDP) and the sandbox, then runs the updater's install over the running app from the same.dmg, and the title bar checks on the installed app (macos-package, with the.dmg).-f only=test(orcheck,app,package) runs one job alone.