{ pkgs, config, ... }: { # # interface # options = let inherit (pkgs) lib; inherit (lib) types mkOption; serviceConfig = with types; submodule { options = { pkg = mkOption { type = package; description = "Package to use for service."; example = "pkgs.caddy"; }; exec = mkOption { type = str; description = "Alternative executable name to use from `pkg`."; example = "caddy"; default = ""; }; args = mkOption { type = str; description = "Arguments to supply to the service binary. Writing %CFG% in this will template to your config location."; example = "run -c %CFG% --adapter caddyfile"; default = ""; }; socket = mkOption { type = attrsOf (listOf str); description = "List of socket options for the unit (see `man systemd.socket`) - supplied as a list due to some options allowing duplicates."; example = { ListenStream = [ "/run/user/1000/myapp.sock" ]; }; default = { }; }; path = mkOption { type = attrsOf (listOf str); description = "List of path options for the unit (see `man systemd.path`) - supplied as a list due to some options allowing duplicates."; example = { PathModified = [ "/some/path" ]; }; default = { }; }; timer = mkOption { type = attrsOf (listOf str); description = "List of timer options for the unit (see `man systemd.path`) - supplied as a list due to some options allowing duplicates."; example = { OnActiveSec = [ 50 ]; }; default = { }; }; config = mkOption { description = "Options for setting the service's configuration."; default = { }; type = submodule { options = { text = mkOption { type = str; default = ""; description = "Plaintext configuration to use."; example = '' http://*:8080 { respond "hello" } ''; }; ext = mkOption { type = str; default = ""; description = "If your service config requires a file extension, set it here. This overrides `format`'s output path'."; example = "json"; }; file = mkOption { type = nullOr path; description = "Path to config file. This overrides all other values."; example = "./configs/my-config.ini"; default = null; }; content = mkOption { type = nullOr attrs; description = "Attributes that define your config values."; default = null; example = { this = "that"; }; }; format = mkOption { type = nullOr (enum [ "java" "json" "yaml" "toml" "ini" "xml" "php" ]); description = "Config output format.\nOne of:\n`java json yaml toml ini xml php`."; example = "json"; default = null; }; formatter = mkOption { type = types.anything; description = "Serialisation/writer function to apply to `content`.\n`format` will auto-apply the correct format if the option value is valid.\nShould take `path: attrs:` and return a storepath."; example = "pkgs.formats.yaml {}.generate"; default = null; }; }; }; }; }; }; in { serviceDefs = mkOption { type = types.attrsOf serviceConfig; description = "Concrete service definitions, as per submodule options.\nPlease put service-related options into `options.services` instead, and use this to implement those options."; }; auto = mkOption { type = types.bool; description = "Whether to autostart ides services at devshell instantiation."; default = true; }; # to prevent generating docs for this option; see https://github.com/NixOS/nixpkgs/issues/293510 _module.args = mkOption { internal = true; }; # for internal use _buildIdes = mkOption { type = types.attrs; internal = true; }; }; # # implementation # config = let # control flow monstrosity branchOnConfig = cfg: { text, file, content, contentFmt, }: if (cfg.text != "") then text else if (cfg.file != null) then file else if (cfg.content != { }) then if (cfg.format != null) then content else if (cfg.formatter != null) then contentFmt else throw "`format` or `formatter` must be set for `content` value ${cfg.content}!" else ""; in { # validate and complete the service configurations _buildIdes.finalServices = builtins.mapAttrs ( name: { pkg, args ? "", exec ? "", config, path, socket, timer, }: let # make our best effort to use the correct binary bin = if (exec == "") then pkgs.lib.getExe pkg else pkgs.lib.getExe' pkg exec; # set file extension ext = if (config.ext != "") || (config.format != null) then "." + (config.ext or config.format) else ""; # config hash for unique service names cfgHash = let # method to hash a set hashContent = builtins.hashString "sha256" (builtins.toJSON config.content); in branchOnConfig config { text = builtins.hashString "sha256" config.text; file = builtins.hashFile "sha256" config.file; content = hashContent; contentFmt = hashContent; }; confFile = let writers = { java = pkgs.formats.javaProperties { }; json = pkgs.formats.json { }; yaml = pkgs.formats.yaml { }; ini = pkgs.formats.ini { }; toml = pkgs.formats.toml { }; xml = pkgs.formats.xml { }; php = pkgs.formats.php { finalVariable = null; }; }; # final config name confPath = "config-${name}-${cfgHash}${ext}"; in # write out config branchOnConfig config { text = pkgs.writeText confPath config.text; inherit (config) file; content = writers.${config.format}.generate confPath config.content; contentFmt = config.formatter confPath config.content; }; # template the config path into the launch command cfgArgs = builtins.replaceStrings [ "%CFG%" ] [ "${confFile}" ] args; # flatten unit options into cli args sdArgs = let inherit (pkgs.lib) foldlAttrs; inherit (builtins) concatStringsSep; convertToArgList = prefix: name: values: (map (inner: "${prefix} ${name}=${inner}") values); writeArgListFor = attrs: prefix: if (attrs != { }) then concatStringsSep " " ( foldlAttrs ( acc: n: v: acc + (convertToArgList prefix n v) + " " ) "" attrs ) else ""; in concatStringsSep " " [ (writeArgListFor socket "--socket-property") (writeArgListFor path "--path-property") (writeArgListFor timer "--timer-property") ]; in # transform into attrs that mkWorks expects to receive { inherit bin sdArgs cfgArgs ; unitName = "shell-${name}-${cfgHash}"; } ) config.serviceDefs; # generate service scripts and create the shell _buildIdes.shell = let # create commands to run and clean up services mkWorks = name: { unitName, bin, cfgArgs, sdArgs, }: { runner = pkgs.writeShellScriptBin "run" '' echo "[ides]: Starting ${name}.." systemd-run --user -G -u ${unitName} ${sdArgs} ${bin} ${cfgArgs} ''; cleaner = pkgs.writeShellScriptBin "clean" '' echo "[ides]: Stopping ${name}.." systemctl --user stop ${unitName} ''; }; works = pkgs.lib.mapAttrs ( name: serviceConf: mkWorks name serviceConf ) config._buildIdes.finalServices; # create the ides cli inherit (pkgs) writeShellScriptBin; inherit (pkgs.lib) foldlAttrs; cli = let runAll = foldlAttrs ( acc: name: works: acc + "${works.runner}/bin/run\n" ) "" works; runFns = foldlAttrs ( acc: name: works: acc + '' function run-${name}() { ${works.runner}/bin/run } '' ) "" works; cleanAll = foldlAttrs ( acc: name: works: acc + "${works.cleaner}/bin/clean\n" ) "" works; cleanFns = foldlAttrs ( acc: name: works: acc + '' function clean-${name}() { ${works.cleaner}/bin/clean } '' ) "" works; names = foldlAttrs ( acc: name: _: acc + "${name}\n" ) "" works; in writeShellScriptBin "ides" ( let help = '' [ides]: use "ides [action] [target]" to control services. actions: start synonyms: run r - start a service stop synonyms: s clean et-tu - stop a service restart synonyms: qq - stop and then restart all services targets - print a list of available targets help - print this helpful information target names are the same as the attribute used to define a service. an empty target will execute the action on all available services. current targets: ''; in '' function print-help() { printf '${help}' list-targets } function list-targets() { printf '${names}\n' } ${runFns} function run-all() { ${runAll} } ${cleanFns} function clean-all() { ${cleanAll} } function action() { action=$1 if [ $# -gt 1 ]; then shift for service in "$@"; do $action-$service done else $action-all fi } case $1 in start|run|r) shift action run $@ ;; clean|stop|et-tu|s) shift action clean $@ ;; restart|qq) clean-all run-all ;; targets) list-targets ;; -h|h|help|*) print-help ;; esac '' ); # create the ides shell final = let inherit (config._buildIdes) shellArgs; in shellArgs // { nativeBuildInputs = (shellArgs.nativeBuildInputs or [ ]) ++ [ cli ]; shellHook = let autoRun = if config.auto then '' ides run '' else ""; in (shellArgs.shellHook or "") + '' ides help '' + autoRun; }; in config._buildIdes.shellFn final; }; }