Compare commits

...

9 commits

Author SHA1 Message Date
Qyriad
447ae19b3c tests for PoC! 2026-02-08 18:47:43 +01:00
Qyriad
15ed56d8ad PoC part 2 2026-02-03 22:31:32 +01:00
Qyriad
0580ad02bd refactor assertions 2026-02-03 18:37:16 +01:00
Qyriad
fe8d00b2c2 move nixlang impls to their own directory tree 2026-02-03 16:22:25 +01:00
Qyriad
a06790a2af add tracing 2026-02-02 17:43:44 +01:00
Qyriad
9ae0630db4 factor out do_append in prep for delta subcommands 2026-02-02 17:12:25 +01:00
Qyriad
8477c73067 rename to dynix 2026-02-02 11:35:14 +01:00
Qyriad
80ff0b36cb don't require existing override 2026-02-02 10:33:25 +01:00
Qyriad
7bce1e7a6e handle relative paths without leading ./ 2026-01-29 13:53:05 +01:00
25 changed files with 1375 additions and 282 deletions

427
Cargo.lock generated
View file

@ -2,6 +2,15 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 4 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]] [[package]]
name = "anstream" name = "anstream"
version = "0.6.21" version = "0.6.21"
@ -38,7 +47,7 @@ version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [ dependencies = [
"windows-sys", "windows-sys 0.61.2",
] ]
[[package]] [[package]]
@ -49,19 +58,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"once_cell_polyfill", "once_cell_polyfill",
"windows-sys", "windows-sys 0.61.2",
]
[[package]]
name = "append-override"
version = "0.1.0"
dependencies = [
"clap",
"command-error",
"fs-err",
"libc",
"serde",
"serde_json",
] ]
[[package]] [[package]]
@ -152,12 +149,45 @@ version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d0881ea181b1df73ff77ffaaf9c7544ecc11e82fba9b5f27b262a3c73a332555" 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]] [[package]]
name = "equivalent" name = "equivalent"
version = "1.0.2" version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" 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]] [[package]]
name = "fs-err" name = "fs-err"
version = "3.2.2" version = "3.2.2"
@ -179,6 +209,12 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.13.0" version = "2.13.0"
@ -189,24 +225,86 @@ dependencies = [
"hashbrown", "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]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.2" version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]]
name = "lazy_static"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.180" version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" 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]] [[package]]
name = "memchr" name = "memchr"
version = "2.7.6" version = "2.7.6"
@ -225,6 +323,15 @@ dependencies = [
"libc", "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]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.21.3" version = "1.21.3"
@ -237,6 +344,39 @@ version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" 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]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.16" version = "0.2.16"
@ -273,6 +413,51 @@ dependencies = [
"proc-macro2", "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]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@ -316,18 +501,58 @@ dependencies = [
"zmij", "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]] [[package]]
name = "shell-words" name = "shell-words"
version = "1.1.1" version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77" 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]] [[package]]
name = "strsim" name = "strsim"
version = "0.11.1" version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" 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]] [[package]]
name = "syn" name = "syn"
version = "2.0.114" version = "2.0.114"
@ -339,6 +564,43 @@ dependencies = [
"unicode-ident", "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]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
@ -368,6 +630,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
dependencies = [ dependencies = [
"once_cell", "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]] [[package]]
@ -376,6 +683,18 @@ version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 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]] [[package]]
name = "utf8-command" name = "utf8-command"
version = "1.0.1" version = "1.0.1"
@ -388,6 +707,12 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "valuable"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "windows" name = "windows"
version = "0.61.3" version = "0.61.3"
@ -496,6 +821,15 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "windows-sys" name = "windows-sys"
version = "0.61.2" version = "0.61.2"
@ -505,6 +839,23 @@ dependencies = [
"windows-link 0.2.1", "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]] [[package]]
name = "windows-threading" name = "windows-threading"
version = "0.1.0" version = "0.1.0"
@ -514,6 +865,54 @@ dependencies = [
"windows-link 0.1.3", "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]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.16" version = "1.0.16"

View file

@ -1,20 +1,25 @@
[package] [package]
name = "append-override" name = "dynix"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[[bin]] [[bin]]
name = "append-override" name = "dynix"
path = "src/main.rs" path = "src/main.rs"
[lib] [lib]
name = "append_override" name = "dynix"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
clap = { version = "4.5.54", features = ["color", "derive"] } clap = { version = "4.5.54", features = ["color", "derive"] }
command-error = "0.8.0" command-error = "0.8.0"
fs-err = "3.2.2" fs-err = "3.2.2"
itertools = "0.14.0"
libc = { version = "0.2.180", features = ["extra_traits"] } libc = { version = "0.2.180", features = ["extra_traits"] }
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
tap = "1.0.1"
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"] }

36
configuration.nix Normal file
View file

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

View file

@ -6,15 +6,70 @@
}: let }: let
inherit (qpkgs) lib; inherit (qpkgs) lib;
PKGNAME = qpkgs.callPackage ./package.nix { }; dynix = qpkgs.callPackage ./package.nix { };
byStdenv = lib.mapAttrs (stdenvName: stdenv: let byStdenv = lib.mapAttrs (stdenvName: stdenv: let
withStdenv = PKGNAME.override { inherit stdenv; }; withStdenv = dynix.override { inherit stdenv; };
PKGNAME' = withStdenv.overrideAttrs (prev: { dynix' = withStdenv.overrideAttrs (prev: {
pname = "${prev.pname}-${stdenvName}"; pname = "${prev.pname}-${stdenvName}";
}); });
in PKGNAME') qpkgs.validStdenvs; in dynix') qpkgs.validStdenvs;
in PKGNAME.overrideAttrs (prev: lib.recursiveUpdate prev { evalNixos = import (pkgs.path + "/nixos");
passthru = { inherit byStdenv; };
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";
};
};
}) })

