modularise ides workings

This commit is contained in:
atagen 2025-02-07 15:12:17 +11:00
parent 9248cc0945
commit caa156daf4
6 changed files with 470 additions and 440 deletions

View File

@ -28,7 +28,7 @@ let
modules =
[
# ides
./ides.nix
./lib/ides.nix
# service config and build params
(_: {
inherit services serviceDefs auto;

439
ides.nix
View File

@ -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;
};
}

188
lib/build.nix Normal file
View File

@ -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;
};
}

132
lib/cli.nix Normal file
View File

@ -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
''

9
lib/ides.nix Normal file
View File

@ -0,0 +1,9 @@
{
...
}:
{
imports = [
./options.nix
./build.nix
];
}

140
lib/options.nix Normal file
View File

@ -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;
};
};
}