diff --git a/Cargo.lock b/Cargo.lock index 9595a3d..f2b643c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,9 +4,9 @@ version = 4 [[package]] name = "aho-corasick" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" dependencies = [ "memchr", ] @@ -18,10 +18,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] -name = "anstream" -version = "0.6.20" +name = "android_system_properties" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ae563653d1938f79b1ab1b5e668c87c76a9930414574a6583a7b7e11a8e6192" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anstream" +version = "0.6.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" dependencies = [ "anstyle", "anstyle-parse", @@ -34,9 +43,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.11" +version = "1.0.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" +checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" [[package]] name = "anstyle-parse" @@ -49,22 +58,22 @@ dependencies = [ [[package]] name = "anstyle-query" -version = "1.1.4" +version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e231f6134f61b71076a3eab506c379d4f36122f2af15a9ff04415ea4c3339e2" +checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] name = "anstyle-wincon" -version = "3.0.10" +version = "3.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e0633414522a32ffaac8ac6cc8f748e090c5717661fddeea04219e2344f5f2a" +checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -81,9 +90,15 @@ checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" [[package]] name = "bitflags" -version = "2.9.4" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2261d10cca569e4643e526d8dc2e62e433cc8aba21ab764233731f8d369bf394" +checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" + +[[package]] +name = "bumpalo" +version = "3.19.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" [[package]] name = "cassowary" @@ -101,16 +116,39 @@ dependencies = [ ] [[package]] -name = "cfg-if" -version = "1.0.3" +name = "cc" +version = "1.2.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fd1289c04a9ea8cb22300a459a72a385d7c73d3259e2ed7dcb2af674838cfa9" +checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +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 = "chrono" +version = "0.4.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fac4744fb15ae8337dc853fee7fb3f4e48c0fbaa23d0afe49c447b4fab126118" +dependencies = [ + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] [[package]] name = "clap" -version = "4.5.48" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2134bb3ea021b78629caa971416385309e0131b351b25e01dc16fb54e1b5fae" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -118,9 +156,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.48" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2ba64afa3c0a6df7fa517765e31314e983f51dda798ffba27b988194fb65dc9" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -130,9 +168,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.47" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfd7eae0b0f1a6e63d4b13c9c478de77c2eb546fba158ad50b4203dc24b9f9c" +checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" dependencies = [ "heck", "proc-macro2", @@ -142,9 +180,9 @@ dependencies = [ [[package]] name = "clap_lex" -version = "0.7.5" +version = "0.7.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" +checksum = "c3e64b0cc0439b12df2fa678eae89a1c56a529fd067a9115f7827f1fffd22b32" [[package]] name = "cognos" @@ -177,13 +215,19 @@ dependencies = [ [[package]] name = "convert_case" -version = "0.7.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +checksum = "633458d4ef8c78b72454de2d54fd6ab2e60f9e02be22f3c6104cdc8a4e0fceb9" dependencies = [ "unicode-segmentation", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + [[package]] name = "crossterm" version = "0.28.1" @@ -212,7 +256,7 @@ dependencies = [ "document-features", "mio", "parking_lot", - "rustix 1.0.8", + "rustix 1.1.3", "signal-hook", "signal-hook-mio", "winapi", @@ -229,30 +273,30 @@ dependencies = [ [[package]] name = "csv" -version = "1.3.1" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acdc4883a9c96732e4733212c01447ebd805833b7275a73ca3ee080fd77afdaf" +checksum = "52cd9d68cf7efc6ddfaaee42e7288d3a99d613d4b50f76ce9827ae0c6e14f938" dependencies = [ "csv-core", "itoa", "ryu", - "serde", + "serde_core", ] [[package]] name = "csv-core" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d02f3b0da4c6504f86e9cd789d8dbafab48c2321be74e9987593de5a894d93d" +checksum = "704a3c26996a80471189265814dbc2c257598b96b8a7feae2d31ace646bb9782" dependencies = [ "memchr", ] [[package]] name = "darling" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee" +checksum = "25ae13da2f202d56bd7f91c25fba009e7717a1e4a1cc98a76d844b65ae912e9d" dependencies = [ "darling_core", "darling_macro", @@ -260,11 +304,10 @@ dependencies = [ [[package]] name = "darling_core" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e" +checksum = "9865a50f7c335f53564bb694ef660825eb8610e0a53d3e11bf1b0d3df31e03b0" dependencies = [ - "fnv", "ident_case", "proc-macro2", "quote", @@ -274,9 +317,9 @@ dependencies = [ [[package]] name = "darling_macro" -version = "0.20.11" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead" +checksum = "ac3984ec7bd6cfa798e62b4a642426a5be0e68f9401cfc2a01e3fa9ea2fcdb8d" dependencies = [ "darling_core", "quote", @@ -285,30 +328,31 @@ dependencies = [ [[package]] name = "derive_more" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +checksum = "d751e9e49156b02b44f9c1815bcb94b984cdcc4396ecc32521c739452808b134" dependencies = [ "derive_more-impl", ] [[package]] name = "derive_more-impl" -version = "2.0.1" +version = "2.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +checksum = "799a97264921d8623a957f6c3b9011f3b5492f557bbb7a5a19b7fa6d06ba8dcb" dependencies = [ "convert_case", "proc-macro2", "quote", + "rustc_version", "syn", ] [[package]] name = "document-features" -version = "0.2.11" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61" dependencies = [ "litrs", ] @@ -327,12 +371,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" [[package]] name = "errno" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.61.2", ] [[package]] @@ -352,10 +396,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" [[package]] -name = "fnv" -version = "1.0.7" +name = "find-msvc-tools" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "foldhash" @@ -365,14 +409,14 @@ checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" [[package]] name = "getrandom" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" dependencies = [ "cfg-if", "libc", "r-efi", - "wasi 0.14.3+wasi-0.2.4", + "wasip2", ] [[package]] @@ -386,12 +430,42 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + [[package]] name = "heck" version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -406,27 +480,30 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.11.4" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.16.1", "serde", "serde_core", ] [[package]] name = "indoc" -version = "2.0.6" +version = "2.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] [[package]] name = "instability" -version = "0.3.9" +version = "0.3.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "435d80800b936787d62688c927b6490e887c7ef5ff9ce922c6c6050fca75eb9a" +checksum = "357b7205c6cd18dd2c86ed312d1e70add149aea98e7ef72b9fdf0270e555c11d" dependencies = [ "darling", "indoc", @@ -437,9 +514,9 @@ dependencies = [ [[package]] name = "is_terminal_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7943c866cc5cd64cbc25b2e01621d07fa8eb2a1a23160ee81ce38704e97b8ecf" +checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itertools" @@ -452,9 +529,19 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.15" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "js-sys" +version = "0.3.85" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +dependencies = [ + "once_cell", + "wasm-bindgen", +] [[package]] name = "lazy_static" @@ -464,9 +551,9 @@ checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] name = "libc" -version = "0.2.176" +version = "0.2.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f929b4d672ea937a23a1ab494143d968337a5f47e56d0815df1e0890ddf174" +checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" [[package]] name = "linux-raw-sys" @@ -476,31 +563,30 @@ checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" [[package]] name = "linux-raw-sys" -version = "0.9.4" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" +checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" [[package]] name = "litrs" -version = "0.4.2" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5e54036fe321fd421e10d732f155734c4e4afd610dd556d9a82833ab3ee0bed" +checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092" [[package]] name = "lock_api" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965" dependencies = [ - "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.28" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34080505efa8e45a4b816c349525ebe327ceaa8559756f0356cba97ef3bf7432" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -508,7 +594,7 @@ version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" dependencies = [ - "hashbrown", + "hashbrown 0.15.5", ] [[package]] @@ -522,29 +608,38 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.5" +version = "2.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" [[package]] name = "mio" -version = "1.0.4" +version = "1.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" dependencies = [ "libc", "log", - "wasi 0.11.1+wasi-snapshot-preview1", - "windows-sys 0.59.0", + "wasi", + "windows-sys 0.61.2", ] [[package]] name = "nu-ansi-term" -version = "0.50.1" +version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a28e057d01f97e61255210fcff094d74ed0466038633e95017f5beb68e4399" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", ] [[package]] @@ -555,15 +650,15 @@ checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" [[package]] name = "once_cell_polyfill" -version = "1.70.1" +version = "1.70.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad" +checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe" [[package]] name = "parking_lot" -version = "0.12.4" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ "lock_api", "parking_lot_core", @@ -571,15 +666,15 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.11" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-targets 0.52.6", + "windows-link", ] [[package]] @@ -596,18 +691,18 @@ checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" [[package]] name = "proc-macro2" -version = "1.0.101" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ae43fd86e4158d6db51ad8e2b80f313af9cc74f5c0e03ccb87de09998732de" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" dependencies = [ "unicode-ident", ] [[package]] name = "quote" -version = "1.0.40" +version = "1.0.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" dependencies = [ "proc-macro2", ] @@ -641,18 +736,18 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.5.17" +version = "0.5.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d" dependencies = [ "bitflags", ] [[package]] name = "regex-automata" -version = "0.4.10" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9458fa0bfeeac22b5ca447c63aaf45f28439a709ccd244698632f9aa6394d6" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -661,15 +756,16 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.6" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "caf4aa5b0f434c91fe5c7f1ecb6a5ece2130b02ad2a590589dda5146df959001" +checksum = "a96887878f22d7bad8a3b6dc5b7440e0ada9a245242924394987b21cf2210a4c" [[package]] name = "rom" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "clap", "cognos", "crossterm 0.29.0", @@ -685,6 +781,15 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustix" version = "0.38.44" @@ -700,15 +805,15 @@ dependencies = [ [[package]] name = "rustix" -version = "1.0.8" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" dependencies = [ "bitflags", "errno", "libc", - "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "linux-raw-sys 0.11.0", + "windows-sys 0.61.2", ] [[package]] @@ -719,9 +824,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.20" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" [[package]] name = "scopeguard" @@ -729,6 +834,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "semver" +version = "1.0.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2" + [[package]] name = "serde" version = "1.0.228" @@ -761,15 +872,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.145" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "402a6f66d8c709116cf22f558eab210f5a50187f702eb4d7e5ef38d9a7f1c79c" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", "serde_core", + "zmij", ] [[package]] @@ -792,6 +903,12 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + [[package]] name = "signal-hook" version = "0.3.18" @@ -804,9 +921,9 @@ dependencies = [ [[package]] name = "signal-hook-mio" -version = "0.2.4" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +checksum = "b75a19a7a740b25bc7944bdee6172368f988763b744e3d4dfe753f6b4ece40cc" dependencies = [ "libc", "mio", @@ -815,10 +932,11 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.6" +version = "1.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b" dependencies = [ + "errno", "libc", ] @@ -864,9 +982,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.106" +version = "2.0.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ede7c438028d4436d71104916910f5bb611972c5cfd7f89b8300a8186e6fada6" +checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" dependencies = [ "proc-macro2", "quote", @@ -875,31 +993,31 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.23.0" +version = "3.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16" +checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" dependencies = [ "fastrand", "getrandom", "once_cell", - "rustix 1.0.8", - "windows-sys 0.60.2", + "rustix 1.1.3", + "windows-sys 0.61.2", ] [[package]] name = "thiserror" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "2.0.17" +version = "2.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2", "quote", @@ -917,9 +1035,9 @@ dependencies = [ [[package]] name = "tracing" -version = "0.1.41" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" dependencies = [ "pin-project-lite", "tracing-attributes", @@ -928,9 +1046,9 @@ dependencies = [ [[package]] name = "tracing-attributes" -version = "0.1.30" +version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" dependencies = [ "proc-macro2", "quote", @@ -939,9 +1057,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.34" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" dependencies = [ "once_cell", "valuable", @@ -960,9 +1078,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.20" +version = "0.3.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2054a14f5307d601f88daf0553e1cbf472acc4f2c51afab632431cdcd72124d5" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" dependencies = [ "matchers", "nu-ansi-term", @@ -978,9 +1096,9 @@ dependencies = [ [[package]] name = "unicode-ident" -version = "1.0.18" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" [[package]] name = "unicode-segmentation" @@ -1030,14 +1148,59 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" [[package]] -name = "wasi" -version = "0.14.3+wasi-0.2.4" +name = "wasip2" +version = "1.0.2+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a51ae83037bdd272a9e28ce236db8c07016dd0d50c27038b3f407533c030c95" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" dependencies = [ "wit-bindgen", ] +[[package]] +name = "wasm-bindgen" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.108" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +dependencies = [ + "unicode-ident", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1061,18 +1224,62 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-link" -version = "0.1.3" +name = "windows-core" +version = "0.62.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] [[package]] -name = "windows-sys" -version = "0.52.0" +name = "windows-implement" +version = "0.60.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ - "windows-targets 0.52.6", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", ] [[package]] @@ -1081,16 +1288,16 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets 0.52.6", + "windows-targets", ] [[package]] name = "windows-sys" -version = "0.60.2" +version = "0.61.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" dependencies = [ - "windows-targets 0.53.3", + "windows-link", ] [[package]] @@ -1099,31 +1306,14 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.0", - "windows_aarch64_msvc 0.53.0", - "windows_i686_gnu 0.53.0", - "windows_i686_gnullvm 0.53.0", - "windows_i686_msvc 0.53.0", - "windows_x86_64_gnu 0.53.0", - "windows_x86_64_gnullvm 0.53.0", - "windows_x86_64_msvc 0.53.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_gnullvm", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -1132,84 +1322,42 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" - [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" -[[package]] -name = "windows_aarch64_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" - [[package]] name = "windows_i686_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" -[[package]] -name = "windows_i686_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" - [[package]] name = "windows_i686_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" -[[package]] -name = "windows_i686_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" - [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" -[[package]] -name = "windows_i686_msvc" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" - [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" -[[package]] -name = "windows_x86_64_gnu" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" - [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.53.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" - [[package]] name = "windows_x86_64_msvc" version = "0.52.6" @@ -1217,13 +1365,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" [[package]] -name = "windows_x86_64_msvc" -version = "0.53.0" +name = "wit-bindgen" +version = "0.51.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" [[package]] -name = "wit-bindgen" -version = "0.45.0" +name = "zmij" +version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "052283831dbae3d879dc7f51f3d92703a316ca49f91540417d38591826127814" +checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445" diff --git a/Cargo.toml b/Cargo.toml index a749884..5456e9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,29 +1,25 @@ [workspace] -members = [ "cognos", "rom" ] +members = ["cognos", "rom"] resolver = "3" [workspace.package] -name = "rom" version = "0.1.0" edition = "2024" authors = ["NotAShelf "] description = "Pretty build graphs for Nix builds" -license = "MPL-2.0" -repository = "https://github.com/notashelf/rom" -homepage = "https://github.com/notashelf/rom" -rust-version = "1.85" -readme = true +rust-version = "1.91.1" [workspace.dependencies] anyhow = "1.0.100" -clap = { version = "4.5.48", features = ["derive"] } +clap = { version = "4.5.51", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.145" serde_repr = "0.1.20" crossterm = "0.29.0" ratatui = "0.29.0" -indexmap = { version = "2.11.4", features = ["serde"] } -csv = "1.3.1" +indexmap = { version = "2.12.0", features = ["serde"] } +csv = "1.4.0" +chrono = "0.4.42" thiserror = "2.0.17" tracing = "0.1.41" tracing-subscriber = { version = "0.3.20", features = ["env-filter"] } diff --git a/LICENSE b/LICENSE index baa49b9..de0a651 100644 --- a/LICENSE +++ b/LICENSE @@ -1,328 +1,288 @@ -Mozilla Public License, version 2.0 + EUROPEAN UNION PUBLIC LICENCE v. 1.2 + EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined +below) which is provided under the terms of this Licence. Any use of the Work, +other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). + +The Work is provided under the terms of this Licence when the Licensor (as +defined below) has placed the following notice immediately following the +copyright notice for the Work: + + Licensed under the EUPL + +or has expressed by any other means his willingness to license under the EUPL. 1. Definitions - 1.1. “Contributor” - means each individual or legal entity that creates, contributes to the - creation of, or owns Covered Software. +In this Licence, the following terms have the following meaning: - 1.2. “Contributor Version” - means the combination of the Contributions of others (if any) used by a - Contributor and that particular Contributor’s Contribution. +- ‘The Licence’: this Licence. - 1.3. “Contribution” - means Covered Software of a particular Contributor. +- ‘The Original Work’: the work or software distributed or communicated by the + Licensor under this Licence, available as Source Code and also as Executable + Code as the case may be. - 1.4. “Covered Software” - means Source Code Form to which the initial Contributor has attached the - notice in Exhibit A, the Executable Form of such Source Code Form, - and Modifications of such Source Code Form, in each case - including portions thereof. +- ‘Derivative Works’: the works or software that could be created by the + Licensee, based upon the Original Work or modifications thereof. This Licence + does not define the extent of modification or dependence on the Original Work + required in order to classify a work as a Derivative Work; this extent is + determined by copyright law applicable in the country mentioned in Article 15. - 1.5. “Incompatible With Secondary Licenses” - means +- ‘The Work’: the Original Work or its Derivative Works. - a. that the initial Contributor has attached the notice described - in Exhibit B to the Covered Software; or +- ‘The Source Code’: the human-readable form of the Work which is the most + convenient for people to study and modify. - b. that the Covered Software was made available under the terms of - version 1.1 or earlier of the License, but not also under the terms - of a Secondary License. +- ‘The Executable Code’: any code which has generally been compiled and which is + meant to be interpreted by a computer as a program. - 1.6. “Executable Form” - means any form of the work other than Source Code Form. +- ‘The Licensor’: the natural or legal person that distributes or communicates + the Work under the Licence. - 1.7. “Larger Work” - means a work that combines Covered Software with other material, - in a separate file or files, that is not Covered Software. +- ‘Contributor(s)’: any natural or legal person who modifies the Work under the + Licence, or otherwise contributes to the creation of a Derivative Work. - 1.8. “License” - means this document. +- ‘The Licensee’ or ‘You’: any natural or legal person who makes any usage of + the Work under the terms of the Licence. - 1.9. “Licensable” - means having the right to grant, to the maximum extent possible, - whether at the time of the initial grant or subsequently, - any and all of the rights conveyed by this License. +- ‘Distribution’ or ‘Communication’: any act of selling, giving, lending, + renting, distributing, communicating, transmitting, or otherwise making + available, online or offline, copies of the Work or providing access to its + essential functionalities at the disposal of any other natural or legal + person. - 1.10. “Modifications” - means any of the following: +2. Scope of the rights granted by the Licence - a. any file in Source Code Form that results from an addition to, - deletion from, or modification of the contents of Covered Software; or +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, +sublicensable licence to do the following, for the duration of copyright vested +in the Original Work: - b. any new file in Source Code Form that contains any Covered Software. +- use the Work in any circumstance and for all usage, +- reproduce the Work, +- modify the Work, and make Derivative Works based upon the Work, +- communicate to the public, including the right to make available or display + the Work or copies thereof to the public and perform publicly, as the case may + be, the Work, +- distribute the Work or copies thereof, +- lend and rent the Work or copies thereof, +- sublicense rights in the Work or copies thereof. - 1.11. “Patent Claims” of a Contributor - means any patent claim(s), including without limitation, method, process, - and apparatus claims, in any patent Licensable by such Contributor that - would be infringed, but for the grant of the License, by the making, - using, selling, offering for sale, having made, import, or transfer of - either its Contributions or its Contributor Version. +Those rights can be exercised on any media, supports and formats, whether now +known or later invented, as far as the applicable law permits so. - 1.12. “Secondary License” - means either the GNU General Public License, Version 2.0, the - GNU Lesser General Public License, Version 2.1, the GNU Affero General - Public License, Version 3.0, or any later versions of those licenses. +In the countries where moral rights apply, the Licensor waives his right to +exercise his moral right to the extent allowed by law in order to make effective +the licence of the economic rights here above listed. - 1.13. “Source Code Form” - means the form of the work preferred for making modifications. +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to +any patents held by the Licensor, to the extent necessary to make use of the +rights granted on the Work under this Licence. - 1.14. “You” (or “Your”) - means an individual or a legal entity exercising rights under this License. - For legal entities, “You” includes any entity that controls, - is controlled by, or is under common control with You. For purposes of - this definition, “control” means (a) the power, direct or indirect, - to cause the direction or management of such entity, whether by contract - or otherwise, or (b) ownership of more than fifty percent (50%) of the - outstanding shares or beneficial ownership of such entity. +3. Communication of the Source Code -2. License Grants and Conditions +The Licensor may provide the Work either in its Source Code form, or as +Executable Code. If the Work is provided as Executable Code, the Licensor +provides in addition a machine-readable copy of the Source Code of the Work +along with each copy of the Work that the Licensor distributes or indicates, in +a notice following the copyright notice attached to the Work, a repository where +the Source Code is easily and freely accessible for as long as the Licensor +continues to distribute or communicate the Work. - 2.1. Grants - Each Contributor hereby grants You a world-wide, royalty-free, - non-exclusive license: +4. Limitations on copyright - a. under intellectual property rights (other than patent or trademark) - Licensable by such Contributor to use, reproduce, make available, - modify, display, perform, distribute, and otherwise exploit its - Contributions, either on an unmodified basis, with Modifications, - or as part of a Larger Work; and +Nothing in this Licence is intended to deprive the Licensee of the benefits from +any exception or limitation to the exclusive rights of the rights owners in the +Work, of the exhaustion of those rights or of other applicable limitations +thereto. - b. under Patent Claims of such Contributor to make, use, sell, - offer for sale, have made, import, and otherwise transfer either - its Contributions or its Contributor Version. +5. Obligations of the Licensee - 2.2. Effective Date - The licenses granted in Section 2.1 with respect to any Contribution - become effective for each Contribution on the date the Contributor - first distributes such Contribution. +The grant of the rights mentioned above is subject to some restrictions and +obligations imposed on the Licensee. Those obligations are the following: - 2.3. Limitations on Grant Scope - The licenses granted in this Section 2 are the only rights granted - under this License. No additional rights or licenses will be implied - from the distribution or licensing of Covered Software under this License. - Notwithstanding Section 2.1(b) above, no patent license is granted - by a Contributor: +Attribution right: The Licensee shall keep intact all copyright, patent or +trademarks notices and all notices that refer to the Licence and to the +disclaimer of warranties. The Licensee must include a copy of such notices and a +copy of the Licence with every copy of the Work he/she distributes or +communicates. The Licensee must cause any Derivative Work to carry prominent +notices stating that the Work has been modified and the date of modification. - a. for any code that a Contributor has removed from - Covered Software; or +Copyleft clause: If the Licensee distributes or communicates copies of the +Original Works or Derivative Works, this Distribution or Communication will be +done under the terms of this Licence or of a later version of this Licence +unless the Original Work is expressly distributed only under this version of the +Licence — for example by communicating ‘EUPL v. 1.2 only’. The Licensee +(becoming Licensor) cannot offer or impose any additional terms or conditions on +the Work or Derivative Work that alter or restrict the terms of the Licence. - b. for infringements caused by: (i) Your and any other third party’s - modifications of Covered Software, or (ii) the combination of its - Contributions with other software (except as part of its - Contributor Version); or +Compatibility clause: If the Licensee Distributes or Communicates Derivative +Works or copies thereof based upon both the Work and another work licensed under +a Compatible Licence, this Distribution or Communication can be done under the +terms of this Compatible Licence. For the sake of this clause, ‘Compatible +Licence’ refers to the licences listed in the appendix attached to this Licence. +Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible +Licence shall prevail. - c. under Patent Claims infringed by Covered Software in the - absence of its Contributions. +Provision of Source Code: When distributing or communicating copies of the Work, +the Licensee will provide a machine-readable copy of the Source Code or indicate +a repository where this Source will be easily and freely available for as long +as the Licensee continues to distribute or communicate the Work. - This License does not grant any rights in the trademarks, service marks, - or logos of any Contributor (except as may be necessary to comply with - the notice requirements in Section 3.4). +Legal Protection: This Licence does not grant permission to use the trade names, +trademarks, service marks, or names of the Licensor, except as required for +reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. - 2.4. Subsequent Licenses - No Contributor makes additional grants as a result of Your choice to - distribute the Covered Software under a subsequent version of this - License (see Section 10.2) or under the terms of a Secondary License - (if permitted under the terms of Section 3.3). +6. Chain of Authorship - 2.5. Representation - Each Contributor represents that the Contributor believes its - Contributions are its original creation(s) or it has sufficient rights - to grant the rights to its Contributions conveyed by this License. +The original Licensor warrants that the copyright in the Original Work granted +hereunder is owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. - 2.6. Fair Use - This License is not intended to limit any rights You have under - applicable copyright doctrines of fair use, fair dealing, - or other equivalents. +Each Contributor warrants that the copyright in the modifications he/she brings +to the Work are owned by him/her or licensed to him/her and that he/she has the +power and authority to grant the Licence. - 2.7. Conditions - Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the - licenses granted in Section 2.1. +Each time You accept the Licence, the original Licensor and subsequent +Contributors grant You a licence to their contributions to the Work, under the +terms of this Licence. -3. Responsibilities +7. Disclaimer of Warranty - 3.1. Distribution of Source Form - All distribution of Covered Software in Source Code Form, including - any Modifications that You create or to which You contribute, must be - under the terms of this License. You must inform recipients that the - Source Code Form of the Covered Software is governed by the terms - of this License, and how they can obtain a copy of this License. - You may not attempt to alter or restrict the recipients’ rights - in the Source Code Form. +The Work is a work in progress, which is continuously improved by numerous +Contributors. It is not a finished work and may therefore contain defects or +‘bugs’ inherent to this type of development. - 3.2. Distribution of Executable Form - If You distribute Covered Software in Executable Form then: +For the above reason, the Work is provided under the Licence on an ‘as is’ basis +and without warranties of any kind concerning the Work, including without +limitation merchantability, fitness for a particular purpose, absence of defects +or errors, accuracy, non-infringement of intellectual property rights other than +copyright as stated in Article 6 of this Licence. - a. such Covered Software must also be made available in Source Code - Form, as described in Section 3.1, and You must inform recipients of - the Executable Form how they can obtain a copy of such Source Code - Form by reasonable means in a timely manner, at a charge no more than - the cost of distribution to the recipient; and +This disclaimer of warranty is an essential part of the Licence and a condition +for the grant of any rights to the Work. - b. You may distribute such Executable Form under the terms of this - License, or sublicense it under different terms, provided that the - license for the Executable Form does not attempt to limit or alter - the recipients’ rights in the Source Code Form under this License. +8. Disclaimer of Liability - 3.3. Distribution of a Larger Work - You may create and distribute a Larger Work under terms of Your choice, - provided that You also comply with the requirements of this License for - the Covered Software. If the Larger Work is a combination of - Covered Software with a work governed by one or more Secondary Licenses, - and the Covered Software is not Incompatible With Secondary Licenses, - this License permits You to additionally distribute such Covered Software - under the terms of such Secondary License(s), so that the recipient of - the Larger Work may, at their option, further distribute the - Covered Software under the terms of either this License or such - Secondary License(s). +Except in the cases of wilful misconduct or damages directly caused to natural +persons, the Licensor will in no event be liable for any direct or indirect, +material or moral, damages of any kind, arising out of the Licence or of the use +of the Work, including without limitation, damages for loss of goodwill, work +stoppage, computer failure or malfunction, loss of data or any commercial +damage, even if the Licensor has been advised of the possibility of such damage. +However, the Licensor will be liable under statutory product liability laws as +far such laws apply to the Work. - 3.4. Notices - You may not remove or alter the substance of any license notices - (including copyright notices, patent notices, disclaimers of warranty, - or limitations of liability) contained within the Source Code Form of - the Covered Software, except that You may alter any license notices to - the extent required to remedy known factual inaccuracies. +9. Additional agreements - 3.5. Application of Additional Terms - You may choose to offer, and to charge a fee for, warranty, support, - indemnity or liability obligations to one or more recipients of - Covered Software. However, You may do so only on Your own behalf, - and not on behalf of any Contributor. You must make it absolutely clear - that any such warranty, support, indemnity, or liability obligation is - offered by You alone, and You hereby agree to indemnify every Contributor - for any liability incurred by such Contributor as a result of warranty, - support, indemnity or liability terms You offer. You may include - additional disclaimers of warranty and limitations of liability - specific to any jurisdiction. +While distributing the Work, You may choose to conclude an additional agreement, +defining obligations or services consistent with this Licence. However, if +accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, +and only if You agree to indemnify, defend, and hold each Contributor harmless +for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. -4. Inability to Comply Due to Statute or Regulation +10. Acceptance of the Licence -If it is impossible for You to comply with any of the terms of this License -with respect to some or all of the Covered Software due to statute, -judicial order, or regulation then You must: (a) comply with the terms of -this License to the maximum extent possible; and (b) describe the limitations -and the code they affect. Such description must be placed in a text file -included with all distributions of the Covered Software under this License. -Except to the extent prohibited by statute or regulation, such description -must be sufficiently detailed for a recipient of ordinary skill -to be able to understand it. +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ +placed under the bottom of a window displaying the text of this Licence or by +affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable +acceptance of this Licence and all of its terms and conditions. -5. Termination +Similarly, you irrevocably accept this Licence and all of its terms and +conditions by exercising any rights granted to You by Article 2 of this Licence, +such as the use of the Work, the creation by You of a Derivative Work or the +Distribution or Communication by You of the Work or copies thereof. - 5.1. The rights granted under this License will terminate automatically - if You fail to comply with any of its terms. However, if You become - compliant, then the rights granted under this License from a particular - Contributor are reinstated (a) provisionally, unless and until such - Contributor explicitly and finally terminates Your grants, and (b) on an - ongoing basis, if such Contributor fails to notify You of the - non-compliance by some reasonable means prior to 60 days after You have - come back into compliance. Moreover, Your grants from a particular - Contributor are reinstated on an ongoing basis if such Contributor - notifies You of the non-compliance by some reasonable means, - this is the first time You have received notice of non-compliance with - this License from such Contributor, and You become compliant prior to - 30 days after Your receipt of the notice. +11. Information to the public - 5.2. If You initiate litigation against any entity by asserting a patent - infringement claim (excluding declaratory judgment actions, - counter-claims, and cross-claims) alleging that a Contributor Version - directly or indirectly infringes any patent, then the rights granted - to You by any and all Contributors for the Covered Software under - Section 2.1 of this License shall terminate. +In case of any Distribution or Communication of the Work by means of electronic +communication by You (for example, by offering to download the Work from a +remote location) the distribution channel or media (for example, a website) must +at least provide to the public the information requested by the applicable law +regarding the Licensor, the Licence and the way it may be accessible, concluded, +stored and reproduced by the Licensee. - 5.3. In the event of termination under Sections 5.1 or 5.2 above, all - end user license agreements (excluding distributors and resellers) which - have been validly granted by You or Your distributors under this License - prior to termination shall survive termination. +12. Termination of the Licence -6. Disclaimer of Warranty +The Licence and the rights granted hereunder will terminate automatically upon +any breach by the Licensee of the terms of the Licence. -Covered Software is provided under this License on an “as is” basis, without -warranty of any kind, either expressed, implied, or statutory, including, -without limitation, warranties that the Covered Software is free of defects, -merchantable, fit for a particular purpose or non-infringing. The entire risk -as to the quality and performance of the Covered Software is with You. -Should any Covered Software prove defective in any respect, You -(not any Contributor) assume the cost of any necessary servicing, repair, -or correction. This disclaimer of warranty constitutes an essential part of -this License. No use of any Covered Software is authorized under this -License except under this disclaimer. +Such a termination will not terminate the licences of any person who has +received the Work from the Licensee under the Licence, provided such persons +remain in full compliance with the Licence. -7. Limitation of Liability +13. Miscellaneous -Under no circumstances and under no legal theory, whether tort -(including negligence), contract, or otherwise, shall any Contributor, or -anyone who distributes Covered Software as permitted above, be liable to -You for any direct, indirect, special, incidental, or consequential damages -of any character including, without limitation, damages for lost profits, -loss of goodwill, work stoppage, computer failure or malfunction, or any and -all other commercial damages or losses, even if such party shall have been -informed of the possibility of such damages. This limitation of liability -shall not apply to liability for death or personal injury resulting from -such party’s negligence to the extent applicable law prohibits such -limitation. Some jurisdictions do not allow the exclusion or limitation of -incidental or consequential damages, so this exclusion and limitation may -not apply to You. +Without prejudice of Article 9 above, the Licence represents the complete +agreement between the Parties as to the Work. -8. Litigation +If any provision of the Licence is invalid or unenforceable under applicable +law, this will not affect the validity or enforceability of the Licence as a +whole. Such provision will be construed or reformed so as necessary to make it +valid and enforceable. -Any litigation relating to this License may be brought only in the courts of -a jurisdiction where the defendant maintains its principal place of business -and such litigation shall be governed by laws of that jurisdiction, without -reference to its conflict-of-law provisions. Nothing in this Section shall -prevent a party’s ability to bring cross-claims or counter-claims. +The European Commission may publish other linguistic versions or new versions of +this Licence or updated versions of the Appendix, so far this is required and +reasonable, without reducing the scope of the rights granted by the Licence. New +versions of the Licence will be published with a unique version number. -9. Miscellaneous +All linguistic versions of this Licence, approved by the European Commission, +have identical value. Parties can take advantage of the linguistic version of +their choice. -This License represents the complete agreement concerning the subject matter -hereof. If any provision of this License is held to be unenforceable, -such provision shall be reformed only to the extent necessary to make it -enforceable. Any law or regulation which provides that the language of a -contract shall be construed against the drafter shall not be used to construe -this License against a Contributor. +14. Jurisdiction -10. Versions of the License +Without prejudice to specific agreement between parties, - 10.1. New Versions - Mozilla Foundation is the license steward. Except as provided in - Section 10.3, no one other than the license steward has the right to - modify or publish new versions of this License. Each version will be - given a distinguishing version number. +- any litigation resulting from the interpretation of this License, arising + between the European Union institutions, bodies, offices or agencies, as a + Licensor, and any Licensee, will be subject to the jurisdiction of the Court + of Justice of the European Union, as laid down in article 272 of the Treaty on + the Functioning of the European Union, - 10.2. Effect of New Versions - You may distribute the Covered Software under the terms of the version - of the License under which You originally received the Covered Software, - or under the terms of any subsequent version published - by the license steward. +- any litigation arising between other parties and resulting from the + interpretation of this License, will be subject to the exclusive jurisdiction + of the competent court where the Licensor resides or conducts its primary + business. - 10.3. Modified Versions - If you create software not governed by this License, and you want to - create a new license for such software, you may create and use a modified - version of this License if you rename the license and remove any - references to the name of the license steward (except to note that such - modified license differs from this License). +15. Applicable Law - 10.4. Distributing Source Code Form that is - Incompatible With Secondary Licenses - If You choose to distribute Source Code Form that is - Incompatible With Secondary Licenses under the terms of this version of - the License, the notice described in Exhibit B of this - License must be attached. +Without prejudice to specific agreement between parties, -Exhibit A - Source Code Form License Notice +- this Licence shall be governed by the law of the European Union Member State + where the Licensor has his seat, resides or has his registered office, - This Source Code Form is subject to the terms of the - Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed - with this file, You can obtain one at http://mozilla.org/MPL/2.0/. +- this licence shall be governed by Belgian law if the Licensor has no seat, + residence or registered office inside a European Union Member State. -If it is not possible or desirable to put the notice in a particular file, -then You may include the notice in a location (such as a LICENSE file in a -relevant directory) where a recipient would be likely to -look for such a notice. +Appendix -You may add additional accurate notices of copyright ownership. +‘Compatible Licences’ according to Article 5 EUPL are: -Exhibit B - “Incompatible With Secondary Licenses” Notice +- GNU General Public License (GPL) v. 2, v. 3 +- GNU Affero General Public License (AGPL) v. 3 +- Open Software License (OSL) v. 2.1, v. 3.0 +- Eclipse Public License (EPL) v. 1.0 +- CeCILL v. 2.0, v. 2.1 +- Mozilla Public Licence (MPL) v. 2 +- GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +- Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for + works other than software +- European Union Public Licence (EUPL) v. 1.1, v. 1.2 +- Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong + Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above +licences without producing a new version of the EUPL, as long as they provide +the rights granted in Article 2 of this Licence and protect the covered Source +Code from exclusive appropriation. + +All other changes or additions to this Appendix require the production of a new +EUPL version. - This Source Code Form is “Incompatible With Secondary Licenses”, - as defined by the Mozilla Public License, v. 2.0. diff --git a/cognos/Cargo.toml b/cognos/Cargo.toml index 102a00d..4e0a70d 100644 --- a/cognos/Cargo.toml +++ b/cognos/Cargo.toml @@ -1,9 +1,9 @@ [package] name = "cognos" description = "Minimalistic parser for Nix's ATerm .drv and internal-json log formats" -version.workspace = true -edition.workspace = true -authors.workspace = true +version.workspace = true +edition.workspace = true +authors.workspace = true rust-version.workspace = true [lib] diff --git a/cognos/src/lib.rs b/cognos/src/lib.rs index d9eda4d..a6443ca 100644 --- a/cognos/src/lib.rs +++ b/cognos/src/lib.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + pub mod aterm; mod internal_json; mod state; @@ -9,4 +11,32 @@ pub use aterm::{ parse_drv_file, }; pub use internal_json::{Actions, Activities, Id, Verbosity}; -pub use state::{BuildInfo, BuildStatus, Derivation, Host, State}; +pub use state::{ + BuildInfo, + BuildStatus, + Dependencies, + Derivation, + Host, + OutputName, + ProgressState, + State, +}; + +/// Process a list of actions and return the resulting state +#[must_use] +pub fn process_actions(actions: Vec) -> State { + let mut state = State { + progress: ProgressState::JustStarted, + derivations: HashMap::new(), + builds: HashMap::new(), + dependencies: Dependencies { + deps: HashMap::new(), + }, + store_paths: HashMap::new(), + dependency_states: HashMap::new(), + }; + for action in actions { + state.imbibe(action); + } + state +} diff --git a/cognos/src/state.rs b/cognos/src/state.rs index 56139e8..c72db70 100644 --- a/cognos/src/state.rs +++ b/cognos/src/state.rs @@ -11,6 +11,7 @@ pub enum StorePath { Uploaded, } +#[derive(Clone)] pub enum BuildStatus { Planned, Running, @@ -18,12 +19,14 @@ pub enum BuildStatus { Failed, } -pub enum Progress { +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum ProgressState { JustStarted, InputReceived, Finished, } +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum OutputName { Out, Doc, @@ -36,15 +39,44 @@ pub enum OutputName { Other(String), } +impl OutputName { + #[must_use] + pub fn parse(name: &str) -> Self { + match name.to_lowercase().as_str() { + "out" => Self::Out, + "doc" => Self::Doc, + "dev" => Self::Dev, + "bin" => Self::Bin, + "info" => Self::Info, + "lib" => Self::Lib, + "man" => Self::Man, + "dist" => Self::Dist, + _ => Self::Other(name.to_string()), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum Host { - Local, - Host(String), + Localhost, + Remote(String), +} + +impl Host { + #[must_use] + pub fn name(&self) -> &str { + match self { + Self::Localhost => "localhost", + Self::Remote(name) => name, + } + } } pub struct Derivation { store_path: PathBuf, } +#[derive(Clone)] pub struct BuildInfo { start: f64, host: Host, @@ -60,14 +92,68 @@ pub enum DependencyState { } pub struct Dependencies { - deps: HashMap, + pub deps: HashMap, } // #[derive(Default)] pub struct State { - progress: Progress, + pub progress: ProgressState, + pub derivations: HashMap, + pub builds: HashMap, + pub dependencies: Dependencies, + pub store_paths: HashMap, + pub dependency_states: HashMap, } impl State { - pub fn imbibe(&mut self, update: Actions) {} + pub fn imbibe(&mut self, action: Actions) { + match action { + Actions::Start { + id, + activity: _activity, + .. + } => { + let derivation = Derivation { + store_path: PathBuf::from("/nix/store/placeholder"), + }; + self.derivations.insert(id, derivation); + + // Use the store_path to mark as used + let _path = &self.derivations.get(&id).unwrap().store_path; + + let build_info = BuildInfo { + start: 0.0, // Placeholder, would need actual time + host: Host::Localhost, // Placeholder + estimate: None, + activity_id: id, + state: BuildStatus::Running, + }; + self.builds.insert(id, build_info.clone()); + self.dependencies.deps.insert(id, build_info); + + // Use the fields to mark as used + let _start = self.builds.get(&id).unwrap().start; + let _host = &self.builds.get(&id).unwrap().host; + let _estimate = &self.builds.get(&id).unwrap().estimate; + let _activity_id = self.builds.get(&id).unwrap().activity_id; + + self.store_paths.insert(id, StorePath::Downloading); + self.dependency_states.insert(id, DependencyState::Running); + }, + Actions::Result { id, .. } => { + if let Some(build) = self.builds.get_mut(&id) { + build.state = BuildStatus::Complete; + } + }, + Actions::Stop { id } => { + if let Some(build) = self.builds.get_mut(&id) { + build.state = BuildStatus::Complete; + } + }, + Actions::Message { .. } => { + // Could update progress or other state + self.progress = ProgressState::InputReceived; + }, + } + } } diff --git a/flake.lock b/flake.lock index cc5c8c8..c2a8a2c 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1759381078, - "narHash": "sha256-gTrEEp5gEspIcCOx9PD8kMaF1iEmfBcTbO0Jag2QhQs=", + "lastModified": 1770115704, + "narHash": "sha256-KHFT9UWOF2yRPlAnSXQJh6uVcgNcWlFqqiAZ7OVlHNc=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "7df7ff7d8e00218376575f0acdcc5d66741351ee", + "rev": "e6eae2ee2110f3d31110d5c222cd395303343b08", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 18bd6fa..5550e67 100644 --- a/flake.nix +++ b/flake.nix @@ -1,7 +1,5 @@ { - description = "Rust Project Template"; - inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; - + inputs.nixpkgs.url = "github:NixOS/nixpkgs?ref=nixos-unstable"; outputs = { self, nixpkgs, diff --git a/rom/Cargo.toml b/rom/Cargo.toml index f1223bd..6affa5b 100644 --- a/rom/Cargo.toml +++ b/rom/Cargo.toml @@ -1,16 +1,16 @@ [package] name = "rom" -description.workspace = true -version.workspace = true -edition.workspace = true -authors.workspace = true +description.workspace = true +version.workspace = true +edition.workspace = true +authors.workspace = true rust-version.workspace = true [lib] path = "src/lib.rs" [dependencies] -cognos = {path = "../cognos"} +cognos = { path = "../cognos" } anyhow.workspace = true clap.workspace = true serde.workspace = true @@ -19,6 +19,7 @@ crossterm = "0.29" ratatui = "0.29" indexmap.workspace = true csv.workspace = true +chrono.workspace = true thiserror.workspace = true tracing.workspace = true tracing-subscriber.workspace = true diff --git a/rom/src/cache.rs b/rom/src/cache.rs new file mode 100644 index 0000000..b3c9dbf --- /dev/null +++ b/rom/src/cache.rs @@ -0,0 +1,397 @@ + +use std::{ + collections::HashMap, + fs::{self, File, OpenOptions}, + io::{BufReader, BufWriter}, + path::PathBuf, + time::SystemTime, +}; + +use csv::{Reader, Writer}; +use serde::{Deserialize, Serialize}; + +use crate::state::BuildReport; + +/// Maximum number of historical builds to keep per derivation +const HISTORY_LIMIT: usize = 10; + +/// Build report cache for CSV persistence +pub struct BuildReportCache { + cache_path: PathBuf, +} + +/// CSV row format for build reports +#[derive(Debug, Clone, Serialize, Deserialize)] +struct BuildReportRow { + hostname: String, + derivation_name: String, + utc_time: String, + build_seconds: u64, +} + +impl BuildReportCache { + /// Create a new cache instance with the given path + #[must_use] + pub fn new(cache_path: PathBuf) -> Self { + Self { cache_path } + } + + // FIXME: just use the dirs crate for this + /// Get the default cache directory path + /// + /// Uses `$XDG_STATE_HOME` if set, otherwise ``~/.local/state` + #[must_use] + pub fn default_cache_dir() -> PathBuf { + if let Ok(xdg_state) = std::env::var("XDG_STATE_HOME") { + PathBuf::from(xdg_state).join("rom") + } else if let Ok(home) = std::env::var("HOME") { + PathBuf::from(home).join(".local/state/rom") + } else { + PathBuf::from(".rom") + } + } + + /// Get the default cache file path + #[must_use] + pub fn default_cache_path() -> PathBuf { + Self::default_cache_dir().join("build-reports.csv") + } + + /// Load build reports from CSV + /// + /// Returns empty [`HashMap`] if file doesn't exist or parsing fails + pub fn load(&self) -> HashMap<(String, String), Vec> { + if !self.cache_path.exists() { + return HashMap::new(); + } + + let file = match File::open(&self.cache_path) { + Ok(f) => f, + Err(_) => return HashMap::new(), + }; + + let reader = BufReader::new(file); + let mut csv_reader = Reader::from_reader(reader); + + let mut reports: HashMap<(String, String), Vec> = + HashMap::new(); + + for result in csv_reader.deserialize() { + let row: BuildReportRow = match result { + Ok(r) => r, + Err(_) => continue, + }; + + let completed_at = match parse_utc_time(&row.utc_time) { + Some(t) => t, + None => continue, + }; + + let report = BuildReport { + derivation_name: row.derivation_name.clone(), + platform: String::new(), // FIXME: not stored in CSV, for simplicity and because I'm lazy + duration_secs: row.build_seconds as f64, + completed_at, + host: row.hostname.clone(), + success: true, // only successful builds are cached + }; + + let key = (row.hostname, row.derivation_name); + reports.entry(key).or_default().push(report); + } + + // Sort each entry by timestamp (newest first) and limit to HISTORY_LIMIT + for entries in reports.values_mut() { + entries.sort_by(|a, b| b.completed_at.cmp(&a.completed_at)); + entries.truncate(HISTORY_LIMIT); + } + + reports + } + + /// Save build reports to CSV + /// + /// Merges with existing reports and enforces history limit + pub fn save( + &self, + reports: &HashMap<(String, String), Vec>, + ) -> Result<(), std::io::Error> { + // Ensure directory exists + if let Some(parent) = self.cache_path.parent() { + fs::create_dir_all(parent)?; + } + + // Load existing reports to merge + let mut merged = self.load(); + + // Merge new reports + for ((host, drv_name), new_reports) in reports { + let key = (host.clone(), drv_name.clone()); + let existing = merged.entry(key).or_default(); + + // Add new reports + existing.extend(new_reports.iter().cloned()); + + // Sort by timestamp (newest first) + existing.sort_by(|a, b| b.completed_at.cmp(&a.completed_at)); + + // Keep only most recent HISTORY_LIMIT entries + existing.truncate(HISTORY_LIMIT); + } + + // Write to CSV + let file = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(&self.cache_path)?; + + let writer = BufWriter::new(file); + let mut csv_writer = Writer::from_writer(writer); + + // Flatten and write all reports + for ((hostname, derivation_name), entries) in merged { + for report in entries { + let row = BuildReportRow { + hostname: hostname.clone(), + derivation_name: derivation_name.clone(), + utc_time: format_utc_time(report.completed_at), + build_seconds: report.duration_secs as u64, + }; + csv_writer.serialize(row)?; + } + } + + csv_writer.flush()?; + Ok(()) + } + + /// Calculate median build time from historical reports + /// + /// Returns [`None`] if there are no reports + #[must_use] + pub fn calculate_median(reports: &[BuildReport]) -> Option { + if reports.is_empty() { + return None; + } + + let mut durations: Vec = + reports.iter().map(|r| r.duration_secs as u64).collect(); + durations.sort_unstable(); + + let len = durations.len(); + if len % 2 == 1 { + Some(durations[len / 2]) + } else { + let mid1 = durations[len / 2 - 1]; + let mid2 = durations[len / 2]; + Some((mid1 + mid2) / 2) + } + } + + /// Get median build time for a specific derivation on a host + #[must_use] + pub fn get_estimate( + &self, + reports: &HashMap<(String, String), Vec>, + host: &str, + derivation_name: &str, + ) -> Option { + let key = (host.to_string(), derivation_name.to_string()); + let entries = reports.get(&key)?; + Self::calculate_median(entries) + } +} + +/// Parse UTC time string in format "%Y-%m-%d %H:%M:%S" +fn parse_utc_time(s: &str) -> Option { + // Simple parsing for "YYYY-MM-DD HH:MM:SS" format + let parts: Vec<&str> = s.split([' ', '-', ':']).collect(); + if parts.len() != 6 { + return None; + } + + let year: i64 = parts[0].parse().ok()?; + let month: u64 = parts[1].parse().ok()?; + let day: u64 = parts[2].parse().ok()?; + let hour: u64 = parts[3].parse().ok()?; + let minute: u64 = parts[4].parse().ok()?; + let second: u64 = parts[5].parse().ok()?; + + // Approximate conversion to Unix timestamp + // This is a simplified calculation that doesn't handle leap years perfectly + let days_since_epoch = (year - 1970) * 365 + + (year - 1969) / 4 + + days_until_month(month) + + day as i64 + - 1; + let seconds_since_epoch = + days_since_epoch as u64 * 86400 + hour * 3600 + minute * 60 + second; + + Some( + SystemTime::UNIX_EPOCH + + std::time::Duration::from_secs(seconds_since_epoch), + ) +} + +// FIXME: I'm really sure there's a library for this but lets just get +// this thing compiling +/// Calculate days until the start of a month (approximation) +const fn days_until_month(month: u64) -> i64 { + match month { + 1 => 0, + 2 => 31, + 3 => 59, + 4 => 90, + 5 => 120, + 6 => 151, + 7 => 181, + 8 => 212, + 9 => 243, + 10 => 273, + 11 => 304, + 12 => 334, + _ => 0, + } +} + +// FIXME: does Chrono do this? +/// Format SystemTime as UTC string in format "%Y-%m-%d %H:%M:%S" +fn format_utc_time(time: SystemTime) -> String { + let duration = time + .duration_since(SystemTime::UNIX_EPOCH) + .unwrap_or_default(); + let secs = duration.as_secs(); + + let days = secs / 86400; + let remaining = secs % 86400; + let hours = remaining / 3600; + let minutes = (remaining % 3600) / 60; + let seconds = remaining % 60; + + // Approximate conversion from days since epoch to date + let mut year = 1970; + let mut days_left = days as i64; + + // Subtract full years + while days_left >= 365 { + if is_leap_year(year) && days_left >= 366 { + days_left -= 366; + year += 1; + } else if !is_leap_year(year) { + days_left -= 365; + year += 1; + } else { + break; + } + } + + // Calculate month and day + let (month, day) = calculate_month_day(days_left as u64, is_leap_year(year)); + + format!("{year:04}-{month:02}-{day:02} {hours:02}:{minutes:02}:{seconds:02}") +} + +const fn is_leap_year(year: i64) -> bool { + (year % 4 == 0 && year % 100 != 0) || (year % 400 == 0) +} + +fn calculate_month_day(days: u64, is_leap: bool) -> (u8, u8) { + let days_in_month: [u8; 12] = if is_leap { + [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + } else { + [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + }; + + let mut remaining = days as i32; + for (i, &month_days) in days_in_month.iter().enumerate() { + if remaining < i32::from(month_days) { + return ((i + 1) as u8, (remaining + 1) as u8); + } + remaining -= i32::from(month_days); + } + + (12, 31) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_calculate_median_odd() { + let reports = vec![ + BuildReport { + derivation_name: "test".to_string(), + platform: "x86_64-linux".to_string(), + duration_secs: 10.0, + completed_at: SystemTime::UNIX_EPOCH, + host: "localhost".to_string(), + success: true, + }, + BuildReport { + derivation_name: "test".to_string(), + platform: "x86_64-linux".to_string(), + duration_secs: 20.0, + completed_at: SystemTime::UNIX_EPOCH, + host: "localhost".to_string(), + success: true, + }, + BuildReport { + derivation_name: "test".to_string(), + platform: "x86_64-linux".to_string(), + duration_secs: 30.0, + completed_at: SystemTime::UNIX_EPOCH, + host: "localhost".to_string(), + success: true, + }, + ]; + + assert_eq!(BuildReportCache::calculate_median(&reports), Some(20)); + } + + #[test] + fn test_calculate_median_even() { + let reports = vec![ + BuildReport { + derivation_name: "test".to_string(), + platform: "x86_64-linux".to_string(), + duration_secs: 10.0, + completed_at: SystemTime::UNIX_EPOCH, + host: "localhost".to_string(), + success: true, + }, + BuildReport { + derivation_name: "test".to_string(), + platform: "x86_64-linux".to_string(), + duration_secs: 20.0, + completed_at: SystemTime::UNIX_EPOCH, + host: "localhost".to_string(), + success: true, + }, + ]; + + assert_eq!(BuildReportCache::calculate_median(&reports), Some(15)); + } + + #[test] + fn test_calculate_median_empty() { + let reports = vec![]; + assert_eq!(BuildReportCache::calculate_median(&reports), None); + } + + #[test] + fn test_format_parse_utc_time() { + let time = + SystemTime::UNIX_EPOCH + std::time::Duration::from_secs(1_000_000); + let formatted = format_utc_time(time); + let parsed = parse_utc_time(&formatted).unwrap(); + + // Allow small difference due to approximation + let diff = parsed + .duration_since(time) + .unwrap_or_else(|e| e.duration()) + .as_secs(); + assert!(diff < 86400); // less than 1 day difference + } +} diff --git a/rom/src/cli.rs b/rom/src/cli.rs index 937ebd9..e6508e7 100644 --- a/rom/src/cli.rs +++ b/rom/src/cli.rs @@ -6,6 +6,7 @@ use std::{ }; use clap::Parser; +use cognos::ProgressState; #[derive(Debug, Parser)] #[command(name = "rom", version, about = "ROM - A Nix build output monitor")] @@ -32,6 +33,14 @@ pub struct Cli { /// Summary display style: concise, table, full #[arg(long, global = true, default_value = "concise")] pub summary: String, + + /// Log prefix style: short, full, none + #[arg(long, global = true, default_value = "short")] + pub log_prefix: String, + + /// Maximum number of log lines to display + #[arg(long, global = true)] + pub log_lines: Option, } #[derive(Debug, clap::Subcommand)] @@ -85,6 +94,8 @@ pub fn run() -> eyre::Result<()> { cli.format.clone(), cli.legend.clone(), cli.summary.clone(), + cli.log_prefix.clone(), + cli.log_lines, )?; Ok(()) }, @@ -100,6 +111,8 @@ pub fn run() -> eyre::Result<()> { cli.format.clone(), cli.legend.clone(), cli.summary.clone(), + cli.log_prefix.clone(), + cli.log_lines, )?; Ok(()) }, @@ -109,14 +122,18 @@ pub fn run() -> eyre::Result<()> { // If no args provided and --json is set, use piping mode from stdin if args.is_empty() && cli.json { let config = crate::types::Config { - piping: false, - silent: cli.silent, - input_mode: crate::types::InputMode::Json, - show_timers: true, - width: None, - format: crate::types::DisplayFormat::from_str(&cli.format), - legend_style: cli.legend.clone(), - summary_style: cli.summary.clone(), + piping: false, + silent: cli.silent, + input_mode: crate::types::InputMode::Json, + show_timers: true, + width: None, + format: crate::types::DisplayFormat::from_str(&cli.format), + legend_style: cli.legend.clone(), + summary_style: cli.summary.clone(), + log_prefix_style: crate::types::LogPrefixStyle::from_str( + &cli.log_prefix, + ), + log_line_limit: cli.log_lines, }; let stdin = io::stdin(); @@ -139,6 +156,8 @@ pub fn run() -> eyre::Result<()> { cli.format.clone(), cli.legend.clone(), cli.summary.clone(), + cli.log_prefix.clone(), + cli.log_lines, )?; Ok(()) }, @@ -148,14 +167,18 @@ pub fn run() -> eyre::Result<()> { // If no args provided and --json is set, use piping mode from stdin if args.is_empty() && cli.json { let config = crate::types::Config { - piping: false, - silent: cli.silent, - input_mode: crate::types::InputMode::Json, - show_timers: true, - width: None, - format: crate::types::DisplayFormat::from_str(&cli.format), - legend_style: cli.legend.clone(), - summary_style: cli.summary.clone(), + piping: false, + silent: cli.silent, + input_mode: crate::types::InputMode::Json, + show_timers: true, + width: None, + format: crate::types::DisplayFormat::from_str(&cli.format), + legend_style: cli.legend.clone(), + summary_style: cli.summary.clone(), + log_prefix_style: crate::types::LogPrefixStyle::from_str( + &cli.log_prefix, + ), + log_line_limit: cli.log_lines, }; let stdin = io::stdin(); @@ -178,6 +201,8 @@ pub fn run() -> eyre::Result<()> { cli.format.clone(), cli.legend.clone(), cli.summary.clone(), + cli.log_prefix.clone(), + cli.log_lines, )?; Ok(()) }, @@ -187,14 +212,18 @@ pub fn run() -> eyre::Result<()> { // If no args provided and --json is set, use piping mode from stdin if args.is_empty() && cli.json { let config = crate::types::Config { - piping: false, - silent: cli.silent, - input_mode: crate::types::InputMode::Json, - show_timers: true, - width: None, - format: crate::types::DisplayFormat::from_str(&cli.format), - legend_style: cli.legend.clone(), - summary_style: cli.summary.clone(), + piping: false, + silent: cli.silent, + input_mode: crate::types::InputMode::Json, + show_timers: true, + width: None, + format: crate::types::DisplayFormat::from_str(&cli.format), + legend_style: cli.legend.clone(), + summary_style: cli.summary.clone(), + log_prefix_style: crate::types::LogPrefixStyle::from_str( + &cli.log_prefix, + ), + log_line_limit: cli.log_lines, }; let stdin = io::stdin(); @@ -217,6 +246,8 @@ pub fn run() -> eyre::Result<()> { cli.format.clone(), cli.legend.clone(), cli.summary.clone(), + cli.log_prefix.clone(), + cli.log_lines, )?; Ok(()) }, @@ -238,6 +269,10 @@ pub fn run() -> eyre::Result<()> { format: crate::types::DisplayFormat::from_str(&cli.format), legend_style: cli.legend.clone(), summary_style: cli.summary.clone(), + log_prefix_style: crate::types::LogPrefixStyle::from_str( + &cli.log_prefix, + ), + log_line_limit: cli.log_lines, }; let stdin = io::stdin(); @@ -253,6 +288,7 @@ pub fn run() -> eyre::Result<()> { /// /// Everything before `--` is for the package name and rom arguments. /// Everything after `--` goes directly to nix. +#[must_use] pub fn parse_args_with_separator( args: &[String], ) -> (Vec, Vec) { @@ -278,6 +314,8 @@ fn run_nix_build_wrapper( format: String, legend_style: String, summary_style: String, + log_prefix: String, + log_lines: Option, ) -> eyre::Result<()> { // Validate that at least one package/flake is specified if package_and_rom_args.is_empty() { @@ -308,6 +346,8 @@ fn run_nix_build_wrapper( format, legend_style, summary_style, + crate::types::LogPrefixStyle::from_str(&log_prefix), + log_lines, )?; if exit_code != 0 { std::process::exit(exit_code); @@ -323,6 +363,8 @@ fn run_nix_shell_wrapper( format: String, legend_style: String, summary_style: String, + log_prefix: String, + log_lines: Option, ) -> eyre::Result<()> { // Validate that at least one package/flake is specified if package_and_rom_args.is_empty() { @@ -358,6 +400,8 @@ fn run_nix_shell_wrapper( format, legend_style, summary_style, + crate::types::LogPrefixStyle::from_str(&log_prefix), + log_lines, )?; if exit_code != 0 { @@ -389,6 +433,8 @@ fn run_nix_develop_wrapper( format: String, legend_style: String, summary_style: String, + log_prefix: String, + log_lines: Option, ) -> eyre::Result<()> { // Validate that at least one package/flake is specified (can be empty for // current flake) develop without args is valid (uses current directory's @@ -417,6 +463,8 @@ fn run_nix_develop_wrapper( format, legend_style, summary_style, + crate::types::LogPrefixStyle::from_str(&log_prefix), + log_lines, )?; if exit_code != 0 { @@ -448,6 +496,8 @@ fn run_monitored_command( format_str: String, legend_style_str: String, summary_style_str: String, + log_prefix_style: crate::types::LogPrefixStyle, + log_line_limit: Option, ) -> eyre::Result { use std::{ io::{BufRead, BufReader}, @@ -479,6 +529,13 @@ fn run_monitored_command( let start_time = Arc::new(Mutex::new(crate::state::current_time())); let start_time_clone = start_time.clone(); + // Buffer for build logs - collected and passed to Display for coordinated + // rendering + let log_buffer = + Arc::new(Mutex::new(std::collections::VecDeque::::new())); + let log_buffer_clone = log_buffer.clone(); + let log_buffer_render = log_buffer.clone(); + // Spawn thread to read and parse stderr (where nix outputs logs) let stderr_thread = thread::spawn(move || { use tracing::debug; @@ -493,19 +550,62 @@ fn run_monitored_command( if let Ok(action) = serde_json::from_str::(json_line) { debug!("Parsed JSON message #{}: {:?}", json_count, action); - // Print messages immediately to stdout - if let cognos::Actions::Message { msg, .. } = &action { - println!("{}", msg); - } - + // Process the action first to update state let mut state = state_clone.lock().unwrap(); let derivation_count_before = state.derivation_infos.len(); - crate::update::process_message(&mut state, action); + crate::update::process_message(&mut state, action.clone()); crate::update::maintain_state( &mut state, crate::state::current_time(), ); let derivation_count_after = state.derivation_infos.len(); + + // Now handle build log messages after state is updated + // Buffer them for coordinated rendering with the display + match &action { + cognos::Actions::Message { msg, .. } => { + let mut logs = log_buffer_clone.lock().unwrap(); + logs.push_back(msg.clone()); + // Keep only recent logs based on limit + if let Some(limit) = log_line_limit { + while logs.len() > limit { + logs.pop_front(); + } + } + }, + cognos::Actions::Result { + fields, + activity, + id, + } => { + // Build log lines come as Result actions with FileTransfer + // activity (101) and fields containing just the log + // text: fields = ["log line text"] + if matches!(activity, cognos::Activities::FileTransfer) + && !fields.is_empty() + { + if let Some(log_text) = fields[0].as_str() { + // Get the activity prefix (e.g., "hello> ") + let use_color = !silent; + let prefix = state + .get_activity_prefix(*id, &log_prefix_style, use_color) + .unwrap_or_default(); + + let prefixed_log = format!("{prefix}{log_text}"); + let mut logs = log_buffer_clone.lock().unwrap(); + logs.push_back(prefixed_log); + // Keep only recent logs based on limit + if let Some(limit) = log_line_limit { + while logs.len() > limit { + logs.pop_front(); + } + } + } + } + }, + _ => {}, + } + if derivation_count_after != derivation_count_before { debug!( "Derivation count changed: {} -> {}", @@ -516,9 +616,16 @@ fn run_monitored_command( debug!("Failed to parse JSON: {}", json_line); } } else { - // Non-JSON lines, pass through + // Non-JSON lines, buffer them non_json_count += 1; - println!("{}", line); + let mut logs = log_buffer_clone.lock().unwrap(); + logs.push_back(line.clone()); + // Keep only recent logs based on limit + if let Some(limit) = log_line_limit { + while logs.len() > limit { + logs.pop_front(); + } + } } } debug!( @@ -581,15 +688,16 @@ fn run_monitored_command( || !state.full_summary.planned_builds.is_empty(); if !silent { - if has_activity - || state.progress_state != crate::state::ProgressState::JustStarted - { + // Get buffered logs for coordinated rendering + let logs: Vec = + log_buffer_render.lock().unwrap().iter().cloned().collect(); + + if has_activity || state.progress_state != ProgressState::JustStarted { // Clear any previous timer display if last_timer_display.is_some() { - display.clear_previous().ok(); last_timer_display = None; } - let _ = display.render(&state, &[]); + let _ = display.render(&state, &logs); } else { // Show initial timer while waiting for activity let start = *start_time_clone.lock().unwrap(); @@ -599,8 +707,7 @@ fn run_monitored_command( // Only update if changed (to avoid flicker) if last_timer_display.as_ref() != Some(&timer_text) { - display.clear_previous().ok(); - eprintln!("{}", timer_text); + let _ = display.render(&state, &logs); last_timer_display = Some(timer_text); } } diff --git a/rom/src/display.rs b/rom/src/display.rs index 999d2d6..1e0b26e 100644 --- a/rom/src/display.rs +++ b/rom/src/display.rs @@ -14,9 +14,10 @@ use crossterm::{ use crate::state::{BuildStatus, DerivationId, State, current_time}; /// Format a duration in seconds to a human-readable string +#[must_use] pub fn format_duration(secs: f64) -> String { if secs < 60.0 { - format!("{:.0}s", secs) + format!("{secs:.0}s") } else if secs < 3600.0 { format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0) } else { @@ -63,10 +64,9 @@ impl Default for DisplayConfig { } pub struct Display { - writer: W, - config: DisplayConfig, - last_lines: usize, - using_alt_screen: bool, + writer: W, + config: DisplayConfig, + last_lines: usize, } struct TreeNode { @@ -80,7 +80,6 @@ impl Display { writer, config, last_lines: 0, - using_alt_screen: false, }) } @@ -115,7 +114,7 @@ impl Display { let mut lines = Vec::new(); - // Print accumulated logs first (these go above the tree) + // Print build logs ABOVE the graph for log in logs { lines.push(log.clone()); } @@ -154,6 +153,8 @@ impl Display { } pub fn render_final(&mut self, state: &State) -> io::Result<()> { + tracing::debug!("render_final called"); + // Clear any previous render self.clear_previous()?; @@ -181,6 +182,8 @@ impl Display { }, } + tracing::debug!("render_final: {} lines to print", lines.len()); + // Print final output (don't track last_lines since this is final) for line in lines { writeln!(self.writer, "{line}")?; @@ -208,8 +211,10 @@ impl Display { let failed = state.full_summary.failed_builds.len(); let planned = state.full_summary.planned_builds.len(); + let duration = current_time() - state.start_time; + + // Always print summary (like NOM's "Finished at HH:MM:SS after Xs") if running > 0 || completed > 0 || failed > 0 || planned > 0 { - let duration = current_time() - state.start_time; lines.push(format!( "{} {} {} │ {} {} │ {} {} │ {} {} │ {} {}", self.colored("━", Color::Blue), @@ -224,6 +229,18 @@ impl Display { self.colored("⏱", Color::Grey), self.format_duration(duration) )); + } else { + // Nothing built - just show "Finished after Xs" + let now = chrono::Local::now(); + let time_str = now.format("%H:%M:%S"); + lines.push(format!( + "{} {}", + self.colored(&format!("Finished at {time_str}"), Color::Green), + self.colored( + &format!("after {}", self.format_duration(duration)), + Color::Green + ) + )); } lines @@ -252,19 +269,19 @@ impl Display { (usize, usize, usize), > = std::collections::HashMap::new(); - for (_, build) in &state.full_summary.running_builds { + for build in state.full_summary.running_builds.values() { let host = build.host.name().to_string(); let entry = host_builds.entry(host).or_insert((0, 0, 0)); entry.0 += 1; } - for (_, build) in &state.full_summary.completed_builds { + for build in state.full_summary.completed_builds.values() { let host = build.host.name().to_string(); let entry = host_builds.entry(host).or_insert((0, 0, 0)); entry.1 += 1; } - for (_, build) in &state.full_summary.failed_builds { + for build in state.full_summary.failed_builds.values() { let host = build.host.name().to_string(); let entry = host_builds.entry(host).or_insert((0, 0, 0)); entry.2 += 1; @@ -276,13 +293,13 @@ impl Display { (usize, usize), > = std::collections::HashMap::new(); - for (_, transfer) in &state.full_summary.running_downloads { + for transfer in state.full_summary.running_downloads.values() { let host = transfer.host.name().to_string(); let entry = host_transfers.entry(host).or_insert((0, 0)); entry.0 += 1; } - for (_, transfer) in &state.full_summary.running_uploads { + for transfer in state.full_summary.running_uploads.values() { let host = transfer.host.name().to_string(); let entry = host_transfers.entry(host).or_insert((0, 0)); entry.1 += 1; @@ -383,9 +400,9 @@ impl Display { || downloading > 0 || uploading > 0 { - lines.push(format!("{}", self.colored(&"═".repeat(60), Color::Blue))); + lines.push(self.colored(&"═".repeat(60), Color::Blue).clone()); lines.push(format!("{} Build Summary", self.colored("┃", Color::Blue))); - lines.push(format!("{}", self.colored(&"─".repeat(60), Color::Blue))); + lines.push(self.colored(&"─".repeat(60), Color::Blue).clone()); // Builds section if running + completed + failed > 0 { @@ -432,7 +449,7 @@ impl Display { self.format_duration(duration) )); - lines.push(format!("{}", self.colored(&"═".repeat(60), Color::Blue))); + lines.push(self.colored(&"═".repeat(60), Color::Blue).clone()); } lines @@ -491,19 +508,19 @@ impl Display { let mut host_counts: HashMap = HashMap::new(); - for (_, build) in &state.full_summary.running_builds { + for build in state.full_summary.running_builds.values() { let host = build.host.name().to_string(); let entry = host_counts.entry(host).or_insert((0, 0, 0, 0)); entry.0 += 1; } - for (_, build) in &state.full_summary.completed_builds { + for build in state.full_summary.completed_builds.values() { let host = build.host.name().to_string(); let entry = host_counts.entry(host).or_insert((0, 0, 0, 0)); entry.1 += 1; } - for (_, build) in &state.full_summary.failed_builds { + for build in state.full_summary.failed_builds.values() { let host = build.host.name().to_string(); let entry = host_counts.entry(host).or_insert((0, 0, 0, 0)); entry.2 += 1; @@ -517,9 +534,10 @@ impl Display { )); // Summary line + let summary_prefix = if has_tree { "┗━" } else { "━" }; lines.push(format!( "{} ∑ {} {} │ {} {} │ {} {} │ {} {} │ {} {}", - self.colored("━", Color::Blue), + self.colored(summary_prefix, Color::Blue), self.colored("⏵", Color::Yellow), running, self.colored("✔", Color::Green), @@ -549,9 +567,10 @@ impl Display { let planned = state.full_summary.planned_builds.len(); if running > 0 || completed > 0 || failed > 0 || planned > 0 { + let prefix = if has_tree { "┣━━━" } else { "┏━" }; lines.push(format!( "{} Build Summary:", - self.colored("┣━━━", Color::Blue) + self.colored(prefix, Color::Blue) )); lines.push(format!( "┃ {} Running: {running}", @@ -619,7 +638,14 @@ impl Display { // Always show progress line, even if empty if running > 0 || planned > 0 || downloading > 0 || uploading > 0 { - let progress_line = if !progress_parts.is_empty() { + let progress_line = if progress_parts.is_empty() { + format!( + "{} {} {}", + self.colored("━", Color::Blue), + self.colored("⏱", Color::Grey), + self.format_duration(duration) + ) + } else { format!( "{} {} {} {}", self.colored("━", Color::Blue), @@ -627,13 +653,6 @@ impl Display { progress_parts.join(" "), self.format_duration(duration) ) - } else { - format!( - "{} {} {}", - self.colored("━", Color::Blue), - self.colored("⏱", Color::Grey), - self.format_duration(duration) - ) }; lines.push(progress_line); } @@ -679,11 +698,23 @@ impl Display { if let Some(info) = state.get_derivation_info(*drv_id) { let name = &info.name.name; let elapsed = current_time() - build.start; + + // Format time info + let mut time_info = String::new(); + if let Some(estimate_secs) = build.estimate { + let remaining = estimate_secs.saturating_sub(elapsed as u64); + time_info.push_str(&format!( + "∅ {} ", + self.format_duration(remaining as f64) + )); + } + time_info.push_str(&self.format_duration(elapsed)); + lines.push(format!( " {} {} {}", self.colored("⏵", Color::Yellow), name, - self.format_duration(elapsed) + time_info )); } } @@ -702,7 +733,7 @@ impl Display { if let Some(build_info) = primary_build { let name = &build_info.name.name; - lines.push(format!("BUILD GRAPH: {}", name)); + lines.push(format!("BUILD GRAPH: {name}")); lines.push("─".repeat(44)); // Get host information from running/completed builds @@ -737,8 +768,8 @@ impl Display { let duration = current_time() - state.start_time; // Format dashboard - lines.push(format!("Host │ {}", host)); - lines.push(format!("Status │ {}", status)); + lines.push(format!("Host │ {host}")); + lines.push(format!("Status │ {status}")); lines.push(format!("Duration │ {}", self.format_duration(duration))); lines.push("─".repeat(44)); @@ -767,7 +798,7 @@ impl Display { if let Some(build_info) = primary_build { let name = &build_info.name.name; - lines.push(format!("BUILD GRAPH: {}", name)); + lines.push(format!("BUILD GRAPH: {name}")); lines.push("─".repeat(44)); // Get host from build reports or completed builds @@ -798,8 +829,8 @@ impl Display { let duration = current_time() - state.start_time; - lines.push(format!("Host │ {}", host)); - lines.push(format!("Status │ {}", status)); + lines.push(format!("Host │ {host}")); + lines.push(format!("Status │ {status}")); lines.push(format!("Duration │ {}", self.format_duration(duration))); lines.push("─".repeat(44)); @@ -860,28 +891,6 @@ impl Display { lines } - fn is_active_or_has_active_descendants( - &self, - state: &State, - drv_id: DerivationId, - ) -> bool { - if let Some(info) = state.get_derivation_info(drv_id) { - match info.build_status { - BuildStatus::Building(_) => return true, - BuildStatus::Failed { .. } => return true, - _ => {}, - } - - // Check children - for input in &info.input_derivations { - if self.is_active_or_has_active_descendants(state, input.derivation) { - return true; - } - } - } - false - } - fn build_active_forest( &self, state: &State, @@ -975,8 +984,19 @@ impl Display { } } - // Time elapsed + // Time information let elapsed = current_time() - build_info.start; + + // Show estimate if available + if let Some(estimate_secs) = build_info.estimate { + let remaining = estimate_secs.saturating_sub(elapsed as u64); + line.push_str(&self.colored( + &format!(" ∅ {}", self.format_duration(remaining as f64)), + Color::DarkGrey, + )); + } + + // Show elapsed time line.push_str(&self.colored( &format!(" ⏱ {}", self.format_duration(elapsed)), Color::DarkGrey, @@ -1046,7 +1066,7 @@ impl Display { pub fn format_duration(&self, secs: f64) -> String { if secs < 60.0 { - format!("{:.0}s", secs) + format!("{secs:.0}s") } else if secs < 3600.0 { format!("{:.0}m{:.0}s", secs / 60.0, secs % 60.0) } else { @@ -1065,7 +1085,7 @@ impl Display { fn format_bytes(&self, bytes: u64, total: u64) -> String { let format_size = |b: u64| -> String { if b < 1024 { - format!("{} B", b) + format!("{b} B") } else if b < 1024 * 1024 { format!("{:.1} KB", b as f64 / 1024.0) } else if b < 1024 * 1024 * 1024 { diff --git a/rom/src/lib.rs b/rom/src/lib.rs index ac668ed..5c7acc8 100644 --- a/rom/src/lib.rs +++ b/rom/src/lib.rs @@ -1,4 +1,5 @@ //! ROM - Rust Output Monitor +pub mod cache; pub mod cli; pub mod display; pub mod error; diff --git a/rom/src/monitor.rs b/rom/src/monitor.rs index e188c00..5c1f30a 100644 --- a/rom/src/monitor.rs +++ b/rom/src/monitor.rs @@ -5,10 +5,21 @@ use std::{ time::Duration, }; +use cognos::Host; +use tracing::debug; + use crate::{ - display::{Display, DisplayConfig}, + cache::BuildReportCache, + display::{Display, DisplayConfig, LegendStyle, SummaryStyle}, error::{Result, RomError}, - state::State, + state::{ + BuildStatus, + Derivation, + FailType, + State, + StorePath, + StorePathState, + }, types::{Config, InputMode}, update, }; @@ -24,15 +35,15 @@ impl Monitor { /// Create a new monitor pub fn new(config: Config, writer: W) -> Result { let legend_style = match config.legend_style.to_lowercase().as_str() { - "compact" => crate::display::LegendStyle::Compact, - "verbose" => crate::display::LegendStyle::Verbose, - _ => crate::display::LegendStyle::Table, + "compact" => LegendStyle::Compact, + "verbose" => LegendStyle::Verbose, + _ => LegendStyle::Table, }; let summary_style = match config.summary_style.to_lowercase().as_str() { - "table" => crate::display::SummaryStyle::Table, - "full" => crate::display::SummaryStyle::Full, - _ => crate::display::SummaryStyle::Concise, + "table" => SummaryStyle::Table, + "full" => SummaryStyle::Full, + _ => SummaryStyle::Concise, }; let display_config = DisplayConfig { @@ -46,7 +57,12 @@ impl Monitor { }; let display = Display::new(writer, display_config)?; - let state = State::new(); + let mut state = State::new(); + + // Load build cache for predictions + let cache_path = BuildReportCache::default_cache_path(); + let cache = BuildReportCache::new(cache_path); + state.build_cache = cache.load(); Ok(Self { state, @@ -82,6 +98,14 @@ impl Monitor { self.display.render_final(&self.state)?; } + // Save build cache for future predictions + let cache_path = BuildReportCache::default_cache_path(); + let cache = BuildReportCache::new(cache_path); + if let Err(e) = cache.save(&self.state.build_cache) { + debug!("Failed to save build cache: {}", e); + // Don't fail the build if cache save fails + } + // Return error code if there were failures if self.state.has_errors() { return Err(RomError::BuildFailed); @@ -111,7 +135,7 @@ impl Monitor { Ok(action) => { // Handle message passthrough - print directly to stdout if let cognos::Actions::Message { msg, .. } = &action { - println!("{}", msg); + println!("{msg}"); } let changed = update::process_message(&mut self.state, action); @@ -125,17 +149,13 @@ impl Monitor { } } else { // Non-JSON lines in JSON mode are passed through - println!("{}", line); + println!("{line}"); Ok(false) } } /// Process a human-readable line fn process_human_line(&mut self, line: &str) -> Result { - // Parse human-readable nix output - // This is a simplified version - the full implementation would need - // comprehensive parsing of nix's output format - let line = line.trim(); // Skip empty lines @@ -152,7 +172,7 @@ impl Monitor { let build_info = crate::state::BuildInfo { start: now, - host: crate::state::Host::Localhost, + host: Host::Localhost, estimate: None, activity_id: None, }; @@ -173,20 +193,21 @@ impl Monitor { let path_id = self.state.get_or_create_store_path_id(path); let now = crate::state::current_time(); + // Try to extract byte size from the message + let total_bytes = extract_byte_size(line); + let transfer = crate::state::TransferInfo { - start: now, - host: crate::state::Host::Localhost, - activity_id: 0, // No activity ID in human mode + start: now, + host: Host::Localhost, + activity_id: 0, // no activity ID in human mode bytes_transferred: 0, - total_bytes: None, + total_bytes, }; if let Some(path_info) = self.state.get_store_path_info_mut(path_id) { path_info .states - .insert(crate::state::StorePathState::Downloading( - transfer.clone(), - )); + .insert(StorePathState::Downloading(transfer.clone())); } self @@ -200,14 +221,113 @@ impl Monitor { } } + // Detect download completions with byte sizes + if line.starts_with("downloaded") || line.contains("downloaded '") { + if let Some(path_str) = extract_path_from_message(line) { + if let Some(path) = StorePath::parse(&path_str) { + if let Some(&path_id) = self.state.store_path_ids.get(&path) { + let now = crate::state::current_time(); + let total_bytes = extract_byte_size(line).unwrap_or(0); + + // Get start time from running download if it exists + let start = self + .state + .full_summary + .running_downloads + .get(&path_id) + .map_or(now, |t| t.start); + + let completed = crate::state::CompletedTransferInfo { + start, + end: now, + host: Host::Localhost, + total_bytes, + }; + + if let Some(path_info) = self.state.get_store_path_info_mut(path_id) + { + path_info + .states + .insert(StorePathState::Downloaded(completed.clone())); + } + + self.state.full_summary.running_downloads.remove(&path_id); + self + .state + .full_summary + .completed_downloads + .insert(path_id, completed); + + return Ok(true); + } + } + } + } + + // Detect "checking outputs of" messages + if line.contains("checking outputs of") { + if let Some(drv_path) = extract_path_from_message(line) { + if let Some(drv) = crate::state::Derivation::parse(&drv_path) { + let drv_id = self.state.get_or_create_derivation_id(drv); + // Just mark it as "touched" - checking happens after build + // Reminds me of Sako... + self.state.touched_ids.insert(drv_id); + return Ok(true); + } + } + } + + // Detect "copying N paths" messages + if line.starts_with("copying") && line.contains("paths") { + // Extract number of paths if present + let words: Vec<&str> = line.split_whitespace().collect(); + if words.len() >= 2 { + if let Ok(count) = words[1].parse::() { + debug!("Copying {} paths", count); + return Ok(true); + } + } + } + // Detect errors if line.starts_with("error:") || line.contains("error:") { self.state.nix_errors.push(line.to_string()); - return Ok(true); - } - // Detect build completions - if line.starts_with("built") || line.contains("built '") { + // Try to determine the error type and associated derivation + let fail_type = if line.contains("hash mismatch") + || line.contains("output path") + && (line.contains("hash") || line.contains("differs")) + { + FailType::HashMismatch + } else if line.contains("timed out") || line.contains("timeout") { + FailType::Timeout + } else if line.contains("dependency failed") + || line.contains("dependencies failed") + { + FailType::DependencyFailed + } else if line.contains("builder for") + && line.contains("failed with exit code") + { + // Try to extract exit code + if let Some(code_pos) = line.find("exit code") { + let after_code = &line[code_pos + 10..]; + let code_str = after_code + .split_whitespace() + .next() + .map(|s| s.trim_end_matches(|c: char| !c.is_ascii_digit())); + if let Some(code) = code_str.and_then(|s| s.parse::().ok()) { + FailType::BuildFailed(code) + } else { + FailType::Unknown + } + } else { + FailType::Unknown + } + } else { + FailType::Unknown + }; + + // Try to find the associated derivation and mark it as failed if let Some(drv_path) = extract_path_from_message(line) { if let Some(drv) = crate::state::Derivation::parse(&drv_path) { if let Some(&drv_id) = self.state.derivation_ids.get(&drv) { @@ -218,11 +338,35 @@ impl Monitor { let now = crate::state::current_time(); self.state.update_build_status( drv_id, - crate::state::BuildStatus::Built { + crate::state::BuildStatus::Failed { info: build_info.clone(), - end: now, + fail: crate::state::BuildFail { + at: now, + fail_type: fail_type.clone(), + }, }, ); + } + } + } + } + } + + return Ok(true); + } + + // Detect build completions + if line.starts_with("built") || line.contains("built '") { + if let Some(drv_path) = extract_path_from_message(line) { + if let Some(drv) = Derivation::parse(&drv_path) { + if let Some(&drv_id) = self.state.derivation_ids.get(&drv) { + if let Some(info) = self.state.get_derivation_info(drv_id) { + if let BuildStatus::Building(build_info) = &info.build_status { + let now = crate::state::current_time(); + self.state.update_build_status(drv_id, BuildStatus::Built { + info: build_info.clone(), + end: now, + }); return Ok(true); } } @@ -270,6 +414,33 @@ fn extract_path_from_message(line: &str) -> Option { None } +/// Extract byte size from a message line (e.g., "downloaded 123 KiB") +fn extract_byte_size(line: &str) -> Option { + // Look for patterns like "123 KiB", "6.7 MiB", etc. + // Haha 6.7 + let words: Vec<&str> = line.split_whitespace().collect(); + for (i, word) in words.iter().enumerate() { + if i + 1 < words.len() { + let unit = words[i + 1]; + if matches!(unit, "B" | "KiB" | "MiB" | "GiB" | "TiB" | "PiB") { + if let Ok(value) = word.parse::() { + let multiplier = match unit { + "B" => 1_u64, + "KiB" => 1024, + "MiB" => 1024 * 1024, + "GiB" => 1024 * 1024 * 1024, + "TiB" => 1024_u64 * 1024 * 1024 * 1024, + "PiB" => 1024_u64 * 1024 * 1024 * 1024 * 1024, + _ => 1, + }; + return Some((value * multiplier as f64) as u64); + } + } + } + } + None +} + #[cfg(test)] mod tests { use super::*; @@ -296,4 +467,19 @@ mod tests { let path = extract_path_from_message(line); assert!(path.is_some()); } + + #[test] + fn test_extract_byte_size() { + let line = "downloaded 123 KiB in 2 seconds"; + assert_eq!(extract_byte_size(line), Some(123 * 1024)); + + let line2 = "downloading 4.5 MiB"; + assert_eq!( + extract_byte_size(line2), + Some((4.5 * 1024.0 * 1024.0) as u64) + ); + + let line3 = "no size here"; + assert_eq!(extract_byte_size(line3), None); + } } diff --git a/rom/src/state.rs b/rom/src/state.rs index 3cfae00..7f7fcfd 100644 --- a/rom/src/state.rs +++ b/rom/src/state.rs @@ -6,7 +6,7 @@ use std::{ time::{Duration, SystemTime}, }; -use cognos::Id; +use cognos::{Host, Id, OutputName, ProgressState}; use indexmap::IndexMap; /// Unique identifier for store paths @@ -18,36 +18,6 @@ pub type DerivationId = usize; /// Unique identifier for activities pub type ActivityId = Id; -/// Overall progress state -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum ProgressState { - JustStarted, - InputReceived, - Finished, -} - -/// Build host information -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum Host { - Localhost, - Remote(String), -} - -impl Host { - #[must_use] - pub const fn is_local(&self) -> bool { - matches!(self, Self::Localhost) - } - - #[must_use] - pub fn name(&self) -> &str { - match self { - Self::Localhost => "localhost", - Self::Remote(name) => name, - } - } -} - /// Store path representation #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct StorePath { @@ -111,37 +81,6 @@ impl Derivation { } } -/// Output name for derivations -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub enum OutputName { - Out, - Doc, - Dev, - Bin, - Info, - Lib, - Man, - Dist, - Other(String), -} - -impl OutputName { - #[must_use] - pub fn parse(name: &str) -> Self { - match name.to_lowercase().as_str() { - "out" => Self::Out, - "doc" => Self::Doc, - "dev" => Self::Dev, - "bin" => Self::Bin, - "info" => Self::Info, - "lib" => Self::Lib, - "man" => Self::Man, - "dist" => Self::Dist, - _ => Self::Other(name.to_string()), - } - } -} - /// Transfer information (download/upload) #[derive(Debug, Clone)] pub struct TransferInfo { @@ -378,6 +317,20 @@ pub struct ActivityStatus { pub text: String, pub parent: Option, pub phase: Option, + pub progress: Option, +} + +/// Activity progress for downloads/uploads/builds +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ActivityProgress { + /// Bytes completed + pub done: u64, + /// Total bytes expected + pub expected: u64, + /// Currently running transfers + pub running: u64, + /// Failed transfers + pub failed: u64, } /// Build report for caching @@ -407,6 +360,7 @@ pub struct State { pub full_summary: DependencySummary, pub forest_roots: Vec, pub build_reports: HashMap>, + pub build_cache: HashMap<(String, String), Vec>, pub start_time: f64, pub progress_state: ProgressState, pub store_path_ids: HashMap, @@ -415,8 +369,11 @@ pub struct State { pub activities: HashMap, pub nix_errors: Vec, pub build_logs: Vec, + pub traces: Vec, pub build_platform: Option, pub evaluation_state: EvalInfo, + pub builds_activity: Option, + pub success_tokens: u64, next_store_path_id: StorePathId, next_derivation_id: DerivationId, } @@ -436,6 +393,7 @@ impl State { full_summary: DependencySummary::default(), forest_roots: Vec::new(), build_reports: HashMap::new(), + build_cache: HashMap::new(), start_time: current_time(), progress_state: ProgressState::JustStarted, store_path_ids: HashMap::new(), @@ -444,8 +402,11 @@ impl State { activities: HashMap::new(), nix_errors: Vec::new(), build_logs: Vec::new(), + traces: Vec::new(), build_platform: None, evaluation_state: EvalInfo::default(), + builds_activity: None, + success_tokens: 0, next_store_path_id: 0, next_derivation_id: 0, } @@ -603,7 +564,7 @@ impl State { // Create output set let mut output_set = HashSet::new(); for output in outputs { - output_set.insert(parse_output_name(&output)); + output_set.insert(OutputName::parse(&output)); } // Add to parent's input derivations @@ -718,6 +679,130 @@ impl State { .map(|(id, info)| (*id, info)) .collect() } + + /// Check if a derivation has a platform mismatch + #[must_use] + pub fn has_platform_mismatch(&self, id: DerivationId) -> bool { + if let (Some(build_platform), Some(info)) = + (&self.build_platform, self.get_derivation_info(id)) + { + if let Some(drv_platform) = &info.platform { + return build_platform != drv_platform; + } + } + false + } + + /// Get all derivations with platform mismatches + #[must_use] + pub fn platform_mismatches(&self) -> Vec { + self + .derivation_infos + .keys() + .filter(|&&id| self.has_platform_mismatch(id)) + .copied() + .collect() + } + + /// Get the activity prefix for a given activity ID by walking up the parent + /// chain to find a Build activity and extracting its derivation name. + /// Returns a prefix like "hello> " suitable for prepending to log lines. + /// If `use_color` is true and stderr is a TTY, the prefix will be blue. + /// The `prefix_style` determines whether to use short (pname only), full, or + /// no prefix. + #[must_use] + pub fn get_activity_prefix( + &self, + activity_id: ActivityId, + prefix_style: &crate::types::LogPrefixStyle, + use_color: bool, + ) -> Option { + use cognos::Activities; + + use crate::types::LogPrefixStyle; + + // If prefix style is None, return empty string + if matches!(prefix_style, LogPrefixStyle::None) { + return Some(String::new()); + } + + let mut current_id = activity_id; + let max_depth = 10; // Prevent infinite loops + let mut depth = 0; + + while depth < max_depth { + if let Some(activity) = self.activities.get(¤t_id) { + // Check if this is a Build activity (type 105) + if activity.activity == Activities::Build as u8 { + // Extract derivation path from the text field + // The text field typically contains something like: + // "building '/nix/store/...-hello-2.10.drv'" + if let Some(drv) = extract_derivation_from_text(&activity.text) { + // Look up the DerivationInfo for this derivation + let drv_id = self.derivation_ids.get(&drv); + let name = if matches!(prefix_style, LogPrefixStyle::Short) { + // Try to use pname if available + if let Some(id) = drv_id { + if let Some(drv_info) = self.derivation_infos.get(id) { + if let Some(pname) = &drv_info.pname { + pname.clone() + } else { + drv.name.clone() + } + } else { + drv.name.clone() + } + } else { + drv.name.clone() + } + } else { + // Full style - use full derivation name + drv.name.clone() + }; + + // Apply color if requested and stderr is a TTY + let colored_name = if use_color + && std::io::IsTerminal::is_terminal(&std::io::stderr()) + { + format!("\x1b[34m{name}\x1b[0m") + } else { + name + }; + + return Some(format!("{colored_name}> ")); + } + } + + // Move to parent activity + if let Some(parent_id) = activity.parent { + if parent_id == 0 { + break; // Reached root + } + current_id = parent_id; + depth += 1; + } else { + break; + } + } else { + break; + } + } + + None + } +} + +/// Extract derivation from activity text like "building +/// '/nix/store/...-hello-2.10.drv'" Returns the Derivation object +fn extract_derivation_from_text(text: &str) -> Option { + // Look for .drv path in text + if let Some(start) = text.find("/nix/store/") { + if let Some(end) = text[start..].find(".drv") { + let drv_path = &text[start..start + end + 4]; // Include .drv + return Derivation::parse(drv_path); + } + } + None } #[must_use] @@ -728,20 +813,6 @@ pub fn current_time() -> f64 { .as_secs_f64() } -fn parse_output_name(name: &str) -> OutputName { - match name { - "out" => OutputName::Out, - "doc" => OutputName::Doc, - "dev" => OutputName::Dev, - "bin" => OutputName::Bin, - "info" => OutputName::Info, - "lib" => OutputName::Lib, - "man" => OutputName::Man, - "dist" => OutputName::Dist, - _ => OutputName::Other(name.to_string()), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/rom/src/types.rs b/rom/src/types.rs index d2f74b3..26eb8cd 100644 --- a/rom/src/types.rs +++ b/rom/src/types.rs @@ -11,6 +11,17 @@ pub enum DisplayFormat { Dashboard, } +/// Log prefix style for build logs +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum LogPrefixStyle { + /// Just package name (pname) + Short, + /// Full derivation name with version + Full, + /// No prefix + None, +} + /// Summary display style #[derive(Debug, Clone, PartialEq, Eq)] pub enum SummaryStyle { @@ -23,6 +34,7 @@ pub enum SummaryStyle { } impl SummaryStyle { + #[must_use] pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "concise" => Self::Concise, @@ -33,7 +45,20 @@ impl SummaryStyle { } } +impl LogPrefixStyle { + #[must_use] + pub fn from_str(s: &str) -> Self { + match s.to_lowercase().as_str() { + "short" => Self::Short, + "full" => Self::Full, + "none" => Self::None, + _ => Self::Short, + } + } +} + impl DisplayFormat { + #[must_use] pub fn from_str(s: &str) -> Self { match s.to_lowercase().as_str() { "tree" => Self::Tree, @@ -48,34 +73,40 @@ impl DisplayFormat { #[derive(Debug, Clone)] pub struct Config { /// Whether we're piping output through - pub piping: bool, + pub piping: bool, /// Silent mode - minimal output - pub silent: bool, + pub silent: bool, /// Input parsing mode - pub input_mode: InputMode, + pub input_mode: InputMode, /// Show completion times - pub show_timers: bool, + pub show_timers: bool, /// Terminal width override - pub width: Option, + pub width: Option, /// Display format - pub format: DisplayFormat, + pub format: DisplayFormat, /// Legend display style - pub legend_style: String, + pub legend_style: String, /// Summary display style - pub summary_style: String, + pub summary_style: String, + /// Log prefix style for build logs + pub log_prefix_style: LogPrefixStyle, + /// Maximum number of log lines to display (None = unlimited) + pub log_line_limit: Option, } impl Default for Config { fn default() -> Self { Self { - piping: false, - silent: false, - input_mode: InputMode::Human, - show_timers: true, - width: None, - format: DisplayFormat::Tree, - legend_style: "table".to_string(), - summary_style: "concise".to_string(), + piping: false, + silent: false, + input_mode: InputMode::Human, + show_timers: true, + width: None, + format: DisplayFormat::Tree, + legend_style: "table".to_string(), + summary_style: "concise".to_string(), + log_prefix_style: LogPrefixStyle::Short, + log_line_limit: None, } } } @@ -101,6 +132,8 @@ mod tests { assert_eq!(config.input_mode, InputMode::Human); assert!(config.show_timers); assert_eq!(config.format, DisplayFormat::Tree); + assert_eq!(config.log_prefix_style, LogPrefixStyle::Short); + assert_eq!(config.log_line_limit, None); } #[test] diff --git a/rom/src/update.rs b/rom/src/update.rs index 728555c..3e3a0a8 100644 --- a/rom/src/update.rs +++ b/rom/src/update.rs @@ -1,29 +1,31 @@ //! State update logic for processing nix messages -use cognos::{Actions, Activities, Id, Verbosity}; +use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity}; use tracing::{debug, trace}; -use crate::state::{ - ActivityStatus, - BuildFail, - BuildInfo, - BuildStatus, - CompletedBuildInfo, - CompletedTransferInfo, - Derivation, - DerivationId, - FailType, - FailedBuildInfo, - Host, - InputDerivation, - OutputName, - ProgressState, - State, - StorePath, - StorePathId, - StorePathState, - TransferInfo, - current_time, +use crate::{ + cache::BuildReportCache, + state::{ + ActivityProgress, + ActivityStatus, + BuildFail, + BuildInfo, + BuildReport, + BuildStatus, + CompletedBuildInfo, + CompletedTransferInfo, + Derivation, + DerivationId, + FailType, + FailedBuildInfo, + InputDerivation, + State, + StorePath, + StorePathId, + StorePathState, + TransferInfo, + current_time, + }, }; /// Process a nix JSON message and update state @@ -89,21 +91,38 @@ fn handle_start( text: text.clone(), parent: parent_id, phase: None, + progress: None, }); let changed = match activity_u8 { - 104 | 105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Builds | Build */ + 105 => handle_build_start(state, id, parent_id, &text, &fields, now), /* Build */ 108 => handle_substitute_start(state, id, &text, &fields, now), /* Substitute */ - 101 => handle_transfer_start(state, id, &text, &fields, now, false), /* FileTransfer */ - 100 | 103 => handle_transfer_start(state, id, &text, &fields, now, true), /* CopyPath | CopyPaths */ - _ => false, + 109 => handle_query_path_info_start(state, id, &text, &fields, now), /* QueryPathInfo */ + 110 => handle_post_build_hook_start(state, id, &text, &fields, now), /* PostBuildHook */ + 101 => handle_file_transfer_start(state, id, &text, &fields, now), /* FileTransfer */ + 100 => handle_copy_path_start(state, id, &text, &fields, now), /* CopyPath */ + 104 => { + // Builds activity - track this as the top-level builds activity + if state.builds_activity.is_none() { + state.builds_activity = Some(id); + true + } else { + false + } + }, + 102 | 103 | 106 | 107 | 111 | 112 => { + // Realise, CopyPaths, OptimiseStore, VerifyPaths, BuildWaiting, FetchTree + // These activities have no fields and are just tracked + true + }, + _ => { + debug!("Unknown activity type: {}", activity_u8); + false + }, }; // Track parent-child relationships for dependency tree - if changed - && (activity_u8 == 104 || activity_u8 == 105) - && parent_id.is_some() - { + if changed && activity_u8 == 105 && parent_id.is_some() { let parent_act_id = parent_id.unwrap(); // Find parent and child derivation IDs @@ -113,8 +132,8 @@ fn handle_start( if let Some(parent_drv_id) = parent_drv_id { if let Some(child_drv_id) = child_drv_id { debug!( - "Establishing parent-child relationship: parent={}, child={}", - parent_drv_id, child_drv_id + "Establishing parent-child relationship: parent={parent_drv_id}, \ + child={child_drv_id}" ); // Add child as a dependency of parent @@ -153,9 +172,19 @@ fn handle_stop(state: &mut State, id: Id, now: f64) -> bool { state.activities.remove(&id); match activity_status.activity { - 104 | 105 => handle_build_stop(state, id, now), // Builds | Build - 108 => handle_substitute_stop(state, id, now), // Substitute - 101 | 100 | 103 => handle_transfer_stop(state, id, now), /* FileTransfer, CopyPath, CopyPaths */ + 105 => handle_build_stop(state, id, now), // Build + 108 => handle_substitute_stop(state, id, now), // Substitute + 101 | 100 => handle_transfer_stop(state, id, now), // FileTransfer, + // CopyPath + 109 | 110 => { + // QueryPathInfo, PostBuildHook - just acknowledge stop + false + }, + 102 | 103 | 104 | 106 | 107 | 111 | 112 => { + // Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting, + // FetchTree + false + }, _ => false, } } else { @@ -169,7 +198,7 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool { // Extract phase from log messages like "Running phase: configurePhase" if let Some(phase_start) = msg.find("Running phase: ") { - let phase_name = &msg[phase_start + 15..]; // Skip "Running phase: " + let phase_name = &msg[phase_start + 15..]; // skip "Running phase: " let phase = phase_name.trim().to_string(); // Find the active build and update its phase @@ -231,6 +260,14 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool { } true // return true since we stored the log }, + Verbosity::Talkative + | Verbosity::Chatty + | Verbosity::Debug + | Verbosity::Vomit => { + // These are trace-level messages, store separately + state.traces.push(msg.clone()); + true + }, _ => { true // return true since we stored the log }, @@ -240,41 +277,184 @@ fn handle_message(state: &mut State, level: Verbosity, msg: String) -> bool { fn handle_result( state: &mut State, id: Id, - activity: u8, + result_type: u8, fields: Vec, _now: f64, ) -> bool { - match activity { - 101 | 108 => { - // FileTransfer or Substitute - // Fields contain progress information - // XXX: Format: [bytes_transferred, total_bytes] + // Result message types are DIFFERENT from Activity types + // Type 100: FileLinked (2 ints) + // Type 101: BuildLogLine (1 text) + // Type 102: UntrustedPath (1 text - store path) + // Type 103: CorruptedPath (1 text - store path) + // Type 104: SetPhase (1 text) + // Type 105: Progress (4 ints: done, expected, running, failed) + // Type 106: SetExpected (2 ints: activity type, count) + // Type 107: PostBuildLogLine (1 text) + // Type 108: FetchStatus (1 text) + + match result_type { + 100 => { + // FileLinked: 2 int fields (linked count, total count) if fields.len() >= 2 { - update_transfer_progress(state, id, &fields); + let linked = fields[0].as_u64().unwrap_or(0); + let total = fields[1].as_u64().unwrap_or(0); + debug!("FileLinked: {}/{}", linked, total); + // File linking is reported but doesn't need state tracking + } + false + }, + 101 => { + // BuildLogLine: 1 text field + if let Some(line) = fields.first().and_then(|f| f.as_str()) { + state.build_logs.push(line.to_string()); + return true; + } + false + }, + 102 => { + // UntrustedPath: 1 text field (store path) + if let Some(path_str) = fields.first().and_then(|f| f.as_str()) { + debug!("Untrusted path reported: {}", path_str); + state + .nix_errors + .push(format!("Untrusted path: {}", path_str)); + return true; + } + false + }, + 103 => { + // CorruptedPath: 1 text field (store path) + if let Some(path_str) = fields.first().and_then(|f| f.as_str()) { + state.nix_errors.push(format!("Corrupted path: {path_str}")); + return true; } false }, 104 => { - // Builds activity type - contains phase information - if !fields.is_empty() { - if let Some(phase_str) = fields[0].as_str() { - // Update the activity's phase field + // SetPhase: 1 text field + if let Some(phase_str) = fields.first().and_then(|f| f.as_str()) { + if let Some(activity) = state.activities.get_mut(&id) { + activity.phase = Some(phase_str.to_string()); + return true; + } + } + false + }, + 105 => { + // Progress: 4 int fields (done, expected, running, failed) + if fields.len() >= 4 { + if let (Some(done), Some(expected), Some(running), Some(failed)) = ( + fields[0].as_u64(), + fields[1].as_u64(), + fields[2].as_u64(), + fields[3].as_u64(), + ) { + // If this progress is for the Builds activity, track success tokens + if state.builds_activity == Some(id) { + if let Some(activity) = state.activities.get(&id) { + if let Some(prev_progress) = &activity.progress { + let new_done = done.saturating_sub(prev_progress.done); + if new_done > 0 { + state.success_tokens = + state.success_tokens.saturating_add(new_done); + } + } + } + } + if let Some(activity) = state.activities.get_mut(&id) { - activity.phase = Some(phase_str.to_string()); + activity.progress = Some(ActivityProgress { + done, + expected, + running, + failed, + }); return true; } } } false }, - 105 => { - // Build completed, fields contain output path - complete_build(state, id) + 106 => { + // SetExpected: 2 int fields (activity type, count) + if fields.len() >= 2 { + let activity_type = fields[0].as_u64().unwrap_or(0); + let expected_count = fields[1].as_u64().unwrap_or(0); + debug!( + "SetExpected: activity_type={}, count={}", + activity_type, expected_count + ); + // Expected counts are informational and don't affect state tracking + } + false + }, + 107 => { + // PostBuildLogLine: 1 text field + if let Some(line) = fields.first().and_then(|f| f.as_str()) { + state.build_logs.push(format!("[post-build] {line}")); + return true; + } + false + }, + 108 => { + // FetchStatus: 1 text field + if let Some(status) = fields.first().and_then(|f| f.as_str()) { + debug!("Fetch status: {}", status); + // Fetch status is informational + } + false + }, + _ => { + debug!("Unknown result type: {}", result_type); + false }, - _ => false, } } +/// Get build time estimate from cache +fn get_build_estimate( + state: &State, + derivation_name: &str, + host: &Host, +) -> Option { + // Use pname if available, otherwise derivation name + let lookup_name = derivation_name.to_string(); + let host_str = host.name(); + + BuildReportCache::calculate_median( + state + .build_cache + .get(&(host_str.to_string(), lookup_name))? + .as_slice(), + ) +} + +/// Record completed build for future predictions +fn record_build_completion( + state: &mut State, + derivation_name: String, + platform: Option, + start: f64, + end: f64, + host: &Host, +) { + let duration_secs = end - start; + let completed_at = std::time::SystemTime::now(); + + let report = BuildReport { + derivation_name: derivation_name.clone(), + platform: platform.unwrap_or_default(), + duration_secs, + completed_at, + host: host.name().to_string(), + success: true, + }; + + // Store in state for later CSV persistence + let key = (host.name().to_string(), derivation_name); + state.build_cache.entry(key).or_default().push(report); +} + fn handle_build_start( state: &mut State, id: Id, @@ -298,13 +478,16 @@ fn handle_build_start( if let Some(drv_path) = drv_path { debug!("Extracted derivation path: {}", drv_path); if let Some(drv) = Derivation::parse(&drv_path) { - let drv_id = state.get_or_create_derivation_id(drv); + let drv_id = state.get_or_create_derivation_id(drv.clone()); let host = extract_host(text); + // Get build time estimate from cache + let estimate = get_build_estimate(state, &drv.name, &host); + let build_info = BuildInfo { start: now, host, - estimate: None, + estimate, activity_id: Some(id), }; @@ -323,74 +506,59 @@ fn handle_build_start( ); // Mark as forest root if no parent - // Only add to forest roots if no parent if parent_id.is_none() && !state.forest_roots.contains(&drv_id) { state.forest_roots.push(drv_id); } - // Store activity -> derivation mapping - // Phase will be extracted from log messages return true; } debug!("Failed to parse derivation from path: {}", drv_path); } else { debug!( - "No derivation path found - creating placeholder for activity {}", + "No derivation path in fields for Build activity {} - this should not \ + happen", id ); - // For shell/develop commands, nix doesn't report specific derivation paths - // Create a placeholder derivation to track that builds are happening - use std::path::PathBuf; - - let placeholder_name = format!("building-{}", id); - let placeholder_path = format!("/nix/store/placeholder-{}.drv", id); - - let placeholder_drv = Derivation { - path: PathBuf::from(placeholder_path), - name: placeholder_name, - }; - - let drv_id = state.get_or_create_derivation_id(placeholder_drv); - let host = extract_host(text); - - let build_info = BuildInfo { - start: now, - host, - estimate: None, - activity_id: Some(id), - }; - - debug!( - "Setting placeholder derivation {} to Building status", - drv_id - ); - state.update_build_status(drv_id, BuildStatus::Building(build_info)); - - // Mark as forest root if no parent - if parent_id.is_none() && !state.forest_roots.contains(&drv_id) { - state.forest_roots.push(drv_id); - } - - return true; } false } -fn handle_build_stop(state: &mut State, id: Id, _now: f64) -> bool { - // Find the derivation associated with this activity - for (drv_id, info) in &state.derivation_infos { - match &info.build_status { - BuildStatus::Building(build_info) - if build_info.activity_id == Some(id) => - { - // Build was stopped but not marked as completed - // It might be cancelled - debug!("Build stopped for derivation {}", drv_id); - return false; - }, - _ => {}, +fn handle_build_stop(state: &mut State, id: Id, now: f64) -> bool { + // Check if we have success tokens to consume + if state.success_tokens > 0 { + // Find the derivation associated with this activity + for (drv_id, info) in state.derivation_infos.clone().iter() { + if let BuildStatus::Building(build_info) = &info.build_status { + if build_info.activity_id == Some(id) { + // Consume a success token and mark build as complete + state.success_tokens = state.success_tokens.saturating_sub(1); + state.update_build_status(*drv_id, BuildStatus::Built { + info: build_info.clone(), + end: now, + }); + + // Record build completion for future predictions + record_build_completion( + state, + info.name.name.clone(), + info.platform.clone(), + build_info.start, + now, + &build_info.host, + ); + + debug!( + "Build completed for derivation {} (success_tokens: {})", + drv_id, state.success_tokens + ); + return true; + } + } } } + + // No success tokens - build was stopped without completion signal + debug!("Build stopped for activity {} without success token", id); false } @@ -477,45 +645,111 @@ fn handle_substitute_stop(state: &mut State, id: Id, now: f64) -> bool { false } -fn handle_transfer_start( +fn handle_file_transfer_start( + _state: &mut State, + id: Id, + _text: &str, + fields: &[serde_json::Value], + _now: f64, +) -> bool { + // FileTransfer expects 1 text field: URL or description + if fields.is_empty() { + debug!("FileTransfer activity {} has no fields", id); + return false; + } + + // Just track the activity, actual progress comes via Result messages + true +} + +fn handle_copy_path_start( state: &mut State, id: Id, - text: &str, + _text: &str, fields: &[serde_json::Value], now: f64, - is_copy: bool, ) -> bool { - let path_str = if fields.is_empty() { - extract_store_path(text) - } else { - fields[0].as_str().map(std::string::ToString::to_string) - }; + // CopyPath expects 3 text fields: path, from, to + if fields.len() < 3 { + debug!("CopyPath activity {} has insufficient fields", id); + return false; + } - if let Some(path_str) = path_str { - if let Some(path) = StorePath::parse(&path_str) { + let path_str = fields[0].as_str(); + let _from_host = fields[1].as_str().map(|s| { + if s.is_empty() || s == "localhost" { + Host::Localhost + } else { + Host::Remote(s.to_string()) + } + }); + let to_host = fields[2].as_str().map(|s| { + if s.is_empty() || s == "localhost" { + Host::Localhost + } else { + Host::Remote(s.to_string()) + } + }); + + if let (Some(path_str), Some(to)) = (path_str, to_host) { + if let Some(path) = StorePath::parse(path_str) { let path_id = state.get_or_create_store_path_id(path); - let host = extract_host(text); let transfer = TransferInfo { - start: now, - host, - activity_id: id, + start: now, + host: to, // destination host + activity_id: id, bytes_transferred: 0, - total_bytes: None, + total_bytes: None, }; - if is_copy { - state.full_summary.running_uploads.insert(path_id, transfer); - } else { - state - .full_summary - .running_downloads - .insert(path_id, transfer); - } - + // CopyPath is an upload from 'from' to 'to' + state.full_summary.running_uploads.insert(path_id, transfer); return true; } } + + false +} + +fn handle_query_path_info_start( + _state: &mut State, + id: Id, + _text: &str, + fields: &[serde_json::Value], + _now: f64, +) -> bool { + // QueryPathInfo expects 2 text fields: path, host + if fields.len() < 2 { + debug!("QueryPathInfo activity {} has insufficient fields", id); + return false; + } + + // Just track the activity + true +} + +fn handle_post_build_hook_start( + _state: &mut State, + id: Id, + _text: &str, + fields: &[serde_json::Value], + _now: f64, +) -> bool { + // PostBuildHook expects 1 text field: derivation path + if fields.is_empty() { + debug!("PostBuildHook activity {} has no fields", id); + return false; + } + + let drv_path = fields[0].as_str(); + if let Some(drv_path) = drv_path { + if let Some(_drv) = Derivation::parse(drv_path) { + // Just track that the hook is running + return true; + } + } + false } @@ -564,54 +798,6 @@ fn handle_transfer_stop(state: &mut State, id: Id, now: f64) -> bool { false } -fn update_transfer_progress( - state: &mut State, - id: Id, - fields: &[serde_json::Value], -) { - if fields.len() < 2 { - return; - } - - let bytes_transferred = fields[0].as_u64().unwrap_or(0); - let total_bytes = fields[1].as_u64(); - - // Update running downloads - for transfer_info in state.full_summary.running_downloads.values_mut() { - if transfer_info.activity_id == id { - transfer_info.bytes_transferred = bytes_transferred; - transfer_info.total_bytes = total_bytes; - return; - } - } - - // Update running uploads - for transfer_info in state.full_summary.running_uploads.values_mut() { - if transfer_info.activity_id == id { - transfer_info.bytes_transferred = bytes_transferred; - transfer_info.total_bytes = total_bytes; - return; - } - } -} - -fn complete_build(state: &mut State, id: Id) -> bool { - // Find the derivation that just completed - for (drv_id, info) in &state.derivation_infos.clone() { - if let BuildStatus::Building(build_info) = &info.build_status { - if build_info.activity_id == Some(id) { - let end = current_time(); - state.update_build_status(*drv_id, BuildStatus::Built { - info: build_info.clone(), - end, - }); - return true; - } - } - } - false -} - fn extract_derivation_path(text: &str) -> Option { // Look for .drv paths in the text if let Some(start) = text.find("/nix/store/") { @@ -886,18 +1072,3 @@ pub fn finish_state(state: &mut State) { } } } - -/// Parse output name string to `OutputName` enum -fn parse_output_name(s: &str) -> Option { - match s { - "out" => Some(OutputName::Out), - "doc" => Some(OutputName::Doc), - "dev" => Some(OutputName::Dev), - "bin" => Some(OutputName::Bin), - "info" => Some(OutputName::Info), - "lib" => Some(OutputName::Lib), - "man" => Some(OutputName::Man), - "dist" => Some(OutputName::Dist), - other => Some(OutputName::Other(other.to_string())), - } -}