Compare commits
204
Commits
sb/jade/coverage
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1e48ac50ca | ||
|
|
3dd53ecdef | ||
|
|
289fab6a68 | ||
|
|
f5b9f105e2 | ||
|
|
57fb4f534d | ||
|
|
553ae61ffd | ||
|
|
33d0984e27 | ||
|
|
93ca179023 | ||
|
|
ece46d54c7 | ||
|
|
4c4d8b8ccd | ||
|
|
f91bdc9367 | ||
|
|
25d47498bd | ||
|
|
99c6ad45b5 | ||
|
|
0e180a479f | ||
|
|
7307475218 | ||
|
|
66090810f1 | ||
|
|
9bd9947f41 | ||
|
|
25f53b755c | ||
|
|
f51148a75b | ||
|
|
e6ddca402b | ||
|
|
c739701077 | ||
|
|
734cfd1d48 | ||
|
|
fca58aa044 | ||
|
|
782d967342 | ||
|
|
08f18285ef | ||
|
|
c6d22874d6 | ||
|
|
1657d0eb47 | ||
|
|
527ecba696 | ||
|
|
72f8000b8f | ||
|
|
8294860ccf | ||
|
|
3b286bbd62 | ||
|
|
28fc206387 | ||
|
|
64c486e22c | ||
|
|
1a2853bf7b | ||
|
|
81084c7fc9 | ||
|
|
41bad096e3 | ||
|
|
dd8f1843cc | ||
|
|
9302845601 | ||
|
|
523df65fba | ||
|
|
de67d1424d | ||
|
|
405ff2eb03 | ||
|
|
c1370bc81c | ||
|
|
12f10fca7e | ||
|
|
b62b37296c | ||
|
|
abeefa0e4f | ||
|
|
4126e671cc | ||
|
|
ad199c4723 | ||
|
|
e9a01a5be6 | ||
|
|
48f7747deb | ||
|
|
2c45e056e9 | ||
|
|
d4e445f251 | ||
|
|
43c5aa2076 | ||
|
|
dbf1ef3d42 | ||
|
|
325364fc26 | ||
|
|
cde71e45a1 | ||
|
|
1705515e79 | ||
|
|
d26a73bfe6 | ||
|
|
cf1e565a0a | ||
|
|
a92e4b5c42 | ||
|
|
418918b599 | ||
|
|
6e19a940b1 | ||
|
|
57c00a20c5 | ||
|
|
d0deb1a150 | ||
|
|
22e2ecdc43 | ||
|
|
5434ddf4cd | ||
|
|
7ef274600d | ||
|
|
12c1132f07 | ||
|
|
d5044f0016 | ||
|
|
2605d3eab1 | ||
|
|
567348a90c | ||
|
|
1c2b5d4b5a | ||
|
|
c5bcb9a312 | ||
|
|
91663cca2d | ||
|
|
d9ecb5301e | ||
|
|
9e846fbaea | ||
|
|
84fb93a009 | ||
|
|
7868f52c2a | ||
|
|
fc6772a7dd | ||
|
|
fce63f6dbf | ||
|
|
de68de1d11 | ||
|
|
071b810f95 | ||
|
|
63fb295ad8 | ||
|
|
2ea4b5d9e1 | ||
|
|
8902b78442 | ||
|
|
30ca2e38c4 | ||
|
|
3f7bc8ffb8 | ||
|
|
20453629ab | ||
|
|
0cddd2d38a | ||
|
|
10845bfe63 | ||
|
|
05e9ca3e26 | ||
|
|
ff920956a9 | ||
|
|
cf0ce785b7 | ||
|
|
d748b4e509 | ||
|
|
0d6b372c19 | ||
|
|
7bf0edcc0d | ||
|
|
4b0371bff3 | ||
|
|
c019a776e9 | ||
|
|
93c872ab25 | ||
|
|
2ea51a6c71 | ||
|
|
fe79d62b73 | ||
|
|
9614f12908 | ||
|
|
bc8a95457f | ||
|
|
ba3a026f40 | ||
|
|
12baca7bea | ||
|
|
abfe9d0c3e | ||
|
|
64b434b87b | ||
|
|
da9f0c494e | ||
|
|
a50b85b7d6 | ||
|
|
cb4703019b | ||
|
|
ab8cdcc9b8 | ||
|
|
f7ce20d879 | ||
|
|
ab149a6cd8 | ||
|
|
99175779c4 | ||
|
|
a4634bca67 | ||
|
|
c6fdf7d843 | ||
|
|
5d91fcba5f | ||
|
|
28a8c5c42a | ||
|
|
868ec73e16 | ||
|
|
1a1234cf68 | ||
|
|
daa2bc82bc | ||
|
|
0cad504c8a | ||
|
|
1396012a03 | ||
|
|
267a8e7c9b | ||
|
|
2698fe8611 | ||
|
|
d5cf115a3c | ||
|
|
4f0214b4bc | ||
|
|
6cd8fcd9d8 | ||
|
|
e108432cb7 | ||
|
|
c13ca9adcf | ||
|
|
82d23e8f1b | ||
|
|
1c70995b25 | ||
|
|
4931a5f10c | ||
|
|
9b2a19530c | ||
|
|
9fe22f4e5e | ||
|
|
52449f3f8e | ||
|
|
eb4997592d | ||
|
|
8d581f42d9 | ||
|
|
79d6056d26 | ||
|
|
13bcd07385 | ||
|
|
1b9570162e | ||
|
|
003946f062 | ||
|
|
c64fbcbdb2 | ||
|
|
eef57410d7 | ||
|
|
8edb857248 | ||
|
|
b113be5f13 | ||
|
|
720306b693 | ||
|
|
f6cfd6b3ea | ||
|
|
839ae964fb | ||
|
|
e5ca862ea9 | ||
|
|
1f100371d7 | ||
|
|
b118785864 | ||
|
|
1c81bd926d | ||
|
|
b34f4d7b17 | ||
|
|
1971824580 | ||
|
|
7831c98a4d | ||
|
|
cc271148b0 | ||
|
|
2b409ad322 | ||
|
|
7cdda9c5fa | ||
|
|
481fc30ae7 | ||
|
|
d0a4b55a0e | ||
|
|
b2c95d41c9 | ||
|
|
010d062926 | ||
|
|
af4d064be7 | ||
|
|
7edb421e1b | ||
|
|
de3a2451eb | ||
|
|
4da9a53b66 | ||
|
|
8ab7547a74 | ||
|
|
2f7d7444f1 | ||
|
|
c595477002 | ||
|
|
c71d2ebace | ||
|
|
65346101a2 | ||
|
|
0d2c48a797 | ||
|
|
078bbe6171 | ||
|
|
cd573beb0a | ||
|
|
d94bb49cf4 | ||
|
|
076d1aa4cd | ||
|
|
8782c7533f | ||
|
|
7017def185 | ||
|
|
e70ae36b3f | ||
|
|
89f5974125 | ||
|
|
faaa14a303 | ||
|
|
f8b13b254b | ||
|
|
68834b7841 | ||
|
|
0c63036c7d | ||
|
|
de212be82b | ||
|
|
bb8bc31e58 | ||
|
|
be34bc0481 | ||
|
|
eeac529d5e | ||
|
|
47c9f73d36 | ||
|
|
84347f94da | ||
|
|
cda9e01053 | ||
|
|
c40afdea73 | ||
|
|
dda519baff | ||
|
|
af99ede1fd | ||
|
|
0a09782cc8 | ||
|
|
e3ee75633e | ||
|
|
d505a477a4 | ||
|
|
f006b41a3c | ||
|
|
a5205265c0 | ||
|
|
23b2797321 | ||
|
|
0e971cddee | ||
|
|
5afda956a7 | ||
|
|
ae4b3d585d | ||
|
|
62b728e766 |
@@ -0,0 +1,14 @@
|
||||
[target.'cfg(true)']
|
||||
rustflags = [
|
||||
# rustc will pass `-nodefaultlibs` without this, but we need the C++ standard library.
|
||||
'-Cdefault-linker-libraries=yes',
|
||||
]
|
||||
|
||||
[target.'cfg(target_env = "musl")']
|
||||
rustflags = [
|
||||
'-Cdefault-linker-libraries=yes',
|
||||
# musl, at least in Nixpkgs, is not compiled with -fPIE.
|
||||
# XXX: nevermind? as of Nixpkgs 26.05??
|
||||
# Oh gods do we need to gate this??
|
||||
#'-Crelocation-model=static',
|
||||
]
|
||||
+2
-1
@@ -1,4 +1,5 @@
|
||||
outputs/
|
||||
/build
|
||||
/outputs
|
||||
|
||||
# GNU Global
|
||||
GPATH
|
||||
|
||||
Generated
+878
-4
@@ -2,18 +2,320 @@
|
||||
# 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 = "allocator-api2"
|
||||
version = "0.2.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
|
||||
|
||||
[[package]]
|
||||
name = "anstream"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"anstyle-parse",
|
||||
"anstyle-query",
|
||||
"anstyle-wincon",
|
||||
"colorchoice",
|
||||
"is_terminal_polyfill",
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle"
|
||||
version = "1.0.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-parse"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
|
||||
dependencies = [
|
||||
"utf8parse",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-query"
|
||||
version = "1.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "anstyle-wincon"
|
||||
version = "3.0.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
||||
dependencies = [
|
||||
"anstyle",
|
||||
"once_cell_polyfill",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ar_archive_writer"
|
||||
version = "0.5.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4087686b4b0a3427190bae57a1d9a478dbb2d40c5dc1bd6e2b6d797913bdd348"
|
||||
dependencies = [
|
||||
"object",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ariadne"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "72fe02fc62033df9ba41cba57ee19acf5e742511a140c7dbc3a873e19a19a1bd"
|
||||
dependencies = [
|
||||
"unicode-width 0.1.14",
|
||||
"yansi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama"
|
||||
version = "0.12.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b79091df18a97caea757e28cd2d5fda49c6cd4bd01ddffd7ff01ace0c0ad2c28"
|
||||
dependencies = [
|
||||
"askama_derive",
|
||||
"askama_escape",
|
||||
"humansize",
|
||||
"num-traits",
|
||||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_derive"
|
||||
version = "0.12.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19fe8d6cb13c4714962c072ea496f3392015f0989b1a2847bb4b2d9effd71d83"
|
||||
dependencies = [
|
||||
"askama_parser",
|
||||
"basic-toml",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"serde",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "askama_escape"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "619743e34b5ba4e9703bba34deac3427c72507c7159f5fd030aea8cac0cfe341"
|
||||
|
||||
[[package]]
|
||||
name = "askama_parser"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "acb1161c6b64d1c3d83108213c2a2533a342ac225aabd0bda218278c2ddb00c0"
|
||||
dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "autocfg"
|
||||
version = "1.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
|
||||
|
||||
[[package]]
|
||||
name = "basic-toml"
|
||||
version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4388bee8683e3d04af747c73422af53102d2bd24d9eadb6cbc100baef4b43f8"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.2.65"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e228eec9be7c17ccb640b59b36a5cd805ea2a564a4c5e162c2f659fea30d3b96"
|
||||
dependencies = [
|
||||
"find-msvc-tools",
|
||||
"shlex",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
|
||||
|
||||
[[package]]
|
||||
name = "cfg_aliases"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
|
||||
|
||||
[[package]]
|
||||
name = "chumsky"
|
||||
version = "1.0.0-alpha.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0e82d74e6c83060ec269fe9e0d408d6de4a1645d525f9a0bbbb841ba4efd91ac"
|
||||
dependencies = [
|
||||
"hashbrown 0.15.5",
|
||||
"regex-automata 0.3.9",
|
||||
"serde",
|
||||
"stacker",
|
||||
"unicode-ident",
|
||||
"unicode-segmentation",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
"clap_lex",
|
||||
"strsim",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_derive"
|
||||
version = "4.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
|
||||
dependencies = [
|
||||
"heck",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clap_lex"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bde03770d3df201d4fb868f2c9c59e66a3e4e2bd06692a0fe701e7103c7e84d4"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
|
||||
|
||||
[[package]]
|
||||
name = "countme"
|
||||
version = "3.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7704b5fdd17b18ae31c4c1da5a2e0305a2bf17b5249300a9ee9ed7b72114c636"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dissimilar"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59f8e79d1fbf76bdfbde321e902714bf6c49df88a7dda6fc682fc2979226962d"
|
||||
|
||||
[[package]]
|
||||
name = "either"
|
||||
version = "1.16.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
|
||||
|
||||
[[package]]
|
||||
name = "endian-type"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "869b0adbda23651a9c5c0c3d270aac9fcb52e8622a8f2b17e57802d7791962f2"
|
||||
|
||||
[[package]]
|
||||
name = "equivalent"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
||||
|
||||
[[package]]
|
||||
name = "error-code"
|
||||
version = "3.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dea2df4cf52843e0452895c455a1a2cfbb842a1e7329671acf418fdc53ed4c59"
|
||||
|
||||
[[package]]
|
||||
name = "expect-test"
|
||||
version = "1.5.0"
|
||||
@@ -24,12 +326,139 @@ dependencies = [
|
||||
"once_cell",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
|
||||
|
||||
[[package]]
|
||||
name = "foldhash"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
|
||||
dependencies = [
|
||||
"allocator-api2",
|
||||
"equivalent",
|
||||
"foldhash",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "hashbrown"
|
||||
version = "0.17.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
|
||||
|
||||
[[package]]
|
||||
name = "heck"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
|
||||
|
||||
[[package]]
|
||||
name = "hex"
|
||||
version = "0.4.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
|
||||
|
||||
[[package]]
|
||||
name = "home"
|
||||
version = "0.5.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
|
||||
dependencies = [
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "humansize"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
|
||||
dependencies = [
|
||||
"libm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
|
||||
dependencies = [
|
||||
"equivalent",
|
||||
"hashbrown 0.17.1",
|
||||
]
|
||||
|
||||
[[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.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57"
|
||||
dependencies = [
|
||||
"either",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.186"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||
|
||||
[[package]]
|
||||
name = "libm"
|
||||
version = "0.2.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
|
||||
|
||||
[[package]]
|
||||
name = "licxxbridge"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"zngur",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lix"
|
||||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"lix-doc",
|
||||
"pkg-config",
|
||||
"regex",
|
||||
"rootcause",
|
||||
"rustyline",
|
||||
"rustyline-derive",
|
||||
"zngur",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lix-doc"
|
||||
version = "0.0.1"
|
||||
@@ -40,8 +469,87 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lixutil-rs"
|
||||
version = "0.0.0"
|
||||
name = "log"
|
||||
version = "0.4.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
|
||||
|
||||
[[package]]
|
||||
name = "memchr"
|
||||
version = "2.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "88904434abc2901f197fe8cc55f0445e7ded921dba5911dad2e2b39b48e663c4"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
|
||||
|
||||
[[package]]
|
||||
name = "nibble_vec"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77a5d83df9f36fe23f0c3648c6bbb8b0298bb5f1939c8f2704431371f4b84d43"
|
||||
dependencies = [
|
||||
"smallvec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nix"
|
||||
version = "0.31.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf20d2fde8ff38632c426f1165ed7436270b44f199fc55284c38276f9db47c3d"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nom"
|
||||
version = "7.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
"minimal-lexical",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.19"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "object"
|
||||
version = "0.37.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
@@ -49,6 +557,108 @@ version = "1.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "once_cell_polyfill"
|
||||
version = "1.70.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "pkg-config"
|
||||
version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.106"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
|
||||
dependencies = [
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psm"
|
||||
version = "0.1.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
|
||||
dependencies = [
|
||||
"ar_archive_writer",
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.45"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "radix_trie"
|
||||
version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3b4431027dcd37fc2a73ef740b5f233aa805897935b8bce0195e41bbf9a3289a"
|
||||
dependencies = [
|
||||
"endian-type",
|
||||
"nibble_vec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.12.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata 0.4.14",
|
||||
"regex-syntax 0.8.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59b23e92ee4318893fa3fe3e6fb365258efbfe6ac6ab30f090cdcbb7aa37efa9"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.7.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax 0.8.11",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da"
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "rnix"
|
||||
version = "0.12.0"
|
||||
@@ -58,6 +668,28 @@ dependencies = [
|
||||
"rowan",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rootcause"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b660d9968fae12f4e691f2b2be5d9a3a6de875300c682e8d2cb89a618dd60875"
|
||||
dependencies = [
|
||||
"hashbrown 0.17.1",
|
||||
"indexmap",
|
||||
"rootcause-internals",
|
||||
"rustc-hash 2.1.3",
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rootcause-internals"
|
||||
version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0184f6fcff3b58b7c963aee6e3cc915c04331aa6eef974f79d7d44d21e246c24"
|
||||
dependencies = [
|
||||
"triomphe",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rowan"
|
||||
version = "0.15.16"
|
||||
@@ -65,8 +697,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a542b0253fa46e632d27a1dc5cf7b930de4df8659dc6e720b647fc72147ae3d"
|
||||
dependencies = [
|
||||
"countme",
|
||||
"hashbrown",
|
||||
"rustc-hash",
|
||||
"hashbrown 0.14.5",
|
||||
"rustc-hash 1.1.0",
|
||||
"text-size",
|
||||
]
|
||||
|
||||
@@ -76,8 +708,250 @@ version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
|
||||
|
||||
[[package]]
|
||||
name = "rustc-hash"
|
||||
version = "2.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
|
||||
|
||||
[[package]]
|
||||
name = "rustyline"
|
||||
version = "18.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "53f6a737db68eb1a8ccff86b584b2fc13eca6a7bb6f78ebc7c529547e3ab9684"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cfg-if",
|
||||
"clipboard-win",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
"memchr",
|
||||
"nix",
|
||||
"radix_trie",
|
||||
"unicode-segmentation",
|
||||
"unicode-width 0.2.2",
|
||||
"utf8parse",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "rustyline-derive"
|
||||
version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64e5587417a3c4e16a4415e8d7d07f80998ed835ade621d19dfbe9fbe3205b0f"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_core"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.228"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "shlex"
|
||||
version = "2.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
||||
|
||||
[[package]]
|
||||
name = "smallvec"
|
||||
version = "1.15.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
|
||||
|
||||
[[package]]
|
||||
name = "stacker"
|
||||
version = "0.1.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"psm",
|
||||
"windows-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "2.0.118"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-ident",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "text-size"
|
||||
version = "1.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f18aa187839b2bdb1ad2fa35ead8c4c2976b64e4363c386d45ac0f7ee85c9233"
|
||||
|
||||
[[package]]
|
||||
name = "triomphe"
|
||||
version = "0.1.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b40688ea6389c8171614b25491f71d4a27946e0c7ce2da1c6de27e25abf1a0ae"
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-ident"
|
||||
version = "1.0.24"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-segmentation"
|
||||
version = "1.13.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-width"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
|
||||
|
||||
[[package]]
|
||||
name = "utf8parse"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
|
||||
|
||||
[[package]]
|
||||
name = "windows-link"
|
||||
version = "0.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
|
||||
|
||||
[[package]]
|
||||
name = "windows-sys"
|
||||
version = "0.61.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
|
||||
dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "yansi"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec"
|
||||
|
||||
[[package]]
|
||||
name = "zngur"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7fc912d12934b4d04aabc52c14db6fc88ae8aa5a47902f27e8759c2de254723f"
|
||||
dependencies = [
|
||||
"zngur-generator",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zngur-def"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f27e49a62db537cea43a6c122ded7eda99c4ac0ca1d7cfd74f2bf6a6c88712d"
|
||||
dependencies = [
|
||||
"indexmap",
|
||||
"itertools",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zngur-generator"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5ffad8c6994c477023aba7613bdf17c470003bbd191846d3096e84ffecf86d4c"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"hex",
|
||||
"indexmap",
|
||||
"itertools",
|
||||
"sha2",
|
||||
"zngur-def",
|
||||
"zngur-parser",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zngur-parser"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d66c1b85ca6eab9576df5de31758cb1f9046b4ce47b25f45dabcb0fc7ef8789"
|
||||
dependencies = [
|
||||
"ariadne",
|
||||
"chumsky",
|
||||
"itertools",
|
||||
"zngur-def",
|
||||
]
|
||||
|
||||
+23
-1
@@ -1,6 +1,28 @@
|
||||
[workspace]
|
||||
resolver = "2"
|
||||
members = ["lix/lix-doc", "lix/libutil"]
|
||||
members = [
|
||||
"lix/lix-doc",
|
||||
"lix/lix-rs",
|
||||
"tools/licxxbridge",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
edition = "2021"
|
||||
|
||||
[workspace.dependencies]
|
||||
clap = "4"
|
||||
regex = "1.12.4"
|
||||
rootcause = "0.13.0"
|
||||
rustyline = "18"
|
||||
rustyline-derive = "0.12"
|
||||
syn = "2.0"
|
||||
zngur = "0.10"
|
||||
pkg-config = "0.3.33"
|
||||
|
||||
[profile.dev]
|
||||
opt-level = 1
|
||||
|
||||
[profile.release]
|
||||
debug = "full"
|
||||
debug-assertions = true
|
||||
overflow-checks = true
|
||||
|
||||
+2
-1
@@ -10,6 +10,7 @@ import platform
|
||||
import shlex
|
||||
import textwrap
|
||||
import dataclasses
|
||||
from pathlib import Path
|
||||
|
||||
flake_args = ["--extra-experimental-features", "nix-command flakes"]
|
||||
cases = {
|
||||
@@ -18,7 +19,7 @@ cases = {
|
||||
*flake_args,
|
||||
"search",
|
||||
"--no-eval-cache",
|
||||
"github:nixos/nixpkgs/e1fa12d4f6c6fe19ccb59cac54b5b3f25e160870",
|
||||
f"path:{Path('./bench/nixpkgs/').readlink()}",
|
||||
"hello",
|
||||
],
|
||||
"rebuild": lambda build: [
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
# Darwin: don't link liblix* into plugins (host process provides them at runtime).
|
||||
# Explicitly link curl so it binds to Nix-store libcurl, not /usr/lib/libcurl.
|
||||
if is_darwin
|
||||
plugin_deps = [
|
||||
liblix.partial_dependency(includes : true, compile_args : true),
|
||||
curl,
|
||||
]
|
||||
else
|
||||
plugin_deps = [liblix, curl]
|
||||
endif
|
||||
|
||||
plugin_mtls_store = shared_module(
|
||||
'plugin_mtls_store',
|
||||
'plugin_mtls_store.cc',
|
||||
dependencies : plugin_deps,
|
||||
# don't link liblix* into plugins (host process provides them at runtime).
|
||||
# Explicitly link curl so it binds to Nix-store libcurl, not /usr/lib/libcurl.
|
||||
dependencies : [
|
||||
liblix.partial_dependency(includes : true, compile_args : true),
|
||||
curl,
|
||||
],
|
||||
install : false,
|
||||
build_by_default : true,
|
||||
link_args : is_darwin ? shared_module_link_args : strict_shared_module_link_args,
|
||||
link_args : plugin_link_args,
|
||||
)
|
||||
|
||||
@@ -24,8 +24,7 @@ def map_contents_recursively(transformer):
|
||||
def process_command:
|
||||
.[0] as $context |
|
||||
.[1] as $body |
|
||||
# XXX FUTURE: drop sections once mdBook is at 0.5.0 or above in nixpkgs
|
||||
$body | (.items? // .sections) |= map(map_contents_recursively(if $context.renderer == "html" then transform_anchors_html else transform_anchors_strip end))
|
||||
$body | .items |= map(map_contents_recursively(if $context.renderer == "html" then transform_anchors_html else transform_anchors_strip end))
|
||||
;
|
||||
|
||||
process_command
|
||||
|
||||
@@ -31,12 +31,11 @@ command = "jq --from-file anchors.jq"
|
||||
|
||||
[output.markdown]
|
||||
|
||||
# XXX FUTURE: may be reenabled once mdBook 0.5.0 or above and matching mdbook-linkchecker are in nixpkgs
|
||||
#[output.linkcheck]
|
||||
[output.linkcheck2]
|
||||
# no Internet during the build (in the sandbox)
|
||||
#follow-web-links = false
|
||||
follow-web-links = false
|
||||
|
||||
# mdbook-linkcheck does not understand [foo]{#bar} style links, resulting in
|
||||
# excessive "Potential incomplete link" warnings. No other kind of warning was
|
||||
# produced at the time of writing.
|
||||
#warning-policy = "ignore"
|
||||
warning-policy = "ignore"
|
||||
|
||||
@@ -48,6 +48,11 @@ artemist:
|
||||
display_name: Artemis Tosini
|
||||
forgejo: artemist
|
||||
|
||||
astreaprtcl:
|
||||
display_name: Astreaprtcl
|
||||
forgejo: astreaprtcl
|
||||
github: astreaprtcl
|
||||
|
||||
bb010g:
|
||||
display_name: Dusk Banks
|
||||
forgejo: bb010g
|
||||
@@ -77,6 +82,9 @@ detroyejr:
|
||||
display_name: Jonathan De Troye
|
||||
github: detroyejr
|
||||
|
||||
edef:
|
||||
github: edef1c
|
||||
|
||||
edolstra:
|
||||
display_name: Eelco Dolstra
|
||||
github: edolstra
|
||||
@@ -142,6 +150,9 @@ kasimeka:
|
||||
forgejo: janw4ld
|
||||
github: kasimeka
|
||||
|
||||
keysmashes:
|
||||
github: keysmashes
|
||||
|
||||
kfears:
|
||||
display_name: KFears
|
||||
forgejo: kfearsoff
|
||||
@@ -245,6 +256,11 @@ raito:
|
||||
forgejo: raito
|
||||
github: RaitoBezarius
|
||||
|
||||
rkjnsn:
|
||||
display_name: Erik Jensen
|
||||
forgejo: rkjnsn
|
||||
github: rkjnsn
|
||||
|
||||
roberth:
|
||||
display_name: Robert Hensing
|
||||
github: roberth
|
||||
@@ -253,6 +269,9 @@ rootile:
|
||||
display_name: rootile (Rutile)
|
||||
forgejo: rootile
|
||||
|
||||
sandydoo:
|
||||
github: sandydoo
|
||||
|
||||
seppel3210:
|
||||
github: Seppel3210
|
||||
|
||||
@@ -317,5 +336,8 @@ yorickvp:
|
||||
yshui:
|
||||
github: yshui
|
||||
|
||||
ysndr:
|
||||
github: ysndr
|
||||
|
||||
zimbatm:
|
||||
github: zimbatm
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
---
|
||||
synopsis: "check for missing ca-file or netrc-file if one is specified"
|
||||
cls: [5646]
|
||||
category: "Improvements"
|
||||
credits: [astreaprtcl]
|
||||
issues: [fj#1106]
|
||||
---
|
||||
|
||||
If the settings `ssl-cert-file` or `netrc-file` have been set by the user, check if those files actually exist and fail if they are missing.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
synopsis: "don't treat tarball fetches with empty or zero hash as locked"
|
||||
cls: []
|
||||
category: "Fixes"
|
||||
credits: [horrors]
|
||||
issues: [fj#1233]
|
||||
---
|
||||
|
||||
Lix no longer treats tarball fetches with empty or zero hashes as locked.
|
||||
All such fetches are now also affected by `tarball-ttl` as a consequence.
|
||||
@@ -0,0 +1,13 @@
|
||||
---
|
||||
synopsis: builtins.floor/builtins.ceil handle out-of-range inputs correctly
|
||||
issues: [nix#12899]
|
||||
cls: [3923]
|
||||
prs: [nix#13013]
|
||||
category: "Breaking Changes"
|
||||
credits: [jade, nan-git, rootile]
|
||||
---
|
||||
Previously, `builtins.floor` and `builtins.ceil` always cast the input into a floating point value before running the operation and casting the floating point result back into an integer.
|
||||
No checks were made for precision loss in either coercing integer inputs or converting the output to an integer (and in fact in the latter case, invoked undefined behaviour).
|
||||
|
||||
Now, Lix checks for precision loss on integer input (to avoid a silent eval semantics change if we were to simply pass it through as-is) and on integer output.
|
||||
If your code fails to evaluate after this change, use `--extra-deprecated-features floor-ceil-corrupt-integers`.
|
||||
@@ -0,0 +1,21 @@
|
||||
---
|
||||
synopsis: "Use a lock when fetching inputs"
|
||||
issues: [1122]
|
||||
cls: [5438]
|
||||
category: "Fixes"
|
||||
credits: [lheckemann]
|
||||
---
|
||||
|
||||
Up to now, attempting to fetch the same git input from multiple processes
|
||||
concurrently when the input is not yet cached presented multiple issues:
|
||||
|
||||
- If the input was not already present, it would unnecessarily be fetched
|
||||
multiple times;
|
||||
|
||||
- Access to the fetcher cache database was contentious, and could lead to
|
||||
evaluation or flake locking failing unnecessary because the fetcher cache
|
||||
was locked.
|
||||
|
||||
We now acquire a lock on a path based on a hash of the input specification
|
||||
before accessing the fetcher db, reducing contention significantly, and
|
||||
preventing more than one process from fetching the same path at the same time.
|
||||
@@ -0,0 +1,16 @@
|
||||
---
|
||||
synopsis: "Use mimalloc for faster evaluation"
|
||||
cls: [5645]
|
||||
category: Features
|
||||
credits: [getchoo, lovesegfault]
|
||||
---
|
||||
|
||||
Lix now links with [mimalloc](https://github.com/microsoft/mimalloc),
|
||||
replacing the system's default `malloc()` for all non-GC allocations.
|
||||
|
||||
This yields a **5–12% wall-clock improvement** on evaluation workloads,
|
||||
ranging from `nix-instantiate hello` to `nix-env -qa` and full NixOS
|
||||
configurations.
|
||||
|
||||
The allocator can be disabled at build time with `-Dmimalloc=disabled`,
|
||||
or by passing the `useMimalloc = false` override to the `lix` package.
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
synopsis: "nix-eval-jobs support `--apply` flag"
|
||||
cls: [5748]
|
||||
category: "Features"
|
||||
credits: [isabelroses,mic92,ysndr]
|
||||
issues: [fj#1214]
|
||||
---
|
||||
|
||||
`nix-eval-jobs` now supports the `--apply` flag. With this you can apply the
|
||||
provided function to the each derivation, the result of this function will then
|
||||
be serialized as a JSON value and stored inside `"extraValue"` key of the json
|
||||
line output.
|
||||
@@ -0,0 +1,10 @@
|
||||
---
|
||||
synopsis: "Fix `nix-copy-closure --include-outputs`"
|
||||
issues: [gh#5105]
|
||||
cls: [5588]
|
||||
category: "Fixes"
|
||||
credits: [rkjnsn]
|
||||
---
|
||||
|
||||
The `--include-outputs` flag for `nix-copy-closure` now works as intended.
|
||||
Previously, the option was accepted but silently ignored.
|
||||
@@ -0,0 +1,14 @@
|
||||
---
|
||||
synopsis: "Improve nix doctor"
|
||||
cls: [5316, 5317, 5318, 5319, 5320, 5768, 5829]
|
||||
category: Features
|
||||
credits: [rootile, raito]
|
||||
---
|
||||
The `nix doctor` diagnosics interface now provides a lot more useful information including, but not limited to:
|
||||
- General system information (OS, Hardware etc)
|
||||
- Nix Information like Sandbox, Version, Store, State and other directories
|
||||
- Flake registry
|
||||
- Search path Information
|
||||
- Nixpkgs provenance
|
||||
- Remote builder configuration (including remote connection)
|
||||
- fix crash when having relative Paths in PATH
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
synopsis: "Remove `max-connections` store parameters for `ssh://` and `ssh-ng://` stores"
|
||||
cls: []
|
||||
category: Miscellany
|
||||
credits: [horrors]
|
||||
---
|
||||
|
||||
The `max-connections` parameter was undocumented, untested, and (in the case of `ssh`) even ignored
|
||||
entirely for remote builds. During a survey of public nixos configurations we have found *two* uses
|
||||
of `max-connections` for `ssh-ng`, and none at all for `ssh`. Since it is so rarely used but brings
|
||||
significant internal complexity that hinders improvements we have decided to remove these features.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
synopsis: "Print REPL backtraces in more convenient order"
|
||||
issues: []
|
||||
cls: [5491]
|
||||
category: "Improvements"
|
||||
credits: [blokyk]
|
||||
---
|
||||
|
||||
When using the debugger, stack traces printed with the `:bt` command were
|
||||
previously printed in reverse order compared to most other situations where they
|
||||
appeared: the current stack frame would be printed at the very top, with the
|
||||
most outer frame at the bottom, meaning that you'd have to scroll up to get a
|
||||
sense of where you are.
|
||||
|
||||
With this change, the stack frames are printed such that the most relevant ones
|
||||
are immediatly visible at the bottom, just like other traces in lix (e.g.
|
||||
ones caused by errors).
|
||||
@@ -0,0 +1,12 @@
|
||||
---
|
||||
synopsis: "REPL now uses rustyline"
|
||||
cls: [5703]
|
||||
category: "Improvements"
|
||||
credits: [horrors]
|
||||
issues: []
|
||||
---
|
||||
|
||||
The REPL now uses [rustyline](https://github.com/kkawakam/rustyline) for input processing instead
|
||||
of editline. This comes with some improvements to REPL behavior: wrapping lines no longer confuse
|
||||
the line editor, unicode is fully supported, pasting multiline expressions is noew possible, even
|
||||
undo commands are now available! We plan to improve the REPL further using these newfound powers.
|
||||
@@ -0,0 +1,11 @@
|
||||
---
|
||||
synopsis: "Hash mismatch diagnostics now work with `structuredAttrs`"
|
||||
issues: [fj#1175]
|
||||
cls: [5441]
|
||||
category: Fixes
|
||||
credits: [keysmashes]
|
||||
---
|
||||
|
||||
Nixpkgs fetchers like `fetchurl` now use `structuredAttrs`, which broke the
|
||||
hash mismatch diagnostics added in Lix 2.91. This has been fixed and the likely
|
||||
URL is now shown again.
|
||||
@@ -0,0 +1,18 @@
|
||||
---
|
||||
synopsis: "Changes to `flake.nix` validation"
|
||||
cls: [5523]
|
||||
category: "Breaking Changes"
|
||||
credits: [piegames, Qyriad, horrors]
|
||||
issues: [gh#4945]
|
||||
---
|
||||
|
||||
Flakes try to keep their inputs and metadata "simple", to make sure no unbounded computation may happen when calling e.g. `nix flake show`.
|
||||
Those checks were haphazard, a maintenance burden, and also easily circumventable.
|
||||
|
||||
Lix has now replaced all the old checks by a simple rule: **No function calls outside of `outputs`.**
|
||||
This is easier to reason about than the previous set of inconsistent rules, and crucially now also allows syntax features that users felt like they *should* have worked in the past, like let bindings.
|
||||
However, some warts still remain for now: Some syntax constructs like `-1` internally desugar to `__sub 0 1`, which is a function call and thus remains forbidden.
|
||||
This will be rectified as soon as the deprecation period of the respective anti-features has been completed.
|
||||
|
||||
This change is **breaking** in the sense that flakes which are written with the newly allowed language features will not evaluate with an older Lix version which still uses the old, more restrictive checks.
|
||||
Crucially, this also affects **all transitive dependants** of such Flakes.
|
||||
@@ -0,0 +1,28 @@
|
||||
---
|
||||
synopsis: "Fix unsigned overflow leading to out-of-band write in the NAR parser"
|
||||
cls: [5554]
|
||||
category: "Fixes"
|
||||
credits: [horrors, raito, edef, sandydoo]
|
||||
issues: []
|
||||
---
|
||||
|
||||
The NAR parser contained an unsigned integer overflow that could be used by an
|
||||
attacker to write arbitrary data to an unknown memory location and possibly
|
||||
achieve code execution. A successful attack on the system-wide Lix daemon
|
||||
could lead to privilege escalation to root. Any process that involves NAR
|
||||
serialization could trigger this issue, including (but not limited to)
|
||||
|
||||
- local user interaction, whether the users are trusted or untrusted
|
||||
- malicious substituters sending malformed NARs
|
||||
- remote builders sending malformed build results
|
||||
- remote daemons sending malformed inputs when requesting remote builds
|
||||
|
||||
Successful attacks using this bug require ASLR weakening of some sort, whether
|
||||
by architecture constraints (e.g. on 32 bit systems, where little randomization
|
||||
is possible) or system configuration (e.g. low ASLR entropy when loading
|
||||
libraries), and millions of attempts. Local attacks can be mounted in less than
|
||||
an hour. Remote builds typically require a fresh SSH connection for each build
|
||||
and are thus less susceptible. Only one attempt can be made by substituters for
|
||||
every build using substituters, they are thus not a likely vector for attacks.
|
||||
|
||||
At the time of writing, MITRE has not assigned this a CVE yet.
|
||||
@@ -0,0 +1,23 @@
|
||||
---
|
||||
synopsis: "Always print frames from `addErrorContext` in error traces"
|
||||
cls: [5847]
|
||||
category: "Improvements"
|
||||
credits: [blokyk]
|
||||
issues: []
|
||||
---
|
||||
|
||||
The [`builtins.addErrorContext`](@docroot@/language/builtins.md#builtins-addErrorContext)
|
||||
function allows an author to add artificial stack frames with custom messages to
|
||||
help end-users understand the context of an error and the path the code took to
|
||||
get there, without having to read and understand the original source code. A
|
||||
particularly notable user of this is the Nixpkgs module system, which adds
|
||||
custom frames detailing what option it's evaluating or which definition it's
|
||||
looking at.
|
||||
|
||||
However, previously, these frames would end up treated just as any other,
|
||||
meaning they would most often not be visible without `--show-trace`; yet, using
|
||||
`--show-trace`, they would be drowned out in the noise of the hundreds of other
|
||||
frames, rendering them just as unusable.
|
||||
|
||||
With this change, these frames are now unconditionally shown, even without
|
||||
`--show-trace`, which makes basic error traces much more informative.
|
||||
@@ -51,7 +51,7 @@ $ nix-shell -A native-clangStdenvPackages
|
||||
|
||||
### Building from the development shell
|
||||
|
||||
We have a [justfile](just.systems) for extra convenient building.
|
||||
We have a [justfile](https://just.systems/) for extra convenient building.
|
||||
It defaults to using `./build` as the build directory, and `$out` (`./outputs/out`) as the install directory.
|
||||
For most cases, you can clean-build, install, and run the tests with:
|
||||
|
||||
@@ -183,13 +183,13 @@ The development shell on Linux uses LLD by default for faster link times.
|
||||
This is set using `mesonFlags`, so to override it, you can simplify re-specify the linker to Meson:
|
||||
|
||||
```bash
|
||||
$ just setup-custom -Dc_link_args=-fuse-ld=ld -Dcpp_link_args=-fuse-ld=ld
|
||||
$ just setup -Dc_link_args=-fuse-ld=ld -Dcpp_link_args=-fuse-ld=ld
|
||||
```
|
||||
|
||||
While using LLD, you may find it helpful to use ThinLTO for even further improvements to link times for incremental builds:
|
||||
|
||||
```bash
|
||||
$ just setup-custom -Db_lto=true -Db_lto_mode=thin -Db_thinlto_cache=true
|
||||
$ just setup -Db_lto=true -Db_lto_mode=thin -Db_thinlto_cache=true
|
||||
```
|
||||
|
||||
## Sending changes to Gerrit for review {#sending-to-gerrit}
|
||||
@@ -200,7 +200,7 @@ Our instance is at <https://gerrit.lix.systems>.
|
||||
There's much more information about how to use Gerrit in the [wiki section on Gerrit][wiki-gerrit] including how to use Jujutsu, how to use the UI and more.
|
||||
The Snix project also has some Gerrit information [in their contributing docs][snix-gerrit].
|
||||
|
||||
[wiki-gerrit]: https://wiki.lix.systems/books/lix-contributors/chapter/gerrit
|
||||
[wiki-gerrit]: https://wiki.lix.systems/books/contributing/chapter/intro-to-gerrit
|
||||
[snix-gerrit]: https://snix.dev/docs/guides/contributing/
|
||||
|
||||
The gist is that once you have your SSH key and git remote set up, you can send commits for review with:
|
||||
@@ -226,7 +226,7 @@ The `Code-Review+2` from before will stick around through trivial rebases so no
|
||||
We use Buildkite for our CI, usually you will not have to interact directly with it other than reviewing any errors it produces, which are linked from Gerrit.
|
||||
|
||||
However in certain cases a CI run will fail due to transient issues not related to your code and you will need to rerun it by hand.
|
||||
You can log in to the CI via [SSO](https://buildkite.com/sso/lix-project). On your job you can then hit the "Retry failed" button to rerun it, normally you will not have a repeat of the transient issue.
|
||||
You can log in to the CI via [SSO](https://buildkite.com/sso/afnix). On your job you can then hit the "Retry failed" button to rerun it, normally you will not have a repeat of the transient issue.
|
||||
If the build still fails on CI issues or all builds are failing this should be reported via [Zulip on #T-infra](https://zulip.lix.systems/#narrow/channel/7-T-infra) or [Matrix on #dev](https://matrix.to/#/%23dev%3Alix.systems?via=lix.systems).
|
||||
|
||||
## Building Lix with `nix`
|
||||
@@ -566,7 +566,7 @@ Then add the new file there, and don't forget to register it in the appropriate
|
||||
The following metadata properties are supported for builtin functions:
|
||||
* `name` (required): the language-facing name (as a member of the `builtins` attribute set) of the function.
|
||||
* `implementation` (optional): a C++ expression specifying the implementation of the builtin.
|
||||
It must be a function of signature `void(EvalState &, PosIdx, Value * *, Value &)`.
|
||||
It must be a function of signature `Value(EvalState &, PosIdx, Value * *)`.
|
||||
If not specified, defaults to `prim_${name}`.
|
||||
* `renameInGlobalScope` (optional): whether the definition should be "hidden" in the global scope by prefixing its name with two underscores.
|
||||
If not specified, defaults to `true`.
|
||||
|
||||
@@ -50,10 +50,6 @@ The most current alternative to this section is to read `package.nix` and see wh
|
||||
- The `boost` library of version 1.66.0 or higher. It can be obtained
|
||||
from the official web site <https://www.boost.org/>.
|
||||
|
||||
- The `editline` library of version 1.14.0 or higher. It can be
|
||||
obtained from the its repository
|
||||
<https://github.com/troglobit/editline>.
|
||||
|
||||
- Recent versions of Bison and Flex to build the parser. (This is
|
||||
because Nix needs GLR support in Bison and reentrancy support in
|
||||
Flex.) For Bison, you need version 2.6, which can be obtained from
|
||||
|
||||
@@ -164,7 +164,7 @@ Note that lists are only lazy in values, and they are strict in length.
|
||||
|
||||
An attribute set is a collection of name-value-pairs (called *attributes*) enclosed in curly brackets (`{ }`).
|
||||
|
||||
An attribute name can be an identifier or a [string](#type-string).
|
||||
An attribute name can be an identifier or a [double-quoted string](#type-string).
|
||||
An identifier must start with a letter (`a-z`, `A-Z`) or underscore (`_`), and can otherwise contain letters (`a-z`, `A-Z`), numbers (`0-9`), underscores (`_`), apostrophes (`'`), or dashes (`-`).
|
||||
|
||||
> *name* = *identifier* | *string* \
|
||||
|
||||
@@ -70,11 +70,6 @@ def do_include(content: str, relative_md_path: Path, source_root: Path, search_p
|
||||
|
||||
def recursive_replace(data, book_root, search_path):
|
||||
match data:
|
||||
# XXX FUTURE: drop sections once mdBook is at 0.5.0 or above in nixpkgs
|
||||
case {'sections': sections}:
|
||||
return data | dict(
|
||||
sections = [recursive_replace(section, book_root, search_path) for section in sections],
|
||||
)
|
||||
case {'items': items}:
|
||||
return data | dict(
|
||||
items = [recursive_replace(item, book_root, search_path) for item in items],
|
||||
|
||||
Generated
+4
-4
@@ -106,16 +106,16 @@
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1773082486,
|
||||
"narHash": "sha256-TKUDrM0nKUo5s/b8jhjXa2prcu5KU5Cck3HBTRLDjfo=",
|
||||
"lastModified": 1783770249,
|
||||
"narHash": "sha256-K8pGvFito5dp9T0+clr60q+bJPGEskK75aAJ39w7HBM=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "7f8b8875bdb38a70c7b5ceb9ba6a6a8d69859e16",
|
||||
"rev": "62463162b3ce92919f19ada41a70a0d943a08da8",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-25.11-small",
|
||||
"ref": "nixos-26.05-small",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
description = "Lix: A modern, delicious implementation of the Nix package manager";
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11-small";
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05-small";
|
||||
nixpkgs-regression.url = "github:NixOS/nixpkgs/215d4d0fd80ca5163643b03a33fde804a29cc1e2";
|
||||
|
||||
# Required because Nix 2.18 is not in Nixpkgs ≥ 25.05 anymore.
|
||||
@@ -42,6 +42,7 @@
|
||||
|
||||
let
|
||||
inherit (nixpkgs) lib;
|
||||
lixSrc = self;
|
||||
|
||||
# This notice gets echoed as a dev shell hook, and can be turned off with
|
||||
# `touch .nocontribmsg`
|
||||
@@ -76,206 +77,35 @@
|
||||
(Run `touch .nocontribmsg` to hide this message.)
|
||||
'';
|
||||
|
||||
versionJson = builtins.fromJSON (builtins.readFile ./version.json);
|
||||
officialRelease = versionJson.official_release;
|
||||
scope = import ./nix-support/build/inputs.nix {
|
||||
inherit
|
||||
lib
|
||||
nixpkgs
|
||||
nix_2_18
|
||||
nix2container
|
||||
lixSrc
|
||||
nixpkgs-regression
|
||||
;
|
||||
};
|
||||
|
||||
# Set to true to build the release notes for the next release.
|
||||
buildUnreleasedNotes = true;
|
||||
inherit (scope)
|
||||
crossSystems
|
||||
darwinSystems
|
||||
forAllStdenvs
|
||||
forAllSystems
|
||||
forAvailableSystems
|
||||
linux64BitSystems
|
||||
nixpkgsFor
|
||||
overlayFor
|
||||
systems
|
||||
versionSuffix
|
||||
;
|
||||
|
||||
versionSuffix =
|
||||
if officialRelease then
|
||||
""
|
||||
else
|
||||
"pre${
|
||||
builtins.substring 0 8 (self.lastModifiedDate or self.lastModified or "19700101")
|
||||
}-dev_${self.shortRev or "dirty"}";
|
||||
|
||||
linux32BitSystems = [ "i686-linux" ];
|
||||
linux64BitSystems = [
|
||||
"x86_64-linux"
|
||||
"aarch64-linux"
|
||||
];
|
||||
linuxSystems = linux32BitSystems ++ linux64BitSystems;
|
||||
darwinSystems = [
|
||||
"x86_64-darwin"
|
||||
"aarch64-darwin"
|
||||
];
|
||||
nonDarwinSystems = linuxSystems;
|
||||
systems = linuxSystems ++ darwinSystems;
|
||||
|
||||
# If you add something here, please update the list in doc/manual/src/contributing/hacking.md.
|
||||
# Thanks~
|
||||
crossSystems = [
|
||||
"armv6l-linux"
|
||||
"armv7l-linux"
|
||||
"riscv64-linux"
|
||||
"aarch64-linux"
|
||||
"x86_64-freebsd"
|
||||
# FIXME: broken dev shell due to python
|
||||
# "x86_64-netbsd"
|
||||
];
|
||||
|
||||
stdenvs = [
|
||||
# see assertion in package.nix why these two are disabled
|
||||
# "stdenv"
|
||||
# "gccStdenv"
|
||||
"clangStdenv"
|
||||
"libcxxStdenv"
|
||||
"ccacheStdenv"
|
||||
];
|
||||
|
||||
forAllSystems = lib.genAttrs systems;
|
||||
# Same as forAllSystems, but removes nulls, in case something is broken
|
||||
# on that system.
|
||||
forAvailableSystems =
|
||||
f: lib.filterAttrs (name: value: value != null && value != { }) (forAllSystems f);
|
||||
|
||||
forAllCrossSystems = lib.genAttrs crossSystems;
|
||||
|
||||
forAllStdenvs =
|
||||
f:
|
||||
lib.listToAttrs (
|
||||
map (stdenvName: {
|
||||
name = "${stdenvName}Packages";
|
||||
value = f stdenvName;
|
||||
}) stdenvs
|
||||
)
|
||||
// {
|
||||
# TODO delete this and reënable gcc stdenvs once gcc compiles kj coros correctly
|
||||
stdenvPackages = f "clangStdenv";
|
||||
};
|
||||
|
||||
# Memoize nixpkgs for different platforms for efficiency.
|
||||
nixpkgsFor = forAllSystems (
|
||||
system:
|
||||
let
|
||||
make-pkgs =
|
||||
crossSystem: stdenv:
|
||||
import nixpkgs {
|
||||
localSystem = {
|
||||
inherit system;
|
||||
};
|
||||
crossSystem = if crossSystem == null then null else { system = crossSystem; };
|
||||
overlays = [ (overlayFor (p: p.${stdenv})) ];
|
||||
};
|
||||
stdenvs = forAllStdenvs (make-pkgs null);
|
||||
native = stdenvs.stdenvPackages;
|
||||
in
|
||||
{
|
||||
inherit stdenvs native;
|
||||
static = native.pkgsStatic;
|
||||
cross = forAllCrossSystems (crossSystem: make-pkgs crossSystem "clangStdenv");
|
||||
}
|
||||
);
|
||||
|
||||
overlayFor =
|
||||
getStdenv: final: prev:
|
||||
let
|
||||
currentStdenv = getStdenv final;
|
||||
in
|
||||
{
|
||||
nixStable = prev.nix;
|
||||
|
||||
nixVersions = prev.nixVersions // {
|
||||
# Nix 2.18 has been removed from Nixpkgs ≥ 25.05, so we need to reintroduce it ourselves for our tests.
|
||||
nix_2_18 =
|
||||
nix_2_18.outputs.packages.${currentStdenv.hostPlatform.system}.default.overrideAttrs
|
||||
(_: {
|
||||
pname = "nix";
|
||||
});
|
||||
};
|
||||
|
||||
check-headers = final.buildPackages.callPackage ./maintainers/check-headers.nix { };
|
||||
check-syscalls = final.buildPackages.callPackage ./maintainers/check-syscalls.nix { };
|
||||
|
||||
default-busybox-sandbox-shell = final.busybox.override {
|
||||
useMusl = true;
|
||||
enableStatic = true;
|
||||
enableMinimal = true;
|
||||
extraConfig = ''
|
||||
CONFIG_FEATURE_FANCY_ECHO y
|
||||
CONFIG_FEATURE_SH_MATH y
|
||||
CONFIG_FEATURE_SH_MATH_64 y
|
||||
|
||||
CONFIG_ASH y
|
||||
CONFIG_ASH_OPTIMIZE_FOR_SIZE y
|
||||
|
||||
CONFIG_ASH_ALIAS y
|
||||
CONFIG_ASH_BASH_COMPAT y
|
||||
CONFIG_ASH_CMDCMD y
|
||||
CONFIG_ASH_ECHO y
|
||||
CONFIG_ASH_GETOPTS y
|
||||
CONFIG_ASH_INTERNAL_GLOB y
|
||||
CONFIG_ASH_JOB_CONTROL y
|
||||
CONFIG_ASH_PRINTF y
|
||||
CONFIG_ASH_TEST y
|
||||
'';
|
||||
};
|
||||
|
||||
nix = final.callPackage ./package.nix {
|
||||
inherit versionSuffix officialRelease;
|
||||
stdenv = currentStdenv;
|
||||
busybox-sandbox-shell = final.busybox-sandbox-shell or final.default-busybox-sandbox-shell;
|
||||
# See below
|
||||
lowdown = final.lowdown_3_0;
|
||||
lowdown-unsandboxed = final.lowdown_3_0.override { enableDarwinSandbox = false; };
|
||||
};
|
||||
|
||||
lix-clang-tidy = final.callPackage ./subprojects/lix-clang-tidy { };
|
||||
|
||||
nix-eval-jobs = final.callPackage ./subprojects/nix-eval-jobs {
|
||||
stdenv = currentStdenv;
|
||||
srcDir = ./subprojects/nix-eval-jobs;
|
||||
};
|
||||
|
||||
# HACK: We need nix-prefetch-git for fetchCargoVendor for Rust stuff,
|
||||
# so it can't use Lix, or we infrec:
|
||||
# lix -> Rust stuff -> fetchCargoVendor -> nix-prefetch-git -> nix (lix)
|
||||
# This will eventually become a problem upstream, but until then,
|
||||
# apply some duct tape and pray.
|
||||
nix-prefetch-git =
|
||||
if (lib.functionArgs prev.nix-prefetch-git.override) ? "nix" then
|
||||
prev.nix-prefetch-git.override { nix = prev.nix; }
|
||||
else
|
||||
prev.nix-prefetch-git;
|
||||
|
||||
# Export the patched version of boehmgc that Lix uses into the overlay
|
||||
# for consumers of this flake.
|
||||
boehmgc-nix = final.nix.passthru.boehmgc-nix;
|
||||
# And same thing for our build-release-notes package.
|
||||
build-release-notes = final.nix.passthru.build-release-notes;
|
||||
|
||||
# As soon as Nixpkgs updates to >= 3.0.0, change to lowdown_2_0!
|
||||
# We don't change the default version in order to not change the hash
|
||||
# of Nix/Lix from upstream Nixpkgs.
|
||||
lowdown_3_0 =
|
||||
if (lib.versions.major prev.lowdown.version == "3") then
|
||||
prev.lowdown
|
||||
else
|
||||
prev.lowdown.overrideAttrs (
|
||||
finalAttrs: _prevAttrs: {
|
||||
version = "3.0.0";
|
||||
|
||||
src = final.fetchurl {
|
||||
url = "https://kristaps.bsd.lv/lowdown/snapshots/lowdown-${finalAttrs.version}.tar.gz";
|
||||
sha512 = "94e97234d598382c3c3dc27f9bfdb3a3a2fcf7dbb6a8df3c85ee09f27f792449034a41d49d9cfd3d8450d2de01b8562c20c3d120e65c81af4d7d6c9454119e93";
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
capnproto = prev.capnproto.overrideAttrs (old: {
|
||||
patches =
|
||||
old.patches or [ ]
|
||||
++ [
|
||||
# backport of https://github.com/capnproto/capnproto/pull/1810
|
||||
./misc/capnproto-promise-nodiscard.patch
|
||||
]
|
||||
++ lib.optionals (lib.versionOlder old.version "1.2.0") [
|
||||
# backport of https://github.com/capnproto/capnproto/pull/2296
|
||||
./misc/capnproto-monotonic-clocks-are-a-lie.patch
|
||||
];
|
||||
});
|
||||
};
|
||||
inherit (scope.callPackage ./nix-support/build/outputs.nix { })
|
||||
packages
|
||||
ciArtifacts
|
||||
tests
|
||||
;
|
||||
in
|
||||
{
|
||||
# for repl debugging
|
||||
@@ -285,202 +115,13 @@
|
||||
# 'nix.perl-bindings' packages.
|
||||
overlays.default = overlayFor (p: p.clangStdenv);
|
||||
|
||||
hydraJobs = {
|
||||
# Aggregate job that is finished in Hydra _after_ all constituent jobs (here: grouped by system)
|
||||
# succeed.
|
||||
# This is used to run CD scripts once all builds are finished on Hydra.
|
||||
release = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system}.native;
|
||||
in
|
||||
pkgs.runCommand "release"
|
||||
{
|
||||
_hydraAggregate = true;
|
||||
constituents = lib.filter (x: x != null) (
|
||||
lib.mapAttrsToListRecursiveCond
|
||||
(_: val: !(lib.isDerivation val || builtins.any (system': val ? ${system'}) systems))
|
||||
(
|
||||
path: drv:
|
||||
if drv ? ${system} then
|
||||
lib.concatStringsSep "." (path ++ [ system ])
|
||||
else if drv.system or null == system then
|
||||
lib.concatStringsSep "." path
|
||||
else
|
||||
null
|
||||
)
|
||||
(
|
||||
removeAttrs self.hydraJobs [
|
||||
"devShell"
|
||||
"release"
|
||||
"rl-next"
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
''
|
||||
touch $out
|
||||
''
|
||||
);
|
||||
|
||||
# Binary package for various platforms.
|
||||
build = forAllSystems (system: self.packages.${system}.nix);
|
||||
|
||||
# Ensure support for lowdown < 3.0 doesn't regress for NixOS 25.11
|
||||
build-lowdown_2_0 = lib.genAttrs [ "aarch64-linux" ] (
|
||||
system:
|
||||
assert lib.versionOlder nixpkgsFor.${system}.native.lowdown.version "3.0.0";
|
||||
self.packages.${system}.nix.override {
|
||||
lowdown = nixpkgsFor.${system}.native.lowdown;
|
||||
lowdown-unsandboxed = nixpkgsFor.${system}.native.lowdown-unsandboxed;
|
||||
}
|
||||
);
|
||||
|
||||
buildStatic = lib.genAttrs linux64BitSystems (system: self.packages.${system}.nix-static);
|
||||
|
||||
hydraJobs = ciArtifacts // {
|
||||
devShell = forAllSystems (system: {
|
||||
default = self.devShells.${system}.default;
|
||||
clang = self.devShells.${system}.native-clangStdenvPackages;
|
||||
});
|
||||
|
||||
rl-next = forAllSystems (
|
||||
system:
|
||||
let
|
||||
rl-next-check =
|
||||
name: dir:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system}.native;
|
||||
in
|
||||
pkgs.buildPackages.runCommand "test-${name}-release-notes" { } ''
|
||||
LANG=C.UTF-8 ${lib.getExe pkgs.build-release-notes} --change-authors ${./doc/manual/change-authors.yml} ${dir} >$out
|
||||
'';
|
||||
in
|
||||
{
|
||||
user = rl-next-check "rl-next" ./doc/manual/rl-next;
|
||||
}
|
||||
);
|
||||
|
||||
# Completion tests for the Nix REPL.
|
||||
repl-completion = forAllSystems (
|
||||
system: nixpkgsFor.${system}.native.callPackage ./tests/repl-completion.nix { }
|
||||
);
|
||||
|
||||
# Perl bindings for various platforms.
|
||||
perlBindings = forAllSystems (system: nixpkgsFor.${system}.native.nix.passthru.perl-bindings);
|
||||
|
||||
# nix-eval-jobs can be built against this Lix.
|
||||
nix-eval-jobs = forAllSystems (system: nixpkgsFor.${system}.native.nix-eval-jobs);
|
||||
|
||||
# Binary tarball for various platforms, containing a Nix store
|
||||
# with the closure of 'nix' package.
|
||||
binaryTarball = forAllSystems (system: nixpkgsFor.${system}.native.nix.passthru.binaryTarball);
|
||||
|
||||
# docker image with Lix inside
|
||||
dockerImage = lib.genAttrs linux64BitSystems (system: self.packages.${system}.dockerImage);
|
||||
|
||||
# API docs for Nix's unstable internal C++ interfaces.
|
||||
internal-api-docs =
|
||||
let
|
||||
nixpkgs = nixpkgsFor.x86_64-linux.native;
|
||||
inherit (nixpkgs) pkgs;
|
||||
|
||||
nix = pkgs.callPackage ./package.nix {
|
||||
inherit versionSuffix officialRelease buildUnreleasedNotes;
|
||||
inherit (pkgs) build-release-notes;
|
||||
# Required since we don't support gcc stdenv
|
||||
stdenv = pkgs.clangStdenv;
|
||||
internalApiDocs = true;
|
||||
busybox-sandbox-shell = pkgs.busybox-sandbox-shell;
|
||||
};
|
||||
in
|
||||
nix.overrideAttrs (prev: {
|
||||
# This Hydra job is just for the internal API docs.
|
||||
# We don't need the build artifacts here.
|
||||
dontBuild = true;
|
||||
doCheck = false;
|
||||
doInstallCheck = false;
|
||||
});
|
||||
|
||||
# System tests.
|
||||
tests =
|
||||
import ./tests/nixos {
|
||||
inherit
|
||||
self
|
||||
lib
|
||||
nixpkgs
|
||||
nixpkgsFor
|
||||
;
|
||||
}
|
||||
// {
|
||||
# the n-e-j test suite is unusably slow in darwin ci. disbled until anywho fixes this.
|
||||
nix-eval-jobs = (lib.genAttrs nonDarwinSystems) (
|
||||
system: self.packages.${system}.nix-eval-jobs.tests.nix-eval-jobs
|
||||
);
|
||||
|
||||
# This is x86_64-linux only, just because we have significantly
|
||||
# cheaper x86_64-linux compute in CI.
|
||||
# It is clangStdenv because clang's sanitizers are nicer.
|
||||
asanBuild = self.packages.x86_64-linux.nix-clangStdenv.override {
|
||||
# Improve caching of non-code changes by not changing the
|
||||
# derivation name every single time, since this will never be seen
|
||||
# by users anyway.
|
||||
versionSuffix = "";
|
||||
sanitize = [
|
||||
"address"
|
||||
"undefined"
|
||||
];
|
||||
# it is very hard to make *every* CI build use this option such
|
||||
# that we don't wind up building Lix twice, so we do it here where
|
||||
# we are already doing so.
|
||||
werror = true;
|
||||
};
|
||||
|
||||
# Although this might be nicer to do with pre-commit, that would
|
||||
# require adding 12MB of nodejs to the dev shell, whereas building it
|
||||
# in CI with Nix avoids that at a cost of slower feedback on rarely
|
||||
# touched files.
|
||||
jsSyntaxCheck =
|
||||
let
|
||||
nixpkgs = nixpkgsFor.x86_64-linux.native;
|
||||
inherit (nixpkgs) pkgs;
|
||||
docSources = lib.fileset.toSource {
|
||||
root = ./doc;
|
||||
fileset = lib.fileset.fileFilter (f: f.hasExt "js") ./doc;
|
||||
};
|
||||
in
|
||||
pkgs.runCommand "js-syntax-check" { } ''
|
||||
find ${docSources} -type f -print -exec ${pkgs.nodejs-slim}/bin/node --check '{}' ';'
|
||||
touch $out
|
||||
'';
|
||||
|
||||
# clang-tidy run against the Lix codebase using the Lix clang-tidy plugin
|
||||
clang-tidy = forAllSystems (
|
||||
system:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system}.native;
|
||||
in
|
||||
pkgs.callPackage ./package.nix {
|
||||
# Required since we don't support gcc stdenv
|
||||
stdenv = pkgs.clangStdenv;
|
||||
versionSuffix = "";
|
||||
lintInsteadOfBuild = true;
|
||||
}
|
||||
);
|
||||
|
||||
# Make sure that nix-env still produces the exact same result
|
||||
# on a particular version of Nixpkgs.
|
||||
evalNixpkgs = nixpkgsFor.x86_64-linux.native.callPackage ./tests/nixpkgs/eval.nix {
|
||||
inherit nixpkgs-regression;
|
||||
};
|
||||
|
||||
nixpkgsLibTests = forAllSystems (
|
||||
system:
|
||||
nixpkgsFor.${system}.native.callPackage ./tests/nixpkgs/lib.nix {
|
||||
inherit nixpkgs system;
|
||||
inherit (self.packages.${system}) nix;
|
||||
}
|
||||
);
|
||||
};
|
||||
inherit tests;
|
||||
|
||||
pre-commit = forAvailableSystems (
|
||||
system:
|
||||
@@ -540,45 +181,7 @@
|
||||
}
|
||||
);
|
||||
|
||||
packages = forAllSystems (
|
||||
system:
|
||||
rec {
|
||||
inherit (nixpkgsFor.${system}.native) nix;
|
||||
default = nix;
|
||||
|
||||
inherit (nixpkgsFor.${system}.native) lix-clang-tidy nix-eval-jobs;
|
||||
}
|
||||
// (
|
||||
lib.optionalAttrs (builtins.elem system linux64BitSystems) {
|
||||
# python doesn't work in static builds as of 2025-06-27
|
||||
nix-static = nixpkgsFor.${system}.static.nix.overrideAttrs (_: {
|
||||
doCheck = false;
|
||||
});
|
||||
dockerImage =
|
||||
let
|
||||
pkgs = nixpkgsFor.${system}.native;
|
||||
nix2container' = import nix2container { inherit pkgs; };
|
||||
in
|
||||
import ./docker.nix {
|
||||
inherit pkgs;
|
||||
nix2container = nix2container'.nix2container;
|
||||
tag = pkgs.nix.version;
|
||||
};
|
||||
}
|
||||
// builtins.listToAttrs (
|
||||
map (crossSystem: {
|
||||
name = "nix-${crossSystem}";
|
||||
value = nixpkgsFor.${system}.cross.${crossSystem}.nix;
|
||||
}) crossSystems
|
||||
)
|
||||
// builtins.listToAttrs (
|
||||
map (stdenvName: {
|
||||
name = "nix-${stdenvName}";
|
||||
value = nixpkgsFor.${system}.stdenvs."${stdenvName}Packages".nix;
|
||||
}) stdenvs
|
||||
)
|
||||
)
|
||||
);
|
||||
inherit packages;
|
||||
|
||||
devShells =
|
||||
let
|
||||
@@ -611,12 +214,30 @@
|
||||
in
|
||||
(makeShells "native" nixpkgsFor.${system}.native)
|
||||
// (makeShells "static" nixpkgsFor.${system}.static)
|
||||
// (forAllCrossSystems (
|
||||
crossSystem:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system}.cross.${crossSystem};
|
||||
in
|
||||
makeShell pkgs pkgs.clangStdenv
|
||||
// (lib.listToAttrs (
|
||||
# Provide e.g., both '.#native-aarch64-linux` and `.#static-aarch64-linux`,
|
||||
# for each cross-system.
|
||||
# "native" feels like a misnomer here since it's literally cross compiling,
|
||||
# but at least it's consistent with the native/static dichotomy we've set up.
|
||||
lib.concatMap (
|
||||
crossSystem:
|
||||
let
|
||||
pkgs = nixpkgsFor.${system}.cross.${crossSystem};
|
||||
inherit (pkgs) pkgsStatic;
|
||||
native = makeShell pkgs pkgs.clangStdenv;
|
||||
static = makeShell pkgsStatic pkgsStatic.clangStdenv;
|
||||
in
|
||||
[
|
||||
{
|
||||
name = "native-${crossSystem}";
|
||||
value = native;
|
||||
}
|
||||
{
|
||||
name = "static-${crossSystem}";
|
||||
value = static;
|
||||
}
|
||||
]
|
||||
) crossSystems
|
||||
))
|
||||
// {
|
||||
default = self.devShells.${system}.native-clangStdenvPackages;
|
||||
|
||||
@@ -3,6 +3,14 @@
|
||||
# Take a look at ./doc/manual/src/contributing/hacking.md for a detailed
|
||||
# explanation on how to use this file!
|
||||
|
||||
# Pin the shell to bash (anything sufficiently POSIX-y would do)
|
||||
# HACK: We use https://github.com/casey/just#positional-arguments
|
||||
# and `"@$"` to forward arguments to the inner commands.
|
||||
# The reason we require this is that `{{ OPTIONS }}` does not escape any values,
|
||||
# and thus requires one additional level of escaping when running `just` commands with e.g. spaces in them.
|
||||
# just provides no good solution to this problem, so we have to rely on its forwarding of arguments and shell semantics.
|
||||
set shell := ["bash", "-uc"]
|
||||
|
||||
outdir := x"${out:-$PWD/outputs/out}"
|
||||
builddir := "build"
|
||||
|
||||
@@ -10,28 +18,33 @@ builddir := "build"
|
||||
list:
|
||||
just --list
|
||||
|
||||
# Clean build artifacts
|
||||
# Clean build artifacts and outputs.
|
||||
clean:
|
||||
rm -rf {{ builddir }}
|
||||
rm -rf {{ quote(builddir) }}/* {{ quote(builddir) }}/.* {{ quote(outdir) }}/* {{ quote(outdir) }}/.*
|
||||
cargo clean
|
||||
|
||||
# Prepare meson for building.
|
||||
[positional-arguments]
|
||||
setup *OPTIONS:
|
||||
meson setup {{ builddir }} --reconfigure --prefix="{{outdir}}" $mesonFlags {{ OPTIONS }}
|
||||
meson setup {{ builddir }} --reconfigure --prefix="{{outdir}}" $mesonFlags "$@"
|
||||
|
||||
# Build lix with extra options
|
||||
[positional-arguments]
|
||||
build *OPTIONS:
|
||||
meson compile -C {{ builddir }} {{ OPTIONS }}
|
||||
meson compile -C {{ builddir }} "$@"
|
||||
|
||||
alias compile := build
|
||||
|
||||
# `meson install` will automatically build anything that needs to be built to install it.
|
||||
[doc("Install Lix for local development")]
|
||||
[positional-arguments]
|
||||
install *OPTIONS:
|
||||
meson install --quiet -C {{ builddir }} {{ OPTIONS }}
|
||||
meson install --quiet -C {{ builddir }} "$@"
|
||||
|
||||
# Run all tests tests (installs first).
|
||||
[positional-arguments]
|
||||
test *OPTIONS: (install)
|
||||
meson test -C {{ builddir }} --print-errorlogs --max-lines 10000 {{ OPTIONS }}
|
||||
meson test -C {{ builddir }} --print-errorlogs --max-lines 10000 "$@"
|
||||
|
||||
# Run unit tests only
|
||||
test-unit *OPTIONS: (test "--suite" "check")
|
||||
@@ -40,10 +53,15 @@ test-unit *OPTIONS: (test "--suite" "check")
|
||||
test-integration *OPTIONS: (test "--suite" "installcheck" OPTIONS)
|
||||
|
||||
# Run functional2 tests using pytest directly, allowing for additional arguments to be passed to pytest e.g. for more granular test selection
|
||||
[positional-arguments]
|
||||
test-functional2 *OPTIONS:
|
||||
cd tests/functional2 && python -m pytest -v {{ OPTIONS }}
|
||||
cd tests/functional2 && python -m pytest -v "$@"
|
||||
|
||||
alias clang-tidy := lint
|
||||
# special target for cargo because meson cannot be convinced to not mangle cargo test output,
|
||||
# and getting properly colored test output any other way also doesn't look all that possible.
|
||||
[positional-arguments]
|
||||
test-rs *OPTIONS:
|
||||
meson test -C {{ builddir }} --interactive lix-rs-tests "$@"
|
||||
|
||||
# Lint with `clang-tidy`
|
||||
lint:
|
||||
|
||||
@@ -52,7 +52,7 @@ class ExtraFeature:
|
||||
{
|
||||
"\n ".join(
|
||||
[
|
||||
f"- {event.date}, {event.release}: {event.message} [{", ".join([f'[CL {cl}](https://git.lix.systems/c/lix/+/{cl})' for cl in event.cls])}]"
|
||||
f"- {event.date}, {event.release}: {event.message} [{", ".join([f'[CL {cl}](https://gerrit.lix.systems/c/lix/+/{cl})' for cl in event.cls])}]"
|
||||
for event in self.timeline
|
||||
]
|
||||
)
|
||||
|
||||
+32
-112
@@ -5,6 +5,7 @@
|
||||
#include "lix/libutil/file-descriptor.hh"
|
||||
#include "lix/libutil/logging-rpc.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include "lix/libutil/result.hh"
|
||||
#include "lix/libutil/rpc.hh"
|
||||
#include "lix/libutil/types-rpc.hh" // IWYU pragma: keep
|
||||
#include "lix/libutil/types.hh"
|
||||
@@ -50,7 +51,7 @@ struct Instance final : rpc::build_remote::HookInstance::Server
|
||||
|
||||
kj::Promise<void> init(InitContext context) override;
|
||||
|
||||
kj::Promise<void> buildImpl(BuildContext context);
|
||||
kj::Promise<Result<void>> buildImpl(BuildContext context);
|
||||
kj::Promise<void> build(BuildContext context) override;
|
||||
};
|
||||
}
|
||||
@@ -96,7 +97,7 @@ static std::tuple<bool, Machine *, AutoCloseFD> selectBestMachine(
|
||||
uint64_t bestLoad = 0;
|
||||
|
||||
for (auto & m : machines) {
|
||||
debug("considering building on remote machine '%s'", m.storeUri);
|
||||
debug("considering building on remote machine '%s'", m.name);
|
||||
|
||||
if (m.enabled && m.systemSupported(neededSystem) && m.allSupported(requiredFeatures)
|
||||
&& m.mandatoryMet(requiredFeatures))
|
||||
@@ -184,67 +185,6 @@ struct BuilderConnection
|
||||
AutoCloseFD slotLock;
|
||||
std::shared_ptr<Store> sshStore;
|
||||
std::string storeUri;
|
||||
Pipe logPipe;
|
||||
|
||||
// start the thread that reads ssh stderr and turns it into log items.
|
||||
// this future *must* outlive sshStore, otherwise it will never finish
|
||||
kj::Promise<Result<void>> startLogThread(std::string buildDescription, std::string drvPath)
|
||||
try {
|
||||
if (!logPipe.readSide) {
|
||||
co_return result::success();
|
||||
}
|
||||
|
||||
logPipe.writeSide.close();
|
||||
|
||||
// NOTE this is very similar to handleBuilderOutput in DerivationGoal, but unlike
|
||||
// the derivation goal we do not need to handle EIO from a pty here. we also have
|
||||
// no timeouts or limits to keep track of, which makes deduplication less useful.
|
||||
auto act = logger->startActivity(
|
||||
lvlInfo, actBuild, buildDescription, Logger::Fields{drvPath, storeUri, 1, 1}
|
||||
);
|
||||
|
||||
std::map<ActivityId, Activity> activities;
|
||||
|
||||
auto reader = AIO().lowLevelProvider.wrapInputFd(logPipe.readSide.get());
|
||||
|
||||
LogLineSplitter splitter;
|
||||
|
||||
auto flushLine = [&](const std::string & line) {
|
||||
if (const auto state =
|
||||
handleJSONLogMessage(line, act, activities, "the derivation builder"))
|
||||
{
|
||||
return *state;
|
||||
} else {
|
||||
return act.result(resBuildLogLine, line);
|
||||
}
|
||||
};
|
||||
|
||||
auto buf = kj::heapArray<char>(4096);
|
||||
while (true) {
|
||||
const auto got = co_await reader->tryRead(buf.begin(), 1, buf.size());
|
||||
if (got == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::string_view data{buf.begin(), got};
|
||||
while (!data.empty()) {
|
||||
if (auto line = splitter.feed(data)) {
|
||||
if (flushLine(*line) == Logger::BufferState::NeedsFlush) {
|
||||
TRY_AWAIT(act.getLogger().flush());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto line = splitter.finish(); !line.empty()) {
|
||||
(void) flushLine(line);
|
||||
TRY_AWAIT(act.getLogger().flush());
|
||||
}
|
||||
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
};
|
||||
|
||||
struct AcceptedBuild final : rpc::build_remote::HookInstance::AcceptedBuild::Server
|
||||
@@ -261,7 +201,7 @@ struct AcceptedBuild final : rpc::build_remote::HookInstance::AcceptedBuild::Ser
|
||||
{
|
||||
}
|
||||
|
||||
kj::Promise<void> runImpl(RunContext context);
|
||||
kj::Promise<Result<void>> runImpl(RunContext context);
|
||||
kj::Promise<void> run(RunContext context) override;
|
||||
};
|
||||
|
||||
@@ -326,26 +266,15 @@ try {
|
||||
lock.reset();
|
||||
|
||||
std::shared_ptr<Store> sshStore;
|
||||
Pipe logPipe;
|
||||
|
||||
try {
|
||||
auto act = logger->startActivity(
|
||||
lvlTalkative, actUnknown, fmt("connecting to '%s'", bestMachine->storeUri)
|
||||
);
|
||||
auto act =
|
||||
logger->startActivity(lvlTalkative, actUnknown, fmt("connecting to '%s'", bestMachine->name));
|
||||
|
||||
std::tie(sshStore, logPipe) = TRY_AWAIT(bestMachine->openStore());
|
||||
TRY_AWAIT(sshStore->connect());
|
||||
co_return BuilderConnection{
|
||||
std::move(bestSlotLock), sshStore, bestMachine->storeUri, std::move(logPipe)
|
||||
};
|
||||
sshStore = TRY_AWAIT(bestMachine->openStore());
|
||||
co_return BuilderConnection{std::move(bestSlotLock), sshStore, bestMachine->storeUri};
|
||||
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
||||
std::string msg = logPipe.readSide ? chomp(drainFD(logPipe.readSide.get(), false)) : "";
|
||||
printError(
|
||||
"cannot build on '%s': %s%s",
|
||||
bestMachine->storeUri,
|
||||
e.what(),
|
||||
msg.empty() ? "" : ": " + msg
|
||||
);
|
||||
printError("cannot build on '%s': %s", bestMachine->name, e.what());
|
||||
bestMachine->enabled = false;
|
||||
}
|
||||
}
|
||||
@@ -366,7 +295,7 @@ static int main_build_remote(AsyncIoRoot & aio, std::string programName, Strings
|
||||
if (argv.size() != 1)
|
||||
throw UsageError("called without required arguments");
|
||||
|
||||
verbosity = (Verbosity) std::stoll(argv.front());
|
||||
setVerbosity((Verbosity) std::stoll(argv.front()));
|
||||
|
||||
auto conn = aio.kj.lowLevelProvider->wrapUnixSocketFd(1);
|
||||
capnp::TwoPartyServer srv(kj::heap<Instance>());
|
||||
@@ -395,16 +324,14 @@ kj::Promise<void> Instance::init(InitContext context)
|
||||
initPlugins();
|
||||
|
||||
initialized = true;
|
||||
|
||||
context.getResults().initResult().setGood();
|
||||
} catch (...) {
|
||||
RPC_FILL(context.getResults(), initResult, std::current_exception());
|
||||
rpc::rethrow_as_rpc_error();
|
||||
}
|
||||
|
||||
return kj::READY_NOW;
|
||||
}
|
||||
|
||||
kj::Promise<void> Instance::buildImpl(BuildContext context)
|
||||
kj::Promise<Result<void>> Instance::buildImpl(BuildContext context)
|
||||
try {
|
||||
if (!initialized) {
|
||||
throw Error("build hook not fully initialized");
|
||||
@@ -431,8 +358,8 @@ try {
|
||||
debug("got %d remote builders", machines.size());
|
||||
|
||||
if (machines.empty()) {
|
||||
context.getResults().initResult().initGood().setDeclinePermanently();
|
||||
co_return;
|
||||
context.getResults().initResult().setDeclinePermanently();
|
||||
co_return result::success();
|
||||
}
|
||||
|
||||
auto amWilling = context.getParams().getAmWilling();
|
||||
@@ -448,21 +375,23 @@ try {
|
||||
if (auto immediateResponse = std::get_if<BuildRejected>(&result)) {
|
||||
switch (*immediateResponse) {
|
||||
case BuildRejected::Temporarily:
|
||||
context.getResults().initResult().initGood().setPostpone();
|
||||
co_return;
|
||||
context.getResults().initResult().setPostpone();
|
||||
co_return result::success();
|
||||
case BuildRejected::Permanently:
|
||||
context.getResults().initResult().initGood().setDecline();
|
||||
co_return;
|
||||
context.getResults().initResult().setDecline();
|
||||
co_return result::success();
|
||||
}
|
||||
}
|
||||
|
||||
auto builder = std::get_if<BuilderConnection>(&result);
|
||||
assert(builder);
|
||||
|
||||
auto ac = context.getResults().initResult().initGood().initAccept();
|
||||
auto ac = context.getResults().initResult().initAccept();
|
||||
ac.setMachine(kj::heap<AcceptedBuild>(store, drvPath, std::move(*builder)));
|
||||
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
RPC_FILL(context.getResults(), initResult, std::current_exception());
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<void> Instance::build(BuildContext context)
|
||||
@@ -471,11 +400,12 @@ try {
|
||||
throw Error("build hooks can only accept a single job");
|
||||
}
|
||||
used = true; // lock out other rpc calls during processing
|
||||
co_await buildImpl(context);
|
||||
auto result = co_await buildImpl(context);
|
||||
TRY_AWAIT(logger->flush());
|
||||
used = context.getResults().getResult().getGood().isAccept();
|
||||
used = result.has_value() && context.getResults().getResult().isAccept();
|
||||
result.value();
|
||||
} catch (...) {
|
||||
RPC_FILL(context.getResults(), getResult, std::current_exception());
|
||||
rpc::rethrow_as_rpc_error();
|
||||
}
|
||||
|
||||
kj::Promise<void> AcceptedBuild::run(RunContext context)
|
||||
@@ -493,23 +423,17 @@ kj::Promise<void> AcceptedBuild::run(RunContext context)
|
||||
throw Error("build hooks builds are single-use items");
|
||||
}
|
||||
used = true;
|
||||
co_await runImpl(context);
|
||||
auto result = co_await runImpl(context);
|
||||
TRY_AWAIT(logger->flush());
|
||||
result.value();
|
||||
} catch (...) {
|
||||
RPC_FILL(context.getResults(), getResult, std::current_exception());
|
||||
rpc::rethrow_as_rpc_error();
|
||||
}
|
||||
}
|
||||
|
||||
kj::Promise<void> AcceptedBuild::runImpl(RunContext context)
|
||||
kj::Promise<Result<void>> AcceptedBuild::runImpl(RunContext context)
|
||||
{
|
||||
try {
|
||||
auto logHandler = builder.startLogThread(
|
||||
fmt("%s on '%s'",
|
||||
rpc::to<std::string_view>(context.getParams().getDescription()),
|
||||
builder.storeUri),
|
||||
store->printStorePath(drvPath)
|
||||
);
|
||||
|
||||
auto & sshStore = builder.sshStore;
|
||||
auto & storeUri = builder.storeUri;
|
||||
|
||||
@@ -619,13 +543,9 @@ kj::Promise<void> AcceptedBuild::runImpl(RunContext context)
|
||||
);
|
||||
}
|
||||
|
||||
// drop store connection, let log handler process any remaining input
|
||||
builder.sshStore = nullptr;
|
||||
TRY_AWAIT(logHandler);
|
||||
|
||||
context.getResults().initResult().setGood();
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
RPC_FILL(context.getResults(), initResult, std::current_exception());
|
||||
co_return result::current_exception();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ static int main_builtin_builder(AsyncIoRoot & aio, std::string programName, Stri
|
||||
};
|
||||
|
||||
if (auto val = string2Int<int>(getArg("verbosity"))) {
|
||||
verbosity = verbosityFromIntClamped(*val);
|
||||
setVerbosity(verbosityFromIntClamped(*val));
|
||||
} else {
|
||||
throw Error("expected a verbosity argument");
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ static int main_nix_copy_closure(AsyncIoRoot & aio, std::string programName, Str
|
||||
for (auto & path : storePaths)
|
||||
storePaths2.insert(from->followLinksToStorePath(path));
|
||||
|
||||
aio.blockOn(copyClosure(*from, *to, storePaths2, NoRepair, NoCheckSigs, useSubstitutes));
|
||||
aio.blockOn(copyClosure(*from, *to, storePaths2, NoRepair, NoCheckSigs, useSubstitutes, includeOutputs));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -948,7 +948,7 @@ opServe(std::shared_ptr<Store> store, AsyncIoRoot & aio, Strings opFlags, String
|
||||
auto getBuildSettings = [&]() {
|
||||
// FIXME: changing options here doesn't work if we're
|
||||
// building through the daemon.
|
||||
verbosity = lvlError;
|
||||
setVerbosity(lvlError);
|
||||
settings.keepLog.override(false);
|
||||
settings.useSubstitutes.override(false);
|
||||
settings.maxSilentTime.override(readNum<unsigned>(in));
|
||||
|
||||
+60
-215
@@ -1,251 +1,101 @@
|
||||
#include "lix/libutil/c-calls.hh"
|
||||
#include "libutil/fmt.hh"
|
||||
#include "libutil/terminal.hh"
|
||||
#include "lix/libutil/error.hh"
|
||||
#include "lix/libutil/file-system.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include <csignal>
|
||||
#include <cstdio>
|
||||
#include "lix/lix-rs/main.gen.hh"
|
||||
#include "lix/lix-rs/utils.hh"
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <cerrno>
|
||||
|
||||
// editline < 1.15.2 don't wrap their API for C++ usage
|
||||
// (added in https://github.com/troglobit/editline/commit/91398ceb3427b730995357e9d120539fb9bb7461).
|
||||
// This results in linker errors due to to name-mangling of editline C symbols.
|
||||
// For compatibility with these versions, we wrap the API here
|
||||
// (wrapping multiple times on newer versions is no problem).
|
||||
extern "C" {
|
||||
#include <editline.h>
|
||||
}
|
||||
|
||||
#include "lix/libutil/finally.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
#include "lix/libcmd/repl-interacter.hh"
|
||||
|
||||
namespace rust {
|
||||
Vec<String> Impl<lix::repl::CxxCompleter, lix::repl::ReplCompleter>::complete(
|
||||
Ref<lix::repl::CxxCompleter> self, Ref<Str> input
|
||||
)
|
||||
try {
|
||||
auto s = to_std_string(input);
|
||||
|
||||
auto result = std::vec::Vec<std::string::String>::new_();
|
||||
for (auto & possible : self.cpp().completePrefix(s)) {
|
||||
result.push(rust::to_string(possible));
|
||||
}
|
||||
return result;
|
||||
} catch (...) {
|
||||
// the completer should have logged anything interesting.
|
||||
return std::vec::Vec<std::string::String>::new_();
|
||||
}
|
||||
}
|
||||
|
||||
namespace nix {
|
||||
|
||||
namespace {
|
||||
// Used to communicate to NixRepl::getLine whether a signal occurred in ::readline.
|
||||
volatile sig_atomic_t g_signal_received = 0;
|
||||
ReadlineLikeInteracter::ReadlineLikeInteracter(std::string historyFile) : historyFile(historyFile) {}
|
||||
|
||||
void sigintHandler(int signo)
|
||||
void ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
||||
{
|
||||
g_signal_received = signo;
|
||||
}
|
||||
|
||||
static detail::ReplCompleterMixin * curRepl; // ugly
|
||||
|
||||
/**
|
||||
* @return a null-terminated list of completions as expected by `el_print_columns`
|
||||
*/
|
||||
char ** copyCompletions(const StringSet& possible)
|
||||
{
|
||||
int ac = 0;
|
||||
char ** vp = nullptr;
|
||||
|
||||
auto check = [&](auto * p) {
|
||||
if (!p) {
|
||||
if (vp) {
|
||||
while (--ac >= 0)
|
||||
free(vp[ac]);
|
||||
free(static_cast<void *>(vp));
|
||||
}
|
||||
throw Error("allocation failure");
|
||||
}
|
||||
return p;
|
||||
};
|
||||
vp = check(static_cast<char **>(malloc(possible.size() * sizeof(char *))));
|
||||
for (auto & p : possible)
|
||||
vp[ac++] = check(strdup(p.c_str()));
|
||||
|
||||
return vp;
|
||||
}
|
||||
|
||||
// Instead of using the readline-provided prefix, do our own tokenization
|
||||
// to avoid the default behavior of treating dots/quotes as word boundaries.
|
||||
// See the definition of SEPS for what it treats as a boundary:
|
||||
// https://github.com/troglobit/editline/blob/caf4b3c0ce3b0785791198b11de6f3134e9f05d8/src/editline.c
|
||||
std::string getLastTokenBeforeCursor()
|
||||
{
|
||||
std::string_view line{rl_line_buffer, static_cast<size_t>(rl_point)};
|
||||
|
||||
auto tokens = tokenizeString<std::vector<std::string>>(
|
||||
line,
|
||||
// Same as editline's SEPS, except for double and single quotes:
|
||||
"#$&()*:;<=>?[\\]^`{|}~\n\t "
|
||||
);
|
||||
|
||||
if (tokens.empty()) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return tokens.back();
|
||||
}
|
||||
|
||||
// Sometimes inserting text or listing possible completions has a side effect
|
||||
// of hiding the text after the cursor (even though it remains in the buffer).
|
||||
// This helper just refreshes the display while keeping the cursor in place.
|
||||
//
|
||||
// Inserting text also sometimes moves the whole buffer down one line, usually
|
||||
// if the cursor is inside a quoted attr name. I'm not sure why (vs unquoted)
|
||||
// but it still seems to work pretty well and is just a visual artifact.
|
||||
el_status_t redisplay()
|
||||
{
|
||||
int cursorPos = rl_point;
|
||||
rl_refresh_line(0, 0);
|
||||
rl_point = cursorPos;
|
||||
return (rl_point == rl_end) ? CSstay : CSmove;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
|
||||
static el_status_t doCompletion() {
|
||||
auto s = getLastTokenBeforeCursor();
|
||||
auto possible = curRepl->completePrefix(s);
|
||||
|
||||
if (possible.empty()) {
|
||||
return el_ring_bell();
|
||||
}
|
||||
|
||||
if (possible.size() == 1) {
|
||||
const auto completion = *possible.cbegin();
|
||||
if (completion.size() > s.size()) {
|
||||
rl_insert_text(requireCString(completion.substr(s.size())));
|
||||
return redisplay();
|
||||
}
|
||||
|
||||
return el_ring_bell();
|
||||
}
|
||||
|
||||
auto checkAllHaveSameAt = [&](size_t pos) {
|
||||
auto & first = *possible.begin();
|
||||
for (auto & p : possible) {
|
||||
if (p.size() <= pos || p[pos] != first[pos]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
size_t start = s.size();
|
||||
size_t len = 0;
|
||||
while (checkAllHaveSameAt(start + len)) {
|
||||
++len;
|
||||
}
|
||||
if (len > 0) {
|
||||
auto commonPrefix = possible.begin()->substr(start, len);
|
||||
rl_insert_text(requireCString(commonPrefix));
|
||||
el_ring_bell();
|
||||
return redisplay();
|
||||
}
|
||||
|
||||
char** columns = copyCompletions(possible);
|
||||
el_print_columns(possible.size(), columns);
|
||||
return redisplay();
|
||||
}
|
||||
|
||||
ReadlineLikeInteracter::Guard ReadlineLikeInteracter::init(detail::ReplCompleterMixin * repl)
|
||||
{
|
||||
// Allow nix-repl specific settings in .inputrc
|
||||
rl_readline_name = "nix-repl";
|
||||
try {
|
||||
createDirs(dirOf(historyFile));
|
||||
} catch (SysError & e) {
|
||||
logWarning(e.info());
|
||||
}
|
||||
el_hist_size = 1000;
|
||||
read_history(requireCString(historyFile));
|
||||
auto oldRepl = curRepl;
|
||||
curRepl = repl;
|
||||
Guard restoreRepl([oldRepl] { curRepl = oldRepl; });
|
||||
// editline does its own escaping of completions, so we rebind tab
|
||||
// to our own completion function to skip that and do nix escaping
|
||||
// instead of shell escaping.
|
||||
el_bind_key(CTL('I'), doCompletion);
|
||||
return restoreRepl;
|
||||
|
||||
auto rl = repl::Rustyline::new_(rust::to_string(historyFile).as_str(), *repl);
|
||||
match_result(
|
||||
std::move(rl),
|
||||
[&](repl::Rustyline ok) { this->rl = std::make_unique<repl::Rustyline>(std::move(ok)); },
|
||||
[&](rust::Box<rust::Dyn<rust::std::error::Error>> err) {
|
||||
throw Error("%s", Uncolored(to_std_string(err.to_string())));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
static constexpr const char * promptForType(ReplPromptType promptType)
|
||||
static rust::Ref<rust::Str> promptForType(ReplPromptType promptType)
|
||||
{
|
||||
switch (promptType) {
|
||||
case ReplPromptType::ReplPrompt:
|
||||
return "nix-repl> ";
|
||||
return "nix-repl> "_rs;
|
||||
case ReplPromptType::ContinuationPrompt:
|
||||
return " ";
|
||||
return " "_rs;
|
||||
}
|
||||
assert(false);
|
||||
}
|
||||
|
||||
bool ReadlineLikeInteracter::getLine(std::string & input, ReplPromptType promptType)
|
||||
{
|
||||
struct sigaction act, old;
|
||||
sigset_t savedSignalMask, set;
|
||||
auto s = rl->ask(promptForType(promptType));
|
||||
|
||||
auto setupSignals = [&]() {
|
||||
act.sa_handler = sigintHandler;
|
||||
sigfillset(&act.sa_mask);
|
||||
act.sa_flags = 0;
|
||||
if (sigaction(SIGINT, &act, &old))
|
||||
throw SysError("installing handler for SIGINT");
|
||||
// rustyline temporarily sets a SIGWINCH handler
|
||||
KJ_DEFER(invalidateWindowSize());
|
||||
|
||||
sigemptyset(&set);
|
||||
sigaddset(&set, SIGINT);
|
||||
if (sigprocmask(SIG_UNBLOCK, &set, &savedSignalMask))
|
||||
throw SysError("unblocking SIGINT");
|
||||
};
|
||||
auto restoreSignals = [&]() {
|
||||
if (sigprocmask(SIG_SETMASK, &savedSignalMask, nullptr))
|
||||
throw SysError("restoring signals");
|
||||
return match_result(
|
||||
std::move(s),
|
||||
[&](rust::String ok) {
|
||||
input += to_std_string(ok);
|
||||
input += '\n';
|
||||
return true;
|
||||
},
|
||||
[&](rust::rustyline::error::ReadlineError err) {
|
||||
if (err.matches_Interrupted()) {
|
||||
input.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (sigaction(SIGINT, &old, 0))
|
||||
throw SysError("restoring handler for SIGINT");
|
||||
};
|
||||
if (err.matches_Eof()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
setupSignals();
|
||||
char * s = readline(promptForType(promptType)); // NOLINT(lix-unsafe-c-calls)
|
||||
Finally doFree([&]() { free(s); });
|
||||
restoreSignals();
|
||||
|
||||
if (g_signal_received) {
|
||||
g_signal_received = 0;
|
||||
input.clear();
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!s)
|
||||
return false;
|
||||
|
||||
this->writeHistory();
|
||||
input += s;
|
||||
input += '\n';
|
||||
return true;
|
||||
throw Error("%s", Uncolored(to_std_string(err.into().to_string())));
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
void ReadlineLikeInteracter::writeHistory()
|
||||
{
|
||||
int ret = write_history(requireCString(historyFile));
|
||||
int writeHistErr = errno;
|
||||
|
||||
if (ret == 0) {
|
||||
return;
|
||||
if (rl) {
|
||||
rl->write_history();
|
||||
}
|
||||
|
||||
// If the open fails, editline returns EOF. If the close fails, editline
|
||||
// forwards the return value of fclose(), which is EOF on error.
|
||||
// readline however, returns the errno.
|
||||
// So if we didn't get exactly EOF, then consider the return value the error
|
||||
// code; otherwise use the errno we saved above.
|
||||
// https://github.com/troglobit/editline/issues/66
|
||||
if (ret != EOF) {
|
||||
writeHistErr = ret;
|
||||
}
|
||||
|
||||
// In any of these cases, we should explicitly ignore the error, but log
|
||||
// them so the user isn't confused why their history is getting eaten.
|
||||
|
||||
std::string_view const errMsg(std::strerror(writeHistErr));
|
||||
printTaggedWarning("ignoring error writing repl history to %s: %s", this->historyFile, errMsg);
|
||||
}
|
||||
|
||||
ReadlineLikeInteracter::~ReadlineLikeInteracter()
|
||||
@@ -253,11 +103,6 @@ ReadlineLikeInteracter::~ReadlineLikeInteracter()
|
||||
this->writeHistory();
|
||||
}
|
||||
|
||||
AutomationInteracter::Guard AutomationInteracter::init(detail::ReplCompleterMixin *)
|
||||
{
|
||||
return Guard([] {});
|
||||
}
|
||||
|
||||
// ASCII ENQ character
|
||||
constexpr const char * automationPrompt = "\x05";
|
||||
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
#pragma once
|
||||
/// @file
|
||||
|
||||
#include "lix/libutil/finally.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
#include <functional>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
namespace rust::lix::repl {
|
||||
struct Rustyline;
|
||||
}
|
||||
|
||||
namespace nix {
|
||||
|
||||
namespace detail {
|
||||
@@ -24,9 +27,7 @@ enum class ReplPromptType {
|
||||
class ReplInteracter
|
||||
{
|
||||
public:
|
||||
using Guard = Finally<std::function<void()>>;
|
||||
|
||||
virtual Guard init(detail::ReplCompleterMixin * repl) = 0;
|
||||
virtual void init(detail::ReplCompleterMixin * repl) {}
|
||||
/** Returns a boolean of whether the interacter got EOF */
|
||||
virtual bool getLine(std::string & input, ReplPromptType promptType) = 0;
|
||||
virtual ~ReplInteracter(){};
|
||||
@@ -35,12 +36,12 @@ public:
|
||||
class ReadlineLikeInteracter final : public ReplInteracter
|
||||
{
|
||||
std::string historyFile;
|
||||
std::unique_ptr<rust::lix::repl::Rustyline> rl;
|
||||
|
||||
public:
|
||||
ReadlineLikeInteracter(std::string historyFile)
|
||||
: historyFile(historyFile)
|
||||
{
|
||||
}
|
||||
virtual Guard init(detail::ReplCompleterMixin * repl) override;
|
||||
ReadlineLikeInteracter(std::string historyFile);
|
||||
|
||||
virtual void init(detail::ReplCompleterMixin * repl) override;
|
||||
virtual bool getLine(std::string & input, ReplPromptType promptType) override;
|
||||
/** Writes the current history to the history file.
|
||||
*
|
||||
@@ -54,7 +55,6 @@ class AutomationInteracter final : public ReplInteracter
|
||||
{
|
||||
public:
|
||||
AutomationInteracter() = default;
|
||||
virtual Guard init(detail::ReplCompleterMixin * repl) override;
|
||||
virtual bool getLine(std::string & input, ReplPromptType promptType) override;
|
||||
virtual ~AutomationInteracter() override = default;
|
||||
};
|
||||
|
||||
+22
-19
@@ -1,6 +1,5 @@
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <editline.h>
|
||||
#include <iostream>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
@@ -380,7 +379,7 @@ ReplExitStatus NixRepl::mainLoop()
|
||||
std::list<ReplLoadable> loadables = std::exchange(loaded, {});
|
||||
loadFiles(loadables);
|
||||
|
||||
auto _guard = interacter->init(static_cast<detail::ReplCompleterMixin *>(this));
|
||||
interacter->init(static_cast<detail::ReplCompleterMixin *>(this));
|
||||
|
||||
/* Stop the progress bar because it interferes with the display of
|
||||
the repl. */
|
||||
@@ -441,15 +440,10 @@ StringSet NixRepl::completePrefix(const std::string &prefix)
|
||||
{
|
||||
StringSet completions;
|
||||
|
||||
// We should only complete colon commands if there's a colon at the beginning,
|
||||
// but editline (for... whatever reason) doesn't *give* us the colon in the
|
||||
// completion callback. If the user types :rel<TAB>, `prefix` will only be `rel`.
|
||||
// Luckily, editline provides a global variable for its current buffer, so we can
|
||||
// check for the presence of a colon there.
|
||||
if (rl_line_buffer != nullptr && rl_line_buffer[0] == ':') {
|
||||
if (prefix.starts_with(':')) {
|
||||
for (auto const & [colonCmd, cmd] : registeredCommands) {
|
||||
if ((!cmd->attributes.debugModeOnly || inDebugger()) && colonCmd.starts_with(prefix)) {
|
||||
completions.insert(std::string(colonCmd));
|
||||
if ((!cmd->attributes.debugModeOnly || inDebugger()) && colonCmd.starts_with(prefix.substr(1))) {
|
||||
completions.insert(":" + colonCmd);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,17 +693,26 @@ void NixRepl::initDebugBuiltinCommands()
|
||||
addCommand(
|
||||
"backtrace",
|
||||
[](NixRepl & repl, const std::string & _arg) {
|
||||
auto traces = repl.evaluator.debug->traces();
|
||||
for (const auto & [idx, i] : enumerate(traces)) {
|
||||
std::cout << "\n" << ANSI_BLUE << idx << ANSI_NORMAL << ": ";
|
||||
showDebugTrace(std::cout, repl.evaluator.positions, *i);
|
||||
auto tracesGenerator = repl.evaluator.debug->traces();
|
||||
|
||||
// since we want to print the stack trace in reverse order,
|
||||
// we have to first traverse all frames and accumulate them
|
||||
// in a list (in which we store each new trace at the /beginning/)
|
||||
std::list<std::pair<size_t, const DebugTrace *>> reversedTraces;
|
||||
for (const auto trace : tracesGenerator) {
|
||||
// because the original traces are indexed from 0 upto N,
|
||||
// this gives us their original index
|
||||
auto idx = reversedTraces.size();
|
||||
reversedTraces.push_front({idx, trace});
|
||||
}
|
||||
|
||||
for (const auto & [traceIdx, trace] : reversedTraces) {
|
||||
std::cout << "\n" << ANSI_BLUE << traceIdx << ANSI_NORMAL << ": ";
|
||||
showDebugTrace(std::cout, repl.evaluator.positions, *trace);
|
||||
}
|
||||
return ProcessLineResult::PromptAgain;
|
||||
},
|
||||
{.aliases = {"bt"},
|
||||
.debugModeOnly = true,
|
||||
.help = "Show trace stack",
|
||||
.section = "Debug mode"}
|
||||
{.aliases = {"bt"}, .debugModeOnly = true, .help = "Show trace stack", .section = "Debug mode"}
|
||||
);
|
||||
|
||||
addCommand(
|
||||
@@ -817,7 +820,7 @@ void NixRepl::initDebugBuiltinCommands()
|
||||
{.aliases = {"st"},
|
||||
.debugModeOnly = true,
|
||||
.help = "Show current trace. If an integer is provided, this switches to that stack "
|
||||
"beforehand. If the integer has an explicit + or - sign, it is treated as"
|
||||
"beforehand. If the integer has an explicit + or - sign, it is treated as "
|
||||
"relative to the current stack index.",
|
||||
.section = "Debug mode",
|
||||
.positionalArgsSpecifiers = {{.placeholderText = "integer index", .optional = true}}}
|
||||
|
||||
@@ -36,9 +36,12 @@ int helperMain(const char * name, std::span<char *> args) noexcept
|
||||
other things, determines if kill(-1, signo) affects the
|
||||
calling process. In the OSX libc, it's set to true,
|
||||
which means "follow POSIX", which we don't want here */
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
if (syscall(SYS_kill, -1, SIGKILL, false) == 0) {
|
||||
break;
|
||||
}
|
||||
#pragma clang diagnostic pop
|
||||
#else
|
||||
if (kill(-1, SIGKILL) == 0) {
|
||||
break;
|
||||
|
||||
@@ -59,7 +59,7 @@ static void setPersonality(std::string_view system)
|
||||
|
||||
bool pathExists(const fs::path & path)
|
||||
{
|
||||
return fs::exists(fs::symlink_status(path));
|
||||
return fs::exists(fs::symlink_status(path)); // NOLINT(lix-foreign-exceptions): caught by main
|
||||
}
|
||||
|
||||
void bindPath(const fs::path & source, const fs::path & target, bool optional = false)
|
||||
@@ -72,7 +72,7 @@ void bindPath(const fs::path & source, const fs::path & target, bool optional =
|
||||
}
|
||||
};
|
||||
|
||||
auto st = fs::symlink_status(source);
|
||||
auto st = fs::symlink_status(source); // NOLINT(lix-foreign-exceptions): caught by main
|
||||
if (st.type() == fs::file_type::not_found) {
|
||||
if (optional) {
|
||||
return;
|
||||
@@ -82,14 +82,14 @@ void bindPath(const fs::path & source, const fs::path & target, bool optional =
|
||||
}
|
||||
|
||||
if (st.type() == fs::file_type::directory) {
|
||||
fs::create_directories(target);
|
||||
fs::create_directories(target); // NOLINT(lix-foreign-exceptions): caught by main
|
||||
bindMount();
|
||||
} else if (st.type() == fs::file_type::symlink) {
|
||||
// Symlinks can (apparently) not be bind-mounted, so just copy it
|
||||
fs::create_directories(target.parent_path());
|
||||
fs::copy_symlink(source, target);
|
||||
fs::create_directories(target.parent_path()); // NOLINT(lix-foreign-exceptions): caught by main
|
||||
fs::copy_symlink(source, target); // NOLINT(lix-foreign-exceptions): caught by main
|
||||
} else {
|
||||
fs::create_directories(target.parent_path());
|
||||
fs::create_directories(target.parent_path()); // NOLINT(lix-foreign-exceptions): caught by main
|
||||
if (kj::AutoCloseFd file{open(target.c_str(), O_RDWR | O_CREAT, 0644)}; file == nullptr) {
|
||||
throw SysError("could not create %s", target);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,17 @@
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
|
||||
LIBEXEC_HELPER(4)
|
||||
LIBEXEC_HELPER(5)
|
||||
|
||||
static int resultFd = -1;
|
||||
|
||||
static void sendResult(int result)
|
||||
{
|
||||
// NOTE posix says pipe writes smaller than PIPE_BUF must be atomic, so this either
|
||||
// succeeds or fails (pipe bufs of four bytes make no sense at all for our systems)
|
||||
int error = result;
|
||||
DIE_UNLESS_SYS("writing result", ::write(resultFd, &error, sizeof(error)));
|
||||
}
|
||||
|
||||
int helperMain(const char *, std::span<char *> args) noexcept
|
||||
{
|
||||
@@ -10,8 +20,12 @@ int helperMain(const char *, std::span<char *> args) noexcept
|
||||
std::string_view method = args[1];
|
||||
const auto dir = args[2];
|
||||
const auto name = args[3];
|
||||
resultFd = argToInt<int>("result-fd", args[4]);
|
||||
|
||||
DIE_UNLESS_SYS("chdir", chdir(dir));
|
||||
if (chdir(dir)) {
|
||||
sendResult(errno);
|
||||
return 0;
|
||||
}
|
||||
|
||||
struct sockaddr_un addr;
|
||||
addr.sun_family = AF_UNIX;
|
||||
@@ -23,9 +37,11 @@ int helperMain(const char *, std::span<char *> args) noexcept
|
||||
}
|
||||
|
||||
if (method == "bind") {
|
||||
DIE_UNLESS_SYS("bind", bind(socket, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)));
|
||||
auto result = bind(socket, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) ? errno : 0;
|
||||
sendResult(result);
|
||||
} else if (method == "connect") {
|
||||
DIE_UNLESS_SYS("connect", connect(socket, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)));
|
||||
auto result = connect(socket, reinterpret_cast<struct sockaddr *>(&addr), sizeof(addr)) ? errno : 0;
|
||||
sendResult(result);
|
||||
} else {
|
||||
die(std::format("invalid method %s", method));
|
||||
}
|
||||
|
||||
@@ -23,70 +23,14 @@ countDown 2
|
||||
Then, evaluating the file will give the following stack trace:
|
||||
|
||||
```console
|
||||
$ nix-instantiate --show-trace err.nix
|
||||
$ nix-instantiate err.nix
|
||||
error:
|
||||
… from call site
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:9:1:
|
||||
8| in
|
||||
9| countDown 2
|
||||
| ^
|
||||
10|
|
||||
|
||||
… while calling 'countDown'
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:3:5:
|
||||
2| countDown =
|
||||
3| n:
|
||||
| ^
|
||||
4| if n == 0 then
|
||||
|
||||
… while calling the 'addErrorContext' builtin
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:7:7:
|
||||
6| else
|
||||
7| builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1));
|
||||
| ^
|
||||
8| in
|
||||
|
||||
… while counting down; n = 2
|
||||
|
||||
… from call site
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:7:80:
|
||||
6| else
|
||||
7| builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1));
|
||||
| ^
|
||||
8| in
|
||||
|
||||
… while calling 'countDown'
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:3:5:
|
||||
2| countDown =
|
||||
3| n:
|
||||
| ^
|
||||
4| if n == 0 then
|
||||
|
||||
… while calling the 'addErrorContext' builtin
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:7:7:
|
||||
6| else
|
||||
7| builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1));
|
||||
| ^
|
||||
8| in
|
||||
|
||||
… while counting down; n = 1
|
||||
|
||||
… from call site
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:7:80:
|
||||
6| else
|
||||
7| builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1));
|
||||
| ^
|
||||
8| in
|
||||
|
||||
… while calling 'countDown'
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:3:5:
|
||||
2| countDown =
|
||||
3| n:
|
||||
| ^
|
||||
4| if n == 0 then
|
||||
|
||||
… caused by explicit throw
|
||||
at /home/plop/git.lix.systems/lix-project/lix/err.nix:5:7:
|
||||
at err.nix:5:7:
|
||||
4| if n == 0 then
|
||||
5| throw "kaboom"
|
||||
| ^
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: ceil
|
||||
args: [double]
|
||||
args: [number]
|
||||
---
|
||||
Converts an IEEE-754 double-precision floating-point number (*double*) to
|
||||
the next higher integer.
|
||||
Returns an integer value containing the smallest integer greater than or equal to *number* (which is either a floating-point or integer value).
|
||||
|
||||
If the datatype is neither an integer nor a "float", an evaluation error will be
|
||||
thrown.
|
||||
If the result is out of range for the integer type, such as NaN, infinity, or a number with magnitude out of range, Lix throws an evaluation error.
|
||||
|
||||
Lix currently throws an evaluation error for some *integer* inputs between 2\*\*52 and 2\*\*63 - 1 as those previously experienced floating-point precision loss due to a Nix bug (https://github.com/NixOS/nix/issues/12899).
|
||||
In a future release, such integers will be passed through.
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
---
|
||||
name: floor
|
||||
args: [double]
|
||||
args: [number]
|
||||
---
|
||||
Converts an IEEE-754 double-precision floating-point number (*double*) to
|
||||
the next lower integer.
|
||||
Returns an integer value containing the largest integer less than or equal to *number* (which is either a float or integer).
|
||||
|
||||
If the datatype is neither an integer nor a "float", an evaluation error will be
|
||||
thrown.
|
||||
If the result is out of range for the integer type, such as NaN, infinity, or a number with magnitude out of range, Lix throws an evaluation error.
|
||||
|
||||
Lix currently throws an evaluation error for some *integer* inputs between 2\*\*52 and 2\*\*63 - 1 as those previously experienced floating-point precision loss due to a Nix bug (https://github.com/NixOS/nix/issues/12899).
|
||||
In a future release, such integers will be passed through.
|
||||
|
||||
@@ -53,7 +53,7 @@ public:
|
||||
ref<AttrCursor> getRoot();
|
||||
};
|
||||
|
||||
enum AttrType {
|
||||
enum AttrType : uint8_t {
|
||||
Placeholder = 0,
|
||||
FullAttrs = 1,
|
||||
String = 2,
|
||||
|
||||
+29
-24
@@ -1,4 +1,5 @@
|
||||
#include "eval.hh"
|
||||
#include "libexpr/nixexpr.hh"
|
||||
#include "primops.hh"
|
||||
#include "gc-small-vector.hh"
|
||||
|
||||
@@ -10,14 +11,19 @@
|
||||
|
||||
namespace nix {
|
||||
|
||||
Value Expr::makeThunk(Evaluator & ctx, Env & env)
|
||||
{
|
||||
ctx.stats.nrThunks++;
|
||||
return {NewValueAs::thunk, ctx.mem, env, *this};
|
||||
}
|
||||
|
||||
/* Create a thunk for the delayed computation of the given expression
|
||||
in the given environment. But if the expression is a variable,
|
||||
then look it up right away. This significantly reduces the number
|
||||
of thunks allocated. */
|
||||
Value Expr::maybeThunk(EvalState & state, Env & env)
|
||||
{
|
||||
state.ctx.stats.nrThunks++;
|
||||
return {NewValueAs::thunk, state.ctx.mem, env, *this};
|
||||
return makeThunk(state.ctx, env);
|
||||
}
|
||||
|
||||
Value ExprVar::maybeThunk(EvalState & state, Env & env)
|
||||
@@ -99,18 +105,10 @@ Value ExprSet::eval(EvalState & state, Env & env)
|
||||
in the original environment. */
|
||||
Displacement displ = 0;
|
||||
for (auto & i : attrs) {
|
||||
Value vAttr;
|
||||
if (hasOverrides && i.second.kind != ExprAttrs::AttrDef::Kind::Inherited) {
|
||||
vAttr = {
|
||||
NewValueAs::thunk,
|
||||
state.ctx.mem,
|
||||
*i.second.chooseByKind(&env2, &env, inheritEnv),
|
||||
*i.second.e
|
||||
};
|
||||
state.ctx.stats.nrThunks++;
|
||||
} else {
|
||||
vAttr = i.second.e->maybeThunk(state, *i.second.chooseByKind(&env2, &env, inheritEnv));
|
||||
}
|
||||
Env & thunkEnv = *i.second.chooseByKind(&env2, &env, inheritEnv);
|
||||
Value vAttr = hasOverrides && i.second.kind != ExprAttrs::AttrDef::Kind::Inherited
|
||||
? i.second.e->makeThunk(state.ctx, thunkEnv)
|
||||
: i.second.e->maybeThunk(state, thunkEnv);
|
||||
env2.values[displ++] = vAttr;
|
||||
v.attrs()->push_back(Attr(i.first, vAttr, i.second.pos));
|
||||
}
|
||||
@@ -358,12 +356,14 @@ Value ExprOpConcatLists::eval(EvalState & state, Env & env)
|
||||
{
|
||||
state.ctx.stats.nrListConcats++;
|
||||
|
||||
/* We don't call into `concatLists` as that loses the position information of the expressions. */
|
||||
/* We could simply call into `state.concatLists`, but that would add a redundant trace to our errors,
|
||||
* and to fix that we would need to make the error on it and `forceList` optional, and *sigh*
|
||||
*/
|
||||
|
||||
Value v1 = e1->eval(state, env);
|
||||
state.checkList(v1, env, *e1);
|
||||
state.checkList(v1, env, *this); // Pass in `this` instead of `e1` to make the error point to the `++`
|
||||
Value v2 = e2->eval(state, env);
|
||||
state.checkList(v2, env, *e2);
|
||||
state.checkList(v2, env, *this); // Pass in `this` instead of `e2` to make the error point to the `++`
|
||||
|
||||
size_t l1 = v1.listSize(), l2 = v2.listSize(), len = l1 + l2;
|
||||
|
||||
@@ -436,7 +436,7 @@ Value ExprConcatStrings::eval(EvalState & state, Env & env)
|
||||
} else {
|
||||
state.ctx.errors
|
||||
.make<EvalError>("integer overflow in adding %1% + %2%", n, vTmp.integer())
|
||||
.atPos(i_pos)
|
||||
.atPos(isInterpolation ? i_pos : pos)
|
||||
.debugThrow();
|
||||
}
|
||||
} else if (vTmp.type() == nFloat) {
|
||||
@@ -446,7 +446,7 @@ Value ExprConcatStrings::eval(EvalState & state, Env & env)
|
||||
nf += vTmp.fpoint();
|
||||
} else {
|
||||
state.ctx.errors.make<EvalError>("cannot add %1% to an integer", showType(vTmp))
|
||||
.atPos(i_pos)
|
||||
.atPos(isInterpolation ? i_pos : pos)
|
||||
.withFrame(env, *this)
|
||||
.debugThrow();
|
||||
}
|
||||
@@ -457,7 +457,7 @@ Value ExprConcatStrings::eval(EvalState & state, Env & env)
|
||||
nf += vTmp.fpoint();
|
||||
} else {
|
||||
state.ctx.errors.make<EvalError>("cannot add %1% to a float", showType(vTmp))
|
||||
.atPos(i_pos)
|
||||
.atPos(isInterpolation ? i_pos : pos)
|
||||
.withFrame(env, *this)
|
||||
.debugThrow();
|
||||
}
|
||||
@@ -473,13 +473,18 @@ Value ExprConcatStrings::eval(EvalState & state, Env & env)
|
||||
: StringCoercionMode::Strict;
|
||||
|
||||
/* skip canonization of first path, which would only be not
|
||||
canonized in the first place if it's coming from a ./${foo} type
|
||||
path */
|
||||
canonized in the first place if it's coming from a ./${foo} type
|
||||
path */
|
||||
auto part = state.coerceToString(
|
||||
i_pos,
|
||||
isInterpolation ? i_pos : pos,
|
||||
vTmp,
|
||||
context,
|
||||
"while evaluating a path segment",
|
||||
(isInterpolation && firstType == nPath)
|
||||
? "while evaluating a path interpolation"
|
||||
: (isInterpolation ? "while evaluating a string interpolation"
|
||||
// TODO: to the person who eventually cleans up all of this mess,
|
||||
// please turn this into "cannot concatenate $type to a $type" instead.
|
||||
: "while concatenating"),
|
||||
coercionMode,
|
||||
firstType == nString,
|
||||
!first
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
#include "lix/libexpr/eval-error.hh"
|
||||
#include "lix/libexpr/gc-alloc.hh"
|
||||
#include "value.hh"
|
||||
#include <cstdint>
|
||||
|
||||
namespace nix {
|
||||
|
||||
|
||||
+14
-19
@@ -847,8 +847,7 @@ Value::List * EvalMemory::newList(size_t size)
|
||||
|
||||
Value Evaluator::evalLazily(Expr & e)
|
||||
{
|
||||
stats.nrThunks++;
|
||||
return {NewValueAs::thunk, mem, builtins.env, e};
|
||||
return e.makeThunk(*this, builtins.env);
|
||||
}
|
||||
|
||||
Value EvalState::mkPos(PosIdx p)
|
||||
@@ -908,19 +907,6 @@ std::string EvalState::mkSingleDerivedPathStringRaw(
|
||||
}
|
||||
|
||||
|
||||
void EvalState::mkSingleDerivedPathString(
|
||||
const SingleDerivedPath & p,
|
||||
Value & v)
|
||||
{
|
||||
v = {
|
||||
NewValueAs::string,
|
||||
mkSingleDerivedPathStringRaw(p),
|
||||
NixStringContext{
|
||||
std::visit([](auto && v) -> NixStringContextElem { return v; }, p),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct CachedEvalFile
|
||||
{
|
||||
Value result;
|
||||
@@ -1132,8 +1118,9 @@ Env & AttrsPattern::match(
|
||||
|
||||
Value EvalState::callFunction(Value & fun, std::span<Value> args, const PosIdx pos)
|
||||
{
|
||||
if (callDepth > evalSettings.maxCallDepth)
|
||||
if (callDepth >= evalSettings.maxCallDepth) {
|
||||
ctx.errors.make<EvalError>("stack overflow; max-call-depth exceeded").atPos(pos).debugThrow();
|
||||
}
|
||||
MaintainCount _level(callDepth);
|
||||
|
||||
auto trace = evalSettings.traceFunctionCalls
|
||||
@@ -1208,12 +1195,18 @@ Value EvalState::callFunction(Value & fun, std::span<Value> args, const PosIdx p
|
||||
// was being evaluated and an explicit thrown error.
|
||||
if (fn->name == "throw" && !e.hasTrace()) {
|
||||
e.addTrace(ctx.positions[pos], "caused by explicit %s", "throw");
|
||||
} else {
|
||||
}
|
||||
// otherwise, print a trace of this builtin, as long as it isn't
|
||||
// a 'addErrorContext' call (which would just create noise)
|
||||
else if (fn->name != "addErrorContext")
|
||||
{
|
||||
e.addTrace(ctx.positions[pos], "while calling the '%s' builtin", fn->name);
|
||||
}
|
||||
throw;
|
||||
} catch (Error & e) {
|
||||
e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
|
||||
if (fn->name != "addErrorContext") {
|
||||
e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
@@ -1259,7 +1252,9 @@ Value EvalState::callFunction(Value & fun, std::span<Value> args, const PosIdx p
|
||||
// so the debugger allows to inspect the wrong parameters passed to the builtin.
|
||||
vCur = fn->fun(*this, vArgs.data());
|
||||
} catch (Error & e) {
|
||||
e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
|
||||
if (fn->name != "addErrorContext") {
|
||||
e.addTrace(ctx.positions[pos], "while calling the '%1%' builtin", fn->name);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -817,15 +817,6 @@ public:
|
||||
*/
|
||||
Value mkOutputString(const SingleDerivedPath::Built & b, const StorePath & staticOutputPath);
|
||||
|
||||
/**
|
||||
* Create a string representing a `SingleDerivedPath`.
|
||||
*
|
||||
* A combination of `mkStorePathString` and `mkOutputString`.
|
||||
*/
|
||||
void mkSingleDerivedPathString(
|
||||
const SingleDerivedPath & p,
|
||||
Value & v);
|
||||
|
||||
Value updateAttrs(const Value & v1, const Value & v2);
|
||||
Value concatLists(std::span<Value> lists, const PosIdx pos, std::string_view errorCtx);
|
||||
|
||||
|
||||
+119
-63
@@ -4,6 +4,7 @@
|
||||
#include "lix/libutil/json.hh"
|
||||
#include "lix/libutil/users.hh"
|
||||
#include "lix/libfetchers/fetch-settings.hh"
|
||||
#include <cctype>
|
||||
|
||||
namespace nix::flake {
|
||||
|
||||
@@ -30,6 +31,64 @@ static void writeTrustedList(const TrustedList & trustedList)
|
||||
writeFile(path, JSON(trustedList).dump());
|
||||
}
|
||||
|
||||
static bool
|
||||
askForEachSetting(TrustedList & trustedList, std::map<std::string, std::string> & untrustedSettings)
|
||||
{
|
||||
// clang-format off
|
||||
constexpr auto prompt =
|
||||
"[" ANSI_BOLD "y" ANSI_NORMAL "]es for now/"
|
||||
"[" ANSI_BOLD "N" ANSI_NORMAL "]o for now/"
|
||||
"[" ANSI_BOLD "a" ANSI_NORMAL "]lways allow";
|
||||
// clang-format on
|
||||
|
||||
auto didTrustedListChange = false;
|
||||
int acceptedCount = 0;
|
||||
for (const auto & [name, valueS] : untrustedSettings) {
|
||||
auto reply =
|
||||
logger
|
||||
->ask(
|
||||
fmt("Do you want to allow setting '" ANSI_MAGENTA "%s = %s" ANSI_NORMAL "'? (%s) ",
|
||||
name,
|
||||
valueS,
|
||||
prompt)
|
||||
)
|
||||
.value_or("n");
|
||||
|
||||
reply = toLower(reply);
|
||||
|
||||
static const std::string yes = "yes for now";
|
||||
static const std::string no = "no for now";
|
||||
static const std::string always = "always allow";
|
||||
|
||||
while (true) {
|
||||
if (no.starts_with(reply)) {
|
||||
break;
|
||||
}
|
||||
|
||||
if (yes.starts_with(reply) || always.starts_with(reply)) {
|
||||
if (reply[0] == 'a') {
|
||||
trustedList[name][valueS] = true;
|
||||
didTrustedListChange = true;
|
||||
}
|
||||
|
||||
globalConfig.set(name, valueS);
|
||||
acceptedCount++;
|
||||
break;
|
||||
}
|
||||
|
||||
// if the reply wasn't a prefix of any answer, ask the user again
|
||||
reply = logger->ask(fmt("Couldn't understand reply.\n%s: ", prompt)).value_or("n");
|
||||
}
|
||||
}
|
||||
|
||||
if (didTrustedListChange) {
|
||||
writeTrustedList(trustedList);
|
||||
}
|
||||
|
||||
// return false if *none* of the settings were accepted
|
||||
return acceptedCount > 0;
|
||||
}
|
||||
|
||||
static bool batchAskForSetting(
|
||||
bool & negativeTrustOverride,
|
||||
TrustedList & trustedList,
|
||||
@@ -43,77 +102,74 @@ static bool batchAskForSetting(
|
||||
|
||||
printWarning("%s", warning);
|
||||
|
||||
auto reply = logger
|
||||
->ask(
|
||||
fmt("Do you want to allow configuration settings to be applied?\nThis may allow the "
|
||||
"flake to gain root, see the nix.conf manual page (" ANSI_BOLD "y" ANSI_NORMAL
|
||||
"es for now/" ANSI_BOLD "A" ANSI_NORMAL "llow always/" ANSI_BOLD "n" ANSI_NORMAL
|
||||
"o/" ANSI_BOLD "N" ANSI_NORMAL "o to all) ")
|
||||
)
|
||||
.value_or('n');
|
||||
// clang-format off
|
||||
constexpr auto globalPrompt =
|
||||
"[" ANSI_BOLD "y" ANSI_NORMAL "]es for now/"
|
||||
"[" ANSI_BOLD "n" ANSI_NORMAL "]o for now/"
|
||||
"[" ANSI_BOLD "a" ANSI_NORMAL "]lways allow/"
|
||||
"[" ANSI_BOLD "I" ANSI_NORMAL "]ndividually review";
|
||||
// clang-format on
|
||||
|
||||
if (reply == 'N') {
|
||||
printWarning("Rejecting all untrusted nix.conf entries");
|
||||
printTaggedWarning(
|
||||
"you can set '%s' to '%b' to automatically reject configuration options supplied by "
|
||||
"flakes",
|
||||
"accept-flake-config",
|
||||
false
|
||||
);
|
||||
negativeTrustOverride = true;
|
||||
return false;
|
||||
}
|
||||
auto reply =
|
||||
logger
|
||||
->ask(
|
||||
fmt("Do you want to allow these configuration settings to be applied?\n" ANSI_BOLD
|
||||
"This may allow the flake to gain root" ANSI_NORMAL ", see the nix.conf manual page.\n"
|
||||
"(%s) ",
|
||||
globalPrompt)
|
||||
)
|
||||
.value_or("I"); // if the answer is empty (or there is no interactive prompt),
|
||||
// just default to reviewing each individually
|
||||
|
||||
reply = toLower(reply);
|
||||
|
||||
static const std::string yes = "yes for now";
|
||||
static const std::string no = "no for now";
|
||||
static const std::string always = "always allow";
|
||||
static const std::string review = "individually review";
|
||||
|
||||
// when interactive, loops and reprompts until the reply is one of y/n/a/i (or any prefix of the answers)
|
||||
while (true) {
|
||||
if (no.starts_with(reply)) {
|
||||
printWarning("Rejecting all untrusted nix.conf entries");
|
||||
printTaggedWarning(
|
||||
"you can set '%s' to '%b' to automatically reject configuration options supplied by "
|
||||
"flakes",
|
||||
"accept-flake-config",
|
||||
false
|
||||
);
|
||||
negativeTrustOverride = true;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (yes.starts_with(reply) || always.starts_with(reply)) {
|
||||
auto alwaysAllow = reply[0] == 'a';
|
||||
for (const auto & [name, valueS] : untrustedSettings) {
|
||||
if (alwaysAllow) {
|
||||
trustedList[name][valueS] = true;
|
||||
}
|
||||
globalConfig.set(name, valueS);
|
||||
}
|
||||
|
||||
if (reply == 'y' || reply == 'A') {
|
||||
auto alwaysAllow = reply == 'A';
|
||||
for (const auto & [name, valueS] : untrustedSettings) {
|
||||
if (alwaysAllow) {
|
||||
trustedList[name][valueS] = true;
|
||||
}
|
||||
globalConfig.set(name, valueS);
|
||||
}
|
||||
|
||||
if (alwaysAllow) {
|
||||
writeTrustedList(trustedList);
|
||||
}
|
||||
|
||||
return true;
|
||||
} else {
|
||||
printTaggedWarning(
|
||||
"you can set '%s' to '%b' to automatically reject configuration options supplied "
|
||||
"by flakes",
|
||||
"accept-flake-config",
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
auto didTrustedListChange = false;
|
||||
for (const auto & [name, valueS] : untrustedSettings) {
|
||||
auto individualReply = logger
|
||||
->ask(
|
||||
fmt("Do you want to allow setting '%s = %s'? (" ANSI_BOLD
|
||||
"y" ANSI_NORMAL "es for now/" ANSI_BOLD "A" ANSI_NORMAL
|
||||
"llow always/" ANSI_BOLD "n" ANSI_NORMAL "o for now) ",
|
||||
name,
|
||||
valueS)
|
||||
)
|
||||
.value_or('n');
|
||||
|
||||
if (individualReply == 'y' || individualReply == 'A') {
|
||||
if (individualReply == 'A') {
|
||||
trustedList[name][valueS] = true;
|
||||
didTrustedListChange = true;
|
||||
printTaggedWarning(
|
||||
"adding these configuration settings to the trusted list at %s, "
|
||||
"edit it if you want to remove them in the future",
|
||||
trustedListPath()
|
||||
);
|
||||
writeTrustedList(trustedList);
|
||||
}
|
||||
|
||||
globalConfig.set(name, valueS);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (didTrustedListChange) {
|
||||
writeTrustedList(trustedList);
|
||||
}
|
||||
if (review.starts_with(reply)) {
|
||||
return askForEachSetting(trustedList, untrustedSettings);
|
||||
}
|
||||
|
||||
return false;
|
||||
// if the reply wasn't a prefix of any, ask the user again
|
||||
reply = logger->ask(fmt("Couldn't understand reply.\n%s: ", globalPrompt)).value_or("n");
|
||||
}
|
||||
}
|
||||
|
||||
void ConfigFile::apply()
|
||||
|
||||
@@ -78,10 +78,21 @@ try {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
/** Force a value that cannot contain any function calls */
|
||||
static void forceTrivialValue(EvalState & state, Value & value, const PosIdx pos)
|
||||
{
|
||||
if (value.isThunk() && value.isTrivial())
|
||||
state.forceValue(value, pos);
|
||||
/* /piegames sighs at the settings API */
|
||||
auto prevOverridden = evalSettings.maxCallDepth.overridden;
|
||||
auto prevMaxCallDepth = evalSettings.maxCallDepth;
|
||||
evalSettings.maxCallDepth.override(0);
|
||||
|
||||
/* /piegames sighs at C++ */
|
||||
KJ_DEFER({
|
||||
evalSettings.maxCallDepth.override(prevMaxCallDepth);
|
||||
evalSettings.maxCallDepth.overridden = prevOverridden;
|
||||
});
|
||||
|
||||
state.forceValue(value, pos);
|
||||
}
|
||||
|
||||
|
||||
@@ -327,11 +338,17 @@ static Flake getFlake(
|
||||
Expr & flakeExpr = state.ctx.parseExprFromFile(resolvedFlakeFile);
|
||||
|
||||
// Enforce that 'flake.nix' is a direct attrset, not a computation.
|
||||
if (!flakeExpr.try_cast<ExprAttrs>()) {
|
||||
state.ctx.errors.make<EvalError>("file '%s' must be an attribute set", resolvedFlakeFile).debugThrow();
|
||||
}
|
||||
// We do this by disallowing any function calls (maxCallDepth 0) and checking that the resulting value is
|
||||
// an attrset.
|
||||
// The logic is already implemented in `forceTrivialValue`, but that takes a value instead of an Expr, so
|
||||
// we wrap the expression in a thunk to be able to call it.
|
||||
Value vInfo = flakeExpr.maybeThunk(state, state.ctx.builtins.env);
|
||||
forceTrivialValue(state, vInfo, flakeExpr.getPos());
|
||||
|
||||
Value vInfo = state.eval(flakeExpr);
|
||||
if (vInfo.type() != nAttrs) {
|
||||
state.ctx.errors.make<EvalError>("file '%s' must be an attribute set", resolvedFlakeFile)
|
||||
.debugThrow();
|
||||
}
|
||||
|
||||
if (auto description = vInfo.attrs()->get(state.ctx.symbols.sym_description)) {
|
||||
expectType(state, nString, description->value, description->pos);
|
||||
|
||||
@@ -67,16 +67,6 @@ FlakeRef parseFlakeRef(
|
||||
return flakeRef;
|
||||
}
|
||||
|
||||
std::optional<FlakeRef> maybeParseFlakeRef(
|
||||
const std::string & url, const std::optional<Path> & baseDir)
|
||||
{
|
||||
try {
|
||||
return parseFlakeRef(url, baseDir);
|
||||
} catch (Error &) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
|
||||
const std::string & url,
|
||||
const std::optional<Path> & baseDir,
|
||||
@@ -234,16 +224,6 @@ std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::pair<FlakeRef, std::string>> maybeParseFlakeRefWithFragment(
|
||||
const std::string & url, const std::optional<Path> & baseDir)
|
||||
{
|
||||
try {
|
||||
return parseFlakeRefWithFragment(url, baseDir);
|
||||
} catch (Error & e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
FlakeRef FlakeRef::fromAttrs(const fetchers::Attrs & attrs)
|
||||
{
|
||||
auto attrs2(attrs);
|
||||
|
||||
@@ -70,18 +70,12 @@ FlakeRef parseFlakeRef(
|
||||
bool allowMissing = false,
|
||||
bool isFlake = true);
|
||||
|
||||
std::optional<FlakeRef> maybeParseFlake(
|
||||
const std::string & url, const std::optional<Path> & baseDir = {});
|
||||
|
||||
std::pair<FlakeRef, std::string> parseFlakeRefWithFragment(
|
||||
const std::string & url,
|
||||
const std::optional<Path> & baseDir = {},
|
||||
bool allowMissing = false,
|
||||
bool isFlake = true);
|
||||
|
||||
std::optional<std::pair<FlakeRef, std::string>> maybeParseFlakeRefWithFragment(
|
||||
const std::string & url, const std::optional<Path> & baseDir = {});
|
||||
|
||||
std::tuple<FlakeRef, std::string, ExtendedOutputsSpec> parseFlakeRefWithFragmentAndExtendedOutputsSpec(
|
||||
const std::string & url,
|
||||
const std::optional<Path> & baseDir = {},
|
||||
|
||||
@@ -1,125 +1,75 @@
|
||||
#include "lix/libexpr/json-to-value.hh"
|
||||
#include "gc-alloc.hh"
|
||||
#include "libutil/types.hh"
|
||||
#include "lix/libexpr/value.hh"
|
||||
#include "lix/libexpr/eval.hh"
|
||||
#include "lix/libutil/json.hh"
|
||||
|
||||
#include <limits>
|
||||
#include <variant>
|
||||
|
||||
namespace nix {
|
||||
|
||||
// for more information, refer to
|
||||
// https://github.com/nlohmann/json/blob/master/include/nlohmann/detail/input/json_sax.hpp
|
||||
class JSONSax : nlohmann::json_sax<JSON> {
|
||||
class JSONState {
|
||||
protected:
|
||||
std::unique_ptr<JSONState> parent;
|
||||
JSONState() = default;
|
||||
public:
|
||||
virtual std::unique_ptr<JSONState> resolve(EvalState &) = 0;
|
||||
explicit JSONState(std::unique_ptr<JSONState> && p) : parent(std::move(p)) {}
|
||||
JSONState(JSONState & p) = delete;
|
||||
virtual Value & finalValue()
|
||||
{
|
||||
assert(false && "tried to read a final value from a non-toplevel json parser state");
|
||||
}
|
||||
virtual ~JSONState() {}
|
||||
virtual void addValue(Value v) = 0;
|
||||
};
|
||||
|
||||
class TopLevelJSONState : public JSONState
|
||||
struct WIPObject
|
||||
{
|
||||
RootValue v;
|
||||
public:
|
||||
std::unique_ptr<JSONState> resolve(EvalState &) override
|
||||
{
|
||||
assert(false && "tried to close toplevel json parser state");
|
||||
}
|
||||
TopLevelJSONState() = default;
|
||||
TopLevelJSONState(TopLevelJSONState & p) = delete;
|
||||
Value & finalValue() override
|
||||
{
|
||||
assert(v && "tried to read nonexistent final value from json parser");
|
||||
return *v;
|
||||
}
|
||||
void addValue(Value v) override
|
||||
{
|
||||
assert(!this->v && "duplicate value in toplevel JSON scope");
|
||||
this->v = allocRootValue(v);
|
||||
}
|
||||
GcMap<Symbol, Value> entries;
|
||||
Symbol nextKey;
|
||||
};
|
||||
using WIPArray = GcVector<Value>;
|
||||
|
||||
class JSONObjectState : public JSONState {
|
||||
using JSONState::JSONState;
|
||||
GcMap<Symbol, Value> attrs;
|
||||
Symbol _key;
|
||||
std::unique_ptr<JSONState> resolve(EvalState & state) override
|
||||
{
|
||||
auto attrs2 = state.ctx.buildBindings(attrs.size());
|
||||
for (auto & i : attrs)
|
||||
attrs2.insert(i.first, i.second);
|
||||
parent->addValue({NewValueAs::attrs, attrs2.alreadySorted()});
|
||||
return std::move(parent);
|
||||
}
|
||||
void addValue(Value v) override
|
||||
{
|
||||
attrs.insert_or_assign(_key, v);
|
||||
}
|
||||
public:
|
||||
void key(string_t & name, EvalState & state)
|
||||
{
|
||||
_key = state.ctx.symbols.create(name);
|
||||
}
|
||||
};
|
||||
// A stack of all of the currently opened and not yet closed JSON objects and arrays at the current moment
|
||||
// of parsing
|
||||
std::vector<std::variant<WIPObject, WIPArray>> stack;
|
||||
|
||||
class JSONListState : public JSONState {
|
||||
GcVector<Value> values;
|
||||
std::unique_ptr<JSONState> resolve(EvalState & state) override
|
||||
{
|
||||
auto list = state.ctx.mem.newList(values.size());
|
||||
parent->addValue({NewValueAs::list, list});
|
||||
for (size_t n = 0; n < values.size(); ++n) {
|
||||
list->elems[n] = values[n];
|
||||
}
|
||||
return std::move(parent);
|
||||
std::optional<Value> final_value;
|
||||
|
||||
void addValue(Value v)
|
||||
{
|
||||
if (stack.size() == 0) {
|
||||
assert(!final_value);
|
||||
final_value = v;
|
||||
return;
|
||||
}
|
||||
void addValue(Value v) override
|
||||
{
|
||||
values.push_back(v);
|
||||
}
|
||||
public:
|
||||
JSONListState(std::unique_ptr<JSONState> && p, std::size_t reserve) : JSONState(std::move(p))
|
||||
{
|
||||
values.reserve(reserve);
|
||||
}
|
||||
};
|
||||
std::visit(
|
||||
overloaded{
|
||||
[&](WIPObject & currentObject) {
|
||||
currentObject.entries.insert_or_assign(currentObject.nextKey, v);
|
||||
currentObject.nextKey = {}; // clear the current symbol
|
||||
},
|
||||
[&](WIPArray & arr) { arr.push_back(v); }
|
||||
},
|
||||
stack.back()
|
||||
);
|
||||
}
|
||||
|
||||
EvalState & state;
|
||||
std::unique_ptr<JSONState> rs;
|
||||
|
||||
public:
|
||||
JSONSax(EvalState & state) : state(state), rs(new TopLevelJSONState()) {};
|
||||
JSONSax(EvalState & state) : state(state) {};
|
||||
|
||||
Value result()
|
||||
{
|
||||
return rs->finalValue();
|
||||
return final_value.value();
|
||||
}
|
||||
|
||||
bool null() override
|
||||
{
|
||||
rs->addValue(Value::VNULL);
|
||||
addValue(Value::VNULL);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool boolean(bool val) override
|
||||
{
|
||||
rs->addValue({NewValueAs::boolean, val});
|
||||
addValue({NewValueAs::boolean, val});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool number_integer(number_integer_t val) override
|
||||
{
|
||||
rs->addValue({NewValueAs::integer, val});
|
||||
addValue({NewValueAs::integer, val});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -131,19 +81,19 @@ public:
|
||||
return number_float(static_cast<number_float_t>(val_), "");
|
||||
}
|
||||
NixInt::Inner val = val_;
|
||||
rs->addValue({NewValueAs::integer, val});
|
||||
addValue({NewValueAs::integer, val});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool number_float(number_float_t val, const string_t & s) override
|
||||
{
|
||||
rs->addValue({NewValueAs::floating, val});
|
||||
addValue({NewValueAs::floating, val});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool string(string_t & val) override
|
||||
{
|
||||
rs->addValue({NewValueAs::string, val});
|
||||
addValue({NewValueAs::string, val});
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -158,28 +108,66 @@ public:
|
||||
|
||||
bool start_object(std::size_t len) override
|
||||
{
|
||||
rs = std::make_unique<JSONObjectState>(std::move(rs));
|
||||
stack.emplace_back(std::in_place_type<WIPObject>);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool key(string_t & name) override
|
||||
{
|
||||
dynamic_cast<JSONObjectState*>(rs.get())->key(name, state);
|
||||
auto & back = stack.back();
|
||||
std::visit(
|
||||
overloaded{
|
||||
[&](WIPObject & frame) {
|
||||
assert(!frame.nextKey);
|
||||
frame.nextKey = state.ctx.symbols.create(name);
|
||||
},
|
||||
[](const WIPArray &) { assert(false && "tried adding a json key to an array value??"); }
|
||||
},
|
||||
back
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool end_object() override {
|
||||
rs = rs->resolve(state);
|
||||
auto back = std::move(stack.back());
|
||||
stack.pop_back();
|
||||
std::visit(
|
||||
overloaded{
|
||||
[&](const WIPObject & frame) {
|
||||
auto attrs2 = state.ctx.buildBindings(frame.entries.size());
|
||||
for (auto & i : frame.entries) {
|
||||
attrs2.insert(i.first, i.second);
|
||||
}
|
||||
addValue({NewValueAs::attrs, attrs2.alreadySorted()});
|
||||
},
|
||||
[](const WIPArray &) { assert(false && "tried to close a JSON object while in an array"); }
|
||||
},
|
||||
back
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool end_array() override {
|
||||
return end_object();
|
||||
auto back = std::move(stack.back());
|
||||
stack.pop_back();
|
||||
std::visit(
|
||||
overloaded{
|
||||
[](const WIPObject &) { assert(false && "tried to close a JSON array while in an object"); },
|
||||
[&](const WIPArray & values) {
|
||||
auto list = state.ctx.mem.newList(values.size());
|
||||
std::ranges::copy(values, list->elems);
|
||||
addValue({NewValueAs::list, list});
|
||||
}
|
||||
},
|
||||
back
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool start_array(size_t len) override {
|
||||
rs = std::make_unique<JSONListState>(std::move(rs),
|
||||
len != std::numeric_limits<size_t>::max() ? len : 128);
|
||||
auto v = WIPArray{};
|
||||
v.reserve(len != std::numeric_limits<size_t>::max() ? len : 128);
|
||||
stack.push_back(std::move(v));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -99,13 +99,6 @@ JSON ExprVar::toJSON(const SymbolTable & symbols) const
|
||||
return {{"_type", "ExprVar"}, {"value", stringToJSON(symbols[name])}};
|
||||
}
|
||||
|
||||
JSON ExprInheritFrom::toJSON(SymbolTable const & symbols) const
|
||||
{
|
||||
return {
|
||||
{"_type", "ExprInheritFrom"}
|
||||
};
|
||||
}
|
||||
|
||||
JSON ExprSelect::toJSON(const SymbolTable & symbols) const
|
||||
{
|
||||
JSON out = {
|
||||
@@ -747,17 +740,6 @@ void ExprLambda::setName(Symbol name)
|
||||
body->setName(name);
|
||||
}
|
||||
|
||||
|
||||
std::string ExprLambda::showNamePos(const EvalState & state) const
|
||||
{
|
||||
std::string id(name
|
||||
? concatStrings("'", state.ctx.symbols[name], "'")
|
||||
: "anonymous function");
|
||||
return fmt("%1% at %2%", id, state.ctx.positions[pos]);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* Position table. */
|
||||
|
||||
Pos PosTable::operator[](PosIdx p) const
|
||||
|
||||
+12
-3
@@ -126,9 +126,10 @@ public:
|
||||
std::unique_ptr<Expr> parsed, Evaluator & es, const std::shared_ptr<const StaticEnv> & env
|
||||
);
|
||||
|
||||
virtual JSON toJSON(const SymbolTable & symbols) const;
|
||||
virtual JSON toJSON(const SymbolTable & symbols) const = 0;
|
||||
virtual void accept(ExprVisitor & ev, std::unique_ptr<Expr> & ptr) = 0;
|
||||
virtual Value eval(EvalState & state, Env & env);
|
||||
Value makeThunk(Evaluator & ctx, Env & env);
|
||||
virtual Value maybeThunk(EvalState & state, Env & env);
|
||||
/* Lambdas have a name associated with them, when they are declared in a binding:
|
||||
* `identity = x: x` will print the resulting value as `«lambda identity @ «string»:1:14»`.
|
||||
@@ -302,7 +303,11 @@ struct ExprInheritFrom : Expr
|
||||
{
|
||||
}
|
||||
|
||||
JSON toJSON(const SymbolTable & symbols) const override;
|
||||
JSON toJSON(const SymbolTable & symbols) const override
|
||||
{
|
||||
abort();
|
||||
}
|
||||
|
||||
Value eval(EvalState & state, Env & env) override;
|
||||
void accept(ExprVisitor & ev, std::unique_ptr<Expr> & ptr) override { ev.visit(*this, ptr); }
|
||||
};
|
||||
@@ -517,7 +522,6 @@ struct ExprLambda : Expr
|
||||
{
|
||||
}
|
||||
void setName(Symbol name) override;
|
||||
std::string showNamePos(const EvalState & state) const;
|
||||
|
||||
/** Returns the name of the lambda,
|
||||
* or "anonymous lambda" if it doesn't have one.
|
||||
@@ -656,6 +660,11 @@ struct ExprPos : Expr
|
||||
/* only used to mark thunks as black holes. */
|
||||
struct ExprBlackHole : Expr
|
||||
{
|
||||
JSON toJSON(const SymbolTable & symbols) const override
|
||||
{
|
||||
abort();
|
||||
}
|
||||
|
||||
Value eval(EvalState & state, Env & env) override;
|
||||
void accept(ExprVisitor & ev, std::unique_ptr<Expr> & ptr) override { ev.visit(*this, ptr); }
|
||||
};
|
||||
|
||||
@@ -133,14 +133,24 @@ inline void State::badEscapeFound(const PosIdx pos, char found, bool isIndented)
|
||||
{
|
||||
auto escape = std::string(isIndented ? "''\\" : "\\");
|
||||
auto interpolEscape = std::string(isIndented ? "''${" : "\\${");
|
||||
auto backslashChar = std::string("\\") + found;
|
||||
|
||||
HintFmt dedicatedInsert = isIndented
|
||||
? HintFmt("you can simply write it as %s in the string", backslashChar)
|
||||
: HintFmt("you need to escape the %s itself: %s", escape, escape + escape + found);
|
||||
|
||||
HintFmt msg = HintFmt(
|
||||
"%s is an ill-defined escape. You can drop the %s and simply write %s instead. "
|
||||
"%s is an ill-defined escape sequence. In Nix, it simply means %s, therefore the %s is redundant and "
|
||||
"should be removed. If the intent of the string was to mean %s instead (e.g. in a regex), %s. "
|
||||
"Use %s to silence this warning.",
|
||||
escape + found,
|
||||
escape,
|
||||
found,
|
||||
escape,
|
||||
backslashChar,
|
||||
Uncolored(dedicatedInsert.str()),
|
||||
"--extra-deprecated-features broken-string-escape"
|
||||
);
|
||||
|
||||
/* Special case some common escapes to provide better messages */
|
||||
if (found == '$' || found == '{') {
|
||||
/* Someone possibly tried to escape an interpolation but used the wrong sequence.
|
||||
|
||||
+79
-6
@@ -1,3 +1,4 @@
|
||||
#include "libutil/error-trace.hh"
|
||||
#include "lix/libutil/archive.hh"
|
||||
#include "lix/libstore/derivations.hh"
|
||||
#include "lix/libexpr/eval.hh"
|
||||
@@ -36,6 +37,7 @@
|
||||
#include <dlfcn.h>
|
||||
|
||||
#include <cmath>
|
||||
#include <cfenv>
|
||||
|
||||
namespace nix {
|
||||
|
||||
@@ -300,8 +302,16 @@ Value prim_importNative(EvalState & state, Value ** args)
|
||||
state.ctx.errors.make<EvalError>("symbol '%1%' from '%2%' resolved to NULL when a function pointer was expected", sym, path).debugThrow();
|
||||
}
|
||||
|
||||
// Default construction of `Value` is deprecated, and uses of `Value&` out parameters
|
||||
// have mostly been removed in favor of returning a `Value` instead. However in this
|
||||
// particular case, this signature (ValueInitializer defined above) is externally visible,
|
||||
// changing it would be an API breaking change, so for this one instance we just suppress
|
||||
// the warning instead.
|
||||
#pragma clang diagnostic push
|
||||
#pragma clang diagnostic ignored "-Wdeprecated-declarations"
|
||||
Value v;
|
||||
(func)(state, v);
|
||||
#pragma clang diagnostic pop
|
||||
|
||||
/* We don't dlclose because v may be a primop referencing a function in the shared object file */
|
||||
return v;
|
||||
@@ -690,22 +700,85 @@ static Value prim_addErrorContext(EvalState & state, Value ** args)
|
||||
auto message = state.coerceToString(noPos, *args[0], context,
|
||||
"while evaluating the error message passed to builtins.addErrorContext",
|
||||
StringCoercionMode::Strict, false).toOwned();
|
||||
e.addTrace(nullptr, HintFmt(message));
|
||||
e.addTrace(nullptr, HintFmt(message), TraceKind::UserTrace);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static std::optional<NixInt> floatToIntChecked(NixFloat f)
|
||||
{
|
||||
// Required to detect overflows when converting floats to integers
|
||||
#pragma STDC FENV_ACCESS ON
|
||||
|
||||
std::feclearexcept(FE_ALL_EXCEPT);
|
||||
auto converted = llrint(f);
|
||||
if (std::fetestexcept(FE_ALL_EXCEPT)) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return NixInt{converted};
|
||||
}
|
||||
|
||||
/*
|
||||
Note [floor/ceil corrupt integers]:
|
||||
Integers above 2**54 that aren't a power of two get corrupted when passed
|
||||
through floor/ceil.
|
||||
|
||||
Corrupting integers would become impossible if we just passed through integer
|
||||
inputs, since the corruption actually happens when casting from int to float,
|
||||
*not* from float to int, which is the one we actually safely cast.
|
||||
|
||||
Floats generate an error as intended if they are out of range.
|
||||
|
||||
In a future release, we'd like to pass through all integers, but it would be
|
||||
an eval semantics change, so it's safer to error first before relaxing the
|
||||
semantics again.
|
||||
*/
|
||||
|
||||
using FloorCeilFunc = auto (*)(NixFloat) -> NixFloat;
|
||||
|
||||
static Value
|
||||
floorCeil(std::string_view const which, FloorCeilFunc f, EvalState & state, NixFloat value, Value * arg0)
|
||||
{
|
||||
bool isInt = arg0->type() == nInt;
|
||||
|
||||
NixFloat result = f(value);
|
||||
|
||||
if (auto checked = floatToIntChecked(result); checked.has_value()) {
|
||||
// See Note [floor/ceil corrupt integers].
|
||||
if (isInt && *checked != arg0->integer()
|
||||
&& !featureSettings.isEnabled(DeprecatedFeature::FloorCeilCorruptIntegers))
|
||||
{
|
||||
state.ctx.errors
|
||||
.make<EvalError>(
|
||||
"%s was corrupting your integer (was %d, became %d) in previous versions due to a "
|
||||
"historical Nix bug (https://github.com/NixOS/nix/issues/12899).\n"
|
||||
"This may be changed in the future to pass through integers as-is, which will change the "
|
||||
"semantics of this code.\n"
|
||||
"To suppress this error, use %s",
|
||||
which,
|
||||
arg0->integer(),
|
||||
*checked,
|
||||
"--extra-deprecated-features floor-ceil-corrupt-integers"
|
||||
)
|
||||
.debugThrow();
|
||||
}
|
||||
return {NewValueAs::integer, NixInt::Inner(*checked)};
|
||||
} else {
|
||||
state.ctx.errors.make<EvalError>("%s result %f is out of range for Nix integer (i64)", which, result)
|
||||
.debugThrow();
|
||||
}
|
||||
}
|
||||
|
||||
static Value prim_ceil(EvalState & state, Value ** args)
|
||||
{
|
||||
auto value = state.forceFloat(*args[0], noPos,
|
||||
"while evaluating the first argument passed to builtins.ceil");
|
||||
return {NewValueAs::integer, NixInt::Inner(ceil(value))};
|
||||
auto value = state.forceFloat(*args[0], noPos, "while evaluating the argument passed to builtins.ceil");
|
||||
return floorCeil("builtins.ceil", ceil, state, value, args[0]);
|
||||
}
|
||||
|
||||
static Value prim_floor(EvalState & state, Value ** args)
|
||||
{
|
||||
auto value = state.forceFloat(*args[0], noPos, "while evaluating the first argument passed to builtins.floor");
|
||||
return {NewValueAs::integer, NixInt::Inner(floor(value))};
|
||||
auto value = state.forceFloat(*args[0], noPos, "while evaluating the argument passed to builtins.floor");
|
||||
return floorCeil("builtins.floor", floor, state, value, args[0]);
|
||||
}
|
||||
|
||||
/* Try evaluating the argument. Success => {success=true; value=something;},
|
||||
|
||||
@@ -308,10 +308,16 @@ static Value fetch(
|
||||
// https://github.com/NixOS/nix/issues/4313
|
||||
auto storePath = unpack
|
||||
? state.aio
|
||||
.blockOn(fetchers::downloadTarball(state.ctx.store, *url, name, (bool) expectedHash))
|
||||
.blockOn(
|
||||
fetchers::downloadTarball(
|
||||
state.ctx.store, *url, name, expectedHash != Hash(HashType::SHA256)
|
||||
)
|
||||
)
|
||||
.tree.storePath
|
||||
: state.aio
|
||||
.blockOn(fetchers::downloadFile(state.ctx.store, *url, name, (bool) expectedHash))
|
||||
.blockOn(
|
||||
fetchers::downloadFile(state.ctx.store, *url, name, expectedHash != Hash(HashType::SHA256))
|
||||
)
|
||||
.storePath;
|
||||
|
||||
if (expectedHash) {
|
||||
|
||||
@@ -53,15 +53,6 @@ void Value::print(EvalState & state, std::ostream & str, PrintOptions options)
|
||||
printValue(state, str, *this, options);
|
||||
}
|
||||
|
||||
bool Value::isTrivial() const
|
||||
{
|
||||
return internalType() != tApp
|
||||
&& (internalType() != tThunk
|
||||
|| (thunk().expr->try_cast<ExprSet>()
|
||||
&& static_cast<ExprSet *>(thunk().expr)->dynamicAttrs.empty())
|
||||
|| thunk().expr->try_cast<ExprLambda>() || thunk().expr->try_cast<ExprList>());
|
||||
}
|
||||
|
||||
Value::Value(string_t, Str * s, const NixStringContext & context)
|
||||
: Value(NewValueAs::string, s, copyContext(context))
|
||||
{
|
||||
|
||||
@@ -284,6 +284,7 @@ private:
|
||||
|
||||
InternalType internalType() const
|
||||
{
|
||||
// NOLINTNEXTLINE(lix-cast-to-non-fixed-enum): TAG_MASK ensures it's in range
|
||||
return InternalType(raw & TAG_MASK);
|
||||
}
|
||||
|
||||
@@ -723,6 +724,8 @@ public:
|
||||
|
||||
Type type() const
|
||||
{
|
||||
// TAG_MASK == 7, max enumerator is 5, so all possible values are in range
|
||||
// NOLINTNEXTLINE(lix-cast-to-non-fixed-enum)
|
||||
return Type(raw & TAG_MASK);
|
||||
}
|
||||
};
|
||||
@@ -775,13 +778,6 @@ public:
|
||||
|
||||
size_t listSize() const;
|
||||
|
||||
/**
|
||||
* Check whether forcing this value requires a trivial amount of
|
||||
* computation. In particular, function applications are
|
||||
* non-trivial.
|
||||
*/
|
||||
bool isTrivial() const;
|
||||
|
||||
auto listItems() const
|
||||
{
|
||||
struct ListIterable
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "lix/libfetchers/cache.hh"
|
||||
#include "libstore/pathlocks.hh"
|
||||
#include "libutil/hash.hh"
|
||||
#include "lix/libstore/sqlite.hh"
|
||||
#include "lix/libutil/async.hh"
|
||||
#include "lix/libutil/sync.hh"
|
||||
@@ -90,6 +92,28 @@ struct CacheImpl : Cache
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::variant<std::pair<Attrs, StorePath>, PathLock>>> lookupOrLock(ref<Store> store, const Attrs & inAttrs) override try {
|
||||
// In order to avoid fetching the same input multiple times
|
||||
// concurrently, we first acquire a lock based on the input attributes.
|
||||
auto hashResult = hashString(HashType::SHA256, attrsToJSON(inAttrs).dump());
|
||||
auto lockPath = getCacheDir() + "/nix/fetcher-lock-" + hashResult.to_string(HashFormat::Base32, false);
|
||||
auto pathLock = TRY_AWAIT(lockPathAsync(lockPath));
|
||||
// Once this lock is acquired, we check if the input is already present
|
||||
// in the cache (which requires locking the cache db)
|
||||
auto lookedUp = TRY_AWAIT(lookup(store, inAttrs));
|
||||
// We return either the cache hit or the lock; this allows fetchers to
|
||||
// keep a lock on fetching the input in question if it's not already in
|
||||
// the cache, and otherwise frees the cache db up again.
|
||||
std::variant<std::pair<Attrs, StorePath>, PathLock> result(std::move(pathLock));
|
||||
if (lookedUp) {
|
||||
// Reassigning the variant frees the lock
|
||||
result = *lookedUp;
|
||||
}
|
||||
co_return result;
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::optional<LookupResult>>> lookupExpired(
|
||||
ref<Store> store,
|
||||
const Attrs & inAttrs) override
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "libstore/pathlocks.hh"
|
||||
#include "lix/libfetchers/fetchers.hh"
|
||||
#include "lix/libstore/path.hh"
|
||||
|
||||
@@ -21,6 +22,10 @@ struct Cache
|
||||
ref<Store> store,
|
||||
const Attrs & inAttrs) = 0;
|
||||
|
||||
virtual kj::Promise<Result<std::variant<std::pair<Attrs, StorePath>, PathLock>>> lookupOrLock(
|
||||
ref<Store> store,
|
||||
const Attrs & inAttrs) = 0;
|
||||
|
||||
struct LookupResult
|
||||
{
|
||||
bool expired = false;
|
||||
|
||||
+43
-16
@@ -1,3 +1,4 @@
|
||||
#include "libfetchers/attrs.hh"
|
||||
#include "lix/libutil/archive.hh"
|
||||
#include "lix/libutil/async-io.hh"
|
||||
#include "lix/libutil/async.hh"
|
||||
@@ -15,9 +16,10 @@
|
||||
#include "lix/libutil/url-parts.hh"
|
||||
#include "lix/libstore/pathlocks.hh"
|
||||
#include "lix/libutil/users.hh"
|
||||
#include "lix/libutil/git.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include "lix/libutil/finally.hh"
|
||||
#include "lix/lix-rs/main.gen.hh"
|
||||
#include "lix/lix-rs/utils.hh"
|
||||
|
||||
#include "lix/libfetchers/fetch-settings.hh"
|
||||
|
||||
@@ -80,17 +82,17 @@ try {
|
||||
));
|
||||
|
||||
std::string_view line = output;
|
||||
line = line.substr(0, line.find("\n"));
|
||||
if (const auto parseResult = git::parseLsRemoteLine(line)) {
|
||||
switch (parseResult->kind) {
|
||||
case git::LsRemoteRefLine::Kind::Symbolic:
|
||||
debug("resolved HEAD ref '%s' for repo '%s'", parseResult->target, path);
|
||||
break;
|
||||
case git::LsRemoteRefLine::Kind::Object:
|
||||
debug("resolved HEAD rev '%s' for repo '%s'", parseResult->target, path);
|
||||
break;
|
||||
auto line_rs = rust::to_string(line.substr(0, line.find("\n")));
|
||||
if (const auto parseResult =
|
||||
to_std(rust::lix::fetchers::git::LsRemoteRefLine::try_from(line_rs.as_str()).ok()))
|
||||
{
|
||||
auto target = to_std_string(parseResult->target.as_str());
|
||||
if (parseResult->kind.matches_Object()) {
|
||||
debug("resolved HEAD rev '%s' for repo '%s'", target, path);
|
||||
} else {
|
||||
debug("resolved HEAD ref '%s' for repo '%s'", target, path);
|
||||
}
|
||||
co_return parseResult->target;
|
||||
co_return to_std_string(parseResult->target.as_str());
|
||||
}
|
||||
co_return std::nullopt;
|
||||
} catch (ExecError &) {
|
||||
@@ -605,9 +607,18 @@ struct GitInputScheme : InputScheme
|
||||
return {std::move(storePath), input};
|
||||
};
|
||||
|
||||
// If the input isn't present in the fetcher cache, we get a lock for
|
||||
// fetching the given input. We want to hold this lock until we're done
|
||||
// fetching, so we keep it in this scope.
|
||||
std::optional<PathLock> fetchLock;
|
||||
if (input.getRev()) {
|
||||
if (auto res = TRY_AWAIT(getCache()->lookup(store, getLockedAttrs())))
|
||||
auto resOrLock = TRY_AWAIT(getCache()->lookupOrLock(store, getLockedAttrs()));
|
||||
|
||||
if (auto res = std::get_if<std::pair<Attrs, StorePath>>(&resOrLock))
|
||||
co_return makeResult(res->first, std::move(res->second));
|
||||
auto lock = std::get_if<PathLock>(&resOrLock);
|
||||
assert(lock);
|
||||
fetchLock = std::move(*lock);
|
||||
}
|
||||
|
||||
auto [isLocal, actualUrl_] = getActualUrl(input);
|
||||
@@ -676,7 +687,10 @@ struct GitInputScheme : InputScheme
|
||||
}
|
||||
}
|
||||
|
||||
if (auto res = TRY_AWAIT(getCache()->lookup(store, unlockedAttrs))) {
|
||||
// If the input isn't in the cache, we get a lock for fetching it.
|
||||
// Make sure to keep this until we're done fetching!
|
||||
auto cached = TRY_AWAIT(getCache()->lookupOrLock(store, unlockedAttrs));
|
||||
if (auto res = std::get_if<std::pair<Attrs, StorePath>>(&cached)) {
|
||||
auto rev2 = Hash::parseAny(getStrAttr(res->first, "rev"), HashType::SHA1);
|
||||
if (!input.getRev() || input.getRev() == rev2) {
|
||||
input.attrs.insert_or_assign("rev", rev2.gitRev());
|
||||
@@ -858,8 +872,21 @@ struct GitInputScheme : InputScheme
|
||||
|
||||
/* Now that we know the ref, check again whether we have it in
|
||||
the store. */
|
||||
if (auto res = TRY_AWAIT(getCache()->lookup(store, getLockedAttrs())))
|
||||
co_return makeResult(res->first, std::move(res->second));
|
||||
// If we already have a lock for fetching this path, we can't use lookupOrLock because it would deadlock.
|
||||
if (fetchLock) {
|
||||
if (auto res = TRY_AWAIT(getCache()->lookup(store, getLockedAttrs()))) {
|
||||
co_return makeResult(res->first, std::move(res->second));
|
||||
}
|
||||
} else {
|
||||
// If we don't have the lock, we need to acquire it by using lookupOrLock.
|
||||
auto resultOrLock = TRY_AWAIT(getCache()->lookupOrLock(store, getLockedAttrs()));
|
||||
if (auto res = std::get_if<std::pair<Attrs, StorePath>>(&resultOrLock)) {
|
||||
co_return makeResult(res->first, std::move(res->second));
|
||||
}
|
||||
auto lock = std::get_if<PathLock>(&resultOrLock);
|
||||
assert(lock);
|
||||
fetchLock = std::move(*lock);
|
||||
}
|
||||
|
||||
Path tmpDir = createTempDir();
|
||||
AutoDelete delTmpDir(tmpDir, true);
|
||||
@@ -974,7 +1001,7 @@ struct GitInputScheme : InputScheme
|
||||
});
|
||||
Finally const _wait([&] { proc.waitAndCheck(); });
|
||||
|
||||
TRY_AWAIT(unpackTarfile(*proc.getStdout(), tmpDir));
|
||||
TRY_AWAIT(unpackTarfile(_input.toURLString(), *proc.getStdout(), tmpDir));
|
||||
}
|
||||
|
||||
auto storePath = TRY_AWAIT(
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
#include "lix/libutil/result.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
#include "lix/libutil/url-parts.hh"
|
||||
#include "lix/libutil/git.hh"
|
||||
#include "lix/libutil/json.hh"
|
||||
#include "lix/libfetchers/fetchers.hh"
|
||||
#include "lix/libfetchers/fetch-settings.hh"
|
||||
#include "lix/lix-rs/main.gen.hh"
|
||||
#include "lix/lix-rs/utils.hh"
|
||||
|
||||
#include <optional>
|
||||
#include <fstream>
|
||||
@@ -486,15 +487,16 @@ struct SourceHutInputScheme : GitArchiveInputScheme
|
||||
std::string line;
|
||||
getline(is, line);
|
||||
|
||||
auto remoteLine = git::parseLsRemoteLine(line);
|
||||
auto remoteLine = to_std(
|
||||
rust::lix::fetchers::git::LsRemoteRefLine::try_from(rust::to_string(line).as_str()).ok()
|
||||
);
|
||||
if (!remoteLine) {
|
||||
throw BadURL("in '%d', couldn't resolve HEAD ref '%d'", input.to_string(), ref);
|
||||
}
|
||||
refUri = remoteLine->target;
|
||||
refUri = to_std_string(remoteLine->target.as_str());
|
||||
} else {
|
||||
refUri = fmt("refs/(heads|tags)/%s", ref);
|
||||
}
|
||||
std::regex refRegex = regex::parse(refUri);
|
||||
|
||||
auto file = store->toRealPath(
|
||||
TRY_AWAIT(downloadFile(store, fmt("%s/info/refs", base_url), "source", false, headers))
|
||||
@@ -505,9 +507,12 @@ struct SourceHutInputScheme : GitArchiveInputScheme
|
||||
std::string line;
|
||||
std::optional<std::string> id;
|
||||
while(!id && getline(is, line)) {
|
||||
auto parsedLine = git::parseLsRemoteLine(line);
|
||||
if (parsedLine && parsedLine->reference && std::regex_match(*parsedLine->reference, refRegex))
|
||||
id = parsedLine->target;
|
||||
auto parsedLine = to_std(
|
||||
rust::lix::fetchers::git::LsRemoteRefLine::try_from(rust::to_string(line).as_str()).ok()
|
||||
);
|
||||
if (parsedLine && (*parsedLine).matches_ref_uri(rust::to_string(refUri).as_str())) {
|
||||
id = to_std_string(parsedLine->target.as_str());
|
||||
}
|
||||
}
|
||||
|
||||
if(!id)
|
||||
|
||||
@@ -195,7 +195,7 @@ try {
|
||||
} else {
|
||||
Path tmpDir = createTempDir();
|
||||
AutoDelete autoDelete(tmpDir, true);
|
||||
unpackTarfile(store->toRealPath(res.storePath), tmpDir);
|
||||
unpackTarfile(url, store->toRealPath(res.storePath), tmpDir);
|
||||
auto members = readDirectory(tmpDir);
|
||||
if (members.size() != 1)
|
||||
throw nix::Error("tarball '%s' contains an unexpected number of top-level files", url);
|
||||
|
||||
@@ -16,21 +16,21 @@ MixCommonArgs::MixCommonArgs(const std::string & programName)
|
||||
.shortName = 'v',
|
||||
.description = "Increase the logging verbosity level.",
|
||||
.category = loggingCategory,
|
||||
.handler = {[]() { verbosity = verbosityFromIntClamped(int(verbosity) + 1); }},
|
||||
.handler = {[]() { setVerbosity(verbosityFromIntClamped(int(getVerbosity()) + 1)); }},
|
||||
});
|
||||
|
||||
addFlag({
|
||||
.longName = "quiet",
|
||||
.description = "Decrease the logging verbosity level.",
|
||||
.category = loggingCategory,
|
||||
.handler = {[]() { verbosity = verbosityFromIntClamped(int(verbosity) - 1); }},
|
||||
.handler = {[]() { setVerbosity(verbosityFromIntClamped(int(getVerbosity()) - 1)); }},
|
||||
});
|
||||
|
||||
addFlag({
|
||||
.longName = "debug",
|
||||
.description = "Set the logging verbosity level to 'debug'.",
|
||||
.category = loggingCategory,
|
||||
.handler = {[]() { verbosity = lvlDebug; }},
|
||||
.handler = {[]() { setVerbosity(lvlDebug); }},
|
||||
});
|
||||
|
||||
addFlag({
|
||||
|
||||
@@ -106,7 +106,7 @@ bool ProgressBar::isVerbose()
|
||||
|
||||
Logger::BufferState ProgressBar::log(Verbosity lvl, std::string_view s)
|
||||
{
|
||||
if (lvl > verbosity) {
|
||||
if (lvl > getVerbosity()) {
|
||||
return BufferState::HasSpace;
|
||||
}
|
||||
auto state(state_.lock());
|
||||
@@ -142,8 +142,9 @@ Logger::BufferState ProgressBar::startActivityImpl(
|
||||
{
|
||||
auto state(state_.lock());
|
||||
|
||||
if (lvl <= verbosity && !s.empty() && type != actBuildWaiting)
|
||||
if (lvl <= getVerbosity() && !s.empty() && type != actBuildWaiting) {
|
||||
(void) log(*state, lvl, s + "...");
|
||||
}
|
||||
|
||||
state->activities.emplace_back(ActInfo {
|
||||
.s = s,
|
||||
@@ -197,8 +198,10 @@ Logger::BufferState ProgressBar::startActivityImpl(
|
||||
|
||||
if ((type == actFileTransfer && hasAncestor(*state, actCopyPath, parent))
|
||||
|| (type == actFileTransfer && hasAncestor(*state, actQueryPathInfo, parent))
|
||||
|| (type == actCopyPath && hasAncestor(*state, actSubstitute, parent)))
|
||||
|| (type == actCopyPath && hasAncestor(*state, actSubstitute, parent)) || (s == "daemon connection"))
|
||||
{
|
||||
i->visible = false;
|
||||
}
|
||||
|
||||
update(*state);
|
||||
return BufferState::HasSpace;
|
||||
@@ -560,7 +563,7 @@ void ProgressBar::writeToStdout(std::string_view s)
|
||||
restoreProgressDisplay(*state);
|
||||
}
|
||||
|
||||
std::optional<char> ProgressBar::ask(std::string_view msg)
|
||||
std::optional<std::string> ProgressBar::ask(std::string_view msg)
|
||||
{
|
||||
auto state(state_.lock());
|
||||
if (state->paused > 0 || !isatty(STDIN_FILENO)) return {};
|
||||
@@ -569,9 +572,9 @@ std::optional<char> ProgressBar::ask(std::string_view msg)
|
||||
std::cerr << msg;
|
||||
auto s = trim(readLine(STDIN_FILENO));
|
||||
writeLogsToStderr("\e[?2026h"); // begin synchronized update
|
||||
if (s.size() != 1) return {};
|
||||
restoreProgressDisplay(*state);
|
||||
return s[0];
|
||||
// only return the string if it's not empty
|
||||
return s.size() != 0 ? s : std::optional<std::string>{};
|
||||
}
|
||||
|
||||
void ProgressBar::setPrintBuildLogs(bool printBuildLogs)
|
||||
|
||||
@@ -103,7 +103,7 @@ struct ProgressBar : public Logger
|
||||
|
||||
void writeToStdout(std::string_view s) override;
|
||||
|
||||
std::optional<char> ask(std::string_view msg) override;
|
||||
std::optional<std::string> ask(std::string_view msg) override;
|
||||
|
||||
void setPrintBuildLogs(bool printBuildLogs) override;
|
||||
|
||||
|
||||
+2
-12
@@ -21,6 +21,7 @@
|
||||
#include "lix/libutil/terminal.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
#include "lix/libutil/exit.hh"
|
||||
#include "lix/libutil/compile-time-features.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <exception>
|
||||
@@ -175,12 +176,6 @@ void initNix()
|
||||
}
|
||||
|
||||
#if __APPLE__
|
||||
/* HACK: on darwin, we need can’t use sigprocmask with SIGWINCH.
|
||||
* Instead, add a dummy sigaction handler, and signalHandlerThread
|
||||
* can handle the rest. */
|
||||
act.sa_handler = sigHandler;
|
||||
if (sigaction(SIGWINCH, &act, 0)) throw SysError("handling SIGWINCH");
|
||||
|
||||
/* Disable SA_RESTART for interrupts, so that system calls on this thread
|
||||
* error with EINTR like they do on Linux.
|
||||
* Most signals on BSD systems default to SA_RESTART on, but Nix
|
||||
@@ -302,14 +297,9 @@ bool LegacyArgs::processArgs(const Strings & args, bool finish)
|
||||
void printVersion(const std::string & programName)
|
||||
{
|
||||
std::cout << fmt("%1% (Lix, like Nix) %2%", programName, nixVersion) << std::endl;
|
||||
Strings cfg;
|
||||
#if HAVE_BOEHMGC
|
||||
cfg.push_back("gc");
|
||||
#endif
|
||||
cfg.push_back("signed-caches");
|
||||
std::cout << "System type: " << settings.thisSystem << "\n";
|
||||
std::cout << "Additional system types: " << concatStringsSep(", ", settings.extraPlatforms.get()) << "\n";
|
||||
std::cout << "Features: " << concatStringsSep(", ", cfg) << "\n";
|
||||
std::cout << "Features: " << concatStringsSep(", ", getNixFeatures()) << "\n";
|
||||
std::cout << "System configuration file: " << settings.nixConfDir + "/nix.conf" << "\n";
|
||||
std::cout << "User configuration files: " <<
|
||||
concatStringsSep(":", settings.nixUserConfFiles)
|
||||
|
||||
@@ -21,7 +21,7 @@ struct BuildResult
|
||||
* Therefore, don't remove status codes, and only add new status
|
||||
* codes at the end of the list.
|
||||
*/
|
||||
enum Status {
|
||||
enum Status : uint8_t {
|
||||
Built = 0,
|
||||
Substituted,
|
||||
AlreadyValid,
|
||||
|
||||
@@ -1094,8 +1094,8 @@ try {
|
||||
RPC_FILL(buildReq, setNeededSystem, drv->platform);
|
||||
RPC_FILL(buildReq, initDrvPath, drvPath, worker.store);
|
||||
RPC_FILL(buildReq, initRequiredFeatures, parsedDrv->getRequiredSystemFeatures());
|
||||
auto buildRespPromise = buildReq.send();
|
||||
auto buildResp = TRY_AWAIT_RPC(buildRespPromise);
|
||||
auto buildRespV = TRY_AWAIT_RPC(buildReq.send());
|
||||
auto buildResp = buildRespV.getResult();
|
||||
|
||||
debug("hook reply is '%1%'", buildResp.toString().flatten().cStr());
|
||||
|
||||
@@ -1136,10 +1136,9 @@ try {
|
||||
missingOutputs.insert(outputName);
|
||||
}
|
||||
RPC_FILL(runReq, initWantedOutputs, missingOutputs);
|
||||
RPC_FILL(runReq, setDescription, buildDescription());
|
||||
}
|
||||
|
||||
auto runPromise = runReq.send();
|
||||
auto runPromise = LIX_WRAP_RPC_PROMISE_V1(runReq.send());
|
||||
|
||||
// build via hook is now properly running. wait for it to finish
|
||||
actLock.reset();
|
||||
@@ -1150,8 +1149,8 @@ try {
|
||||
wrapChildHandler(runPromise.then([&](auto result) -> kj::Promise<Result<WorkResult>> {
|
||||
try {
|
||||
std::shared_ptr<Error> remoteError;
|
||||
if (result.getResult().isBad()) {
|
||||
remoteError = std::make_shared<Error>(from(result.getResult().getBad()));
|
||||
if (result.has_error()) {
|
||||
remoteError = std::make_shared<Error>(detail::wrap_exception_as_lix(result.error()));
|
||||
logErrorInfo(remoteError->info().level, remoteError->info());
|
||||
}
|
||||
// close the rpc connection to have the hook exit
|
||||
|
||||
@@ -360,10 +360,6 @@ public:
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JobCategory jobCategory() const override {
|
||||
return JobCategory::Build;
|
||||
};
|
||||
};
|
||||
|
||||
MakeError(NotDeterministic, BuildError);
|
||||
|
||||
@@ -27,23 +27,6 @@ typedef std::shared_ptr<Goal> GoalPtr;
|
||||
*/
|
||||
typedef std::set<GoalPtr> Goals;
|
||||
|
||||
/**
|
||||
* Used as a hint to the worker on how to schedule a particular goal. For example,
|
||||
* builds are typically CPU- and memory-bound, while substitutions are I/O bound.
|
||||
* Using this information, the worker might decide to schedule more or fewer goals
|
||||
* of each category in parallel.
|
||||
*/
|
||||
enum struct JobCategory {
|
||||
/**
|
||||
* A build of a derivation; it will use CPU and disk resources.
|
||||
*/
|
||||
Build,
|
||||
/**
|
||||
* A substitution an arbitrary store object; it will use network resources.
|
||||
*/
|
||||
Substitution,
|
||||
};
|
||||
|
||||
struct Goal
|
||||
{
|
||||
typedef enum {ecSuccess, ecFailed, ecNoSubstituters, ecIncompleteClosure} ExitCode;
|
||||
@@ -139,13 +122,7 @@ public:
|
||||
return name;
|
||||
}
|
||||
|
||||
virtual void cleanup() { }
|
||||
|
||||
/**
|
||||
* @brief Hint for the scheduler, which concurrency limit applies.
|
||||
* @see JobCategory
|
||||
*/
|
||||
virtual JobCategory jobCategory() const = 0;
|
||||
virtual void cleanup() {}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -8,14 +8,13 @@ using Types = import "/lix/libutil/types.capnp";
|
||||
using Log = import "/lix/libutil/logging.capnp";
|
||||
using StoreTypes = import "/lix/libstore/types.capnp";
|
||||
|
||||
interface HookInstance {
|
||||
interface HookInstance $Types.throws(Types.v1Errors) {
|
||||
interface AcceptedBuild {
|
||||
run @0 (
|
||||
logger :Log.LogStream,
|
||||
inputs :List(StoreTypes.StorePath), # actual a set
|
||||
wantedOutputs :List(Data), # actually StringSet
|
||||
description :Text, # root activity description for this build
|
||||
) -> (result :Types.ResultV);
|
||||
);
|
||||
}
|
||||
|
||||
struct BuildResponse {
|
||||
@@ -32,11 +31,11 @@ interface HookInstance {
|
||||
init @0 (
|
||||
logger :Log.LogStream,
|
||||
settings :Types.Settings,
|
||||
) -> (result :Types.ResultV);
|
||||
);
|
||||
build @1 (
|
||||
amWilling :Bool,
|
||||
neededSystem :Data,
|
||||
drvPath :StoreTypes.StorePath,
|
||||
requiredFeatures :List(Data),
|
||||
) -> (result :Types.Result(BuildResponse));
|
||||
) -> (result :BuildResponse);
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ try {
|
||||
|
||||
auto buildHook = canonPath(buildHookArgs.front());
|
||||
buildHookArgs.emplace(std::next(buildHookArgs.begin()), baseNameOf(buildHook));
|
||||
buildHookArgs.push_back(std::to_string(verbosity));
|
||||
buildHookArgs.push_back(std::to_string(getVerbosity()));
|
||||
|
||||
/* Create the communication pipes. */
|
||||
auto [selfRPC, hookRPC] = SocketPair::stream();
|
||||
|
||||
@@ -897,7 +897,7 @@ try {
|
||||
|
||||
if (drv->isBuiltin()) {
|
||||
args.push_back("builtin-builder");
|
||||
args.push_back(std::to_string(verbosity));
|
||||
args.push_back(std::to_string(getVerbosity()));
|
||||
|
||||
std::map<std::string, AbstractConfig::SettingInfo> changedSettings;
|
||||
globalConfig.getChangedSettings(changedSettings);
|
||||
@@ -984,7 +984,7 @@ try {
|
||||
auto groups = buildUser->getSupplementaryGIDs();
|
||||
creds.setSupplementaryGroups({groups.data(), groups.size()});
|
||||
}
|
||||
request.setDebug(verbosity >= lvlDebug);
|
||||
request.setDebug(getVerbosity() >= lvlDebug);
|
||||
|
||||
fillBuilderConfig(request);
|
||||
|
||||
@@ -1711,6 +1711,23 @@ try {
|
||||
anyHashMismatchSeen = true;
|
||||
// XXX: shameless layering violation hack that makes the hash mismatch error at least not utterly worthless
|
||||
auto guessedUrl = getOr(drv->env, "urls", getOr(drv->env, "url", "(unknown)"));
|
||||
if (guessedUrl == "(unknown)") {
|
||||
if (auto structuredAttrs = parsedDrv->getStructuredAttrs()) {
|
||||
auto json = *structuredAttrs;
|
||||
if (json.contains("urls") && json["urls"].is_array() && !json["urls"].empty()) {
|
||||
guessedUrl = "";
|
||||
for (auto it = json["urls"].begin(); it != json["urls"].end(); ++it) {
|
||||
if (!it->is_string()) continue;
|
||||
if (guessedUrl != "") {
|
||||
guessedUrl += " or ";
|
||||
}
|
||||
guessedUrl += it->get<std::string>();
|
||||
}
|
||||
} else if (json.contains("url") && json["url"].is_string()) {
|
||||
guessedUrl = json["url"].get<std::string>();
|
||||
}
|
||||
}
|
||||
}
|
||||
delayedException = std::make_exception_ptr(BuildError(
|
||||
"hash mismatch in fixed-output derivation '%s':\n likely URL: %s\n "
|
||||
"specified: %s\n got: %s\n expected path: %s\n got path: %s",
|
||||
|
||||
@@ -90,10 +90,6 @@ public:
|
||||
kj::Promise<Result<WorkResult>> referencesValid() noexcept;
|
||||
kj::Promise<Result<WorkResult>> tryToRun() noexcept;
|
||||
kj::Promise<Result<WorkResult>> finished() noexcept;
|
||||
|
||||
JobCategory jobCategory() const override {
|
||||
return JobCategory::Substitution;
|
||||
};
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ void builtinUnpackChannel(const Path & out, const std::string & channelName, con
|
||||
{
|
||||
createDirs(out);
|
||||
|
||||
unpackTarfile(src, out);
|
||||
unpackTarfile(fmt("channel:%s", channelName), src, out);
|
||||
|
||||
auto entries = readDirectory(out);
|
||||
if (entries.size() != 1)
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
#include <cstddef>
|
||||
#include <new>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <curl/urlapi.h>
|
||||
#include <kj/async.h>
|
||||
#include <kj/common.h>
|
||||
#include <kj/encoding.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#if ENABLE_S3
|
||||
#include <aws/core/client/ClientConfiguration.h>
|
||||
#endif
|
||||
|
||||
#include "lix/libstore/curlfiletransfer.hh"
|
||||
#include "lix/libstore/curlmulti.hh"
|
||||
#include "lix/libstore/filetransfer.hh"
|
||||
#include "lix/libstore/s3.hh"
|
||||
#include "lix/libstore/store-api.hh"
|
||||
#include "lix/libutil/async-io.hh"
|
||||
#include "lix/libutil/async.hh"
|
||||
#include "lix/libutil/box_ptr.hh"
|
||||
#include "lix/libutil/c-calls.hh"
|
||||
#include "lix/libutil/error.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include "lix/libutil/result.hh"
|
||||
#include "lix/libutil/tracepoint.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
|
||||
#if ENABLE_DTRACE
|
||||
#include "trace-probes.gen.hh"
|
||||
#endif
|
||||
|
||||
namespace nix {
|
||||
|
||||
curlFileTransfer::curlFileTransfer(unsigned int baseRetryTimeMs)
|
||||
: multi(std::make_shared<CurlMulti>(baseRetryTimeMs))
|
||||
{
|
||||
}
|
||||
|
||||
curlFileTransfer::~curlFileTransfer()
|
||||
{
|
||||
multi->stopWorkerThread();
|
||||
}
|
||||
|
||||
#if ENABLE_S3
|
||||
std::tuple<std::string, std::string, StoreConfig::Params> curlFileTransfer::parseS3Uri(std::string uri)
|
||||
{
|
||||
auto [path, params] = splitUriAndParams(uri);
|
||||
|
||||
auto slash = path.find('/', 5); // 5 is the length of "s3://" prefix
|
||||
if (slash == std::string::npos)
|
||||
throw nix::Error("bad S3 URI '%s'", path);
|
||||
|
||||
std::string bucketName(path, 5, slash - 5);
|
||||
std::string key(path, slash + 1);
|
||||
|
||||
return {bucketName, key, params};
|
||||
}
|
||||
#endif
|
||||
|
||||
kj::Promise<Result<void>> curlFileTransfer::upload(
|
||||
const std::string & uri,
|
||||
std::string data,
|
||||
FileTransferOptions options,
|
||||
const Activity * context
|
||||
)
|
||||
try {
|
||||
TRY_AWAIT(enqueueFileTransfer(uri, std::move(options), std::move(data), false, context));
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::optional<std::pair<FileTransferResult, box_ptr<AsyncInputStream>>>>>
|
||||
curlFileTransfer::tryEagerTransfers(
|
||||
const std::string & uri,
|
||||
const FileTransferOptions & options,
|
||||
const std::optional<std::string> & data,
|
||||
bool noBody
|
||||
)
|
||||
try {
|
||||
// curl transfers using file:// urls cannot be paused, and are a bit unruly
|
||||
// in other ways too. since their metadata is trivial and we already have a
|
||||
// backend for simple file system reads we can use that instead. we'll pass
|
||||
// uploads to files to curl even so, those will fail in enqueueItem anyway.
|
||||
// on all other decoding failures we also let curl fail for us a bit later.
|
||||
//
|
||||
// note that we use kj to decode the url, not curl. curl uses only the path
|
||||
// component of the url to determine the file name, but it does note expose
|
||||
// the decoding method it uses for this. for file:// transfers curl forbids
|
||||
// only \0 characters in the urldecoded path, not all control characters as
|
||||
// it does in the public curl_url_get(CURLUPART_PATH, CURLU_URLDECODE) api.
|
||||
//
|
||||
// also note: everything weird you see here is for compatibility with curl.
|
||||
// we can't even fix it because nix-channel relies on this. even reading of
|
||||
// directories being allowed and returning something (though hopefully it's
|
||||
// enough to return anything instead of a directory listing like curl does)
|
||||
if (uri.starts_with("file://") && !data.has_value()) {
|
||||
if (!uri.starts_with("file:///")) {
|
||||
throw FileTransferError(NotFound, std::nullopt, "file not found");
|
||||
}
|
||||
auto url = curl_url();
|
||||
if (!url) {
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
KJ_DEFER(curl_url_cleanup(url));
|
||||
curl_url_set(url, CURLUPART_URL, requireCString(uri), 0);
|
||||
char * path = nullptr;
|
||||
curl_url_get(url, CURLUPART_PATH, &path, 0);
|
||||
auto decoded = kj::decodeUriComponent(kj::arrayPtr(path, path + strlen(path)));
|
||||
if (!decoded.hadErrors && decoded.findFirst(0) == nullptr) {
|
||||
Path fsPath(decoded.cStr(), decoded.size());
|
||||
FileTransferResult metadata{.effectiveUri = std::string("file://") + path};
|
||||
struct stat st;
|
||||
AutoCloseFD fd(sys::open(fsPath, O_RDONLY));
|
||||
if (!fd || fstat(fd.get(), &st) != 0) {
|
||||
throw FileTransferError(
|
||||
NotFound, std::nullopt, "%s: file not found (%s)", fsPath, strerror(errno)
|
||||
);
|
||||
}
|
||||
if (S_ISDIR(st.st_mode)) {
|
||||
co_return std::pair{
|
||||
std::move(metadata), make_box_ptr<AsyncStringInputStream>("")
|
||||
};
|
||||
}
|
||||
struct OwningFdStream : AsyncInputStream
|
||||
{
|
||||
AutoCloseFD fd;
|
||||
OwningFdStream(AutoCloseFD fd) : fd(std::move(fd)) {}
|
||||
kj::Promise<Result<std::optional<size_t>>>
|
||||
read(void * buffer, size_t size) override
|
||||
{
|
||||
// NOTE the synchronous implementation used to have a buffer for
|
||||
// file data, but we cannot be bothered to treat this edge case.
|
||||
if (const auto got = ::read(fd.get(), buffer, size); got >= 0) {
|
||||
if (got == 0) {
|
||||
return {result::success(std::nullopt)};
|
||||
} else {
|
||||
return {result::success(got)};
|
||||
}
|
||||
} else {
|
||||
return {result::failure(std::make_exception_ptr(SysError("reading file")
|
||||
))};
|
||||
}
|
||||
}
|
||||
};
|
||||
co_return std::pair{
|
||||
std::move(metadata), make_box_ptr<OwningFdStream>(std::move(fd))
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/* Ugly hack to support s3:// URIs. */
|
||||
if (uri.starts_with("s3://")) {
|
||||
// FIXME: do this on a worker thread
|
||||
#if ENABLE_S3
|
||||
auto [bucketName, key, params] = parseS3Uri(uri);
|
||||
|
||||
std::string profile = getOr(params, "profile", "");
|
||||
std::string region = getOr(params, "region", Aws::Region::US_EAST_1);
|
||||
std::string scheme = getOr(params, "scheme", "");
|
||||
std::string endpoint = getOr(params, "endpoint", "");
|
||||
|
||||
S3Helper s3Helper(profile, region, scheme, endpoint);
|
||||
|
||||
// FIXME: implement ETag
|
||||
auto s3Res = TRY_AWAIT(s3Helper.getObject(bucketName, key));
|
||||
FileTransferResult res;
|
||||
if (!s3Res.data)
|
||||
throw FileTransferError(NotFound, "S3 object '%s' does not exist", uri);
|
||||
struct OwningStringStream : private std::string, AsyncStringInputStream
|
||||
{
|
||||
OwningStringStream(std::string data)
|
||||
: std::string(std::move(data))
|
||||
, AsyncStringInputStream(*this)
|
||||
{
|
||||
}
|
||||
};
|
||||
co_return std::pair{res, make_box_ptr<OwningStringStream>(std::move(*s3Res.data))};
|
||||
#else
|
||||
throw nix::Error(
|
||||
"cannot download '%s' because Lix is not built with S3 support", uri
|
||||
);
|
||||
#endif
|
||||
}
|
||||
|
||||
co_return std::nullopt;
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::pair<FileTransferResult, box_ptr<AsyncInputStream>>>>
|
||||
curlFileTransfer::enqueueFileTransfer(
|
||||
const std::string & uri,
|
||||
FileTransferOptions && options,
|
||||
std::optional<std::string> data,
|
||||
bool noBody,
|
||||
const Activity * context
|
||||
)
|
||||
try {
|
||||
if (auto eager = TRY_AWAIT(tryEagerTransfers(uri, options, data, noBody))) {
|
||||
co_return std::move(*eager);
|
||||
}
|
||||
|
||||
auto source = make_box_ptr<TransferStream>(
|
||||
*this, uri, std::move(options), std::move(data), noBody, context
|
||||
);
|
||||
TRY_AWAIT(source->init());
|
||||
TRY_AWAIT(source->awaitData());
|
||||
co_return {source->metadata, std::move(source)};
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
curlFileTransfer::TransferStream::TransferStream(
|
||||
curlFileTransfer & parent,
|
||||
const std::string & uri,
|
||||
FileTransferOptions && options,
|
||||
std::optional<std::string> data,
|
||||
bool noBody,
|
||||
const Activity * context
|
||||
)
|
||||
: parent(parent.multi)
|
||||
, uri(uri)
|
||||
, options(options)
|
||||
, data(std::move(data))
|
||||
, noBody(noBody)
|
||||
, parentAct(context)
|
||||
, backoff(backoffTimeouts(
|
||||
fileTransferSettings.tries,
|
||||
std::chrono::seconds(fileTransferSettings.maxConnectTimeout.get()),
|
||||
std::chrono::seconds(fileTransferSettings.initialConnectTimeout.get()),
|
||||
std::chrono::milliseconds(this->parent->baseRetryTimeMs)
|
||||
))
|
||||
{
|
||||
}
|
||||
|
||||
curlFileTransfer::TransferStream::~TransferStream()
|
||||
{
|
||||
// wake up the download thread if it's still going and have it abort
|
||||
try {
|
||||
if (transfer) {
|
||||
parent->cancel(transfer);
|
||||
}
|
||||
} catch (...) {
|
||||
ignoreExceptionInDestructor();
|
||||
}
|
||||
}
|
||||
|
||||
kj::Promise<Result<void>> curlFileTransfer::TransferStream::init()
|
||||
try {
|
||||
metadata = TRY_AWAIT(withRetries(
|
||||
[&]() {
|
||||
return startTransfer(
|
||||
uri, std::chrono::seconds(fileTransferSettings.initialConnectTimeout.get())
|
||||
);
|
||||
},
|
||||
[&](const std::chrono::milliseconds & timeout) {
|
||||
return startTransfer(uri, timeout);
|
||||
}
|
||||
));
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<FileTransferResult>> curlFileTransfer::TransferStream::startTransfer(
|
||||
const std::string & uri,
|
||||
const std::chrono::milliseconds & timeout,
|
||||
curl_off_t offset
|
||||
)
|
||||
try {
|
||||
auto uploadData = data ? std::optional(std::string_view(*data)) : std::nullopt;
|
||||
auto pfp = kj::newPromiseAndCrossThreadFulfiller<Result<FileTransferResult>>();
|
||||
transfer = std::make_shared<TransferItem>(
|
||||
uri,
|
||||
std::move(options),
|
||||
parentAct,
|
||||
uploadData,
|
||||
noBody,
|
||||
offset,
|
||||
std::move(pfp.fulfiller),
|
||||
timeout
|
||||
);
|
||||
parent->enqueueItem(transfer);
|
||||
co_return TRY_AWAIT(pfp.promise);
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<void>> curlFileTransfer::TransferStream::prepareRetry(
|
||||
const std::string & context,
|
||||
const std::chrono::milliseconds & waitTime,
|
||||
unsigned int attempt
|
||||
)
|
||||
try {
|
||||
if (totalReceived) {
|
||||
printTaggedWarning(
|
||||
"%s; retrying from offset %d in %d ms (attempt %d/%d)",
|
||||
Uncolored(context),
|
||||
totalReceived,
|
||||
waitTime.count(),
|
||||
Uncolored(attempt),
|
||||
Uncolored(tries)
|
||||
);
|
||||
} else {
|
||||
printTaggedWarning(
|
||||
"%s; retrying in %d ms (attempt %d/%d)",
|
||||
Uncolored(context),
|
||||
waitTime.count(),
|
||||
Uncolored(attempt),
|
||||
Uncolored(tries)
|
||||
);
|
||||
}
|
||||
|
||||
co_await AIO().provider.getTimer().afterDelay(waitTime.count() * kj::MILLISECONDS);
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<void>> curlFileTransfer::TransferStream::restartTransfer(const std::chrono::milliseconds & timeout)
|
||||
try {
|
||||
auto onChange =
|
||||
[&](std::string_view what, std::string_view from, std::string_view to, bool throw_
|
||||
) -> void {
|
||||
if (!from.empty() && from != to) {
|
||||
FileTransferError e = FileTransferError(
|
||||
Misc,
|
||||
{},
|
||||
"uri %s changed %s from %s to %s during transfer",
|
||||
uri,
|
||||
what,
|
||||
from,
|
||||
to
|
||||
);
|
||||
|
||||
if (throw_) {
|
||||
throw e;
|
||||
}
|
||||
|
||||
logWarning(e.info());
|
||||
}
|
||||
};
|
||||
|
||||
// use the effective URI of the previous transfer for retries. this avoids
|
||||
// some silent corruption if a redirect changes between starting and retry
|
||||
const auto & uri = metadata.effectiveUri.empty() ? this->uri : metadata.effectiveUri;
|
||||
|
||||
auto newMeta = TRY_AWAIT(startTransfer(uri, timeout, totalReceived));
|
||||
|
||||
onChange("final destination", metadata.effectiveUri, newMeta.effectiveUri, false);
|
||||
onChange("ETag", metadata.etag, newMeta.etag, true);
|
||||
onChange(
|
||||
"immutable url",
|
||||
metadata.immutableUrl.value_or(""),
|
||||
newMeta.immutableUrl.value_or(""),
|
||||
true
|
||||
);
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<bool>> curlFileTransfer::TransferStream::waitForData()
|
||||
try {
|
||||
/* Grab data if available, otherwise wait for the download
|
||||
thread to wake us up. */
|
||||
std::optional<kj::Promise<void>> signal;
|
||||
|
||||
while (buffered.empty()) {
|
||||
if (signal) {
|
||||
co_await *signal;
|
||||
signal.reset();
|
||||
}
|
||||
|
||||
auto state(transfer->downloadState.lock());
|
||||
|
||||
if (!state->data.empty()) {
|
||||
chunk = std::exchange(state->data, {});
|
||||
buffered = chunk;
|
||||
totalReceived += chunk.size();
|
||||
parent->unpause(transfer);
|
||||
} else if (state->exc) {
|
||||
std::rethrow_exception(state->exc);
|
||||
} else if (state->done) {
|
||||
co_return false;
|
||||
} else {
|
||||
parent->unpause(transfer);
|
||||
signal = state->wait();
|
||||
}
|
||||
}
|
||||
|
||||
co_return true;
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<bool>> curlFileTransfer::TransferStream::restartAndWaitForData(const std::chrono::milliseconds & timeout)
|
||||
try {
|
||||
TRY_AWAIT(restartTransfer(timeout));
|
||||
co_return TRY_AWAIT(waitForData());
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<bool>> curlFileTransfer::TransferStream::awaitData()
|
||||
try {
|
||||
co_return TRY_AWAIT(withRetries(
|
||||
[&] { return waitForData(); },
|
||||
[&](const std::chrono::milliseconds & timeout) {
|
||||
return restartAndWaitForData(timeout);
|
||||
}
|
||||
));
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::optional<size_t>>> curlFileTransfer::TransferStream::read(void * buffer, size_t len)
|
||||
try {
|
||||
TRACE(LIX_STORE_FILETRANSFER_READ(uri.c_str(), len));
|
||||
|
||||
size_t total = 0;
|
||||
auto data = static_cast<char *>(buffer);
|
||||
while (total < len && TRY_AWAIT(awaitData())) {
|
||||
const auto available = std::min(len - total, buffered.size());
|
||||
memcpy(data + total, buffered.data(), available);
|
||||
buffered.remove_prefix(available);
|
||||
total += available;
|
||||
}
|
||||
|
||||
if (total == 0) {
|
||||
co_return std::nullopt;
|
||||
} else {
|
||||
co_return total;
|
||||
}
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<bool>>
|
||||
curlFileTransfer::exists(const std::string & uri, FileTransferOptions options, const Activity * context)
|
||||
try {
|
||||
try {
|
||||
TRY_AWAIT(enqueueFileTransfer(uri, std::move(options), std::nullopt, true, context));
|
||||
co_return true;
|
||||
} catch (FileTransferError & e) {
|
||||
/* S3 buckets return 403 if a file doesn't exist and the
|
||||
bucket is unlistable, so treat 403 as 404. */
|
||||
if (e.error == FileTransfer::NotFound || e.error == FileTransfer::Forbidden)
|
||||
co_return false;
|
||||
throw;
|
||||
}
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::pair<FileTransferResult, box_ptr<AsyncInputStream>>>> curlFileTransfer::download(
|
||||
const std::string & uri, FileTransferOptions options, const Activity * context
|
||||
)
|
||||
{
|
||||
return enqueueFileTransfer(uri, std::move(options), std::nullopt, false, context);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,170 @@
|
||||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "lix/libstore/filetransfer.hh"
|
||||
#include "lix/libstore/store-api.hh"
|
||||
#include "lix/libstore/transferitem.hh"
|
||||
#include "lix/libutil/async-io.hh"
|
||||
#include "lix/libutil/backoff.hh"
|
||||
#include "lix/libutil/box_ptr.hh"
|
||||
#include "lix/libutil/result.hh"
|
||||
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include <kj/async.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
struct CurlMulti;
|
||||
|
||||
struct curlFileTransfer : public FileTransfer
|
||||
{
|
||||
// Types.
|
||||
public:
|
||||
template<typename T>
|
||||
using Async = kj::Promise<Result<T>>;
|
||||
|
||||
struct TransferStream : AsyncInputStream
|
||||
{
|
||||
std::shared_ptr<CurlMulti> parent;
|
||||
std::string uri;
|
||||
FileTransferOptions options;
|
||||
std::optional<std::string> data;
|
||||
bool noBody;
|
||||
const Activity * parentAct;
|
||||
|
||||
std::shared_ptr<TransferItem> transfer;
|
||||
FileTransferResult metadata;
|
||||
std::string chunk;
|
||||
std::string_view buffered;
|
||||
|
||||
const size_t tries = fileTransferSettings.tries;
|
||||
curl_off_t totalReceived = 0;
|
||||
|
||||
Generator<BackoffTiming> backoff;
|
||||
|
||||
TransferStream(
|
||||
curlFileTransfer & parent,
|
||||
const std::string & uri,
|
||||
FileTransferOptions && options,
|
||||
std::optional<std::string> data,
|
||||
bool noBody,
|
||||
const Activity * context
|
||||
);
|
||||
|
||||
~TransferStream();
|
||||
|
||||
kj::Promise<Result<void>> init();
|
||||
|
||||
inline auto withRetries(auto && initial, auto && retry) -> decltype(initial())
|
||||
try {
|
||||
std::optional<std::string> retryContext;
|
||||
BackoffTiming timings;
|
||||
while (true) {
|
||||
try {
|
||||
if (retryContext) {
|
||||
TRY_AWAIT(prepareRetry(*retryContext, timings.waitTime, timings.attempt));
|
||||
co_return TRY_AWAIT(retry(timings.downloadTimeout));
|
||||
} else {
|
||||
co_return TRY_AWAIT(initial());
|
||||
}
|
||||
} catch (FileTransferError & e) {
|
||||
auto next = backoff.next();
|
||||
// If this is a transient error, then maybe retry after a while. after any
|
||||
// bytes have been received we require range support to proceed, otherwise
|
||||
// we'd need to start from scratch and discard everything we already have.
|
||||
if (e.error != Transient || data.has_value() || !next.has_value()
|
||||
|| (totalReceived > 0 && !transfer->acceptsRanges()))
|
||||
{
|
||||
throw;
|
||||
}
|
||||
retryContext = e.what();
|
||||
timings = *next;
|
||||
}
|
||||
}
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<FileTransferResult>> startTransfer(
|
||||
const std::string & uri,
|
||||
const std::chrono::milliseconds & timeout,
|
||||
curl_off_t offset = 0
|
||||
);
|
||||
|
||||
kj::Promise<Result<void>> prepareRetry(
|
||||
const std::string & context,
|
||||
const std::chrono::milliseconds & waitTime,
|
||||
unsigned int attempt
|
||||
);
|
||||
|
||||
kj::Promise<Result<void>> restartTransfer(const std::chrono::milliseconds & timeout);
|
||||
|
||||
kj::Promise<Result<bool>> waitForData();
|
||||
|
||||
kj::Promise<Result<bool>> restartAndWaitForData(const std::chrono::milliseconds & timeout);
|
||||
|
||||
kj::Promise<Result<bool>> awaitData();
|
||||
|
||||
kj::Promise<Result<std::optional<size_t>>> read(void * buffer, size_t len) override;
|
||||
};
|
||||
|
||||
// Fields.
|
||||
public:
|
||||
std::shared_ptr<CurlMulti> multi;
|
||||
|
||||
// Specials.
|
||||
public:
|
||||
curlFileTransfer(unsigned int baseRetryTimeMs);
|
||||
~curlFileTransfer();
|
||||
|
||||
#if ENABLE_S3
|
||||
using S3Uri = std::tuple<std::string, std::string, StoreConfig::Params>;
|
||||
static auto parseS3Uri(std::string url) -> S3Uri;
|
||||
#endif
|
||||
|
||||
// Overrides.
|
||||
public:
|
||||
auto upload(
|
||||
std::string const & uri,
|
||||
std::string data,
|
||||
FileTransferOptions options,
|
||||
Activity const * context
|
||||
) -> Async<void> override;
|
||||
|
||||
auto exists(
|
||||
std::string const & uri,
|
||||
FileTransferOptions options,
|
||||
Activity const * context
|
||||
) -> Async<bool> override;
|
||||
|
||||
auto download(
|
||||
std::string const & uri,
|
||||
FileTransferOptions options,
|
||||
Activity const * context = nullptr
|
||||
) -> Async<std::pair<FileTransferResult, box_ptr<AsyncInputStream>>> override;
|
||||
|
||||
// Actual API.
|
||||
public:
|
||||
auto tryEagerTransfers(
|
||||
std::string const & url,
|
||||
FileTransferOptions const & options,
|
||||
std::optional<std::string> const & data,
|
||||
bool noBody
|
||||
) -> Async<std::optional<std::pair<FileTransferResult, box_ptr<AsyncInputStream>>>>;
|
||||
|
||||
auto enqueueFileTransfer(
|
||||
std::string const & uri,
|
||||
FileTransferOptions && options,
|
||||
std::optional<std::string> data,
|
||||
bool noBody,
|
||||
Activity const * context
|
||||
) -> Async<std::pair<FileTransferResult, box_ptr<AsyncInputStream>>>;
|
||||
};
|
||||
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
#include "lix/libstore/curlmulti.hh"
|
||||
#include "lix/libstore/filetransfer.hh"
|
||||
#include "lix/libutil/error.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include "lix/libutil/namespaces.hh"
|
||||
#include "lix/libutil/signals.hh"
|
||||
#include "lix/libutil/thread-name.hh"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <curl/curl.h>
|
||||
#include <curl/multi.h>
|
||||
#include <kj/common.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
void CurlMulti::State::quit()
|
||||
{
|
||||
quitting = true;
|
||||
// We will not be processing any more requests.
|
||||
for (auto & item : incoming) {
|
||||
item->finish(CURLE_ABORTED_BY_CALLBACK);
|
||||
}
|
||||
incoming.clear();
|
||||
unpause.clear();
|
||||
// make a note that we're dying and acknowledge all pending cancel
|
||||
// requests by individual transfers. not doing this can cause bugs
|
||||
// like #1218 in which the process deadlocks waiting for transfers
|
||||
// to cancel with no download thread to make this happen; this was
|
||||
// likely caused by a transfer requesting a cancellation *exactly*
|
||||
// before a signal was received, causing the curl thread to die in
|
||||
// a hurry without processing cancellations. the transfer is stuck
|
||||
// from that point on, and since this happened in a destructor the
|
||||
// entire process locked up solid. curl exceptions could have also
|
||||
// caused this; we set the `quit` flag just in case to avoid this.
|
||||
for (auto & [item, promise] : cancel) {
|
||||
promise.set_value();
|
||||
}
|
||||
}
|
||||
|
||||
CurlMulti::CurlMulti(unsigned int baseRetryTimeMs)
|
||||
: curlm(curl_multi_init())
|
||||
, baseRetryTimeMs(baseRetryTimeMs)
|
||||
{
|
||||
if (curlm == nullptr) {
|
||||
throw FileTransferError(FileTransfer::Misc, {}, "could not allocate curl handle");
|
||||
}
|
||||
|
||||
static std::once_flag globalInit;
|
||||
std::call_once(globalInit, curl_global_init, CURL_GLOBAL_ALL);
|
||||
|
||||
curl_multi_setopt(curlm.get(), CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
||||
curl_multi_setopt(curlm.get(), CURLMOPT_MAX_TOTAL_CONNECTIONS,
|
||||
fileTransferSettings.httpConnections.get());
|
||||
|
||||
workerThread = std::thread([&]() {
|
||||
setCurrentThreadName("curlFileTransfer worker");
|
||||
workerThreadEntry();
|
||||
});
|
||||
}
|
||||
|
||||
CurlMulti::~CurlMulti()
|
||||
{
|
||||
try {
|
||||
stopWorkerThread();
|
||||
} catch (nix::Error & e) {
|
||||
// This can only fail if a socket to our own process cannot be
|
||||
// written to, so it is always a bug in the program if it fails.
|
||||
//
|
||||
// Joining the thread would probably only cause a deadlock if this
|
||||
// happened, so just die on purpose.
|
||||
printError("failed to join curl file transfer worker thread: %1%", e.what());
|
||||
std::terminate();
|
||||
}
|
||||
workerThread.join();
|
||||
}
|
||||
|
||||
void CurlMulti::unpause(const std::shared_ptr<TransferItem> & transfer)
|
||||
{
|
||||
auto lock = state_.lock();
|
||||
lock->unpause.push_back(transfer);
|
||||
wakeup(*lock);
|
||||
}
|
||||
|
||||
void CurlMulti::cancel(const std::shared_ptr<TransferItem> & transfer)
|
||||
{
|
||||
std::promise<void> promise;
|
||||
auto wait = promise.get_future();
|
||||
{
|
||||
auto lock = state_.lock();
|
||||
if (lock->quitting) {
|
||||
return;
|
||||
}
|
||||
lock->cancel[transfer] = std::move(promise);
|
||||
wakeup(*lock);
|
||||
}
|
||||
wait.get();
|
||||
}
|
||||
|
||||
void CurlMulti::wakeup(State & locked)
|
||||
{
|
||||
locked.workAvailable = true;
|
||||
if (auto mc = curl_multi_wakeup(curlm.get()))
|
||||
throw nix::Error("unexpected error from curl_multi_wakeup(): %s", curl_multi_strerror(mc));
|
||||
}
|
||||
|
||||
void CurlMulti::stopWorkerThread()
|
||||
{
|
||||
/* Signal the worker thread to exit. */
|
||||
auto state(state_.lock());
|
||||
state->quit();
|
||||
wakeup(*state);
|
||||
}
|
||||
|
||||
void CurlMulti::workerThreadMain()
|
||||
{
|
||||
/* Cause this thread to be notified on SIGINT. */
|
||||
auto callback = createInterruptCallback([&]() {
|
||||
stopWorkerThread();
|
||||
});
|
||||
|
||||
unshareFilesystem();
|
||||
|
||||
std::map<CURL *, std::shared_ptr<TransferItem>> items;
|
||||
|
||||
// clear all current transfers in case of an early exit, as can happen
|
||||
// via Interrupted if the interruption occured right before a log call
|
||||
KJ_DEFER({
|
||||
for (auto & [_, item] : items) {
|
||||
item->finish(CURLE_ABORTED_BY_CALLBACK);
|
||||
}
|
||||
|
||||
auto lock = state_.lock();
|
||||
lock->quit();
|
||||
});
|
||||
|
||||
bool quit = false;
|
||||
|
||||
// NOTE: we will need to use CURLMOPT_TIMERFUNCTION to integrate this
|
||||
// loop with kj. until then curl will handle its timeouts internally.
|
||||
int64_t timeoutMs = INT64_MAX;
|
||||
|
||||
while (!quit) {
|
||||
/* Let curl do its thing. */
|
||||
int running;
|
||||
CURLMcode mc = curl_multi_perform(curlm.get(), &running);
|
||||
if (mc != CURLM_OK)
|
||||
throw nix::Error("unexpected error from curl_multi_perform(): %s", curl_multi_strerror(mc));
|
||||
|
||||
/* Set the promises of any finished requests. */
|
||||
CURLMsg * msg;
|
||||
int left;
|
||||
while ((msg = curl_multi_info_read(curlm.get(), &left))) {
|
||||
if (msg->msg == CURLMSG_DONE) {
|
||||
auto i = items.find(msg->easy_handle);
|
||||
assert(i != items.end());
|
||||
i->second->finish(msg->data.result);
|
||||
curl_multi_remove_handle(curlm.get(), i->second->req.get());
|
||||
items.erase(i);
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
auto cancel = [&] { return std::exchange(state_.lock()->cancel, {}); }();
|
||||
for (auto & [item, promise] : cancel) {
|
||||
curl_multi_remove_handle(curlm.get(), item->req.get());
|
||||
items.erase(item->req.get());
|
||||
promise.set_value();
|
||||
}
|
||||
}
|
||||
|
||||
/* Add new curl requests from the incoming requests queue,
|
||||
except for requests that are embargoed (waiting for a
|
||||
retry timeout to expire). */
|
||||
|
||||
std::vector<std::shared_ptr<TransferItem>> incoming;
|
||||
std::vector<std::shared_ptr<TransferItem>> unpause;
|
||||
bool haveWork = false;
|
||||
|
||||
timeoutMs = 10'000;
|
||||
|
||||
{
|
||||
auto state(state_.lock());
|
||||
incoming = std::exchange(state->incoming, {});
|
||||
unpause = std::exchange(state->unpause, {});
|
||||
haveWork = std::exchange(state->workAvailable, false);
|
||||
quit = state->quitting;
|
||||
}
|
||||
|
||||
for (auto & item : incoming) {
|
||||
debug("starting %s of %s", item->verb(), item->uri);
|
||||
curl_multi_add_handle(curlm.get(), item->req.get());
|
||||
items[item->req.get()] = item;
|
||||
}
|
||||
|
||||
// exit immediately and abort all running transfers. waiting for transfers to finish
|
||||
// before exiting this loop may hang the shutdown procedure forever, e.g. if blocked
|
||||
// transfers would be destroyed (thus aborted) after the curl thread for any reason.
|
||||
if (quit) {
|
||||
break;
|
||||
}
|
||||
|
||||
for (auto & item : unpause) {
|
||||
curl_easy_pause(item->req.get(), CURLPAUSE_CONT);
|
||||
}
|
||||
|
||||
if (haveWork) {
|
||||
timeoutMs = 0;
|
||||
}
|
||||
|
||||
/* Wait for activity, including wakeup events. */
|
||||
mc = curl_multi_poll(curlm.get(), nullptr, 0, timeoutMs, nullptr);
|
||||
if (mc != CURLM_OK)
|
||||
throw nix::Error("unexpected error from curl_multi_poll(): %s", curl_multi_strerror(mc));
|
||||
}
|
||||
|
||||
debug("download thread shutting down");
|
||||
}
|
||||
|
||||
void CurlMulti::workerThreadEntry()
|
||||
{
|
||||
try {
|
||||
workerThreadMain();
|
||||
} catch (nix::Interrupted & e) {
|
||||
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
||||
printError("unexpected error in download thread: %s", e.what());
|
||||
} catch (...) {
|
||||
printError("unexpected error in download thread");
|
||||
}
|
||||
|
||||
state_.lock()->quit();
|
||||
}
|
||||
|
||||
void CurlMulti::enqueueItem(std::shared_ptr<TransferItem> item)
|
||||
{
|
||||
if (item->uploadData
|
||||
&& !item->uri.starts_with("http://")
|
||||
&& !item->uri.starts_with("https://"))
|
||||
throw nix::Error("uploading to '%s' is not supported", item->uri);
|
||||
|
||||
{
|
||||
auto state(state_.lock());
|
||||
if (state->quitting)
|
||||
throw nix::Error("cannot enqueue download request because the download thread is shutting down");
|
||||
state->incoming.push_back(item);
|
||||
wakeup(*state);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace nix
|
||||
@@ -0,0 +1,61 @@
|
||||
#pragma once
|
||||
///@file
|
||||
|
||||
#include "lix/libstore/transferitem.hh"
|
||||
#include "lix/libutil/sync.hh"
|
||||
|
||||
#include <future>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include <curl/multi.h>
|
||||
|
||||
namespace nix {
|
||||
|
||||
struct CurlMulti
|
||||
{
|
||||
// Types.
|
||||
public:
|
||||
struct State
|
||||
{
|
||||
bool quitting = false;
|
||||
bool workAvailable = false;
|
||||
std::vector<std::shared_ptr<TransferItem>> incoming;
|
||||
std::vector<std::shared_ptr<TransferItem>> unpause;
|
||||
std::map<std::shared_ptr<TransferItem>, std::promise<void>> cancel;
|
||||
|
||||
void quit();
|
||||
};
|
||||
|
||||
// Fields.
|
||||
public:
|
||||
std::unique_ptr<CURLM, decltype([](auto * m) { curl_multi_cleanup(m); })> curlm;
|
||||
const unsigned int baseRetryTimeMs;
|
||||
Sync<State> state_;
|
||||
std::thread workerThread;
|
||||
|
||||
// Specials.
|
||||
public:
|
||||
CurlMulti(unsigned int baseRetryTimeMs);
|
||||
~CurlMulti();
|
||||
|
||||
// Actual API.
|
||||
public:
|
||||
void unpause(std::shared_ptr<TransferItem> const & transfer);
|
||||
|
||||
void cancel(std::shared_ptr<TransferItem> const & transfer);
|
||||
|
||||
void wakeup(State & locked);
|
||||
|
||||
void stopWorkerThread();
|
||||
|
||||
void workerThreadMain();
|
||||
|
||||
void workerThreadEntry();
|
||||
|
||||
void enqueueItem(std::shared_ptr<TransferItem> item);
|
||||
};
|
||||
|
||||
} // namespace nix
|
||||
@@ -0,0 +1,54 @@
|
||||
@0xd8aa4d286ba6797b;
|
||||
|
||||
# IMPORTANT NOTICE
|
||||
#
|
||||
# these definitions are EXPERIMENTAL and come with NO stability guarantees
|
||||
|
||||
using Cxx = import "/capnp/c++.capnp";
|
||||
$Cxx.namespace("nix::rpc::daemon");
|
||||
$Cxx.allowCancellation;
|
||||
|
||||
using T = import "/lix/libutil/types.capnp";
|
||||
using Log = import "/lix/libutil/logging.capnp";
|
||||
|
||||
struct ProtocolDescription {
|
||||
id @0 :Text;
|
||||
description @1 :Text;
|
||||
}
|
||||
|
||||
interface Bootstrap {
|
||||
supported @0 () -> (protocols :List(ProtocolDescription));
|
||||
request @1 (
|
||||
clientInfo :Text,
|
||||
protocol :Text,
|
||||
) -> (result :Protocol);
|
||||
}
|
||||
|
||||
interface Protocol {
|
||||
# TODO maybe add information or something
|
||||
}
|
||||
|
||||
interface LegacyBoot extends(Protocol) $T.throws(T.v1Errors) {
|
||||
enum Trust {
|
||||
unknown @0;
|
||||
untrusted @1;
|
||||
trusted @2;
|
||||
}
|
||||
|
||||
init @0 (
|
||||
logger :Log.LogStream,
|
||||
replyStream :LegacyStream,
|
||||
) -> (result :InitResult);
|
||||
|
||||
struct InitResult {
|
||||
requestStream @0 :LegacyStream;
|
||||
trust @1 :Trust;
|
||||
version @2 :Text;
|
||||
}
|
||||
}
|
||||
|
||||
interface LegacyStream $T.throws(T.v1Errors) {
|
||||
feed @0 (raw :Data) -> stream;
|
||||
# must be called before a new op is started, otherwise errors may get lost
|
||||
sync @1 ();
|
||||
}
|
||||
+353
-51
@@ -1,5 +1,7 @@
|
||||
#include "lix/libstore/daemon.hh"
|
||||
#include "filetransfer.hh"
|
||||
#include "libutil/async.hh"
|
||||
#include "libutil/logging-rpc.hh"
|
||||
#include "lix/libutil/async-io.hh"
|
||||
#include "lix/libutil/monitor-fd.hh"
|
||||
#include "lix/libstore/worker-protocol.hh"
|
||||
@@ -17,12 +19,21 @@
|
||||
#include "lix/libutil/serialise.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
#include "lix/libutil/args.hh"
|
||||
#include "lix/libstore/daemon.capnp.h"
|
||||
#include "lix/libutil/rpc.hh"
|
||||
#include "lix/libutil/types-rpc.hh"
|
||||
|
||||
#include <boost/core/demangle.hpp>
|
||||
#include <capnp/rpc-twoparty.h>
|
||||
#include <cstdint>
|
||||
#include <ctime>
|
||||
#include <kj/encoding.h>
|
||||
#include <kj/exception.h>
|
||||
#include <kj/memory.h>
|
||||
#include <sstream>
|
||||
|
||||
const std::string nix::rpc::daemon::UNSTABLE_LEGACY_TUNNELED = "lix/legacy/" PACKAGE_VERSION;
|
||||
|
||||
namespace nix::daemon {
|
||||
|
||||
Sink & operator << (Sink & sink, const Logger::Fields & fields)
|
||||
@@ -86,7 +97,7 @@ struct TunnelLogger : public Logger
|
||||
|
||||
BufferState log(Verbosity lvl, std::string_view s) override
|
||||
{
|
||||
if (lvl > verbosity) {
|
||||
if (lvl > getVerbosity()) {
|
||||
return BufferState::HasSpace;
|
||||
}
|
||||
|
||||
@@ -97,7 +108,7 @@ struct TunnelLogger : public Logger
|
||||
|
||||
BufferState logEI(const ErrorInfo & ei) override
|
||||
{
|
||||
if (ei.level > verbosity) {
|
||||
if (ei.level > getVerbosity()) {
|
||||
return BufferState::HasSpace;
|
||||
}
|
||||
|
||||
@@ -186,7 +197,7 @@ struct ClientSettings
|
||||
settings.keepFailed.override(keepFailed);
|
||||
settings.keepGoing.override(keepGoing);
|
||||
settings.tryFallback.override(tryFallback);
|
||||
nix::verbosity = verbosity;
|
||||
setVerbosity(verbosity);
|
||||
settings.maxBuildJobs.override(maxBuildJobs);
|
||||
settings.maxSilentTime.override(maxSilentTime);
|
||||
settings.verboseBuild = verboseBuild;
|
||||
@@ -834,11 +845,12 @@ static void performOp(AsyncIoRoot & aio, TunnelLogger * logger, ref<Store> store
|
||||
case WorkerProto::Op::AddBuildLog: {
|
||||
StorePath path{readString(from)};
|
||||
logger->startWork();
|
||||
if (!trusted)
|
||||
throw Error("you are not privileged to add logs");
|
||||
auto & logStore = require<LogStore>(*store);
|
||||
{
|
||||
FramedSource source(from);
|
||||
if (!trusted) {
|
||||
throw Error("you are not privileged to add logs");
|
||||
}
|
||||
StringSink sink;
|
||||
source.drainInto(sink);
|
||||
aio.blockOn(logStore.addBuildLog(path, sink.s));
|
||||
@@ -857,12 +869,66 @@ static void performOp(AsyncIoRoot & aio, TunnelLogger * logger, ref<Store> store
|
||||
}
|
||||
}
|
||||
|
||||
void processConnection(
|
||||
static void processLegacyRequests(
|
||||
AsyncIoRoot & aio,
|
||||
Logger * prevLogger,
|
||||
TunnelLogger * tunnelLogger,
|
||||
ref<Store> store,
|
||||
FdSource & from,
|
||||
FdSink & to,
|
||||
TrustedFlag trusted)
|
||||
TrustedFlag trusted,
|
||||
WorkerProto::Version clientVersion
|
||||
)
|
||||
{
|
||||
unsigned int opCount = 0;
|
||||
|
||||
Finally finally([&]() { printMsgUsing(prevLogger, lvlDebug, "%d operations", opCount); });
|
||||
|
||||
while (true) {
|
||||
WorkerProto::Op op;
|
||||
try {
|
||||
op = (enum WorkerProto::Op) readNum<unsigned>(from);
|
||||
} catch (Interrupted & e) {
|
||||
break;
|
||||
} catch (EndOfFile & e) {
|
||||
break;
|
||||
}
|
||||
|
||||
printMsgUsing(prevLogger, lvlDebug, "received daemon op %d", op);
|
||||
|
||||
opCount++;
|
||||
|
||||
debug("performing daemon worker op: %d", op);
|
||||
|
||||
try {
|
||||
KJ_DEFER(aio.blockOn(logger->flush()));
|
||||
performOp(aio, tunnelLogger, store, trusted, clientVersion, from, to, op);
|
||||
} catch (Error & e) {
|
||||
/* If we're not in a state where we can send replies, then
|
||||
something went wrong processing the input of the
|
||||
client. This can happen especially if I/O errors occur
|
||||
during addTextToStore() / importPath(). If that
|
||||
happens, just send the error message and exit. */
|
||||
bool errorAllowed = tunnelLogger->state_.lock()->canSendStderr;
|
||||
tunnelLogger->stopWork(&e);
|
||||
if (!errorAllowed) {
|
||||
throw;
|
||||
}
|
||||
} catch (std::bad_alloc & e) {
|
||||
auto ex = Error("Lix daemon out of memory");
|
||||
tunnelLogger->stopWork(&ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
to.flush();
|
||||
|
||||
assert(!tunnelLogger->state_.lock()->canSendStderr);
|
||||
}
|
||||
}
|
||||
|
||||
void processLegacyConnection(
|
||||
AsyncIoRoot & aio, ref<Store> store, FdSource & from, FdSink & to, TrustedFlag trusted
|
||||
)
|
||||
{
|
||||
auto monitor = std::make_unique<MonitorFdHup>(from.fd);
|
||||
|
||||
@@ -880,10 +946,6 @@ void processConnection(
|
||||
auto prevLogger = nix::logger;
|
||||
logger = tunnelLogger;
|
||||
|
||||
unsigned int opCount = 0;
|
||||
|
||||
Finally finally([&]() { printMsgUsing(prevLogger, lvlDebug, "%d operations", opCount); });
|
||||
|
||||
// FIXME: what is *supposed* to be in this even?
|
||||
if (readNum<unsigned>(from)) {
|
||||
// Obsolete CPU affinity.
|
||||
@@ -906,49 +968,10 @@ void processConnection(
|
||||
tunnelLogger->startWork();
|
||||
|
||||
try {
|
||||
|
||||
tunnelLogger->stopWork();
|
||||
to.flush();
|
||||
|
||||
/* Process client requests. */
|
||||
while (true) {
|
||||
WorkerProto::Op op;
|
||||
try {
|
||||
op = (enum WorkerProto::Op) readNum<unsigned>(from);
|
||||
} catch (Interrupted & e) {
|
||||
break;
|
||||
} catch (EndOfFile & e) {
|
||||
break;
|
||||
}
|
||||
|
||||
printMsgUsing(prevLogger, lvlDebug, "received daemon op %d", op);
|
||||
|
||||
opCount++;
|
||||
|
||||
debug("performing daemon worker op: %d", op);
|
||||
|
||||
try {
|
||||
performOp(aio, tunnelLogger, store, trusted, clientVersion, from, to, op);
|
||||
} catch (Error & e) {
|
||||
/* If we're not in a state where we can send replies, then
|
||||
something went wrong processing the input of the
|
||||
client. This can happen especially if I/O errors occur
|
||||
during addTextToStore() / importPath(). If that
|
||||
happens, just send the error message and exit. */
|
||||
bool errorAllowed = tunnelLogger->state_.lock()->canSendStderr;
|
||||
tunnelLogger->stopWork(&e);
|
||||
if (!errorAllowed) throw;
|
||||
} catch (std::bad_alloc & e) {
|
||||
auto ex = Error("Lix daemon out of memory");
|
||||
tunnelLogger->stopWork(&ex);
|
||||
throw;
|
||||
}
|
||||
|
||||
to.flush();
|
||||
|
||||
assert(!tunnelLogger->state_.lock()->canSendStderr);
|
||||
};
|
||||
|
||||
processLegacyRequests(aio, prevLogger, tunnelLogger, store, from, to, trusted, clientVersion);
|
||||
} catch (Error & e) {
|
||||
tunnelLogger->stopWork(&e);
|
||||
to.flush();
|
||||
@@ -967,4 +990,283 @@ void processConnection(
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
using namespace rpc::daemon;
|
||||
|
||||
// Shared state for all legacy protocol implementation structs
|
||||
struct LegacyState
|
||||
{
|
||||
ref<Store> store;
|
||||
TrustedFlag trusted;
|
||||
|
||||
LegacyState(ref<Store> store, TrustedFlag trusted) : store(store), trusted(trusted) {}
|
||||
};
|
||||
|
||||
struct RequestStreamImpl final : LegacyStream::Server
|
||||
{
|
||||
ref<LegacyState> state;
|
||||
std::exception_ptr error;
|
||||
AsyncFdIoStream workerSock;
|
||||
kj::Promise<void> responseForwarder;
|
||||
|
||||
RequestStreamImpl(
|
||||
ref<LegacyState> state,
|
||||
kj::Promise<std::exception_ptr> error,
|
||||
LegacyStream::Client callbacks,
|
||||
AutoCloseFD workerFd
|
||||
)
|
||||
: state(state)
|
||||
, workerSock(std::move(workerFd))
|
||||
, responseForwarder(forwardResponse(callbacks).exclusiveJoin(
|
||||
error.then([&](std::exception_ptr e) -> kj::Promise<void> {
|
||||
onError(e);
|
||||
return kj::NEVER_DONE;
|
||||
})
|
||||
))
|
||||
{
|
||||
}
|
||||
|
||||
void onError(std::exception_ptr e)
|
||||
{
|
||||
if (!error) {
|
||||
error = e;
|
||||
}
|
||||
}
|
||||
|
||||
kj::Promise<void> forwardResponse(LegacyStream::Client callbacks)
|
||||
try {
|
||||
std::array<char, 8192> buf;
|
||||
while (true) {
|
||||
if (auto got = TRY_AWAIT(workerSock.read(buf.data(), buf.size())); !got) {
|
||||
break;
|
||||
} else {
|
||||
auto req = callbacks.feedRequest();
|
||||
req.initRaw(*got);
|
||||
std::copy(buf.begin(), buf.begin() + *got, req.getRaw().begin());
|
||||
TRY_AWAIT_RPC(req.send());
|
||||
}
|
||||
}
|
||||
TRY_AWAIT_RPC(callbacks.syncRequest().send());
|
||||
} catch (...) {
|
||||
onError(std::current_exception());
|
||||
}
|
||||
|
||||
kj::Promise<void> feed(FeedContext context) override
|
||||
try {
|
||||
if (error) {
|
||||
std::rethrow_exception(error);
|
||||
}
|
||||
auto bytes = context.getParams().getRaw();
|
||||
TRY_AWAIT(workerSock.writeFull(bytes.begin(), bytes.size()));
|
||||
} catch (...) {
|
||||
onError(std::current_exception());
|
||||
rpc::rethrow_as_rpc_error();
|
||||
}
|
||||
|
||||
kj::Promise<void> sync(SyncContext context) override
|
||||
try {
|
||||
TRY_AWAIT(logger->flush());
|
||||
if (error) {
|
||||
std::rethrow_exception(error);
|
||||
}
|
||||
} catch (...) {
|
||||
rpc::rethrow_as_rpc_error();
|
||||
}
|
||||
};
|
||||
|
||||
struct LegacyBootImpl final : LegacyBoot::Server
|
||||
{
|
||||
ref<LegacyState> state;
|
||||
bool used = false;
|
||||
|
||||
LegacyBootImpl(TrustedFlag trusted, ref<Store> store) : state(make_ref<LegacyState>(store, trusted)) {}
|
||||
|
||||
kj::Promise<void> init(InitContext context) override
|
||||
try {
|
||||
if (used) {
|
||||
throw Error("connection already initialized");
|
||||
}
|
||||
|
||||
auto prevLogger = logger;
|
||||
logger = rpc::log::makeRpcLoggerClient(context.getParams().getLogger());
|
||||
|
||||
auto args = context.getParams();
|
||||
auto result = context.initResults().initResult();
|
||||
// We and the underlying store both need to trust the client for it to be trusted.
|
||||
if (!state->trusted) {
|
||||
result.setTrust(LegacyBoot::Trust::UNTRUSTED);
|
||||
} else if (auto trust = TRY_AWAIT(state->store->isTrustedClient()); trust) {
|
||||
result.setTrust(*trust ? LegacyBoot::Trust::TRUSTED : LegacyBoot::Trust::UNTRUSTED);
|
||||
} else {
|
||||
result.setTrust(LegacyBoot::Trust::UNKNOWN);
|
||||
}
|
||||
result.setVersion(PACKAGE_VERSION);
|
||||
|
||||
auto [rpcSock, workerSock] = SocketPair::stream();
|
||||
auto pfp = kj::newPromiseAndCrossThreadFulfiller<std::exception_ptr>();
|
||||
|
||||
struct Request
|
||||
{
|
||||
AutoCloseFD fd;
|
||||
kj::Own<kj::CrossThreadPromiseFulfiller<std::exception_ptr>> signal;
|
||||
|
||||
Request(AutoCloseFD fd, kj::Own<kj::CrossThreadPromiseFulfiller<std::exception_ptr>> fulfiller)
|
||||
: fd(std::move(fd))
|
||||
, signal(std::move(fulfiller))
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
auto req = make_ref<Request>(std::move(rpcSock), std::move(pfp.fulfiller));
|
||||
|
||||
auto legacyThread = std::async(std::launch::async, [prevLogger, state{state}, req] {
|
||||
AsyncIoRoot aio;
|
||||
FdSource from(req->fd.get());
|
||||
FdSink to(req->fd.get());
|
||||
TunnelLogger logger(to, PROTOCOL_VERSION);
|
||||
|
||||
try {
|
||||
processLegacyRequests(
|
||||
aio, prevLogger, &logger, state->store, from, to, state->trusted, PROTOCOL_VERSION
|
||||
);
|
||||
} catch (Error & e) {
|
||||
req->signal->fulfill(std::current_exception());
|
||||
} catch (std::bad_alloc & e) {
|
||||
req->signal->fulfill(std::make_exception_ptr(Error("Lix daemon out of memory")));
|
||||
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
||||
// TODO print stack trace to daemon log, maybe crash?
|
||||
// boost stacktrace has from_current_exception (at a cost) with not-great symbolization,
|
||||
// cpptrace has a *much* better symbolizer (at unknown cost)
|
||||
req->signal->fulfill(
|
||||
std::make_exception_ptr(Error(
|
||||
"Unexpected exception on the Lix daemon; this is a bug in Lix.\n"
|
||||
"We would appreciate a report of the circumstances it happened in at "
|
||||
"https://git.lix.systems/lix-project/lix.\n%s: %s",
|
||||
Uncolored(boost::core::demangle(typeid(e).name())),
|
||||
e.what()
|
||||
))
|
||||
);
|
||||
} catch (...) {
|
||||
// TODO print stack trace to daemon log, maybe crash?
|
||||
req->signal->fulfill(
|
||||
std::make_exception_ptr(Error(
|
||||
"Unexpected exception on the Lix daemon; this is a bug in Lix.\n"
|
||||
"We would appreciate a report of the circumstances it happened in at "
|
||||
"https://git.lix.systems/lix-project/lix.\n"
|
||||
))
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
result.setRequestStream(
|
||||
kj::heap<RequestStreamImpl>(
|
||||
state, std::move(pfp.promise), args.getReplyStream(), std::move(workerSock)
|
||||
)
|
||||
.attach(std::move(legacyThread))
|
||||
);
|
||||
used = true;
|
||||
} catch (...) {
|
||||
rpc::rethrow_as_rpc_error();
|
||||
}
|
||||
};
|
||||
|
||||
struct BootstrapImpl final : Bootstrap::Server
|
||||
{
|
||||
struct ProtocolEntry
|
||||
{
|
||||
std::string description;
|
||||
std::function<rpc::daemon::Protocol::Client(TrustedFlag, ref<Store>)> factory;
|
||||
};
|
||||
|
||||
TrustedFlag trusted;
|
||||
ref<Store> store;
|
||||
std::map<kj::StringPtr, ProtocolEntry> protocols;
|
||||
bool used = false;
|
||||
|
||||
BootstrapImpl(TrustedFlag trusted, ref<Store> store) : trusted(trusted), store(store)
|
||||
{
|
||||
if (store->isThreadSafe()) {
|
||||
protocols.emplace(
|
||||
rpc::daemon::UNSTABLE_LEGACY_TUNNELED,
|
||||
ProtocolEntry{"tunneled legacy wire protocol", [](TrustedFlag trusted, ref<Store> store) {
|
||||
return kj::heap<LegacyBootImpl>(trusted, store);
|
||||
}}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
kj::Promise<void> supported(SupportedContext context) override
|
||||
{
|
||||
if (!experimentalFeatureSettings.isEnabled(Xp::RpcSockets)) {
|
||||
kj::throwFatalException(
|
||||
kj::Exception(
|
||||
kj::Exception::Type::UNIMPLEMENTED, "main", 0, kj::str("rpc sockets not enabled")
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
auto result = context.initResults();
|
||||
auto protocols = result.initProtocols(this->protocols.size());
|
||||
for (auto [i, proto] : enumerate(this->protocols)) {
|
||||
protocols[i].setId(proto.first);
|
||||
protocols[i].setDescription(proto.second.description);
|
||||
}
|
||||
return kj::READY_NOW;
|
||||
}
|
||||
|
||||
kj::Promise<void> request(RequestContext context) override
|
||||
{
|
||||
auto id = rpc::to<std::string>(context.getParams().getProtocol());
|
||||
if (used) {
|
||||
kj::throwFatalException(
|
||||
kj::Exception(
|
||||
kj::Exception::Type::FAILED, "main", 0, kj::str("connection already initialized")
|
||||
)
|
||||
);
|
||||
} else if (const auto & protocol = get(protocols, id)) {
|
||||
used = true;
|
||||
context.initResults().setResult(protocol->factory(trusted, store));
|
||||
} else {
|
||||
kj::throwFatalException(
|
||||
kj::Exception(
|
||||
kj::Exception::Type::UNIMPLEMENTED, "main", 0, kj::str("unsupported protocol", id)
|
||||
)
|
||||
);
|
||||
}
|
||||
return kj::READY_NOW;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
kj::Promise<Result<void>>
|
||||
processConnection(ref<Store> store, kj::AsyncIoStream & connection, TrustedFlag trusted)
|
||||
try {
|
||||
// TODO trace encoders can do neat error info things, use them. we could stuff some serialized
|
||||
// error struct into the remote trace field instead of using result types and get pipelineable
|
||||
// calls out of it. needs more investigation to say if it's worth the possible reporting skew.
|
||||
capnp::TwoPartyServer server{kj::heap<BootstrapImpl>(trusted, store)};
|
||||
|
||||
// NOTE we can't easily disconnect a peer without shutting shutting down the socket connection
|
||||
// independently of capnp since capnp does not offer such functionality. shutting down sockets
|
||||
// in this manner is very disruptive and pretty unreliable, so we will have to find some other
|
||||
// way to disconnect clients. or we just don't do it because the DoS risk is not large anyway.
|
||||
//
|
||||
// since we have control over the promise we can have the following await finish early to stop
|
||||
// processing events, and if we close the socket after that we've dropped the connection. this
|
||||
// does not guarantee that responses have been sent though, so we can only do this on requests
|
||||
// received *after* a fatal error response has been *sent*, inflicting per-operation overhead.
|
||||
{
|
||||
auto prevLogger = logger;
|
||||
co_await server.accept(connection).exclusiveJoin(connection.whenWriteDisconnected());
|
||||
// NOTE: we do not flush the logger here because the connection is already closed! we only
|
||||
// delete the non-local logger (if one was set) to ensure all rpc references were dropped.
|
||||
if (prevLogger != logger) {
|
||||
std::swap(prevLogger, logger);
|
||||
delete prevLogger;
|
||||
}
|
||||
}
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
}
|
||||
|
||||
+12
-6
@@ -4,14 +4,20 @@
|
||||
#include "lix/libutil/async.hh"
|
||||
#include "lix/libutil/serialise.hh"
|
||||
#include "lix/libstore/store-api.hh"
|
||||
#include <kj/async-io.h>
|
||||
|
||||
namespace nix::rpc::daemon {
|
||||
// legacy boot protocol. EXPLICITLY UNSTABLE, this id will change frequently and without notice.
|
||||
// every change to the experimental tunneling protocol may also change this protocol identifier.
|
||||
extern const std::string UNSTABLE_LEGACY_TUNNELED;
|
||||
}
|
||||
|
||||
namespace nix::daemon {
|
||||
|
||||
void processConnection(
|
||||
AsyncIoRoot & aio,
|
||||
ref<Store> store,
|
||||
FdSource & from,
|
||||
FdSink & to,
|
||||
TrustedFlag trusted);
|
||||
void processLegacyConnection(
|
||||
AsyncIoRoot & aio, ref<Store> store, FdSource & from, FdSink & to, TrustedFlag trusted
|
||||
);
|
||||
|
||||
kj::Promise<Result<void>>
|
||||
processConnection(ref<Store> store, kj::AsyncIoStream & connection, TrustedFlag trusted);
|
||||
}
|
||||
|
||||
@@ -744,12 +744,6 @@ WireFormatGenerator serializeDerivation(const Store & store, const BasicDerivati
|
||||
}
|
||||
}
|
||||
|
||||
void writeDerivation(Sink & out, const Store & store, const BasicDerivation & drv)
|
||||
{
|
||||
out << serializeDerivation(store, drv);
|
||||
}
|
||||
|
||||
|
||||
std::string hashPlaceholder(const OutputNameView outputName)
|
||||
{
|
||||
// FIXME: memoize?
|
||||
|
||||
@@ -359,7 +359,6 @@ struct Sink;
|
||||
|
||||
Source & readDerivation(Source & in, const Store & store, BasicDerivation & drv, std::string_view name);
|
||||
WireFormatGenerator serializeDerivation(const Store & store, const BasicDerivation & drv);
|
||||
void writeDerivation(Sink & out, const Store & store, const BasicDerivation & drv);
|
||||
|
||||
/**
|
||||
* This creates an opaque and almost certainly unique string
|
||||
|
||||
@@ -5,3 +5,4 @@ type: bool
|
||||
default: true
|
||||
---
|
||||
Whether to enable HTTP/2 support.
|
||||
HTTP/2 support cannot be disabled if HTTP/3 support is enabled, the `http2` setting will be ignored in this case.
|
||||
|
||||
@@ -5,3 +5,4 @@ type: bool
|
||||
default: false
|
||||
---
|
||||
Whether to enable HTTP/3 support.
|
||||
Enabling HTTP/3 support forcibly enables HTTP/2 as well; the `http2` will be ignored in this case.
|
||||
|
||||
+10
-1271
File diff suppressed because it is too large
Load Diff
@@ -3,17 +3,17 @@
|
||||
|
||||
#include "lix/libutil/async-io.hh"
|
||||
#include "lix/libutil/box_ptr.hh"
|
||||
#include "lix/libutil/ref.hh"
|
||||
#include "lix/libutil/config.hh"
|
||||
#include "lix/libutil/logging.hh"
|
||||
#include "lix/libutil/ref.hh"
|
||||
#include "lix/libutil/result.hh"
|
||||
#include "lix/libutil/serialise.hh"
|
||||
#include "lix/libutil/strings.hh"
|
||||
#include "lix/libutil/types.hh"
|
||||
#include "lix/libutil/config.hh"
|
||||
|
||||
#include <kj/async.h>
|
||||
#include <curl/curl.h>
|
||||
#include <string>
|
||||
#include <future>
|
||||
|
||||
namespace nix {
|
||||
|
||||
@@ -118,7 +118,18 @@ public:
|
||||
std::optional<std::string> response;
|
||||
|
||||
template<typename... Args>
|
||||
FileTransferError(FileTransfer::Error error, std::optional<std::string> response, const Args & ... args);
|
||||
FileTransferError(FileTransfer::Error error, std::optional<std::string> response, const Args & ... args)
|
||||
: Error(args...), error(error), response(response)
|
||||
{
|
||||
const auto hf = HintFmt(args...);
|
||||
// FIXME: Due to https://github.com/NixOS/nix/issues/3841 we don't know how
|
||||
// to print different messages for different verbosity levels. For now
|
||||
// we add some heuristics for detecting when we want to show the response.
|
||||
if (response && (response->size() < 1024 || response->find("<html>") != std::string::npos))
|
||||
err.msg = HintFmt("%1%\n\nresponse body:\n\n%2%", Uncolored(hf.str()), chomp(*response));
|
||||
else
|
||||
err.msg = hf;
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -39,13 +39,13 @@ struct GCOptions
|
||||
* Any that could not be deleted are returned via the
|
||||
* `kept` field of GCResults.
|
||||
*/
|
||||
typedef enum {
|
||||
enum GCAction : uint8_t {
|
||||
gcReturnLive,
|
||||
gcReturnDead,
|
||||
gcDeleteDead,
|
||||
gcDeleteSpecific,
|
||||
gcTryDeleteSpecific,
|
||||
} GCAction;
|
||||
};
|
||||
|
||||
GCAction action{gcDeleteDead};
|
||||
|
||||
|
||||
+47
-5
@@ -67,15 +67,16 @@ Settings::Settings()
|
||||
, nixManDir(canonPath(NIX_MAN_DIR))
|
||||
{
|
||||
if (auto socketDirFromEnv = getEnvNonEmpty("LIX_DAEMON_SOCKET_DIR")) {
|
||||
nixDaemonSockets_ = {{canonPath(*socketDirFromEnv + LEGACY_SOCKET_COMBINED)}};
|
||||
nixDaemonSockets_ = daemon::supportedProtocols(*socketDirFromEnv);
|
||||
socketsPath = *socketDirFromEnv;
|
||||
} else if (auto socketPathFromEnv = getEnvNonEmpty("NIX_DAEMON_SOCKET_PATH")) {
|
||||
nixDaemonSockets_ = {{canonPath(*socketPathFromEnv)}};
|
||||
nixDaemonSockets_ = {{canonPath(*socketPathFromEnv), daemon::Protocol::LEGACY_COMBINED}};
|
||||
socketsPath = *socketPathFromEnv;
|
||||
} else {
|
||||
auto baseDir = nixStateDir + DEFAULT_SOCKET_DIR;
|
||||
socketsPath = baseDir;
|
||||
// this should always match the list of sockets created by daemonLoop and the socket units
|
||||
nixDaemonSockets_ = {
|
||||
{canonPath(baseDir + LEGACY_SOCKET_COMBINED)},
|
||||
};
|
||||
nixDaemonSockets_ = daemon::supportedProtocols(baseDir);
|
||||
}
|
||||
|
||||
buildUsersGroup.setDefault(getuid() == 0 ? "nixbld" : "");
|
||||
@@ -530,4 +531,45 @@ void initLibStore()
|
||||
|
||||
initLibStoreDone = true;
|
||||
}
|
||||
|
||||
namespace daemon {
|
||||
static inline constexpr auto LEGACY_COMBINED_STR = "legacy-combined";
|
||||
static inline constexpr auto LEGACY_STR = "legacy";
|
||||
static inline constexpr auto LIX_XP_1_STR = "lix-xp-1";
|
||||
|
||||
std::list<daemon::Protocol> supportedProtocols(std::optional<PathView> prefix)
|
||||
{
|
||||
const Path base = prefix ? *prefix + "/" : "";
|
||||
return {
|
||||
{base + "lix-xp-1/socket", Protocol::RPC_V1},
|
||||
{base + "socket", Protocol::LEGACY},
|
||||
};
|
||||
}
|
||||
|
||||
std::string_view daemon::Protocol::id() const
|
||||
{
|
||||
switch (type) {
|
||||
case LEGACY_COMBINED:
|
||||
return LEGACY_COMBINED_STR;
|
||||
case LEGACY:
|
||||
return LEGACY_STR;
|
||||
case RPC_V1:
|
||||
return LIX_XP_1_STR;
|
||||
}
|
||||
}
|
||||
|
||||
daemon::Protocol getProtocol(std::string_view protocol, std::optional<PathView> prefix)
|
||||
{
|
||||
const Path base = prefix ? *prefix + "/" : "";
|
||||
if (protocol == LEGACY_COMBINED_STR) {
|
||||
return {prefix ? Path(*prefix) : "", Protocol::LEGACY_COMBINED};
|
||||
} else if (protocol == LEGACY_STR) {
|
||||
return {base + "socket", Protocol::LEGACY_COMBINED};
|
||||
} else if (protocol == LIX_XP_1_STR) {
|
||||
return {base + "lix-xp-1/socket", Protocol::RPC_V1};
|
||||
} else {
|
||||
throw Error("unsupported daemon protocol %s", protocol);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+38
-9
@@ -10,6 +10,37 @@
|
||||
|
||||
namespace nix {
|
||||
|
||||
namespace daemon {
|
||||
struct Protocol
|
||||
{
|
||||
/// path of socket. may be relative or absolute, depending on context.
|
||||
Path path;
|
||||
enum Type : int {
|
||||
LEGACY_COMBINED,
|
||||
LEGACY,
|
||||
RPC_V1,
|
||||
} type;
|
||||
|
||||
/// external identifier of the protocol (eg for `protocol` store parameters)
|
||||
std::string_view id() const;
|
||||
};
|
||||
|
||||
/**
|
||||
* returns the list of protocols we can serve, most preferred first. set the `prefix` to
|
||||
* retrieve socket paths beneath the prefix, otherwise the search suffices are returned.
|
||||
*/
|
||||
std::list<Protocol> supportedProtocols(std::optional<PathView> prefix = {});
|
||||
|
||||
/**
|
||||
* get the protocol description with id `protocol` at `prefix`. if `prefix` is `nullopt`
|
||||
* the returned protocol will contain a relative path. throws an exception if `protocol`
|
||||
* is not supported or unknown. understands all protocols in `supportedProtocols` and is
|
||||
* able to resolve `legacy-combined` even though `supportedProtocols` would never return
|
||||
* it; this is mainly useful for client-side socket resolution using default store uris.
|
||||
*/
|
||||
Protocol getProtocol(std::string_view protocol, std::optional<PathView> prefix = {});
|
||||
}
|
||||
|
||||
typedef enum { smEnabled, smRelaxed, smDisabled } SandboxMode;
|
||||
|
||||
void to_json(JSON & j, const SandboxMode & e);
|
||||
@@ -63,14 +94,7 @@ const uint32_t maxIdsPerBuild =
|
||||
|
||||
class Settings : public Config
|
||||
{
|
||||
public:
|
||||
struct DaemonSocketPath
|
||||
{
|
||||
Path path;
|
||||
};
|
||||
|
||||
private:
|
||||
std::list<DaemonSocketPath> nixDaemonSockets_;
|
||||
std::list<daemon::Protocol> nixDaemonSockets_;
|
||||
|
||||
unsigned int getDefaultCores();
|
||||
|
||||
@@ -105,6 +129,11 @@ public:
|
||||
*/
|
||||
Path nixStateDir;
|
||||
|
||||
/**
|
||||
* The directory where sockets are stored.
|
||||
*/
|
||||
Path socketsPath;
|
||||
|
||||
/**
|
||||
* The directory where system configuration files are stored.
|
||||
*/
|
||||
@@ -128,7 +157,7 @@ public:
|
||||
/**
|
||||
* Socket paths a client should connect to, in order of decreasing preference.
|
||||
*/
|
||||
const std::list<DaemonSocketPath> & nixDaemonSockets() const
|
||||
const std::list<daemon::Protocol> & nixDaemonSockets() const
|
||||
{
|
||||
return nixDaemonSockets_;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
#include "lix/libstore/legacy-ssh-store.hh"
|
||||
#include "libutil/error.hh"
|
||||
#include "libutil/logging.hh"
|
||||
#include "libutil/sync.hh"
|
||||
#include "lix/libutil/archive.hh"
|
||||
#include "lix/libutil/async-io.hh"
|
||||
#include "lix/libutil/async.hh"
|
||||
@@ -20,6 +23,8 @@
|
||||
#include "path-info.hh"
|
||||
#include "path.hh"
|
||||
#include <cstdint>
|
||||
#include <exception>
|
||||
#include <kj/async.h>
|
||||
#include <optional>
|
||||
|
||||
namespace nix {
|
||||
@@ -82,9 +87,6 @@ struct LegacySSHStoreConfig : CommonSSHStoreConfig
|
||||
const Setting<Path> remoteProgram{this, "nix-store", "remote-program",
|
||||
"Path to the `nix-store` executable on the remote machine."};
|
||||
|
||||
const Setting<int> maxConnections{this, 1, "max-connections",
|
||||
"Maximum number of concurrent SSH connections."};
|
||||
|
||||
const std::string name() override { return "SSH Store"; }
|
||||
|
||||
std::string doc() override
|
||||
@@ -95,25 +97,25 @@ struct LegacySSHStoreConfig : CommonSSHStoreConfig
|
||||
}
|
||||
};
|
||||
|
||||
struct LegacySSHStoreConfigWithLog : LegacySSHStoreConfig
|
||||
{
|
||||
using LegacySSHStoreConfig::LegacySSHStoreConfig;
|
||||
|
||||
// Hack for getting remote build log output.
|
||||
// Intentionally not in `LegacySSHStoreConfig` so that it doesn't appear in
|
||||
// the documentation
|
||||
const Setting<int> logFD{this, -1, "log-fd", "file descriptor to which SSH's stderr is connected"};
|
||||
};
|
||||
|
||||
struct LegacySSHStore final : public Store
|
||||
{
|
||||
LegacySSHStoreConfigWithLog config_;
|
||||
LegacySSHStoreConfig config_;
|
||||
|
||||
LegacySSHStoreConfigWithLog & config() override { return config_; }
|
||||
const LegacySSHStoreConfigWithLog & config() const override { return config_; }
|
||||
LegacySSHStoreConfig & config() override
|
||||
{
|
||||
return config_;
|
||||
}
|
||||
const LegacySSHStoreConfig & config() const override
|
||||
{
|
||||
return config_;
|
||||
}
|
||||
|
||||
struct Connection
|
||||
{
|
||||
// make sure this is destroyed last so the sshConn that feeds it dies first
|
||||
kj::Promise<void> logHandlerPromise{nullptr};
|
||||
std::list<Activity> act;
|
||||
|
||||
ref<IoBuffer> fromBuf{make_ref<IoBuffer>()};
|
||||
std::unique_ptr<SSH::Connection> sshConn;
|
||||
ServeProto::Version remoteVersion;
|
||||
@@ -136,6 +138,54 @@ struct LegacySSHStore final : public Store
|
||||
};
|
||||
}
|
||||
|
||||
kj::Promise<void> logHandler(std::string storeUri)
|
||||
try {
|
||||
// NOTE this is very similar to handleBuilderOutput in DerivationGoal, but unlike
|
||||
// the derivation goal we do not need to handle EIO from a pty here. we also have
|
||||
// no timeouts or limits to keep track of, which makes deduplication less useful.
|
||||
|
||||
std::map<ActivityId, Activity> activities;
|
||||
|
||||
auto stderrPipe = std::move(sshConn->stderrPipe);
|
||||
auto reader = AIO().lowLevelProvider.wrapInputFd(stderrPipe.get());
|
||||
|
||||
LogLineSplitter splitter;
|
||||
|
||||
auto flushLine = [&](const std::string & line) {
|
||||
if (const auto state = handleJSONLogMessage(line, act.back(), activities, storeUri)) {
|
||||
return *state;
|
||||
} else {
|
||||
return act.back().result(resBuildLogLine, line);
|
||||
}
|
||||
};
|
||||
|
||||
auto buf = kj::heapArray<char>(4096);
|
||||
while (true) {
|
||||
const auto got = co_await reader->tryRead(buf.begin(), 1, buf.size());
|
||||
if (got == 0) {
|
||||
break;
|
||||
}
|
||||
|
||||
std::string_view data{buf.begin(), got};
|
||||
while (!data.empty()) {
|
||||
if (auto line = splitter.feed(data)) {
|
||||
if (flushLine(*line) == Logger::BufferState::NeedsFlush) {
|
||||
TRY_AWAIT(act.back().getLogger().flush());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (auto line = splitter.finish(); !line.empty()) {
|
||||
(void) flushLine(line);
|
||||
TRY_AWAIT(act.back().getLogger().flush());
|
||||
}
|
||||
} catch (std::exception & e) { // NOLINT(lix-foreign-exceptions)
|
||||
logException("remote store error", e);
|
||||
} catch (...) {
|
||||
std::terminate();
|
||||
}
|
||||
|
||||
template<typename Arg>
|
||||
kj::Promise<Result<void>>
|
||||
sendArg(AsyncOutputStream & stream, StringSink & buffer, Arg && arg)
|
||||
@@ -204,72 +254,70 @@ struct LegacySSHStore final : public Store
|
||||
|
||||
std::string host;
|
||||
|
||||
ref<Pool<Connection>> connections;
|
||||
Sync<Connection, AsyncMutex> connection;
|
||||
|
||||
SSH ssh;
|
||||
|
||||
static std::set<std::string> uriSchemes() { return {"ssh"}; }
|
||||
|
||||
LegacySSHStore(
|
||||
const std::string & scheme, const std::string & host, LegacySSHStoreConfigWithLog config
|
||||
)
|
||||
LegacySSHStore(const std::string & scheme, const std::string & host, LegacySSHStoreConfig config)
|
||||
: Store(config)
|
||||
, config_(std::move(config))
|
||||
, host(host)
|
||||
, connections(make_ref<Pool<Connection>>(
|
||||
std::max(1, (int) config_.maxConnections),
|
||||
[this]() { return openConnection(); },
|
||||
[](const ref<Connection> & r) { return r->good; }
|
||||
))
|
||||
, ssh(
|
||||
host,
|
||||
config_.port,
|
||||
config_.sshKey,
|
||||
config_.sshPublicHostKey,
|
||||
config_.compress,
|
||||
config_.logFD)
|
||||
, ssh(host, config_.port, config_.sshKey, config_.sshPublicHostKey, config_.compress)
|
||||
{
|
||||
}
|
||||
|
||||
kj::Promise<Result<ref<Connection>>> openConnection()
|
||||
kj::Promise<Result<decltype(connection)::Lock>> getConnection()
|
||||
try {
|
||||
auto conn = make_ref<Connection>();
|
||||
conn->sshConn = ssh.startCommand(
|
||||
fmt("%s --serve --write", config_.remoteProgram)
|
||||
+ (config_.remoteStore.get() == ""
|
||||
? ""
|
||||
: " --store " + shellEscape(config_.remoteStore.get()))
|
||||
);
|
||||
FdSink to(conn->sshConn->socket.get());
|
||||
FdSource from(conn->sshConn->socket.get(), conn->fromBuf);
|
||||
conn->store = this;
|
||||
|
||||
try {
|
||||
to << SERVE_MAGIC_1 << SERVE_PROTOCOL_VERSION;
|
||||
to.flush();
|
||||
|
||||
uint64_t magic = readNum<uint64_t>(from);
|
||||
if (magic != SERVE_MAGIC_2)
|
||||
throw Error("'nix-store --serve' protocol mismatch from '%s'", host);
|
||||
conn->remoteVersion = readNum<unsigned>(from);
|
||||
if (GET_PROTOCOL_MAJOR(conn->remoteVersion) != 0x200)
|
||||
throw Error("unsupported 'nix-store --serve' protocol version on '%s'", host);
|
||||
|
||||
/* No longer support protocols this old*/
|
||||
if (GET_PROTOCOL_MINOR(conn->remoteVersion) < 4) {
|
||||
throw Error(
|
||||
"remote '%s' is too old (protocol version %x)", host, conn->remoteVersion
|
||||
);
|
||||
}
|
||||
|
||||
} catch (EndOfFile & e) {
|
||||
throw Error("cannot connect to '%1%'", host);
|
||||
auto conn = co_await connection.lock();
|
||||
if (conn->good && conn->sshConn) {
|
||||
co_return conn;
|
||||
}
|
||||
|
||||
return {conn};
|
||||
*conn = {};
|
||||
conn->sshConn = ssh.startCommand(
|
||||
fmt("%s --serve --write", config_.remoteProgram)
|
||||
+ (config_.remoteStore.get() == "" ? "" : " --store " + shellEscape(config_.remoteStore.get()))
|
||||
);
|
||||
|
||||
try {
|
||||
FdSink to(conn->sshConn->socket.get());
|
||||
FdSource from(conn->sshConn->socket.get(), conn->fromBuf);
|
||||
conn->store = this;
|
||||
|
||||
try {
|
||||
to << SERVE_MAGIC_1 << SERVE_PROTOCOL_VERSION;
|
||||
to.flush();
|
||||
|
||||
uint64_t magic = readNum<uint64_t>(from);
|
||||
if (magic != SERVE_MAGIC_2) {
|
||||
throw Error("'nix-store --serve' protocol mismatch from '%s'", host);
|
||||
}
|
||||
conn->remoteVersion = readNum<unsigned>(from);
|
||||
if (GET_PROTOCOL_MAJOR(conn->remoteVersion) != 0x200) {
|
||||
throw Error("unsupported 'nix-store --serve' protocol version on '%s'", host);
|
||||
}
|
||||
|
||||
/* No longer support protocols this old*/
|
||||
if (GET_PROTOCOL_MINOR(conn->remoteVersion) < 4) {
|
||||
throw Error("remote '%s' is too old (protocol version %x)", host, conn->remoteVersion);
|
||||
}
|
||||
|
||||
} catch (EndOfFile & e) {
|
||||
throw Error("cannot connect to '%1%'", host);
|
||||
}
|
||||
|
||||
conn->act.emplace_back(logger->startActivity(lvlDebug, actUnknown, "remote store " + getUri()));
|
||||
conn->logHandlerPromise = conn->logHandler(host);
|
||||
co_return conn;
|
||||
} catch (Error & e) {
|
||||
std::string msg = chomp(drainFD(conn->sshConn->stderrPipe.get(), false));
|
||||
throw Error("cannot connect to %s: %s (%s)", getUri(), e.msg(), msg);
|
||||
}
|
||||
} catch (...) {
|
||||
return {result::current_exception()};
|
||||
};
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
std::string getUri() override
|
||||
{
|
||||
@@ -279,7 +327,7 @@ struct LegacySSHStore final : public Store
|
||||
kj::Promise<Result<std::shared_ptr<const ValidPathInfo>>>
|
||||
queryPathInfoUncached(const StorePath & path, const Activity * context) override
|
||||
try {
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
|
||||
debug("querying remote host '%s' for info on '%s'", host, printStorePath(path));
|
||||
|
||||
@@ -307,7 +355,7 @@ struct LegacySSHStore final : public Store
|
||||
try {
|
||||
debug("adding path '%s' to remote host '%s'", printStorePath(info.path), host);
|
||||
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
unsigned result;
|
||||
|
||||
if (GET_PROTOCOL_MINOR(conn->remoteVersion) >= 5) {
|
||||
@@ -351,16 +399,16 @@ struct LegacySSHStore final : public Store
|
||||
kj::Promise<Result<box_ptr<AsyncInputStream>>>
|
||||
narFromPath(const StorePath & path, const Activity * context) override
|
||||
try {
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
|
||||
struct NarStream : AsyncInputStream
|
||||
{
|
||||
Pool<Connection>::Handle conn;
|
||||
Sync<Connection, AsyncMutex>::Lock conn;
|
||||
AsyncFdIoStream stream{AsyncFdIoStream::shared_fd{}, conn->sshConn->socket.get()};
|
||||
AsyncBufferedInputStream buffered{stream, conn->fromBuf};
|
||||
box_ptr<AsyncInputStream> copier{copyNAR(buffered)};
|
||||
|
||||
NarStream(Pool<Connection>::Handle conn) : conn(std::move(conn)) {}
|
||||
NarStream(Sync<Connection, AsyncMutex>::Lock conn) : conn(std::move(conn)) {}
|
||||
|
||||
kj::Promise<Result<std::optional<size_t>>> read(void * buffer, size_t size) override
|
||||
{
|
||||
@@ -424,7 +472,19 @@ public:
|
||||
const StorePath & drvPath, const BasicDerivation & drv, BuildMode buildMode
|
||||
) override
|
||||
try {
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
|
||||
// this is a duplicate of DerivationGoal::buildDescription because ugh.
|
||||
// getting that information into here where needed is nigh *impossible*
|
||||
auto description = fmt(buildMode == bmRepair ? "repairing outputs of '%s'"
|
||||
: buildMode == bmCheck ? "checking outputs of '%s'"
|
||||
: "building '%s'",
|
||||
printStorePath(drvPath))
|
||||
+ "on " + getUri();
|
||||
conn->act.emplace_back(logger->startActivity(
|
||||
lvlInfo, actBuild, description, Logger::Fields{printStorePath(drvPath), getUri(), 1, 1}
|
||||
));
|
||||
KJ_DEFER(conn->act.pop_back());
|
||||
|
||||
co_return TRY_AWAIT(conn->sendCommand<BuildResult>(
|
||||
ServeProto::Command::BuildDerivation,
|
||||
@@ -445,7 +505,7 @@ public:
|
||||
if (evalStore && evalStore.get() != this)
|
||||
throw Error("building on an SSH store is incompatible with '--eval-store'");
|
||||
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
|
||||
Strings ss;
|
||||
for (auto & p : drvPaths) {
|
||||
@@ -504,7 +564,7 @@ public:
|
||||
co_return result::success();
|
||||
}
|
||||
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
|
||||
out.merge(TRY_AWAIT(conn->sendCommand<StorePathSet>(
|
||||
ServeProto::Command::QueryClosure, includeOutputs, ServeProto::write(*conn, paths)
|
||||
@@ -518,7 +578,7 @@ public:
|
||||
kj::Promise<Result<StorePathSet>> queryValidPaths(const StorePathSet & paths,
|
||||
SubstituteFlag maybeSubstitute = NoSubstitute) override
|
||||
try {
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
|
||||
co_return TRY_AWAIT(conn->sendCommand<StorePathSet>(
|
||||
ServeProto::Command::QueryValidPaths,
|
||||
@@ -530,9 +590,9 @@ public:
|
||||
co_return result::current_exception();
|
||||
}
|
||||
|
||||
kj::Promise<Result<void>> connect() override
|
||||
kj::Promise<Result<void>> init() override
|
||||
try {
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
co_return result::success();
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
@@ -540,7 +600,7 @@ public:
|
||||
|
||||
kj::Promise<Result<unsigned int>> getProtocol() override
|
||||
try {
|
||||
auto conn(TRY_AWAIT(connections->get()));
|
||||
auto conn(TRY_AWAIT(getConnection()));
|
||||
co_return conn->remoteVersion;
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
|
||||
@@ -34,19 +34,11 @@ bool Machine::mandatoryMet(const std::set<std::string> & features) const
|
||||
});
|
||||
}
|
||||
|
||||
kj::Promise<Result<std::pair<ref<Store>, Pipe>>> Machine::openStore() const
|
||||
kj::Promise<Result<ref<Store>>> Machine::openStore() const
|
||||
try {
|
||||
Pipe pipe;
|
||||
|
||||
StoreConfig::Params storeParams;
|
||||
if (storeUri.starts_with("ssh://")) {
|
||||
// Remote builds become flakey, when having more than one ssh connection
|
||||
storeParams["max-connections"] = "1";
|
||||
}
|
||||
|
||||
if (storeUri.starts_with("ssh://") || storeUri.starts_with("ssh-ng://")) {
|
||||
pipe.create();
|
||||
storeParams["log-fd"] = std::to_string(pipe.writeSide.get());
|
||||
if (sshKey != "")
|
||||
storeParams["ssh-key"] = sshKey;
|
||||
if (sshPublicHostKey != "")
|
||||
@@ -65,7 +57,7 @@ try {
|
||||
append(mandatoryFeatures);
|
||||
}
|
||||
|
||||
co_return {TRY_AWAIT(nix::openStore(storeUri, storeParams)), std::move(pipe)};
|
||||
co_return TRY_AWAIT(nix::openStore(storeUri, storeParams));
|
||||
} catch (...) {
|
||||
co_return result::current_exception();
|
||||
}
|
||||
@@ -167,6 +159,7 @@ static Machine parseBuilderLine(const std::string & line)
|
||||
}
|
||||
|
||||
return {
|
||||
storeUri,
|
||||
storeUri,
|
||||
systemTypes,
|
||||
sshKey,
|
||||
@@ -262,7 +255,8 @@ static toml::result<float, std::string> getSpeedFactor(const toml::value & data)
|
||||
return toml::success(1.0f);
|
||||
}
|
||||
|
||||
static toml::result<Machine, std::vector<std::string>> parseMachine(const toml::value & data)
|
||||
static toml::result<Machine, std::vector<std::string>>
|
||||
parseMachine(const std::string name, const toml::value & data)
|
||||
{
|
||||
std::vector<std::string> errs;
|
||||
|
||||
@@ -339,6 +333,7 @@ static toml::result<Machine, std::vector<std::string>> parseMachine(const toml::
|
||||
return toml::failure(errs);
|
||||
}
|
||||
return toml::success<Machine>({
|
||||
name,
|
||||
storeUri.unwrap(),
|
||||
std::set(systemTypes.unwrap().begin(), systemTypes.unwrap().end()),
|
||||
sshKey.unwrap(),
|
||||
@@ -404,7 +399,7 @@ static toml::result<Machines, std::vector<std::string>> parseToml(const toml::va
|
||||
}
|
||||
|
||||
for (const auto & [name, machine] : data.at(array_name).as_table()) {
|
||||
auto const res = parseMachine(machine);
|
||||
auto const res = parseMachine(name, machine);
|
||||
if (res.is_err()) {
|
||||
auto err = res.as_err();
|
||||
parserErrors.push_back(fmt("for machine %s:", name));
|
||||
|
||||
@@ -14,6 +14,7 @@ class Store;
|
||||
|
||||
struct Machine {
|
||||
|
||||
const std::string name;
|
||||
const std::string storeUri;
|
||||
const std::set<std::string> systemTypes;
|
||||
const std::string sshKey;
|
||||
@@ -41,7 +42,7 @@ struct Machine {
|
||||
*/
|
||||
bool mandatoryMet(const std::set<std::string> & features) const;
|
||||
|
||||
kj::Promise<Result<std::pair<ref<Store>, Pipe>>> openStore() const;
|
||||
kj::Promise<Result<ref<Store>>> openStore() const;
|
||||
};
|
||||
|
||||
typedef std::vector<Machine> Machines;
|
||||
|
||||
+31
-20
@@ -11,28 +11,33 @@ endforeach
|
||||
|
||||
libstore_rpc = []
|
||||
|
||||
libstore_rpc += custom_target(
|
||||
command : [
|
||||
capnpc_wrapper,
|
||||
'--language=c++',
|
||||
'--src-prefix=@CURRENT_SOURCE_DIR@',
|
||||
'--outdir=@OUTDIR@',
|
||||
'--depfile=@DEPFILE@',
|
||||
'-I@SOURCE_ROOT@',
|
||||
'@INPUT@',
|
||||
],
|
||||
input : files(
|
||||
# keep-sorted start
|
||||
'types.capnp',
|
||||
# keep-sorted end
|
||||
),
|
||||
output : [
|
||||
'@PLAINNAME@.h',
|
||||
'@PLAINNAME@.c++',
|
||||
],
|
||||
depfile : '@PLAINNAME@.d',
|
||||
libstore_rpc_files = files(
|
||||
# keep-sorted start
|
||||
'daemon.capnp',
|
||||
'types.capnp',
|
||||
# keep-sorted end
|
||||
)
|
||||
|
||||
foreach rpc_file : libstore_rpc_files
|
||||
libstore_rpc += custom_target(
|
||||
command : [
|
||||
capnpc_wrapper,
|
||||
'--language=c++',
|
||||
'--src-prefix=@CURRENT_SOURCE_DIR@',
|
||||
'--outdir=@OUTDIR@',
|
||||
'--depfile=@DEPFILE@',
|
||||
'-I@SOURCE_ROOT@',
|
||||
'@INPUT@',
|
||||
],
|
||||
input : rpc_file,
|
||||
output : [
|
||||
'@PLAINNAME@.h',
|
||||
'@PLAINNAME@.c++',
|
||||
],
|
||||
depfile : '@PLAINNAME@.d',
|
||||
)
|
||||
endforeach
|
||||
|
||||
# rpc definitions only here to get relative paths of generated files right
|
||||
subdir('build')
|
||||
|
||||
@@ -189,6 +194,8 @@ liblix_sources += files(
|
||||
'common-protocol.cc',
|
||||
'content-address.cc',
|
||||
'crypto.cc',
|
||||
'curlfiletransfer.cc',
|
||||
'curlmulti.cc',
|
||||
'daemon.cc',
|
||||
'derivations.cc',
|
||||
'derived-path.cc',
|
||||
@@ -232,6 +239,7 @@ liblix_sources += files(
|
||||
'ssh.cc',
|
||||
'store-api.cc',
|
||||
'temporary-dir.cc',
|
||||
'transferitem.cc',
|
||||
'uds-remote-store.cc',
|
||||
'worker-protocol.cc',
|
||||
# keep-sorted end
|
||||
@@ -255,6 +263,8 @@ libstore_headers = files(
|
||||
'common-protocol.hh',
|
||||
'content-address.hh',
|
||||
'crypto.hh',
|
||||
'curlfiletransfer.hh',
|
||||
'curlmulti.hh',
|
||||
'daemon.hh',
|
||||
'derivations.hh',
|
||||
'derived-path.hh',
|
||||
@@ -302,6 +312,7 @@ libstore_headers = files(
|
||||
'store-api.hh',
|
||||
'store-cast.hh',
|
||||
'temporary-dir.hh',
|
||||
'transferitem.hh',
|
||||
'types-rpc.hh',
|
||||
'uds-remote-store.hh',
|
||||
'worker-protocol-impl.hh',
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user