diff --git a/Cargo.lock b/Cargo.lock index 81a43f1..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.51" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c26d721170e0295f191a69bd9a1f93efcdb0aff38684b61ab5750468972e5f5" +checksum = "6899ea499e3fb9305a65d5ebf6e3d2248c5fab291f300ad0a704fbe142eae31a" dependencies = [ "clap_builder", "clap_derive", @@ -118,9 +156,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.51" +version = "4.5.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75835f0c7bf681bfd05abe44e965760fea999a5286c6eb2d59883634fd02011a" +checksum = "7b12c8b680195a62a8364d16b8447b01b6c2c8f9aaf68bee653be34d4245e238" dependencies = [ "anstream", "anstyle", @@ -130,9 +168,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.49" +version = "4.5.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" +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", @@ -241,18 +285,18 @@ dependencies = [ [[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]] @@ -388,9 +432,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.16.0" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5419bdc4f6a9207fbeba6d11b604d481addf78ecd10c11ad51e76c2f6482748d" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" [[package]] name = "heck" @@ -398,6 +442,30 @@ 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" @@ -412,27 +480,30 @@ checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" [[package]] name = "indexmap" -version = "2.12.0" +version = "2.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6717a8d2a5a929a1a2eb43a12812498ed141a0bcfb7e8f7844fbdbe4303bba9f" +checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017" dependencies = [ "equivalent", - "hashbrown 0.16.0", + "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", @@ -443,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" @@ -458,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" @@ -470,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" @@ -482,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" @@ -528,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]] @@ -561,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", @@ -577,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]] @@ -602,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", ] @@ -647,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", @@ -667,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", @@ -691,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" @@ -706,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]] @@ -725,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" @@ -735,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" @@ -767,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]] @@ -798,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" @@ -810,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", @@ -821,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", ] @@ -870,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", @@ -881,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", @@ -923,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", @@ -934,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", @@ -945,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", @@ -966,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", @@ -984,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" @@ -1036,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" @@ -1067,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]] @@ -1087,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]] @@ -1105,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]] @@ -1138,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" @@ -1223,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 00288d0..5456e9a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ version = "0.1.0" edition = "2024" authors = ["NotAShelf "] description = "Pretty build graphs for Nix builds" -rust-version = "1.85" +rust-version = "1.91.1" [workspace.dependencies] anyhow = "1.0.100" @@ -19,6 +19,7 @@ crossterm = "0.29.0" ratatui = "0.29.0" 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/rom/Cargo.toml b/rom/Cargo.toml index 979e965..6affa5b 100644 --- a/rom/Cargo.toml +++ b/rom/Cargo.toml @@ -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 98ecaef..e6508e7 100644 --- a/rom/src/cli.rs +++ b/rom/src/cli.rs @@ -33,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)] @@ -86,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(()) }, @@ -101,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(()) }, @@ -110,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(); @@ -140,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(()) }, @@ -149,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(); @@ -179,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(()) }, @@ -188,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(); @@ -218,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(()) }, @@ -239,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(); @@ -280,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() { @@ -310,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); @@ -325,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() { @@ -360,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 { @@ -391,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 @@ -419,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 { @@ -450,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}, @@ -481,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; @@ -495,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: {} -> {}", @@ -518,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!( @@ -583,13 +688,16 @@ fn run_monitored_command( || !state.full_summary.planned_builds.is_empty(); if !silent { + // 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 d8cb720..1e0b26e 100644 --- a/rom/src/display.rs +++ b/rom/src/display.rs @@ -114,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()); } @@ -153,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()?; @@ -180,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}")?; @@ -207,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), @@ -223,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 @@ -680,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 )); } } @@ -954,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, 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 18e23b7..5c1f30a 100644 --- a/rom/src/monitor.rs +++ b/rom/src/monitor.rs @@ -1,12 +1,15 @@ //! Monitor module for orchestrating state updates and display rendering + use std::{ io::{BufRead, Write}, time::Duration, }; use cognos::Host; +use tracing::debug; use crate::{ + cache::BuildReportCache, display::{Display, DisplayConfig, LegendStyle, SummaryStyle}, error::{Result, RomError}, state::{ @@ -54,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, @@ -90,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); @@ -140,10 +156,6 @@ impl Monitor { /// 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 @@ -270,10 +282,8 @@ impl Monitor { // 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::() { - // XXX: This is a PlanCopies message, we'll probably track this - // For now just acknowledge it, and let future work decide how - // we should go around doing it. + if let Ok(count) = words[1].parse::() { + debug!("Copying {} paths", count); return Ok(true); } } diff --git a/rom/src/state.rs b/rom/src/state.rs index cc47ee7..7f7fcfd 100644 --- a/rom/src/state.rs +++ b/rom/src/state.rs @@ -360,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, @@ -371,6 +372,8 @@ pub struct State { 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, } @@ -390,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(), @@ -401,6 +405,8 @@ impl State { 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, } @@ -697,6 +703,106 @@ impl State { .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] diff --git a/rom/src/types.rs b/rom/src/types.rs index b355ec9..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 { @@ -34,6 +45,18 @@ 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 { @@ -50,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, } } } @@ -103,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 0524dfb..3e3a0a8 100644 --- a/rom/src/update.rs +++ b/rom/src/update.rs @@ -3,25 +3,29 @@ use cognos::{Actions, Activities, Host, Id, ProgressState, Verbosity}; use tracing::{debug, trace}; -use crate::state::{ - ActivityProgress, - ActivityStatus, - BuildFail, - BuildInfo, - BuildStatus, - CompletedBuildInfo, - CompletedTransferInfo, - Derivation, - DerivationId, - FailType, - FailedBuildInfo, - InputDerivation, - 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 @@ -96,10 +100,19 @@ fn handle_start( 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 - 102 | 103 | 104 | 106 | 107 | 111 | 112 => { - // Realise, CopyPaths, Builds, OptimiseStore, VerifyPaths, BuildWaiting, - // FetchTree These activities have no fields and are just tracked + 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 }, _ => { @@ -281,11 +294,12 @@ fn handle_result( match result_type { 100 => { - // FileLinked: 2 int fields + // FileLinked: 2 int fields (linked count, total count) if fields.len() >= 2 { - let _linked = fields[0].as_u64(); - let _total = fields[1].as_u64(); - // TODO: Track file linking progress + 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 }, @@ -300,17 +314,18 @@ fn handle_result( 102 => { // UntrustedPath: 1 text field (store path) if let Some(path_str) = fields.first().and_then(|f| f.as_str()) { - debug!("Untrusted path: {}", path_str); - // TODO: Track untrusted paths + 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}")); + state.nix_errors.push(format!("Corrupted path: {path_str}")); return true; } false @@ -334,6 +349,19 @@ fn handle_result( 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.progress = Some(ActivityProgress { done, @@ -350,9 +378,13 @@ fn handle_result( 106 => { // SetExpected: 2 int fields (activity type, count) if fields.len() >= 2 { - let _activity_type = fields[0].as_u64(); - let _expected_count = fields[1].as_u64(); - // TODO: Track expected counts + 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 }, @@ -368,7 +400,7 @@ fn handle_result( // FetchStatus: 1 text field if let Some(status) = fields.first().and_then(|f| f.as_str()) { debug!("Fetch status: {}", status); - // TODO: Track fetch status + // Fetch status is informational } false }, @@ -379,6 +411,50 @@ fn handle_result( } } +/// 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, @@ -402,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), }; @@ -444,21 +523,42 @@ fn handle_build_start( 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 }