View file

@ -21,21 +21,21 @@
qpkgs = import qyriad-nur { inherit pkgs; }; qpkgs = import qyriad-nur { inherit pkgs; };
inherit (qpkgs) lib; inherit (qpkgs) lib;
PKGNAME = import ./default.nix { inherit pkgs qpkgs; }; dynix = import ./default.nix { inherit pkgs qpkgs; };
extraVersions = lib.mapAttrs' (stdenvName: value: { extraVersions = lib.mapAttrs' (stdenvName: value: {
name = "${stdenvName}-PKGNAME"; name = "${stdenvName}-dynix";
inherit value; 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: { extraDevShells = lib.mapAttrs' (stdenvName: value: {
name = "${stdenvName}-PKGNAME"; name = "${stdenvName}-dynix";
inherit value; inherit value;
}) PKGNAME.byStdenv; }) dynix.byStdenv;
in { in {
packages = extraVersions // { packages = extraVersions // {
default = PKGNAME; default = dynix;
inherit PKGNAME; inherit dynix;
}; };
devShells = extraDevShells // { devShells = extraDevShells // {

View file

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

View file

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

View file

@ -0,0 +1,24 @@
{
lib ? import <nixpkgs/lib>,
}: 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;
})

View file

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

4
modules/tests-common.nix Normal file
View file

@ -0,0 +1,4 @@
{ pkgs, ... }:
{
nix.package = pkgs.lixPackageSets.latest.lix;
}

76
modules/tests-main.py Normal file
View file

@ -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 <nixpkgs/nixos> {{ 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=}")

52
modules/tests.nix Normal file
View file

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

View file

@ -81,6 +81,6 @@ in stdenv.mkDerivation (self: {
}); });
meta = { meta = {
mainProgram = "PKGNAME"; mainProgram = "dynix";
}; };
})) }))

View file

@ -3,15 +3,21 @@
qpkgs ? let qpkgs ? let
src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz"; src = fetchTarball "https://github.com/Qyriad/nur-packages/archive/main.tar.gz";
in import src { inherit pkgs; }, in import src { inherit pkgs; },
PKGNAME ? import ./default.nix { inherit pkgs qpkgs; }, dynix ? import ./default.nix { inherit pkgs qpkgs; },
}: let }: let
inherit (pkgs) lib; inherit (pkgs) lib;
mkDevShell = PKGNAME: qpkgs.callPackage PKGNAME.mkDevShell { }; mkDevShell = dynix: qpkgs.callPackage dynix.mkDevShell { };
devShell = mkDevShell PKGNAME; 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 { in devShell.overrideAttrs (prev: lib.recursiveUpdate prev {
passthru = { inherit byStdenv; }; passthru = { inherit byStdenv; };
env.PYTHONPATH = [
"${pkgs.python3Packages.beartype}/${pkgs.python3.sitePackages}"
] |> lib.concatStringsSep ":";
packages = prev.packages or [ ] ++ [
pkgs.python3Packages.beartype
];
}) })

