This commit is contained in:
Qyriad 2026-02-16 18:02:39 +01:00
parent da509d97c7
commit 3765e918d6
18 changed files with 348 additions and 226 deletions

20
Cargo.lock generated
View file

@ -158,6 +158,8 @@ dependencies = [
"fs-err", "fs-err",
"itertools", "itertools",
"libc", "libc",
"regex",
"regex-lite",
"serde", "serde",
"serde_json", "serde_json",
"tap", "tap",
@ -422,6 +424,18 @@ dependencies = [
"bitflags", "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]] [[package]]
name = "regex-automata" name = "regex-automata"
version = "0.4.14" version = "0.4.14"
@ -433,6 +447,12 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-lite"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cab834c73d247e67f4fae452806d17d3c7501756d98c8808d7c9c7aa7d18f973"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.8.9" version = "0.8.9"

View file

@ -11,12 +11,19 @@ path = "src/main.rs"
name = "dynix" name = "dynix"
path = "src/lib.rs" path = "src/lib.rs"
[features]
default = ["regex-full"]
regex-full = ["dep:regex"]
regex-lite = ["dep:regex-lite"]
[dependencies] [dependencies]
clap = { version = "4.5.54", features = ["color", "derive"] } clap = { version = "4.5.54", features = ["color", "derive"] }
command-error = "0.8.0" command-error = "0.8.0"
fs-err = "3.2.2" fs-err = "3.2.2"
itertools = "0.14.0" itertools = "0.14.0"
libc = { version = "0.2.180", features = ["extra_traits"] } 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 = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
tap = "1.0.1" tap = "1.0.1"

View file

@ -11,7 +11,6 @@ let
inherit (import ./lib.nix { inherit lib; }) inherit (import ./lib.nix { inherit lib; })
typeCheck typeCheck
convenientAttrPath
concatFoldl concatFoldl
recUpdateFoldl recUpdateFoldl
recUpdateFoldlAttrs recUpdateFoldlAttrs
@ -64,6 +63,13 @@ in
default = { }; 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 { finalSettings = mkOption {
type = t.attrsOf t.raw; type = t.attrsOf t.raw;
internal = true; internal = true;
@ -78,14 +84,6 @@ in
type = t.functionTo t.raw; type = t.functionTo t.raw;
readOnly = true; readOnly = true;
}; };
doChange = mkOption {
type = t.functionTo t.pathInStore;
readOnly = true;
description = ''
The function to call to Do The Thing.
'';
};
}; };
# Assertions. # Assertions.
@ -94,27 +92,49 @@ in
# #
# Generic implementation. # Generic implementation.
# #
config.system.activationScripts."dynamicism-reset" = {
deps = [ "etc" "stdio" "specialfs" ]; config.system.activationScripts = config.dynamicism.for
text = '' |> lib.filterAttrs (lib.const (lib.getAttr "enable"))
echo "DYNIX: removing existing systemd dropins" |> lib.mapAttrs' (name: submod: let
# FIXME: do for each enabled submodule forUnit = unitName: assert lib.isString unitName; let
if [[ -d /run/systemd/system ]]; then dropinDir = "/run/systemd/system/${unitName}.d";
rm -v /run/systemd/system/*/dynix-*.conf || true 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 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 = { config.dynamicism = {
applyDynamicConfiguration = { applyDynamicConfiguration = {
baseConfiguration ? builtins.getEnv "NIXOS_CONFIG", baseConfiguration ? builtins.getEnv "NIXOS_CONFIG",
newConfiguration ? baseConfiguration + "/dynamic.nix", newConfiguration ? (lib.filesystem.dirOf baseConfiguration) + "/dynamic.nix",
}: let }: let
locFor = appendage: lib.concatLists [ locFor = appendage: lib.concatLists [
opts.applyDynamicConfiguration.loc opts.applyDynamicConfiguration.loc
[ "(function argument)" ] [ "(function argument)" ]
(lib.toList appendage) (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}»"; _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 nixosBefore.config.dynamicism.for.${submodName}.finalSettings
!= !=
nixosAfter.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 = { runForSubmodCalled = name: ''
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: ''
echo "Activating dynamic configuration for ${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"; |> lib.concatStringsSep "\n";
in pkgs.writeShellApplication { in pkgs.writeShellApplication {
name = "dynamicism-activate"; name = "dynamicism-activate";
text = runAllActivationScripts; text = runForChanged;
passthru.configuration = nixosAfter; passthru.configuration = nixosAfter;
}; };

View file

@ -12,7 +12,7 @@ in
text = '' text = ''
[Service] [Service]
ExecStart= 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; }; passthru = { inherit configFile; };
}; };

View file

@ -18,6 +18,19 @@ in lib.fix (self: {
/** Either a list of strings, or a dotted string that will be split. */ /** Either a list of strings, or a dotted string that will be split. */
convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); 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; concatFoldl = f: list: lib.foldl' (acc: value: acc ++ (f value)) [ ] list;
recUpdateFoldl = f: list: lib.foldl' (acc: value: lib.recursiveUpdate 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; recUpdateFoldlAttrs = f: attrs: lib.foldlAttrs (acc: name: value: lib.recursiveUpdate acc (f name value)) { } attrs;

