diff --git a/default.nix b/default.nix index cb7f5f0..fe977bf 100644 --- a/default.nix +++ b/default.nix @@ -28,7 +28,7 @@ let modules = [ # ides - ./ides.nix + ./lib/ides.nix # service config and build params (_: { inherit services serviceDefs auto; diff --git a/ides.nix b/ides.nix deleted file mode 100644 index 084910a..0000000 --- a/ides.nix +++ /dev/null @@ -1,439 +0,0 @@ -{ - 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; - }; -} diff --git a/lib/build.nix b/lib/build.nix new file mode 100644 index 0000000..c92f650 --- /dev/null +++ b/lib/build.nix @@ -0,0 +1,188 @@ +{ + pkgs, + config, + ... +}: +{ + 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 + cli = import ./cli.nix { + inherit (pkgs) writeShellScriptBin; + inherit (pkgs.lib) foldlAttrs; + inherit works; + }; + + # 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 "") + + '' + printf '[ides]: use "ides [action] [target]" to control services. type "ides help" to find out more.\n' + '' + + autoRun; + }; + in + config._buildIdes.shellFn final; + }; + +} diff --git a/lib/cli.nix b/lib/cli.nix new file mode 100644 index 0000000..61251ad --- /dev/null +++ b/lib/cli.nix @@ -0,0 +1,132 @@ +{ + foldlAttrs, + writeShellScriptBin, + works, +}: +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 ] + ) [ ] works; + 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 synonyms: t + - 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 +writeShellScriptBin "ides" '' + targets=(${builtins.concatStringsSep " " names}) + + function print-help() { + printf '${help}' + list-targets + } + + function list-targets() { + echo ''${targets[@]} + } + + function check-target() { + found=1 + for target in "''${targets[@]}"; do + if [ "$1" == "$target" ]; then + found=0 + break + fi + done + printf $found + } + + ${runFns} + + function run-all() { + ${runAll} + } + + ${cleanFns} + + function clean-all() { + ${cleanAll} + } + + function action() { + action=$1 + if [[ $# -gt 1 ]]; then + shift + for service in "$@"; do + if [[ $(check-target $service) -eq 0 ]]; then + $action-$service + else + echo "[ides]: no such target: $service" + fi + 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|t) + list-targets + ;; + -h|h|help|*) + print-help + ;; + esac +'' diff --git a/lib/ides.nix b/lib/ides.nix new file mode 100644 index 0000000..ae7f958 --- /dev/null +++ b/lib/ides.nix @@ -0,0 +1,9 @@ +{ + ... +}: +{ + imports = [ + ./options.nix + ./build.nix + ]; +} diff --git a/lib/options.nix b/lib/options.nix new file mode 100644 index 0000000..5a07c78 --- /dev/null +++ b/lib/options.nix @@ -0,0 +1,140 @@ +{ + pkgs, + ... +}: +{ + 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; + }; + }; + +}