View file

@ -1,3 +1,5 @@
use std::sync::Arc;
use clap::ColorChoice; use clap::ColorChoice;
use crate::prelude::*; use crate::prelude::*;
@ -47,18 +49,39 @@ impl FromStr for NixOsOption {
} }
#[derive(Debug, Clone, PartialEq, clap::Parser)] #[derive(Debug, Clone, PartialEq, clap::Parser)]
#[command(version, about, author, arg_required_else_help(true))] pub struct AppendCmd {
pub struct Parser { #[arg(required = true)]
#[arg(long, default_value = "auto")] pub name: Arc<str>,
#[arg(required = true)]
pub value: Arc<str>,
}
#[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, pub color: ColorChoice,
#[arg(long)] // FIXME: default to /etc/configuration.nix, or something?
pub file: Box<OsStr>, #[arg(long, global(true), default_value = "./configuration.nix")]
pub file: Arc<OsStr>,
#[arg(required = true)] #[command(subcommand)]
pub name: Box<str>, pub subcommand: Subcommand,
#[arg(required = true)] }
pub value: Box<str>,
///// Flakeref to a base configuration to modify. ///// Flakeref to a base configuration to modify.
//#[arg(group = "config", long, default_value("."))] //#[arg(group = "config", long, default_value("."))]
//#[arg(long, default_value(Some(".")))] //#[arg(long, default_value(Some(".")))]
@ -66,7 +89,6 @@ pub struct Parser {
// //
//#[arg(group = "config", long)] //#[arg(group = "config", long)]
//expr: Option<String>, //expr: Option<String>,
}
//impl Parser { //impl Parser {
// fn eval_cmd(&self) { // fn eval_cmd(&self) {

View file