View file

@ -14,32 +14,15 @@ let
mkEnableOption mkEnableOption
literalExpression literalExpression
; ;
inherit (lib.types)
mkOptionType
;
t = lib.types; t = lib.types;
inherit (import ./lib.nix { inherit lib; }) inherit (import ./lib.nix { inherit lib; })
convenientAttrPath
executablePathInStore
recUpdateFoldl recUpdateFoldl
; ;
pkgs = host.pkgs; 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 in
{ {
options = { options = {
@ -74,8 +57,9 @@ in
}; };
configFile = mkOption { configFile = mkOption {
type = t.pathInStore; type = t.nullOr t.pathInStore;
internal = true; internal = true;
default = null;
}; };
unitDropins = mkOption { unitDropins = mkOption {

View file

@ -122,9 +122,7 @@ in {
fenixToolchain, fenixToolchain,
}: let }: let
mkShell' = mkShell.override { inherit stdenv; }; mkShell' = mkShell.override { inherit stdenv; };
pyEnv = python3Packages.python.withPackages (p: [ pyEnv = python3Packages.python.withPackages (p: [ p.beartype ]);
p.beartype
]);
in mkShell' { in mkShell' {
name = "devshell-for-${self.name}"; name = "devshell-for-${self.name}";
inputsFrom = [ self ]; inputsFrom = [ self ];

View file

@ -1,4 +1,7 @@
use std::sync::Arc; use std::{
env,
sync::{Arc, LazyLock},
};
use clap::ColorChoice; use clap::ColorChoice;
@ -52,6 +55,7 @@ impl FromStr for NixOsOption {
pub struct AppendCmd { pub struct AppendCmd {
#[arg(required = true)] #[arg(required = true)]
pub name: Arc<str>, pub name: Arc<str>,
#[arg(required = true)] #[arg(required = true)]
pub value: Arc<str>, pub value: Arc<str>,
} }
@ -67,6 +71,28 @@ pub enum Subcommand {
Delta(DeltaCmd), Delta(DeltaCmd),
} }
static DEFAULT_PATH: LazyLock<Box<OsStr>> = 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)] #[derive(Debug, Clone, PartialEq, clap::Parser)]
#[command(version, about, author)] #[command(version, about, author)]
#[command(arg_required_else_help(true), args_override_self(true))] #[command(arg_required_else_help(true), args_override_self(true))]
@ -75,8 +101,10 @@ pub struct Args {
#[arg(long, global(true), default_value = "auto")] #[arg(long, global(true), default_value = "auto")]
pub color: ColorChoice, pub color: ColorChoice,
// FIXME: default to /etc/configuration.nix, or something? /// The .nix file with dynamic overrides to modify.
#[arg(long, global(true), default_value = "./configuration.nix")] /// [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<OsStr>, pub file: Arc<OsStr>,
#[command(subcommand)] #[command(subcommand)]

View file

@ -1,4 +1,7 @@
use std::{iter, sync::Arc}; use std::{
iter,
sync::{Arc, LazyLock},
};
pub(crate) mod prelude { pub(crate) mod prelude {
#![allow(unused_imports)] #![allow(unused_imports)]
@ -41,14 +44,38 @@ pub mod line;
mod nixcmd; mod nixcmd;
pub use line::Line; pub use line::Line;
pub mod source; 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 serde::{Deserialize, Serialize};
use crate::source::SourceFile;
pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; 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<Regex> = 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+\((?<priority>[\d-]+)\)").unwrap()
});
#[tracing::instrument(level = "debug")] #[tracing::instrument(level = "debug")]
pub fn do_delta(args: Arc<Args>, delta_args: DeltaCmd) -> Result<(), BoxDynError> { pub fn do_delta(args: Arc<Args>, delta_args: DeltaCmd) -> Result<(), BoxDynError> {
todo!(); todo!();
@ -65,17 +92,15 @@ pub fn do_append(args: Arc<Args>, append_args: AppendCmd) -> Result<(), BoxDynEr
filepath.to_path_buf() 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(); let mut opts = File::options();
opts.read(true) opts.read(true)
.write(true) .write(true)
.create(false) .create(false)
.custom_flags(libc::O_CLOEXEC); .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 = pri - 1;
let new_pri_line = get_next_prio_line( let new_pri_line = get_next_prio_line(
@ -85,7 +110,7 @@ pub fn do_append(args: Arc<Args>, append_args: AppendCmd) -> Result<(), BoxDynEr
append_args.value.into(), 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)?; write_next_prio(source_file, new_pri_line)?;
@ -108,35 +133,30 @@ pub fn expr_for_configuration(source_file: &Path) -> OsString {
.collect() .collect()
} }
pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result<Box<Path>, BoxDynError> { fn maybe_extract_prio_from_line(line: &SourceLine) -> Option<i64> {
let expr = expr_for_configuration(configuration_nix); MK_OVERRIDE_RE
let attrpath = format!("options.{}.definitionsWithLocations", option_name); .captures(line.text_ref())
.map(|caps| caps.name("priority").unwrap().as_str())
let output = nixcmd::NixEvalExpr { expr, attrpath } .map(|prio_str| {
.into_command() i64::from_str(prio_str).unwrap_or_else(|e| {
.output_checked_utf8()?; panic!(
let stdout = output.stdout(); "lib.mkOverride called with non-integer {}: {}. Nix source code is wrong!\n{}",
prio_str, e, line,
let definitions: Box<[DefinitionWithLocation]> = serde_json::from_str(&stdout)?; );
let last_location = definitions.into_iter().last().unwrap(); })
})
Ok(Box::from(last_location.file))
} }
pub fn get_highest_prio(option_name: &str, source: SourceFile) -> Result<i64, BoxDynError> { pub fn get_where(dynamic_nix: SourceFile) -> Result<i64, BoxDynError> {
// Get the current highest priority. 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()); Ok(prio)
// 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)
} }
pub fn get_next_prio_line( pub fn get_next_prio_line(
@ -146,15 +166,19 @@ pub fn get_next_prio_line(
new_value: Arc<str>, new_value: Arc<str>,
) -> Result<SourceLine, BoxDynError> { ) -> Result<SourceLine, BoxDynError> {
let source_lines = source.lines()?; let source_lines = source.lines()?;
let last_line = source_lines.last(); let penultimate = source_lines.get(source_lines.len() - 2);
assert_eq!(last_line.map(SourceLine::text).as_deref(), Some("]")); // FIXME: don't rely on whitespace lol
let last_line = last_line.unwrap(); debug_assert_eq!(penultimate.map(SourceLine::text).as_deref(), Some(" ];"));
let penultimate = penultimate.unwrap();
let new_generation = 0 - new_prio;
let new_line = SourceLine { let new_line = SourceLine {
line: last_line.line, line: penultimate.line,
path: source.path(), path: source.path(),
text: Arc::from(format!( 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 { let new_mod_start = SourceLine {
line: new_line.line.prev(), line: new_line.line.prev(),
path: source.path(), path: source.path(),
text: Arc::from(" {"), text: Arc::from(" {"),
}; };
let new_mod_end = SourceLine { let new_mod_end = SourceLine {
line: new_line.line.next(), line: new_line.line.next(),
path: source.path(), path: source.path(),
text: Arc::from(" }"), text: Arc::from(" }"),
}; };
source.insert_lines(&[new_mod_start, new_line, new_mod_end])?; source.insert_lines(&[new_mod_start, new_line, new_mod_end])?;

View file

@ -39,12 +39,17 @@
enable = true; enable = true;
serviceConfig.Type = "oneshot"; serviceConfig.Type = "oneshot";
serviceConfig.RemainAfterExit = true; serviceConfig.RemainAfterExit = true;
serviceConfig.RequisteOf = [ "multi-user.target" ];
path = [ config.system.path ]; path = [ config.system.path ];
wantedBy = [ "multi-user.target" ]; wantedBy = [ "multi-user.target" ];
requiredBy = [ "multi-user.target" ];
after = [ "default.target" ]; after = [ "default.target" ];
script = '' 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 mkdir -vp /etc/nixos
nixos-generate-config nixos-generate-config
@ -58,6 +63,8 @@
passthru = { inherit options; }; passthru = { inherit options; };
environment.systemPackages = [ config.passthru.configurationDotNix ];
# Just making something in this strict in `name`, # Just making something in this strict in `name`,
# which is only present as an argument for nodes and I don't want to # which is only present as an argument for nodes and I don't want to
# confuse that with the test modules. # confuse that with the test modules.

View file

@ -4,7 +4,7 @@ import functools
#from pprint import pformat #from pprint import pformat
import shlex import shlex
import textwrap import textwrap
from typing import cast, TYPE_CHECKING from typing import Any, cast, TYPE_CHECKING
from beartype import beartype from beartype import beartype
@ -68,6 +68,27 @@ def get_cli_args() -> argparse.Namespace:
args, rest = parser.parse_known_args(cmdline_args) args, rest = parser.parse_known_args(cmdline_args)
return 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 <nixpkgs/nixos> { };
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") machine.wait_for_unit("default.target")
assert "lix" in machine.succeed("nix --version").lower() assert "lix" in machine.succeed("nix --version").lower()
machine.log("INIT") machine.log("INIT")
@ -89,17 +110,8 @@ args = get_cli_args()
#assert int(args['max_connection_rate']) == 256, f"{args['max_connection_rate']=} != 256" #assert int(args['max_connection_rate']) == 256, f"{args['max_connection_rate']=} != 256"
# #
new_jobs = 4 new_jobs = 4
expr = textwrap.dedent(f""" dynix_append("services.distccd.maxJobs", new_jobs)
let do_apply()
nixos = import <nixpkgs/nixos> {{ }};
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())
args = get_cli_args() 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' assert args.log_level == 'warning', f'{args.log_level=} != warning'
new_log_level = 'error' new_log_level = 'error'
expr = textwrap.dedent(f""" dynix_append("services.distccd.logLevel", f'"{new_log_level}"')
let do_apply()
nixos = import <nixpkgs/nixos> {{ }};
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())
args = get_cli_args() args = get_cli_args()
#assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}' #assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}'

View file

@ -1,4 +1,7 @@
{ mkDynixConfigurationDotNix, config, ... }: { mkDynixConfigurationDotNix, config, ... }:
let
testName = config.name;
in
{ {
name = "nixos-test-dynamicism-distccd"; name = "nixos-test-dynamicism-distccd";
@ -6,17 +9,13 @@
extraPythonPackages = p: [ p.beartype ]; extraPythonPackages = p: [ p.beartype ];
nodes.machine = { name, pkgs, ... }: { nodes.machine = { ... }: {
imports = [ ./configuration.nix ]; imports = [ ./configuration.nix ];
environment.systemPackages = let passthru.configurationDotNix = mkDynixConfigurationDotNix {
configFileTree = mkDynixConfigurationDotNix { name = testName;
inherit (config) name; configuration = ./configuration.nix;
configuration = ./configuration.nix; };
};
in [
configFileTree
];
}; };
testScript = builtins.readFile ./test-script.py; testScript = builtins.readFile ./test-script.py;

View file

@ -46,37 +46,75 @@ def get_config_file() -> str:
config_file_path = machine.out_dir / config_file.name config_file_path = machine.out_dir / config_file.name
with open(config_file_path, "r") as f: 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 <nixpkgs/nixos> { };
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") machine.wait_for_unit("default.target")
assert "lix" in machine.succeed("nix --version").lower() 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.log("REBUILDING configuration inside VM")
machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback")
machine.wait_for_unit("gotosocial.service")
config_text = get_config_file() config_text = get_config_file()
lines = config_text.splitlines() lines = config_text.splitlines()
application_name = next((line for line in lines if line.startswith("application-name:")), None) try:
assert application_name is not None, f"no 'application-name:' found in config file: {textwrap.indent(config_text, "")}" 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=}" 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 <nixpkgs/nixos> {{ }};
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=}"

View file

@ -1,5 +1,7 @@
{ mkDynixConfigurationDotNix, config, ... }: { mkDynixConfigurationDotNix, config, ... }:
let
testName = config.name;
in
{ {
name = "nixos-test-dynamicism-gotosocial"; name = "nixos-test-dynamicism-gotosocial";
@ -9,21 +11,17 @@
p.beartype p.beartype
]; ];
nodes.machine = { pkgs, ... }: { nodes.machine = { ... }: {
# NOTE: Anything in this `nodes.machine = ` module will not be included # NOTE: Anything in this `nodes.machine = ` module will not be included
# in the VM's NixOS configuration once it does `nixos-rebuild switch`, # in the VM's NixOS configuration once it does `nixos-rebuild switch`,
# except for `./configuration.nix` which will be copied to `/etc/nixos/`. # except for `./configuration.nix` which will be copied to `/etc/nixos/`.
# dynix will also be statefully installed to root's user profile. # dynix will also be statefully installed to root's user profile.
imports = [ ./configuration.nix ]; imports = [ ./configuration.nix ];
environment.systemPackages = let passthru.configurationDotNix = mkDynixConfigurationDotNix {
configFileTree = mkDynixConfigurationDotNix { name = testName;
inherit (config) name; configuration = ./configuration.nix;
configuration = ./configuration.nix; };
};
in [
configFileTree
];
}; };
testScript = builtins.readFile ./test-script.py; testScript = builtins.readFile ./test-script.py;

View file

@ -55,6 +55,27 @@ def get_config_file() -> dict[str, Any]:
config_file_path.unlink() config_file_path.unlink()
return config_data 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 <nixpkgs/nixos> { };
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") machine.wait_for_unit("default.target")
assert "lix" in machine.succeed("nix --version").lower() assert "lix" in machine.succeed("nix --version").lower()
machine.log("INIT") 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" assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256"
new_workers = 20 new_workers = 20
expr = textwrap.dedent(f""" dynix_append("services.harmonia.settings.workers", new_workers)
let do_apply()
nixos = import <nixpkgs/nixos> {{ }};
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())
# Workers, but not max connection rate, should have changed. # Workers, but not max connection rate, should have changed.
config_toml = get_config_file() 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" assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256"
new_max_connection_rate = 100 new_max_connection_rate = 100
expr = textwrap.dedent(f""" dynix_append("services.harmonia.settings.max_connection_rate", new_max_connection_rate)
let do_apply()
nixos = import <nixpkgs/nixos> {{ }};
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())
# Max connection rate should have changed, but workers should have reverted. # Max connection rate should have changed, but workers should have reverted.
config_toml = get_config_file() config_toml = get_config_file()

View file

@ -1,4 +1,7 @@
{ mkDynixConfigurationDotNix, config, ... }: { mkDynixConfigurationDotNix, config, ... }:
let
testName = config.name;
in
{ {
name = "nixos-test-dynamicism-harmonia"; name = "nixos-test-dynamicism-harmonia";
@ -6,21 +9,13 @@
extraPythonPackages = p: [ p.beartype ]; extraPythonPackages = p: [ p.beartype ];
nodes.machine = { name, pkgs, ... }: { nodes.machine = { ... }: {
imports = [ ./configuration.nix ]; imports = [ ./configuration.nix ];
environment.systemPackages = let passthru.configurationDotNix = mkDynixConfigurationDotNix {
#configFileTree = pkgs.runCommand "${name}-configuration-dot-nix" { } '' name = testName;
# set -euo pipefail configuration = ./configuration.nix;
# install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" };
#'';
configFileTree = mkDynixConfigurationDotNix {
inherit (config) name;
configuration = ./configuration.nix;
};
in [
configFileTree
];
}; };
testScript = builtins.readFile ./test-script.py; testScript = builtins.readFile ./test-script.py;

View file

@ -44,6 +44,15 @@
echo " ];" >> "$modulesOut/configuration.nix" echo " ];" >> "$modulesOut/configuration.nix"
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 runHook postInstall
''; '';

View file

@ -13,8 +13,8 @@
installBootLoader = true; installBootLoader = true;
# With how much memory Nix eval uses, this is essentially required. # With how much memory Nix eval uses, this is essentially required.
memorySize = 4096; memorySize = 8192;
cores = 4; cores = 8;
}; };