mirror of
https://gitlab.com/TECHNOFAB/soonix.git
synced 2026-02-01 23:05:06 +01:00
chore: initial commit
This commit is contained in:
commit
25cc087b1d
17 changed files with 741 additions and 0 deletions
25
lib/default.nix
Normal file
25
lib/default.nix
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
pkgs,
|
||||
lib ? pkgs.lib,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) evalModules;
|
||||
|
||||
soonix_lib = import ./lib.nix {inherit pkgs lib;};
|
||||
in rec {
|
||||
inherit (soonix_lib) engines buildAllFiles;
|
||||
|
||||
module = ./module.nix;
|
||||
devshellModule = ./devshellModule.nix;
|
||||
|
||||
make = userConfig:
|
||||
evalModules {
|
||||
specialArgs = {inherit pkgs;};
|
||||
modules = [
|
||||
module
|
||||
userConfig
|
||||
];
|
||||
};
|
||||
|
||||
mkShellHook = userConfig: (make userConfig).config.shellHook;
|
||||
}
|
||||
23
lib/devshellModule.nix
Normal file
23
lib/devshellModule.nix
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
config,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) mkOption types;
|
||||
soonixModule = ./module.nix;
|
||||
in {
|
||||
options.soonix = mkOption {
|
||||
type = types.submodule {
|
||||
# propagate pkgs to the soonix module
|
||||
_module.args.pkgs = pkgs;
|
||||
imports = [soonixModule];
|
||||
};
|
||||
default = {};
|
||||
};
|
||||
|
||||
config.enterShellCommands.soonix = {
|
||||
text = config.soonix.shellHook;
|
||||
deps = ["env"];
|
||||
};
|
||||
}
|
||||
6
lib/flake.nix
Normal file
6
lib/flake.nix
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
{
|
||||
outputs = i: {
|
||||
lib = import ./.;
|
||||
devshellModule = ./devshellModule.nix;
|
||||
};
|
||||
}
|
||||
101
lib/lib.nix
Normal file
101
lib/lib.nix
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
{
|
||||
pkgs,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
inherit (pkgs) writeText writeTextFile runCommand formats;
|
||||
|
||||
engines = {
|
||||
nix = {
|
||||
name,
|
||||
opts,
|
||||
data,
|
||||
}: let
|
||||
format = opts.format or "json";
|
||||
formatOpts = builtins.removeAttrs opts ["format"];
|
||||
formatter = formats.${format} formatOpts;
|
||||
in
|
||||
formatter.generate name data;
|
||||
|
||||
string = {
|
||||
name,
|
||||
opts,
|
||||
data,
|
||||
}: let
|
||||
executable = opts.executable or false;
|
||||
in
|
||||
writeTextFile {
|
||||
inherit name executable;
|
||||
text = data;
|
||||
};
|
||||
|
||||
# only a passthru
|
||||
derivation = {data, ...}: data;
|
||||
|
||||
gotmpl = {
|
||||
name,
|
||||
opts,
|
||||
data,
|
||||
}: let
|
||||
inherit (opts) template;
|
||||
gomplate = opts.gomplate or pkgs.gomplate;
|
||||
in
|
||||
runCommand name {
|
||||
buildInputs = [gomplate];
|
||||
passAsFile = ["dataJson"];
|
||||
dataJson = builtins.toJSON data;
|
||||
} ''
|
||||
gomplate \
|
||||
-c ".=$dataJsonPath?type=application/json" \
|
||||
-f "${template}" \
|
||||
-o "$out"
|
||||
'';
|
||||
|
||||
jinja = {
|
||||
name,
|
||||
opts,
|
||||
data,
|
||||
}: let
|
||||
inherit (opts) template;
|
||||
python = opts.python or pkgs.python3;
|
||||
dataJson = writeText "template-data.json" (builtins.toJSON data);
|
||||
renderScript =
|
||||
writeText "render.py"
|
||||
# py
|
||||
''
|
||||
import json
|
||||
import sys
|
||||
from jinja2 import Template
|
||||
|
||||
with open('${dataJson}', 'r') as f:
|
||||
data = json.load(f)
|
||||
|
||||
with open('${template}', 'r') as f:
|
||||
template_str = f.read()
|
||||
|
||||
template = Template(template_str)
|
||||
print(template.render(**data))
|
||||
'';
|
||||
in
|
||||
runCommand name {
|
||||
buildInputs = [python python.pkgs.jinja2];
|
||||
} ''
|
||||
python ${renderScript} > $out
|
||||
'';
|
||||
};
|
||||
|
||||
buildAllFiles = files:
|
||||
runCommand "soonix-files" {} ''
|
||||
mkdir -p $out
|
||||
${lib.concatMapStringsSep "\n" (file:
|
||||
# sh
|
||||
''
|
||||
target_dir="$out/$(dirname "${file.path}")"
|
||||
mkdir -p "$target_dir"
|
||||
ln -sf "${file.src}" "$out/${file.path}"
|
||||
'')
|
||||
files}
|
||||
'';
|
||||
in {
|
||||
inherit engines buildAllFiles;
|
||||
}
|
||||
256
lib/module.nix
Normal file
256
lib/module.nix
Normal file
|
|
@ -0,0 +1,256 @@
|
|||
{
|
||||
pkgs,
|
||||
config,
|
||||
lib,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) types mkOption concatMapStringsSep;
|
||||
soonix_lib = import ./. {inherit pkgs;};
|
||||
inherit (soonix_lib) engines buildAllFiles;
|
||||
in {
|
||||
options = {
|
||||
hooks = mkOption {
|
||||
type = types.attrsOf (types.submodule ({
|
||||
name,
|
||||
config,
|
||||
...
|
||||
}: {
|
||||
options = {
|
||||
name = mkOption {
|
||||
type = types.str;
|
||||
internal = true;
|
||||
default = name;
|
||||
};
|
||||
|
||||
output = mkOption {
|
||||
type = types.str;
|
||||
description = "The relative path where the generated file should be placed";
|
||||
};
|
||||
|
||||
generator = mkOption {
|
||||
type = types.enum ["nix" "string" "derivation" "gotmpl" "jinja" "template"];
|
||||
description = "Which engine to use for content generation";
|
||||
};
|
||||
|
||||
data = mkOption {
|
||||
type = types.anything;
|
||||
description = "The input data for the chosen generator";
|
||||
};
|
||||
|
||||
opts = mkOption {
|
||||
type = types.attrs;
|
||||
default = {};
|
||||
description = "Generator-specific options";
|
||||
};
|
||||
|
||||
hook = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
mode = mkOption {
|
||||
type = types.enum ["link" "copy"];
|
||||
default = "link";
|
||||
description = "How the file should be managed (link or copy)";
|
||||
};
|
||||
|
||||
gitignore = mkOption {
|
||||
type = types.bool;
|
||||
default = true;
|
||||
description = "Whether to add the output path to .gitignore";
|
||||
};
|
||||
|
||||
extra = mkOption {
|
||||
type = types.str;
|
||||
default = "";
|
||||
description = "Additional bash commands to execute after file operation";
|
||||
};
|
||||
};
|
||||
};
|
||||
default = {};
|
||||
description = "Hook-specific options for file management";
|
||||
};
|
||||
|
||||
generatedDerivation = mkOption {
|
||||
type = types.package;
|
||||
internal = true;
|
||||
readOnly = true;
|
||||
description = "The generated derivation for this file";
|
||||
};
|
||||
};
|
||||
|
||||
config = {
|
||||
generatedDerivation =
|
||||
(engines.${config.generator} or (throw "Generator ${config.generator} not found"))
|
||||
{
|
||||
inherit (config) opts data name;
|
||||
};
|
||||
};
|
||||
}));
|
||||
default = {};
|
||||
description = "Configuration hooks for file generation and management";
|
||||
};
|
||||
|
||||
shellHook = mkOption {
|
||||
type = types.str;
|
||||
readOnly = true;
|
||||
description = "Generated shell hook script for managing all files";
|
||||
};
|
||||
shellHookFile = mkOption {
|
||||
type = types.package;
|
||||
readOnly = true;
|
||||
description = "Generated shell hook script for managing all files";
|
||||
};
|
||||
|
||||
finalFiles = mkOption {
|
||||
type = types.package;
|
||||
readOnly = true;
|
||||
description = "Aggregated derivation containing all managed files";
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
hooks = config.hooks;
|
||||
hookNames = builtins.attrNames hooks;
|
||||
|
||||
runHooks = concatMapStringsSep "\n" (hookName: let
|
||||
hook = hooks.${hookName};
|
||||
modes = {
|
||||
link =
|
||||
# sh
|
||||
''
|
||||
if [[ ! -L "${hook.output}" ]] || [[ "$(readlink "${hook.output}")" != "${hook.generatedDerivation}" ]]; then
|
||||
_soonix_log "info" "${hookName}" "Creating symlink: ${hook.output} -> ${hook.generatedDerivation}"
|
||||
mkdir -p "$(dirname "${hook.output}")"
|
||||
ln -sf "${hook.generatedDerivation}" "${hook.output}"
|
||||
_changed=true
|
||||
else
|
||||
_soonix_log "info" "${hookName}" "Symlink up to date: ${hook.output}"
|
||||
fi
|
||||
'';
|
||||
copy =
|
||||
# sh
|
||||
''
|
||||
if [[ ! -f "${hook.output}" ]] || ! cmp -s "${hook.generatedDerivation}" "${hook.output}"; then
|
||||
_soonix_log "info" "${hookName}" "Copying file: ${hook.generatedDerivation} -> ${hook.output}"
|
||||
mkdir -p "$(dirname "${hook.output}")"
|
||||
cp "${hook.generatedDerivation}" "${hook.output}"
|
||||
_changed=true
|
||||
else
|
||||
_soonix_log "info" "${hookName}" "File up to date: ${hook.output}"
|
||||
fi
|
||||
'';
|
||||
};
|
||||
|
||||
optionalGitignore =
|
||||
if hook.hook.gitignore
|
||||
then ''
|
||||
_soonix_add_to_gitignore "${hook.output}"
|
||||
''
|
||||
else "";
|
||||
in
|
||||
# sh
|
||||
''
|
||||
# Process hook: ${hookName}
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
UPDATED) _soonix_updated+=("${hookName}") ;;
|
||||
UPTODATE) _soonix_uptodate+=("${hookName}") ;;
|
||||
*) echo "$line" ;;
|
||||
esac
|
||||
done < <(
|
||||
set -euo pipefail
|
||||
_changed=false
|
||||
|
||||
${modes.${hook.hook.mode} or (throw "Mode ${hook.hook.mode} doesnt exist")}
|
||||
|
||||
# Add to gitignore if requested
|
||||
${optionalGitignore}
|
||||
|
||||
# Run extra commands if file changed
|
||||
if [[ "$_changed" == "true" && -n "${hook.hook.extra}" ]]; then
|
||||
_soonix_log "info" "${hookName}" "Running extra command: ${hook.hook.extra}"
|
||||
eval "${hook.hook.extra}"
|
||||
fi
|
||||
|
||||
if [[ "$_changed" == "true" ]]; then
|
||||
echo "UPDATED"
|
||||
else
|
||||
echo "UPTODATE"
|
||||
fi
|
||||
) || {
|
||||
_soonix_log "error" "${hookName}" "Failed to process hook"
|
||||
_soonix_failed+=("${hookName}")
|
||||
}
|
||||
'')
|
||||
hookNames;
|
||||
|
||||
generatedShellHook =
|
||||
# sh
|
||||
''
|
||||
_soonix_log() {
|
||||
local level="$1"
|
||||
local hook="$2"
|
||||
local message="$3"
|
||||
[[ "''${SOONIX_LOG-}" == "true" ]] && echo "$level [$hook]: $message" || true
|
||||
}
|
||||
|
||||
_soonix_add_to_gitignore() {
|
||||
local file="$1"
|
||||
local gitignore=".gitignore"
|
||||
|
||||
if [[ ! -f "$gitignore" ]]; then
|
||||
touch "$gitignore"
|
||||
fi
|
||||
|
||||
# Check if file is already in gitignore
|
||||
if ! grep -Fxq "$file" "$gitignore"; then
|
||||
# Add sentinel comments if not present
|
||||
if ! grep -q "# soonix" "$gitignore"; then
|
||||
echo "" >> "$gitignore"
|
||||
echo "# soonix" >> "$gitignore"
|
||||
echo "# end soonix" >> "$gitignore"
|
||||
fi
|
||||
|
||||
# Insert the file path before the end comment
|
||||
${pkgs.gnused}/bin/sed -i "/# end soonix/i $file" "$gitignore"
|
||||
fi
|
||||
}
|
||||
|
||||
_soonix_updated=()
|
||||
_soonix_failed=()
|
||||
_soonix_uptodate=()
|
||||
|
||||
${runHooks}
|
||||
|
||||
echo -n "[soonix] " >&2
|
||||
if [[ ''${#_soonix_updated[@]} -gt 0 ]]; then
|
||||
echo -n "[updated: ''${_soonix_updated[*]}] " >&2
|
||||
fi
|
||||
|
||||
if [[ ''${#_soonix_uptodate[@]} -gt 0 ]]; then
|
||||
echo -n "[unchanged: ''${_soonix_uptodate[*]}] " >&2
|
||||
fi
|
||||
|
||||
if [[ ''${#_soonix_failed[@]} -gt 0 ]]; then
|
||||
echo "[failed: ''${_soonix_failed[*]}]" >&2
|
||||
exit 1
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
'';
|
||||
|
||||
allFiles =
|
||||
lib.mapAttrsToList (name: hook: {
|
||||
src = hook.generatedDerivation;
|
||||
path = hook.output;
|
||||
})
|
||||
hooks;
|
||||
in rec {
|
||||
# nothing to do if no hooks exist
|
||||
shellHook =
|
||||
if (builtins.length hookNames > 0)
|
||||
then generatedShellHook
|
||||
else "";
|
||||
shellHookFile = pkgs.writeShellScript "shellHook" shellHook;
|
||||
finalFiles = buildAllFiles allFiles;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue