diff --git a/Cargo.lock b/Cargo.lock index d5fd1ed..eef46d8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "0.6.21" @@ -38,7 +47,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys", + "windows-sys 0.61.2", ] [[package]] @@ -49,19 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys", -] - -[[package]] -name = "append-override" -version = "0.1.0" -dependencies = [ - "clap", - "command-error", - "fs-err", - "libc", - "serde", - "serde_json", + "windows-sys 0.61.2", ] [[package]] @@ -152,12 +149,45 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" +[[package]] +name = "dynix" +version = "0.1.0" +dependencies = [ + "clap", + "command-error", + "fs-err", + "itertools", + "libc", + "serde", + "serde_json", + "tap", + "tracing", + "tracing-human-layer", + "tracing-subscriber", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + [[package]] name = "equivalent" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "errno" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" +dependencies = [ + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "fs-err" version = "3.2.2" @@ -179,6 +209,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + [[package]] name = "indexmap" version = "2.13.0" @@ -189,24 +225,86 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "is-terminal" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.61.2", +] + +[[package]] +name = "is_ci" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7655c9839580ee829dfacba1d1278c2b7883e50a277ff7541299489d6bdfdc45" + [[package]] name = "is_terminal_polyfill" version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" +[[package]] +name = "itertools" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + [[package]] name = "libc" version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +[[package]] +name = "linux-raw-sys" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" + +[[package]] +name = "lock_api" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" +dependencies = [ + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.7.6" @@ -225,6 +323,15 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + [[package]] name = "once_cell" version = "1.21.3" @@ -237,6 +344,39 @@ version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" +[[package]] +name = "owo-colors" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +dependencies = [ + "supports-color 2.1.0", + "supports-color 3.0.2", +] + +[[package]] +name = "parking_lot" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-link 0.2.1", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -273,6 +413,51 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "redox_syscall" +version = "0.5.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" +dependencies = [ + "bitflags", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "rustix" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +dependencies = [ + "bitflags", + "errno", + "libc", + "linux-raw-sys", + "windows-sys 0.61.2", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + [[package]] name = "serde" version = "1.0.228" @@ -316,18 +501,58 @@ dependencies = [ "zmij", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shell-words" version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + [[package]] name = "strsim" version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "supports-color" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" +dependencies = [ + "is-terminal", + "is_ci", +] + +[[package]] +name = "supports-color" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c64fc7232dd8d2e4ac5ce4ef302b1d81e0b80d055b9d77c7c4f51f6aa4c867d6" +dependencies = [ + "is_ci", +] + [[package]] name = "syn" version = "2.0.114" @@ -339,6 +564,43 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "terminal_size" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" +dependencies = [ + "rustix", + "windows-sys 0.60.2", +] + +[[package]] +name = "textwrap" +version = "0.16.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" +dependencies = [ + "smawk", + "terminal_size", + "unicode-linebreak", + "unicode-width", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + [[package]] name = "tracing" version = "0.1.44" @@ -368,6 +630,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-human-layer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b285fd79bba4659408f5d290b3f30fd69d428c630d8c00bb4ba255f2501d50e3" +dependencies = [ + "itertools", + "owo-colors", + "parking_lot", + "textwrap", + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "parking_lot", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -376,6 +683,18 @@ version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-width" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254" + [[package]] name = "utf8-command" version = "1.0.1" @@ -388,6 +707,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + [[package]] name = "windows" version = "0.61.3" @@ -496,6 +821,15 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets", +] + [[package]] name = "windows-sys" version = "0.61.2" @@ -505,6 +839,23 @@ dependencies = [ "windows-link 0.2.1", ] +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link 0.2.1", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + [[package]] name = "windows-threading" version = "0.1.0" @@ -514,6 +865,54 @@ dependencies = [ "windows-link 0.1.3", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + [[package]] name = "zmij" version = "1.0.16" diff --git a/Cargo.toml b/Cargo.toml index d0ea161..dd24d30 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,20 +1,25 @@ [package] -name = "append-override" +name = "dynix" version = "0.1.0" edition = "2024" [[bin]] -name = "append-override" +name = "dynix" path = "src/main.rs" [lib] -name = "append_override" +name = "dynix" path = "src/lib.rs" [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"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.149" +tap = "1.0.1" +tracing = { version = "0.1.44", features = ["attributes"] } +tracing-human-layer = "0.2.1" +tracing-subscriber = { version = "0.3.22", default-features = false, features = ["std", "env-filter", "fmt", "ansi", "registry", "parking_lot"] } 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 13e49d4..b7a3bef 100644 --- a/default.nix +++ b/default.nix @@ -6,15 +6,70 @@ }: let inherit (qpkgs) lib; - PKGNAME = qpkgs.callPackage ./package.nix { }; + dynix = qpkgs.callPackage ./package.nix { }; byStdenv = lib.mapAttrs (stdenvName: stdenv: let - withStdenv = PKGNAME.override { inherit stdenv; }; - PKGNAME' = withStdenv.overrideAttrs (prev: { + withStdenv = dynix.override { inherit stdenv; }; + dynix' = withStdenv.overrideAttrs (prev: { pname = "${prev.pname}-${stdenvName}"; }); - in PKGNAME') qpkgs.validStdenvs; + in dynix') qpkgs.validStdenvs; -in PKGNAME.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.nix b/flake.nix index a577619..8e0dfb6 100644 --- a/flake.nix +++ b/flake.nix @@ -21,21 +21,21 @@ qpkgs = import qyriad-nur { inherit pkgs; }; inherit (qpkgs) lib; - PKGNAME = import ./default.nix { inherit pkgs qpkgs; }; + dynix = import ./default.nix { inherit pkgs qpkgs; }; extraVersions = lib.mapAttrs' (stdenvName: value: { - name = "${stdenvName}-PKGNAME"; + name = "${stdenvName}-dynix"; inherit value; - }) PKGNAME.byStdenv; + }) dynix.byStdenv; - devShell = import ./shell.nix { inherit pkgs qpkgs PKGNAME; }; + devShell = import ./shell.nix { inherit pkgs qpkgs dynix; }; extraDevShells = lib.mapAttrs' (stdenvName: value: { - name = "${stdenvName}-PKGNAME"; + name = "${stdenvName}-dynix"; inherit value; - }) PKGNAME.byStdenv; + }) dynix.byStdenv; in { packages = extraVersions // { - default = PKGNAME; - inherit PKGNAME; + default = dynix; + inherit dynix; }; devShells = extraDevShells // { 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 new file mode 100644 index 0000000..8e4764c --- /dev/null +++ b/modules/dynamicism/default.nix @@ -0,0 +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 +{ + # + # 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/lib.nix b/modules/dynamicism/lib.nix new file mode 100644 index 0000000..a988ae4 --- /dev/null +++ b/modules/dynamicism/lib.nix @@ -0,0 +1,24 @@ +{ + lib ? import , +}: let + t = lib.types; +in lib.fix (self: { + /** Perform module-system type checking and resolving on a single option value. */ + typeCheck = loc: option: value: + assert lib.isOptionType option; + assert lib.isList loc; + assert lib.all lib.isString loc; + let + merged = lib.modules.mergeDefinitions loc option [ { + inherit value; + file = "«inline»"; + } ]; + in merged.mergedValue; + + /** Either a list of strings, or a dotted string that will be split. */ + convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); + + 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 new file mode 100644 index 0000000..1b9dac9 --- /dev/null +++ b/modules/dynamicism/submodule.nix @@ -0,0 +1,98 @@ +{ + name, + pkgs, + lib, + config, + ... +}: +let + inherit (lib.modules) + mkIf + ; + inherit (lib.options) + mkOption + mkEnableOption + literalExpression + ; + inherit (lib.types) + mkOptionType + ; + t = lib.types; + + /** Either a list of strings, or a dotted string that will be split. */ + convenientAttrPath = t.coercedTo t.str (lib.splitString ".") (t.listOf t.str); + + executablePathInStore = mkOptionType { + name = "exepath"; + description = "executable path in the Nix store"; + descriptionClass = "noun"; + merge = lib.mergeEqualOption; + functor = lib.defaultFunctor "exepath"; + check = x: if lib.isDerivation x then ( + x.meta.mainProgram or null != null + ) else ( + lib.pathInStore.check x + ); + }; +in +{ + options = { + enable = mkEnableOption "dynamicism for '${name}'"; + + source-options = mkOption { + type = t.listOf convenientAttrPath; + description = "A list of attrpaths of the NixOS option the dynamicism for '${name}' uses"; + example = literalExpression '' + [ [ "services" "gotosocial" "settings" ] "services.nginx.settings" ] + ''; + }; + + systemd-services-updated = mkOption { + type = t.listOf t.str; + description = '' + A list of systemd unit names (including the suffix, e.g. `.service`) that need to be updated. + ''; + example = literalExpression '' + [ "gotosocial.service" ] + ''; + + default = lib.attrNames config.unitDropins; + }; + + configFile = mkOption { + type = t.pathInStore; + internal = true; + }; + + unitDropins = mkOption { + type = t.attrsOf t.pathInStore; + internal = true; + }; + + activate = mkOption { + type = executablePathInStore; + internal = true; + }; + }; + + config = mkIf config.enable { + activate = pkgs.writeShellApplication { + name = "dynamicism-for-${name}-activate"; + runtimeInputs = [ pkgs.systemd ]; + text = let + doEdits = config.unitDropins + |> lib.mapAttrsToList (service: dropin: '' + cat "${dropin}" | systemctl edit "${service}" --runtime --stdin + ''); + doReloads = config.unitDropins + |> lib.mapAttrsToList (service: _: '' + systemctl reload-or-restart "${service}" + ''); + in [ + doEdits + doReloads + ] |> lib.concatLists + |> lib.concatStringsSep "\n"; + }; + }; +} 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 68878aa..2cb8cee 100644 --- a/package.nix +++ b/package.nix @@ -81,6 +81,6 @@ in stdenv.mkDerivation (self: { }); meta = { - mainProgram = "PKGNAME"; + mainProgram = "dynix"; }; })) diff --git a/shell.nix b/shell.nix index 9e803f7..c73c766 100644 --- a/shell.nix +++ b/shell.nix @@ -3,15 +3,21 @@ qpkgs ? let src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; in import src { inherit pkgs; }, - PKGNAME ? import ./default.nix { inherit pkgs qpkgs; }, + dynix ? import ./default.nix { inherit pkgs qpkgs; }, }: let inherit (pkgs) lib; - mkDevShell = PKGNAME: qpkgs.callPackage PKGNAME.mkDevShell { }; - devShell = mkDevShell PKGNAME; + mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { }; + devShell = mkDevShell dynix; - byStdenv = lib.mapAttrs (lib.const mkDevShell) PKGNAME.byStdenv; + byStdenv = lib.mapAttrs (lib.const mkDevShell) dynix.byStdenv; 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 8de2c39..cc2fa07 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,3 +1,5 @@ +use std::sync::Arc; + use clap::ColorChoice; use crate::prelude::*; @@ -47,26 +49,46 @@ impl FromStr for NixOsOption { } #[derive(Debug, Clone, PartialEq, clap::Parser)] -#[command(version, about, author, arg_required_else_help(true))] -pub struct Parser { - #[arg(long, default_value = "auto")] +pub struct AppendCmd { + #[arg(required = true)] + pub name: Arc, + #[arg(required = true)] + pub value: Arc, +} + +#[derive(Debug, Clone, PartialEq, clap::Parser)] +pub struct DeltaCmd {} + +#[derive(Debug, Clone, PartialEq, clap::Subcommand)] +#[command(flatten_help = true)] +pub enum Subcommand { + Append(AppendCmd), + // TODO: rename + Delta(DeltaCmd), +} + +#[derive(Debug, Clone, PartialEq, clap::Parser)] +#[command(version, about, author)] +#[command(arg_required_else_help(true), args_override_self(true))] +#[command(propagate_version = true)] +pub struct Args { + #[arg(long, global(true), default_value = "auto")] pub color: ColorChoice, - #[arg(long)] - pub file: Box, + // FIXME: default to /etc/configuration.nix, or something? + #[arg(long, global(true), default_value = "./configuration.nix")] + pub file: Arc, - #[arg(required = true)] - pub name: Box, - #[arg(required = true)] - pub value: Box, - ///// Flakeref to a base configuration to modify. - //#[arg(group = "config", long, default_value("."))] - //#[arg(long, default_value(Some(".")))] - //flake: Option>>, - // - //#[arg(group = "config", long)] - //expr: Option, + #[command(subcommand)] + pub subcommand: Subcommand, } +///// Flakeref to a base configuration to modify. +//#[arg(group = "config", long, default_value("."))] +//#[arg(long, default_value(Some(".")))] +//flake: Option>>, +// +//#[arg(group = "config", long)] +//expr: Option, //impl Parser { // fn eval_cmd(&self) { diff --git a/src/color.rs b/src/color.rs index 19d90c7..1e420f6 100644 --- a/src/color.rs +++ b/src/color.rs @@ -6,10 +6,14 @@ use std::{ #[allow(unused_imports)] use crate::prelude::*; -pub static CLI_ENABLE_COLOR: OnceLock = OnceLock::new(); +/// The actual, final value for whether color should be used, based on CLI and environment values. +pub static SHOULD_COLOR: LazyLock = LazyLock::new(|| is_clicolor_forced() || is_color_reqd()); + +/// Initialized from the `--color` value from the CLI, along with `io::stdin().is_terminal()`. +pub static _CLI_ENABLE_COLOR: OnceLock = OnceLock::new(); fn is_color_reqd() -> bool { - CLI_ENABLE_COLOR.get().copied().unwrap_or(false) + _CLI_ENABLE_COLOR.get().copied().unwrap_or(false) } fn is_clicolor_forced() -> bool { @@ -24,8 +28,6 @@ fn is_clicolor_forced() -> bool { .unwrap_or(false) } -pub static SHOULD_COLOR: LazyLock = LazyLock::new(|| is_clicolor_forced() || is_color_reqd()); - /// Silly wrapper around LazyLock<&'static str> to impl Display. pub(crate) struct _LazyLockDisplay(LazyLock<&'static str>); impl Display for _LazyLockDisplay { @@ -47,8 +49,5 @@ pub(crate) const ANSI_CYAN: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| })); pub(crate) const ANSI_RESET: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| { - SHOULD_COLOR - // C'mon rustfmt, just format it to match ^. - .then_some("\x1b[0m") - .unwrap_or_default() + SHOULD_COLOR.then_some("\x1b[0m").unwrap_or_default() })); diff --git a/src/lib.rs b/src/lib.rs index d939afa..1956803 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -use std::sync::{Arc, LazyLock}; +use std::{iter, sync::Arc}; pub(crate) mod prelude { #![allow(unused_imports)] @@ -25,14 +25,18 @@ pub(crate) mod prelude { pub use fs_err::File; #[cfg(unix)] pub use fs_err::os::unix::fs::{FileExt, OpenOptionsExt}; + + pub use tap::{Pipe, Tap}; + + pub use tracing::{Level, debug, error, info, trace, warn}; } use prelude::*; pub mod args; -pub use args::Parser; +pub use args::{AppendCmd, Args, DeltaCmd}; mod color; -pub use color::{CLI_ENABLE_COLOR, SHOULD_COLOR}; +pub use color::{_CLI_ENABLE_COLOR, SHOULD_COLOR}; pub mod line; mod nixcmd; pub use line::Line; @@ -45,23 +49,67 @@ use crate::source::SourceFile; pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; +#[tracing::instrument(level = "debug")] +pub fn do_delta(args: Arc, delta_args: DeltaCmd) -> Result<(), BoxDynError> { + todo!(); +} + +#[tracing::instrument(level = "debug")] +pub fn do_append(args: Arc, append_args: AppendCmd) -> Result<(), BoxDynError> { + let filepath = Path::new(&args.file); + let filepath: PathBuf = if filepath.is_relative() && !filepath.starts_with("./") { + iter::once(OsStr::new("./")) + .chain(filepath.iter()) + .collect() + } else { + filepath.to_path_buf() + }; + + // Get what file that thing is defined in. + let def_path = get_where(&append_args.name, &filepath)?; + + let mut opts = File::options(); + opts.read(true) + .write(true) + .create(false) + .custom_flags(libc::O_CLOEXEC); + let source_file = SourceFile::open_from(Arc::from(def_path), opts)?; + + let pri = get_highest_prio(&append_args.name, source_file.clone())?; + let new_pri = pri - 1; + + let new_pri_line = get_next_prio_line( + source_file.clone(), + append_args.name.into(), + new_pri, + append_args.value.into(), + )?; + + eprintln!("new_pri_line={new_pri_line}"); + + write_next_prio(source_file, new_pri_line)?; + + Ok(()) +} + #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)] pub struct DefinitionWithLocation { pub file: Box, - pub value: Box, + pub value: Box, } -pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result, BoxDynError> { - let expr: OsString = [ - // foo +pub fn expr_for_configuration(source_file: &Path) -> OsString { + [ OsStr::new("import { configuration = "), - configuration_nix.as_os_str(), + source_file.as_os_str(), OsStr::new("; }"), ] .into_iter() - .map(ToOwned::to_owned) - .collect(); + .collect() +} +pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result, BoxDynError> { + let expr = expr_for_configuration(configuration_nix); let attrpath = format!("options.{}.definitionsWithLocations", option_name); let output = nixcmd::NixEvalExpr { expr, attrpath } @@ -75,19 +123,10 @@ pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result Result<(i64, SourceLine), BoxDynError> { +pub fn get_highest_prio(option_name: &str, source: SourceFile) -> Result { // Get the current highest priority. - let expr: OsString = [ - OsStr::new("import { configuration = "), - source.path().as_os_str(), - OsStr::new("; }"), - ] - .into_iter() - .collect(); + let expr = expr_for_configuration(&source.path()); // Get the highest priority, and the file its defined in. let attrpath = format!("options.{}.highestPrio", option_name); @@ -97,93 +136,44 @@ pub fn get_highest_prio( let stdout = output.stdout(); let highest_prio = i64::from_str(stdout.trim())?; - let needle = format!("lib.mkOverride ({})", highest_prio); - - let path = source.path(); - let lines = source.lines()?; - let line = lines - .iter() - // We're more likely to find it at the end, so let's start there. - .rev() - .find(|&line| line.text.contains(&needle)) - .unwrap_or_else(|| { - panic!( - "couldn't find override number {highest_prio} in {}", - path.display(), - ) - }); - - Ok((highest_prio, line.clone())) + Ok(highest_prio) } pub fn get_next_prio_line( source: SourceFile, option_name: Arc, - last_line_def: SourceLine, new_prio: i64, new_value: Arc, ) -> Result { - if !last_line_def.text.ends_with(';') { - todo!(); - } - let next_line = source.line(last_line_def.line.next())?; - if next_line.text.trim() != "}" { - todo!(); - } - - let (indentation, _rest) = last_line_def.text.split_at( - last_line_def - .text - .find(|ch: char| !ch.is_ascii_whitespace()) - .unwrap_or_default(), - ); - // FIXME: fix indentation - let new_text = format!("{indentation}{option_name} = lib.mkOverride ({new_prio}) ({new_value});",); + let source_lines = source.lines()?; + let last_line = source_lines.last(); + assert_eq!(last_line.map(SourceLine::text).as_deref(), Some("]")); + let last_line = last_line.unwrap(); let new_line = SourceLine { - line: next_line.line.next(), + line: last_line.line, path: source.path(), - text: Arc::from(new_text), + text: Arc::from(format!( + " {option_name} = lib.mkOverride ({new_prio}) ({new_value});", + )), }; Ok(new_line) } -pub fn write_next_prio( - mut source: SourceFile, - last_line_def: SourceLine, - new_text: Arc, -) -> Result<(), BoxDynError> { - //let lines = source.lines()?; - - let open_brace_line = source.line(last_line_def.line.prev())?.text(); - let close_brace_line = source.line(last_line_def.line.next())?.text(); - +pub fn write_next_prio(mut source: SourceFile, new_line: SourceLine) -> Result<(), BoxDynError> { let new_mod_start = SourceLine { - line: last_line_def.line.next(), + line: new_line.line.prev(), path: source.path(), - text: open_brace_line, - }; - let new_line = SourceLine { - line: new_mod_start.line.next(), - path: source.path(), - text: Arc::from(new_text), + text: Arc::from(" {"), }; let new_mod_end = SourceLine { line: new_line.line.next(), path: source.path(), - text: close_brace_line, + text: Arc::from(" }"), }; - dbg!(&new_mod_start.text()); - - source.insert_lines(&[ - new_mod_start, - new_line, - new_mod_end, - ])?; - - //source.insert_line(new_line.line, new_line.text())?; + source.insert_lines(&[new_mod_start, new_line, new_mod_end])?; Ok(()) } diff --git a/src/main.rs b/src/main.rs index f9bfa8d..ef00265 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,17 +1,16 @@ use std::io::{self, IsTerminal}; -use std::path::Path; use std::process::ExitCode; use std::{error::Error as StdError, sync::Arc}; -use append_override::source::SourceFile; use clap::{ColorChoice, Parser as _}; -use fs_err::File; -use fs_err::os::unix::fs::OpenOptionsExt; +use tracing_human_layer::HumanLayer; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::{EnvFilter, layer::SubscriberExt}; fn main_wrapped() -> Result<(), Box> { - let args = append_override::Parser::parse(); + let args = Arc::new(dynix::Args::parse()); - let success = append_override::CLI_ENABLE_COLOR.set(match args.color { + let success = dynix::_CLI_ENABLE_COLOR.set(match args.color { ColorChoice::Always => true, ColorChoice::Auto => io::stdin().is_terminal(), ColorChoice::Never => false, @@ -20,35 +19,20 @@ fn main_wrapped() -> Result<(), Box> { success.expect("logic error in CLI_ENABLE_COLOR"); } - // FIXME: handle relative paths without leading ./ - let filepath = Path::new(&args.file); + tracing_subscriber::registry() + .with(HumanLayer::new().with_color_output(*dynix::SHOULD_COLOR)) + .with(EnvFilter::from_default_env()) + .init(); - // Get what file that thing is defined in. - let def_path = append_override::get_where(&args.name, filepath)?; - let def_path = Arc::from(def_path); - let mut opts = File::options(); - opts.read(true) - .write(true) - .create(false) - .custom_flags(libc::O_CLOEXEC); - let source_file = SourceFile::open_from(Arc::clone(&def_path), opts)?; + tracing::debug!("Parsed command-line arguments: {args:?}"); - let (pri, last_def_line) = append_override::get_highest_prio(&args.name, source_file.clone())?; - let new_pri = pri - 1; - - eprintln!("{last_def_line}"); - - let new_pri_line = append_override::get_next_prio_line( - source_file.clone(), - args.name.into(), - last_def_line.clone(), - new_pri, - args.value.into(), - )?; - - eprintln!("new_pri_line={new_pri_line}"); - - append_override::write_next_prio(source_file, last_def_line, new_pri_line.text())?; + { + use dynix::args::Subcommand::*; + match &args.subcommand { + Append(append_args) => dynix::do_append(args.clone(), append_args.clone())?, + Delta(delta_args) => dynix::do_delta(args.clone(), delta_args.clone())?, + }; + } Ok(()) } @@ -57,7 +41,7 @@ fn main() -> ExitCode { match main_wrapped() { Ok(_) => ExitCode::SUCCESS, Err(e) => { - eprintln!("append-override: error: {}", e); + eprintln!("dynix: error: {}", e); ExitCode::FAILURE } } diff --git a/src/source.rs b/src/source.rs index 7a482db..c0099a1 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,10 +1,10 @@ use std::{ - cell::{Cell, Ref, RefCell}, + cell::{Ref, RefCell}, hash::Hash, io::{BufRead, BufReader, BufWriter}, - iter, mem, - ops::{Deref, DerefMut}, - sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, RwLock}, + ops::Deref, + ptr, + sync::{Arc, Mutex, OnceLock}, }; use crate::Line; @@ -13,6 +13,33 @@ use crate::color::{ANSI_CYAN, ANSI_GREEN, ANSI_MAGENTA, ANSI_RESET}; use crate::prelude::*; use fs_err::OpenOptions; +use itertools::Itertools; + +pub fn replace_file<'a>( + path: &Path, + contents: impl IntoIterator, +) -> Result<(), IoError> { + let tmp_path = path.with_added_extension(".tmp"); + let tmp_file = File::options() + .create(true) + .write(true) + .truncate(true) + .custom_flags(libc::O_EXCL | libc::O_CLOEXEC) + .open(&tmp_path)?; + + let mut writer = BufWriter::new(tmp_file); + for slice in contents { + writer.write_all(slice)?; + } + + writer.flush()?; + drop(writer); + + // Rename the temporary file to the new file, which is atomic (TODO: I think). + fs_err::rename(&tmp_path, &path)?; + + Ok(()) +} #[derive(Debug, Clone, PartialEq, Hash)] pub struct SourceLine { @@ -25,6 +52,30 @@ impl SourceLine { pub fn text(&self) -> Arc { Arc::clone(&self.text) } + + pub fn text_ref(&self) -> &str { + &self.text + } + + pub fn text_bytes(&self) -> Arc<[u8]> { + let len: usize = self.text.as_bytes().len(); + + // We need to consume an Arc, but we are &self. + let text = Arc::clone(&self.text); + let str_ptr: *const str = Arc::into_raw(text); + let start: *const u8 = str_ptr.cast(); + let slice_ptr: *const [u8] = ptr::slice_from_raw_parts(start, len); + + unsafe { Arc::<[u8]>::from_raw(slice_ptr) } + } + + pub fn text_bytes_ref(&self) -> &[u8] { + self.text.as_bytes() + } + + pub fn path(&self) -> Arc { + Arc::clone(&self.path) + } } impl Display for SourceLine { @@ -39,11 +90,6 @@ impl Display for SourceLine { } } -#[derive(Debug, Clone, PartialEq, Hash)] -pub struct SourcePath { - path: Arc, -} - #[derive(Debug, Clone)] pub struct SourceFile { path: Arc, @@ -56,6 +102,11 @@ pub struct SourceFile { impl SourceFile { /// Panics if `path` is a directory path instead of a file path. pub fn open_from(path: Arc, options: OpenOptions) -> Result { + trace!( + "SourceFile::open_from(path={:?}, options={:?})", + path, + options.options(), + ); assert!(path.file_name().is_some()); let file = Arc::new(Mutex::new(options.open(&*path)?)); @@ -126,6 +177,7 @@ impl SourceFile { if new_lines.is_empty() { return Ok(()); } + let num_lines_before_new = new_lines.last().unwrap().line.prev().index() as usize; debug_assert!(new_lines.is_sorted_by(|lhs, rhs| lhs.line.next() == rhs.line)); @@ -133,12 +185,12 @@ impl SourceFile { let cur_lines = self.lines()?; let first_half = cur_lines .iter() - .take(new_lines.last().unwrap().line.prev().index() as usize) + .take(num_lines_before_new) .map(SourceLine::text); let middle = new_lines.iter().map(SourceLine::text); let second_half = cur_lines .iter() - .skip(new_lines.last().unwrap().line.prev().index() as usize) + .skip(num_lines_before_new) .map(SourceLine::text); let final_lines: Vec = first_half @@ -158,31 +210,11 @@ impl SourceFile { drop(cur_lines); - // Write it to a file in the same directory. - let new_name: OsString = [ - // foo - path.file_name().unwrap(), - OsStr::new(".tmp"), - ] - .into_iter() - .collect::(); - let tmp_path = path.with_file_name(&new_name); - let tmp_file = File::options() - .create(true) - .write(true) - .truncate(true) - .custom_flags(libc::O_EXCL | libc::O_CLOEXEC) - .open(&tmp_path)?; - - let mut writer = BufWriter::new(tmp_file); - for line in final_lines.iter() { - writer.write_all(line.text().as_bytes())?; - writer.write_all(b"\n")?; - } - writer.flush()?; - drop(writer); - // Rename the temporary file to the new file, which is atomic (TODO: I think). - fs_err::rename(&tmp_path, &path)?; + let data = final_lines + .iter() + .map(SourceLine::text_bytes_ref) + .pipe(|iterator| Itertools::intersperse(iterator, b"\n")); + replace_file(&path, data)?; // Finally, update state. self.lines.get().unwrap().replace(final_lines); @@ -190,74 +222,6 @@ impl SourceFile { Ok(()) } - pub fn insert_line(&mut self, at: Line, text: Arc) -> Result<(), IoError> { - self.lines()?; - let path = self.path(); - - //let lines = Arc::get_mut(&mut self.lines).unwrap().get_mut().unwrap(); - let lines_guard = self.lines.get().unwrap().borrow(); - let lines = &*lines_guard; - let first_half = lines.iter().take(at.index() as usize).map(SourceLine::text); - let second_half = lines.iter().skip(at.index() as usize).map(SourceLine::text); - - let new_lines: Vec = first_half - .chain(iter::once(Arc::clone(&text))) - .chain(second_half) - .enumerate() - .map(|(idx, text)| SourceLine { - line: Line::from_index(idx as u64), - text, - path: Arc::clone(&path), - }) - .collect(); - - if cfg!(debug_assertions) { - assert_eq!(new_lines.len(), lines.len() + 1); - let newly = new_lines.get(at.index() as usize); - assert_eq!(newly.map(SourceLine::text), Some(text)); - - // Assert lines are continuous. - let linenrs: Vec = new_lines - .iter() - .map(|source_line| source_line.line) - .collect(); - assert!(linenrs.is_sorted()); - } - - // Write it to a file in the same directory. - let new_name: OsString = [ - // foo - path.file_name().unwrap(), - OsStr::new(".tmp"), - ] - .into_iter() - .collect::(); - let tmp_path = path.with_file_name(&new_name); - let tmp_file = File::options() - .create(true) - .write(true) - .truncate(true) - .custom_flags(libc::O_EXCL | libc::O_CLOEXEC) - .open(&tmp_path)?; - - let mut writer = BufWriter::new(tmp_file); - for line in new_lines.iter() { - writer.write_all(line.text().as_bytes())?; - writer.write_all(b"\n")?; - } - writer.flush()?; - drop(writer); - // Rename the temporary file to the new file, which is atomic (TODO: I think). - fs_err::rename(&tmp_path, &path)?; - - drop(lines_guard); - let mut lines_guard = self.lines.get().unwrap().borrow_mut(); - // Finally, update state. - let _old_lines = mem::replace(&mut *lines_guard, new_lines); - - Ok(()) - } - pub fn path(&self) -> Arc { Arc::clone(&self.path) } 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/basic/configuration.nix b/tests/basic/configuration.nix new file mode 100644 index 0000000..06470e3 --- /dev/null +++ b/tests/basic/configuration.nix @@ -0,0 +1,69 @@ +{ pkgs, lib, config, modulesPath, ... }: +let + name = config.networking.hostName; + nixosLibPath = (modulesPath + "/../lib"); + moduleList = import (modulesPath + "/module-list.nix"); + + optionalPath = p: lib.optional (builtins.pathExists p) p; +in +assert builtins.pathExists nixosLibPath; +builtins.seq lib +builtins.seq modulesPath +builtins.seq moduleList +{ + imports = moduleList ++ [ + (modulesPath + "/testing/test-instrumentation.nix") + ] ++ lib.concatLists [ + (optionalPath ./hardware-configuration.nix) + (optionalPath ./dynamicism) + (optionalPath ../../modules/dynamicism) + ]; + + system.switch.enable = true; + documentation.enable = false; + + networking.hostName = "machine"; + + boot.loader.grub = { + enable = true; + device = "/dev/vda"; + forceInstall = true; + }; + + nix = { + package = pkgs.lixPackageSets.latest.lix; + nixPath = [ "nixpkgs=${pkgs.path}" ]; + + settings = { + experimental-features = [ "nix-command" "pipe-operator" ]; + substituters = lib.mkForce [ ]; + hashed-mirrors = null; + connect-timeout = 1; + }; + }; + + services.gotosocial = { + enable = true; + setupPostgresqlDB = true; + settings = { + application-name = "gotosocial-for-${name}"; + host = "${name}.local"; + }; + }; + + dynamicism.for.gotosocial.enable = true; + + environment.pathsToLink = [ "/share" ]; + 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/basic/default.nix b/tests/basic/default.nix new file mode 100644 index 0000000..7396103 --- /dev/null +++ b/tests/basic/default.nix @@ -0,0 +1,7 @@ +/** + * Convenience shortcut for running this test from the command-line. + * Normally this test is initialized from /tests/default.nix. + */ +{ + pkgs ? import { }, +}: pkgs.testers.runNixOSTest ./test.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 new file mode 100644 index 0000000..bc4beef --- /dev/null +++ b/tests/default.nix @@ -0,0 +1,8 @@ +{ + pkgs ? import { }, + lib ? pkgs.lib, +}: lib.makeScope lib.callPackageWith (self: let + inherit (pkgs.testers) runNixOSTest; +in { + basic = runNixOSTest ./basic/test.nix; +})