mirror of
https://github.com/js0ny/dotfiles.git
synced 2026-03-22 10:42:42 +00:00
188 lines
5.9 KiB
Nix
188 lines
5.9 KiB
Nix
# mergetools.nix — Dual-mode config merge library
|
|
#
|
|
# Supports both Home Manager ("home") and NixOS system-level ("system") modes.
|
|
# All targets are ABSOLUTE paths regardless of mode.
|
|
#
|
|
# Usage (home mode — default):
|
|
# mergetools = import ../../modules/lib/mergetools.nix { inherit pkgs lib config; };
|
|
# myConfig = mergetools.mkMergedJson {
|
|
# name = "my-config";
|
|
# target = "${config.home.homeDirectory}/.config/app/config.json";
|
|
# settings = { key = "value"; };
|
|
# };
|
|
# # Then: imports = [ myConfig ];
|
|
#
|
|
# Usage (system mode):
|
|
# mergetools = import ../../modules/lib/mergetools.nix { inherit pkgs lib config; };
|
|
# myConfig = mergetools.mkMergedJson {
|
|
# name = "my-config";
|
|
# target = "/var/lib/myapp/config.json";
|
|
# settings = { key = "value"; };
|
|
# mode = "system";
|
|
# # owner = "myapp"; # default: "root"
|
|
# # group = "myapp"; # default: "root"
|
|
# # permissions = "0640"; # default: "0644"
|
|
# };
|
|
# # Then: imports = [ myConfig ];
|
|
{
|
|
config,
|
|
lib,
|
|
pkgs,
|
|
...
|
|
}: let
|
|
mkForceVar = force:
|
|
if force
|
|
then "true"
|
|
else "false";
|
|
|
|
# Derive a safe relative path for home.file from an absolute target.
|
|
# Strips the leading $HOME/ to get the relative portion.
|
|
# e.g., "/home/js0ny/.config/foo" -> ".config/foo"
|
|
stripHomePrefix = target: let
|
|
homeDir = config.home.homeDirectory;
|
|
homeDirSlash = homeDir + "/";
|
|
len = builtins.stringLength homeDirSlash;
|
|
in
|
|
if lib.hasPrefix homeDirSlash target
|
|
then builtins.substring len (builtins.stringLength target - len) target
|
|
else builtins.abort "mergetools (home mode): target '${target}' must start with '${homeDirSlash}'";
|
|
|
|
# ── Home mode ──────────────────────────────────────────────────────
|
|
|
|
mkHomeMerge = {
|
|
name,
|
|
target,
|
|
patchContent,
|
|
mergeCmdStr,
|
|
force,
|
|
emptyInit,
|
|
}: let
|
|
relTarget = stripHomePrefix target;
|
|
patchFile = "${relTarget}.nix-managed";
|
|
in {
|
|
home.file."${patchFile}".text = patchContent;
|
|
|
|
home.activation."merge-${name}" = lib.hm.dag.entryAfter ["writeBoundary"] ''
|
|
TARGET="${target}"
|
|
PATCH="$HOME/${patchFile}"
|
|
FORCE="${mkForceVar force}"
|
|
|
|
if [ -f "$TARGET" ] || [ "$FORCE" = "true" ]; then
|
|
if [ -f "$PATCH" ]; then
|
|
verboseEcho "Merging Nix managed config into: $TARGET"
|
|
run mkdir -p "$(dirname "$TARGET")"
|
|
if [ ! -f "$TARGET" ]; then
|
|
echo '${emptyInit}' > "$TARGET"
|
|
fi
|
|
run ${mergeCmdStr}
|
|
fi
|
|
else
|
|
verboseEcho "Skipping merge for $TARGET: file missing and force=false"
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# ── System mode ────────────────────────────────────────────────────
|
|
|
|
mkSystemMerge = {
|
|
name,
|
|
target,
|
|
patchContent,
|
|
mergeCmdStr,
|
|
force,
|
|
emptyInit,
|
|
owner ? "root",
|
|
group ? "root",
|
|
permissions ? "0644",
|
|
}: let
|
|
patchFile = pkgs.writeText "${name}.nix-managed" patchContent;
|
|
in {
|
|
system.activationScripts."merge-${name}" = lib.stringAfter ["etc"] ''
|
|
TARGET="${target}"
|
|
PATCH="${patchFile}"
|
|
FORCE="${mkForceVar force}"
|
|
|
|
if [ -f "$TARGET" ] || [ "$FORCE" = "true" ]; then
|
|
echo "mergetools: Merging Nix managed config into: $TARGET"
|
|
mkdir -p "$(dirname "$TARGET")"
|
|
if [ ! -f "$TARGET" ]; then
|
|
echo '${emptyInit}' > "$TARGET"
|
|
fi
|
|
${mergeCmdStr}
|
|
chown ${owner}:${group} "$TARGET"
|
|
chmod ${permissions} "$TARGET"
|
|
else
|
|
echo "mergetools: Skipping merge for $TARGET: file missing and force=false"
|
|
fi
|
|
'';
|
|
};
|
|
|
|
# ── Dispatch ───────────────────────────────────────────────────────
|
|
|
|
mkMerge = {mode ? "home", ...} @ args: let
|
|
# Strip mode-irrelevant attrs before passing
|
|
homeArgs = builtins.removeAttrs args ["mode" "owner" "group" "permissions"];
|
|
systemArgs = builtins.removeAttrs args ["mode"];
|
|
in
|
|
if mode == "home"
|
|
then mkHomeMerge homeArgs
|
|
else if mode == "system"
|
|
then mkSystemMerge systemArgs
|
|
else builtins.abort "mergetools: unknown mode '${mode}', expected 'home' or 'system'";
|
|
|
|
# ── Public API ─────────────────────────────────────────────────────
|
|
|
|
mkMergedYaml = {
|
|
name,
|
|
target,
|
|
settings,
|
|
force ? false,
|
|
mode ? "home",
|
|
owner ? "root",
|
|
group ? "root",
|
|
permissions ? "0644",
|
|
}:
|
|
mkMerge {
|
|
inherit name target mode force owner group permissions;
|
|
patchContent = lib.generators.toYAML {} settings;
|
|
# $TARGET and $PATCH are shell variables set in the activation script
|
|
mergeCmdStr = ''${pkgs.yq-go}/bin/yq -i -oy -P ". *= load(\"$PATCH\")" "$TARGET"'';
|
|
emptyInit = "{}";
|
|
};
|
|
|
|
mkMergedJson = {
|
|
name,
|
|
target,
|
|
settings,
|
|
force ? false,
|
|
mode ? "home",
|
|
owner ? "root",
|
|
group ? "root",
|
|
permissions ? "0644",
|
|
}:
|
|
mkMerge {
|
|
inherit name target mode force owner group permissions;
|
|
patchContent = builtins.toJSON settings;
|
|
mergeCmdStr = ''${pkgs.yq-go}/bin/yq -i -o json -P --indent 2 ". *= load(\"$PATCH\")" "$TARGET"'';
|
|
emptyInit = "{}";
|
|
};
|
|
|
|
mkMergedIni = {
|
|
name,
|
|
target,
|
|
settings,
|
|
force ? false,
|
|
mode ? "home",
|
|
owner ? "root",
|
|
group ? "root",
|
|
permissions ? "0644",
|
|
}:
|
|
mkMerge {
|
|
inherit name target mode force owner group permissions;
|
|
patchContent = lib.generators.toINI {} settings;
|
|
mergeCmdStr = ''${pkgs.crudini}/bin/crudini --merge "$TARGET" < "$PATCH"'';
|
|
emptyInit = "";
|
|
};
|
|
in {
|
|
inherit mkMergedYaml mkMergedJson mkMergedIni;
|
|
}
|