diff --git a/Cargo.lock b/Cargo.lock index 7b10766..d2a7a07 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -86,6 +86,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + [[package]] name = "backtrace" version = "0.3.69" @@ -101,6 +107,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base64" +version = "0.21.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35636a1494ede3b646cc98f74f8e62c773a38a659ebc777a2cf26b9b74171df9" + [[package]] name = "bitflags" version = "1.3.2" @@ -113,6 +125,18 @@ version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +[[package]] +name = "bumpalo" +version = "3.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f30e7476521f6f8af1a1c4c0b8cc94f0bee37d91763d0ca2665f299b6cd8aec" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" + [[package]] name = "cc" version = "1.0.83" @@ -128,6 +152,16 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "num-traits", + "serde", +] + [[package]] name = "clap" version = "4.4.10" @@ -202,17 +236,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" -[[package]] -name = "colored" -version = "1.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f741c91823341bebf717d4c71bda820630ce065443b58bd1b7451af008355" -dependencies = [ - "is-terminal", - "lazy_static", - "winapi", -] - [[package]] name = "console" version = "0.15.7" @@ -226,6 +249,22 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" + [[package]] name = "darling" version = "0.14.4" @@ -308,12 +347,39 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "elasticsearch-dsl" +version = "0.4.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7066057f05cce0d1c6c7cdbd1e99a83f903bad35faf070cd86e34aa5bc504feb" +dependencies = [ + "chrono", + "num-traits", + "serde", + "serde_json", +] + [[package]] name = "encode_unicode" version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + [[package]] name = "errno" version = "0.3.8" @@ -341,20 +407,78 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" [[package]] -name = "fern" -version = "0.6.2" +name = "fnv" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9f0c14694cbd524c8720dd69b0e3179344f04ebb5f90f2e4a440c6ea3b2f1ee" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ - "colored", - "log", + "percent-encoding", ] [[package]] -name = "fnv" -version = "1.0.7" +name = "futures-channel" +version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +checksum = "ff4dd66668b557604244583e3e1e1eada8c5c2e96a6d0d6653ede395b78bbacb" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" + +[[package]] +name = "futures-io" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8bf34a163b5c4c52d0478a4d757da8fb65cabef42ba90515efee0f6f9fa45aaa" + +[[package]] +name = "futures-sink" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e36d3378ee38c2a36ad710c5d30c2911d752cb941c00c72dbabfb786a7970817" + +[[package]] +name = "futures-task" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "efd193069b0ddadc69c46389b740bbccdd97203899b48d09c5f7969591d6bae2" + +[[package]] +name = "futures-util" +version = "0.3.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a19526d624e703a3179b3d322efec918b6246ea0fa51d41124525f00f1cc8104" +dependencies = [ + "futures-core", + "futures-io", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe9006bed769170c11f845cf00c7c1e9092aeb3f268e007c3e760ac68008070f" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] [[package]] name = "gimli" @@ -362,6 +486,31 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +[[package]] +name = "h2" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d6250322ef6e60f93f9a2162799302cd6f68f79f6e5d85c8c16f14d1d958178" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" + [[package]] name = "heck" version = "0.4.1" @@ -385,18 +534,100 @@ dependencies = [ "winapi", ] +[[package]] +name = "http" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + [[package]] name = "humantime" version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" +[[package]] +name = "hyper" +version = "0.14.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http", + "hyper", + "rustls", + "tokio", + "tokio-rustls", +] + [[package]] name = "ident_case" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indenter" version = "0.3.3" @@ -404,16 +635,32 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" [[package]] -name = "is-terminal" -version = "0.4.9" +name = "indexmap" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" +checksum = "d530e1a18b1cb4c484e6e34556a0d948706958449fca0cab753d649f2bce3d1f" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ "hermit-abi", - "rustix", + "libc", "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "itertools" version = "0.10.5" @@ -429,6 +676,15 @@ version = "1.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" +[[package]] +name = "js-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cee9c64da59eae3b50095c18d3e74f8b73c0b86d2792824ff01bbce68ba229ca" +dependencies = [ + "wasm-bindgen", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -441,6 +697,12 @@ version = "0.2.150" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +[[package]] +name = "linux-raw-sys" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef53942eb7bf7ff43a617b3e2c1c4a5ecf5944a7c1bc12d7ee39bbb15e5c1519" + [[package]] name = "linux-raw-sys" version = "0.4.12" @@ -459,12 +721,27 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + [[package]] name = "memchr" version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -474,9 +751,20 @@ dependencies = [ "adler", ] +[[package]] +name = "mio" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f3d0b296e374a4e6f3c7b0a1f5a51d748a0d34c85e7dc48fc3fa9a87657fe09" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "nh" -version = "3.4.15" +version = "3.5.0" dependencies = [ "ambassador", "anstyle", @@ -486,19 +774,24 @@ dependencies = [ "color-eyre", "derive_builder", "dialoguer", - "fern", + "elasticsearch-dsl", "hostname", "humantime", - "log", "nix", "once_cell", + "owo-colors", "regex", + "reqwest", "serde", "serde_json", "subprocess", "tempfile", + "textwrap", "thiserror", "timeago", + "tracing", + "tracing-subscriber", + "uzers", ] [[package]] @@ -512,6 +805,35 @@ dependencies = [ "libc", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" +dependencies = [ + "hermit-abi", + "libc", +] + [[package]] name = "object" version = "0.32.1" @@ -527,12 +849,36 @@ version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "owo-colors" version = "3.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pin-project-lite" +version = "0.2.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "proc-macro2" version = "1.0.70" @@ -568,8 +914,17 @@ checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" dependencies = [ "aho-corasick", "memchr", - "regex-automata", - "regex-syntax", + "regex-automata 0.4.3", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", ] [[package]] @@ -580,21 +935,95 @@ checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" dependencies = [ "aho-corasick", "memchr", - "regex-syntax", + "regex-syntax 0.8.2", ] +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + [[package]] name = "regex-syntax" version = "0.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +[[package]] +name = "reqwest" +version = "0.11.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b1ae8d9ac08420c66222fb9096fc5de435c3c48542bc5336c51892cffafb41" +dependencies = [ + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-rustls", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tokio-rustls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", +] + +[[package]] +name = "ring" +version = "0.17.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "688c63d65483050968b2a8937f7995f443e27041a0f7700aa59b0822aedebb74" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.48.0", +] + [[package]] name = "rustc-demangle" version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustix" +version = "0.37.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea8ca367a3a01fe35e6943c400addf443c0f57670e6ec51196f71a4b8762dd2" +dependencies = [ + "bitflags 1.3.2", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys 0.3.8", + "windows-sys 0.48.0", +] + [[package]] name = "rustix" version = "0.38.26" @@ -604,16 +1033,57 @@ dependencies = [ "bitflags 2.4.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.4.12", "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.21.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "ryu" version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741" +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + [[package]] name = "serde" version = "1.0.193" @@ -645,12 +1115,70 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "shell-words" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24188a676b6ae68c3b2cb3a01be17fbf7240ce009799bb56d5b1409051e78fde" +[[package]] +name = "slab" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dccd0940a2dcdf68d092b8cbab7dc0ad8fa938bf95787e1b916b0e3d0e8e970" + +[[package]] +name = "smawk" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c" + +[[package]] +name = "socket2" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b5fac59a5cb5dd637972e5fca70daf0523c9067fcdc4842f053dae04a18f8e9" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + [[package]] name = "strsim" version = "0.10.0" @@ -689,6 +1217,27 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tempfile" version = "3.8.1" @@ -698,10 +1247,32 @@ dependencies = [ "cfg-if", "fastrand", "redox_syscall", - "rustix", + "rustix 0.38.26", + "windows-sys 0.48.0", +] + +[[package]] +name = "terminal_size" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e6bf6f19e9f8ed8d4048dc22981458ebcf406d67e94cd422e5ecd73d63b3237" +dependencies = [ + "rustix 0.37.27", "windows-sys 0.48.0", ] +[[package]] +name = "textwrap" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" +dependencies = [ + "smawk", + "terminal_size", + "unicode-linebreak", + "unicode-width", +] + [[package]] name = "thiserror" version = "1.0.50" @@ -722,30 +1293,318 @@ dependencies = [ "syn 2.0.39", ] +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "timeago" version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1710e589de0a76aaf295cd47a6699f6405737dbfd3cf2b75c92d000b548d0e6" +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.35.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +dependencies = [ + "backtrace", + "bytes", + "libc", + "mio", + "num_cpus", + "pin-project-lite", + "socket2", + "windows-sys 0.48.0", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +dependencies = [ + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "tracing-core" +version = "0.1.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicode-bidi" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f2528f27a9eb2b21e69c95319b30bd0efd85d09c379741b0f78ea1d86be2416" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-linebreak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-width" version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + [[package]] name = "utf8parse" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" +[[package]] +name = "uzers" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" +dependencies = [ + "libc", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ed0d4f68a3015cc185aff4db9506a015f4b96f95303897bfa23f846db54064e" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b56f625e64f3a1084ded111c4d5f477df9f8c92df113852fa5a374dbda78826" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac36a15a220124ac510204aec1c3e5db8a22ab06fd6706d881dc6149f8ed9a12" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0162dbf37223cd2afce98f3d0785506dcb8d266223983e4b5b525859e6e182b2" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0eb82fcb7930ae6219a7ecfd55b217f5f0893484b7a13022ebb2b2bf20b5283" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ab9b36309365056cd639da3134bf87fa8f3d86008abf99e612384a6eecd459f" + +[[package]] +name = "web-sys" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50c24a44ec86bb68fbecd1b3efed7e85ea5621b39b35ef2766b66cd984f8010f" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki-roots" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1778a42e8b3b90bff8d0f5032bf22250792889a5cdc752aa0020c84abe3aaf10" + [[package]] name = "winapi" version = "0.3.9" @@ -965,3 +1824,13 @@ name = "windows_x86_64_msvc" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] diff --git a/Cargo.toml b/Cargo.toml index 16263ad..e7e1f17 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,43 +1,51 @@ [package] name = "nh" -version = "3.4.15" +version = "3.5.0" edition = "2021" license = "EUPL-1.2" - # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +ambassador = "0.3.5" +anstyle = "1.0.0" clap = { version = "4.0", features = [ + "cargo", + "color", "derive", "env", "unstable-styles", - "color", - "cargo", ] } clap_complete = "4.0" -fern = { version = "0.6", features = [ - "colored", -] } -log = "0.4" -hostname = "^0.3" clean-path = "0.2" -subprocess = "0.2" -dialoguer = { version = "0.10.2", default-features = false } -thiserror = "1.0" +color-eyre = { version = "0.6.2", default-features = false, features = [ + "track-caller", +] } derive_builder = "0.12.0" -tempfile = "3.5.0" +dialoguer = { version = "0.10.2", default-features = false } +elasticsearch-dsl = "0.4.19" +hostname = "^0.3" +humantime = "2.1.0" nix = { version = "0.26.2", default-features = false, features = [ - "user", "fs", + "user", ] } -anstyle = "1.0.0" -color-eyre = { version = "0.6.2", default-features = false, features = [ - "track-caller", -] } -ambassador = "0.3.5" -regex = "1.8.4" once_cell = "1.18.0" -timeago = { version = "0.4.1", default-features = false } -humantime = "2.1.0" +owo-colors = "3.5.0" +regex = "1.8.4" +reqwest = { version = "0.11.23", features = ["rustls-tls", "blocking", "json"], default-features = false } +serde = { version = "1.0.166", features = [ + "derive", +] } serde_json = "1.0.100" -serde = { version = "1.0.166", features = ["derive"] } +subprocess = "0.2" +tempfile = "3.5.0" +textwrap = { version = "0.16.0", features = ["terminal_size"] } +thiserror = "1.0" +timeago = { version = "0.4.1", default-features = false } +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = [ + "env-filter", + "registry", + "std" +] } +uzers = { version = "0.11.3", default-features = false } diff --git a/README.md b/README.md index 90e22f3..6a16682 100644 --- a/README.md +++ b/README.md @@ -136,3 +136,8 @@ Just `nix develop` [^1]: At the time of this writing. [^2]: The toplevel package is what you can build with `nix build /flake#nixosConfiguration.HOSTNAME.config.system.build.toplevel`, and what sits on `/run/current-system`, `/run/booted-system` and `/nix/var/nix/profiles/system`. + +## FIXME + +https://hydra.nixos.org/job/nixos/trunk-combined/nixpkgs.nh.x86_64-linux +https://hydra.nixos.org/job/nixos/trunk-combined/nixpkgs.nh.aarch64-linux \ No newline at end of file diff --git a/devshell.nix b/devshell.nix index 2b553b0..291f56e 100644 --- a/devshell.nix +++ b/devshell.nix @@ -27,6 +27,7 @@ mkShell { env = { NH_NOM = "1"; + RUST_LOG = "nh=trace"; RUST_SRC_PATH = "${rustPlatform.rustLibSrc}"; }; } diff --git a/src/clean.rs b/src/clean.rs index aa22f83..81bdb9a 100644 --- a/src/clean.rs +++ b/src/clean.rs @@ -1,331 +1,345 @@ -use std::collections::HashMap; -use std::io::ErrorKind; -use std::os::unix::process::CommandExt; -use std::path::{Component, Path, PathBuf}; -use std::time::SystemTime; - -use color_eyre::eyre::{bail, ensure, Context, ContextCompat}; -use color_eyre::Result; -use log::{debug, info, trace, warn}; -use once_cell::sync::Lazy; +use std::{ + collections::{BTreeMap, HashMap}, + fmt, + path::{Path, PathBuf}, + time::SystemTime, +}; + +use crate::*; +use color_eyre::eyre::{bail, eyre, Context, ContextCompat}; +use nix::errno::Errno; +use nix::{ + fcntl::AtFlags, + unistd::{faccessat, AccessFlags}, +}; use regex::Regex; +use std::os::unix::fs::MetadataExt; +use tracing::{debug, info, instrument, span, warn, Level}; +use uzers::os::unix::UserExt; -use crate::commands; -use crate::interface::NHRunnable; -use crate::interface::{CleanArgs, CleanMode}; +// Nix impl: +// https://github.com/NixOS/nix/blob/master/src/nix-collect-garbage/nix-collect-garbage.cc -// Reference: https://github.com/NixOS/nix/blob/master/src/nix-collect-garbage/nix-collect-garbage.cc +#[derive(Debug, Hash, PartialEq, Eq, PartialOrd, Ord)] +struct Generation { + number: u32, + last_modified: SystemTime, + path: PathBuf, +} + +type ToBeRemoved = bool; +// BTreeMap to automatically sort generations by id +type GenerationsTagged = BTreeMap; +type ProfilesTagged = HashMap; -impl NHRunnable for CleanMode { +impl NHRunnable for interface::CleanMode { fn run(&self) -> Result<()> { - match self { - CleanMode::Info => todo!(), - CleanMode::All(args) => { - let uid = nix::unistd::Uid::effective(); + let mut profiles = Vec::new(); + let mut gcroots_tagged: HashMap = HashMap::new(); + let now = SystemTime::now(); + let mut is_profile_clean = false; + + // What profiles to clean depending on the call mode + let uid = nix::unistd::Uid::effective(); + let args = match self { + interface::CleanMode::Profile(args) => { + profiles.push(args.profile.clone()); + is_profile_clean = true; + &args.common + } + interface::CleanMode::All(args) => { if !uid.is_root() { - let mut cmd = std::process::Command::new("sudo"); - cmd.args(std::env::args()); - debug!("{:?}", cmd); - let err = cmd.exec(); - bail!(err); + crate::self_elevate(); } - - let mut profiles = Vec::new(); - let mut gcroots = Vec::new(); - - gcroots.push(PathBuf::from("/nix/var/nix/gcroots/auto")); - profiles.push(PathBuf::from("/nix/var/nix/profiles")); - - for entry in std::fs::read_dir("/home")? { - let homedir = entry?.path(); - profiles.push(homedir.join(".local/state/nix/profiles")); + profiles.extend(profiles_in_dir("/nix/var/nix/profiles")); + for read_dir in PathBuf::from("/nix/var/nix/profiles/per-user").read_dir()? { + let path = read_dir?.path(); + profiles.extend(profiles_in_dir(path)); } - for entry in std::fs::read_dir("/nix/var/nix/profiles/per-user")? { - let path = entry?.path(); - profiles.push(path); - } - for entry in std::fs::read_dir("/nix/var/nix/gcroots/per-user")? { - let path = entry?.path(); - gcroots.push(path); + for user in unsafe { uzers::all_users() } { + if user.uid() >= 1000 || user.uid() == 0 { + debug!(?user, "Adding XDG profiles for user"); + profiles.extend(profiles_in_dir( + user.home_dir().join(".local/state/nix/profiles"), + )); + } } - - clean(args, &profiles, &gcroots) + args } - CleanMode::User(args) => { - let uid = nix::unistd::Uid::effective(); + interface::CleanMode::User(args) => { if uid.is_root() { bail!("nh clean user: don't run me as root!"); } let user = nix::unistd::User::from_uid(uid)?.unwrap(); - - let profiles = std::env::var("NIX_PROFILES") - .wrap_err("Reading NIX_PROFILES to detect the profiles locations")? - .split(' ') - .map(PathBuf::from) - .filter(|profile| { - use nix::unistd::AccessFlags; - let parent = match profile.parent() { - Some(p) => p, - None => { - return false; - } - }; - let access = nix::unistd::access( - parent, - AccessFlags::F_OK | AccessFlags::R_OK | AccessFlags::W_OK, - ); - trace!("eaccess {parent:?} -> {access:?}"); - - if let (Ok(_), true) = (access, profile.exists()) { - true - } else { - false - } - }) - .collect::>(); - - clean( - args, - &profiles, - // FIXME scan auto - &[PathBuf::from("/nix/var/nix/gcroots/per-user").join(user.name)], - ) + profiles.extend(profiles_in_dir( + &PathBuf::from(std::env::var("HOME")?).join(".local/state/nix/profiles"), + )); + profiles.extend(profiles_in_dir( + &PathBuf::from("/nix/var/nix/profiles/per-user").join(user.name), + )); + args } - } - } -} + }; -fn clean

(args: &CleanArgs, base_dirs: &[P], gcroots_dirs: &[P]) -> Result<()> -where - P: AsRef + std::fmt::Debug, -{ - info!("Calculating transaction"); - trace!("{:?}", gcroots_dirs); - - let mut gc_roots_to_remove = Vec::new(); - if !args.nogcroots { - for dir in gcroots_dirs { - for entry in std::fs::read_dir(dir)? { - let entry = entry?.path(); - trace!("Checking entry {:?}", entry); - - let pointing_to = match std::fs::read_link(&entry) { - Ok(p) => p, - Err(err) => match err.kind() { - std::io::ErrorKind::NotFound => continue, - other => bail!(other), - }, - }; + // Use mutation to raise errors as they come + let mut profiles_tagged = ProfilesTagged::new(); + for p in profiles { + profiles_tagged.insert( + p.clone(), + cleanable_generations(&p, args.keep, args.keep_since)?, + ); + } - let last_modified = std::fs::symlink_metadata(&entry)?.modified()?; - let now = SystemTime::now(); - match now.duration_since(last_modified) { - Err(err) => { - warn!("Failed to compare time!: {entry:?} {now:?} , {last_modified:?}, err: {err:?}"); - warn!("Please file a bug on https://github.com/viperML/nh/issues"); - continue; - } - Ok(val) if val <= args.keep_since.into() => { - continue; - } - Ok(_) => {} + // Query gcroots + let filename_tests = [r".*/.direnv/.*", r".*result.*"]; + let regexes = filename_tests + .into_iter() + .map(Regex::new) + .collect::, regex::Error>>()?; + + if !is_profile_clean { + for elem in PathBuf::from("/nix/var/nix/gcroots/auto") + .read_dir() + .wrap_err("Reading auto gcroots dir")? + { + let src = elem.wrap_err("Reading auto gcroots element")?.path(); + let dst = src.read_link().wrap_err("Reading symlink destination")?; + let span = span!(Level::TRACE, "gcroot detection", ?dst); + let _entered = span.enter(); + debug!(?src); + + if !regexes + .iter() + .any(|next| next.is_match(&dst.to_string_lossy())) + { + debug!("dst doesn't match any gcroot regex, skipping"); + continue; }; - let delete = pointing_to.components().any(|comp| { - if let Component::Normal(s) = comp { - let s = s.to_str().expect("Couldn't convert OsStr to UTF-8 str"); - if s == ".direnv" { - return true; + // Use .exists to not travel symlinks + if match faccessat( + None, + &dst, + AccessFlags::F_OK | AccessFlags::W_OK, + AtFlags::AT_SYMLINK_NOFOLLOW, + ) { + Ok(_) => true, + Err(errno) => match errno { + Errno::EACCES | Errno::ENOENT => false, + _ => { + bail!(eyre!("Checking access for gcroot {:?}, unknown error", dst) + .wrap_err(errno)) } - if s.contains("result") { - return true; - } - }; - false - }); - - if delete { - eprintln!( - " 🗑 {} -> {}", - entry.to_str().unwrap(), - pointing_to.to_str().unwrap() + }, + } { + let dur = now.duration_since( + dst.symlink_metadata() + .wrap_err("Reading gcroot metadata")? + .modified()?, ); - gc_roots_to_remove.push(entry); - }; + debug!(?dur); + match dur { + Err(err) => { + warn!(?err, ?now, "Failed to compare time!"); + } + Ok(val) if val <= args.keep_since.into() => { + gcroots_tagged.insert(dst, false); + } + Ok(_) => { + gcroots_tagged.insert(dst, true); + } + } + } else { + debug!("dst doesn't exist or is not writable, skipping"); + } } } - } - - trace!("{:?}", base_dirs); - let mut profiles: HashMap> = HashMap::new(); - for base_dir in base_dirs { - // let read = std::fs::read_dir(base_dir)?; - let read = match std::fs::read_dir(base_dir) { - Ok(inner) => inner, - Err(inner) => match inner.kind() { - ErrorKind::NotFound => { - debug!("Base dir not found!"); - continue; + // Present the user the information about the paths to clean + use owo_colors::OwoColorize; + println!(); + println!("{}", "Welcome to nh clean".bold()); + println!("Keeping {} generation(s)", args.keep.green()); + println!("Keeping paths newer than {}", args.keep_since.green()); + println!(); + println!("legend:"); + println!("{}: path to be kept", "OK".green()); + println!("{}: path to be removed", "DEL".red()); + println!(); + if !gcroots_tagged.is_empty() { + println!( + "{}", + "gcroots (matching the following regex patterns)" + .blue() + .bold() + ); + for re in regexes { + println!("- {} {}", "RE".purple(), re); + } + for (path, tbr) in &gcroots_tagged { + if *tbr { + println!("- {} {}", "DEL".red(), path.to_string_lossy()); + } else { + println!("- {} {}", "OK ".green(), path.to_string_lossy()); } - _ => Err(inner) - .context(base_dir.as_ref().to_str().unwrap().to_string()) - .context("Reading base dir")?, - }, - }; - - for entry in read { - // let x = x.await; - let path = entry?.path(); - let parent = path.parent().unwrap(); - let name = path.file_name().unwrap().to_str().unwrap().to_string(); - - if let Some((base_name, id)) = parse_profile(&name) { - let base_profile = parent.join(base_name); - let last_modified: SystemTime = std::fs::symlink_metadata(&path)?.modified()?; - - let profile = Generation { - id, - path, - last_modified, - marked_for_deletion: true, - }; - - match profiles.get_mut(&base_profile) { - None => { - profiles.insert(base_profile, vec![profile]); - } - Some(v) => { - v.push(profile); - } + } + println!(); + } + for (profile, generations_tagged) in profiles_tagged.iter() { + println!("{}", profile.to_string_lossy().blue().bold()); + for (gen, tbr) in generations_tagged.iter().rev() { + if *tbr { + println!("- {} {}", "DEL".red(), gen.path.to_string_lossy()); + } else { + println!("- {} {}", "OK ".green(), gen.path.to_string_lossy()); }; } - - if name.ends_with("-link") {} + println!(); } - } - trace!("{:?}", profiles); - - for (base_profile, generations) in &mut profiles { - generations.sort_by(|a, b| a.id.cmp(&b.id)); - trace!("generations: {:?}", generations); - - let last_id = generations.last().unwrap().id; - - let base_profile_link = base_profile.read_link()?; - let base_profile_link = base_profile_link.to_str().unwrap(); - let (_, base_profile_id) = - parse_profile(base_profile_link).wrap_err("Parsing base profile")?; - trace!("({base_profile_id:?}) {}", base_profile_link); - trace!( - "({last_id:?}) {}", - generations.last().unwrap().path.to_str().unwrap() - ); - ensure!( - base_profile_id == last_id, - "Profile doesn't point into the generation with highest number, aborting" - ); - - eprintln!(); - eprintln!("- {}", base_profile.as_os_str().to_str().unwrap()); - - for gen in generations.iter_mut() { - // Use relative numbering, 1,2,3,4 - gen.id = last_id - gen.id + 1; - - if gen.id <= args.keep { - gen.marked_for_deletion = false; + // Clean the paths + if args.ask { + info!("Confirm the cleanup plan?"); + if !dialoguer::Confirm::new().default(false).interact()? { + return Ok(()); } + } - let age = SystemTime::now().duration_since(gen.last_modified)?; - if age <= args.keep_since.into() { - gen.marked_for_deletion = false; + if !args.dry { + for (path, tbr) in &gcroots_tagged { + if *tbr { + remove_path_nofail(path); + } } - if gen.marked_for_deletion { - eprintln!(" 🗑 {}", gen.path.to_str().unwrap()); - } else { - eprintln!(" ✅ {}", gen.path.to_str().unwrap()); + for (_, generations_tagged) in profiles_tagged.iter() { + for (gen, tbr) in generations_tagged.iter().rev() { + if *tbr { + remove_path_nofail(&gen.path); + } + } } } - } - if args.dry { - return Ok(()); - } + commands::CommandBuilder::default() + .args(["nix", "store", "gc"]) + .dry(args.dry) + .message("Performing garbage collection on the nix store") + .build()? + .exec()?; - if args.ask { - info!("Confirm the cleanup plan?"); - let confirmation = dialoguer::Confirm::new().default(false).interact()?; - if !confirmation { - return Ok(()); - } + Ok(()) } +} - for root in gc_roots_to_remove { - info!("Removing {:?}", root); - if let Err(e) = std::fs::remove_file(root) { - warn!("Failed to remove: {:?}", e); - } - } +#[instrument(ret, level = "debug")] +fn profiles_in_dir + fmt::Debug>(dir: P) -> Vec { + let mut res = Vec::new(); + let dir = dir.as_ref(); + + match dir.read_dir() { + Ok(read_dir) => { + for entry in read_dir { + match entry { + Ok(e) => { + let path = e.path(); - for (_, generations) in profiles { - for gen in generations { - if gen.marked_for_deletion { - info!("Removing {:?}", gen.path); - if let Err(e) = std::fs::remove_file(gen.path) { - warn!("Failed to remove: {:?}", e); + if let Ok(dst) = path.read_link() { + let name = dst + .file_name() + .expect("Failed to get filename") + .to_string_lossy(); + + let generation_regex = Regex::new(r"^(.*)-(\d+)-link$").unwrap(); + + if let Some(_) = generation_regex.captures(&name) { + res.push(path); + } + } + } + Err(error) => { + warn!(?dir, ?error, "Failed to read folder element"); + } } } } + Err(error) => { + warn!(?dir, ?error, "Failed to read profiles directory"); + } } - if !args.nogc { - commands::CommandBuilder::default() - .args(&["nix", "store", "gc"]) - .message("nix store gc") - .capture(false) - .build()? - .exec()?; - } - - Ok(()) -} - -#[derive(Debug, Clone)] -struct Generation { - id: u32, - path: PathBuf, - last_modified: SystemTime, - marked_for_deletion: bool, + res } -static PROFILE_PATTERN: Lazy = Lazy::new(|| Regex::new(r"^(.*)-(\d+)-link$").unwrap()); +#[instrument(err, level = "debug")] +fn cleanable_generations( + profile: &Path, + keep: u32, + keep_since: humantime::Duration, +) -> Result { + let name = profile + .file_name() + .context("Checking profile's name")? + .to_str() + .unwrap(); + + let generation_regex = Regex::new(&format!(r"^{name}-(\d+)-link"))?; + + let mut result = GenerationsTagged::new(); + + for entry in profile + .parent() + .context("Reading profile's parent dir")? + .read_dir() + .context("Reading profile's generations")? + { + let path = entry?.path(); + let captures = generation_regex.captures(path.file_name().unwrap().to_str().unwrap()); + + if let Some(caps) = captures { + if let Some(number) = caps.get(1) { + let last_modified = path + .symlink_metadata() + .context("Checking symlink metadata")? + .modified() + .context("Reading modified time")?; + + result.insert( + Generation { + number: number.as_str().parse().unwrap(), + last_modified, + path: path.clone(), + }, + true, + ); + } + } + } -fn parse_profile(s: &str) -> Option<(&str, u32)> { - let captures = PROFILE_PATTERN.captures(s)?; + let now = SystemTime::now(); + for (gen, tbr) in result.iter_mut() { + match now.duration_since(gen.last_modified) { + Err(err) => { + warn!(?err, ?now, ?gen, "Failed to compare time!"); + } + Ok(val) if val <= keep_since.into() => { + *tbr = false; + } + Ok(_) => {} + } + } - let base = captures.get(1)?.as_str(); - let number = captures.get(2)?.as_str().parse().ok()?; + for (_, tbr) in result.iter_mut().rev().take(keep as _) { + *tbr = false; + } - Some((base, number)) + debug!("{:#?}", result); + Ok(result) } -#[test] -fn test_parse_profile() { - assert_eq!( - parse_profile("home-manager-3-link"), - Some(("home-manager", 3)) - ); - assert_eq!( - parse_profile("home-manager-30-link"), - Some(("home-manager", 30)) - ); - assert_eq!(parse_profile("home-manager"), None); - assert_eq!( - parse_profile("foo-bar-baz-0-link"), - Some(("foo-bar-baz", 0)) - ); - assert_eq!(parse_profile("foo-bar-baz-X-link"), None); +fn remove_path_nofail(path: &Path) { + info!("Removing {}", path.to_string_lossy()); + if let Err(err) = std::fs::remove_file(path) { + warn!(?path, ?err, "Failed to remove path"); + } } diff --git a/src/commands.rs b/src/commands.rs index 097d9c2..176a1c3 100644 --- a/src/commands.rs +++ b/src/commands.rs @@ -6,88 +6,91 @@ use color_eyre::{ use std::ffi::{OsStr, OsString}; use thiserror::Error; -use log::{debug, info}; -use subprocess::{Exec, ExitStatus, PopenError, Redirection}; +use subprocess::{Exec, ExitStatus, Redirection}; +use tracing::{debug, info}; -use crate::*; - -#[derive(Debug, derive_builder::Builder, Default)] -#[builder(derive(Debug), setter(into), default)] +#[derive(Debug, derive_builder::Builder)] +#[builder(derive(Debug), setter(into))] pub struct Command { /// Whether to actually run the command or just log it + #[builder(default = "false")] dry: bool, /// Human-readable message regarding what the command does - #[builder(setter(strip_option))] + #[builder(setter(strip_option), default = "None")] message: Option, - /// Whether to capture the stdout or let it inherit the parent - capture: bool, /// Arguments 0..N - #[builder(setter(custom), default = "vec![]")] + #[builder(setter(custom))] args: Vec, } impl CommandBuilder { - pub fn args(&mut self, input: &[impl AsRef]) -> &mut Self { - if let Some(args) = &mut self.args { - args.extend(input.iter().map(|elem| elem.as_ref().to_owned())); - self - } else { - self.args = Some(Vec::new()); - self.args(input) - } + pub fn args(&mut self, input: I) -> &mut Self + where + S: AsRef, + I: IntoIterator, + { + self.args + .get_or_insert_with(Default::default) + .extend(input.into_iter().map(|s| s.as_ref().to_owned())); + self } } impl Command { - fn exec_inner(&self) -> Result, PopenError> { + pub fn exec(&self) -> Result<()> { let [head, tail @ ..] = &*self.args else { - panic!("Args was length 0"); + bail!("Args was length 0"); }; - let cmd = if self.capture { - Exec::cmd(head) - .args(tail) - .stderr(Redirection::None) - .stdout(Redirection::Pipe) - } else { - Exec::cmd(head) - .args(tail) - .stderr(Redirection::None) - .stdout(Redirection::None) - }; + let cmd = Exec::cmd(head) + .args(tail) + .stderr(Redirection::None) + .stdout(Redirection::None); if let Some(m) = &self.message { info!("{}", m); } - debug!("{:?}", cmd); + debug!(?cmd); - let result = if self.capture { - Some(cmd.capture()?.stdout_str()) - } else { - cmd.join()?; - None - }; + if !self.dry { + if let Some(m) = &self.message { + cmd.join().wrap_err(m.clone())?; + } else { + cmd.join()?; + } + } - Ok(result) + Ok(()) } - pub fn exec(self) -> Result> { - let result = self.exec_inner(); + pub fn exec_capture(&self) -> Result> { + let [head, tail @ ..] = &*self.args else { + bail!("Args was length 0"); + }; + + let cmd = Exec::cmd(head) + .args(tail) + .stderr(Redirection::None) + .stdout(Redirection::Pipe); + + if let Some(m) = &self.message { + info!("{}", m); + } + debug!(?cmd); - if let Some(m) = self.message { - Ok(result.context(m)?) + if !self.dry { + Ok(Some(cmd.capture()?.stdout_str())) } else { - Ok(result?) + Ok(None) } } } -#[derive(Debug, Default, derive_builder::Builder)] -#[builder(setter(into), default)] +#[derive(Debug, derive_builder::Builder)] +#[builder(setter(into))] pub struct BuildCommand { /// Human-readable message regarding what the command does - #[builder(setter(strip_option))] - message: Option, + message: String, // Flakeref to build flakeref: String, // Extra arguments passed to nix build @@ -98,22 +101,21 @@ pub struct BuildCommand { } impl BuildCommandBuilder { - pub fn extra_args(&mut self, input: &[impl AsRef]) -> &mut Self { - if let Some(args) = &mut self.extra_args { - args.extend(input.iter().map(|elem| elem.as_ref().to_owned())); - self - } else { - self.extra_args = Some(Vec::new()); - self.extra_args(input) - } + pub fn extra_args(&mut self, input: I) -> &mut Self + where + S: AsRef, + I: IntoIterator, + { + self.extra_args + .get_or_insert_with(Default::default) + .extend(input.into_iter().map(|s| s.as_ref().to_owned())); + self } } impl BuildCommand { - fn exec_inner(&self) -> Result<()> { - if let Some(m) = &self.message { - info!("{}", m); - } + pub fn exec(&self) -> Result<()> { + info!("{}", self.message); let exit = if self.nom { let cmd = { @@ -131,7 +133,7 @@ impl BuildCommand { | Exec::cmd("nom").args(&["--json"]) } .stdout(Redirection::None); - debug!("{:?}", cmd); + debug!(?cmd); cmd.join() } else { let cmd = Exec::cmd("nix") @@ -140,33 +142,17 @@ impl BuildCommand { .stdout(Redirection::None) .stderr(Redirection::Merge); - debug!("{:?}", cmd); + debug!(?cmd); cmd.join() }; - let exit: ExitStatus = if let Some(ref m) = self.message { - exit.context(m.clone())? - } else { - exit? - }; - - match exit { + match exit.wrap_err(self.message.clone())? { ExitStatus::Exited(0) => (), other => bail!(ExitError(other)), } Ok(()) } - - pub fn exec(self) -> Result<()> { - let result = self.exec_inner(); - - if let Some(m) = self.message { - Ok(result.context(m)?) - } else { - Ok(result?) - } - } } #[derive(Debug, Error)] diff --git a/src/completion.rs b/src/completion.rs index 03368ac..4e6a883 100644 --- a/src/completion.rs +++ b/src/completion.rs @@ -1,10 +1,11 @@ use crate::*; use clap_complete::generate; use color_eyre::Result; +use tracing::instrument; impl NHRunnable for interface::CompletionArgs { + #[instrument(ret, level = "trace")] fn run(&self) -> Result<()> { - trace!("{:?}", self); let mut cmd = ::command(); generate(self.shell, &mut cmd, "nh", &mut std::io::stdout()); Ok(()) diff --git a/src/home.rs b/src/home.rs index 276a827..8c14c1c 100644 --- a/src/home.rs +++ b/src/home.rs @@ -4,8 +4,8 @@ use std::path::PathBuf; use color_eyre::eyre::bail; use color_eyre::Result; -use log::{debug, info, trace}; use thiserror::Error; +use tracing::{debug, info, instrument}; use crate::*; use crate::{ @@ -60,7 +60,7 @@ impl HomeRebuildArgs { if self.common.update { commands::CommandBuilder::default() - .args(&["nix", "flake", "update", &self.common.flakeref]) + .args(["nix", "flake", "update", &self.common.flakeref]) .message("Updating flake") .build()? .exec()?; @@ -68,7 +68,7 @@ impl HomeRebuildArgs { commands::BuildCommandBuilder::default() .flakeref(&flakeref) - .extra_args(&["--out-link", out_link_str]) + .extra_args(["--out-link", out_link_str]) .extra_args(&self.extra_args) .message("Building home configuration") .nom(self.common.nom) @@ -91,12 +91,7 @@ impl HomeRebuildArgs { // just do nothing for None case (fresh installs) if let Some(prev_gen) = prev_generation { commands::CommandBuilder::default() - .args(&[ - "nvd", - "diff", - (prev_gen.to_str().unwrap()), - out_link_str, - ]) + .args(["nvd", "diff", (prev_gen.to_str().unwrap()), out_link_str]) .message("Comparing changes") .build()? .exec()?; @@ -116,7 +111,7 @@ impl HomeRebuildArgs { } commands::CommandBuilder::default() - .args(&[&format!("{}/activate", out_link_str)]) + .args([&format!("{}/activate", out_link_str)]) .message("Activating configuration") .build()? .exec()?; @@ -158,19 +153,18 @@ fn get_home_output + std::fmt::Display>( } } +#[instrument(ret, err, level = "debug")] fn configuration_exists(flakeref: &FlakeRef, configuration: &str) -> Result { let output = format!("{}#homeConfigurations", flakeref.deref()); let filter = format!(r#" x: x ? "{}" "#, configuration); let result = commands::CommandBuilder::default() - .args(&["nix", "eval", &output, "--apply", &filter]) - .capture(true) - .build() - .unwrap() - .exec()? + .args(["nix", "eval", &output, "--apply", &filter]) + .build()? + .exec_capture()? .unwrap(); - trace!("{:?}", result); + debug!(?result); match result.as_str().trim() { "true" => Ok(true), diff --git a/src/interface.rs b/src/interface.rs index ff2f43f..201a4bc 100644 --- a/src/interface.rs +++ b/src/interface.rs @@ -2,7 +2,7 @@ use ambassador::{delegatable_trait, Delegate}; use anstyle::Style; use clap::{builder::Styles, Args, Parser, Subcommand}; use color_eyre::Result; -use std::{ffi::OsString, ops::Deref}; +use std::{ffi::OsString, ops::Deref, path::PathBuf}; #[derive(Debug, Clone, Default)] pub struct FlakeRef(String); @@ -141,15 +141,22 @@ pub struct CommonRebuildArgs { } #[derive(Args, Debug)] -/// Search a package +/// Searches packages by querying search.nixos.org pub struct SearchArgs { - #[arg(long, short, default_value = "10")] - pub max_results: usize, + #[arg(long, short, default_value = "30")] + /// Number of search results to display + pub limit: u64, - pub query: String, + #[arg(long, short = 'L')] + /// Display more information about each result + pub long: bool, + + #[arg(long, short, default_value = "nixos-unstable")] + /// Name of the channel to query (e.g nixos-23.11, nixos-unstable) + pub channel: String, - #[arg(default_value = "nixpkgs")] - pub flake: FlakeRef, + /// Name of the package to search + pub query: String, } // Needed a struct to have multiple sub-subcommands @@ -163,13 +170,12 @@ pub struct CleanProxy { #[derive(Debug, Clone, Subcommand)] /// Enhanced nix cleanup pub enum CleanMode { - /// Elevate to root to clean all profiles and gcroots + /// Cleans root profiles and calls a store gc All(CleanArgs), - /// Clean your user's profiles and gcroots + /// Cleans the current user's profiles and calls a store gc User(CleanArgs), - /// Print information about the store of the system - #[clap(hide = true)] - Info, + /// Cleans a specific profile + Profile(CleanProfileArgs), } #[derive(Args, Clone, Debug)] @@ -182,7 +188,7 @@ pub struct CleanArgs { /// At least keep this number of generations pub keep: u32, - #[arg(long, short = 'K', default_value = "0s")] + #[arg(long, short = 'K', default_value = "0h")] /// At least keep gcroots and generations in this time range since now. pub keep_since: humantime::Duration, @@ -203,6 +209,14 @@ pub struct CleanArgs { pub nogcroots: bool, } +#[derive(Debug, Clone, Args)] +pub struct CleanProfileArgs { + #[command(flatten)] + pub common: CleanArgs, + + pub profile: PathBuf, +} + #[derive(Debug, Args)] /// Home-manager functionality pub struct HomeArgs { diff --git a/src/logging.rs b/src/logging.rs new file mode 100644 index 0000000..df64b4e --- /dev/null +++ b/src/logging.rs @@ -0,0 +1,89 @@ +use crate::*; +use owo_colors::OwoColorize; +use tracing::Event; +use tracing::Level; +use tracing::Subscriber; +use tracing_subscriber::filter::filter_fn; +use tracing_subscriber::filter::FilterExt; +use tracing_subscriber::fmt; + +use tracing_subscriber::fmt::FormatEvent; +use tracing_subscriber::fmt::FormatFields; +use tracing_subscriber::prelude::*; +use tracing_subscriber::registry::LookupSpan; +use tracing_subscriber::EnvFilter; + +struct InfoFormatter; + +impl FormatEvent for InfoFormatter +where + S: Subscriber + for<'a> LookupSpan<'a>, + N: for<'a> FormatFields<'a> + 'static, +{ + fn format_event( + &self, + ctx: &fmt::FmtContext<'_, S, N>, + mut writer: fmt::format::Writer, + event: &Event, + ) -> std::fmt::Result { + // Based on https://docs.rs/tracing-subscriber/latest/tracing_subscriber/fmt/trait.FormatEvent.html#examples + // Without the unused parts + let metadata = event.metadata(); + let level = metadata.level(); + + if *level == Level::ERROR { + write!(writer, "{} ", "!".red())?; + } else if *level == Level::WARN { + write!(writer, "{} ", "!".yellow())?; + } else { + write!(writer, "{} ", ">".green())?; + } + + ctx.field_format().format_fields(writer.by_ref(), event)?; + + if *level != Level::INFO { + if let (Some(file), Some(line)) = (metadata.file(), metadata.line()) { + write!(writer, " @ {}:{}", file, line)?; + } + } + + writeln!(writer)?; + Ok(()) + } +} + +pub(crate) fn setup_logging(verbose: bool) -> Result<()> { + color_eyre::config::HookBuilder::default() + .display_location_section(true) + .panic_section("Please report the bug at https://github.com/viperML/nh/issues") + .display_env_section(false) + .install()?; + + let layer_debug = fmt::layer() + .with_writer(std::io::stderr) + .without_time() + .compact() + .with_line_number(true) + .with_filter(EnvFilter::from_default_env().or(filter_fn(move |_| verbose))) + .with_filter(filter_fn(|meta| *meta.level() > Level::INFO)); + + let layer_info = fmt::layer() + .with_writer(std::io::stderr) + .without_time() + .with_target(false) + .with_level(false) + .event_format(InfoFormatter) + .with_filter(filter_fn(|meta| { + let level = *meta.level(); + (level == Level::INFO) || (level == Level::WARN) + })); + + tracing_subscriber::registry() + .with(layer_debug) + .with(layer_info) + .init(); + + tracing::trace!("Logging OK"); + + Ok(()) +} diff --git a/src/main.rs b/src/main.rs index 4e14eb0..c2394cd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,76 +3,31 @@ mod commands; mod completion; mod home; mod interface; +mod logging; mod nixos; mod search; -use color_eyre::Result; -use fern::colors::Color; -use log::{error, trace, SetLoggerError}; - use crate::interface::NHParser; use crate::interface::NHRunnable; +use color_eyre::Result; +use tracing::debug; -fn main() -> Result<()> { - if cfg!(debug_assertions) { - std::env::set_var("RUST_BACKTRACE", "full"); - } - - color_eyre::config::HookBuilder::default() - .display_location_section(false) - .panic_section("consider reporting the bug at https://github.com/viperML/nh") - .display_env_section(false) - .install()?; +const NH_VERSION: &str = env!("CARGO_PKG_VERSION"); +fn main() -> Result<()> { let args = ::parse(); - - setup_logging(args.verbose)?; + crate::logging::setup_logging(args.verbose)?; + tracing::debug!(?args); args.command.run() } -fn setup_logging(verbose: bool) -> Result<(), SetLoggerError> { - let loglevel = if cfg!(debug_assertions) { - log::LevelFilter::Trace - } else if verbose { - log::LevelFilter::Debug - } else { - log::LevelFilter::Info - }; - - let color_text = fern::colors::ColoredLevelConfig::new() - .debug(Color::BrightBlack) - .error(Color::White) - .trace(Color::BrightBlue); - - let color_symbol = fern::colors::ColoredLevelConfig::new() - .debug(Color::BrightBlack) - .error(Color::Red) - .error(Color::Red) - .info(Color::Green) - .trace(Color::BrightBlue) - .warn(Color::Yellow); +fn self_elevate() -> ! { + use std::os::unix::process::CommandExt; - fern::Dispatch::new() - .format(move |out, message, record| { - let prefix = match record.level() { - log::Level::Info | log::Level::Warn | log::Level::Error => "\n", - _ => "", - }; - out.finish(format_args!( - "{prefix}{color_symbol}>\x1B[0m {color_line}{message}\x1B[0m", - color_symbol = format_args!( - "\x1B[{}m", - color_symbol.get_color(&record.level()).to_fg_str() - ), - color_line = format_args!( - "\x1B[{}m", - color_text.get_color(&record.level()).to_fg_str() - ), - message = message, - )); - }) - .level(loglevel) - .chain(std::io::stdout()) - .apply() + let mut cmd = std::process::Command::new("sudo"); + cmd.args(std::env::args()); + debug!("{:?}", cmd); + let err = cmd.exec(); + panic!("{}", err); } diff --git a/src/nixos.rs b/src/nixos.rs index 3457d65..3121bf9 100644 --- a/src/nixos.rs +++ b/src/nixos.rs @@ -3,7 +3,7 @@ use std::ops::Deref; use color_eyre::eyre::{bail, Context}; use color_eyre::Result; -use log::{debug, info, trace}; +use tracing::{debug, info}; use crate::interface::NHRunnable; use crate::interface::OsRebuildType::{self, Boot, Switch, Test}; @@ -17,8 +17,6 @@ const SPEC_LOCATION: &str = "/etc/specialisation"; impl NHRunnable for interface::OsArgs { fn run(&self) -> Result<()> { - trace!("{:?}", self); - match &self.action { Switch(args) | Boot(args) | Test(args) => args.rebuild(&self.action), s => bail!("Subcommand {:?} not yet implemented", s), @@ -51,7 +49,7 @@ impl OsRebuildArgs { if self.common.update { commands::CommandBuilder::default() - .args(&["nix", "flake", "update", &self.common.flakeref]) + .args(["nix", "flake", "update", &self.common.flakeref]) .message("Updating flake") .build()? .exec()?; @@ -60,7 +58,7 @@ impl OsRebuildArgs { commands::BuildCommandBuilder::default() .flakeref(flake_output) .message("Building NixOS configuration") - .extra_args(&["--out-link", out_link_str]) + .extra_args(["--out-link", out_link_str]) .extra_args(&self.extra_args) .nom(self.common.nom) .build()? @@ -84,7 +82,7 @@ impl OsRebuildArgs { target_profile.try_exists().context("Doesn't exist")?; commands::CommandBuilder::default() - .args(&[ + .args([ "nvd", "diff", CURRENT_PROFILE, @@ -108,7 +106,7 @@ impl OsRebuildArgs { } commands::CommandBuilder::default() - .args(&[ + .args([ "sudo", "nix-env", "--profile", @@ -126,7 +124,7 @@ impl OsRebuildArgs { let switch_to_configuration = switch_to_configuration.to_str().unwrap(); commands::CommandBuilder::default() - .args(&["sudo", switch_to_configuration, "test"]) + .args(["sudo", switch_to_configuration, "test"]) .message("Activating configuration") .build()? .exec()?; @@ -138,7 +136,7 @@ impl OsRebuildArgs { let switch_to_configuration = switch_to_configuration.to_str().unwrap(); commands::CommandBuilder::default() - .args(&["sudo", switch_to_configuration, "boot"]) + .args(["sudo", switch_to_configuration, "boot"]) .message("Adding configuration to bootloader") .build()? .exec()?; diff --git a/src/search.rs b/src/search.rs index e6a6aa9..811d699 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,32 +1,147 @@ -use std::{collections::HashMap, ops::Deref, process::Command}; - use crate::*; +use color_eyre::eyre::Context; use interface::SearchArgs; -#[derive(Debug, serde::Deserialize)] -struct RawEntry<'a> { - description: &'a str, - pname: &'a str, - version: &'a str, -} +use std::time::Instant; +use tracing::{debug, trace}; -type RawResults<'a> = HashMap<&'a str, RawEntry<'a>>; +use elasticsearch_dsl::*; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +#[allow(non_snake_case, dead_code)] +struct SearchResult { + // r#type: String, + package_attr_name: String, + package_attr_set: String, + package_pname: String, + package_pversion: String, + package_platforms: Vec, + package_outputs: Vec, + package_default_output: Option, + package_programs: Vec, + // package_license: Vec, + package_license_set: Vec, + // package_maintainers: Vec>, + package_description: Option, + package_longDescription: Option, + package_hydra: (), + package_system: String, + package_homepage: Vec, + package_position: Option, +} impl NHRunnable for SearchArgs { fn run(&self) -> Result<()> { trace!("args: {self:?}"); - let results = Command::new("nix") - .arg("search") - .arg(self.flake.deref()) - .arg(&self.query) - .arg("--json") - .output()?; + let query = Search::new().from(0).size(self.limit).query( + Query::bool().filter(Query::term("type", "package")).must( + Query::dis_max() + .tie_breaker(0.7) + .query( + Query::multi_match( + [ + "package_attr_name^9", + "package_attr_name.*^5.3999999999999995", + "package_programs^9", + "package_programs.*^5.3999999999999995", + "package_pname^6", + "package_pname.*^3.5999999999999996", + "package_description^1.3", + "package_description.*^0.78", + "package_longDescription^1", + "package_longDescription.*^0.6", + "flake_name^0.5", + "flake_name.*^0.3", + ], + self.query.as_str(), + ) + .r#type(TextQueryType::CrossFields) + .analyzer("whitespace") + .auto_generate_synonyms_phrase_query(false) + .operator(Operator::And), + ) + .query( + Query::wildcard("package_attr_name", format!("*{}*", self.query)) + .case_insensitive(true), + ), + ), + ); + + println!( + "Querying search.nixos.org, with channel {}...", + self.channel + ); + let then = Instant::now(); + + let client = reqwest::blocking::Client::new(); + let req = client + // I guess 42 is the version of the backend API + // TODO: have a GH action or something check if they updated this thing + .post(format!( + "https://search.nixos.org/backend/latest-42-{}/_search", + self.channel + )) + .json(&query) + .header("User-Agent", format!("nh/{}", crate::NH_VERSION)) + // Hardcoded upstream + // https://github.com/NixOS/nixos-search/blob/744ec58e082a3fcdd741b2c9b0654a0f7fda4603/frontend/src/index.js + .basic_auth("aWVSALXpZv", Some("X8gPHnzL52wFEekuxsfQ9cSh")) + .build() + .context("building search query")?; + + debug!(?req); + + let response = client + .execute(req) + .context("querying the elasticsearch API")?; + let elapsed = then.elapsed(); + debug!(?elapsed); + trace!(?response); + println!("Took {}ms", elapsed.as_millis()); + println!("Most relevant results at end"); + println!(); + + let parsed_response: SearchResponse = response + .json() + .context("parsing response into the elasticsearch format")?; + trace!(?parsed_response); + + let documents = parsed_response + .documents::() + .context("parsing search document")?; + + for elem in documents.iter().rev() { + println!(); + use owo_colors::OwoColorize; + trace!("{elem:#?}"); + print!("{}", elem.package_attr_name.blue(),); + let v = &elem.package_pversion; + if !v.is_empty() { + print!(" ({})", v.green()); + } + + println!(); + + if let Some(ref desc) = elem.package_description { + let desc = desc.replace('\n', " "); + for line in textwrap::wrap(&desc, textwrap::Options::with_termwidth()) { + println!(" {}", line); + } + } - let parsed: RawResults = serde_json::from_slice(&results.stdout)?; + if self.long { + for url in elem.package_homepage.iter() { + println!(" Homepage: {}", url); + } - trace!("{:?}", parsed); + if !elem.package_license_set.is_empty() { + println!(" License: {}", elem.package_license_set.join(", ")); + } + } + } - todo!(); + Ok(()) } }