{ pkgs, lib, config, ... }: let inherit (lib) mkEnableOption mkOption mkIf ; inherit (lib.types) submodule bool nullOr lines str path int attrsOf either ; fileType = submodule ( { options, ... }: { options = { enable = mkEnableOption "creation of this file" // { default = true; example = false; }; text = mkOption { default = ""; type = lines; description = "Text of the file"; }; source = mkOption { type = nullOr (either path str); default = null; description = "Path of the source file or directory - be sure to quote paths you don't want nix to copy to the store."; }; executable = mkOption { type = bool; default = false; example = true; description = '' Whether to set the execute bit on the target file. Only effective in combination with `.text` specified, else permissions will be of the source file. ''; }; uid = mkOption { type = nullOr int; default = null; description = '' Uid this file will belong to ''; }; gid = mkOption { type = nullOr int; default = null; description = '' Gid this file will belong to ''; }; clobber = mkOption { default = config.environment.arbys.clobber; defaultText = ''config.environment.arbys.clobber''; type = bool; description = '' Whether to clobber file or throw error on conflict ''; }; }; } ); in { options.environment = { arbys = { clobber = mkOption { type = bool; default = false; description = '' Whether to clobber conflicted files by default ''; }; enable = mkEnableOption "Arbitrary Symlink Manager"; }; files = mkOption { description = "Files to link"; type = attrsOf fileType; default = { }; }; }; config = let files = let enabled = lib.filterAttrs (_: v: v.enable) config.environment.files; sourced = builtins.mapAttrs ( n: v: let processed = if (v.source == null) then ( if (v.text == "") then throw "Must provide source or text for ${n}" else v // { source = pkgs.writeTextFile { name = "${n}-source"; text = v.text; executable = v.executable ? false; }; } ) else v; filtered = builtins.removeAttrs processed [ "text" "enable" "executable" ]; nullFiltered = lib.filterAttrs (_: v: v != null) filtered; in nullFiltered // { type = "symlink"; } ) enabled; in lib.foldlAttrs ( acc: n: v: acc ++ (lib.singleton ({ target = n; } // v)) ) [ ] sourced; manifest = pkgs.writeTextFile { name = "manifest.json"; text = ( builtins.toJSON { inherit files; clobber_by_default = config.environment.arbys.clobber; version = 1; } ); checkPhase = '' set -e CUE_CACHE_DIR=$(pwd)/.cache CUE_CONFIG_DIR=$(pwd)/.config ${lib.getExe pkgs.cue} vet -c ${./v1.cue} $target ''; }; in mkIf (config.environment.arbys.enable) { systemd.targets.arbys = { description = "Create Arbitrary Symlinks"; requiredBy = [ "sysinit-reactivation.target" ]; before = [ "sysinit-reactivation.target" ]; requires = [ "arbys-activate.service" "arbys-copy.service" "arbys-cleanup.service" ]; }; systemd.services = let manifestPath = "/var/lib/arbys"; in { arbys-prep = { description = "Ensure manifest dir exists"; script = "mkdir -p ${manifestPath}"; serviceConfig.Type = "oneshot"; unitConfig.RefuseManualStart = true; }; arbys-activate = { description = "Link files from arbys manifest"; serviceConfig.Type = "oneshot"; requires = [ "arbys-prep.service" "arbys-copy.service" ]; after = [ "arbys-prep.service" ]; script = let linker = lib.getExe (pkgs.smfh or pkgs.rustPlatform.buildRustPackage ./smfh.nix { }); in '' new_manifest=${manifest} if [ ! -f ${manifestPath}/manifest.json ]; then ${linker} activate $new_manifest exit 0 fi; ${linker} diff $new_manifest ${manifestPath}/manifest.json ''; }; arbys-copy = { description = "Copy manifest into the state dir"; serviceConfig.Type = "oneshot"; after = [ "arbys-activate.service" ]; script = "cp ${manifest} ${manifestPath}/manifest.json"; }; arbys-cleanup = { description = "Clean up manifest if disabled"; serviceConfig.type = "oneshot"; after = [ "arbys.target" ]; unitConfig.RefuseManualStart = false; script = if (lib.count (builtins.attrNames config.environment.files) == 0) then "rm ${manifestPath}/manifest.json" else "true"; }; }; }; }