Recipes for distributing omnidiff through package managers, kept in-repo so they version
alongside the code they build. Their status differs per target (table below): the Debian package
and the Homebrew tap are published by the release workflow; the Arch, Gentoo and Nix recipes are
not submitted to their distributions, whose submission targets (the AUR, a Gentoo overlay,
nixpkgs) live outside this repository; and the VS Code extension is a separate repository rather
than a recipe here.
| Target | Files | Status |
|---|---|---|
| Arch (AUR) | aur/PKGBUILD |
the AUR is closed to new submissions; the PKGBUILD builds locally with makepkg -si |
| Gentoo | gentoo/dev-util/omnidiff/ |
ready for an overlay |
| Debian/Ubuntu | [package.metadata.deb] in ../Cargo.toml |
published — signed apt repository at ivankovic.github.io/omnidiff/apt |
| Nix / NixOS | nix/package.nix, ../flake.nix, ../flake.lock |
works today via nix run; built by the Nix workflow |
| Homebrew | homebrew/omnidiff.rb.in, rendered by ../scripts/render_homebrew_formula.py |
tap at ivankovic/homebrew-omnidiff, pushed by the release workflow |
| VS Code | omnidiff-vscode | published — on the Marketplace and Open VSX |
The tarball hashes belong to the v0.2.0 tag: they hash GitHub's tag tarball, so they can only
be regenerated after the next tag exists, and until then the recipes name the new version with the
old hash and do not build. make check-versions checks the version strings, not the hashes.
aur/PKGBUILDcarries the sha256 of the tag tarballgentoo/dev-util/omnidiff/Manifestcarries oneDISTline for the tag tarball and one for each vendored crate, each with its size, BLAKE2B and SHA512- Nix needs a
hash =only if you switchpackage.nixtofetchFromGitHub; as long assrcis a parameter andcargoLock.lockFilepoints at the in-tree lock, there is nothing to hash
The crate half is generated, and CI checks it (generate_gentoo_crates.py --check covers both
the ebuild's CRATES and the Manifest). --manifest rebuilds the Manifest's crate lines from the
cargo cache, verifying each .crate against Cargo.lock's sha256 first:
python3 scripts/generate_gentoo_crates.py # the ebuild's CRATES block
python3 scripts/generate_gentoo_crates.py --manifest # the Manifest's crate digestsThe two tarball hashes are the part no script can do ahead of time, because they hash the GitHub tag tarball and that does not exist until the release is tagged. Regenerate them with the real tools where you have them:
cd packaging/aur && updpkgsums # rewrites sha256sums=() in place
ebuild gentoo/dev-util/omnidiff/omnidiff-<version>.ebuild manifest
nix-prefetch-url --unpack https://github.com/ivankovic/omnidiff/archive/refs/tags/v<version>.tar.gzNot from SHA256SUMS.txt. That asset hashes the release assets - the prebuilt binaries, the
.deb, the completions tarball - and GitHub's auto-generated source tarball is not one of them
(see the checksums job in .github/workflows/release.yml, which hashes exactly what
gh release download returns). The recipes here all build from the source tarball, so its hash
has to come from the tarball itself.
Do not hand-write a checksum: a wrong one looks correct until somebody's build fails.
Source is the GitHub tag, not the crates.io tarball. Cargo.toml's include list ships only
the source, the licenses and the README - not tests/**, src/bin/**
or src/test/data/** - so a package built from crates.io has no test suite to run in its check
phase. The GitHub tag tarball has them — at the cost of also carrying research/ and the fixture
corpus in the download.
Tests are restricted to --lib. The fixture-corpus tests are the accuracy benchmark: they
need the test-fixtures feature and substantial time and memory. That is not what a packaging
sanity check is for.
Default features only. stats and test-fixtures gate dataset-analysis dev tools that pull in
git2 (OpenSSL, libssh2) and a bundled SQLite. They are not part of the shipped product, so no
recipe exposes them as a build option.
Completions and the man page are generated, never hand-written. Each recipe runs
omnidiff util man and omnidiff util completions <shell> against the binary it just built, so
they track the real flag list. This assumes a native build — under cross-compilation the target
binary cannot be executed, and these would have to come from a host build instead (which is exactly
what the release workflow's assets job does for the prebuilt tarballs).
Builds are slow, and that is expected. Every tree-sitter grammar compiles from C, and the
release profile sets lto = "fat" with codegen-units = 1. Minutes, not seconds.
CRATES= lists every dependency crate and is generated, not edited:
python3 scripts/generate_gentoo_crates.py # rewrite the block
python3 scripts/generate_gentoo_crates.py --check # fail if stale (for CI)Run this after any Cargo.lock change. A stale list produces a package that fails to build for
users while looking fine in review. pycargoebuild does the same job if you have it installed.
The LICENSE variable enumerates the vendored crates' licenses alongside the package's own
AGPL-3; re-check it if the dependency set changes substantially.
The .deb is built with cargo-deb, attached to each
GitHub release for amd64 and arm64, and served from an apt repository on GitHub Pages. It is
unofficial, and the distinction matters: a package in the Debian archive proper would require
every one of the 300-odd dependency crates — 23 tree-sitter grammars among them — to be packaged as
librust-*-dev first. Almost none are. That path is not reachable, so this is a cargo-deb
artifact served from our own repository, not a route into Debian.
To build one locally:
cargo install cargo-deb
cargo build --release --bin omnidiff
mkdir -p target/dist
./target/release/omnidiff util man > target/dist/omnidiff.1
./target/release/omnidiff util completions bash > target/dist/omnidiff.bash
./target/release/omnidiff util completions zsh > target/dist/_omnidiff
./target/release/omnidiff util completions fish > target/dist/omnidiff.fish
cargo deb --no-buildThe generation step is not optional: cargo-deb copies assets from disk and cannot run the binary
itself, so those four files must exist before it runs.
Both architectures are built natively, on ubuntu-22.04 and ubuntu-22.04-arm, unlike the
plain binaries in the same workflow, which reach aarch64 by cross-compiling. 22.04 rather than the
latest runner because depends = "$auto" writes the build machine's glibc version into the
package as its floor: 2.35 admits Ubuntu 22.04 and Debian 12, 2.39 would not. Two steps here cannot
cross: depends = "$auto" resolves the built ELF's needs against the packages installed on the
build machine, and the man page and completions come from running the binary. Cross-building the
arm64 .deb on an x86-64 host would stamp the host's libc version onto an arm64 package.
scripts/build_apt_repo.sh turns a directory of .deb files into a signed, static apt tree —
pool, per-architecture Packages, a Release signed both inline (InRelease) and detached
(Release.gpg), and the public key dearmoured for signed-by. The Pages workflow calls it; it
takes no repository state and can be run against any directory of packages:
export APT_GPG_PRIVATE_KEY="$(gpg --armor --export-secret-keys <KEYID>)"
scripts/build_apt_repo.sh --debs path/to/debs --out /tmp/aptNothing about the repository is committed. The pool is rebuilt on every Pages run from the
.deb assets of the last five releases, so the published tree is a pure function of the releases
that exist. Losing it costs one workflow run. Five is a KEEP_RELEASES in pages.yml, set
against the 1 GB soft limit on a Pages site rather than for any packaging reason.
The two halves are wired together in an order that matters:
- A
v*tag runsrelease.yml, which creates the release as a draft and has thedebmatrix build and upload both architectures into it. - Its
checksumsjob, after every upload, publishes the draft; itsaptjob —needs: checksums, so strictly after that — dispatchespages.yml. pages.ymldownloads every recent non-draft release's.deb, rebuilds the tree, signs it, and deploys it alongside the mapping site.
Step 2 exists because the obvious alternative does not work: a release: published trigger would
fire before the packages are attached, and pages.yml skips drafts. And the apt tree is deployed
by pages.yml rather than by release.yml because Pages has a single deployment for the whole
site — two workflows deploying separately would each erase the other.
One-time setup, and the only manual step in any of this. The key signs nothing but this
repository's Release file, so it wants no expiry — an expired key breaks apt update for every
user on a date nobody is watching, which is what the trailing never is for:
gpg --batch --pinentry-mode loopback --passphrase '' \
--quick-gen-key 'OmniDiff apt repository <marko@ivankovic.me>' rsa4096 sign never
gpg --armor --export-secret-keys '<KEYID>' | gh secret set APT_GPG_PRIVATE_KEY--pinentry-mode loopback --passphrase '' is not optional shorthand: --batch on its own makes
gpg reach for a pinentry it has no terminal for and fail with Inappropriate ioctl for device.
Drop all three flags to be prompted for a passphrase instead, and store it as a second secret,
APT_GPG_PASSPHRASE — the script signs without one when it is unset. Note what that passphrase
buys, though: it would sit in the same secret store as the key it protects.
A repository secret is enough. pages.yml reads it in its build job, which has no environment,
so an environment secret would need that job attached to one first.
Check that the secret is not empty, because nothing else will tell you:
gpg --armor --export-secret-keys '<KEYID>' | wc -c # thousands of bytes, never 0gh secret set accepts empty stdin and stores a secret that lists normally but expands to nothing;
pages.yml rejects a value that is not an armoured private key block.
The private key exists only in that secret. Back it up somewhere you control: losing it means
generating a new one, and every user who added the old key gets a signature failure on their next
apt update until they re-fetch omnidiff-archive-keyring.gpg. That is also what makes rotation
expensive, so rotate on evidence, not on a schedule.
nix run github:ivankovic/omnidiff works against the repository directly — no tag, no release
artifact, no vendor hash, because cargoLock.lockFile vendors straight from the committed
Cargo.lock. nix develop gives a shell with the toolchain the Makefile targets expect.
A nixpkgs submission would take nix/package.nix as-is but swap src for a fetchFromGitHub call
and cargoLock.lockFile for a cargoHash, since nixpkgs does not carry the lock file. The
maintainers list is deliberately empty until somebody agrees to be on it.
flake.lock is committed, so nix run github:ivankovic/omnidiff builds against one pinned
nixpkgs rather than whatever nixos-unstable is that day. nix flake update moves the pin; do
it deliberately, in a commit of its own.
CI builds the recipe. .github/workflows/nix.yml runs nix flake check and nix build on
every change to the flake, the derivation or the Cargo files, and once a week to catch nixpkgs
moving under the lock file. It then checks the binary runs and the man page and completions are
installed. About twelve minutes uncached, which is why it is its own workflow and not a CI job.
The derivation's check phase runs the library tests under cargo-nextest, as CI and the Makefile
do, with git as a check-time input: the git review tests spawn git and change the working
directory, which is safe in a process of their own and not under plain cargo test.
Building it locally without Nix installed. The official image works through podman or docker; the named volume keeps the store between runs so a retry only rebuilds omnidiff:
podman run --rm -it -v "$PWD":/src -w /src -v omnidiff-nix:/nix docker.io/nixos/nix:latest \
sh -c 'git config --global --add safe.directory /src && \
nix --extra-experimental-features "nix-command flakes" build .#omnidiff -L'
Flakes see only git-tracked files, so a new fixture or source file has to be git added before
the build sees it. The result link it leaves at the root is ignored.
A tap, not homebrew-core: brew install ivankovic/omnidiff/omnidiff taps
ivankovic/homebrew-omnidiff and installs from
it. The formula installs the release tarballs rather than building from source, so a user gets
the same attested binary every other route ships in seconds, instead of compiling every grammar
under fat LTO on their own machine. homebrew-core would not take a binary formula; a personal tap
routinely does. On macOS it installs the Apple Silicon or Intel build; on Linux the static musl
build, which runs on any distribution.
homebrew/omnidiff.rb.in is the source of truth, kept here so it versions with the code. It is a
template: the version and the four checksums come from the release's SHA256SUMS.txt, which
only exists once the release does. scripts/render_homebrew_formula.py fills them in and refuses
to leave a placeholder behind, and release.yml's homebrew job runs it after the checksums job
and pushes Formula/omnidiff.rb to the tap.
The push needs a fine-grained personal access token with Contents: read and write on the
tap repository alone, stored as the HOMEBREW_TAP_TOKEN secret of this repository. Without it the
job prints a warning and the release proceeds; render and push by hand then:
gh release download v<version> --pattern SHA256SUMS.txt
python3 scripts/render_homebrew_formula.py --version <version> --sums SHA256SUMS.txt \
--out ../homebrew-omnidiff/Formula/omnidiff.rbThe man page and completions are generated at install time by the installed binary, as every
other recipe does; brew test omnidiff runs a real headless diff.
- Bump
versioninCargo.toml, thencargo update --workspacesoCargo.lockfollows. python3 scripts/generate_gentoo_crates.py(the ebuild'sCRATESblock) and--manifest(the Manifest's crate digests, from the local cargo cache), andgit mvthe ebuild to the new version.- Update
pkgverinaur/PKGBUILDand the fallbackversioninnix/package.nix.make check-versionspasses once all three agree withCargo.toml;deploy-checksand CI run it too. - Give
CHANGELOG.md's section for the version its release date:## [x.y.z] - YYYY-MM-DD. The release workflow takes the release notes from that section and fails onunreleased. nix flake updateif the nixpkgs pin should move with this release, and commit the lock file; the Nix workflow builds the result before the tag.make deploy— publishes to crates.io, tags, and triggers the release workflow, which creates the release as a draft and publishes it once every asset is attached.- Now that the tag tarball exists, regenerate the two tarball hashes with
updpkgsumsandebuild ... manifest(see "The one thing you cannot skip" above). Not fromSHA256SUMS.txt: that file covers the release assets, and the source tarball is not one. - Push the updated Gentoo recipe to the overlay. The Homebrew tap updates itself from the
release workflow (see "Homebrew" above); check that
Formula/omnidiff.rbnames the new version. - Check that the apt repository picked the release up —
curl -s https://ivankovic.github.io/omnidiff/apt/dists/stable/main/binary-amd64/Packages | grep ^Versionshould name the new version. It refreshes itself (step 2 above), so this is a check, not a task; if it is stale, re-run the Pages workflow.