feat: modernize

This commit is contained in:
eric
2026-03-21 01:27:42 +01:00
parent e9d04c7f8d
commit 8fec37023f
37 changed files with 3270 additions and 3145 deletions

View File

@@ -0,0 +1,29 @@
{ nixpkgs }:
let
lib = nixpkgs.lib;
in
{
inherit lib;
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;
}

View File

@@ -0,0 +1,26 @@
{ }:
{
supportedSystems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
defaultReleaseChannels = [
"alpha"
"beta"
"rc"
"internal"
];
defaultShellBanner = {
style = "simple";
icon = "🚀";
title = "Dev shell ready";
titleColor = "GREEN";
subtitle = "";
subtitleColor = "GRAY";
borderColor = "BLUE";
};
}

View File

@@ -0,0 +1,116 @@
{
lib,
sanitizeName,
}:
let
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}'";
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";
in
{
inherit hookStageFileArgs normalizeHookStage;
checkToLefthookConfig =
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
lib.setAttrByPath [ check.stage "commands" name ] {
run = "${wrapper}/bin/${wrapperName}${hookStageFileArgs check.stage check.passFilenames}";
};
normalizeLefthookConfig =
label: raw: if builtins.isAttrs raw then raw else throw "repo-lib: ${label} must be an attrset";
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
);
parallelHookStageConfig =
stage:
if
builtins.elem stage [
"pre-commit"
"pre-push"
]
then
lib.setAttrByPath [ stage "parallel" ] true
else
{ };
}

View File

@@ -0,0 +1,105 @@
{
lib,
nixpkgs,
releaseScriptPath,
defaultReleaseChannels,
importPkgs,
}:
let
normalizeReleaseStep =
step:
if step ? writeFile then
{
kind = "writeFile";
path = step.writeFile.path;
text = step.writeFile.text;
}
else if step ? replace then
{
kind = "replace";
path = step.replace.path;
regex = step.replace.regex;
replacement = step.replace.replacement;
}
else if step ? versionMetaSet then
{
kind = "versionMetaSet";
key = step.versionMetaSet.key;
value = step.versionMetaSet.value;
}
else if step ? versionMetaUnset then
{
kind = "versionMetaUnset";
key = step.versionMetaUnset.key;
}
else
throw "repo-lib: release step must contain one of writeFile, replace, versionMetaSet, or versionMetaUnset";
normalizeReleaseConfig =
raw:
let
steps = if raw ? steps then builtins.map normalizeReleaseStep raw.steps else [ ];
in
{
postVersion = raw.postVersion or "";
channels = raw.channels or defaultReleaseChannels;
runtimeInputs = raw.runtimeInputs or [ ];
steps = steps;
};
mkRelease =
{
system,
nixpkgsInput ? nixpkgs,
...
}@rawArgs:
let
pkgs = importPkgs nixpkgsInput system;
release = normalizeReleaseConfig rawArgs;
channelList = lib.concatStringsSep " " release.channels;
releaseStepsJson = builtins.toJSON release.steps;
releaseRunner = pkgs.buildGoModule {
pname = "repo-lib-release-runner";
version = "0.0.0";
src = ../../release;
vendorHash = "sha256-fGFteYruAda2MBHkKgbTeCpIgO30tKCa+tzF6HcUvWM=";
subPackages = [ "cmd/release" ];
};
script =
builtins.replaceStrings
[
"__CHANNEL_LIST__"
"__RELEASE_STEPS_JSON__"
"__POST_VERSION__"
"__RELEASE_RUNNER__"
]
[
channelList
releaseStepsJson
release.postVersion
(lib.getExe' releaseRunner "release")
]
(builtins.readFile releaseScriptPath);
in
pkgs.writeShellApplication {
name = "release";
runtimeInputs =
with pkgs;
[
git
gnugrep
gawk
gnused
coreutils
]
++ release.runtimeInputs;
text = script;
};
in
{
inherit
normalizeReleaseStep
normalizeReleaseConfig
mkRelease
;
}

View File

@@ -0,0 +1,195 @@
{
flake-parts,
nixpkgs,
lib,
importPkgs,
duplicateStrings,
mergeUniqueAttrs,
supportedSystems,
defaultReleaseChannels,
normalizeStrictTool,
normalizeLefthookConfig,
normalizeShellBanner,
buildShellArtifacts,
mkRelease,
}:
let
normalizeRepoConfig =
rawConfig:
let
merged = lib.recursiveUpdate {
includeStandardPackages = true;
shell = {
env = { };
extraShellText = "";
allowImpureBootstrap = false;
bootstrap = "";
banner = { };
};
formatting = {
programs = { };
settings = { };
};
checks = { };
lefthook = { };
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;
};
};
buildRepoSystemOutputs =
{
pkgs,
system,
src,
nixpkgsInput,
normalizedConfig,
userPerSystem,
}:
let
perSystemResult = {
tools = [ ];
shell = { };
checks = { };
lefthook = { };
packages = { };
apps = { };
}
// userPerSystem {
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;
mergedLefthookConfig =
lib.recursiveUpdate (normalizeLefthookConfig "config.lefthook" normalizedConfig.lefthook)
(normalizeLefthookConfig "perSystem.lefthook" (perSystemResult.lefthook or { }));
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;
lefthookConfig = mergedLefthookConfig;
shellConfig = shellConfig;
extraPackages = perSystemResult.shell.packages or [ ];
};
releasePackages =
if normalizedConfig.release == null then
{ }
else
{
release = mkRelease {
inherit system;
nixpkgsInput = nixpkgsInput;
channels = normalizedConfig.release.channels;
steps = normalizedConfig.release.steps;
postVersion = normalizedConfig.release.postVersion;
runtimeInputs = normalizedConfig.release.runtimeInputs;
};
};
in
{
checks = env.checks;
formatter = env.formatter;
shell = env.shell;
packages = mergeUniqueAttrs "package" releasePackages perSystemResult.packages;
apps = perSystemResult.apps;
};
in
{
inherit normalizeRepoConfig;
mkRepo =
{
self,
nixpkgs,
src ? ./.,
systems ? supportedSystems,
config ? { },
perSystem ? (
{
pkgs,
system,
lib,
config,
}:
{ }
),
}:
let
normalizedConfig = normalizeRepoConfig config;
userPerSystem = perSystem;
in
flake-parts.lib.mkFlake
{
inputs = {
inherit self nixpkgs;
flake-parts = flake-parts;
};
}
{
inherit systems;
perSystem =
{
pkgs,
system,
...
}:
let
systemOutputs = buildRepoSystemOutputs {
inherit
pkgs
system
src
normalizedConfig
;
nixpkgsInput = nixpkgs;
userPerSystem = userPerSystem;
};
in
{
devShells.default = systemOutputs.shell;
inherit (systemOutputs)
apps
checks
formatter
packages
;
};
};
}

