Canonical source of the shared tooling used by every *-nix packaging repo in
the Daaboulex NixOS package fleet.
This repo is the single source of truth, consumed two ways:
- Nix-side logic (lint/format gate, dev shell, package→check aliasing,
conformance + schema checks) is a flake-parts flakeModule that each repo
imports via a pinned
inputs.std— versioned, type-checked, reproducible. - GitHub Actions YAML + scripts (which cannot be a Nix import) are
bootstrapped into each repo by
sync.sh, and their byte-identity to the canonical is then enforced by thestd-conformanceflake check — no curl-based drift workflow, noraw.githubusercontentdependency.
{
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
git-hooks = {
url = "github:cachix/git-hooks.nix";
inputs.nixpkgs.follows = "nixpkgs";
};
std = {
url = "github:Daaboulex/nix-packaging-standard?ref=v2.4.0"; # pin a tag
inputs.nixpkgs.follows = "nixpkgs";
inputs.git-hooks.follows = "git-hooks";
};
};
outputs =
inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } {
systems = [ "x86_64-linux" ]; # add "aarch64-linux" only if it builds
imports = [ inputs.std.flakeModules.base ]; # + archetype modules as needed
perSystem =
{ pkgs, ... }:
{
packages.default = pkgs.callPackage ./package.nix { };
};
};
}Pin a release tag, never main: an eval-breaking change in the standard
must never reach a repo except by a deliberate, reviewed lock bump.
Every fleet repo follows these. Metadata (description + topics) is declared in
.github/update.json and applied with sync-meta.sh, so it can't silently drift.
- Default branch:
main, and a single branch — themaintenance.ymlcleanup job prunes staleupdate/*branches. - License: the repo's
LICENSEis MIT — it licenses your Nix packaging code, which is permissive and reusable. The packaged software's licence is declared in the derivation'smeta.license, which must be accurate. Two exceptions keep upstream's licence verbatim: forks (the repo vendors upstream source) and derivative transcriptions (e.g. a module that re-expresses GPL config). Proprietary upstreams: MIT packaging +meta.licensemarked unfree. - Topics:
nix,nixos,flakebaseline; addnixos-module/home-managerwhen the repo ships one; then the software name and a few domain topics. Lowercase, hyphenated. - Description:
<Upstream Name> packaged for NixOS — <one concise clause>. HM-only → "… Home Manager module — …"; forks lead with the fork's value-add; original (non-packaging) projects describe themselves directly. ≤120 chars, one clause, no cross-repo references, no marketing fluff.
Packages that track an upstream branch (mesa-git, scx-git) follow the pinned-repo rules above, plus:
- Inherit, don't fork the nixpkgs build closure — override
src/rev/hashes on the nixpkgs derivation; do not fork shared libraries (libdrm, libbpf) or strip the validated driver/build flags. The closure stays in lockstep with nixpkgs and bleeding-edge drift is contained tosrc. - Relax exact-output assertions — an
installCheckthat asserts the precise set of produced binaries/outputs breaks on every upstream add/remove; check "produced a reasonable set" instead. - Crates come from
static.crates.io(thecrates.io/api/v1download endpoint rate-limits CI and 403s);ci.ymlalso retries transient fetch failures once.
| Module | Provides |
|---|---|
base |
git-hooks gate (nixfmt-rfc-style, typos, rumdl, check-readme-sections), formatter, devShells.default, every declared package aliased into checks (so nix flake check BUILDS it), std-conformance (synced files byte-match the canonical), std-update-json (validates .github/update.json against the schema) |
Helpers consumed as inputs.std.lib.*. Module repos add a module-instantiation
check that forces FULL evaluation (options + assertions + every mkIf path)
without building the closure — eval-only and cheap, even in CI.
| Helper | Use case |
|---|---|
nixosModuleCheck { nixpkgs, system, module, config?, overlays? } |
NixOS module repos. overlays supplies the repo's overlay when the module refs overlay-only pkgs. |
homeModuleCheck { nixpkgs, home-manager, system, module, config?, overlays? } |
Home Manager module repos. Imports nixpkgs with config.allowUnfree = true for unfree packages. |
drvEvalCheck { pkgs, name?, drv } |
Packages whose closure is not on cache.nixos.org and cannot build on a free CI runner (CUDA/ROCm, very large builds). Forces drv's full build-graph evaluation without realizing it. See the off-CI exception below. |
Example:
checks.module-eval-nixos = inputs.std.lib.nixosModuleCheck {
inherit (inputs) nixpkgs;
inherit system;
overlays = [ self.overlays.default ];
module = ./module.nix;
config.programs.foo.enable = true;
};| File | Bootstrapped to (per repo) | Purpose |
|---|---|---|
flake.nix |
(not synced) | Exposes flakeModules.* |
flake-modules/base.nix |
(imported, not synced) | The shared flakeModule |
update.sh |
scripts/update.sh |
Detect + apply upstream updates |
ci.yml |
.github/workflows/ci.yml |
Archetype-blind CI (build every output) |
maintenance.yml |
.github/workflows/maintenance.yml |
Weekly flake.lock refresh |
update.yml |
.github/workflows/update.yml |
Scheduled Update workflow |
update.schema.json |
(reference, not synced) | JSON Schema for update.json |
sync.sh |
(run from here) | Bootstrap canonical files into repos |
sync-meta.sh |
(run from here) | Apply repo description + topics from update.json to GitHub |
The synced workflow files + scripts/update.sh are byte-identical fleet-wide
and enforced by std-conformance. Keep them stable across minor standard
releases — evolve via additive flakeModules. A change to a synced file is a
major bump that re-syncs every repo in one coordinated batch.
The standard is designed for dendritic extension — each repo adds what it needs
in its own perSystem, never by patching the standard:
- Module collision (
disabledModules): when nixpkgs ships a module at the same option path, the repo'smodule.nixaddsdisabledModules = [ "programs/foo.nix" ];(streamcontroller, coolercontrol). - Custom devShell:
lib.mkForcethe standard's lint-only default to fold in the repo's own build toolchain (eden: cmake/ninja/ccache). - Typos exclusions:
_typos.tomlwith[files] extend-excludeto skip vendored/forked C++ source (vkBasalt, lmstudio). - Rumdl exclusions/overrides: in
perSystem, extendpre-commit.settings.hooks.rumdl.excludesor.settings.configurationto skip vendored markdown or disable rules the README legitimately triggers (vkBasalt:MD033for inline-HTML screenshots). - Unfree:
config.nixpkgs.config.allowUnfree = truein the module-eval config (lmstudio); standalonepkgsimport withconfig.allowUnfree = trueinperSystem(mesa-git).
One archetype-blind ci.yml, identical fleet-wide. It runs the AI-artifact
guard, then on a [ubuntu-latest, ubuntu-24.04-arm] matrix builds every output
the flake declares for that runner's system via
nix-fast-build --skip-cached — so a repo that declares no outputs for an arch
simply no-ops there (declared == built). There is no per-repo build target, no
archetype conditional, and no binary-cache token: cache.nixos.org substitutes
every unmodified dependency for free.
Exception — off-CI packages. A package whose closure is not on
cache.nixos.org and cannot build on a free runner (CUDA/ROCm toolchains, very
large builds) is exposed via flake.packages.<system> — a real nix build .#x
target — instead of perSystem.packages (which base aliases into checks and
so builds). CI gates it with std.lib.drvEvalCheck, forcing its full
build-graph evaluation without realizing it: CI proves it evaluates (catching
dependency/version breakage) while the heavy build runs off-CI against a project
cache (e.g. cachix use cuda-maintainers). Document that substituter in the
repo README — declare the off-CI build, never silently drop coverage.
git clone https://github.com/Daaboulex/nix-packaging-standard.git
cd nix-packaging-standard
export PKG_REPOS_DIR=/path/to/repos # directory holding the packaging clones
./sync.sh # bootstrap canonical files into all repos
./sync.sh ripgrep-nix # named repos only
./sync.sh --check # report drift, change nothing, exit 1 if anycustom-type repos keep a bespoke scripts/update.sh (skipped here).
Enforcement is the per-repo std-conformance flake check; sync.sh --check is
the same comparison locally.
versionAttrmatches bothversion = "x"and parameterized<attr> ? "x"default-argument forms.versionFiledecouples the version literal's location frompackageFile.revFilescopes therevliteral bump for commit-tracked upstreams.hashesentries list SRI hash fields in evaluation-dependency order (source first), each a bare field name or{"field","file"}to disambiguate.- For commit-tracked packages prefer
versionFile: "version.json". versionSchemecontrols the written version literal;unstable-datewrites<versionBase>-unstable-<YYYY-MM-DD>(commit-tracked only).
github-release · github-tag · github-commit · gitlab-tag ·
gitlab-commit · gitea-commit · git-ls-remote · none · custom
none — module/multi-component repos with nothing to track.
custom — the repo ships its own scripts/update.sh (multi-channel apps, or
non-API sources like OCCT). The canonical update.sh exits 0 early for them;
their bespoke script must honour the same exit contract.
- exit 0 — no update needed, or update applied + verified.
- exit 1 — a real failure (config, version read/write, hash extraction,
build, verification) → workflow opens an
update-failedissue. - exit 2 — network / API error → no issue, retried next run.
- Outputs (to
$GITHUB_OUTPUT):updated,old_version,new_version,package_name,upstream_url,error_type.
- Update success → silent commit + push to the default branch.
- Update failure (
exit 1) →update-failedissue with the build log + a recovery branch; previous failure issues auto-close on the next success. EXIT_CODE=${PIPESTATUS[0]}captures the real exit — nottee's.- Maintenance: weekly
nix flake update, rebuild, push only if green, else open a labeled issue; plus stale-branch cleanup (>30 days).
Pre-2026-05-19 each repo carried its own update.sh and they silently diverged.
The standard was centralized, then promoted to this dedicated repo 2026-05-20.
v2.0.0 (2026-05) replaced file-copy + a curl-based drift-check with the
flake-parts flakeModule + in-flake std-conformance model above.
MIT. See LICENSE.
{ "package": "ripgrep", // package / repo name "upstream": { "type": "...", ... }, // see upstream types below "packageFile": "package.nix", // file `nix build .#default` centers on "versionFile": "flake.nix", // file holding the canonical version // literal (default: packageFile) "versionAttr": "version", // attribute name to match (default // "version"; e.g. "portmasterVersion") "revFile": "package.nix", // file holding the src `rev` literal // (default: versionFile) "versionScheme": "unstable-date", // optional; "literal" (default) or // "unstable-date" (commit-tracked) "versionBase": "2.0.0", // base for "unstable-date" (optional) "hashes": [ // SRI hash fields, dependency order: "hash", // bare name -> auto-located, or { "field": "vendorHash", // {field,file} to disambiguate when "file": "agfs.nix" } // a name appears in several files ], "verify": { "binary": null, "check": "wrapper" } }