Compare commits
29 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0d339e2de0 | ||
|
|
7dcb0d1b3a | ||
|
|
f8658265ae | ||
|
|
c42899c89e | ||
|
|
00fb6ef297 | ||
|
|
dc475afcd1 | ||
|
|
96d2d19046 | ||
|
|
976fc8c1a7 | ||
|
|
30029e5954 | ||
|
|
9edb042e69 | ||
|
|
198b0bb1b0 | ||
|
|
00a9ab240a | ||
|
|
53e498ca45 | ||
|
|
80cc529de7 | ||
|
|
4d2579ae1e | ||
|
|
1399862dec | ||
|
|
ba4a992474 | ||
|
|
aa4a050390 | ||
|
|
b7558a4218 | ||
|
|
f7dce637d5 | ||
|
|
250882da1f | ||
|
|
e445e49baf | ||
|
|
ef3cf30a34 | ||
|
|
86a0792b6e | ||
|
|
d1aea76dd9 | ||
|
|
cdc9e18035 | ||
|
|
374ba596ab | ||
|
|
ffeede1dca | ||
|
|
a7c17bc738 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
||||
.pre-commit-config.yaml
|
||||
lefthook.yml
|
||||
.direnv
|
||||
result
|
||||
template/flake.lock
|
||||
166
README.md
166
README.md
@@ -1,72 +1,170 @@
|
||||
# repo-lib
|
||||
|
||||
Simple Nix flake library for:
|
||||
`repo-lib` is a pure-first Nix flake library for repo-level developer workflows:
|
||||
|
||||
- a shared development shell (`mkDevShell`)
|
||||
- an optional release command (`mkRelease`)
|
||||
- a starter template (`template/`)
|
||||
- `mkRepo` for `devShells`, `checks`, `formatter`, and optional `packages.release`
|
||||
- structured tool banners driven from package-backed tool specs
|
||||
- structured release steps (`writeFile`, `replace`, `run`)
|
||||
- a minimal starter template in [`template/`](/Users/eric/Projects/repo-lib/template)
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [Nix](https://nixos.org/download/) with flakes enabled
|
||||
- [`direnv`](https://direnv.net/) (recommended)
|
||||
|
||||
## Use the template (new repo)
|
||||
|
||||
From your new project folder:
|
||||
## Use the template
|
||||
|
||||
```bash
|
||||
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=v1.0.1#default' --refresh
|
||||
nix flake new myapp -t 'git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.2.0#default' --refresh
|
||||
```
|
||||
|
||||
## Use the library (existing repo)
|
||||
## Use the library
|
||||
|
||||
Add this flake input:
|
||||
|
||||
```nix
|
||||
inputs.devshell-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=v1.0.1";
|
||||
inputs.devshell-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.2.0";
|
||||
inputs.repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||
```
|
||||
|
||||
Create your shell from `mkDevShell`:
|
||||
Build your repo outputs from `mkRepo`:
|
||||
|
||||
```nix
|
||||
env = devshell-lib.lib.mkDevShell {
|
||||
inherit system;
|
||||
extraPackages = [ ];
|
||||
tools = [ ];
|
||||
additionalHooks = { };
|
||||
outputs = { self, nixpkgs, repo-lib, ... }:
|
||||
repo-lib.lib.mkRepo {
|
||||
inherit self nixpkgs;
|
||||
src = ./.;
|
||||
|
||||
config = {
|
||||
checks.tests = {
|
||||
command = "echo 'No tests defined yet.'";
|
||||
stage = "pre-push";
|
||||
passFilenames = false;
|
||||
};
|
||||
|
||||
release = {
|
||||
steps = [ ];
|
||||
};
|
||||
};
|
||||
|
||||
perSystem = { pkgs, system, ... }: {
|
||||
tools = [
|
||||
(repo-lib.lib.tools.fromCommand {
|
||||
name = "Nix";
|
||||
version.args = [ "--version" ];
|
||||
command = "nix";
|
||||
})
|
||||
];
|
||||
|
||||
shell.packages = [
|
||||
self.packages.${system}.release
|
||||
];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
`mkRepo` generates:
|
||||
|
||||
- `devShells.${system}.default`
|
||||
- `checks.${system}.hook-check`
|
||||
- `checks.${system}.lefthook-check`
|
||||
- `formatter.${system}`
|
||||
- `packages.${system}.release` when `config.release != null`
|
||||
- merged `packages` and `apps` from `perSystem`
|
||||
|
||||
Checks are installed through `lefthook`, with `pre-commit` and `pre-push` commands configured to run in parallel.
|
||||
|
||||
## Tool banners
|
||||
|
||||
Tools are declared once. Package-backed tools are added to the shell automatically, and both package-backed and command-backed tools are rendered in the startup banner.
|
||||
|
||||
```nix
|
||||
(repo-lib.lib.tools.fromPackage {
|
||||
name = "Go";
|
||||
package = pkgs.go;
|
||||
version.args = [ "version" ];
|
||||
banner.color = "CYAN";
|
||||
})
|
||||
```
|
||||
|
||||
Required tools fail shell startup if their version probe fails. This keeps banner output honest instead of silently hiding misconfiguration.
|
||||
|
||||
When a tool should come from the host environment instead of `nixpkgs`, use `fromCommand`:
|
||||
|
||||
```nix
|
||||
(repo-lib.lib.tools.fromCommand {
|
||||
name = "Nix";
|
||||
command = "nix";
|
||||
version.args = [ "--version" ];
|
||||
})
|
||||
```
|
||||
|
||||
## Purity model
|
||||
|
||||
The default path is pure: declare tools and packages in Nix, then let `mkRepo` assemble the shell.
|
||||
|
||||
Impure bootstrap work is still possible, but it must be explicit:
|
||||
|
||||
```nix
|
||||
config.shell = {
|
||||
bootstrap = ''
|
||||
export GOBIN="$PWD/.tools/bin"
|
||||
export PATH="$GOBIN:$PATH"
|
||||
'';
|
||||
allowImpureBootstrap = true;
|
||||
};
|
||||
```
|
||||
|
||||
Expose it in `devShells` as `default` and run:
|
||||
## Release steps
|
||||
|
||||
```bash
|
||||
nix develop
|
||||
```
|
||||
|
||||
## Common commands
|
||||
|
||||
```bash
|
||||
nix fmt # format files
|
||||
```
|
||||
|
||||
## Optional: release command
|
||||
|
||||
If your flake defines:
|
||||
Structured release steps are preferred over raw `sed` snippets:
|
||||
|
||||
```nix
|
||||
packages.${system}.release = devshell-lib.lib.mkRelease { inherit system; };
|
||||
config.release = {
|
||||
steps = [
|
||||
{
|
||||
writeFile = {
|
||||
path = "src/version.ts";
|
||||
text = ''
|
||||
export const APP_VERSION = "$FULL_VERSION" as const;
|
||||
'';
|
||||
};
|
||||
}
|
||||
{
|
||||
replace = {
|
||||
path = "README.md";
|
||||
regex = ''^(version = ")[^"]*(")$'';
|
||||
replacement = ''\1$FULL_VERSION\2'';
|
||||
};
|
||||
}
|
||||
{
|
||||
run = {
|
||||
script = ''
|
||||
echo "Released $FULL_TAG"
|
||||
'';
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
```
|
||||
|
||||
Run releases with:
|
||||
The generated `release` command still supports:
|
||||
|
||||
```bash
|
||||
release
|
||||
release patch
|
||||
release beta
|
||||
release minor beta
|
||||
release stable
|
||||
release set 1.2.3
|
||||
```
|
||||
|
||||
The release script uses `./VERSION` as the source of truth and creates tags like `v1.2.3`.
|
||||
## Low-level APIs
|
||||
|
||||
`mkDevShell` and `mkRelease` remain available for repos that want lower-level control or a migration path from the older library shape.
|
||||
|
||||
## Common command
|
||||
|
||||
```bash
|
||||
nix fmt
|
||||
```
|
||||
|
||||
77
flake.lock
generated
77
flake.lock
generated
@@ -1,79 +1,26 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-compat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
"lastModified": 1767039857,
|
||||
"narHash": "sha256-vNpUSpF5Nuw8xvDLj2KCwwksIbjua2LZCqhV1LNRDns=",
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"rev": "5edf11c44bc78a0d334f6334cdaf7d60d732daab",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "flake-compat",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"git-hooks": {
|
||||
"inputs": {
|
||||
"flake-compat": "flake-compat",
|
||||
"gitignore": "gitignore",
|
||||
"nixpkgs": "nixpkgs"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1772024342,
|
||||
"narHash": "sha256-+eXlIc4/7dE6EcPs9a2DaSY3fTA9AE526hGqkNID3Wg=",
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"rev": "6e34e97ed9788b17796ee43ccdbaf871a5c2b476",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "cachix",
|
||||
"repo": "git-hooks.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"gitignore": {
|
||||
"lefthook-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": [
|
||||
"git-hooks",
|
||||
"nixpkgs"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1709087332,
|
||||
"narHash": "sha256-HG2cCnktfHsKV0s4XW83gU3F57gaTljL9KNSuG6bnQs=",
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
|
||||
"lastModified": 1770377107,
|
||||
"narHash": "sha256-/QEXSDeAo5RK81PtM0yDhmt9k3v1/pse/jsrT1yXNhU=",
|
||||
"owner": "sudosubin",
|
||||
"repo": "lefthook.nix",
|
||||
"rev": "9cdaf7ce95ae77cbabc5b556bdd35d3cf0b849f5",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "hercules-ci",
|
||||
"repo": "gitignore.nix",
|
||||
"owner": "sudosubin",
|
||||
"repo": "lefthook.nix",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1770073757,
|
||||
"narHash": "sha256-Vy+G+F+3E/Tl+GMNgiHl9Pah2DgShmIUBJXmbiQPHbI=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "47472570b1e607482890801aeaf29bfb749884f6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixpkgs-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1772542754,
|
||||
"narHash": "sha256-WGV2hy+VIeQsYXpsLjdr4GvHv5eECMISX1zKLTedhdg=",
|
||||
@@ -89,7 +36,7 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_3": {
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1770107345,
|
||||
"narHash": "sha256-tbS0Ebx2PiA1FRW8mt8oejR0qMXmziJmPaU1d4kYY9g=",
|
||||
@@ -107,14 +54,14 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"git-hooks": "git-hooks",
|
||||
"nixpkgs": "nixpkgs_2",
|
||||
"lefthook-nix": "lefthook-nix",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"treefmt-nix": "treefmt-nix"
|
||||
}
|
||||
},
|
||||
"treefmt-nix": {
|
||||
"inputs": {
|
||||
"nixpkgs": "nixpkgs_3"
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1770228511,
|
||||
|
||||
361
flake.nix
361
flake.nix
@@ -1,10 +1,11 @@
|
||||
# flake.nix — devshell-lib
|
||||
# flake.nix — repo-lib
|
||||
{
|
||||
description = "Shared devshell boilerplate library";
|
||||
description = "Pure-first repo development platform for Nix flakes";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
git-hooks.url = "github:cachix/git-hooks.nix";
|
||||
lefthook-nix.url = "github:sudosubin/lefthook.nix";
|
||||
lefthook-nix.inputs.nixpkgs.follows = "nixpkgs";
|
||||
treefmt-nix.url = "github:numtide/treefmt-nix";
|
||||
};
|
||||
|
||||
@@ -13,298 +14,118 @@
|
||||
self,
|
||||
nixpkgs,
|
||||
treefmt-nix,
|
||||
git-hooks,
|
||||
lefthook-nix,
|
||||
...
|
||||
}:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
lib = {
|
||||
lib = nixpkgs.lib;
|
||||
repoLib = import ./packages/repo-lib/lib.nix {
|
||||
inherit nixpkgs treefmt-nix;
|
||||
lefthookNix = lefthook-nix;
|
||||
releaseScriptPath = ./packages/release/release.sh;
|
||||
shellHookTemplatePath = ./packages/repo-lib/shell-hook.sh;
|
||||
};
|
||||
supportedSystems = repoLib.systems.default;
|
||||
importPkgs = nixpkgsInput: system: import nixpkgsInput { inherit system; };
|
||||
|
||||
# ── mkDevShell ───────────────────────────────────────────────────────
|
||||
mkDevShell =
|
||||
{
|
||||
system,
|
||||
extraPackages ? [ ],
|
||||
extraShellHook ? "",
|
||||
additionalHooks ? { },
|
||||
tools ? [ ],
|
||||
includeStandardPackages ? true,
|
||||
# tools = list of { name, bin, versionCmd, color? }
|
||||
# e.g. { name = "Bun"; bin = "${pkgs.bun}/bin/bun"; versionCmd = "--version"; color = "YELLOW"; }
|
||||
formatters ? { },
|
||||
# formatters = treefmt-nix programs attrset, merged over { nixfmt.enable = true; }
|
||||
# e.g. { gofmt.enable = true; shfmt.enable = true; }
|
||||
formatterSettings ? { },
|
||||
# formatterSettings = treefmt-nix settings.formatter attrset
|
||||
# e.g. { shfmt.options = [ "-i" "2" "-s" "-w" ]; }
|
||||
features ? { },
|
||||
# features.oxfmt = true → adds pkgs.oxfmt + pkgs.oxlint, enables oxfmt in treefmt
|
||||
}:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
standardPackages = with pkgs; [
|
||||
nixfmt
|
||||
gitlint
|
||||
gitleaks
|
||||
shfmt
|
||||
];
|
||||
selectedStandardPackages = pkgs.lib.optionals includeStandardPackages standardPackages;
|
||||
|
||||
oxfmtEnabled = features.oxfmt or false;
|
||||
oxfmtPackages = pkgs.lib.optionals oxfmtEnabled [
|
||||
pkgs.oxfmt
|
||||
pkgs.oxlint
|
||||
];
|
||||
oxfmtFormatters = pkgs.lib.optionalAttrs oxfmtEnabled {
|
||||
oxfmt.enable = true;
|
||||
};
|
||||
|
||||
treefmtEval = treefmt-nix.lib.evalModule pkgs {
|
||||
projectRootFile = "flake.nix";
|
||||
programs = {
|
||||
nixfmt.enable = true; # always on — every repo has a flake.nix
|
||||
}
|
||||
// oxfmtFormatters
|
||||
// formatters;
|
||||
settings.formatter = { } // formatterSettings;
|
||||
};
|
||||
|
||||
pre-commit-check = git-hooks.lib.${system}.run {
|
||||
src = ./.;
|
||||
hooks = {
|
||||
treefmt = {
|
||||
enable = true;
|
||||
entry = "${treefmtEval.config.build.wrapper}/bin/treefmt";
|
||||
pass_filenames = true;
|
||||
};
|
||||
gitlint.enable = true;
|
||||
gitleaks = {
|
||||
enable = true;
|
||||
entry = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
|
||||
pass_filenames = false;
|
||||
projectOutputs = repoLib.mkRepo {
|
||||
inherit self nixpkgs;
|
||||
src = ./.;
|
||||
config = {
|
||||
release = {
|
||||
steps = [
|
||||
{
|
||||
replace = {
|
||||
path = "template/flake.nix";
|
||||
regex = ''^([[:space:]]*repo-lib\.url = ")git\+https://git\.dgren\.dev/eric/nix-flake-lib[^"]*(";)'';
|
||||
replacement = ''\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/$FULL_TAG\2'';
|
||||
};
|
||||
}
|
||||
// additionalHooks;
|
||||
};
|
||||
|
||||
toolBannerScript = pkgs.lib.concatMapStrings (
|
||||
t:
|
||||
let
|
||||
colorVar = "$" + (t.color or "YELLOW");
|
||||
in
|
||||
''
|
||||
if command -v ${t.bin} >/dev/null 2>&1; then
|
||||
printf " $CYAN ${t.name}:$RESET\t${colorVar}%s$RESET\n" "$(${t.bin} ${t.versionCmd})"
|
||||
fi
|
||||
''
|
||||
) tools;
|
||||
|
||||
in
|
||||
{
|
||||
inherit pre-commit-check;
|
||||
|
||||
formatter = treefmtEval.config.build.wrapper;
|
||||
|
||||
shell = pkgs.mkShell {
|
||||
packages = selectedStandardPackages ++ extraPackages ++ oxfmtPackages;
|
||||
|
||||
buildInputs = pre-commit-check.enabledPackages;
|
||||
|
||||
shellHook = ''
|
||||
${pre-commit-check.shellHook}
|
||||
|
||||
if [ -t 1 ]; then
|
||||
command -v tput >/dev/null 2>&1 && tput clear || printf '\033c'
|
||||
fi
|
||||
|
||||
GREEN='\033[1;32m'
|
||||
CYAN='\033[1;36m'
|
||||
YELLOW='\033[1;33m'
|
||||
BLUE='\033[1;34m'
|
||||
RESET='\033[0m'
|
||||
|
||||
printf "\n$GREEN 🚀 Dev shell ready$RESET\n\n"
|
||||
${toolBannerScript}
|
||||
printf "\n"
|
||||
|
||||
${extraShellHook}
|
||||
'';
|
||||
};
|
||||
{
|
||||
replace = {
|
||||
path = "README.md";
|
||||
regex = ''(nix flake new myapp -t ')git\+https://git\.dgren\.dev/eric/nix-flake-lib[^']*(#default' --refresh)'';
|
||||
replacement = ''\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/$FULL_TAG\2'';
|
||||
};
|
||||
}
|
||||
{
|
||||
replace = {
|
||||
path = "README.md";
|
||||
regex = ''^([[:space:]]*inputs\.repo-lib\.url = ")git\+https://git\.dgren\.dev/eric/nix-flake-lib[^"]*(";)'';
|
||||
replacement = ''\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/$FULL_TAG\2'';
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
|
||||
# ── mkRelease ────────────────────────────────────────────────────────
|
||||
mkRelease =
|
||||
};
|
||||
perSystem =
|
||||
{
|
||||
pkgs,
|
||||
system,
|
||||
# Source of truth is always $ROOT_DIR/VERSION.
|
||||
# Format:
|
||||
# line 1: X.Y.Z
|
||||
# line 2: CHANNEL (stable|alpha|beta|rc|internal|...)
|
||||
# line 3: N (prerelease number, 0 for stable)
|
||||
postVersion ? "",
|
||||
# Shell string — runs after VERSION + release steps are written/run, before git add.
|
||||
# Same env vars available.
|
||||
release ? [ ],
|
||||
# Unified list processed in declaration order:
|
||||
# { file = "path/to/file"; content = ''...$FULL_VERSION...''; } # write file
|
||||
# { run = ''...shell snippet...''; } # run script
|
||||
# Example:
|
||||
# release = [
|
||||
# {
|
||||
# file = "src/version.ts";
|
||||
# content = ''export const APP_VERSION = "$FULL_VERSION" as const;'';
|
||||
# }
|
||||
# {
|
||||
# file = "internal/version/version.go";
|
||||
# content = ''
|
||||
# package version
|
||||
#
|
||||
# const Version = "$FULL_VERSION"
|
||||
# '';
|
||||
# }
|
||||
# {
|
||||
# run = ''
|
||||
# sed -E -i "s#^([[:space:]]*my-lib\\.url = \")github:org/my-lib[^"]*(\";)#\\1github:org/my-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/flake.nix"
|
||||
# '';
|
||||
# }
|
||||
# ];
|
||||
# Runtime env includes: BASE_VERSION, CHANNEL, PRERELEASE_NUM, FULL_VERSION, FULL_TAG.
|
||||
channels ? [
|
||||
"alpha"
|
||||
"beta"
|
||||
"rc"
|
||||
"internal"
|
||||
],
|
||||
# Valid release channels beyond "stable". Validated at runtime.
|
||||
extraRuntimeInputs ? [ ],
|
||||
# Extra packages available in the release script's PATH.
|
||||
...
|
||||
}:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
channelList = pkgs.lib.concatStringsSep " " channels;
|
||||
{
|
||||
tools = [
|
||||
(repoLib.tools.fromCommand {
|
||||
name = "Nix";
|
||||
command = "nix";
|
||||
version = {
|
||||
args = [ "--version" ];
|
||||
group = 1;
|
||||
};
|
||||
banner = {
|
||||
color = "BLUE";
|
||||
icon = "";
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
releaseStepsScript = pkgs.lib.concatMapStrings (
|
||||
entry:
|
||||
if entry ? file then
|
||||
''
|
||||
mkdir -p "$(dirname "${entry.file}")"
|
||||
cat > "${entry.file}" << NIXEOF
|
||||
${entry.content}
|
||||
NIXEOF
|
||||
log "Generated version file: ${entry.file}"
|
||||
''
|
||||
else if entry ? run then
|
||||
''
|
||||
${entry.run}
|
||||
''
|
||||
else
|
||||
builtins.throw "release entry must have either 'file' or 'run'"
|
||||
) release;
|
||||
|
||||
script =
|
||||
builtins.replaceStrings
|
||||
[
|
||||
"__CHANNEL_LIST__"
|
||||
"__RELEASE_STEPS__"
|
||||
"__POST_VERSION__"
|
||||
]
|
||||
[
|
||||
channelList
|
||||
releaseStepsScript
|
||||
postVersion
|
||||
]
|
||||
(builtins.readFile ./packages/release/release.sh);
|
||||
in
|
||||
pkgs.writeShellApplication {
|
||||
name = "release";
|
||||
runtimeInputs =
|
||||
with pkgs;
|
||||
[
|
||||
git
|
||||
gnugrep
|
||||
gawk
|
||||
gnused
|
||||
coreutils
|
||||
]
|
||||
++ extraRuntimeInputs;
|
||||
text = script;
|
||||
shell.packages = [ self.packages.${system}.release ];
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
# ── packages ────────────────────────────────────────────────────────────
|
||||
packages = forAllSystems (system: {
|
||||
# Expose a no-op release package for the lib repo itself (dogfood)
|
||||
release = self.lib.mkRelease {
|
||||
inherit system;
|
||||
release = [
|
||||
{
|
||||
run = ''
|
||||
sed -E -i "s#^([[:space:]]*devshell-lib\\.url = \")git\\+https://git\\.dgren\\.dev/eric/nix-flake-lib[^\"]*(\";)#\\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/template/flake.nix"
|
||||
log "Updated template/flake.nix devshell-lib ref to $FULL_TAG"
|
||||
|
||||
sed -E -i "s|(nix flake new myapp -t ')git\\+https://git\\.dgren\\.dev/eric/nix-flake-lib[^']*(#default' --refresh)|\\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=$FULL_TAG\\2|" "$ROOT_DIR/README.md"
|
||||
sed -E -i "s#^([[:space:]]*inputs\\.devshell-lib\\.url = \")git\\+https://git\\.dgren\\.dev/eric/nix-flake-lib[^\"]*(\";)#\\1git+https://git.dgren.dev/eric/nix-flake-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/README.md"
|
||||
log "Updated README.md devshell-lib refs to $FULL_TAG"
|
||||
'';
|
||||
}
|
||||
];
|
||||
};
|
||||
});
|
||||
|
||||
# ── devShells ───────────────────────────────────────────────────────────
|
||||
devShells = forAllSystems (
|
||||
testChecks = lib.genAttrs supportedSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
env = self.lib.mkDevShell {
|
||||
inherit system;
|
||||
extraPackages = with pkgs; [
|
||||
self.packages.${system}.release
|
||||
];
|
||||
tools = [
|
||||
pkgs = importPkgs nixpkgs system;
|
||||
in
|
||||
{
|
||||
release-tests =
|
||||
pkgs.runCommand "release-tests"
|
||||
{
|
||||
name = "Nix";
|
||||
bin = "${pkgs.nix}/bin/nix";
|
||||
versionCmd = "--version";
|
||||
color = "YELLOW";
|
||||
nativeBuildInputs = with pkgs; [
|
||||
bash
|
||||
git
|
||||
nix
|
||||
gnused
|
||||
coreutils
|
||||
gnugrep
|
||||
perl
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
in
|
||||
{
|
||||
default = env.shell;
|
||||
''
|
||||
export REPO_LIB_ROOT=${./.}
|
||||
export NIXPKGS_FLAKE_PATH=${nixpkgs}
|
||||
export HOME="$TMPDIR"
|
||||
${pkgs.bash}/bin/bash ${./tests/release.sh}
|
||||
touch "$out"
|
||||
'';
|
||||
}
|
||||
);
|
||||
in
|
||||
projectOutputs
|
||||
// {
|
||||
lib = repoLib;
|
||||
|
||||
# ── checks ──────────────────────────────────────────────────────────────
|
||||
checks = forAllSystems (
|
||||
system:
|
||||
let
|
||||
env = self.lib.mkDevShell { inherit system; };
|
||||
in
|
||||
{
|
||||
inherit (env) pre-commit-check;
|
||||
}
|
||||
);
|
||||
|
||||
# ── formatter ───────────────────────────────────────────────────────────
|
||||
formatter = forAllSystems (system: (self.lib.mkDevShell { inherit system; }).formatter);
|
||||
|
||||
# ── templates ───────────────────────────────────────────────────────────
|
||||
templates = {
|
||||
default = {
|
||||
path = ./template;
|
||||
description = "Product repo using devshell-lib";
|
||||
description = "Product repo using repo-lib";
|
||||
};
|
||||
};
|
||||
|
||||
checks = lib.genAttrs supportedSystems (
|
||||
system: projectOutputs.checks.${system} // testChecks.${system}
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,57 +1,16 @@
|
||||
# release.nix
|
||||
{
|
||||
pkgs,
|
||||
postVersion ? "",
|
||||
release ? [ ],
|
||||
# Unified list, processed in declaration order:
|
||||
# { file = "path/to/file"; content = "..."; } — write file
|
||||
# { run = "shell snippet..."; } — run script
|
||||
channels ? [
|
||||
"alpha"
|
||||
"beta"
|
||||
"rc"
|
||||
"internal"
|
||||
],
|
||||
extraRuntimeInputs ? [ ],
|
||||
nixpkgs,
|
||||
treefmt-nix,
|
||||
lefthookNix,
|
||||
releaseScriptPath ? ./release.sh,
|
||||
shellHookTemplatePath ? ../repo-lib/shell-hook.sh,
|
||||
}:
|
||||
let
|
||||
channelList = pkgs.lib.concatStringsSep " " channels;
|
||||
|
||||
releaseScript = pkgs.lib.concatMapStrings (
|
||||
entry:
|
||||
if entry ? file then
|
||||
''
|
||||
mkdir -p "$(dirname "${entry.file}")"
|
||||
cat > "${entry.file}" << NIXEOF
|
||||
${entry.content}
|
||||
NIXEOF
|
||||
log "Generated version file: ${entry.file}"
|
||||
''
|
||||
else if entry ? run then
|
||||
''
|
||||
${entry.run}
|
||||
''
|
||||
else
|
||||
builtins.throw "release entry must have either 'file' or 'run'"
|
||||
) release;
|
||||
|
||||
script =
|
||||
builtins.replaceStrings
|
||||
[ "__CHANNEL_LIST__" "__RELEASE_STEPS__" "__POST_VERSION__" ]
|
||||
[ channelList releaseScript postVersion ]
|
||||
(builtins.readFile ./release.sh);
|
||||
in
|
||||
pkgs.writeShellApplication {
|
||||
name = "release";
|
||||
runtimeInputs =
|
||||
with pkgs;
|
||||
[
|
||||
git
|
||||
gnugrep
|
||||
gawk
|
||||
gnused
|
||||
coreutils
|
||||
]
|
||||
++ extraRuntimeInputs;
|
||||
text = script;
|
||||
import ../repo-lib/lib.nix {
|
||||
inherit
|
||||
nixpkgs
|
||||
treefmt-nix
|
||||
lefthookNix
|
||||
releaseScriptPath
|
||||
shellHookTemplatePath
|
||||
;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ ROOT_DIR="$(git rev-parse --show-toplevel)"
|
||||
GITLINT_FILE="$ROOT_DIR/.gitlint"
|
||||
START_HEAD=""
|
||||
CREATED_TAG=""
|
||||
VERSION_META_LINES=()
|
||||
VERSION_META_EXPORT_NAMES=()
|
||||
|
||||
# ── logging ────────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -22,15 +24,20 @@ usage() {
|
||||
"Bump types:" \
|
||||
" (none) bump patch, keep current channel" \
|
||||
" major/minor/patch bump the given part, keep current channel" \
|
||||
" stable / full remove prerelease suffix" \
|
||||
" __CHANNEL_LIST__ switch channel (bumps prerelease number if same base+channel)" \
|
||||
" stable / full remove prerelease suffix (only opt-in path to promote prerelease -> stable)" \
|
||||
" __CHANNEL_LIST__ switch channel (from stable, auto-bumps patch unless bump is specified)" \
|
||||
"" \
|
||||
"Safety rule:" \
|
||||
" If current version is prerelease (e.g. x.y.z-beta.N), promotion to stable is allowed only via 'stable' or 'full'." \
|
||||
" Commands like '${cmd} set x.y.z' or '${cmd} patch stable' are blocked from prerelease channels." \
|
||||
"" \
|
||||
"Examples:" \
|
||||
" ${cmd} # patch bump on current channel" \
|
||||
" ${cmd} minor # minor bump on current channel" \
|
||||
" ${cmd} beta # from stable: patch bump + beta.1" \
|
||||
" ${cmd} patch beta # patch bump, switch to beta channel" \
|
||||
" ${cmd} rc # switch to rc channel" \
|
||||
" ${cmd} stable # promote to stable release" \
|
||||
" ${cmd} stable # promote prerelease to stable (opt-in)" \
|
||||
" ${cmd} set 1.2.3" \
|
||||
" ${cmd} set 1.2.3-beta.1"
|
||||
}
|
||||
@@ -163,6 +170,119 @@ compute_full_version() {
|
||||
export BASE_VERSION CHANNEL PRERELEASE_NUM FULL_VERSION FULL_TAG
|
||||
}
|
||||
|
||||
meta_env_name() {
|
||||
local key="$1"
|
||||
key="${key//[^[:alnum:]]/_}"
|
||||
key="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||
printf 'VERSION_META_%s\n' "$key"
|
||||
}
|
||||
|
||||
clear_version_meta_exports() {
|
||||
local export_name
|
||||
for export_name in "${VERSION_META_EXPORT_NAMES[@]:-}"; do
|
||||
unset "$export_name"
|
||||
done
|
||||
VERSION_META_EXPORT_NAMES=()
|
||||
}
|
||||
|
||||
load_version_metadata() {
|
||||
VERSION_META_LINES=()
|
||||
[[ ! -f "$ROOT_DIR/VERSION" ]] && return 0
|
||||
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
VERSION_META_LINES+=("$line")
|
||||
done < <(tail -n +4 "$ROOT_DIR/VERSION" 2>/dev/null || true)
|
||||
}
|
||||
|
||||
export_version_metadata() {
|
||||
clear_version_meta_exports
|
||||
|
||||
local line key value export_name
|
||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
||||
[[ $line != *=* ]] && continue
|
||||
key="${line%%=*}"
|
||||
value="${line#*=}"
|
||||
[[ -z $key ]] && continue
|
||||
export_name="$(meta_env_name "$key")"
|
||||
printf -v "$export_name" '%s' "$value"
|
||||
export "${export_name?}=$value"
|
||||
VERSION_META_EXPORT_NAMES+=("$export_name")
|
||||
done
|
||||
}
|
||||
|
||||
write_version_file() {
|
||||
local channel_to_write="$1"
|
||||
local n_to_write="$2"
|
||||
{
|
||||
printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write"
|
||||
local line
|
||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
||||
printf '%s\n' "$line"
|
||||
done
|
||||
} >"$ROOT_DIR/VERSION"
|
||||
}
|
||||
|
||||
version_meta_get() {
|
||||
local key="${1-}"
|
||||
local line
|
||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
||||
if [[ $line == "$key="* ]]; then
|
||||
printf '%s\n' "${line#*=}"
|
||||
return 0
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
version_meta_set() {
|
||||
local key="${1-}"
|
||||
local value="${2-}"
|
||||
[[ -z $key ]] && echo "Error: version_meta_set requires a key" >&2 && exit 1
|
||||
|
||||
local updated=0
|
||||
local index
|
||||
for index in "${!VERSION_META_LINES[@]}"; do
|
||||
if [[ ${VERSION_META_LINES[index]} == "$key="* ]]; then
|
||||
VERSION_META_LINES[index]="$key=$value"
|
||||
updated=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $updated -eq 0 ]]; then
|
||||
VERSION_META_LINES+=("$key=$value")
|
||||
fi
|
||||
|
||||
export_version_metadata
|
||||
version_meta_write
|
||||
}
|
||||
|
||||
version_meta_unset() {
|
||||
local key="${1-}"
|
||||
[[ -z $key ]] && echo "Error: version_meta_unset requires a key" >&2 && exit 1
|
||||
|
||||
local filtered=()
|
||||
local line
|
||||
for line in "${VERSION_META_LINES[@]:-}"; do
|
||||
[[ $line == "$key="* ]] && continue
|
||||
filtered+=("$line")
|
||||
done
|
||||
VERSION_META_LINES=("${filtered[@]}")
|
||||
|
||||
export_version_metadata
|
||||
version_meta_write
|
||||
}
|
||||
|
||||
version_meta_write() {
|
||||
local channel_to_write="$CHANNEL"
|
||||
local n_to_write="${PRERELEASE_NUM:-1}"
|
||||
if [[ $channel_to_write == "stable" || -z $channel_to_write ]]; then
|
||||
channel_to_write="stable"
|
||||
n_to_write="0"
|
||||
fi
|
||||
write_version_file "$channel_to_write" "$n_to_write"
|
||||
}
|
||||
|
||||
# ── gitlint ────────────────────────────────────────────────────────────────
|
||||
|
||||
get_gitlint_title_regex() {
|
||||
@@ -200,6 +320,8 @@ run_release_steps() {
|
||||
# and never contaminates the stdout of do_read_version.
|
||||
init_version_file() {
|
||||
if [[ -f "$ROOT_DIR/VERSION" ]]; then
|
||||
load_version_metadata
|
||||
export_version_metadata
|
||||
return 0
|
||||
fi
|
||||
|
||||
@@ -228,11 +350,16 @@ init_version_file() {
|
||||
n_to_write="0"
|
||||
fi
|
||||
|
||||
printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write" >"$ROOT_DIR/VERSION"
|
||||
VERSION_META_LINES=()
|
||||
write_version_file "$channel_to_write" "$n_to_write"
|
||||
export_version_metadata
|
||||
log "Initialized $ROOT_DIR/VERSION from highest tag: v$highest_tag"
|
||||
}
|
||||
|
||||
do_read_version() {
|
||||
load_version_metadata
|
||||
export_version_metadata
|
||||
|
||||
local base_line channel_line n_line
|
||||
base_line="$(sed -n '1p' "$ROOT_DIR/VERSION" | tr -d '\r')"
|
||||
channel_line="$(sed -n '2p' "$ROOT_DIR/VERSION" | tr -d '\r')"
|
||||
@@ -252,7 +379,8 @@ do_write_version() {
|
||||
channel_to_write="stable"
|
||||
n_to_write="0"
|
||||
fi
|
||||
printf '%s\n%s\n%s\n' "$BASE_VERSION" "$channel_to_write" "$n_to_write" >"$ROOT_DIR/VERSION"
|
||||
write_version_file "$channel_to_write" "$n_to_write"
|
||||
export_version_metadata
|
||||
}
|
||||
|
||||
# ── user-provided hook ─────────────────────────────────────────────────────
|
||||
@@ -282,6 +410,8 @@ main() {
|
||||
exit 1
|
||||
fi
|
||||
parse_full_version "$raw_version"
|
||||
compute_full_version
|
||||
local current_full="$FULL_VERSION"
|
||||
|
||||
log "Current: base=$BASE_VERSION channel=$CHANNEL pre=${PRERELEASE_NUM:-}"
|
||||
|
||||
@@ -290,11 +420,14 @@ main() {
|
||||
|
||||
if [[ $action == "set" ]]; then
|
||||
local newv="${1-}"
|
||||
local current_channel="$CHANNEL"
|
||||
[[ -z $newv ]] && echo "Error: 'set' requires a version argument" >&2 && exit 1
|
||||
compute_full_version
|
||||
local current_full="$FULL_VERSION"
|
||||
parse_full_version "$newv"
|
||||
validate_channel "$CHANNEL"
|
||||
if [[ $current_channel != "stable" && $CHANNEL == "stable" ]]; then
|
||||
echo "Error: from prerelease channel '$current_channel', promote using 'stable' or 'full' only" >&2
|
||||
exit 1
|
||||
fi
|
||||
compute_full_version
|
||||
local cmp_status=0
|
||||
version_cmp "$FULL_VERSION" "$current_full" || cmp_status=$?
|
||||
@@ -310,7 +443,7 @@ main() {
|
||||
esac
|
||||
|
||||
else
|
||||
local part="" target_channel=""
|
||||
local part="" target_channel="" was_channel_only=0
|
||||
|
||||
case "$action" in
|
||||
"") part="patch" ;;
|
||||
@@ -331,6 +464,7 @@ main() {
|
||||
if [[ $is_channel == 1 ]]; then
|
||||
[[ -n ${1-} ]] && echo "Error: channel-only bump takes no second argument" >&2 && usage && exit 1
|
||||
target_channel="$action"
|
||||
was_channel_only=1
|
||||
else
|
||||
echo "Error: unknown argument '$action'" >&2
|
||||
usage
|
||||
@@ -342,6 +476,14 @@ main() {
|
||||
[[ -z $target_channel ]] && target_channel="$CHANNEL"
|
||||
[[ $target_channel == "full" ]] && target_channel="stable"
|
||||
validate_channel "$target_channel"
|
||||
if [[ $CHANNEL != "stable" && $target_channel == "stable" && $action != "stable" && $action != "full" ]]; then
|
||||
echo "Error: from prerelease channel '$CHANNEL', promote using 'stable' or 'full' only" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -z $part && $was_channel_only -eq 1 && $CHANNEL == "stable" && $target_channel != "stable" ]]; then
|
||||
part="patch"
|
||||
fi
|
||||
|
||||
local old_base="$BASE_VERSION" old_channel="$CHANNEL" old_pre="$PRERELEASE_NUM"
|
||||
[[ -n $part ]] && bump_base_version "$part"
|
||||
@@ -360,6 +502,10 @@ main() {
|
||||
fi
|
||||
|
||||
compute_full_version
|
||||
if [[ $FULL_VERSION == "$current_full" ]]; then
|
||||
echo "Version $FULL_VERSION is already current; nothing to do." >&2
|
||||
exit 1
|
||||
fi
|
||||
log "Releasing $FULL_VERSION"
|
||||
|
||||
do_write_version
|
||||
|
||||
860
packages/repo-lib/lib.nix
Normal file
860
packages/repo-lib/lib.nix
Normal file
@@ -0,0 +1,860 @@
|
||||
{
|
||||
nixpkgs,
|
||||
treefmt-nix,
|
||||
lefthookNix,
|
||||
releaseScriptPath,
|
||||
shellHookTemplatePath,
|
||||
}:
|
||||
let
|
||||
lib = nixpkgs.lib;
|
||||
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
|
||||
defaultReleaseChannels = [
|
||||
"alpha"
|
||||
"beta"
|
||||
"rc"
|
||||
"internal"
|
||||
];
|
||||
|
||||
importPkgs = nixpkgsInput: system: import nixpkgsInput { inherit system; };
|
||||
|
||||
duplicateStrings =
|
||||
names:
|
||||
lib.unique (
|
||||
builtins.filter (
|
||||
name: builtins.length (builtins.filter (candidate: candidate == name) names) > 1
|
||||
) names
|
||||
);
|
||||
|
||||
mergeUniqueAttrs =
|
||||
label: left: right:
|
||||
let
|
||||
overlap = builtins.attrNames (lib.intersectAttrs left right);
|
||||
in
|
||||
if overlap != [ ] then
|
||||
throw "repo-lib: duplicate ${label}: ${lib.concatStringsSep ", " overlap}"
|
||||
else
|
||||
left // right;
|
||||
|
||||
sanitizeName = name: lib.strings.sanitizeDerivationName name;
|
||||
|
||||
defaultShellBanner = {
|
||||
style = "simple";
|
||||
icon = "🚀";
|
||||
title = "Dev shell ready";
|
||||
titleColor = "GREEN";
|
||||
subtitle = "";
|
||||
subtitleColor = "GRAY";
|
||||
borderColor = "BLUE";
|
||||
};
|
||||
|
||||
normalizeShellBanner =
|
||||
rawBanner:
|
||||
let
|
||||
banner = defaultShellBanner // rawBanner;
|
||||
in
|
||||
if
|
||||
!(builtins.elem banner.style [
|
||||
"simple"
|
||||
"pretty"
|
||||
])
|
||||
then
|
||||
throw "repo-lib: config.shell.banner.style must be one of simple or pretty"
|
||||
else
|
||||
banner;
|
||||
|
||||
normalizeStrictTool =
|
||||
pkgs: tool:
|
||||
let
|
||||
version = {
|
||||
args = [ "--version" ];
|
||||
match = null;
|
||||
regex = null;
|
||||
group = 0;
|
||||
line = 1;
|
||||
}
|
||||
// (tool.version or { });
|
||||
banner = {
|
||||
color = "YELLOW";
|
||||
icon = null;
|
||||
iconColor = null;
|
||||
}
|
||||
// (tool.banner or { });
|
||||
executable =
|
||||
if tool ? command && tool.command != null then
|
||||
tool.command
|
||||
else if tool ? exe && tool.exe != null then
|
||||
"${lib.getExe' tool.package tool.exe}"
|
||||
else
|
||||
"${lib.getExe tool.package}";
|
||||
in
|
||||
if !(tool ? command && tool.command != null) && !(tool ? package) then
|
||||
throw "repo-lib: tool '${tool.name or "<unnamed>"}' is missing 'package' or 'command'"
|
||||
else
|
||||
{
|
||||
kind = "strict";
|
||||
inherit executable version banner;
|
||||
name = tool.name;
|
||||
package = tool.package or null;
|
||||
required = tool.required or true;
|
||||
};
|
||||
|
||||
normalizeLegacyTool =
|
||||
pkgs: tool:
|
||||
if tool ? package then
|
||||
normalizeStrictTool pkgs tool
|
||||
else
|
||||
{
|
||||
kind = "legacy";
|
||||
name = tool.name;
|
||||
command = tool.bin;
|
||||
versionCommand = tool.versionCmd or "--version";
|
||||
banner = {
|
||||
color = tool.color or "YELLOW";
|
||||
icon = tool.icon or null;
|
||||
iconColor = tool.iconColor or null;
|
||||
};
|
||||
required = tool.required or false;
|
||||
};
|
||||
|
||||
normalizeCheck =
|
||||
pkgs: name: rawCheck:
|
||||
let
|
||||
check = {
|
||||
stage = "pre-commit";
|
||||
passFilenames = false;
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
// rawCheck;
|
||||
wrapperName = "repo-lib-check-${sanitizeName name}";
|
||||
wrapper = pkgs.writeShellApplication {
|
||||
name = wrapperName;
|
||||
runtimeInputs = check.runtimeInputs;
|
||||
text = ''
|
||||
set -euo pipefail
|
||||
${check.command}
|
||||
'';
|
||||
};
|
||||
in
|
||||
if !(check ? command) then
|
||||
throw "repo-lib: check '${name}' is missing 'command'"
|
||||
else if
|
||||
!(builtins.elem check.stage [
|
||||
"pre-commit"
|
||||
"pre-push"
|
||||
])
|
||||
then
|
||||
throw "repo-lib: check '${name}' has unsupported stage '${check.stage}'"
|
||||
else
|
||||
{
|
||||
enable = true;
|
||||
entry = "${wrapper}/bin/${wrapperName}";
|
||||
pass_filenames = check.passFilenames;
|
||||
stages = [ check.stage ];
|
||||
};
|
||||
|
||||
normalizeHookStage =
|
||||
hookName: stage:
|
||||
if
|
||||
builtins.elem stage [
|
||||
"pre-commit"
|
||||
"pre-push"
|
||||
"commit-msg"
|
||||
]
|
||||
then
|
||||
stage
|
||||
else
|
||||
throw "repo-lib: hook '${hookName}' has unsupported stage '${stage}' for lefthook";
|
||||
|
||||
hookStageFileArgs =
|
||||
stage: passFilenames:
|
||||
if !passFilenames then
|
||||
""
|
||||
else if stage == "pre-commit" then
|
||||
" {staged_files}"
|
||||
else if stage == "pre-push" then
|
||||
" {push_files}"
|
||||
else if stage == "commit-msg" then
|
||||
" {1}"
|
||||
else
|
||||
throw "repo-lib: unsupported lefthook stage '${stage}'";
|
||||
|
||||
hookToLefthookConfig =
|
||||
name: hook:
|
||||
let
|
||||
supportedFields = [
|
||||
"description"
|
||||
"enable"
|
||||
"entry"
|
||||
"name"
|
||||
"package"
|
||||
"pass_filenames"
|
||||
"stages"
|
||||
];
|
||||
unsupportedFields = builtins.filter (field: !(builtins.elem field supportedFields)) (
|
||||
builtins.attrNames hook
|
||||
);
|
||||
stages = builtins.map (stage: normalizeHookStage name stage) (hook.stages or [ "pre-commit" ]);
|
||||
passFilenames = hook.pass_filenames or false;
|
||||
in
|
||||
if unsupportedFields != [ ] then
|
||||
throw ''
|
||||
repo-lib: hook '${name}' uses unsupported fields for lefthook: ${lib.concatStringsSep ", " unsupportedFields}
|
||||
''
|
||||
else if !(hook ? entry) then
|
||||
throw "repo-lib: hook '${name}' is missing 'entry'"
|
||||
else
|
||||
lib.foldl' lib.recursiveUpdate { } (
|
||||
builtins.map (
|
||||
stage:
|
||||
lib.setAttrByPath [ stage "commands" name ] {
|
||||
run = "${hook.entry}${hookStageFileArgs stage passFilenames}";
|
||||
}
|
||||
) stages
|
||||
);
|
||||
|
||||
hookStages =
|
||||
hooks:
|
||||
lib.unique (
|
||||
[
|
||||
"pre-commit"
|
||||
"commit-msg"
|
||||
]
|
||||
++ lib.concatMap (hook: hook.stages or [ "pre-commit" ]) (builtins.attrValues hooks)
|
||||
);
|
||||
|
||||
parallelHookStageConfig =
|
||||
stage:
|
||||
if
|
||||
builtins.elem stage [
|
||||
"pre-commit"
|
||||
"pre-push"
|
||||
]
|
||||
then
|
||||
lib.setAttrByPath [ stage "parallel" ] true
|
||||
else
|
||||
{ };
|
||||
|
||||
normalizeReleaseStep =
|
||||
step:
|
||||
if step ? writeFile then
|
||||
{
|
||||
kind = "writeFile";
|
||||
path = step.writeFile.path;
|
||||
text = step.writeFile.text;
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
else if step ? replace then
|
||||
{
|
||||
kind = "replace";
|
||||
path = step.replace.path;
|
||||
regex = step.replace.regex;
|
||||
replacement = step.replace.replacement;
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
else if step ? run && builtins.isAttrs step.run then
|
||||
{
|
||||
kind = "run";
|
||||
script = step.run.script;
|
||||
runtimeInputs = step.run.runtimeInputs or [ ];
|
||||
}
|
||||
else if step ? run then
|
||||
{
|
||||
kind = "run";
|
||||
script = step.run;
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
else if step ? file then
|
||||
{
|
||||
kind = "writeFile";
|
||||
path = step.file;
|
||||
text = step.content;
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
else
|
||||
throw "repo-lib: release step must contain one of writeFile, replace, or run";
|
||||
|
||||
releaseStepScript =
|
||||
step:
|
||||
if step.kind == "writeFile" then
|
||||
''
|
||||
target_path="$ROOT_DIR/${step.path}"
|
||||
mkdir -p "$(dirname "$target_path")"
|
||||
cat >"$target_path" << NIXEOF
|
||||
${step.text}
|
||||
NIXEOF
|
||||
log "Generated version file: ${step.path}"
|
||||
''
|
||||
else if step.kind == "replace" then
|
||||
''
|
||||
target_path="$ROOT_DIR/${step.path}"
|
||||
REPO_LIB_STEP_REGEX=$(cat <<'NIXEOF'
|
||||
${step.regex}
|
||||
NIXEOF
|
||||
)
|
||||
REPO_LIB_STEP_REPLACEMENT=$(cat <<NIXEOF
|
||||
${step.replacement}
|
||||
NIXEOF
|
||||
)
|
||||
export REPO_LIB_STEP_REGEX REPO_LIB_STEP_REPLACEMENT
|
||||
perl - "$target_path" <<'REPO_LIB_PERL_REPLACE'
|
||||
use strict;
|
||||
use warnings;
|
||||
|
||||
my $path = shift @ARGV;
|
||||
my $regex_src = $ENV{"REPO_LIB_STEP_REGEX"} // q{};
|
||||
my $template = $ENV{"REPO_LIB_STEP_REPLACEMENT"} // q{};
|
||||
|
||||
open my $in, q{<}, $path or die "failed to open $path: $!";
|
||||
local $/ = undef;
|
||||
my $content = <$in>;
|
||||
close $in;
|
||||
|
||||
my $regex = qr/$regex_src/ms;
|
||||
$content =~ s{$regex}{
|
||||
my @cap = map { defined $_ ? $_ : q{} } ($1, $2, $3, $4, $5, $6, $7, $8, $9);
|
||||
my $result = $template;
|
||||
$result =~ s{\\([1-9])}{$cap[$1 - 1]}ge;
|
||||
$result;
|
||||
}gems;
|
||||
|
||||
open my $out, q{>}, $path or die "failed to open $path for write: $!";
|
||||
print {$out} $content;
|
||||
close $out;
|
||||
REPO_LIB_PERL_REPLACE
|
||||
log "Updated ${step.path}"
|
||||
''
|
||||
else
|
||||
''
|
||||
${step.script}
|
||||
'';
|
||||
|
||||
normalizeReleaseConfig =
|
||||
raw:
|
||||
let
|
||||
hasLegacySteps = raw ? release;
|
||||
hasStructuredSteps = raw ? steps;
|
||||
steps =
|
||||
if hasLegacySteps && hasStructuredSteps then
|
||||
throw "repo-lib: pass either 'release' or 'steps' to mkRelease, not both"
|
||||
else if hasStructuredSteps then
|
||||
builtins.map normalizeReleaseStep raw.steps
|
||||
else if hasLegacySteps then
|
||||
builtins.map normalizeReleaseStep raw.release
|
||||
else
|
||||
[ ];
|
||||
in
|
||||
{
|
||||
postVersion = raw.postVersion or "";
|
||||
channels = raw.channels or defaultReleaseChannels;
|
||||
runtimeInputs = (raw.runtimeInputs or [ ]) ++ (raw.extraRuntimeInputs or [ ]);
|
||||
steps = steps;
|
||||
};
|
||||
|
||||
buildShellHook =
|
||||
{
|
||||
hooksShellHook,
|
||||
shellEnvScript,
|
||||
bootstrap,
|
||||
shellBannerScript,
|
||||
extraShellText,
|
||||
toolLabelWidth,
|
||||
}:
|
||||
let
|
||||
template = builtins.readFile shellHookTemplatePath;
|
||||
in
|
||||
builtins.replaceStrings
|
||||
[
|
||||
"@HOOKS_SHELL_HOOK@"
|
||||
"@TOOL_LABEL_WIDTH@"
|
||||
"@SHELL_ENV_SCRIPT@"
|
||||
"@BOOTSTRAP@"
|
||||
"@SHELL_BANNER_SCRIPT@"
|
||||
"@EXTRA_SHELL_TEXT@"
|
||||
]
|
||||
[
|
||||
hooksShellHook
|
||||
(toString toolLabelWidth)
|
||||
shellEnvScript
|
||||
bootstrap
|
||||
shellBannerScript
|
||||
extraShellText
|
||||
]
|
||||
template;
|
||||
|
||||
buildShellArtifacts =
|
||||
{
|
||||
pkgs,
|
||||
system,
|
||||
src,
|
||||
includeStandardPackages ? true,
|
||||
formatting,
|
||||
tools ? [ ],
|
||||
shellConfig ? {
|
||||
env = { };
|
||||
extraShellText = "";
|
||||
bootstrap = "";
|
||||
banner = defaultShellBanner;
|
||||
},
|
||||
checkSpecs ? { },
|
||||
rawHookEntries ? { },
|
||||
extraPackages ? [ ],
|
||||
}:
|
||||
let
|
||||
standardPackages = with pkgs; [
|
||||
nixfmt
|
||||
gitlint
|
||||
gitleaks
|
||||
shfmt
|
||||
];
|
||||
toolPackages = lib.filter (pkg: pkg != null) (builtins.map (tool: tool.package or null) tools);
|
||||
selectedStandardPackages = lib.optionals includeStandardPackages standardPackages;
|
||||
|
||||
treefmtEval = treefmt-nix.lib.evalModule pkgs {
|
||||
projectRootFile = "flake.nix";
|
||||
programs = {
|
||||
nixfmt.enable = true;
|
||||
}
|
||||
// formatting.programs;
|
||||
settings.formatter = { } // formatting.settings;
|
||||
};
|
||||
|
||||
normalizedChecks = lib.mapAttrs (name: check: normalizeCheck pkgs name check) checkSpecs;
|
||||
hooks = mergeUniqueAttrs "hook" rawHookEntries normalizedChecks;
|
||||
lefthookCheck = lefthookNix.lib.${system}.run {
|
||||
inherit src;
|
||||
config = lib.foldl' lib.recursiveUpdate { } (
|
||||
[
|
||||
(parallelHookStageConfig "pre-commit")
|
||||
(lib.setAttrByPath [ "pre-commit" "commands" "treefmt" ] {
|
||||
run = "${treefmtEval.config.build.wrapper}/bin/treefmt --ci {staged_files}";
|
||||
})
|
||||
(lib.setAttrByPath [ "pre-commit" "commands" "gitleaks" ] {
|
||||
run = "${pkgs.gitleaks}/bin/gitleaks protect --staged";
|
||||
})
|
||||
(lib.setAttrByPath [ "commit-msg" "commands" "gitlint" ] {
|
||||
run = "${pkgs.gitlint}/bin/gitlint --staged --msg-filename {1}";
|
||||
})
|
||||
]
|
||||
++ builtins.map parallelHookStageConfig (hookStages hooks)
|
||||
++ lib.mapAttrsToList hookToLefthookConfig hooks
|
||||
);
|
||||
};
|
||||
selectedCheckOutputs = {
|
||||
hook-check = lefthookCheck;
|
||||
lefthook-check = lefthookCheck;
|
||||
};
|
||||
|
||||
toolNames = builtins.map (tool: tool.name) tools;
|
||||
toolNameWidth =
|
||||
if toolNames == [ ] then
|
||||
0
|
||||
else
|
||||
builtins.foldl' (maxWidth: name: lib.max maxWidth (builtins.stringLength name)) 0 toolNames;
|
||||
toolLabelWidth = toolNameWidth + 1;
|
||||
|
||||
shellEnvScript = lib.concatStringsSep "\n" (
|
||||
lib.mapAttrsToList (
|
||||
name: value: "export ${name}=${lib.escapeShellArg (toString value)}"
|
||||
) shellConfig.env
|
||||
);
|
||||
|
||||
banner = normalizeShellBanner (shellConfig.banner or { });
|
||||
|
||||
shellBannerScript =
|
||||
if banner.style == "pretty" then
|
||||
''
|
||||
repo_lib_print_pretty_header \
|
||||
${lib.escapeShellArg banner.borderColor} \
|
||||
${lib.escapeShellArg banner.titleColor} \
|
||||
${lib.escapeShellArg banner.icon} \
|
||||
${lib.escapeShellArg banner.title} \
|
||||
${lib.escapeShellArg banner.subtitleColor} \
|
||||
${lib.escapeShellArg banner.subtitle}
|
||||
''
|
||||
+ lib.concatMapStrings (
|
||||
tool:
|
||||
if tool.kind == "strict" then
|
||||
''
|
||||
repo_lib_print_pretty_tool \
|
||||
${lib.escapeShellArg banner.borderColor} \
|
||||
${lib.escapeShellArg tool.name} \
|
||||
${lib.escapeShellArg tool.banner.color} \
|
||||
${lib.escapeShellArg (if tool.banner.icon == null then "" else tool.banner.icon)} \
|
||||
${lib.escapeShellArg (if tool.banner.iconColor == null then "" else tool.banner.iconColor)} \
|
||||
${lib.escapeShellArg (if tool.required then "1" else "0")} \
|
||||
${lib.escapeShellArg (toString tool.version.line)} \
|
||||
${lib.escapeShellArg (toString tool.version.group)} \
|
||||
${lib.escapeShellArg (if tool.version.regex == null then "" else tool.version.regex)} \
|
||||
${lib.escapeShellArg (if tool.version.match == null then "" else tool.version.match)} \
|
||||
${lib.escapeShellArg tool.executable} \
|
||||
${lib.escapeShellArgs tool.version.args}
|
||||
''
|
||||
else
|
||||
''
|
||||
repo_lib_print_pretty_legacy_tool \
|
||||
${lib.escapeShellArg banner.borderColor} \
|
||||
${lib.escapeShellArg tool.name} \
|
||||
${lib.escapeShellArg tool.banner.color} \
|
||||
${lib.escapeShellArg (if tool.banner.icon == null then "" else tool.banner.icon)} \
|
||||
${lib.escapeShellArg (if tool.banner.iconColor == null then "" else tool.banner.iconColor)} \
|
||||
${lib.escapeShellArg (if tool.required then "1" else "0")} \
|
||||
${lib.escapeShellArg tool.command} \
|
||||
${lib.escapeShellArg tool.versionCommand}
|
||||
''
|
||||
) tools
|
||||
+ ''
|
||||
repo_lib_print_pretty_footer \
|
||||
${lib.escapeShellArg banner.borderColor}
|
||||
''
|
||||
else
|
||||
''
|
||||
repo_lib_print_simple_header \
|
||||
${lib.escapeShellArg banner.titleColor} \
|
||||
${lib.escapeShellArg banner.icon} \
|
||||
${lib.escapeShellArg banner.title} \
|
||||
${lib.escapeShellArg banner.subtitleColor} \
|
||||
${lib.escapeShellArg banner.subtitle}
|
||||
''
|
||||
+ lib.concatMapStrings (
|
||||
tool:
|
||||
if tool.kind == "strict" then
|
||||
''
|
||||
repo_lib_print_simple_tool \
|
||||
${lib.escapeShellArg tool.name} \
|
||||
${lib.escapeShellArg tool.banner.color} \
|
||||
${lib.escapeShellArg (if tool.banner.icon == null then "" else tool.banner.icon)} \
|
||||
${lib.escapeShellArg (if tool.banner.iconColor == null then "" else tool.banner.iconColor)} \
|
||||
${lib.escapeShellArg (if tool.required then "1" else "0")} \
|
||||
${lib.escapeShellArg (toString tool.version.line)} \
|
||||
${lib.escapeShellArg (toString tool.version.group)} \
|
||||
${lib.escapeShellArg (if tool.version.regex == null then "" else tool.version.regex)} \
|
||||
${lib.escapeShellArg (if tool.version.match == null then "" else tool.version.match)} \
|
||||
${lib.escapeShellArg tool.executable} \
|
||||
${lib.escapeShellArgs tool.version.args}
|
||||
''
|
||||
else
|
||||
''
|
||||
repo_lib_print_simple_legacy_tool \
|
||||
${lib.escapeShellArg tool.name} \
|
||||
${lib.escapeShellArg tool.banner.color} \
|
||||
${lib.escapeShellArg (if tool.banner.icon == null then "" else tool.banner.icon)} \
|
||||
${lib.escapeShellArg (if tool.banner.iconColor == null then "" else tool.banner.iconColor)} \
|
||||
${lib.escapeShellArg (if tool.required then "1" else "0")} \
|
||||
${lib.escapeShellArg tool.command} \
|
||||
${lib.escapeShellArg tool.versionCommand}
|
||||
''
|
||||
) tools
|
||||
+ ''
|
||||
printf "\n"
|
||||
'';
|
||||
in
|
||||
{
|
||||
checks = selectedCheckOutputs;
|
||||
formatter = treefmtEval.config.build.wrapper;
|
||||
shell = pkgs.mkShell {
|
||||
packages = lib.unique (
|
||||
selectedStandardPackages ++ extraPackages ++ toolPackages ++ [ pkgs.lefthook ]
|
||||
);
|
||||
shellHook = buildShellHook {
|
||||
hooksShellHook = lefthookCheck.shellHook;
|
||||
inherit toolLabelWidth shellEnvScript shellBannerScript;
|
||||
bootstrap = shellConfig.bootstrap;
|
||||
extraShellText = shellConfig.extraShellText;
|
||||
};
|
||||
};
|
||||
}
|
||||
// selectedCheckOutputs;
|
||||
in
|
||||
rec {
|
||||
systems = {
|
||||
default = supportedSystems;
|
||||
};
|
||||
|
||||
tools = rec {
|
||||
fromPackage =
|
||||
{
|
||||
name,
|
||||
package,
|
||||
exe ? null,
|
||||
version ? { },
|
||||
banner ? { },
|
||||
required ? true,
|
||||
}:
|
||||
{
|
||||
inherit
|
||||
name
|
||||
package
|
||||
exe
|
||||
version
|
||||
banner
|
||||
required
|
||||
;
|
||||
};
|
||||
|
||||
fromCommand =
|
||||
{
|
||||
name,
|
||||
command,
|
||||
version ? { },
|
||||
banner ? { },
|
||||
required ? true,
|
||||
}:
|
||||
{
|
||||
inherit
|
||||
name
|
||||
command
|
||||
version
|
||||
banner
|
||||
required
|
||||
;
|
||||
};
|
||||
|
||||
simple =
|
||||
name: package: args:
|
||||
fromPackage {
|
||||
inherit name package;
|
||||
version.args = args;
|
||||
};
|
||||
};
|
||||
|
||||
normalizeRepoConfig =
|
||||
rawConfig:
|
||||
let
|
||||
merged = lib.recursiveUpdate {
|
||||
includeStandardPackages = true;
|
||||
shell = {
|
||||
env = { };
|
||||
extraShellText = "";
|
||||
allowImpureBootstrap = false;
|
||||
bootstrap = "";
|
||||
banner = { };
|
||||
};
|
||||
formatting = {
|
||||
programs = { };
|
||||
settings = { };
|
||||
};
|
||||
checks = { };
|
||||
release = null;
|
||||
} rawConfig;
|
||||
release =
|
||||
if merged.release == null then
|
||||
null
|
||||
else
|
||||
{
|
||||
channels = defaultReleaseChannels;
|
||||
steps = [ ];
|
||||
postVersion = "";
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
// merged.release;
|
||||
in
|
||||
if merged.shell.bootstrap != "" && !merged.shell.allowImpureBootstrap then
|
||||
throw "repo-lib: config.shell.bootstrap requires config.shell.allowImpureBootstrap = true"
|
||||
else
|
||||
merged
|
||||
// {
|
||||
inherit release;
|
||||
shell = merged.shell // {
|
||||
banner = normalizeShellBanner merged.shell.banner;
|
||||
};
|
||||
};
|
||||
|
||||
mkDevShell =
|
||||
{
|
||||
system,
|
||||
src ? ./.,
|
||||
nixpkgsInput ? nixpkgs,
|
||||
extraPackages ? [ ],
|
||||
preToolHook ? "",
|
||||
extraShellHook ? "",
|
||||
additionalHooks ? { },
|
||||
tools ? [ ],
|
||||
includeStandardPackages ? true,
|
||||
formatters ? { },
|
||||
formatterSettings ? { },
|
||||
features ? { },
|
||||
}:
|
||||
let
|
||||
pkgs = importPkgs nixpkgsInput system;
|
||||
oxfmtEnabled = features.oxfmt or false;
|
||||
legacyTools = builtins.map (tool: normalizeLegacyTool pkgs tool) tools;
|
||||
duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) legacyTools);
|
||||
normalizedFormatting = {
|
||||
programs =
|
||||
(lib.optionalAttrs oxfmtEnabled {
|
||||
oxfmt.enable = true;
|
||||
})
|
||||
// formatters;
|
||||
settings = formatterSettings;
|
||||
};
|
||||
shellConfig = {
|
||||
env = { };
|
||||
extraShellText = extraShellHook;
|
||||
allowImpureBootstrap = true;
|
||||
bootstrap = preToolHook;
|
||||
banner = defaultShellBanner;
|
||||
};
|
||||
in
|
||||
if duplicateToolNames != [ ] then
|
||||
throw "repo-lib: duplicate tool names: ${lib.concatStringsSep ", " duplicateToolNames}"
|
||||
else
|
||||
buildShellArtifacts {
|
||||
inherit
|
||||
pkgs
|
||||
system
|
||||
src
|
||||
includeStandardPackages
|
||||
;
|
||||
formatting = normalizedFormatting;
|
||||
rawHookEntries = additionalHooks;
|
||||
shellConfig = shellConfig;
|
||||
tools = legacyTools;
|
||||
extraPackages =
|
||||
extraPackages
|
||||
++ lib.optionals oxfmtEnabled [
|
||||
pkgs.oxfmt
|
||||
pkgs.oxlint
|
||||
];
|
||||
};
|
||||
|
||||
mkRelease =
|
||||
{
|
||||
system,
|
||||
nixpkgsInput ? nixpkgs,
|
||||
...
|
||||
}@rawArgs:
|
||||
let
|
||||
pkgs = importPkgs nixpkgsInput system;
|
||||
release = normalizeReleaseConfig rawArgs;
|
||||
channelList = lib.concatStringsSep " " release.channels;
|
||||
releaseStepsScript = lib.concatMapStrings releaseStepScript release.steps;
|
||||
script =
|
||||
builtins.replaceStrings
|
||||
[
|
||||
"__CHANNEL_LIST__"
|
||||
"__RELEASE_STEPS__"
|
||||
"__POST_VERSION__"
|
||||
]
|
||||
[
|
||||
channelList
|
||||
releaseStepsScript
|
||||
release.postVersion
|
||||
]
|
||||
(builtins.readFile releaseScriptPath);
|
||||
in
|
||||
pkgs.writeShellApplication {
|
||||
name = "release";
|
||||
runtimeInputs =
|
||||
with pkgs;
|
||||
[
|
||||
git
|
||||
gnugrep
|
||||
gawk
|
||||
gnused
|
||||
coreutils
|
||||
perl
|
||||
]
|
||||
++ release.runtimeInputs
|
||||
++ lib.concatMap (step: step.runtimeInputs or [ ]) release.steps;
|
||||
text = script;
|
||||
};
|
||||
|
||||
mkRepo =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
src ? ./.,
|
||||
systems ? supportedSystems,
|
||||
config ? { },
|
||||
perSystem ? (
|
||||
{
|
||||
pkgs,
|
||||
system,
|
||||
lib,
|
||||
config,
|
||||
}:
|
||||
{ }
|
||||
),
|
||||
}:
|
||||
let
|
||||
normalizedConfig = normalizeRepoConfig config;
|
||||
systemResults = lib.genAttrs systems (
|
||||
system:
|
||||
let
|
||||
pkgs = importPkgs nixpkgs system;
|
||||
perSystemResult = {
|
||||
tools = [ ];
|
||||
shell = { };
|
||||
checks = { };
|
||||
packages = { };
|
||||
apps = { };
|
||||
}
|
||||
// perSystem {
|
||||
inherit pkgs system;
|
||||
lib = nixpkgs.lib;
|
||||
config = normalizedConfig;
|
||||
};
|
||||
|
||||
strictTools = builtins.map (tool: normalizeStrictTool pkgs tool) perSystemResult.tools;
|
||||
duplicateToolNames = duplicateStrings (builtins.map (tool: tool.name) strictTools);
|
||||
mergedChecks = mergeUniqueAttrs "check" normalizedConfig.checks perSystemResult.checks;
|
||||
shellConfig = lib.recursiveUpdate normalizedConfig.shell (perSystemResult.shell or { });
|
||||
env =
|
||||
if duplicateToolNames != [ ] then
|
||||
throw "repo-lib: duplicate tool names: ${lib.concatStringsSep ", " duplicateToolNames}"
|
||||
else
|
||||
buildShellArtifacts {
|
||||
inherit
|
||||
pkgs
|
||||
system
|
||||
src
|
||||
;
|
||||
includeStandardPackages = normalizedConfig.includeStandardPackages;
|
||||
formatting = normalizedConfig.formatting;
|
||||
tools = strictTools;
|
||||
checkSpecs = mergedChecks;
|
||||
shellConfig = shellConfig;
|
||||
extraPackages = perSystemResult.shell.packages or [ ];
|
||||
};
|
||||
|
||||
releasePackages =
|
||||
if normalizedConfig.release == null then
|
||||
{ }
|
||||
else
|
||||
{
|
||||
release = mkRelease {
|
||||
inherit system;
|
||||
nixpkgsInput = nixpkgs;
|
||||
channels = normalizedConfig.release.channels;
|
||||
steps = normalizedConfig.release.steps;
|
||||
postVersion = normalizedConfig.release.postVersion;
|
||||
runtimeInputs = normalizedConfig.release.runtimeInputs;
|
||||
};
|
||||
};
|
||||
in
|
||||
{
|
||||
inherit env;
|
||||
packages = mergeUniqueAttrs "package" releasePackages perSystemResult.packages;
|
||||
apps = perSystemResult.apps;
|
||||
}
|
||||
);
|
||||
in
|
||||
{
|
||||
devShells = lib.genAttrs systems (system: {
|
||||
default = systemResults.${system}.env.shell;
|
||||
});
|
||||
|
||||
checks = lib.genAttrs systems (system: systemResults.${system}.env.checks);
|
||||
|
||||
formatter = lib.genAttrs systems (system: systemResults.${system}.env.formatter);
|
||||
packages = lib.genAttrs systems (system: systemResults.${system}.packages);
|
||||
apps = lib.genAttrs systems (system: systemResults.${system}.apps);
|
||||
};
|
||||
}
|
||||
341
packages/repo-lib/shell-hook.sh
Normal file
341
packages/repo-lib/shell-hook.sh
Normal file
@@ -0,0 +1,341 @@
|
||||
@HOOKS_SHELL_HOOK@
|
||||
|
||||
if [ -t 1 ]; then
|
||||
command -v tput >/dev/null 2>&1 && tput clear || printf '\033c'
|
||||
fi
|
||||
|
||||
GREEN=$'\033[1;32m'
|
||||
CYAN=$'\033[1;36m'
|
||||
YELLOW=$'\033[1;33m'
|
||||
BLUE=$'\033[1;34m'
|
||||
RED=$'\033[1;31m'
|
||||
MAGENTA=$'\033[1;35m'
|
||||
WHITE=$'\033[1;37m'
|
||||
GRAY=$'\033[0;90m'
|
||||
BOLD=$'\033[1m'
|
||||
UNDERLINE=$'\033[4m'
|
||||
RESET=$'\033[0m'
|
||||
|
||||
REPO_LIB_TOOL_VERSION=""
|
||||
REPO_LIB_TOOL_ERROR=""
|
||||
|
||||
repo_lib_capture_tool() {
|
||||
local required="$1"
|
||||
local line_no="$2"
|
||||
local group_no="$3"
|
||||
local regex="$4"
|
||||
local match_regex="$5"
|
||||
local executable="$6"
|
||||
shift 6
|
||||
|
||||
local output=""
|
||||
local selected=""
|
||||
local version=""
|
||||
|
||||
REPO_LIB_TOOL_VERSION=""
|
||||
REPO_LIB_TOOL_ERROR=""
|
||||
|
||||
if ! output="$("$executable" "$@" 2>&1)"; then
|
||||
REPO_LIB_TOOL_ERROR="probe failed"
|
||||
printf "%s\n" "$output" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [ -n "$match_regex" ]; then
|
||||
selected="$(printf '%s\n' "$output" | grep -E -m 1 "$match_regex" || true)"
|
||||
else
|
||||
selected="$(printf '%s\n' "$output" | sed -n "${line_no}p")"
|
||||
fi
|
||||
selected="$(printf '%s' "$selected" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
|
||||
if [ -n "$regex" ]; then
|
||||
if [[ "$selected" =~ $regex ]]; then
|
||||
version="${BASH_REMATCH[$group_no]}"
|
||||
else
|
||||
REPO_LIB_TOOL_ERROR="version parse failed"
|
||||
printf "%s\n" "$output" >&2
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
version="$selected"
|
||||
fi
|
||||
|
||||
if [ -z "$version" ]; then
|
||||
REPO_LIB_TOOL_ERROR="empty version"
|
||||
printf "%s\n" "$output" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
REPO_LIB_TOOL_VERSION="$version"
|
||||
return 0
|
||||
}
|
||||
|
||||
repo_lib_capture_legacy_tool() {
|
||||
local required="$1"
|
||||
local command_name="$2"
|
||||
local version_command="$3"
|
||||
|
||||
local output=""
|
||||
local version=""
|
||||
|
||||
REPO_LIB_TOOL_VERSION=""
|
||||
REPO_LIB_TOOL_ERROR=""
|
||||
|
||||
if ! command -v "$command_name" >/dev/null 2>&1; then
|
||||
REPO_LIB_TOOL_ERROR="missing command"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if ! output="$(sh -c "$command_name $version_command" 2>&1)"; then
|
||||
REPO_LIB_TOOL_ERROR="probe failed"
|
||||
printf "%s\n" "$output" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
version="$(printf '%s\n' "$output" | head -n 1 | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||
if [ -z "$version" ]; then
|
||||
REPO_LIB_TOOL_ERROR="empty version"
|
||||
printf "%s\n" "$output" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
REPO_LIB_TOOL_VERSION="$version"
|
||||
return 0
|
||||
}
|
||||
|
||||
repo_lib_print_simple_header() {
|
||||
local title_color_name="$1"
|
||||
local icon="$2"
|
||||
local title="$3"
|
||||
local subtitle_color_name="$4"
|
||||
local subtitle="$5"
|
||||
|
||||
local title_color="${!title_color_name:-$GREEN}"
|
||||
local subtitle_color="${!subtitle_color_name:-$GRAY}"
|
||||
|
||||
printf "\n%s" "$title_color"
|
||||
if [ -n "$icon" ]; then
|
||||
printf "%s " "$icon"
|
||||
fi
|
||||
printf "%s%s" "$title" "$RESET"
|
||||
if [ -n "$subtitle" ]; then
|
||||
printf " %s%s%s" "$subtitle_color" "$subtitle" "$RESET"
|
||||
fi
|
||||
printf "\n\n"
|
||||
}
|
||||
|
||||
repo_lib_print_simple_tool() {
|
||||
local name="$1"
|
||||
local color_name="$2"
|
||||
local icon="$3"
|
||||
local icon_color_name="$4"
|
||||
local required="$5"
|
||||
local line_no="$6"
|
||||
local group_no="$7"
|
||||
local regex="$8"
|
||||
local match_regex="$9"
|
||||
local executable="${10}"
|
||||
shift 10
|
||||
|
||||
local color="${!color_name:-$YELLOW}"
|
||||
local effective_icon_color_name="$icon_color_name"
|
||||
local icon_color=""
|
||||
|
||||
if [ -z "$effective_icon_color_name" ]; then
|
||||
effective_icon_color_name="$color_name"
|
||||
fi
|
||||
|
||||
if repo_lib_capture_tool "$required" "$line_no" "$group_no" "$regex" "$match_regex" "$executable" "$@"; then
|
||||
icon_color="${!effective_icon_color_name:-$color}"
|
||||
printf " "
|
||||
if [ -n "$icon" ]; then
|
||||
printf "%s%s%s " "$icon_color" "$icon" "$RESET"
|
||||
fi
|
||||
printf "$CYAN %-@TOOL_LABEL_WIDTH@s$RESET %s%s$RESET\n" "${name}:" "$color" "$REPO_LIB_TOOL_VERSION"
|
||||
else
|
||||
printf " "
|
||||
if [ -n "$icon" ]; then
|
||||
printf "%s%s%s " "$RED" "$icon" "$RESET"
|
||||
fi
|
||||
printf "$CYAN %-@TOOL_LABEL_WIDTH@s$RESET $RED%s$RESET\n" "${name}:" "$REPO_LIB_TOOL_ERROR"
|
||||
if [ "$required" = "1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
repo_lib_print_simple_legacy_tool() {
|
||||
local name="$1"
|
||||
local color_name="$2"
|
||||
local icon="$3"
|
||||
local icon_color_name="$4"
|
||||
local required="$5"
|
||||
local command_name="$6"
|
||||
local version_command="$7"
|
||||
|
||||
local color="${!color_name:-$YELLOW}"
|
||||
local effective_icon_color_name="$icon_color_name"
|
||||
local icon_color=""
|
||||
|
||||
if [ -z "$effective_icon_color_name" ]; then
|
||||
effective_icon_color_name="$color_name"
|
||||
fi
|
||||
|
||||
if repo_lib_capture_legacy_tool "$required" "$command_name" "$version_command"; then
|
||||
icon_color="${!effective_icon_color_name:-$color}"
|
||||
printf " "
|
||||
if [ -n "$icon" ]; then
|
||||
printf "%s%s%s " "$icon_color" "$icon" "$RESET"
|
||||
fi
|
||||
printf "$CYAN %-@TOOL_LABEL_WIDTH@s$RESET %s%s$RESET\n" "${name}:" "$color" "$REPO_LIB_TOOL_VERSION"
|
||||
else
|
||||
printf " "
|
||||
if [ -n "$icon" ]; then
|
||||
printf "%s%s%s " "$RED" "$icon" "$RESET"
|
||||
fi
|
||||
printf "$CYAN %-@TOOL_LABEL_WIDTH@s$RESET $RED%s$RESET\n" "${name}:" "$REPO_LIB_TOOL_ERROR"
|
||||
if [ "$required" = "1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
repo_lib_print_pretty_header() {
|
||||
local border_color_name="$1"
|
||||
local title_color_name="$2"
|
||||
local icon="$3"
|
||||
local title="$4"
|
||||
local subtitle_color_name="$5"
|
||||
local subtitle="$6"
|
||||
|
||||
local border_color="${!border_color_name:-$BLUE}"
|
||||
local title_color="${!title_color_name:-$GREEN}"
|
||||
local subtitle_color="${!subtitle_color_name:-$GRAY}"
|
||||
|
||||
printf "\n%s╭─%s %s" "$border_color" "$RESET" "$title_color"
|
||||
if [ -n "$icon" ]; then
|
||||
printf "%s " "$icon"
|
||||
fi
|
||||
printf "%s%s" "$title" "$RESET"
|
||||
if [ -n "$subtitle" ]; then
|
||||
printf " %s%s%s" "$subtitle_color" "$subtitle" "$RESET"
|
||||
fi
|
||||
printf "\n"
|
||||
}
|
||||
|
||||
repo_lib_print_pretty_row() {
|
||||
local border_color_name="$1"
|
||||
local icon="$2"
|
||||
local icon_color_name="$3"
|
||||
local label="$4"
|
||||
local value="$5"
|
||||
local value_color_name="$6"
|
||||
|
||||
local border_color="${!border_color_name:-$BLUE}"
|
||||
local icon_color="${!icon_color_name:-$WHITE}"
|
||||
local value_color="${!value_color_name:-$YELLOW}"
|
||||
|
||||
if [ -z "$icon" ]; then
|
||||
icon="•"
|
||||
fi
|
||||
|
||||
printf "%s│%s %s%s%s ${WHITE}%-@TOOL_LABEL_WIDTH@s${RESET} %s%s${RESET}\n" \
|
||||
"$border_color" "$RESET" "$icon_color" "$icon" "$RESET" "$label" "$value_color" "$value"
|
||||
}
|
||||
|
||||
repo_lib_print_pretty_tool() {
|
||||
local border_color_name="$1"
|
||||
local name="$2"
|
||||
local color_name="$3"
|
||||
local icon="$4"
|
||||
local icon_color_name="$5"
|
||||
local required="$6"
|
||||
local line_no="$7"
|
||||
local group_no="$8"
|
||||
local regex="$9"
|
||||
local match_regex="${10}"
|
||||
local executable="${11}"
|
||||
shift 11
|
||||
|
||||
local effective_icon_color_name="$icon_color_name"
|
||||
local value_color_name="$color_name"
|
||||
local value=""
|
||||
|
||||
if [ -z "$effective_icon_color_name" ]; then
|
||||
effective_icon_color_name="$color_name"
|
||||
fi
|
||||
|
||||
if repo_lib_capture_tool "$required" "$line_no" "$group_no" "$regex" "$match_regex" "$executable" "$@"; then
|
||||
value="$REPO_LIB_TOOL_VERSION"
|
||||
else
|
||||
value="$REPO_LIB_TOOL_ERROR"
|
||||
effective_icon_color_name="RED"
|
||||
value_color_name="RED"
|
||||
fi
|
||||
|
||||
repo_lib_print_pretty_row \
|
||||
"$border_color_name" \
|
||||
"$icon" \
|
||||
"$effective_icon_color_name" \
|
||||
"$name" \
|
||||
"$value" \
|
||||
"$value_color_name"
|
||||
|
||||
if [ "$value_color_name" = "RED" ] && [ "$required" = "1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
repo_lib_print_pretty_legacy_tool() {
|
||||
local border_color_name="$1"
|
||||
local name="$2"
|
||||
local color_name="$3"
|
||||
local icon="$4"
|
||||
local icon_color_name="$5"
|
||||
local required="$6"
|
||||
local command_name="$7"
|
||||
local version_command="$8"
|
||||
|
||||
local effective_icon_color_name="$icon_color_name"
|
||||
local value_color_name="$color_name"
|
||||
local value=""
|
||||
|
||||
if [ -z "$effective_icon_color_name" ]; then
|
||||
effective_icon_color_name="$color_name"
|
||||
fi
|
||||
|
||||
if repo_lib_capture_legacy_tool "$required" "$command_name" "$version_command"; then
|
||||
value="$REPO_LIB_TOOL_VERSION"
|
||||
else
|
||||
value="$REPO_LIB_TOOL_ERROR"
|
||||
effective_icon_color_name="RED"
|
||||
value_color_name="RED"
|
||||
fi
|
||||
|
||||
repo_lib_print_pretty_row \
|
||||
"$border_color_name" \
|
||||
"$icon" \
|
||||
"$effective_icon_color_name" \
|
||||
"$name" \
|
||||
"$value" \
|
||||
"$value_color_name"
|
||||
|
||||
if [ "$value_color_name" = "RED" ] && [ "$required" = "1" ]; then
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
repo_lib_print_pretty_footer() {
|
||||
local border_color_name="$1"
|
||||
local border_color="${!border_color_name:-$BLUE}"
|
||||
|
||||
printf "%s╰─%s\n\n" "$border_color" "$RESET"
|
||||
}
|
||||
|
||||
@SHELL_ENV_SCRIPT@
|
||||
|
||||
@BOOTSTRAP@
|
||||
|
||||
@SHELL_BANNER_SCRIPT@
|
||||
|
||||
@EXTRA_SHELL_TEXT@
|
||||
48
skills/repo-lib-consumer/SKILL.md
Normal file
48
skills/repo-lib-consumer/SKILL.md
Normal file
@@ -0,0 +1,48 @@
|
||||
---
|
||||
name: repo-lib-consumer
|
||||
description: Edit or extend repos that consume `repo-lib` through `repo-lib.lib.mkRepo`, `mkDevShell`, or `mkRelease`. Use when Codex needs to add or change tools, shell packages, checks or test phases, formatters, release steps, release channels, bootstrap hooks, or release automation in a Nix flake built on repo-lib.
|
||||
---
|
||||
|
||||
# Repo Lib Consumer
|
||||
|
||||
Use this skill to make idiomatic changes in a repo that already depends on `repo-lib`.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Detect the integration style.
|
||||
Search for `repo-lib.lib.mkRepo`, `repo-lib.lib.mkDevShell`, `repo-lib.lib.mkRelease`, or `inputs.repo-lib`.
|
||||
|
||||
2. Prefer the repo's current abstraction level.
|
||||
If the repo already uses `mkRepo`, stay on `mkRepo`.
|
||||
If the repo still uses `mkDevShell` or `mkRelease`, preserve that style unless the user asked to migrate.
|
||||
|
||||
3. Load the right reference before editing.
|
||||
Read `references/api.md` for exact option names, defaults, generated outputs, and limitations.
|
||||
Read `references/recipes.md` for common edits such as adding a tool, adding a test phase, wiring release file updates, or handling webhooks.
|
||||
|
||||
4. Follow repo-lib conventions.
|
||||
Add bannered CLIs through `perSystem.tools`, not `shell.packages`.
|
||||
Use `shell.packages` for packages that should be present in the shell but not shown in the banner.
|
||||
Keep shells pure-first; only use `bootstrap` with `allowImpureBootstrap = true`.
|
||||
Prefer structured `release.steps` over free-form shell when the task fits `writeFile` or `replace`.
|
||||
|
||||
5. Verify after edits.
|
||||
Run `nix flake show --json`.
|
||||
Run `nix flake check` when feasible.
|
||||
If local flake evaluation cannot see newly created files because the repo is being loaded as a git flake, stage the new files before rerunning checks.
|
||||
|
||||
## Decision Rules
|
||||
|
||||
- Prefer `repo-lib.lib.tools.fromPackage` for tools with explicit metadata.
|
||||
- Use `repo-lib.lib.tools.simple` only for very simple `--version` or `version` probes.
|
||||
- Put pre-commit and pre-push automation in `checks`, not shell hooks.
|
||||
- Treat `postVersion` as pre-tag and pre-push. It is not a true post-tag hook.
|
||||
- For a webhook that must fire after the tag exists remotely, prefer CI triggered by tag push over local release command changes.
|
||||
|
||||
## References
|
||||
|
||||
- `references/api.md`
|
||||
Use for the exact consumer API, option matrix, generated outputs, release ordering, and legacy compatibility.
|
||||
|
||||
- `references/recipes.md`
|
||||
Use for concrete change patterns: add a tool, add a test phase, update release-managed files, or wire webhook behavior.
|
||||
4
skills/repo-lib-consumer/agents/openai.yaml
Normal file
4
skills/repo-lib-consumer/agents/openai.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Repo Lib Consumer"
|
||||
short_description: "Edit repos that use repo-lib safely"
|
||||
default_prompt: "Use $repo-lib-consumer to update a repo that consumes repo-lib."
|
||||
287
skills/repo-lib-consumer/references/api.md
Normal file
287
skills/repo-lib-consumer/references/api.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# repo-lib Consumer API
|
||||
|
||||
## Detect the repo shape
|
||||
|
||||
Look for one of these patterns in the consuming repo:
|
||||
|
||||
- `repo-lib.lib.mkRepo`
|
||||
- `repo-lib.lib.mkDevShell`
|
||||
- `repo-lib.lib.mkRelease`
|
||||
- `inputs.repo-lib`
|
||||
|
||||
Prefer editing the existing style instead of migrating incidentally.
|
||||
|
||||
## Preferred `mkRepo` shape
|
||||
|
||||
```nix
|
||||
repo-lib.lib.mkRepo {
|
||||
inherit self nixpkgs;
|
||||
src = ./.;
|
||||
systems = repo-lib.lib.systems.default; # optional
|
||||
|
||||
config = {
|
||||
includeStandardPackages = true;
|
||||
|
||||
shell = {
|
||||
env = { };
|
||||
extraShellText = "";
|
||||
allowImpureBootstrap = false;
|
||||
bootstrap = "";
|
||||
};
|
||||
|
||||
formatting = {
|
||||
programs = { };
|
||||
settings = { };
|
||||
};
|
||||
|
||||
checks = { };
|
||||
|
||||
release = null; # or attrset below
|
||||
};
|
||||
|
||||
perSystem = { pkgs, system, lib, config }: {
|
||||
tools = [ ];
|
||||
shell.packages = [ ];
|
||||
checks = { };
|
||||
packages = { };
|
||||
apps = { };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Generated outputs:
|
||||
|
||||
- `devShells.${system}.default`
|
||||
- `checks.${system}.hook-check`
|
||||
- `checks.${system}.lefthook-check`
|
||||
- `formatter.${system}`
|
||||
- `packages.${system}.release` when `config.release != null`
|
||||
- merged `packages` and `apps` from `perSystem`
|
||||
|
||||
## `config.shell`
|
||||
|
||||
Fields:
|
||||
|
||||
- `env`
|
||||
Attrset of environment variables exported in the shell.
|
||||
- `extraShellText`
|
||||
Extra shell snippet appended after the banner.
|
||||
- `bootstrap`
|
||||
Shell snippet that runs before the banner.
|
||||
- `allowImpureBootstrap`
|
||||
Must be `true` if `bootstrap` is non-empty.
|
||||
|
||||
Rules:
|
||||
|
||||
- Default is pure-first.
|
||||
- Do not add bootstrap work unless the user actually wants imperative setup.
|
||||
- Use `bootstrap` for unavoidable local setup only.
|
||||
|
||||
## `config.formatting`
|
||||
|
||||
Fields:
|
||||
|
||||
- `programs`
|
||||
Passed to `treefmt-nix.lib.evalModule`.
|
||||
- `settings`
|
||||
Passed to `settings.formatter`.
|
||||
|
||||
Rules:
|
||||
|
||||
- `nixfmt` is always enabled.
|
||||
- Use formatter settings instead of ad hoc shell formatting logic.
|
||||
|
||||
## Checks
|
||||
|
||||
`config.checks.<name>` and `perSystem.checks.<name>` use this shape:
|
||||
|
||||
```nix
|
||||
{
|
||||
command = "go test ./...";
|
||||
stage = "pre-push"; # or "pre-commit"
|
||||
passFilenames = false;
|
||||
runtimeInputs = [ pkgs.go ];
|
||||
}
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
- `stage = "pre-commit"`
|
||||
- `passFilenames = false`
|
||||
- `runtimeInputs = [ ]`
|
||||
|
||||
Rules:
|
||||
|
||||
- Only `pre-commit` and `pre-push` are supported.
|
||||
- The command is wrapped as a script and connected into `lefthook.nix`.
|
||||
- `pre-commit` and `pre-push` commands are configured to run in parallel.
|
||||
|
||||
## Tools
|
||||
|
||||
Preferred shape in `perSystem.tools`:
|
||||
|
||||
```nix
|
||||
(repo-lib.lib.tools.fromPackage {
|
||||
name = "Go";
|
||||
package = pkgs.go;
|
||||
exe = "go"; # optional
|
||||
version = {
|
||||
args = [ "version" ];
|
||||
regex = null;
|
||||
group = 0;
|
||||
line = 1;
|
||||
};
|
||||
banner = {
|
||||
color = "CYAN";
|
||||
};
|
||||
required = true;
|
||||
})
|
||||
```
|
||||
|
||||
For a tool that should come from the host `PATH` instead of `nixpkgs`:
|
||||
|
||||
```nix
|
||||
(repo-lib.lib.tools.fromCommand {
|
||||
name = "Nix";
|
||||
command = "nix";
|
||||
version.args = [ "--version" ];
|
||||
})
|
||||
```
|
||||
|
||||
Helper:
|
||||
|
||||
```nix
|
||||
repo-lib.lib.tools.simple "Go" pkgs.go [ "version" ]
|
||||
```
|
||||
|
||||
Tool behavior:
|
||||
|
||||
- Tool packages are added to the shell automatically.
|
||||
- Command-backed tools are probed from the existing `PATH` and are not added to the shell automatically.
|
||||
- Banner probing uses absolute executable paths.
|
||||
- `required = true` by default.
|
||||
- Required tool probe failure aborts shell startup.
|
||||
|
||||
Use `shell.packages` instead of `tools` when:
|
||||
|
||||
- the package should be in the shell but not in the banner
|
||||
- the package is not a CLI tool with a stable version probe
|
||||
|
||||
## `config.release`
|
||||
|
||||
Shape:
|
||||
|
||||
```nix
|
||||
{
|
||||
channels = [ "alpha" "beta" "rc" "internal" ];
|
||||
steps = [ ];
|
||||
postVersion = "";
|
||||
runtimeInputs = [ ];
|
||||
}
|
||||
```
|
||||
|
||||
Defaults:
|
||||
|
||||
- `channels = [ "alpha" "beta" "rc" "internal" ]`
|
||||
- `steps = [ ]`
|
||||
- `postVersion = ""`
|
||||
- `runtimeInputs = [ ]`
|
||||
|
||||
Set `release = null` to disable the generated release package.
|
||||
|
||||
## Release step shapes
|
||||
|
||||
### `writeFile`
|
||||
|
||||
```nix
|
||||
{
|
||||
writeFile = {
|
||||
path = "src/version.ts";
|
||||
text = ''
|
||||
export const APP_VERSION = "$FULL_VERSION" as const;
|
||||
'';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `replace`
|
||||
|
||||
```nix
|
||||
{
|
||||
replace = {
|
||||
path = "README.md";
|
||||
regex = ''^(version = ")[^"]*(")$'';
|
||||
replacement = ''\1$FULL_VERSION\2'';
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### `run`
|
||||
|
||||
```nix
|
||||
{
|
||||
run = {
|
||||
script = ''
|
||||
curl -fsS https://example.invalid/hook \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"tag":"'"$FULL_TAG"'"}'
|
||||
'';
|
||||
runtimeInputs = [ pkgs.curl ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
Also accepted for compatibility:
|
||||
|
||||
- `{ run = ''...''; }`
|
||||
- legacy `mkRelease { release = [ { file = ...; content = ...; } ... ]; }`
|
||||
|
||||
## Release ordering
|
||||
|
||||
The generated `release` command does this:
|
||||
|
||||
1. Update `VERSION`
|
||||
2. Run `release.steps`
|
||||
3. Run `postVersion`
|
||||
4. Run `nix fmt`
|
||||
5. `git add -A`
|
||||
6. Commit
|
||||
7. Tag
|
||||
8. Push branch
|
||||
9. Push tags
|
||||
|
||||
Important consequence:
|
||||
|
||||
- `postVersion` is still before commit, tag, and push.
|
||||
- There is no true post-tag or post-push hook in current `repo-lib`.
|
||||
|
||||
## Post-tag webhook limitation
|
||||
|
||||
If the user asks for a webhook after the tag exists remotely:
|
||||
|
||||
- Prefer CI triggered by pushed tags in the consuming repo.
|
||||
- Do not claim `postVersion` is post-tag; it is not.
|
||||
- Only extend `repo-lib` itself if the user explicitly wants a new library capability.
|
||||
|
||||
## Legacy API summary
|
||||
|
||||
`mkDevShell` still supports:
|
||||
|
||||
- `extraPackages`
|
||||
- `preToolHook`
|
||||
- `extraShellHook`
|
||||
- `additionalHooks`
|
||||
- old `tools = [ { name; bin; versionCmd; color; } ]`
|
||||
- `features.oxfmt`
|
||||
- `formatters`
|
||||
- `formatterSettings`
|
||||
|
||||
`mkRelease` still supports:
|
||||
|
||||
- `release = [ ... ]` as legacy alias for `steps`
|
||||
- `extraRuntimeInputs` as legacy alias merged into `runtimeInputs`
|
||||
|
||||
When a repo already uses these APIs:
|
||||
|
||||
- preserve them unless the user asked to migrate
|
||||
- do not mix old and new styles accidentally in the same call
|
||||
197
skills/repo-lib-consumer/references/recipes.md
Normal file
197
skills/repo-lib-consumer/references/recipes.md
Normal file
@@ -0,0 +1,197 @@
|
||||
# repo-lib Change Recipes
|
||||
|
||||
## Add a new bannered tool
|
||||
|
||||
Edit `perSystem.tools` in the consuming repo:
|
||||
|
||||
```nix
|
||||
tools = [
|
||||
(repo-lib.lib.tools.fromPackage {
|
||||
name = "Go";
|
||||
package = pkgs.go;
|
||||
version.args = [ "version" ];
|
||||
banner.color = "CYAN";
|
||||
})
|
||||
];
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- Do not also add `pkgs.go` to `shell.packages`; `tools` already adds it.
|
||||
- Use `exe = "name"` only when the package exposes multiple binaries or the main program is not the desired one.
|
||||
|
||||
## Add a non-banner package to the shell
|
||||
|
||||
Use `shell.packages`:
|
||||
|
||||
```nix
|
||||
shell.packages = [
|
||||
self.packages.${system}.release
|
||||
pkgs.jq
|
||||
];
|
||||
```
|
||||
|
||||
Use this for:
|
||||
|
||||
- helper CLIs that do not need a banner entry
|
||||
- internal scripts
|
||||
- the generated `release` package itself
|
||||
|
||||
## Add a test phase or lint hook
|
||||
|
||||
For a simple global check:
|
||||
|
||||
```nix
|
||||
config.checks.tests = {
|
||||
command = "go test ./...";
|
||||
stage = "pre-push";
|
||||
passFilenames = false;
|
||||
runtimeInputs = [ pkgs.go ];
|
||||
};
|
||||
```
|
||||
|
||||
For a system-specific check:
|
||||
|
||||
```nix
|
||||
perSystem = { pkgs, ... }: {
|
||||
checks.lint = {
|
||||
command = "bun test";
|
||||
stage = "pre-push";
|
||||
runtimeInputs = [ pkgs.bun ];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
Guidance:
|
||||
|
||||
- Use `pre-commit` for fast format/lint work.
|
||||
- Use `pre-push` for slower test suites.
|
||||
- Prefer `runtimeInputs` over inline absolute paths when the command needs extra CLIs.
|
||||
|
||||
## Add or change formatters
|
||||
|
||||
Use `config.formatting`:
|
||||
|
||||
```nix
|
||||
config.formatting = {
|
||||
programs = {
|
||||
shfmt.enable = true;
|
||||
gofmt.enable = true;
|
||||
};
|
||||
|
||||
settings = {
|
||||
shfmt.options = [ "-i" "2" "-s" "-w" ];
|
||||
};
|
||||
};
|
||||
```
|
||||
|
||||
## Add release-managed files
|
||||
|
||||
Generate a file from the release version:
|
||||
|
||||
```nix
|
||||
config.release.steps = [
|
||||
{
|
||||
writeFile = {
|
||||
path = "src/version.ts";
|
||||
text = ''
|
||||
export const APP_VERSION = "$FULL_VERSION" as const;
|
||||
'';
|
||||
};
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
Update an existing file with a regex:
|
||||
|
||||
```nix
|
||||
config.release.steps = [
|
||||
{
|
||||
replace = {
|
||||
path = "README.md";
|
||||
regex = ''^(version = ")[^"]*(")$'';
|
||||
replacement = ''\1$FULL_VERSION\2'';
|
||||
};
|
||||
}
|
||||
];
|
||||
```
|
||||
|
||||
## Add a webhook during release
|
||||
|
||||
If the webhook may run before commit and tag creation, use a `run` step or `postVersion`.
|
||||
|
||||
Use a `run` step when it belongs with other release mutations:
|
||||
|
||||
```nix
|
||||
config.release = {
|
||||
runtimeInputs = [ pkgs.curl ];
|
||||
steps = [
|
||||
{
|
||||
run = {
|
||||
script = ''
|
||||
curl -fsS https://example.invalid/release-hook \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"version":"'"$FULL_VERSION"'"}'
|
||||
'';
|
||||
runtimeInputs = [ pkgs.curl ];
|
||||
};
|
||||
}
|
||||
];
|
||||
};
|
||||
```
|
||||
|
||||
Use `postVersion` when the action should happen after all `steps`:
|
||||
|
||||
```nix
|
||||
config.release.postVersion = ''
|
||||
curl -fsS https://example.invalid/release-hook \
|
||||
-H 'content-type: application/json' \
|
||||
-d '{"version":"'"$FULL_VERSION"'","tag":"'"$FULL_TAG"'"}'
|
||||
'';
|
||||
config.release.runtimeInputs = [ pkgs.curl ];
|
||||
```
|
||||
|
||||
Important:
|
||||
|
||||
- Both of these still run before commit, tag, and push.
|
||||
- They are not true post-tag hooks.
|
||||
|
||||
## Add a true post-tag webhook
|
||||
|
||||
Do not fake this with `postVersion`.
|
||||
|
||||
Preferred approach in the consuming repo:
|
||||
|
||||
1. Keep local release generation in `repo-lib`.
|
||||
2. Add CI triggered by tag push.
|
||||
3. Put the webhook call in CI, where the tag is already created and pushed.
|
||||
|
||||
Only change `repo-lib` itself if the user explicitly asks for a new local post-tag capability.
|
||||
|
||||
## Add impure bootstrap work
|
||||
|
||||
Only do this when the user actually wants imperative shell setup:
|
||||
|
||||
```nix
|
||||
config.shell = {
|
||||
bootstrap = ''
|
||||
export GOBIN="$PWD/.tools/bin"
|
||||
export PATH="$GOBIN:$PATH"
|
||||
'';
|
||||
allowImpureBootstrap = true;
|
||||
};
|
||||
```
|
||||
|
||||
Do not add bootstrap work for normal Nix-packaged tools.
|
||||
|
||||
## Migrate a legacy consumer to `mkRepo`
|
||||
|
||||
Only do this if requested.
|
||||
|
||||
Migration outline:
|
||||
|
||||
1. Move repeated shell/check/formatter config into `config`.
|
||||
2. Move old banner tools into `perSystem.tools`.
|
||||
3. Move extra shell packages into `perSystem.shell.packages`.
|
||||
4. Replace old `mkRelease { release = [ ... ]; }` with `config.release.steps`.
|
||||
5. Keep behavior the same first; do not redesign the repo in the same change unless asked.
|
||||
1
template/.gitignore
vendored
1
template/.gitignore
vendored
@@ -1,5 +1,6 @@
|
||||
.direnv/
|
||||
.pre-commit-config.yaml
|
||||
lefthook.yml
|
||||
|
||||
bazel-*
|
||||
build/
|
||||
|
||||
@@ -4,142 +4,161 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
|
||||
devshell-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=v1.0.1";
|
||||
devshell-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||
repo-lib.url = "git+https://git.dgren.dev/eric/nix-flake-lib?ref=refs/tags/v3.2.0";
|
||||
repo-lib.inputs.nixpkgs.follows = "nixpkgs";
|
||||
};
|
||||
|
||||
outputs =
|
||||
{
|
||||
self,
|
||||
nixpkgs,
|
||||
devshell-lib,
|
||||
repo-lib,
|
||||
...
|
||||
}:
|
||||
let
|
||||
supportedSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
|
||||
in
|
||||
{
|
||||
devShells = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = import nixpkgs { inherit system; };
|
||||
env = devshell-lib.lib.mkDevShell {
|
||||
inherit system;
|
||||
repo-lib.lib.mkRepo {
|
||||
inherit self nixpkgs;
|
||||
src = ./.;
|
||||
|
||||
# includeStandardPackages = false; # opt out of nixfmt/gitlint/gitleaks/shfmt defaults
|
||||
config = {
|
||||
# includeStandardPackages = false;
|
||||
|
||||
extraPackages = with pkgs; [
|
||||
# add your tools here, e.g.:
|
||||
# go
|
||||
# bun
|
||||
# rustc
|
||||
];
|
||||
|
||||
features = {
|
||||
# oxfmt = true; # enables oxfmt + oxlint from nixpkgs
|
||||
};
|
||||
|
||||
formatters = {
|
||||
# shfmt.enable = true;
|
||||
# gofmt.enable = true;
|
||||
};
|
||||
|
||||
formatterSettings = {
|
||||
# shfmt.options = [ "-i" "2" "-s" "-w" ];
|
||||
# oxfmt.includes = [ "*.ts" "*.tsx" "*.js" "*.json" ];
|
||||
};
|
||||
|
||||
additionalHooks = {
|
||||
tests = {
|
||||
enable = true;
|
||||
entry = "echo 'No tests defined yet.'"; # replace with your test command
|
||||
pass_filenames = false;
|
||||
stages = [ "pre-push" ];
|
||||
};
|
||||
# my-hook = {
|
||||
# enable = true;
|
||||
# entry = "${pkgs.some-tool}/bin/some-tool";
|
||||
# pass_filenames = false;
|
||||
# };
|
||||
};
|
||||
|
||||
tools = [
|
||||
# { name = "Bun"; bin = "${pkgs.bun}/bin/bun"; versionCmd = "--version"; color = "YELLOW"; }
|
||||
# { name = "Go"; bin = "${pkgs.go}/bin/go"; versionCmd = "version"; color = "CYAN"; }
|
||||
# { name = "Rust"; bin = "${pkgs.rustc}/bin/rustc"; versionCmd = "--version"; color = "YELLOW"; }
|
||||
];
|
||||
|
||||
extraShellHook = ''
|
||||
# any repo-specific shell setup here
|
||||
'';
|
||||
shell = {
|
||||
env = {
|
||||
# FOO = "bar";
|
||||
};
|
||||
in
|
||||
|
||||
extraShellText = ''
|
||||
# any repo-specific shell setup here
|
||||
'';
|
||||
|
||||
# Impure bootstrap is available as an explicit escape hatch.
|
||||
# bootstrap = ''
|
||||
# export GOBIN="$PWD/.tools/bin"
|
||||
# export PATH="$GOBIN:$PATH"
|
||||
# '';
|
||||
# allowImpureBootstrap = true;
|
||||
};
|
||||
|
||||
formatting = {
|
||||
# nixfmt is enabled by default and wired into lefthook.
|
||||
programs = {
|
||||
# shfmt.enable = true;
|
||||
# gofmt.enable = true;
|
||||
};
|
||||
|
||||
settings = {
|
||||
# shfmt.options = [ "-i" "2" "-s" "-w" ];
|
||||
};
|
||||
};
|
||||
|
||||
# These checks become lefthook commands in the generated `lefthook.yml`.
|
||||
# repo-lib runs `pre-commit` and `pre-push` hook commands in parallel.
|
||||
checks = {
|
||||
tests = {
|
||||
command = "echo 'No tests defined yet.'";
|
||||
stage = "pre-push";
|
||||
passFilenames = false;
|
||||
};
|
||||
|
||||
# fmt = {
|
||||
# command = "nix fmt";
|
||||
# stage = "pre-commit";
|
||||
# passFilenames = false;
|
||||
# };
|
||||
};
|
||||
|
||||
# repo-lib also installs built-in hooks for:
|
||||
# - treefmt / nixfmt on `pre-commit`
|
||||
# - gitleaks on `pre-commit`
|
||||
# - gitlint on `commit-msg`
|
||||
|
||||
# release = null;
|
||||
release = {
|
||||
steps = [
|
||||
# Write a generated version file during release.
|
||||
# {
|
||||
# writeFile = {
|
||||
# path = "src/version.ts";
|
||||
# text = ''
|
||||
# export const APP_VERSION = "$FULL_VERSION" as const;
|
||||
# '';
|
||||
# };
|
||||
# }
|
||||
|
||||
# Replace a version string while preserving surrounding captures.
|
||||
# {
|
||||
# replace = {
|
||||
# path = "README.md";
|
||||
# regex = ''^(version = ")[^"]*(")$'';
|
||||
# replacement = ''\1$FULL_VERSION\2'';
|
||||
# };
|
||||
# }
|
||||
|
||||
# Run any extra release step with declared runtime inputs.
|
||||
# {
|
||||
# run = {
|
||||
# runtimeInputs = [ pkgs.git ];
|
||||
# script = ''
|
||||
# git status --short
|
||||
# '';
|
||||
# };
|
||||
# }
|
||||
];
|
||||
};
|
||||
};
|
||||
|
||||
perSystem =
|
||||
{
|
||||
default = env.shell;
|
||||
}
|
||||
);
|
||||
|
||||
checks = forAllSystems (
|
||||
system:
|
||||
let
|
||||
env = devshell-lib.lib.mkDevShell { inherit system; };
|
||||
in
|
||||
pkgs,
|
||||
system,
|
||||
...
|
||||
}:
|
||||
{
|
||||
inherit (env) pre-commit-check;
|
||||
}
|
||||
);
|
||||
tools = [
|
||||
(repo-lib.lib.tools.fromCommand {
|
||||
name = "Nix";
|
||||
command = "nix";
|
||||
version = {
|
||||
args = [ "--version" ];
|
||||
group = 1;
|
||||
};
|
||||
banner = {
|
||||
color = "BLUE";
|
||||
icon = "";
|
||||
};
|
||||
})
|
||||
|
||||
formatter = forAllSystems (system: (devshell-lib.lib.mkDevShell { inherit system; }).formatter);
|
||||
# (repo-lib.lib.tools.fromPackage {
|
||||
# name = "Go";
|
||||
# package = pkgs.go;
|
||||
# version.args = [ "version" ];
|
||||
# banner.color = "CYAN";
|
||||
# })
|
||||
];
|
||||
|
||||
# Optional: release command (`release`)
|
||||
#
|
||||
# The release script always updates VERSION first, then:
|
||||
# 1) runs release steps in order (file writes and scripts)
|
||||
# 2) runs postVersion hook
|
||||
# 3) formats, stages, commits, tags, and pushes
|
||||
#
|
||||
# Runtime env vars available in release.run/postVersion:
|
||||
# BASE_VERSION, CHANNEL, PRERELEASE_NUM, FULL_VERSION, FULL_TAG
|
||||
#
|
||||
# packages = forAllSystems (
|
||||
# system:
|
||||
# {
|
||||
# release = devshell-lib.lib.mkRelease {
|
||||
# inherit system;
|
||||
#
|
||||
# release = [
|
||||
# {
|
||||
# file = "src/version.ts";
|
||||
# content = ''
|
||||
# export const APP_VERSION = "$FULL_VERSION" as const;
|
||||
# '';
|
||||
# }
|
||||
# {
|
||||
# file = "internal/version/version.go";
|
||||
# content = ''
|
||||
# package version
|
||||
#
|
||||
# const Version = "$FULL_VERSION"
|
||||
# '';
|
||||
# }
|
||||
# {
|
||||
# run = ''
|
||||
# sed -E -i "s#^([[:space:]]*my-lib\\.url = \")github:org/my-lib[^"]*(\";)#\\1github:org/my-lib?ref=$FULL_TAG\\2#" "$ROOT_DIR/flake.nix"
|
||||
# '';
|
||||
# }
|
||||
# ];
|
||||
#
|
||||
# postVersion = ''
|
||||
# echo "Released $FULL_TAG"
|
||||
# '';
|
||||
# };
|
||||
# }
|
||||
# );
|
||||
shell.packages = [
|
||||
self.packages.${system}.release
|
||||
# pkgs.go
|
||||
# pkgs.bun
|
||||
];
|
||||
|
||||
# checks.lint = {
|
||||
# command = "bun test";
|
||||
# stage = "pre-push";
|
||||
# passFilenames = false;
|
||||
# runtimeInputs = [ pkgs.bun ];
|
||||
# };
|
||||
|
||||
# checks.generated = {
|
||||
# command = "git diff --exit-code";
|
||||
# stage = "pre-commit";
|
||||
# passFilenames = false;
|
||||
# };
|
||||
|
||||
# packages.my-tool = pkgs.writeShellApplication {
|
||||
# name = "my-tool";
|
||||
# text = ''echo hello'';
|
||||
# };
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
1281
tests/release.sh
Executable file
1281
tests/release.sh
Executable file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user