name: nix-add-package description: Add a new package to this nixcfg flake using the correct pattern based on source type (flake input, non-flake source, or external binary) license: MIT compatibility: opencode metadata: audience: maintainers workflow: nix
What I do
Guide you through adding a new package to this nixcfg repository. There are three main patterns, each with sub-variants. I help you choose the right one and walk you through every file that needs changes.
When to use me
Use this when you want to add a new package, tool, or application to the Nix configuration. Ask clarifying questions if unsure which pattern to apply.
Repository Architecture
This repo uses flakelight with nixDir = ./. and a vertical per-package layout:
- •
packages/— FlakelightcallPackageconvention. Each file returns a single derivation. Flakelight auto-discovers these, calls them viacallPackage, and adds them to bothpackages.<system>output and the overlay. - •
overlays/— For things that needfinal/prev(overriding existing nixpkgs packages, nested overrides, multi-attr returns).overlays/default.nixauto-imports alloverlays/<name>/default.nixfragments. - •Per-package
sources.json— Each package that needs hashes stores them in<dir>/<name>/sources.jsonas a bare entry (not wrapped in a name key). - •Per-package
updater.py— Each package that needs a custom updater stores it in<dir>/<name>/updater.py. Reusable base classes live inupdate/updaters/.
Decision tree
- •Is the source a Nix flake that exports packages or overlays? -> Pattern 1
- •Is the source a Git repo (flake=false) you build from source? -> Pattern 2
- •Is the source a pre-built binary, .dmg, AppImage, or rolling-release app? -> Pattern 3
Choosing packages/ vs overlays/
- •
packages/<name>.nix— Use when the package is a standalone new derivation that doesn't needfinal/prevoverlay semantics. The file receivescallPackageargs (e.g.,{ lib, stdenv, fetchurl, mkGoCliPackage, ... }:). - •
overlays/<name>/default.nix— Use when you needfinal/prevto override an existing nixpkgs package, accessinputs.*, return multiple attrs, or use builders that referencesources/outputs.
Pattern 1: Flake input with packages/overlays
For upstream flakes that export packages.<system>.default or overlays.default.
Files to modify
flake.nix - Add the input
# In inputs = { ... }:
my-package = {
url = "github:owner/repo";
inputs.nixpkgs.follows = "nixpkgs";
};
Option A: Use upstream overlay (preferred if available)
In flake.nix, add to withOverlays:
withOverlays = [ # ... existing overlays ... inputs.my-package.overlays.default ];
No changes to overlays/default.nix needed.
Option B: Direct package assignment in overlay fragment
Create overlays/my-package/default.nix:
{ inputs, system, ... }:
{
my-package = inputs.my-package.packages.${system}.default;
}
Checklist
- • Add flake input with
inputs.nixpkgs.follows = "nixpkgs" - • Either add overlay to
withOverlaysOR create overlay fragment - • Run
nix flake lock --update-input my-packageto populate lock - • No
sources.jsonor updater changes needed
Pattern 2: Non-flake source input (build from source)
For Git repos where you write a derivation. The input has flake = false;.
Files to modify
flake.nix - Add non-flake input
# With a version tag:
my-tool = {
url = "github:owner/repo/v1.2.3";
flake = false;
};
# Or tracking a branch (no version):
my-tool = {
url = "github:owner/repo";
flake = false;
};
Option A: packages/my-tool.nix (standalone callPackage derivation)
Best for Go CLIs, Python packages, Rust crates — any standalone derivation:
{
lib,
mkGoCliPackage,
inputs,
...
}:
mkGoCliPackage {
pname = "my-tool";
input = inputs.my-tool;
subPackages = [ "cmd/my-tool" ];
cmdName = "my-tool";
meta = with lib; {
description = "Description";
homepage = "https://github.com/owner/repo";
license = licenses.mit;
};
}
Requires vendorHash in per-package sources.json (see below).
Option B: overlays/my-tool/default.nix (overlay fragment)
Best for overriding existing nixpkgs packages or when you need final/prev:
Go override (existing nixpkgs package)
{ inputs, outputs, prev, slib, ... }:
{
my-tool = prev.my-tool.overrideAttrs {
version = slib.getFlakeVersion "my-tool";
src = inputs.my-tool;
vendorHash = slib.sourceHash "my-tool" "vendorHash";
};
}
Cargo/Rust override
{ inputs, prev, slib, ... }:
{
my-tool = prev.my-tool.overrideAttrs (old: {
version = slib.getFlakeVersion "my-tool";
src = inputs.my-tool;
cargoDeps = prev.rustPlatform.fetchCargoVendor {
src = inputs.my-tool;
hash = slib.sourceHash "my-tool" "cargoHash";
};
});
}
npm/Node override
{ inputs, prev, slib, ... }:
let
version = slib.getFlakeVersion "my-tool";
npmDepsHash = slib.sourceHash "my-tool" "npmDepsHash";
npmDeps = prev.fetchNpmDeps {
src = inputs.my-tool;
hash = npmDepsHash;
};
in
{
my-tool = prev.my-tool.overrideAttrs (old: {
inherit version npmDepsHash npmDeps;
src = inputs.my-tool;
});
}
Python (uv2nix)
{ inputs, prev, final, ... }:
{
my-tool = final.mkUv2nixPackage {
name = "my-tool";
src = inputs.my-tool;
mainProgram = "my-tool";
};
}
No sources.json entry needed (uv2nix handles deps).
Vim plugin
# Inside overlays/default.nix vimPlugins extend block:
my-plugin = prev.vimUtils.buildVimPlugin {
pname = "my-plugin";
version = inputs.my-plugin.rev;
src = inputs.my-plugin;
};
Per-package sources.json (if needed)
Only for Go (vendorHash), Cargo (cargoHash), npm (npmDepsHash), Deno (denoDepsHash), or bun (nodeModulesHash) builds. Create packages/my-tool/sources.json or overlays/my-tool/sources.json as a bare entry:
{
"hashes": [
{
"hash": "sha256-PLACEHOLDER",
"hashType": "vendorHash"
}
],
"input": "my-tool"
}
To compute the actual hash, temporarily use lib.fakeHash and build to get the expected hash from the error output, or run nixcfg update my-tool.
Note: If using packages/my-tool.nix (a single file, not a directory), you'll need to create packages/my-tool/ as a directory instead and rename the nix file to packages/my-tool/default.nix so the sources.json can be co-located.
Per-package updater.py (if needed)
Create packages/my-tool/updater.py or overlays/my-tool/updater.py:
# Go:
from lib.update.updaters.base import go_vendor_updater
go_vendor_updater("my-tool")
# Cargo:
from lib.update.updaters.base import cargo_vendor_updater
cargo_vendor_updater("my-tool")
# npm:
from lib.update.updaters.base import npm_deps_updater
npm_deps_updater("my-tool")
# Deno:
from lib.update.updaters.base import deno_deps_updater
deno_deps_updater("my-tool")
# Bun:
from lib.update.updaters.base import bun_node_modules_updater
bun_node_modules_updater("my-tool")
Checklist
- • Add flake input with
flake = false;(and version tag if applicable) - • Add derivation in
packages/oroverlays/as appropriate - • If Go/Cargo/npm/Deno/Bun: add
sources.jsonin the package directory - • If Go/Cargo/npm/Deno/Bun: add
updater.pyin the package directory - • Run
nix flake lock --update-input my-tool - • Build to verify:
nix build .#my-toolornh darwin switch --no-nom .
Pattern 3: External binary / rolling-release app (sources.json)
For pre-built binaries, .dmg apps, AppImages, or apps with their own update APIs (not Git source repos). These are NOT flake inputs.
Files to modify
Per-package sources.json
Create overlays/my-app/sources.json as a bare entry:
{
"hashes": {
"aarch64-darwin": "sha256-...",
"x86_64-darwin": "sha256-...",
"x86_64-linux": "sha256-..."
},
"urls": {
"aarch64-darwin": "https://example.com/my-app-arm64.dmg",
"x86_64-darwin": "https://example.com/my-app-x64.dmg",
"x86_64-linux": "https://example.com/my-app-x64.AppImage"
},
"version": "1.0.0"
}
Compute hashes with: nix-prefetch-url --type sha256 <url> then convert with nix hash convert --hash-algo sha256 --to sri <hash>
overlays/my-app/default.nix - Add overlay fragment
Choose based on delivery format:
Override existing nixpkgs package (simplest)
Uses the mkSourceOverride helper:
{ final, sources, ... }:
{
my-app = final.mkSourceOverride "my-app" prev.my-app;
}
For nested packages:
{ final, prev, sources, ... }:
{
jetbrains = prev.jetbrains // {
my-ide = final.mkSourceOverride "my-ide" prev.jetbrains.my-ide;
};
}
macOS .dmg app
{ final, prev, sources, ... }:
{
my-app = final.mkDmgApp {
pname = "my-app";
info = sources.my-app;
meta = with prev.lib; {
description = "Description";
homepage = "https://example.com";
license = licenses.unfree;
platforms = platforms.darwin;
sourceProvenance = with sourceTypes; [ binaryNativeCode ];
mainProgram = "my-app";
};
};
}
Pre-built binary
{ prev, sources, system, ... }:
let
info = sources.my-tool;
in
{
my-tool = prev.stdenvNoCC.mkDerivation {
pname = "my-tool";
inherit (info) version;
src = prev.fetchurl {
url = info.urls.${system};
hash = info.hashes.${system};
};
dontUnpack = true;
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp $src $out/bin/my-tool
chmod +x $out/bin/my-tool
runHook postInstall
'';
meta = with prev.lib; {
description = "Description";
homepage = "https://example.com";
license = licenses.unfree;
platforms = [ "aarch64-darwin" "x86_64-linux" ];
sourceProvenance = with sourceTypes; [ binaryNativeCode ];
mainProgram = "my-tool";
};
};
}
Single file from GitHub (like homebrew-zsh-completion)
For source-only files, place them under packages/ (same update/discovery flow):
# In packages/ or via outputs.lib:
let
source = outputs.lib.sourceHashEntry "my-config" "sha256";
in
prev.stdenvNoCC.mkDerivation {
name = "my-config";
src = builtins.fetchurl {
inherit (source) url;
sha256 = source.hash;
};
dontUnpack = true;
installPhase = ''
mkdir -p $out
cp $src $out/my-config
'';
};
Per-package updater.py - Add Updater class
Create overlays/my-app/updater.py:
DownloadHashUpdater - must download to compute hash
"""Updater for MyApp."""
import aiohttp
from lib.nix.models.sources import SourceEntry, SourceHashes
from lib.update.net import fetch_json
from lib.update.updaters.base import DownloadHashUpdater, VersionInfo
class MyAppUpdater(DownloadHashUpdater):
"""Update MyApp to latest version."""
name = "my-app"
PLATFORMS = {
"aarch64-darwin": "arm64",
"x86_64-darwin": "x64",
}
async def fetch_latest(self, session: aiohttp.ClientSession) -> VersionInfo:
data = await fetch_json(session, "https://api.example.com/latest")
return VersionInfo(version=data["version"], metadata={})
def get_download_url(self, platform: str, info: VersionInfo) -> str:
return f"https://example.com/download/{info.version}/{self.PLATFORMS[platform]}"
def build_result(self, info: VersionInfo, hashes: SourceHashes) -> SourceEntry:
urls = {p: self.get_download_url(p, info) for p in self.PLATFORMS}
return self._build_result_with_urls(info, hashes, urls)
ChecksumProvidedUpdater - API provides checksums (no download needed)
"""Updater for MyApp."""
import aiohttp
from lib.nix.models.sources import SourceEntry, SourceHashes
from lib.update.net import fetch_json
from lib.update.updaters.base import ChecksumProvidedUpdater, VersionInfo
class MyAppUpdater(ChecksumProvidedUpdater):
"""Update MyApp to latest version."""
name = "my-app"
PLATFORMS = {
"aarch64-darwin": "darwin-arm64",
"x86_64-linux": "linux-x64",
}
async def fetch_latest(self, session: aiohttp.ClientSession) -> VersionInfo:
data = await fetch_json(session, "https://api.example.com/releases/latest")
return VersionInfo(version=data["version"], metadata={"release": data})
async def fetch_checksums(
self, info: VersionInfo, session: aiohttp.ClientSession
) -> dict[str, str]:
release = info.metadata["release"]
return {
platform: release["checksums"][api_key]
for platform, api_key in self.PLATFORMS.items()
}
def build_result(self, info: VersionInfo, hashes: SourceHashes) -> SourceEntry:
urls = {p: f"https://example.com/{v}" for p, v in self.PLATFORMS.items()}
return self._build_result_with_urls(info, hashes, urls)
GitHubRawFileUpdater - single file from a GitHub repo
from lib.update.updaters.github_raw_file import github_raw_file_updater
github_raw_file_updater(
"my-config-file",
owner="owner",
repo="repo",
path="path/to/file",
)
PlatformAPIUpdater - per-platform API with version/checksum fields
"""Updater for MyApp platform builds."""
from lib.update.updaters.base import VersionInfo
from lib.update.updaters.platform_api import PlatformAPIUpdater
class MyAppUpdater(PlatformAPIUpdater):
"""Updater for MyApp builds."""
name = "my-app"
PLATFORMS = {
"aarch64-darwin": "darwin-arm64",
"x86_64-linux": "linux-x64",
}
VERSION_KEY = "productVersion"
CHECKSUM_KEY = "sha256hash"
def _api_url(self, api_platform: str) -> str:
return f"https://api.example.com/update/{api_platform}/latest"
def _download_url(self, api_platform: str, info: VersionInfo) -> str:
return f"https://example.com/{info.version}/{api_platform}"
Checklist
- • Create package directory:
overlays/my-app/orpackages/my-app/ - • Add
sources.jsonwith version, urls, and hashes (bare entry format) - • Add
default.nixoverlay fragment - • Add
updater.pywith Updater class or factory call - • Verify:
nixcfg update --validateandnixcfg update my-app - • Build to verify:
nix build .#my-appornh darwin switch --no-nom .
Key helpers in overlays/default.nix
| Helper | Purpose | Needs sources.json? |
|---|---|---|
mkGoCliPackage | Build Go CLI with completions | Yes (vendorHash) |
mkUv2nixPackage | Build Python package via uv2nix | No |
mkDmgApp | Install macOS .dmg app | Yes (full entry) |
mkSourceOverride | Override nixpkgs pkg version+src | Yes (full entry) |
Key utilities in outputs.lib
| Function | What it does |
|---|---|
outputs.lib.sourceHash "name" "hashType" | Read hash from per-package sources.json by hashType |
outputs.lib.sourceHashEntry "name" "hashType" | Read full hash entry (url + hash) from sources.json |
outputs.lib.sources | Pre-parsed sources entries (aggregated from all per-package files) |
outputs.lib.flakeLock | Pre-parsed flake.lock nodes |
Helper functions in overlays/default.nix
| Function | What it does |
|---|---|
normalizeName s | Replace . and _ with - |
stripVersionPrefix s | Remove v / rust-v prefixes |
Updater factory functions (in update/updaters/base.py)
| Factory | Registers | Hash type |
|---|---|---|
go_vendor_updater(name, ...) | GoVendorHashUpdater | vendorHash |
cargo_vendor_updater(name, ...) | CargoVendorHashUpdater | cargoHash |
npm_deps_updater(name, ...) | NpmDepsHashUpdater | npmDepsHash |
deno_deps_updater(name, ...) | DenoDepsHashUpdater | denoDepsHash |
bun_node_modules_updater(name, ...) | BunNodeModulesHashUpdater | nodeModulesHash |
github_raw_file_updater(name, ...) | GitHubRawFileUpdater | sha256 |
Overlay fragment convention
Each overlay fragment in overlays/<name>/default.nix receives these args and returns an attrset:
{ inputs, outputs, final, prev, system, slib, sources, ... }:
{
# attrs to merge into the overlay
}