Managing Dependencies Across Projects
Managing Dependencies Across Projects
Five repos, five copies of desk-checker, five versions. The boring default is: one tooling flake on nixpkgs 26.05 exporting packages and overlays.default; consumers follows that nixpkgs and pin the tooling input.
This is the same idea as the platform-engineering IDP chapter, at package scale: a linter, not a whole OS.
Mental model
desk-tooling (flake)
packages.*.desk-checker
overlays.default
│
▼
billing-service inputs.desk-tooling
inputs.nixpkgs.follows = "desk-tooling/nixpkgs"
# or both follow a platform flake
nix flake update desk-tooling is the upgrade. Not curl | sh. The consumer’s flake.lock records the tooling rev. CI must fetch that git URL, not path:../tooling.
Worked examples
Case 1: Tooling flake
Save as tooling/flake.nix:
# flake.nix
{
description = "Shared desk tooling";
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
outputs = { self, nixpkgs }:
let
system = "x86_64-linux";
pkgs = nixpkgs.legacyPackages.${system};
desk-checker = pkgs.writeShellApplication {
name = "desk-checker";
text = ''
echo "Validating desk organizational standards: OK"
'';
};
in
{
packages.${system}.desk-checker = desk-checker;
packages.${system}.default = desk-checker;
overlays.default = final: prev: {
desk-checker = self.packages.${prev.stdenv.hostPlatform.system}.desk-checker;
};
};
}(If hostPlatform.system is awkward in an overlay, export only packages and reference desk-tooling.packages.${system}.desk-checker from the consumer — often clearer. That is Case 3, and the boring default.)
Case 2: Consumer with follows
Save as project/flake.nix:
# flake.nix
{
description = "Billing service";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05";
desk-tooling.url = "path:../tooling";
desk-tooling.inputs.nixpkgs.follows = "nixpkgs";
};
outputs = { self, nixpkgs, desk-tooling }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
overlays = [ desk-tooling.overlays.default ];
};
in
{
devShells.${system}.default = pkgs.mkShell {
packages = [ pkgs.desk-checker ];
};
};
}cd project
nix develop --command desk-checkerOutput:
Validating desk organizational standards: OK
Case 3: Prefer packages over overlay
devShells.${system}.default = pkgs.mkShell {
packages = [ desk-tooling.packages.${system}.desk-checker ];
};No overlay, no pkgs.desk-checker magic. Overlay when many packages must look like nixpkgs (callPackage trees, override, overrideAttrs). A single checker does not earn an overlay. Overlays that replace stdenv or python3 in a consumer shell are how you get two glibcs and a six-hour rebuild.
Case 4: Update the pin
nix flake update desk-tooling
nix flake check
git diff flake.lockCI on the consumer fails if the tooling flake broke desk-checker. That is the contract. Review the lock diff: one input should move (desk-tooling), and nixpkgs should not move unless you meant nix flake update (everything). follows keeps them on the same nixpkgs; a lock that shows two nixpkgs revs means someone dropped follows.
A scheduled workflow that only runs nix flake update desk-tooling && nix flake check and opens a PR is the boring bump. Humans still merge it.
Case 5: CI uses git, not path:
# flake.nix fragment — CI and humans
{
inputs.desk-tooling.url = "git+ssh://git@git.desk.internal/desk-tooling";
# or: github:desk/desk-tooling
}path:../tooling is for this chapter’s laptop. CI checkouts one repo; path: to a sibling that is not in the workspace is a missing input. Lock the git rev in flake.lock. nix flake metadata must show a git rev, not a dirty path, on the runner.
Private git: runner access-tokens / SSH deploy key (CI secrets chapter), not a token in flake.nix.
The trap
The trap is copying desk-checker.sh into every repo. The fifth copy diverges. One flake.
The other trap is not follows. Tooling on 26.05, app on another nixpkgs, two glibcs in one shell.
A third is overlays for one binary. A fourth is path: in CI, or nix flake update (all inputs) when you meant update desk-tooling.
The boring rule
- One tooling flake. 26.05. Lockfile.
- Export
packages; overlay only if many attrs must look like nixpkgs. Never overlaystdenv“for convenience.” - Consumers
followsnixpkgs. One glibc in the shell. nix flake update desk-toolingis the bump. Reviewflake.lock.- git URL in CI;
path:only locally. Tokens stay in runner config.
Try this
- Two directories as in Cases 1–2;
nix develop --command desk-checker. - Change the echo text in tooling,
updatethe consumer, run again. nix flake metadatain the consumer; one nixpkgs.- Replace the overlay with a direct
packagesreference (Case 3). nix flake metadatain CI logs:desk-toolingis a git rev. If it sayspath:, the workflow checked out the wrong tree.