@ -6,10 +6,14 @@ use std::{
#[allow(unused_imports)] #[allow(unused_imports)]
use crate::prelude::*; use crate::prelude::*;
pub static CLI_ENABLE_COLOR: OnceLock<bool> = OnceLock::new(); /// The actual, final value for whether color should be used, based on CLI and environment values.
pub static SHOULD_COLOR: LazyLock<bool> = 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<bool> = OnceLock::new();
fn is_color_reqd() -> bool { 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 { fn is_clicolor_forced() -> bool {
@ -24,8 +28,6 @@ fn is_clicolor_forced() -> bool {
.unwrap_or(false) .unwrap_or(false)
} }
pub static SHOULD_COLOR: LazyLock<bool> = LazyLock::new(|| is_clicolor_forced() || is_color_reqd());
/// Silly wrapper around LazyLock<&'static str> to impl Display. /// Silly wrapper around LazyLock<&'static str> to impl Display.
pub(crate) struct _LazyLockDisplay(LazyLock<&'static str>); pub(crate) struct _LazyLockDisplay(LazyLock<&'static str>);
impl Display for _LazyLockDisplay { 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(|| { pub(crate) const ANSI_RESET: _LazyLockDisplay = _LazyLockDisplay(LazyLock::new(|| {
SHOULD_COLOR SHOULD_COLOR.then_some("\x1b[0m").unwrap_or_default()
// C'mon rustfmt, just format it to match ^.
.then_some("\x1b[0m")
.unwrap_or_default()
})); }));

View file

@ -1,4 +1,4 @@
use std::sync::{Arc, LazyLock}; use std::{iter, sync::Arc};
pub(crate) mod prelude { pub(crate) mod prelude {
#![allow(unused_imports)] #![allow(unused_imports)]
@ -25,14 +25,18 @@ pub(crate) mod prelude {
pub use fs_err::File; pub use fs_err::File;
#[cfg(unix)] #[cfg(unix)]
pub use fs_err::os::unix::fs::{FileExt, OpenOptionsExt}; 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::*; use prelude::*;
pub mod args; pub mod args;
pub use args::Parser; pub use args::{AppendCmd, Args, DeltaCmd};
mod color; mod color;
pub use color::{CLI_ENABLE_COLOR, SHOULD_COLOR}; pub use color::{_CLI_ENABLE_COLOR, SHOULD_COLOR};
pub mod line; pub mod line;
mod nixcmd; mod nixcmd;
pub use line::Line; pub use line::Line;
@ -45,23 +49,67 @@ use crate::source::SourceFile;
pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' ']; pub const ASCII_WHITESPACE: &[char] = &['\t', '\n', '\x0C', '\r', ' '];
#[tracing::instrument(level = "debug")]
pub fn do_delta(args: Arc<Args>, delta_args: DeltaCmd) -> Result<(), BoxDynError> {
todo!();
}
#[tracing::instrument(level = "debug")]
pub fn do_append(args: Arc<Args>, 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)] #[derive(Debug, Clone, PartialEq, Hash, Serialize, Deserialize)]
pub struct DefinitionWithLocation { pub struct DefinitionWithLocation {
pub file: Box<Path>, pub file: Box<Path>,
pub value: Box<str>, pub value: Box<serde_json::Value>,
} }
pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result<Box<Path>, BoxDynError> { pub fn expr_for_configuration(source_file: &Path) -> OsString {
let expr: OsString = [ [
// foo
OsStr::new("import <nixpkgs/nixos> { configuration = "), OsStr::new("import <nixpkgs/nixos> { configuration = "),
configuration_nix.as_os_str(), source_file.as_os_str(),
OsStr::new("; }"), OsStr::new("; }"),
] ]
.into_iter() .into_iter()
.map(ToOwned::to_owned) .collect()
.collect(); }
pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result<Box<Path>, BoxDynError> {
let expr = expr_for_configuration(configuration_nix);
let attrpath = format!("options.{}.definitionsWithLocations", option_name); let attrpath = format!("options.{}.definitionsWithLocations", option_name);
let output = nixcmd::NixEvalExpr { expr, attrpath } let output = nixcmd::NixEvalExpr { expr, attrpath }
@ -75,19 +123,10 @@ pub fn get_where(option_name: &str, configuration_nix: &Path) -> Result<Box<Path
Ok(Box::from(last_location.file)) Ok(Box::from(last_location.file))
} }
pub fn get_highest_prio( pub fn get_highest_prio(option_name: &str, source: SourceFile) -> Result<i64, BoxDynError> {
option_name: &str,
source: SourceFile,
) -> Result<(i64, SourceLine), BoxDynError> {
// Get the current highest priority. // Get the current highest priority.
let expr: OsString = [ let expr = expr_for_configuration(&source.path());
OsStr::new("import <nixpkgs/nixos> { configuration = "),
source.path().as_os_str(),
OsStr::new("; }"),
]
.into_iter()
.collect();
// Get the highest priority, and the file its defined in. // Get the highest priority, and the file its defined in.
let attrpath = format!("options.{}.highestPrio", option_name); let attrpath = format!("options.{}.highestPrio", option_name);
@ -97,93 +136,44 @@ pub fn get_highest_prio(
let stdout = output.stdout(); let stdout = output.stdout();
let highest_prio = i64::from_str(stdout.trim())?; let highest_prio = i64::from_str(stdout.trim())?;
let needle = format!("lib.mkOverride ({})", highest_prio); Ok(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()))
} }
pub fn get_next_prio_line( pub fn get_next_prio_line(
source: SourceFile, source: SourceFile,
option_name: Arc<str>, option_name: Arc<str>,
last_line_def: SourceLine,
new_prio: i64, new_prio: i64,
new_value: Arc<str>, new_value: Arc<str>,
) -> Result<SourceLine, BoxDynError> { ) -> Result<SourceLine, BoxDynError> {
if !last_line_def.text.ends_with(';') { let source_lines = source.lines()?;
todo!(); let last_line = source_lines.last();
} assert_eq!(last_line.map(SourceLine::text).as_deref(), Some("]"));
let next_line = source.line(last_line_def.line.next())?; let last_line = last_line.unwrap();
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 new_line = SourceLine { let new_line = SourceLine {
line: next_line.line.next(), line: last_line.line,
path: source.path(), path: source.path(),
text: Arc::from(new_text), text: Arc::from(format!(
" {option_name} = lib.mkOverride ({new_prio}) ({new_value});",
)),
}; };
Ok(new_line) Ok(new_line)
} }
pub fn write_next_prio( pub fn write_next_prio(mut source: SourceFile, new_line: SourceLine) -> Result<(), BoxDynError> {
mut source: SourceFile,
last_line_def: SourceLine,
new_text: Arc<str>,
) -> 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();
let new_mod_start = SourceLine { let new_mod_start = SourceLine {
line: last_line_def.line.next(), line: new_line.line.prev(),
path: source.path(), path: source.path(),
text: open_brace_line, text: Arc::from(" {"),
};
let new_line = SourceLine {
line: new_mod_start.line.next(),
path: source.path(),
text: Arc::from(new_text),
}; };
let new_mod_end = SourceLine { let new_mod_end = SourceLine {
line: new_line.line.next(), line: new_line.line.next(),
path: source.path(), path: source.path(),
text: close_brace_line, text: Arc::from(" }"),
}; };
dbg!(&new_mod_start.text()); source.insert_lines(&[new_mod_start, new_line, new_mod_end])?;
source.insert_lines(&[
new_mod_start,
new_line,
new_mod_end,
])?;
//source.insert_line(new_line.line, new_line.text())?;
Ok(()) Ok(())
} }

View file

@ -1,17 +1,16 @@
use std::io::{self, IsTerminal}; use std::io::{self, IsTerminal};
use std::path::Path;
use std::process::ExitCode; use std::process::ExitCode;
use std::{error::Error as StdError, sync::Arc}; use std::{error::Error as StdError, sync::Arc};
use append_override::source::SourceFile;
use clap::{ColorChoice, Parser as _}; use clap::{ColorChoice, Parser as _};
use fs_err::File; use tracing_human_layer::HumanLayer;
use fs_err::os::unix::fs::OpenOptionsExt; use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, layer::SubscriberExt};
fn main_wrapped() -> Result<(), Box<dyn StdError + Send + Sync + 'static>> { fn main_wrapped() -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
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::Always => true,
ColorChoice::Auto => io::stdin().is_terminal(), ColorChoice::Auto => io::stdin().is_terminal(),
ColorChoice::Never => false, ColorChoice::Never => false,
@ -20,35 +19,20 @@ fn main_wrapped() -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
success.expect("logic error in CLI_ENABLE_COLOR"); success.expect("logic error in CLI_ENABLE_COLOR");
} }
// FIXME: handle relative paths without leading ./ tracing_subscriber::registry()
let filepath = Path::new(&args.file); .with(HumanLayer::new().with_color_output(*dynix::SHOULD_COLOR))
.with(EnvFilter::from_default_env())
.init();
// Get what file that thing is defined in. tracing::debug!("Parsed command-line arguments: {args:?}");
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)?;
let (pri, last_def_line) = append_override::get_highest_prio(&args.name, source_file.clone())?; {
let new_pri = pri - 1; use dynix::args::Subcommand::*;
match &args.subcommand {
eprintln!("{last_def_line}"); Append(append_args) => dynix::do_append(args.clone(), append_args.clone())?,
Delta(delta_args) => dynix::do_delta(args.clone(), delta_args.clone())?,
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())?;
Ok(()) Ok(())
} }
@ -57,7 +41,7 @@ fn main() -> ExitCode {
match main_wrapped() { match main_wrapped() {
Ok(_) => ExitCode::SUCCESS, Ok(_) => ExitCode::SUCCESS,
Err(e) => { Err(e) => {
eprintln!("append-override: error: {}", e); eprintln!("dynix: error: {}", e);
ExitCode::FAILURE ExitCode::FAILURE
} }
} }

View file

@ -1,10 +1,10 @@
use std::{ use std::{
cell::{Cell, Ref, RefCell}, cell::{Ref, RefCell},
hash::Hash, hash::Hash,
io::{BufRead, BufReader, BufWriter}, io::{BufRead, BufReader, BufWriter},
iter, mem, ops::Deref,
ops::{Deref, DerefMut}, ptr,
sync::{Arc, Mutex, MutexGuard, OnceLock, PoisonError, RwLock}, sync::{Arc, Mutex, OnceLock},
}; };
use crate::Line; use crate::Line;
@ -13,6 +13,33 @@ use crate::color::{ANSI_CYAN, ANSI_GREEN, ANSI_MAGENTA, ANSI_RESET};
use crate::prelude::*; use crate::prelude::*;
use fs_err::OpenOptions; use fs_err::OpenOptions;
use itertools::Itertools;
pub fn replace_file<'a>(
path: &Path,
contents: impl IntoIterator<Item = &'a [u8]>,
) -> 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)] #[derive(Debug, Clone, PartialEq, Hash)]
pub struct SourceLine { pub struct SourceLine {
@ -25,6 +52,30 @@ impl SourceLine {
pub fn text(&self) -> Arc<str> { pub fn text(&self) -> Arc<str> {
Arc::clone(&self.text) 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<Path> {
Arc::clone(&self.path)
}
} }
impl Display for SourceLine { impl Display for SourceLine {
@ -39,11 +90,6 @@ impl Display for SourceLine {
} }
} }
#[derive(Debug, Clone, PartialEq, Hash)]
pub struct SourcePath {
path: Arc<Path>,
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct SourceFile { pub struct SourceFile {
path: Arc<Path>, path: Arc<Path>,
@ -56,6 +102,11 @@ pub struct SourceFile {
impl SourceFile { impl SourceFile {
/// Panics if `path` is a directory path instead of a file path. /// Panics if `path` is a directory path instead of a file path.
pub fn open_from(path: Arc<Path>, options: OpenOptions) -> Result<Self, IoError> { pub fn open_from(path: Arc<Path>, options: OpenOptions) -> Result<Self, IoError> {
trace!(
"SourceFile::open_from(path={:?}, options={:?})",
path,
options.options(),
);
assert!(path.file_name().is_some()); assert!(path.file_name().is_some());
let file = Arc::new(Mutex::new(options.open(&*path)?)); let file = Arc::new(Mutex::new(options.open(&*path)?));
@ -126,6 +177,7 @@ impl SourceFile {
if new_lines.is_empty() { if new_lines.is_empty() {
return Ok(()); 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)); 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 cur_lines = self.lines()?;
let first_half = cur_lines let first_half = cur_lines
.iter() .iter()
.take(new_lines.last().unwrap().line.prev().index() as usize) .take(num_lines_before_new)
.map(SourceLine::text); .map(SourceLine::text);
let middle = new_lines.iter().map(SourceLine::text); let middle = new_lines.iter().map(SourceLine::text);
let second_half = cur_lines let second_half = cur_lines
.iter() .iter()
.skip(new_lines.last().unwrap().line.prev().index() as usize) .skip(num_lines_before_new)
.map(SourceLine::text); .map(SourceLine::text);
let final_lines: Vec<SourceLine> = first_half let final_lines: Vec<SourceLine> = first_half
@ -158,31 +210,11 @@ impl SourceFile {
drop(cur_lines); drop(cur_lines);
// Write it to a file in the same directory. let data = final_lines
let new_name: OsString = [ .iter()
// foo .map(SourceLine::text_bytes_ref)
path.file_name().unwrap(), .pipe(|iterator| Itertools::intersperse(iterator, b"\n"));
OsStr::new(".tmp"), replace_file(&path, data)?;
]
.into_iter()
.collect::<OsString>();
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)?;
// Finally, update state. // Finally, update state.
self.lines.get().unwrap().replace(final_lines); self.lines.get().unwrap().replace(final_lines);
@ -190,74 +222,6 @@ impl SourceFile {
Ok(()) Ok(())
} }
pub fn insert_line(&mut self, at: Line, text: Arc<str>) -> 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<SourceLine> = 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<Line> = 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::<OsString>();
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<Path> { pub fn path(&self) -> Arc<Path> {
Arc::clone(&self.path) Arc::clone(&self.path)
} }

View file

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

View file

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

7
tests/basic/default.nix Normal file
View file

@ -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 <nixpkgs> { },
}: pkgs.testers.runNixOSTest ./test.nix

View file

@ -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 <nixpkgs/nixos> {{ }};
in nixos.config.dynamicism.doChange {{
option = "services.gotosocial.settings.application-name";
value = "{new_app_name}";
}}
""").strip()
machine.succeed(rf"""
nix run --show-trace --log-format raw-with-logs --impure -E {shlex.quote(expr)}
""".strip())
config_file_new = get_config_file()
lines = config_file_new.splitlines()
application_name = next(line for line in lines if line.startswith("application-name:"))
assert new_app_name in application_name, f"'{new_app_name}' should be in {application_name=}"

43
tests/basic/test.nix Normal file
View file

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

8
tests/default.nix Normal file
View file

@ -0,0 +1,8 @@
{
pkgs ? import <nixpkgs> { },
lib ? pkgs.lib,
}: lib.makeScope lib.callPackageWith (self: let
inherit (pkgs.testers) runNixOSTest;
in {
basic = runNixOSTest ./basic/test.nix;
})