View File

@@ -0,0 +1,221 @@
{
lib,
treefmt-nix,
lefthookNix,
shellHookTemplatePath,
defaultShellBanner,
normalizeShellBanner,
normalizeLefthookConfig,
parallelHookStageConfig,
checkToLefthookConfig,
hookToLefthookConfig,
}:
let
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;
in
{
inherit buildShellHook;
buildShellArtifacts =
{
pkgs,
system,
src,
includeStandardPackages ? true,
formatting,
tools ? [ ],
shellConfig ? {
env = { };
extraShellText = "";
bootstrap = "";
banner = defaultShellBanner;
},
checkSpecs ? { },
rawHookEntries ? { },
lefthookConfig ? { },
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;
};
treefmtWrapper = treefmtEval.config.build.wrapper;
lefthookBinWrapper = pkgs.writeShellScript "lefthook-dumb-term" ''
exec env TERM=dumb ${lib.getExe pkgs.lefthook} "$@"
'';
normalizedLefthookConfig = normalizeLefthookConfig "lefthook config" lefthookConfig;
lefthookCheck = lefthookNix.lib.${system}.run {
inherit src;
config = lib.foldl' lib.recursiveUpdate { } (
[
{
output = [
"failure"
"summary"
];
}
(parallelHookStageConfig "pre-commit")
(parallelHookStageConfig "pre-push")
(lib.setAttrByPath [ "pre-commit" "commands" "treefmt" ] {
run = "${treefmtWrapper}/bin/treefmt --no-cache {staged_files}";
stage_fixed = true;
})
(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}";
})
]
++ lib.mapAttrsToList (name: check: checkToLefthookConfig pkgs name check) checkSpecs
++ lib.mapAttrsToList hookToLefthookConfig rawHookEntries
++ [ normalizedLefthookConfig ]
);
};
selectedCheckOutputs = {
formatting-check = treefmtEval.config.build.check src;
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: ''
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}
'') 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: ''
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}
'') tools
+ ''
printf "\n"
'';
in
{
checks = selectedCheckOutputs;
formatter = treefmtWrapper;
shell = pkgs.mkShell {
LEFTHOOK_BIN = builtins.toString lefthookBinWrapper;
packages = lib.unique (
selectedStandardPackages
++ extraPackages
++ toolPackages
++ [
pkgs.lefthook
treefmtWrapper
]
);
shellHook = buildShellHook {
hooksShellHook = lefthookCheck.shellHook;
inherit toolLabelWidth shellEnvScript shellBannerScript;
bootstrap = shellConfig.bootstrap;
extraShellText = shellConfig.extraShellText;
};
};
}
// selectedCheckOutputs;
}

View File

@@ -0,0 +1,90 @@
{
lib,
}:
let
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;
};
in
{
inherit normalizeStrictTool;
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;
};
};
}