diff --git a/Cargo.lock b/Cargo.lock index 7f13360..cba4ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -158,6 +158,8 @@ dependencies = [ "fs-err", "itertools", "libc", + "regex", + "regex-lite", "serde", "serde_json", "tap", @@ -422,6 +424,18 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.4.14" @@ -433,6 +447,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-lite" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973" + [[package]] name = "regex-syntax" version = "0.8.9" diff --git a/Cargo.toml b/Cargo.toml index dd24d30..2e6dd55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,12 +11,19 @@ path = "src/main.rs" name = "dynix" path = "src/lib.rs" +[features] +default = ["regex-full"] +regex-full = ["dep:regex"] +regex-lite = ["dep:regex-lite"] + [dependencies] clap = { version = "4.5.54", features = ["color", "derive"] } command-error = "0.8.0" fs-err = "3.2.2" itertools = "0.14.0" libc = { version = "0.2.180", features = ["extra_traits"] } +regex = { version = "1.12.3", optional = true } +regex-lite = { version = "0.1.9", optional = true } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" tap = "1.0.1" diff --git a/modules/dynamicism/dynamicism.nix b/modules/dynamicism/dynamicism.nix index 1060f85..50f921d 100644 --- a/modules/dynamicism/dynamicism.nix +++ b/modules/dynamicism/dynamicism.nix @@ -11,7 +11,6 @@ let inherit (import ./lib.nix { inherit lib; }) typeCheck - convenientAttrPath concatFoldl recUpdateFoldl recUpdateFoldlAttrs @@ -64,6 +63,13 @@ in default = { }; }; + finalEnabledSubmodules = mkOption { + type = options.dynamicism.for.type; + internal = true; + readOnly = true; + default = lib.filterAttrs (lib.const (lib.getAttr "enable")) config.dynamicism.for; + }; + finalSettings = mkOption { type = t.attrsOf t.raw; internal = true; @@ -78,14 +84,6 @@ in type = t.functionTo t.raw; readOnly = true; }; - - doChange = mkOption { - type = t.functionTo t.pathInStore; - readOnly = true; - description = '' - The function to call to Do The Thing. - ''; - }; }; # Assertions. @@ -94,27 +92,49 @@ in # # Generic implementation. # - config.system.activationScripts."dynamicism-reset" = { - deps = [ "etc" "stdio" "specialfs" ]; - text = '' - echo "DYNIX: removing existing systemd dropins" - # FIXME: do for each enabled submodule - if [[ -d /run/systemd/system ]]; then - rm -v /run/systemd/system/*/dynix-*.conf || true + + config.system.activationScripts = config.dynamicism.for + |> lib.filterAttrs (lib.const (lib.getAttr "enable")) + |> lib.mapAttrs' (name: submod: let + forUnit = unitName: assert lib.isString unitName; let + dropinDir = "/run/systemd/system/${unitName}.d"; + systemctl = lib.getExe' pkgs.systemd "systemctl"; + in '' + if [[ -d "${dropinDir}" ]]; then + echo "Removing files in "${dropinDir}" >&2 + + rm -rvf "${dropinDir}/"* + rmdir "${dropinDir}" + + ${systemctl} daemon-reload + ${systemctl} try-reload-or-restart "${unitName}" fi ''; - }; + + in { + name = "dynix-reset-dynamicism-for-${name}"; + value.deps = [ "etc" "stdio" "specialfs" "binsh" "usrbinenv" "var" "udevd" ]; + value.text = '' + echo "Removing existing dynamic overrides for ${name}" >&2 + ${submod.systemd-services-updated |> lib.map forUnit |> lib.concatStringsSep "\n"} + ''; + }); + config.dynamicism = { applyDynamicConfiguration = { baseConfiguration ? builtins.getEnv "NIXOS_CONFIG", - newConfiguration ? baseConfiguration + "/dynamic.nix", + newConfiguration ? (lib.filesystem.dirOf baseConfiguration) + "/dynamic.nix", }: let locFor = appendage: lib.concatLists [ opts.applyDynamicConfiguration.loc [ "(function argument)" ] (lib.toList appendage) ]; + in + assert seqTrue (typeCheck (locFor "baseConfiguration") t.deferredModule baseConfiguration); + assert seqTrue (typeCheck (locFor "newConfiguration") t.deferredModule newConfiguration); + let _file = "«inline module in ${showOption opts.applyDynamicConfiguration.loc}»"; @@ -132,49 +152,25 @@ in }; }; - submodulesChanged = lib.filter (submodName: + submodulesChanged = nixosAfter.config.dynamicism.finalEnabledSubmodules + |> lib.filterAttrs (submodName: _: nixosBefore.config.dynamicism.for.${submodName}.finalSettings != nixosAfter.config.dynamicism.for.${submodName}.finalSettings - ) (lib.attrNames config.dynamicism.for); - in - assert seqTrue (typeCheck (locFor "baseConfiguration") t.deferredModule baseConfiguration); - assert seqTrue (typeCheck (locFor "newConfiguration") t.deferredModule newConfiguration); - { - inherit submodulesChanged; - }; + ); - doChange = { - option, - value, - configuration ? builtins.getEnv "NIXOS_CONFIG", - }: let - loc = opts.doChange.loc ++ [ "(function argument)" "value" ]; - option' = typeCheck loc convenientAttrPath option; - nixosAfter = evalNixos { - configuration = { config, ... }: { - imports = [ - configuration - (lib.setAttrByPath option' (lib.mkOverride (-999) value)) - ]; - - environment.systemPackages = [ - config.dynamicism.for.gotosocial.activate - ]; - }; - }; - - runAllActivationScripts = nixosAfter.config.dynamicism.for - |> lib.filterAttrs (lib.const (lib.getAttr "enable")) - |> lib.mapAttrsToList (name: submod: '' + runForSubmodCalled = name: '' echo "Activating dynamic configuration for ${name}" - ${lib.getExe submod.activate} - '') + ${lib.getExe nixosAfter.config.dynamicism.for.${name}.activate} + ''; + + runForChanged = submodulesChanged + |> lib.mapAttrsToList (name: _: runForSubmodCalled name) |> lib.concatStringsSep "\n"; in pkgs.writeShellApplication { name = "dynamicism-activate"; - text = runAllActivationScripts; + text = runForChanged; passthru.configuration = nixosAfter; }; diff --git a/modules/dynamicism/gotosocial.nix b/modules/dynamicism/gotosocial.nix index f8b30a9..b1fedc6 100644 --- a/modules/dynamicism/gotosocial.nix +++ b/modules/dynamicism/gotosocial.nix @@ -12,7 +12,7 @@ in text = '' [Service] ExecStart= - ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${configFile} start + ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${configFile} server start ''; passthru = { inherit configFile; }; }; diff --git a/modules/dynamicism/lib.nix b/modules/dynamicism/lib.nix index a957f38..ec41ab7 100644 --- a/modules/dynamicism/lib.nix +++ b/modules/dynamicism/lib.nix @@ -18,6 +18,19 @@ in lib.fix (self: { /** Either a list of strings, or a dotted string that will be split. */ convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); + executablePathInStore = lib.mkOptionType { + name = "exepath"; + description = "executable path in the Nix store"; + descriptionClass = "noun"; + merge = lib.mergeEqualOption; + functor = lib.defaultFunctor "exepath"; + check = x: if lib.isDerivation x then ( + x.meta.mainProgram or null != null + ) else ( + lib.pathInStore.check x + ); + }; + concatFoldl = f: list: lib.foldl' (acc: value: acc ++ (f value)) [ ] list; recUpdateFoldl = f: list: lib.foldl' (acc: value: lib.recursiveUpdate acc (f value)) { } list; recUpdateFoldlAttrs = f: attrs: lib.foldlAttrs (acc: name: value: lib.recursiveUpdate acc (f name value)) { } attrs; diff --git a/modules/dynamicism/submodule.nix b/modules/dynamicism/submodule.nix index bf26b2a..4100217 100644 --- a/modules/dynamicism/submodule.nix +++ b/modules/dynamicism/submodule.nix @@ -14,32 +14,15 @@ let mkEnableOption literalExpression ; - inherit (lib.types) - mkOptionType - ; t = lib.types; inherit (import ./lib.nix { inherit lib; }) + convenientAttrPath + executablePathInStore recUpdateFoldl ; pkgs = host.pkgs; - - /** Either a list of strings, or a dotted string that will be split. */ - convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); - - executablePathInStore = mkOptionType { - name = "exepath"; - description = "executable path in the Nix store"; - descriptionClass = "noun"; - merge = lib.mergeEqualOption; - functor = lib.defaultFunctor "exepath"; - check = x: if lib.isDerivation x then ( - x.meta.mainProgram or null != null - ) else ( - lib.pathInStore.check x - ); - }; in { options = { @@ -74,8 +57,9 @@ in }; configFile = mkOption { - type = t.pathInStore; + type = t.nullOr t.pathInStore; internal = true; + default = null; }; unitDropins = mkOption { diff --git a/package.nix b/package.nix index 6e16ec3..448ab31 100644 --- a/package.nix +++ b/package.nix @@ -122,9 +122,7 @@ in { fenixToolchain, }: let mkShell' = mkShell.override { inherit stdenv; }; - pyEnv = python3Packages.python.withPackages (p: [ - p.beartype - ]); + pyEnv = python3Packages.python.withPackages (p: [ p.beartype ]); in mkShell' { name = "devshell-for-${self.name}"; inputsFrom = [ self ]; diff --git a/src/args.rs b/src/args.rs index cc2fa07..0028e0f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,4 +1,7 @@ -use std::sync::Arc; +use std::{ + env, + sync::{Arc, LazyLock}, +}; use clap::ColorChoice; @@ -52,6 +55,7 @@ impl FromStr for NixOsOption { pub struct AppendCmd { #[arg(required = true)] pub name: Arc, + #[arg(required = true)] pub value: Arc, } @@ -67,6 +71,28 @@ pub enum Subcommand { Delta(DeltaCmd), } +static DEFAULT_PATH: LazyLock> = LazyLock::new(|| { + // This has to be in a let binding to keep the storage around. + let nixos_config = env::var_os("NIXOS_CONFIG"); + let nixos_config = nixos_config + .as_deref() + .map(Path::new) + .unwrap_or(Path::new("/etc/nixos/configuration.nix")); + + nixos_config + .parent() + .unwrap_or_else(|| { + error!( + "Your $NIXOS_CONFIG value doesn't make sense: {}. Ignoring.", + nixos_config.display(), + ); + Path::new("/etc/nixos") + }) + .join("dynamic.nix") + .into_os_string() + .into_boxed_os_str() +}); + #[derive(Debug, Clone, PartialEq, clap::Parser)] #[command(version, about, author)] #[command(arg_required_else_help(true), args_override_self(true))] @@ -75,8 +101,10 @@ pub struct Args { #[arg(long, global(true), default_value = "auto")] pub color: ColorChoice, - // FIXME: default to /etc/configuration.nix, or something? - #[arg(long, global(true), default_value = "./configuration.nix")] + /// The .nix file with dynamic overrides to modify. + /// [default: $(dirname ${NIXOS_CONFIG-/etc/nixos/configuration.nix})/dynamic.nix] + #[arg(long, global(true), default_value = &**DEFAULT_PATH)] + #[arg(hide_default_value(true))] pub file: Arc, #[command(subcommand)] diff --git a/src/lib.rs b/src/lib.rs index 1956803..cf7839a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,7 @@ -use std::{iter, sync::Arc}; +use std::{ + iter, + sync::{Arc, LazyLock}, +}; pub(crate) mod prelude { #![allow(unused_imports)] @@ -41,14 +44,38 @@ pub mod line; mod nixcmd; pub use line::Line; pub mod source; -pub use source::SourceLine; +pub use source::{SourceFile, SourceLine}; +#[cfg(all(not(feature = "regex-full"), not(feature = "regex-lite")))] +compile_error!("At least one of features `regex-full` or `regex-lite` must be used"); + +#[cfg(feature = "regex-full")] +use regex as _regex; + +// Having both `regex-full` and `regex-lite` isn't an error; it's just wasteful. +#[cfg(not(feature = "regex-full"))] +use regex_lite as _regex; + +use _regex::Regex; + +use itertools::Itertools; use serde::{Deserialize, Serialize}; -use crate::source::SourceFile; - pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; +/// Regex pattern to extract the priority in a `lib.mkOverride` call. +static MK_OVERRIDE_RE: LazyLock = LazyLock::new(|| { + // Named capture group: priority + // - Word boundary + // - Literal `mkOverride` + // - One or more whitespace characters + // - Literal open parenthesis + // - Named capture group "priority" + // - One or more of: digit characters, or literal `-` + // - Literal close parenthesis + Regex::new(r"(?-u)\bmkOverride\s+\((?[\d-]+)\)").unwrap() +}); + #[tracing::instrument(level = "debug")] pub fn do_delta(args: Arc, delta_args: DeltaCmd) -> Result<(), BoxDynError> { todo!(); @@ -65,17 +92,15 @@ pub fn do_append(args: Arc, append_args: AppendCmd) -> Result<(), BoxDynEr filepath.to_path_buf() }; - // Get what file that thing is defined in. - let def_path = get_where(&append_args.name, &filepath)?; - let mut opts = File::options(); opts.read(true) .write(true) .create(false) .custom_flags(libc::O_CLOEXEC); - let source_file = SourceFile::open_from(Arc::from(def_path), opts)?; - let pri = get_highest_prio(&append_args.name, source_file.clone())?; + let source_file = SourceFile::open_from(Arc::from(filepath), opts)?; + let pri = get_where(source_file.clone())?; + let new_pri = pri - 1; let new_pri_line = get_next_prio_line( @@ -85,7 +110,7 @@ pub fn do_append(args: Arc, append_args: AppendCmd) -> Result<(), BoxDynEr append_args.value.into(), )?; - eprintln!("new_pri_line={new_pri_line}"); + debug!("new_pri_line={new_pri_line}"); write_next_prio(source_file, new_pri_line)?; @@ -108,35 +133,30 @@ pub fn expr_for_configuration(source_file: &Path) -> OsString { .collect() } -pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result, BoxDynError> { - let expr = expr_for_configuration(configuration_nix); - let attrpath = format!("options.{}.definitionsWithLocations", option_name); - - let output = nixcmd::NixEvalExpr { expr, attrpath } - .into_command() - .output_checked_utf8()?; - let stdout = output.stdout(); - - let definitions: Box<[DefinitionWithLocation]> = serde_json::from_str(&stdout)?; - let last_location = definitions.into_iter().last().unwrap(); - - Ok(Box::from(last_location.file)) +fn maybe_extract_prio_from_line(line: &SourceLine) -> Option { + MK_OVERRIDE_RE + .captures(line.text_ref()) + .map(|caps| caps.name("priority").unwrap().as_str()) + .map(|prio_str| { + i64::from_str(prio_str).unwrap_or_else(|e| { + panic!( + "lib.mkOverride called with non-integer {}: {}. Nix source code is wrong!\n{}", + prio_str, e, line, + ); + }) + }) } -pub fn get_highest_prio(option_name: &str, source: SourceFile) -> Result { - // Get the current highest priority. +pub fn get_where(dynamic_nix: SourceFile) -> Result { + let lines = dynamic_nix.lines()?; + let prio = lines + .into_iter() + .filter_map(maybe_extract_prio_from_line) + .sorted_unstable() + .next() // Priorities with lower integer values are "stronger" priorities. + .unwrap_or(0); - let expr = expr_for_configuration(&source.path()); - - // Get the highest priority, and the file its defined in. - let attrpath = format!("options.{}.highestPrio", option_name); - let output = nixcmd::NixEvalExpr { expr, attrpath } - .into_command() - .output_checked_utf8()?; - let stdout = output.stdout(); - let highest_prio = i64::from_str(stdout.trim())?; - - Ok(highest_prio) + Ok(prio) } pub fn get_next_prio_line( @@ -146,15 +166,19 @@ pub fn get_next_prio_line( new_value: Arc, ) -> Result { let source_lines = source.lines()?; - let last_line = source_lines.last(); - assert_eq!(last_line.map(SourceLine::text).as_deref(), Some("]")); - let last_line = last_line.unwrap(); + let penultimate = source_lines.get(source_lines.len() - 2); + // FIXME: don't rely on whitespace lol + debug_assert_eq!(penultimate.map(SourceLine::text).as_deref(), Some(" ];")); + let penultimate = penultimate.unwrap(); + + let new_generation = 0 - new_prio; let new_line = SourceLine { - line: last_line.line, + line: penultimate.line, path: source.path(), text: Arc::from(format!( - " {option_name} = lib.mkOverride ({new_prio}) ({new_value});", + " {} = lib.mkOverride ({}) ({}); # DYNIX GENERATION {}", + option_name, new_prio, new_value, new_generation, )), }; @@ -165,12 +189,12 @@ pub fn write_next_prio(mut source: SourceFile, new_line: SourceLine) -> Result<( let new_mod_start = SourceLine { line: new_line.line.prev(), path: source.path(), - text: Arc::from(" {"), + text: Arc::from(" {"), }; let new_mod_end = SourceLine { line: new_line.line.next(), path: source.path(), - text: Arc::from(" }"), + text: Arc::from(" }"), }; source.insert_lines(&[new_mod_start, new_line, new_mod_end])?; diff --git a/tests/default.nix b/tests/default.nix index 43dd66f..535c54b 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -39,12 +39,17 @@ enable = true; serviceConfig.Type = "oneshot"; serviceConfig.RemainAfterExit = true; - serviceConfig.RequisteOf = [ "multi-user.target" ]; path = [ config.system.path ]; wantedBy = [ "multi-user.target" ]; + requiredBy = [ "multi-user.target" ]; after = [ "default.target" ]; script = '' - nix profile install -vv "${dynix.modules}" # " + if [[ -e /etc/nixos/hardware-configuration.nix ]]; then + echo "install-dynix: configuration already copied; nothing to do" + exit 0 + fi + + nix profile install -vv "${dynix.drvPath}^*" # " mkdir -vp /etc/nixos nixos-generate-config @@ -58,6 +63,8 @@ passthru = { inherit options; }; + environment.systemPackages = [ config.passthru.configurationDotNix ]; + # Just making something in this strict in `name`, # which is only present as an argument for nodes and I don't want to # confuse that with the test modules. diff --git a/tests/distccd/test-script.py b/tests/distccd/test-script.py index 9d408af..d5867bf 100644 --- a/tests/distccd/test-script.py +++ b/tests/distccd/test-script.py @@ -4,7 +4,7 @@ import functools #from pprint import pformat import shlex import textwrap -from typing import cast, TYPE_CHECKING +from typing import Any, cast, TYPE_CHECKING from beartype import beartype @@ -68,6 +68,27 @@ def get_cli_args() -> argparse.Namespace: args, rest = parser.parse_known_args(cmdline_args) return args +@beartype +def dynix_append(option: str, value: Any): + machine.succeed(f''' + dynix append {shlex.quote(option)} {shlex.quote(str(value))} + '''.strip()) + +@beartype +def do_apply(): + expr = textwrap.dedent(""" + let + nixos = import { }; + in nixos.config.dynamicism.applyDynamicConfiguration { + baseConfiguration = /etc/nixos/configuration.nix; + newConfiguration = /etc/nixos/dynamic.nix; + } + """).strip() + + machine.succeed(rf""" + nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} + """.strip()) + machine.wait_for_unit("default.target") assert "lix" in machine.succeed("nix --version").lower() machine.log("INIT") @@ -89,17 +110,8 @@ args = get_cli_args() #assert int(args['max_connection_rate']) == 256, f"{args['max_connection_rate']=} != 256" # new_jobs = 4 -expr = textwrap.dedent(f""" - let - nixos = import {{ }}; - in nixos.config.dynamicism.doChange {{ - option = "services.distccd.maxJobs"; - value = {new_jobs}; - }} -""").strip() -machine.succeed(rf""" - nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} -""".strip()) +dynix_append("services.distccd.maxJobs", new_jobs) +do_apply() args = get_cli_args() @@ -109,17 +121,8 @@ assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' assert args.log_level == 'warning', f'{args.log_level=} != warning' new_log_level = 'error' -expr = textwrap.dedent(f""" - let - nixos = import {{ }}; - in nixos.config.dynamicism.doChange {{ - option = "services.distccd.logLevel"; - value = "{new_log_level}"; - }} -""").strip() -machine.succeed(rf""" - nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} -""".strip()) +dynix_append("services.distccd.logLevel", f'"{new_log_level}"') +do_apply() args = get_cli_args() #assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}' diff --git a/tests/distccd/test.nix b/tests/distccd/test.nix index 2e8089f..ad9ac73 100644 --- a/tests/distccd/test.nix +++ b/tests/distccd/test.nix @@ -1,4 +1,7 @@ { mkDynixConfigurationDotNix, config, ... }: +let + testName = config.name; +in { name = "nixos-test-dynamicism-distccd"; @@ -6,17 +9,13 @@ extraPythonPackages = p: [ p.beartype ]; - nodes.machine = { name, pkgs, ... }: { + nodes.machine = { ... }: { imports = [ ./configuration.nix ]; - environment.systemPackages = let - configFileTree = mkDynixConfigurationDotNix { - inherit (config) name; - configuration = ./configuration.nix; - }; - in [ - configFileTree - ]; + passthru.configurationDotNix = mkDynixConfigurationDotNix { + name = testName; + configuration = ./configuration.nix; + }; }; testScript = builtins.readFile ./test-script.py; diff --git a/tests/gotosocial/test-script.py b/tests/gotosocial/test-script.py index 2fa42ed..9799dfb 100644 --- a/tests/gotosocial/test-script.py +++ b/tests/gotosocial/test-script.py @@ -46,37 +46,75 @@ def get_config_file() -> str: config_file_path = machine.out_dir / config_file.name with open(config_file_path, "r") as f: - return f.read() + data = f.read() + + config_file_path.unlink() + + return data + +@beartype +def dynix_append(option: str, value: str): + machine.succeed(f''' + dynix append {shlex.quote(option)} {shlex.quote(value)} + '''.strip()) + +@beartype +def do_apply(): + expr = textwrap.dedent(""" + let + nixos = import { }; + in nixos.config.dynamicism.applyDynamicConfiguration { + baseConfiguration = /etc/nixos/configuration.nix; + newConfiguration = /etc/nixos/dynamic.nix; + } + """).strip() + + machine.succeed(rf""" + nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} + """.strip()) machine.wait_for_unit("default.target") assert "lix" in machine.succeed("nix --version").lower() -machine.log("INIT") +machine.wait_for_unit("install-dynix.service") + +dynix_out = machine.succeed("dynix --version") +assert "dynix" in dynix_out, f"dynix not in {dynix_out=}" + +machine.log("REBUILDING configuration inside VM") +machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") +machine.wait_for_unit("gotosocial.service") + +# Make sure the config before any dynamic changes is what we expect. +config_text = get_config_file() +lines = config_text.splitlines() +try: + application_name = next(line for line in lines if line.startswith("application-name:")) +except StopIteration: + raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") +assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" + +new_app_name = "yay!" +dynix_append("services.gotosocial.settings.application-name", f'"{new_app_name}"') +do_apply() + +config_text = get_config_file() +lines = config_text.splitlines() +try: + application_name = next(line for line in lines if line.startswith("application-name:")) +except StopIteration: + raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") +assert new_app_name in application_name, f"'{new_app_name}' should be in {application_name=}" machine.log("REBUILDING configuration inside VM") machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") +machine.wait_for_unit("gotosocial.service") + config_text = get_config_file() lines = config_text.splitlines() -application_name = next((line for line in lines if line.startswith("application-name:")), None) -assert application_name is not None, f"no 'application-name:' found in config file: {textwrap.indent(config_text, "")}" +try: + application_name = next(line for line in lines if line.startswith("application-name:")) +except StopIteration: + raise AssertionError(f"no 'application-name:' found in config file: {textwrap.indent(config_text, " ")}") assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" - -new_app_name = "yay!" -expr = textwrap.dedent(f""" - let - nixos = import {{ }}; - in nixos.config.dynamicism.doChange {{ - option = "services.gotosocial.settings.application-name"; - value = "{new_app_name}"; - }} -""").strip() -machine.succeed(rf""" - nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} -""".strip()) - -config_file_new = get_config_file() -lines = config_file_new.splitlines() - -application_name = next(line for line in lines if line.startswith("application-name:")) -assert new_app_name in application_name, f"'{new_app_name}' should be in {application_name=}" diff --git a/tests/gotosocial/test.nix b/tests/gotosocial/test.nix index fd5bfbb..ccb71f6 100644 --- a/tests/gotosocial/test.nix +++ b/tests/gotosocial/test.nix @@ -1,5 +1,7 @@ { mkDynixConfigurationDotNix, config, ... }: - +let + testName = config.name; +in { name = "nixos-test-dynamicism-gotosocial"; @@ -9,21 +11,17 @@ p.beartype ]; - nodes.machine = { pkgs, ... }: { + nodes.machine = { ... }: { # NOTE: Anything in this `nodes.machine = ` module will not be included # in the VM's NixOS configuration once it does `nixos-rebuild switch`, # except for `./configuration.nix` which will be copied to `/etc/nixos/`. # dynix will also be statefully installed to root's user profile. imports = [ ./configuration.nix ]; - environment.systemPackages = let - configFileTree = mkDynixConfigurationDotNix { - inherit (config) name; - configuration = ./configuration.nix; - }; - in [ - configFileTree - ]; + passthru.configurationDotNix = mkDynixConfigurationDotNix { + name = testName; + configuration = ./configuration.nix; + }; }; testScript = builtins.readFile ./test-script.py; diff --git a/tests/harmonia/test-script.py b/tests/harmonia/test-script.py index e96a053..215ea38 100644 --- a/tests/harmonia/test-script.py +++ b/tests/harmonia/test-script.py @@ -55,6 +55,27 @@ def get_config_file() -> dict[str, Any]: config_file_path.unlink() return config_data +@beartype +def dynix_append(option: str, value: Any): + machine.succeed(f''' + dynix append {shlex.quote(option)} {shlex.quote(str(value))} + '''.strip()) + +@beartype +def do_apply(): + expr = textwrap.dedent(""" + let + nixos = import { }; + in nixos.config.dynamicism.applyDynamicConfiguration { + baseConfiguration = /etc/nixos/configuration.nix; + newConfiguration = /etc/nixos/dynamic.nix; + } + """).strip() + + machine.succeed(rf""" + nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} + """.strip()) + machine.wait_for_unit("default.target") assert "lix" in machine.succeed("nix --version").lower() machine.log("INIT") @@ -75,17 +96,8 @@ assert int(config_toml['workers']) == 4, f"{config_toml['workers']=} != 4" assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" new_workers = 20 -expr = textwrap.dedent(f""" - let - nixos = import {{ }}; - in nixos.config.dynamicism.doChange {{ - option = "services.harmonia.settings.workers"; - value = {new_workers}; - }} -""").strip() -machine.succeed(rf""" - nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} -""".strip()) +dynix_append("services.harmonia.settings.workers", new_workers) +do_apply() # Workers, but not max connection rate, should have changed. config_toml = get_config_file() @@ -94,17 +106,8 @@ assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} ! assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" new_max_connection_rate = 100 -expr = textwrap.dedent(f""" - let - nixos = import {{ }}; - in nixos.config.dynamicism.doChange {{ - option = [ "services" "harmonia" "settings" "max_connection_rate" ]; - value = {new_max_connection_rate}; - }} -""").strip() -machine.succeed(rf""" - nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)} -""".strip()) +dynix_append("services.harmonia.settings.max_connection_rate", new_max_connection_rate) +do_apply() # Max connection rate should have changed, but workers should have reverted. config_toml = get_config_file() diff --git a/tests/harmonia/test.nix b/tests/harmonia/test.nix index 873c7dd..25fcfbf 100644 --- a/tests/harmonia/test.nix +++ b/tests/harmonia/test.nix @@ -1,4 +1,7 @@ { mkDynixConfigurationDotNix, config, ... }: +let + testName = config.name; +in { name = "nixos-test-dynamicism-harmonia"; @@ -6,21 +9,13 @@ extraPythonPackages = p: [ p.beartype ]; - nodes.machine = { name, pkgs, ... }: { + nodes.machine = { ... }: { imports = [ ./configuration.nix ]; - environment.systemPackages = let - #configFileTree = pkgs.runCommand "${name}-configuration-dot-nix" { } '' - # set -euo pipefail - # install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" - #''; - configFileTree = mkDynixConfigurationDotNix { - inherit (config) name; - configuration = ./configuration.nix; - }; - in [ - configFileTree - ]; + passthru.configurationDotNix = mkDynixConfigurationDotNix { + name = testName; + configuration = ./configuration.nix; + }; }; testScript = builtins.readFile ./test-script.py; diff --git a/tests/mk-test-configuration-dot-nix.nix b/tests/mk-test-configuration-dot-nix.nix index da574ef..6b7dc25 100644 --- a/tests/mk-test-configuration-dot-nix.nix +++ b/tests/mk-test-configuration-dot-nix.nix @@ -44,6 +44,15 @@ echo " ];" >> "$modulesOut/configuration.nix" echo "}" >> "$modulesOut/configuration.nix" + echo "/** GENERATED BY mk-test-configuration-dot-nix! */" >> "$modulesOut/dynamic.nix" + echo "{ lib, ... }:" >> "$modulesOut/dynamic.nix" + echo >> "$modulesOut/dynamic.nix" + echo >> "$modulesOut/dynamic.nix" + echo "{" >> "$modulesOut/dynamic.nix" + echo " imports = [ ./configuration.nix ];" >> "$modulesOut/dynamic.nix" + echo " config = lib.mkMerge [" >> "$modulesOut/dynamic.nix" + echo " ];" >> "$modulesOut/dynamic.nix" + echo "}" >> "$modulesOut/dynamic.nix" runHook postInstall ''; diff --git a/tests/module-allow-rebuild-in-vm.nix b/tests/module-allow-rebuild-in-vm.nix index 3ec14d5..96280fe 100644 --- a/tests/module-allow-rebuild-in-vm.nix +++ b/tests/module-allow-rebuild-in-vm.nix @@ -13,8 +13,8 @@ installBootLoader = true; # With how much memory Nix eval uses, this is essentially required. - memorySize = 4096; - cores = 4; + memorySize = 8192; + cores = 8; };