diff --git a/.editorconfig b/.editorconfig index 061699c..aab3429 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,5 @@ indent_style = space indent_size = 4 [*.nix] -indent_style = space +indent_style = tab indent_size = 2 diff --git a/Cargo.lock b/Cargo.lock index cba4ce7..eef46d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -69,9 +69,9 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.11.0" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" [[package]] name = "cfg-if" @@ -87,9 +87,9 @@ checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" [[package]] name = "clap" -version = "4.5.58" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "63be97961acde393029492ce0be7a1af7e323e6bae9511ebfac33751be5e6806" +checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" dependencies = [ "clap_builder", "clap_derive", @@ -97,9 +97,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.58" +version = "4.5.54" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f13174bda5dfd69d7e947827e5af4b0f2f94a4a3ee92912fba07a66150f21e2" +checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" dependencies = [ "anstream", "anstyle", @@ -109,9 +109,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.5.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" dependencies = [ "heck", "proc-macro2", @@ -121,9 +121,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "1.0.0" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a822ea5bc7590f9d40f1ba12c0dc3c2760f3482c6984db1573ad11031420831" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "colorchoice" @@ -158,8 +158,6 @@ dependencies = [ "fs-err", "itertools", "libc", - "regex", - "regex-lite", "serde", "serde_json", "tap", @@ -192,9 +190,9 @@ dependencies = [ [[package]] name = "fs-err" -version = "3.3.0" +version = "3.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0" +checksum = "baf68cef89750956493a66a10f512b9e58d9db21f2a573c079c0bdf1207a54a7" dependencies = [ "autocfg", ] @@ -273,9 +271,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.182" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6800badb6cb2082ffd7b6a67e6125bb39f18782f793520caee8cb8846be06112" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" @@ -309,9 +307,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.8.0" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "nix" @@ -387,9 +385,9 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "proc-macro2" -version = "1.0.106" +version = "1.0.105" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7" dependencies = [ "unicode-ident", ] @@ -408,9 +406,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.43" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a" dependencies = [ "proc-macro2", ] @@ -424,40 +422,22 @@ 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" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" dependencies = [ "aho-corasick", "memchr", "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" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" [[package]] name = "rustix" @@ -575,9 +555,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.116" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df424c70518695237746f84cede799c9c58fcb37450d7b23716568cc8bc69cb" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -699,9 +679,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.24" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-linebreak" @@ -935,6 +915,6 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" [[package]] name = "zmij" -version = "1.0.21" +version = "1.0.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" +checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" diff --git a/Cargo.toml b/Cargo.toml index 2e6dd55..dd24d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,19 +11,12 @@ 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/configuration.nix b/configuration.nix new file mode 100644 index 0000000..f34f2e5 --- /dev/null +++ b/configuration.nix @@ -0,0 +1,36 @@ +{ pkgs, modulesPath, ... }: + +{ + imports = [ + ./modules/dynamic-overrides.nix + ./modules/dynamicism + "${modulesPath}/profiles/qemu-guest.nix" + ]; + + dynamicism.for.gotosocial.enable = true; + + # Just an example system. + users.mutableUsers = false; + users.users.root = { + password = "root"; + }; + + services.openssh = { + enable = true; + settings = { + PermitRootLogin = "yes"; + }; + }; + + environment.shellAliases = { + ls = "eza --long --header --group --group-directories-first --classify --binary"; + }; + + environment.systemPackages = with pkgs; [ + eza + fd + ripgrep + ]; + + system.stateVersion = "25.11"; +} diff --git a/default.nix b/default.nix index 4372c35..b7a3bef 100644 --- a/default.nix +++ b/default.nix @@ -5,14 +5,71 @@ in import src { inherit pkgs; }, }: let inherit (qpkgs) lib; - dynix = qpkgs.callPackage ./package.nix { } - |> qpkgs.stdlib.mkStdenvPretty; + + dynix = qpkgs.callPackage ./package.nix { }; + byStdenv = lib.mapAttrs (stdenvName: stdenv: let withStdenv = dynix.override { inherit stdenv; }; dynix' = withStdenv.overrideAttrs (prev: { pname = "${prev.pname}-${stdenvName}"; }); in dynix') qpkgs.validStdenvs; -in dynix.overrideAttrs (prev: lib.recursiveUpdate prev { - passthru = { inherit byStdenv; }; + + evalNixos = import (pkgs.path + "/nixos"); + + doChange = { + option, + value, + }: + assert lib.isList option; + assert lib.all lib.isString option; + let + nixosBefore = evalNixos { + configuration = ./configuration.nix; + }; + dynamicBefore = nixosBefore.config.dynamicism.finalSettings; + + nixosAfter = evalNixos { + configuration = { ... }: { + imports = [ + ./configuration.nix + (lib.setAttrByPath option (lib.mkOverride (-999) value)) + ]; + }; + }; + + withActivationScripts = evalNixos { + configuration = ({ ... }: { + imports = [ ./configuration.nix ]; + config.environment.systemPackages = [ nixosAfter.config.dynamicism.for.gotosocial.activate ]; + }); + }; + in { + inherit nixosBefore nixosAfter withActivationScripts; + }; + +in dynix.overrideAttrs (final: prev: let + self = final.finalPackage; +in lib.recursiveUpdate prev { + passthru = { + ts = let + scope = pkgs.callPackage ./modules/tests.nix { }; + in scope.packages scope; + + dync = self.nixos.config.dynamicism; + dyno = self.nixos.options.dynamicism; + gotosocial = self.nixos.options.dynamicism.for.valueMeta.attrs.gotosocial.configuration; + + inherit byStdenv; + nixos = evalNixos { + configuration = ./configuration.nix; + }; + c = self.nixos; + nixos-vm = self.nixos.config.system.build.vm; + doChange = builtins.seq self.nixos.config.dynamicism doChange; + withVox = self.doChange { + option = lib.splitString "." "services.gotosocial.settings.application-name"; + value = "Vox is an asshole"; + }; + }; }) diff --git a/flake.lock b/flake.lock index f07fa2c..9998eff 100644 --- a/flake.lock +++ b/flake.lock @@ -1,21 +1,5 @@ { "nodes": { - "fenix": { - "flake": false, - "locked": { - "lastModified": 1771226183, - "narHash": "sha256-AbaMtaLbe37l2VI/KSRk63PuBnX/YDDFL0G1eFMbvwI=", - "owner": "nix-community", - "repo": "fenix", - "rev": "2e3759c5ef51f320eb0aaf83f2a32baae33db237", - "type": "github" - }, - "original": { - "owner": "nix-community", - "repo": "fenix", - "type": "github" - } - }, "flake-utils": { "inputs": { "systems": "systems" @@ -37,11 +21,11 @@ "nixpkgs": { "flake": false, "locked": { - "lastModified": 1770537093, - "narHash": "sha256-pF1quXG5wsgtyuPOHcLfYg/ft/QMr8NnX0i6tW2187s=", + "lastModified": 1768875095, + "narHash": "sha256-dYP3DjiL7oIiiq3H65tGIXXIT1Waiadmv93JS0sS+8A=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "fef9403a3e4d31b0a23f0bacebbec52c248fbb51", + "rev": "ed142ab1b3a092c4d149245d0c4126a5d7ea00b0", "type": "github" }, "original": { @@ -54,11 +38,11 @@ "qyriad-nur": { "flake": false, "locked": { - "lastModified": 1770385314, - "narHash": "sha256-zDlvon/yF9STxGi3l38j9EgTFHBHOjCJlP8mMX7zw5M=", + "lastModified": 1767357308, + "narHash": "sha256-PWDIBupTHASnzwPUuafIhwBCFKxjsSVj4QRAdX5y/z4=", "owner": "Qyriad", "repo": "nur-packages", - "rev": "23716e0347215a721f9489515a0c3dc91122c7d5", + "rev": "c08309f918a0528ceb23659c0cc4a3c901fe8afa", "type": "github" }, "original": { @@ -69,7 +53,6 @@ }, "root": { "inputs": { - "fenix": "fenix", "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "qyriad-nur": "qyriad-nur" diff --git a/flake.nix b/flake.nix index f4270f0..8e0dfb6 100644 --- a/flake.nix +++ b/flake.nix @@ -5,10 +5,6 @@ flake = false; }; flake-utils.url = "github:numtide/flake-utils"; - fenix = { - url = "github:nix-community/fenix"; - flake = false; - }; qyriad-nur = { url = "github:Qyriad/nur-packages"; flake = false; @@ -19,13 +15,11 @@ self, nixpkgs, flake-utils, - fenix, qyriad-nur, }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; qpkgs = import qyriad-nur { inherit pkgs; }; inherit (qpkgs) lib; - fenixLib = import fenix { inherit pkgs; }; dynix = import ./default.nix { inherit pkgs qpkgs; }; extraVersions = lib.mapAttrs' (stdenvName: value: { @@ -33,7 +27,7 @@ inherit value; }) dynix.byStdenv; - devShell = import ./shell.nix { inherit pkgs qpkgs dynix fenixLib; }; + devShell = import ./shell.nix { inherit pkgs qpkgs dynix; }; extraDevShells = lib.mapAttrs' (stdenvName: value: { name = "${stdenvName}-dynix"; inherit value; diff --git a/modules/dynamic-overrides.nix b/modules/dynamic-overrides.nix new file mode 100644 index 0000000..1952e96 --- /dev/null +++ b/modules/dynamic-overrides.nix @@ -0,0 +1,21 @@ +# Managed by dynix. +{ lib, ... }: + +lib.mkMerge [ + { + services.gotosocial = { + enable = true; + setupPostgresqlDB = true; + settings = { + application-name = "example!"; + host = "yuki.local"; + }; + }; + } + { + services.gotosocial.settings.application-name = lib.mkOverride 99 "removed herobrine"; + } + { + services.gotosocial.settings.application-name = lib.mkOverride 98 "reädded herobrine"; + } +] diff --git a/modules/dynamicism/default.nix b/modules/dynamicism/default.nix index 1fca96c..8e4764c 100644 --- a/modules/dynamicism/default.nix +++ b/modules/dynamicism/default.nix @@ -1,5 +1,138 @@ -{ ... }: +{ pkgs, lib, config, options, ... }: +let + inherit (lib.options) + mkOption + showOption + ; + t = lib.types; + inherit (import ./lib.nix { inherit lib; }) + typeCheck + convenientAttrPath + concatFoldl + recUpdateFoldl + recUpdateFoldlAttrs + ; + + evalNixos = import (pkgs.path + "/nixos"); + + opts = options.dynamicism; + + subOpts = lib.mapAttrs (_: metaAttr: metaAttr.configuration.options) options.dynamicism.for.valueMeta.attrs; + settingsFormat = pkgs.formats.yaml { }; + + finalSettingsFor = { ... }@submod: recUpdateFoldl (optPath: + lib.setAttrByPath optPath (lib.getAttrFromPath optPath config) + ) submod.source-options; + + ourAssertions = lib.concatAttrValues { + unitsExist = subOpts + |> lib.attrValues + |> concatFoldl (submod: submod.systemd-services-updated.value + |> lib.map (unit: { + assertion = config.systemd.units.${unit}.enable or false; + message = '' + ${showOption submod.systemd-services-updated.loc}' specified non-existent unit '${unit}' + ''; + }) + |> lib.optionals submod.enable.value + ); + + optsExist = concatFoldl (submod: lib.optionals submod.enable.value (lib.map (optPath: { + assertion = lib.hasAttrByPath optPath options; + message = "'${showOption submod.source-options.loc}' specified non-existent option '${showOption optPath}'"; + }) submod.source-options.value)) (lib.attrValues subOpts); + }; +in { - imports = [ ./dynamicism.nix ]; + # + # Interface. + # + options.dynamicism = { + for = mkOption { + type = t.attrsOf (t.submoduleWith { + modules = [ ./submodule.nix ]; + shorthandOnlyDefinesConfig = false; + specialArgs = { inherit pkgs; }; + }); + default = { }; + }; + + finalSettings = mkOption { + type = t.attrsOf t.raw; + internal = true; + readOnly = true; + description = '' + Attrset of each `source-options` tree to their actual values. + ''; + }; + + doChange = mkOption { + type = t.functionTo t.pathInStore; + readOnly = true; + description = '' + The function to call to Do The Thing. + ''; + }; + }; + + # Assertions. + config.assertions = ourAssertions; + + # + # Generic implementation. + # + config.dynamicism.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 + ]; + }; + }; + + allActivations = lib.mapAttrsToList (name: submod: submod.activate) config.dynamicism.for; + allActivationScripts = pkgs.writeShellApplication { + name = "dynamicism-activate"; + runtimeInputs = allActivations; + text = nixosAfter.config.dynamicism.for + |> lib.mapAttrsToList (name: submod: '' + echo "Activating dynamicism for ${name}" + ${lib.getExe submod.activate} + '') + |> lib.concatStringsSep "\n"; + }; + in allActivationScripts; + + config.dynamicism.finalSettings = lib.asserts.checkAssertWarn ourAssertions [ ] ( + recUpdateFoldlAttrs (name: { ... }@submod: finalSettingsFor submod) config.dynamicism.for + ); + + # Implementations. + config.dynamicism.for.gotosocial = let + cfg = config.dynamicism.for.gotosocial; + in { + source-options = [ + "services.gotosocial.settings" + ]; + + configFile = settingsFormat.generate "gotosocial-override.yml" config.services.gotosocial.settings; + + unitDropins."gotosocial.service" = pkgs.writeText "gotosocial-override.conf" '' + [Service] + ExecStart= + ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${cfg.configFile} start + ''; + }; } diff --git a/modules/dynamicism/distccd.nix b/modules/dynamicism/distccd.nix deleted file mode 100644 index 3b87764..0000000 --- a/modules/dynamicism/distccd.nix +++ /dev/null @@ -1,50 +0,0 @@ -{ pkgs, lib, config, ... }: -let - cfg = config.services.distccd; - - cliArgs = lib.cli.toCommandLineShellGNU { explicitBool = false; } { - no-detach = true; - daemon = true; - enable-tcp-insecure = true; - port = cfg.port; - - # Nulls are handled automatically. - job-lifetime = cfg.jobTimeout; - log-level = cfg.logLevel; - jobs = cfg.maxJobs; - nice = cfg.nice; - stats = cfg.stats.enable; - stats-port = if cfg.stats.enable then cfg.stats.port else null; - zeroconf = cfg.zeroconf; - allow = cfg.allowedClients; - }; - - startDistccd = pkgs.writeShellApplication { - name = "start-distccd"; - runtimeInputs = [ pkgs.distccMasquerade ]; - - text = '' - ${lib.getExe' cfg.package "distccd"} \ - ${cliArgs} - ''; - }; -in -{ - dynamicism.for.distccd = { - source-options = [ - "services.distccd.jobTimeout" - "services.distccd.logLevel" - "services.distccd.maxJobs" - "services.distccd.nice" - ]; - unitDropins."distccd.service" = pkgs.writeTextFile { - name = "distccd-override.conf"; - text = '' - [Service] - ExecStart= - ExecStart=${lib.getExe startDistccd} - ''; - passthru.startScript = startDistccd; - }; - }; -} diff --git a/modules/dynamicism/dynamicism.nix b/modules/dynamicism/dynamicism.nix deleted file mode 100644 index 6f79993..0000000 --- a/modules/dynamicism/dynamicism.nix +++ /dev/null @@ -1,213 +0,0 @@ -{ pkgs, lib, config, options, ... }: -let - inherit (lib.options) - mkOption - showOption - ; - inherit (lib.asserts) - checkAssertWarn - ; - t = lib.types; - - inherit (import ./lib.nix { inherit lib; }) - typeCheck - convenientAttrPath - executablePathInStore - concatFoldl - recUpdateFoldl - recUpdateFoldlAttrs - ; - - evalNixos = import (pkgs.path + "/nixos"); - - opts = options.dynamicism; - - subOpts = lib.mapAttrs (_: metaAttr: metaAttr.configuration.options) options.dynamicism.for.valueMeta.attrs; - - seqTrue = v: lib.seq v true; - - finalSettingsFor = { ... }@submod: recUpdateFoldl (optPath: - lib.setAttrByPath optPath (lib.getAttrFromPath optPath config) - ) submod.source-options; - - ourAssertions = lib.concatAttrValues { - unitsExist = subOpts - |> lib.attrValues - |> concatFoldl (submod: submod.systemd-services-updated.value - |> lib.map (unit: { - assertion = config.systemd.units.${unit}.enable or false; - message = '' - ${showOption submod.systemd-services-updated.loc}' specified non-existent unit '${unit}' - ''; - }) - |> lib.optionals submod.enable.value - ); - - optsExist = concatFoldl (submod: lib.optionals submod.enable.value (lib.map (optPath: { - assertion = lib.hasAttrByPath optPath options; - message = "'${showOption submod.source-options.loc}' specified non-existent option '${showOption optPath}'"; - }) submod.source-options.value)) (lib.attrValues subOpts); - }; -in -{ - # - # Interface. - # - options.dynamicism = { - for = mkOption { - type = t.attrsOf (t.submoduleWith { - modules = [ ./submodule.nix ]; - shorthandOnlyDefinesConfig = false; - specialArgs = { - host = { inherit pkgs options config; }; - }; - }); - 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; - readOnly = true; - description = '' - Attrset of each `source-options` tree to their actual values. - ''; - }; - - finalActivationScript = mkOption { - type = executablePathInStore; - internal = true; - }; - - applyDynamicConfiguration = mkOption { - #type = t.functionTo t.pathInStore; - type = t.functionTo t.raw; - readOnly = true; - }; - }; - - # Assertions. - config.assertions = ourAssertions; - - # - # Generic implementation. - # - - 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}" - 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 = '' - set -x - echo "Removing existing dynamic overrides for ${name}" - echo "Removing existing dynamic overrides for ${name}" >&2 - - ${submod.systemd-services-updated |> lib.map forUnit |> lib.concatStringsSep "\n"} - - set +x - ''; - }); - - config.dynamicism = { - - applyDynamicConfiguration = { - baseConfiguration ? builtins.getEnv "NIXOS_CONFIG", - 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}»"; - - nixosBefore = evalNixos { - configuration = { ... }: { - inherit _file; - imports = [ baseConfiguration ]; - }; - }; - - nixosAfter = evalNixos { - configuration = { ... }: { - inherit _file; - imports = [ baseConfiguration newConfiguration ]; - }; - }; - - submodulesChanged = nixosAfter.config.dynamicism.for - |> lib.filterAttrs (lib.const (lib.getAttr "enable")) - |> lib.filterAttrs (submodName: _: - nixosBefore.config.dynamicism.for.${submodName}.finalSettings - != - nixosAfter.config.dynamicism.for.${submodName}.finalSettings - ); - - runForSubmodCalled = name: '' - echo "Activating dynamic configuration for ${name}" - ${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 = runForChanged; - passthru.configuration = nixosAfter; - }; - - finalActivationScript = pkgs.writeShellApplication { - name = "dynamicism-activate-all"; - text = config.dynamicism.for - |> lib.filterAttrs (lib.const (lib.getAttr "enable")) - |> lib.mapAttrsToList (name: submod: '' - echo "Activating dynamic configuration for ${name}" - ${lib.getExe submod.activate} - '') - |> lib.concatStringsSep "\n"; - }; - - finalSettings = config.dynamicism.for - |> recUpdateFoldlAttrs (name: { ... }@submod: finalSettingsFor submod) - |> checkAssertWarn ourAssertions [ ]; - }; - - # Implementations. - imports = [ - ./gotosocial.nix - ./harmonia.nix - ./distccd.nix - ]; -} diff --git a/modules/dynamicism/gotosocial.nix b/modules/dynamicism/gotosocial.nix deleted file mode 100644 index b1fedc6..0000000 --- a/modules/dynamicism/gotosocial.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ pkgs, lib, config, ... }: -let - settingsFormat = pkgs.formats.yaml { }; - configFile = settingsFormat.generate "gotosocial-override.yml" config.services.gotosocial.settings; -in -{ - dynamicism.for.gotosocial = { - source-options = [ "services.gotosocial.settings" ]; - - unitDropins."gotosocial.service" = pkgs.writeTextFile { - name = "gotosocial-override.conf"; - text = '' - [Service] - ExecStart= - ExecStart=${lib.getExe' pkgs.gotosocial "gotosocial"} --config-path ${configFile} server start - ''; - passthru = { inherit configFile; }; - }; - }; -} diff --git a/modules/dynamicism/harmonia.nix b/modules/dynamicism/harmonia.nix deleted file mode 100644 index e4284d0..0000000 --- a/modules/dynamicism/harmonia.nix +++ /dev/null @@ -1,26 +0,0 @@ -{ pkgs, lib, config, ... }: -let - cfg = config.dynamicism.for.harmonia; - settingsFormat = pkgs.formats.toml { }; - - # FIXME: referring to config.dynamicism.finalSettings.* in here - # makes lib.checkAssertWarn in the generic module cause infinite recursion. - finalSettings = config.services.harmonia.settings; - - configFile = settingsFormat.generate "harmonia-override.toml" finalSettings; -in -{ - dynamicism.for.harmonia = { - source-options = [ "services.harmonia.settings" ]; - - unitDropins."harmonia.service" = pkgs.writeTextFile { - name = "harmonia-override.conf"; - text = '' - [Service] - Environment=CONFIG_FILE=${configFile} - ''; - - passthru = { inherit configFile; }; - }; - }; -} diff --git a/modules/dynamicism/lib.nix b/modules/dynamicism/lib.nix index ec41ab7..a988ae4 100644 --- a/modules/dynamicism/lib.nix +++ b/modules/dynamicism/lib.nix @@ -4,12 +4,12 @@ t = lib.types; in lib.fix (self: { /** Perform module-system type checking and resolving on a single option value. */ - typeCheck = loc: optionType: value: - assert lib.isOptionType optionType; + typeCheck = loc: option: value: + assert lib.isOptionType option; assert lib.isList loc; assert lib.all lib.isString loc; let - merged = lib.modules.mergeDefinitions loc optionType [ { + merged = lib.modules.mergeDefinitions loc option [ { inherit value; file = "«inline»"; } ]; @@ -18,19 +18,6 @@ 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 4100217..1b9dac9 100644 --- a/modules/dynamicism/submodule.nix +++ b/modules/dynamicism/submodule.nix @@ -1,8 +1,8 @@ { name, + pkgs, lib, config, - host, ... }: let @@ -14,15 +14,26 @@ let mkEnableOption literalExpression ; + inherit (lib.types) + mkOptionType + ; t = lib.types; - inherit (import ./lib.nix { inherit lib; }) - convenientAttrPath - executablePathInStore - recUpdateFoldl - ; + /** Either a list of strings, or a dotted string that will be split. */ + convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); - pkgs = host.pkgs; + 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 = { @@ -48,22 +59,13 @@ in default = lib.attrNames config.unitDropins; }; - finalSettings = mkOption { - type = t.attrsOf t.raw; - internal = true; - default = recUpdateFoldl (optPath: - lib.setAttrByPath optPath (lib.getAttrFromPath optPath host.config) - ) config.source-options; - }; - configFile = mkOption { - type = t.nullOr t.pathInStore; + type = t.pathInStore; internal = true; - default = null; }; unitDropins = mkOption { - type = t.attrsOf t.package; + type = t.attrsOf t.pathInStore; internal = true; }; @@ -80,19 +82,17 @@ in text = let doEdits = config.unitDropins |> lib.mapAttrsToList (service: dropin: '' - cat "${dropin}" | systemctl edit "${service}" --runtime --drop=dynix-${dropin.name} --stdin - '') - |> lib.concatStringsSep "\n"; + cat "${dropin}" | systemctl edit "${service}" --runtime --stdin + ''); doReloads = config.unitDropins - |> lib.mapAttrsToList (service: _: '' - systemctl reload-or-restart "${service}" - '') - |> lib.concatStringsSep "\n"; - in '' - ${doEdits} - - ${doReloads} - ''; + |> lib.mapAttrsToList (service: _: '' + systemctl reload-or-restart "${service}" + ''); + in [ + doEdits + doReloads + ] |> lib.concatLists + |> lib.concatStringsSep "\n"; }; }; } diff --git a/modules/dynamicism/tzupdate.nix b/modules/dynamicism/tzupdate.nix deleted file mode 100644 index 73f634d..0000000 --- a/modules/dynamicism/tzupdate.nix +++ /dev/null @@ -1,20 +0,0 @@ -{ pkgs, lib, config, ... }: let - cfg = config.dynamicism.for.tzupdate; - - - # FIXME: referring to config.dynamicism.finalSettings.* in here - # makes lib.checkAssertWarn in the generic module cause infinite recursion. - #finalSettings = config.dynamicism.finalSettings.tzupdate; - finalSettings = config.services.tzupdate.timer; -in -{ - dynamicism.for.tzupdate = { - source-options = [ "services.tzupdate.timer" ]; - - unitDropins."tzupdate.timer" = pkgs.writeText "tzupdate-timer-override.conf" '' - [Timer] - OnCalendar= - OnCalendar=${finalSettings.interval} - ''; - }; -} diff --git a/modules/tests-common.nix b/modules/tests-common.nix new file mode 100644 index 0000000..322771e --- /dev/null +++ b/modules/tests-common.nix @@ -0,0 +1,4 @@ +{ pkgs, ... }: +{ + nix.package = pkgs.lixPackageSets.latest.lix; +} diff --git a/modules/tests-main.py b/modules/tests-main.py new file mode 100644 index 0000000..dbd33a6 --- /dev/null +++ b/modules/tests-main.py @@ -0,0 +1,76 @@ +#import re +from pathlib import Path +from typing import cast, TYPE_CHECKING + +from test_driver.machine import Machine +from test_driver.errors import RequestedAssertionFailed + +DEFAULT_NIX = "@DEFAULT_NIX@" +CONFIGURATION_NIX = "@CONFIGURATION_NIX@" +DYNAMICISM = "@DYNAMICISM@" + +if TYPE_CHECKING: + global machine + machine = cast(Machine, ...) + +def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: + output = "" + for command in commands: + with machine.nested(f"must succeed: {command}"): + (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) + if status != 0: + machine.log(f"output: {out}") + raise RequestedAssertionFailed( + f"command `{command}` failed (exit code {status})", + ) + output += out + + return output + +def get_config_file() -> Path: + machine.wait_for_unit("gotosocial.service") + gotosocial_pid = int(machine.get_unit_property("gotosocial.service", "MainPID")) + print(f"{gotosocial_pid=}") + + cmdline = machine.succeed(f"cat /proc/{gotosocial_pid}/cmdline") + cmdline_args = cmdline.split("\0") + + config_file_idx = cmdline_args.index("--config-path") + 1 + config_file = Path(cmdline_args[config_file_idx]) + + machine.log(f"copying from VM: {config_file=}") + machine.copy_from_vm(config_file.as_posix()) + + return machine.out_dir / config_file.name + + +machine.wait_for_unit("default.target") +assert "lix" in run_log(machine, "nix --version").lower() + +print(f"{CONFIGURATION_NIX=}") +machine.succeed("mkdir -vp /etc/nixos") +machine.copy_from_host(CONFIGURATION_NIX, "/etc/nixos") +machine.copy_from_host(DYNAMICISM, "/etc/nixos") + +run_log(machine, f"nix build --log-format multiline-with-logs --impure -E 'import {{ configuration = {CONFIGURATION_NIX}; }}'") +run_log(machine, f"nixos-rebuild switch --file {CONFIGURATION_NIX} --verbose --print-build-logs") + +config_file_local = get_config_file() +machine.log(f"opening copied file: {config_file_local=}") +with open(config_file_local, "r") as f: + text = f.read() + lines = text.splitlines() + application_name = next(line for line in lines if line.startswith("application-name:")) + assert "gotosocial-for-machine" in application_name, f"'gotosocial-for-machine' should be in {application_name=}" + +print(f"{DEFAULT_NIX=}") + +run_log(machine, "eza -lah --color=always --group-directories-first --tree /etc/") + +#exec_start = machine.succeed("systemctl show gotosocial.service --property=ExecStart --value") +#exec_start = machine.succeed("systemctl show gotosocial.service --property=ExecStart --value") + +#service_text = machine.succeed("systemctl show gotosocial.service") +#service_props = dict(line.split("=", maxsplit=1) for line in service_text.splitlines()) +#exec_start = service_props['ExecStart'] +#print(f"{exec_start=}") diff --git a/modules/tests.nix b/modules/tests.nix new file mode 100644 index 0000000..917c7bd --- /dev/null +++ b/modules/tests.nix @@ -0,0 +1,52 @@ +{ + pkgs, + lib, + testers, +}: + let + inherit (testers) runNixOSTest; +in lib.makeScope lib.callPackageWith (self: { + main = runNixOSTest { + name = "nixos-test-dynamicism-main"; + + defaults = { pkgs, ... }: { + imports = [ ./dynamicism ]; + + nix = { + package = pkgs.lixPackageSets.latest.lix; + settings.experimental-features = [ "nix-command" ]; + nixPath = [ "nixpkgs=${pkgs.path}" ]; + }; + + environment.shellAliases = { + ls = "eza --long --header --group --group-directories-first --classify --binary"; + }; + environment.systemPackages = with pkgs; [ + eza + fd + ripgrep + ]; + }; + + nodes.machine = { name, ... }: { + #services.gotosocial = { + # enable = true; + # setupPostgresqlDB = true; + # settings = { + # application-name = "gotosocial-for-${name}"; + # host = "${name}.local"; + # }; + #}; + # + #dynamicism.for.gotosocial.enable = true; + }; + + # What's a little IFD between friends? + testScript = pkgs.replaceVars ./tests-main.py { + DEFAULT_NIX = ../default.nix; + CONFIGURATION_NIX = ./tests-configuration.nix; + DYNAMICISM = ./dynamicism; + } + |> builtins.readFile; + }; +}) diff --git a/package.nix b/package.nix index 448ab31..2cb8cee 100644 --- a/package.nix +++ b/package.nix @@ -1,8 +1,6 @@ { lib, - clangStdenv, - callPackage, - linkFarm, + stdenv, rustHooks, rustPackages, versionCheckHook, @@ -10,151 +8,79 @@ rustPlatform, cargo, }: let - stdenv = clangStdenv; cargoToml = lib.importTOML ./Cargo.toml; cargoPackage = cargoToml.package; -in stdenv.mkDerivation (finalAttrs: let - self = finalAttrs.finalPackage; -in { + +in stdenv.mkDerivation (self: { pname = cargoPackage.name; version = cargoPackage.version; strictDeps = true; __structuredAttrs = true; - outputs = [ "out" "modules" ]; - doCheck = true; doInstallCheck = true; - phases = [ "unpackPhase" "patchPhase" "installPhase" ]; - - src = linkFarm "dynix-source" { - inherit (self) dynixCommand dynixModules; + src = lib.fileset.toSource { + root = ./.; + fileset = lib.fileset.unions [ + ./Cargo.toml + ./Cargo.lock + ./src + ]; }; - installPhase = lib.dedent '' - runHook preInstall + cargoDeps = rustPlatform.importCargoLock { + lockFile = ./Cargo.lock; + }; - mkdir -p "$out" - cp -r --reflink=auto "$dynixCommand/"* "$out/" - mkdir -p "$modules" - cp -r --reflink=auto "$dynixModules/"* "$modules/" + versionCheckProgramArg = "--version"; - runHook postInstall - ''; + nativeBuildInputs = rustHooks.asList ++ [ + cargo + ]; - # - # SUB-DERIVATONS - # - - dynixCommand = stdenv.mkDerivation (finalAttrs: let - commandSelf = finalAttrs.finalPackage; - in { - pname = "${self.pname}-command"; - inherit (self) version; - inherit (self) strictDeps __structuredAttrs; - inherit (self) doCheck doInstallCheck; - - src = lib.fileset.toSource { - root = ./.; - fileset = lib.fileset.unions [ - ./Cargo.toml - ./Cargo.lock - ./src - ]; - }; - - cargoDeps = rustPlatform.importCargoLock { - lockFile = ./Cargo.lock; - }; - - nativeBuildInputs = rustHooks.asList ++ [ - cargo - ]; - - nativeInstallCheckInputs = [ - versionCheckHook - ]; - - meta = { - mainProgram = "dynix"; - }; - }); - - dynixModules = stdenv.mkDerivation (finalAttrs: let - modulesSelf = finalAttrs.finalPackage; - in { - pname = "${self.pname}-modules"; - inherit (self) version; - inherit (self) strictDeps __structuredAttrs; - inherit (self) doCheck doInstallCheck; - - src = lib.fileset.toSource { - root = ./modules/dynamicism; - fileset = lib.fileset.unions [ - ./modules/dynamicism - ]; - }; - - phases = [ "unpackPhase" "patchPhase" "installPhase" ]; - - modulesOut = "${placeholder "out"}/share/nixos/modules/dynix"; - - installPhase = lib.dedent '' - runHook preInstall - - mkdir -p "$modulesOut" - cp -r "$src/"* "$modulesOut/" - - runHook postInstall - ''; - }); - - # - # ---------------------------------------------------------------------------- - # + nativeInstallCheckInputs = [ + versionCheckHook + ]; passthru.mkDevShell = { - path, mkShell, - python3Packages, - fenixToolchain, }: let - mkShell' = mkShell.override { inherit stdenv; }; - pyEnv = python3Packages.python.withPackages (p: [ p.beartype ]); + mkShell' = mkShell.override { stdenv = stdenv; }; in mkShell' { - name = "devshell-for-${self.name}"; - inputsFrom = [ self ]; + name = "${self.pname}-devshell-${self.version}"; + inputsFrom = [ self.finalPackage ]; packages = [ - pyEnv - stdenv.cc - fenixToolchain + rustPackages.rustc + rustPackages.rustfmt ]; - env.PYTHONPATH = [ - "${pyEnv}/${pyEnv.sitePackages}" - # Cursed. - "${path}/nixos/lib/test-driver/src" - ] |> lib.concatStringsSep ":"; }; - passthru.modulesPath = self.modules + "/share/nixos/modules"; - passthru.dynix = self.modulesPath + "/dynix"; + passthru.tests.clippy = self.finalPackage.overrideAttrs (prev: { + pname = "${self.pname}-clippy"; - passthru.tests = lib.fix (callPackage ./tests { - dynix = self; - }).packages; + nativeCheckInputs = prev.nativeCheckInputs or [ ] ++ [ + rustPackages.clippy + ]; - passthru.allTests = linkFarm "dynix-all-tests" self.tests; + dontConfigure = true; + dontBuild = true; + doCheck = true; + dontFixup = true; + dontInstallCheck = true; + + checkPhase = lib.trim '' + echo "cargoClippyPhase()" + cargo clippy --all-targets --profile "$cargoCheckType" -- --deny warnings + ''; + + installPhase = lib.trim '' + touch "$out" + ''; + }); meta = { - longDescription = lib.dedent '' - Default output contains the Rust binary. - The `modules` output contains the modules prefixed under `/share/nixos/modules/dynix`. - The `dynix` passthru attr is a shortcut for the modules output *with* the modules prefix, - and thus `dynix.dynix` can be passed directly to `imports = [`. - ''; mainProgram = "dynix"; - outputsToInstall = [ "out" "modules" ]; }; })) diff --git a/shell.nix b/shell.nix index e2af1b4..c73c766 100644 --- a/shell.nix +++ b/shell.nix @@ -1,26 +1,23 @@ { - pkgs ? import { - config = { - checkMeta = true; - allowAliases = false; - }; - }, + pkgs ? import { }, qpkgs ? let src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; in import src { inherit pkgs; }, dynix ? import ./default.nix { inherit pkgs qpkgs; }, - fenixLib ? let - src = fetchTarball "https://github.com/nix-community/fenix/archive/main.tar.gz"; - in import src { inherit pkgs; }, - fenixToolchain ? fenixLib.latest.toolchain, }: let inherit (pkgs) lib; - mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { inherit fenixToolchain; }; + mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { }; devShell = mkDevShell dynix; byStdenv = lib.mapAttrs (lib.const mkDevShell) dynix.byStdenv; -in devShell.overrideAttrs (prev: { +in devShell.overrideAttrs (prev: lib.recursiveUpdate prev { passthru = { inherit byStdenv; }; + env.PYTHONPATH = [ + "${pkgs.python3Packages.beartype}/${pkgs.python3.sitePackages}" + ] |> lib.concatStringsSep ":"; + packages = prev.packages or [ ] ++ [ + pkgs.python3Packages.beartype + ]; }) diff --git a/src/args.rs b/src/args.rs index 0028e0f..cc2fa07 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,7 +1,4 @@ -use std::{ - env, - sync::{Arc, LazyLock}, -}; +use std::sync::Arc; use clap::ColorChoice; @@ -55,7 +52,6 @@ impl FromStr for NixOsOption { pub struct AppendCmd { #[arg(required = true)] pub name: Arc, - #[arg(required = true)] pub value: Arc, } @@ -71,28 +67,6 @@ 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))] @@ -101,10 +75,8 @@ pub struct Args { #[arg(long, global(true), default_value = "auto")] pub color: ColorChoice, - /// 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))] + // FIXME: default to /etc/configuration.nix, or something? + #[arg(long, global(true), default_value = "./configuration.nix")] pub file: Arc, #[command(subcommand)] diff --git a/src/lib.rs b/src/lib.rs index cf7839a..1956803 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,4 @@ -use std::{ - iter, - sync::{Arc, LazyLock}, -}; +use std::{iter, sync::Arc}; pub(crate) mod prelude { #![allow(unused_imports)] @@ -44,37 +41,13 @@ pub mod line; mod nixcmd; pub use line::Line; pub mod source; -pub use source::{SourceFile, SourceLine}; +pub use source::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}; -pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; +use crate::source::SourceFile; -/// 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() -}); +pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; #[tracing::instrument(level = "debug")] pub fn do_delta(args: Arc, delta_args: DeltaCmd) -> Result<(), BoxDynError> { @@ -92,15 +65,17 @@ 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 source_file = SourceFile::open_from(Arc::from(filepath), opts)?; - let pri = get_where(source_file.clone())?; - + let pri = get_highest_prio(&append_args.name, source_file.clone())?; let new_pri = pri - 1; let new_pri_line = get_next_prio_line( @@ -110,7 +85,7 @@ pub fn do_append(args: Arc, append_args: AppendCmd) -> Result<(), BoxDynEr append_args.value.into(), )?; - debug!("new_pri_line={new_pri_line}"); + eprintln!("new_pri_line={new_pri_line}"); write_next_prio(source_file, new_pri_line)?; @@ -133,30 +108,35 @@ pub fn expr_for_configuration(source_file: &Path) -> OsString { .collect() } -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_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)) } -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); +pub fn get_highest_prio(option_name: &str, source: SourceFile) -> Result { + // Get the current highest priority. - Ok(prio) + 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) } pub fn get_next_prio_line( @@ -166,19 +146,15 @@ pub fn get_next_prio_line( new_value: Arc, ) -> Result { let source_lines = source.lines()?; - 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 last_line = source_lines.last(); + assert_eq!(last_line.map(SourceLine::text).as_deref(), Some("]")); + let last_line = last_line.unwrap(); let new_line = SourceLine { - line: penultimate.line, + line: last_line.line, path: source.path(), text: Arc::from(format!( - " {} = lib.mkOverride ({}) ({}); # DYNIX GENERATION {}", - option_name, new_prio, new_value, new_generation, + " {option_name} = lib.mkOverride ({new_prio}) ({new_value});", )), }; @@ -189,12 +165,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/basic/configuration-package.nix b/tests/basic/configuration-package.nix new file mode 100644 index 0000000..4b50af6 --- /dev/null +++ b/tests/basic/configuration-package.nix @@ -0,0 +1,9 @@ +{ + runCommand, +}: runCommand "tests-basic-configuration-dot-nix" { +} '' + set -euo pipefail + mkdir -vp "$out/share/nixos" + cp -rv ${./configuration.nix} "$out/share/nixos/configuration.nix" + cp -rv ${../../modules/dynamicism} "$out/share/nixos/dynamicism" +'' diff --git a/tests/tzupdate/configuration.nix b/tests/basic/configuration.nix similarity index 53% rename from tests/tzupdate/configuration.nix rename to tests/basic/configuration.nix index 176975c..06470e3 100644 --- a/tests/tzupdate/configuration.nix +++ b/tests/basic/configuration.nix @@ -1,30 +1,28 @@ { pkgs, lib, config, modulesPath, ... }: let name = config.networking.hostName; - moduleList = import "${modulesPath}/module-list.nix"; + nixosLibPath = (modulesPath + "/../lib"); + moduleList = import (modulesPath + "/module-list.nix"); - dynixFromSearchPath = let - res = builtins.tryEval ; - in lib.optional res.success res.value; + optionalPath = p: lib.optional (builtins.pathExists p) p; in +assert builtins.pathExists nixosLibPath; +builtins.seq lib +builtins.seq modulesPath +builtins.seq moduleList { - imports = [ - "${modulesPath}/testing/test-instrumentation.nix" - ./hardware-configuration.nix + imports = moduleList ++ [ + (modulesPath + "/testing/test-instrumentation.nix") ] ++ lib.concatLists [ - moduleList - dynixFromSearchPath + (optionalPath ./hardware-configuration.nix) + (optionalPath ./dynamicism) + (optionalPath ../../modules/dynamicism) ]; - dynamicism.for.tzupdate.enable = true; - services.tzupdate = { - enable = true; - }; - system.switch.enable = true; documentation.enable = false; - networking.hostName = "tzupdate-machine"; + networking.hostName = "machine"; boot.loader.grub = { enable = true; @@ -34,23 +32,28 @@ in nix = { package = pkgs.lixPackageSets.latest.lix; - nixPath = [ - "nixpkgs=${pkgs.path}" - "/nix/var/nix/profiles/per-user/root/profile/share/nixos/modules" - ]; + nixPath = [ "nixpkgs=${pkgs.path}" ]; settings = { experimental-features = [ "nix-command" "pipe-operator" ]; substituters = lib.mkForce [ ]; hashed-mirrors = null; connect-timeout = 1; - # For my debugging purposes. - show-trace = true; }; }; + services.gotosocial = { + enable = true; + setupPostgresqlDB = true; + settings = { + application-name = "gotosocial-for-${name}"; + host = "${name}.local"; + }; + }; + + dynamicism.for.gotosocial.enable = true; + environment.pathsToLink = [ "/share" ]; - environment.extraOutputsToInstall = [ "modules" ]; environment.variables = { "NIXOS_CONFIG" = "/etc/nixos/configuration.nix"; }; @@ -58,7 +61,6 @@ in environment.shellAliases = { ls = "eza --long --header --group --group-directories-first --classify --binary"; }; - environment.systemPackages = with pkgs; [ eza fd diff --git a/tests/gotosocial/default.nix b/tests/basic/default.nix similarity index 100% rename from tests/gotosocial/default.nix rename to tests/basic/default.nix diff --git a/tests/basic/test-script.py b/tests/basic/test-script.py new file mode 100644 index 0000000..f507503 --- /dev/null +++ b/tests/basic/test-script.py @@ -0,0 +1,84 @@ +from pathlib import Path +import shlex +import textwrap +from typing import cast, TYPE_CHECKING + +from beartype import beartype + +from test_driver.machine import Machine +from test_driver.errors import RequestedAssertionFailed + +if TYPE_CHECKING: + global machine + machine = cast(Machine, ...) + assert machine.shell is not None + +ls = "eza -lah --color=always --group-directories-first" + +@beartype +def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: + output = "" + for command in commands: + with machine.nested(f"must succeed: {command}"): + (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) + if status != 0: + machine.log(f"output: {out}") + raise RequestedAssertionFailed( + f"command `{command}` failed (exit code {status})", + ) + output += out + + return output + +@beartype +def get_config_file() -> str: + machine.wait_for_unit("gotosocial.service") + gotosocial_pid = int(machine.get_unit_property("gotosocial.service", "MainPID")) + print(f"{gotosocial_pid=}") + + cmdline = machine.succeed(f"cat /proc/{gotosocial_pid}/cmdline") + cmdline_args = cmdline.split("\0") + + config_file_idx = cmdline_args.index("--config-path") + 1 + config_file = Path(cmdline_args[config_file_idx]) + + machine.log(f"copying from VM: {config_file=}") + machine.copy_from_vm(config_file.as_posix()) + + config_file_path = machine.out_dir / config_file.name + with open(config_file_path, "r") as f: + return f.read() + + +machine.wait_for_unit("default.target") +assert "lix" in machine.succeed("nix --version").lower() + +run_log(machine, "nixos-generate-config") +machine.succeed("mkdir -vp /etc/nixos") +machine.succeed("cp -rv /run/current-system/sw/share/nixos/* /etc/nixos/") +machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs -v --fallback >&2") + +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, "")}" +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/basic/test.nix b/tests/basic/test.nix new file mode 100644 index 0000000..61110ad --- /dev/null +++ b/tests/basic/test.nix @@ -0,0 +1,43 @@ +{ + pkgs, + lib, + config, + ... +}: +{ + name = "nixos-test-dynamicism-main"; + + defaults = { ... }: { }; + + #node.pkgsReadOnly = false; + + extraPythonPackages = p: [ + p.beartype + ]; + + nodes.machine = { pkgs, config, ... }: { + imports = [ ./configuration.nix ]; + + system.includeBuildDependencies = true; + system.switch.enable = true; + + virtualisation.additionalPaths = [ config.system.build.toplevel ]; + virtualisation = { + memorySize = 4096; + cores = 4; + writableStore = true; + mountHostNixStore = true; + installBootLoader = true; + }; + + environment.systemPackages = let + configFileTree = pkgs.callPackage ./configuration-package.nix { }; + in [ + configFileTree + ]; + }; + + # What's a little IFD between friends? + testScript = ./test-script.py + |> builtins.readFile; +} diff --git a/tests/default.nix b/tests/default.nix index 535c54b..bc4beef 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -1,79 +1,8 @@ { pkgs ? import { }, - qpkgs ? let - src = fetchTree (builtins.parseFlakeRef "github:Qyriad/nur-packages"); - in import src { inherit pkgs; }, - callPackage ? qpkgs.callPackage, - lib ? qpkgs.lib, - dynix ? qpkgs.callPackage ../package.nix { }, -}: let - - mkDynixConfigurationDotNix = callPackage ./mk-test-configuration-dot-nix.nix { }; - - runDynixTest = testModule: pkgs.testers.runNixOSTest { - imports = [ testModule ]; - - # NOTE: these are arguments to each *test module*. - # Not the NixOS modules of the test's nodes. - _module.args = { inherit mkDynixConfigurationDotNix; }; - - # Why is this argument called "extraBaseModule**s**" but take a single module argument... - # Also note this is an extra base module for each node of the test, - # not an extra test module. - extraBaseModules = { name, config, options, modulesPath, ... }: { - /** - * Everything in this module will disappear once nixos-rebuild switch happens. - * So each test will need to use `mkDynixConfigurationDotNix` to get - * ./dynix-vm-configuration included in the in-VM configuration. - */ - - imports = (import "${modulesPath}/module-list.nix") ++ [ - # For the VM node, but not the in-VM configuration.nix - ./module-allow-rebuild-in-vm.nix - # For the VM node, and the in-VM configuration.nix - ./dynix-vm-configuration.nix - dynix.dynix - ]; - - systemd.services."install-dynix" = { - enable = true; - serviceConfig.Type = "oneshot"; - serviceConfig.RemainAfterExit = true; - path = [ config.system.path ]; - wantedBy = [ "multi-user.target" ]; - requiredBy = [ "multi-user.target" ]; - after = [ "default.target" ]; - script = '' - 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 - cp -rv --dereference /run/current-system/sw/share/nixos/*.nix /etc/nixos/ - if ! [[ -e /etc/nixos/dynix-vm-configuration.nix ]]; then - echo "FAILURE" - echo "FAILURE" >&2 - fi - ''; - }; - - 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. - warnings = builtins.seq name [ ]; - }; - }; - -in lib.makeScope lib.callPackageWith (self: { - gotosocial = runDynixTest ./gotosocial/test.nix; - harmonia = runDynixTest ./harmonia/test.nix; - distccd = runDynixTest ./distccd/test.nix; + lib ? pkgs.lib, +}: lib.makeScope lib.callPackageWith (self: let + inherit (pkgs.testers) runNixOSTest; +in { + basic = runNixOSTest ./basic/test.nix; }) diff --git a/tests/distccd/configuration.nix b/tests/distccd/configuration.nix deleted file mode 100644 index a8fb343..0000000 --- a/tests/distccd/configuration.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ ... }: -{ - services.distccd = { - enable = true; - jobTimeout = 900; - maxJobs = 12; - nice = -10; - }; - - dynamicism.for.distccd.enable = true; - - networking.hostName = "distccd-machine"; -} - diff --git a/tests/distccd/test-script.py b/tests/distccd/test-script.py deleted file mode 100644 index d5867bf..0000000 --- a/tests/distccd/test-script.py +++ /dev/null @@ -1,130 +0,0 @@ -import argparse -import functools -#from pathlib import Path -#from pprint import pformat -import shlex -import textwrap -from typing import Any, cast, TYPE_CHECKING - -from beartype import beartype - -from test_driver.machine import Machine -from test_driver.errors import RequestedAssertionFailed - -if TYPE_CHECKING: - global machine - machine = cast(Machine, ...) - assert machine.shell is not None - -ls = "eza -lah --color=always --group-directories-first" - -indent = functools.partial(textwrap.indent, prefix=' ') - -@beartype -def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: - output = "" - for command in commands: - with machine.nested(f"must succeed: {command}"): - (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) - if status != 0: - machine.log(f"output: {out}") - raise RequestedAssertionFailed( - f"command `{command}` failed (exit code {status})", - ) - output += out - - return output - -parser = argparse.ArgumentParser() -#parser.add_argument("--no-detach", action="store_true") -#parser.add_argument("--daemon", action="store_true") -#parser.add_argument("--enable-tcp-insecure", action="store_true") -#parser.add_argument("--port", type=int) -parser.add_argument("--jobs", type=int) -parser.add_argument("--job-lifetime", type=int) -parser.add_argument("--log-level", type=str) -#parser.add_argument("--nice", type=int) -#parser.add_argument("--stats", action="store_true") -#parser.add_argument("--stats-port", type=int) - -@beartype -def get_cli_args() -> argparse.Namespace: - machine.wait_for_unit("distccd.service") - mainpid = int(machine.get_unit_property("distccd.service", "MainPID")) - machine.log(f"{mainpid=}") - pidtext = machine.succeed(f"pgrep -P {mainpid}") - machine.log(f"{pidtext=}") - pid = int(pidtext.splitlines()[0]) - machine.log(f"{pid=}") - execstart = machine.get_unit_property("distccd.service", "ExecStart") - print(f"{execstart=}") - - cmdline = machine.succeed(f"cat /proc/{pid}/cmdline") - cmdline_args = cmdline.split("\0") - machine.log(f"{cmdline_args=}") - print(f"{cmdline_args=}") - - #return shlex.join(cmdline_args[1:]) - 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") - -# Config should have our initial values. -args = get_cli_args() -#assert '--jobs 12' in args, f'--jobs 12 not in {args=}' -assert args.jobs == 12, f'{args.jobs=} != 12' -assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' -assert args.log_level == 'warning', f'{args.log_level=} != warning' - -with machine.nested("must succeed: initial nixos-rebuild switch"): - machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") - -# Config should not have changed. -args = get_cli_args() -#machine.log(f"config.toml after first rebuild: {indent(pformat(args))}") -#assert int(args['workers']) == 4, f"{args['workers']=} != 4" -#assert int(args['max_connection_rate']) == 256, f"{args['max_connection_rate']=} != 256" -# -new_jobs = 4 -dynix_append("services.distccd.maxJobs", new_jobs) -do_apply() - -args = get_cli_args() - -# Only jobs should have changed. The others should still be default. -assert args.jobs == new_jobs, f'{args.jobs=} != {new_jobs=}' -assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' -assert args.log_level == 'warning', f'{args.log_level=} != warning' - -new_log_level = 'error' -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=}' -#assert args.job_lifetime == 900, f'{args.job_lifetime} != 900' -assert args.log_level == new_log_level, f'{args.log_level=} != {new_log_level=}' diff --git a/tests/distccd/test.nix b/tests/distccd/test.nix deleted file mode 100644 index ad9ac73..0000000 --- a/tests/distccd/test.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ mkDynixConfigurationDotNix, config, ... }: -let - testName = config.name; -in -{ - name = "nixos-test-dynamicism-distccd"; - - defaults = { ... }: { }; - - extraPythonPackages = p: [ p.beartype ]; - - nodes.machine = { ... }: { - imports = [ ./configuration.nix ]; - - passthru.configurationDotNix = mkDynixConfigurationDotNix { - name = testName; - configuration = ./configuration.nix; - }; - }; - - testScript = builtins.readFile ./test-script.py; -} diff --git a/tests/dynix-vm-configuration.nix b/tests/dynix-vm-configuration.nix deleted file mode 100644 index fbc6193..0000000 --- a/tests/dynix-vm-configuration.nix +++ /dev/null @@ -1,59 +0,0 @@ -{ pkgs, lib, modulesPath, ... }: -let - moduleList = import "${modulesPath}/module-list.nix"; - - dynixFromSearchPath = let - res = builtins.tryEval ; - in lib.optional res.success res.value; -in -{ - imports = [ - "${modulesPath}/testing/test-instrumentation.nix" - ] ++ lib.concatLists [ - dynixFromSearchPath - moduleList - ]; - - system.switch.enable = true; - system.includeBuildDependencies = true; - documentation.enable = false; - - boot.loader.grub = { - enable = true; - device = "/dev/vda"; - forceInstall = true; - }; - - nix = { - package = pkgs.lixPackageSets.latest.lix; - nixPath = [ - "nixpkgs=${pkgs.path}" - "/nix/var/nix/profiles/per-user/root/profile/share/nixos/modules" - ]; - - settings = { - experimental-features = [ "nix-command" "pipe-operator" ]; - substituters = lib.mkForce [ ]; - hashed-mirrors = null; - connect-timeout = 1; - # For my debugging purposes. - show-trace = true; - }; - }; - - environment.pathsToLink = [ "/share" ]; - environment.extraOutputsToInstall = [ "modules" ]; - environment.variables = { - "NIXOS_CONFIG" = "/etc/nixos/configuration.nix"; - }; - - environment.shellAliases = { - ls = "eza --long --header --group --group-directories-first --classify --binary"; - }; - - environment.systemPackages = with pkgs; [ - eza - fd - ripgrep - ]; -} diff --git a/tests/gotosocial/configuration-package.nix b/tests/gotosocial/configuration-package.nix deleted file mode 100644 index 96a89d8..0000000 --- a/tests/gotosocial/configuration-package.nix +++ /dev/null @@ -1,7 +0,0 @@ -{ - runCommand, -}: runCommand "tests-gotosocial-configuration-dot-nix" { -} '' - set -euo pipefail - install -Dm a=r ${./configuration.nix} "$out/share/nixos/configuration.nix" -'' diff --git a/tests/gotosocial/configuration.nix b/tests/gotosocial/configuration.nix deleted file mode 100644 index 8ada5c8..0000000 --- a/tests/gotosocial/configuration.nix +++ /dev/null @@ -1,17 +0,0 @@ -{ pkgs, lib, config, modulesPath, ... }: -let - name = config.networking.hostName; -in -{ - networking.hostName = "gotosocial-machine"; - services.gotosocial = { - enable = true; - setupPostgresqlDB = true; - settings = { - application-name = "gotosocial-for-machine"; - host = "${name}.local"; - }; - }; - - dynamicism.for.gotosocial.enable = true; -} diff --git a/tests/gotosocial/hardware-configuration.nix b/tests/gotosocial/hardware-configuration.nix deleted file mode 100644 index f036a72..0000000 --- a/tests/gotosocial/hardware-configuration.nix +++ /dev/null @@ -1,7 +0,0 @@ -/** Dummy hardware configuration. - * Will be replaced with the real one in the test VM. - */ -{ ... }: -{ - -} diff --git a/tests/gotosocial/test-script.py b/tests/gotosocial/test-script.py deleted file mode 100644 index 9799dfb..0000000 --- a/tests/gotosocial/test-script.py +++ /dev/null @@ -1,120 +0,0 @@ -from pathlib import Path -import shlex -import textwrap -from typing import cast, TYPE_CHECKING - -from beartype import beartype - -from test_driver.machine import Machine -from test_driver.errors import RequestedAssertionFailed - -if TYPE_CHECKING: - global machine - machine = cast(Machine, ...) - assert machine.shell is not None - -ls = "eza -lah --color=always --group-directories-first" - -@beartype -def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: - output = "" - for command in commands: - with machine.nested(f"must succeed: {command}"): - (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) - if status != 0: - machine.log(f"output: {out}") - raise RequestedAssertionFailed( - f"command `{command}` failed (exit code {status})", - ) - output += out - - return output - -@beartype -def get_config_file() -> str: - machine.wait_for_unit("gotosocial.service") - gotosocial_pid = int(machine.get_unit_property("gotosocial.service", "MainPID")) - - cmdline = machine.succeed(f"cat /proc/{gotosocial_pid}/cmdline") - cmdline_args = cmdline.split("\0") - - config_file_idx = cmdline_args.index("--config-path") + 1 - config_file = Path(cmdline_args[config_file_idx]) - - machine.log(f"copying from VM: {config_file=}") - machine.copy_from_vm(config_file.as_posix()) - - config_file_path = machine.out_dir / config_file.name - with open(config_file_path, "r") as f: - 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.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() -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=}" diff --git a/tests/gotosocial/test.nix b/tests/gotosocial/test.nix deleted file mode 100644 index ccb71f6..0000000 --- a/tests/gotosocial/test.nix +++ /dev/null @@ -1,28 +0,0 @@ -{ mkDynixConfigurationDotNix, config, ... }: -let - testName = config.name; -in -{ - name = "nixos-test-dynamicism-gotosocial"; - - defaults = { ... }: { }; - - extraPythonPackages = p: [ - p.beartype - ]; - - 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 ]; - - passthru.configurationDotNix = mkDynixConfigurationDotNix { - name = testName; - configuration = ./configuration.nix; - }; - }; - - testScript = builtins.readFile ./test-script.py; -} diff --git a/tests/harmonia/configuration.nix b/tests/harmonia/configuration.nix deleted file mode 100644 index 37494cb..0000000 --- a/tests/harmonia/configuration.nix +++ /dev/null @@ -1,30 +0,0 @@ -{ lib, modulesPath, ... }: -let - moduleList = import "${modulesPath}/module-list.nix"; - - dynixFromSearchPath = let - res = builtins.tryEval ; - in lib.optional res.success res.value; -in -{ - imports = [ - #"${modulesPath}/testing/test-instrumentation.nix" - #./hardware-configuration.nix - ] ++ lib.concatLists [ - #dynixFromSearchPath - moduleList - ]; - - dynamicism.for.harmonia.enable = true; - services.harmonia = { - enable = true; - settings = { - # Default. - workers = 4; - # Default. - max_connection_rate = 256; - }; - }; - - networking.hostName = "harmonia-machine"; -} diff --git a/tests/harmonia/hardware-configuration.nix b/tests/harmonia/hardware-configuration.nix deleted file mode 100644 index facb35d..0000000 --- a/tests/harmonia/hardware-configuration.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ ... }: -{ - -} diff --git a/tests/harmonia/test-script.py b/tests/harmonia/test-script.py deleted file mode 100644 index 215ea38..0000000 --- a/tests/harmonia/test-script.py +++ /dev/null @@ -1,124 +0,0 @@ -import functools -from pathlib import Path -from pprint import pformat -import shlex -import textwrap -import tomllib -from typing import Any, cast, TYPE_CHECKING - -from beartype import beartype - -from test_driver.machine import Machine -from test_driver.errors import RequestedAssertionFailed - -if TYPE_CHECKING: - global machine - machine = cast(Machine, ...) - assert machine.shell is not None - -ls = "eza -lah --color=always --group-directories-first" - -indent = functools.partial(textwrap.indent, prefix=' ') - -@beartype -def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: - output = "" - for command in commands: - with machine.nested(f"must succeed: {command}"): - (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) - if status != 0: - machine.log(f"output: {out}") - raise RequestedAssertionFailed( - f"command `{command}` failed (exit code {status})", - ) - output += out - - return output - -@beartype -def get_config_file() -> dict[str, Any]: - machine.wait_for_unit("harmonia.service") - pid = int(machine.get_unit_property("harmonia.service", "MainPID")) - env_lines: list[str] = machine.succeed(f"cat /proc/{pid}/environ").replace("\0", "\n").splitlines() - pairs: list[list[str]] = [line.split("=", maxsplit=1) for line in env_lines] - env = dict(pairs) - - config_file = Path(env["CONFIG_FILE"]) - - machine.log(f"copying from VM: {config_file=}") - machine.copy_from_vm(config_file.as_posix()) - - config_file_path = machine.out_dir / config_file.name - with open(config_file_path, "rb") as f: - config_data = tomllib.load(f) - - 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") - -# Config should have our initial values. -config_toml = get_config_file() -machine.log(f"config.toml BEFORE first rebuild (initial): {indent(pformat(config_toml))}") -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" - -with machine.nested("must succeed: initial nixos-rebuild switch"): - machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") - -# Config should not have changed. -config_toml = get_config_file() -machine.log(f"config.toml after first rebuild: {indent(pformat(config_toml))}") -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 -dynix_append("services.harmonia.settings.workers", new_workers) -do_apply() - -# Workers, but not max connection rate, should have changed. -config_toml = get_config_file() -machine.log(f"config.toml after DYNAMIC ACTIVATION: {indent(pformat(config_toml))}") -assert int(config_toml['workers']) == new_workers, f"{config_toml['workers']=} != {new_workers}" -assert int(config_toml['max_connection_rate']) == 256, f"{config_toml['max_connection_rate']=} != 256" - -new_max_connection_rate = 100 -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() -machine.log(f"config_toml after SECOND dynamic activation: {indent(pformat(config_toml))}") -assert int(config_toml['max_connection_rate']) == new_max_connection_rate, f"{config_toml['max_connection_rate']=} != {new_max_connection_rate}" - -# And this should set everything back. -machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") -machine.systemctl("restart harmonia.service") -machine.wait_for_unit("harmonia.service") -config_toml = get_config_file() -machine.log(f"config_toml after NORMAL activation: {indent(pformat(config_toml))}") -assert int(config_toml['max_connection_rate']) == 256, f'{config_toml["max_connection_rate"]=} != 256' -assert int(config_toml['workers']) == 4, f'{config_toml["workers"]=} != 4' diff --git a/tests/harmonia/test.nix b/tests/harmonia/test.nix deleted file mode 100644 index 25fcfbf..0000000 --- a/tests/harmonia/test.nix +++ /dev/null @@ -1,22 +0,0 @@ -{ mkDynixConfigurationDotNix, config, ... }: -let - testName = config.name; -in -{ - name = "nixos-test-dynamicism-harmonia"; - - defaults = { ... }: { }; - - extraPythonPackages = p: [ p.beartype ]; - - nodes.machine = { ... }: { - imports = [ ./configuration.nix ]; - - 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 deleted file mode 100644 index 6b7dc25..0000000 --- a/tests/mk-test-configuration-dot-nix.nix +++ /dev/null @@ -1,67 +0,0 @@ -{ - lib, - stdenvNoCC, -}: let - stdenv = stdenvNoCC; - - mkDynixConfigurationDotNix = finalAttrs: { - name, - /** A *path* to a `configuration.nix`-like NixOS module. - * NOT a NixOS module *value*. - */ - configuration, - }: assert lib.isStringLike configuration; let - self = finalAttrs.finalPackage; - in { - name = "configuration-dot-nix-for-${name}"; - strictDeps = true; - __structuredAttrs = true; - preferLocalBuild = true; - - phases = [ "installPhase" ]; - - outputs = [ "out" ]; - modulesOut = "${placeholder "out"}/share/nixos"; - - baseConfiguration = configuration; - dynixVmConfiguration = ./dynix-vm-configuration.nix; - - installPhase = '' - runHook preInstall - - install -Dm a=r "$baseConfiguration" "$modulesOut/test-configuration.nix" - install -Dm a=r "$dynixVmConfiguration" "$modulesOut/dynix-vm-configuration.nix" - - echo "/** GENERATED BY mk-test-configuration-dot-nix! */" >> "$modulesOut/configuration.nix" - echo "{ ... }:" >> "$modulesOut/configuration.nix" - echo >> "$modulesOut/configuration.nix" - echo >> "$modulesOut/configuration.nix" - echo "{" >> "$modulesOut/configuration.nix" - echo " imports = [" >> "$modulesOut/configuration.nix" - echo " ./test-configuration.nix" >> "$modulesOut/configuration.nix" - echo " ./dynix-vm-configuration.nix" >> "$modulesOut/configuration.nix" - echo " ./hardware-configuration.nix" >> "$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 - ''; - - passthru = { - modulesPath = self.out + "/share/nixos"; - configuration = self.out + "/share/nixos/configuration.nix"; - }; - }; -in lib.extendMkDerivation { - constructDrv = stdenv.mkDerivation; - extendDrvArgs = mkDynixConfigurationDotNix; -} diff --git a/tests/module-allow-rebuild-in-vm.nix b/tests/module-allow-rebuild-in-vm.nix deleted file mode 100644 index 96280fe..0000000 --- a/tests/module-allow-rebuild-in-vm.nix +++ /dev/null @@ -1,31 +0,0 @@ -{ name, config, ... }: - -{ - system.includeBuildDependencies = true; - system.switch.enable = true; - - documentation.enable = false; - - virtualisation = { - additionalPaths = [ config.system.build.toplevel ]; - writableStore = true; - mountHostNixStore = true; - installBootLoader = true; - - # With how much memory Nix eval uses, this is essentially required. - memorySize = 8192; - cores = 8; - - }; - - boot.loader.grub = { - enable = true; - device = "/dev/vda"; - forceInstall = true; - }; - - # 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. - warnings = builtins.seq name [ ]; -} diff --git a/tests/tzupdate/hardware-configuration.nix b/tests/tzupdate/hardware-configuration.nix deleted file mode 100644 index facb35d..0000000 --- a/tests/tzupdate/hardware-configuration.nix +++ /dev/null @@ -1,4 +0,0 @@ -{ ... }: -{ - -} diff --git a/tests/tzupdate/test-script.py b/tests/tzupdate/test-script.py deleted file mode 100644 index 53762a0..0000000 --- a/tests/tzupdate/test-script.py +++ /dev/null @@ -1,52 +0,0 @@ -#from pathlib import Path -#import shlex -#import textwrap -from typing import cast, TYPE_CHECKING - -from beartype import beartype - -from test_driver.machine import Machine -from test_driver.errors import RequestedAssertionFailed - -if TYPE_CHECKING: - global machine - machine = cast(Machine, ...) - assert machine.shell is not None - -ls = "eza -lah --color=always --group-directories-first" - -@beartype -def run_log(machine: Machine, *commands: str, timeout: int | None = 60) -> str: - output = "" - for command in commands: - with machine.nested(f"must succeed: {command}"): - (status, out) = machine.execute(f"{command} | tee /dev/stderr", timeout=timeout) - if status != 0: - machine.log(f"output: {out}") - raise RequestedAssertionFailed( - f"command `{command}` failed (exit code {status})", - ) - output += out - - return output - -machine.wait_for_unit("default.target") -assert "lix" in machine.succeed("nix --version").lower() -machine.log("INIT") - - -machine.succeed("nixos-generate-config") -machine.succeed("mkdir -vp /etc/nixos") -# Dereference is required since that configuration.nix is probably a symlink to the store. -machine.succeed("cp -rv --dereference /run/current-system/sw/share/nixos/configuration.nix /etc/nixos/") - -machine.succeed("env PAGER= nixos-rebuild switch --log-format raw-with-logs --fallback") - -@beartype -def get_interval() -> str: - prop = machine.get_unit_property("tzupdate.timer", "OnCalendar") - print(f"{prop=}") - - return prop - -get_interval() diff --git a/tests/tzupdate/test.nix b/tests/tzupdate/test.nix deleted file mode 100644 index 2d42c10..0000000 --- a/tests/tzupdate/test.nix +++ /dev/null @@ -1,24 +0,0 @@ -{ ... }: - -{ - name = "nixos-test-dynamicism-tzupdate"; - - extraPythonPackages = p: [ - p.beartype - ]; - - nodes.machine = { name, pkgs, ... }: { - 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" - ''; - in [ - configFileTree - ]; - }; - - testScript = builtins.readFile ./test-script.py; -}