diff --git a/Cargo.lock b/Cargo.lock index 0fee449..c4fd274 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -37,19 +37,6 @@ dependencies = [ "futures-core", ] -[[package]] -name = "async-compression" -version = "0.3.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695" -dependencies = [ - "futures-core", - "futures-io", - "memchr", - "pin-project-lite", - "xz2", -] - [[package]] name = "async-executor" version = "1.4.1" @@ -235,6 +222,22 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "206fdffcfa2df7cbe15601ef46c813fce0965eb3286db6b56c583b814b51c81c" +dependencies = [ + "byteorder", + "iovec", +] + [[package]] name = "bytes" version = "1.2.1" @@ -334,22 +337,6 @@ dependencies = [ "cache-padded", ] -[[package]] -name = "core-foundation" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" - [[package]] name = "crossbeam-utils" version = "0.8.11" @@ -448,21 +435,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "foreign-types" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" -dependencies = [ - "foreign-types-shared", -] - -[[package]] -name = "foreign-types-shared" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" - [[package]] name = "form_urlencoded" version = "1.0.1" @@ -473,6 +445,18 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fuchsia-cprng" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06f77d526c1a601b7c4cdd98f54b5eaabffc14d5f2f0296febdc7f357c6d3ba" + +[[package]] +name = "futures" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a471a38ef8ed83cd6e40aa59c1ffe17db6855c18e3604d9c4ed8c08ebc28678" + [[package]] name = "futures" version = "0.3.24" @@ -583,6 +567,12 @@ version = "0.26.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +[[package]] +name = "glob" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" + [[package]] name = "gloo-timers" version = "0.2.4" @@ -601,7 +591,7 @@ version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ - "bytes", + "bytes 1.2.1", "fnv", "futures-core", "futures-sink", @@ -618,24 +608,31 @@ dependencies = [ name = "harmonic" version = "0.0.1" dependencies = [ - "async-compression", "async-tar", "async-trait", "atty", + "bytes 1.2.1", "clap", "color-eyre", "crossterm", "eyre", - "futures", + "futures 0.3.24", + "glob", + "nix", "owo-colors", "reqwest", + "tar", "target-lexicon", + "tempdir", "thiserror", "tokio", + "tokio-util", "tracing", "tracing-error", "tracing-subscriber", "valuable", + "walkdir", + "xz2", ] [[package]] @@ -665,7 +662,7 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ - "bytes", + "bytes 1.2.1", "fnv", "itoa", ] @@ -676,7 +673,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes", + "bytes 1.2.1", "http", "pin-project-lite", ] @@ -699,7 +696,7 @@ version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ - "bytes", + "bytes 1.2.1", "futures-channel", "futures-core", "futures-util", @@ -718,16 +715,16 @@ dependencies = [ ] [[package]] -name = "hyper-tls" -version = "0.5.0" +name = "hyper-rustls" +version = "0.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" dependencies = [ - "bytes", + "http", "hyper", - "native-tls", + "rustls", "tokio", - "tokio-native-tls", + "tokio-rustls", ] [[package]] @@ -766,6 +763,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "iovec" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +dependencies = [ + "libc", +] + [[package]] name = "ipnet" version = "2.5.0" @@ -894,21 +900,15 @@ dependencies = [ ] [[package]] -name = "native-tls" -version = "0.2.10" +name = "nix" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd7e2f3618557f980e0b17e8856252eee3c97fa12c54dff0ca290fb6266ca4a9" +checksum = "e322c04a9e3440c327fca7b6c8a63e6890a32fa2ad689db972425f07e0d22abb" dependencies = [ - "lazy_static", + "autocfg", + "bitflags", + "cfg-if", "libc", - "log", - "openssl", - "openssl-probe", - "openssl-sys", - "schannel", - "security-framework", - "security-framework-sys", - "tempfile", ] [[package]] @@ -936,51 +936,6 @@ version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" -[[package]] -name = "openssl" -version = "0.10.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" -dependencies = [ - "bitflags", - "cfg-if", - "foreign-types", - "libc", - "once_cell", - "openssl-macros", - "openssl-sys", -] - -[[package]] -name = "openssl-macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "openssl-probe" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" - -[[package]] -name = "openssl-sys" -version = "0.9.75" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" -dependencies = [ - "autocfg", - "cc", - "libc", - "pkg-config", - "vcpkg", -] - [[package]] name = "os_str_bytes" version = "6.3.0" @@ -1125,6 +1080,43 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rand" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "552840b97013b1a26992c11eac34bdd778e464601a4c2054b5f0bff7c6761293" +dependencies = [ + "fuchsia-cprng", + "libc", + "rand_core 0.3.1", + "rdrand", + "winapi", +] + +[[package]] +name = "rand_core" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a6fdeb83b075e8266dcc8762c22776f6877a63111121f5f8c7411e5be7eed4b" +dependencies = [ + "rand_core 0.4.2", +] + +[[package]] +name = "rand_core" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c33a3c44ca05fa6f1807d8e6743f3824e8509beca625669633be0acbdf509dc" + +[[package]] +name = "rdrand" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678054eb77286b51581ba43620cc911abf02758c91f93f479767aed0f90458b2" +dependencies = [ + "rand_core 0.3.1", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -1174,7 +1166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ "base64", - "bytes", + "bytes 1.2.1", "encoding_rs", "futures-core", "futures-util", @@ -1182,35 +1174,73 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-tls", + "hyper-rustls", "ipnet", "js-sys", "lazy_static", "log", "mime", - "native-tls", "percent-encoding", "pin-project-lite", + "rustls", + "rustls-pemfile", "serde", "serde_json", "serde_urlencoded", "tokio", - "tokio-native-tls", + "tokio-rustls", "tokio-util", "tower-service", "url", "wasm-bindgen", "wasm-bindgen-futures", "web-sys", + "webpki-roots", "winreg", ] +[[package]] +name = "ring" +version = "0.16.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +dependencies = [ + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", +] + [[package]] name = "rustc-demangle" version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" +[[package]] +name = "rustls" +version = "0.20.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" +dependencies = [ + "log", + "ring", + "sct", + "webpki", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0864aeff53f8c05aa08d86e5ef839d3dfcf07aeba2db32f12db0ef716e87bd55" +dependencies = [ + "base64", +] + [[package]] name = "ryu" version = "1.0.11" @@ -1218,13 +1248,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" [[package]] -name = "schannel" -version = "0.1.20" +name = "same-file" +version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" dependencies = [ - "lazy_static", - "windows-sys", + "winapi-util", ] [[package]] @@ -1234,26 +1263,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] -name = "security-framework" -version = "2.7.0" +name = "sct" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" +checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" -dependencies = [ - "core-foundation-sys", - "libc", + "ring", + "untrusted", ] [[package]] @@ -1349,6 +1365,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + [[package]] name = "strsim" version = "0.10.0" @@ -1376,6 +1398,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "tar" +version = "0.4.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b55807c0344e1e6c04d7c965f5289c39a8d94ae23ed5c0b57aabac549f871c6" +dependencies = [ + "filetime", + "libc", + "xattr", +] + [[package]] name = "target-lexicon" version = "0.12.4" @@ -1383,17 +1416,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c02424087780c9b71cc96799eaeddff35af2bc513278cda5c99fc1f5d026d3c1" [[package]] -name = "tempfile" -version = "3.3.0" +name = "tempdir" +version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "15f2b5fb00ccdf689e0149d1b1b3c03fead81c2b37735d812fa8bddbbf41b6d8" dependencies = [ - "cfg-if", - "fastrand", - "libc", - "redox_syscall", + "rand", "remove_dir_all", - "winapi", ] [[package]] @@ -1462,7 +1491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" dependencies = [ "autocfg", - "bytes", + "bytes 1.2.1", "libc", "memchr", "mio", @@ -1476,6 +1505,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "tokio-io" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57fc868aae093479e3131e3d165c93b1c7474109d13c90ec0dda2a1bbfff0674" +dependencies = [ + "bytes 0.4.12", + "futures 0.1.31", + "log", +] + [[package]] name = "tokio-macros" version = "1.8.0" @@ -1488,13 +1528,14 @@ dependencies = [ ] [[package]] -name = "tokio-native-tls" -version = "0.3.0" +name = "tokio-rustls" +version = "0.23.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" dependencies = [ - "native-tls", + "rustls", "tokio", + "webpki", ] [[package]] @@ -1503,7 +1544,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ - "bytes", + "bytes 1.2.1", "futures-core", "futures-sink", "pin-project-lite", @@ -1631,6 +1672,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + [[package]] name = "url" version = "2.2.2" @@ -1683,12 +1730,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "vcpkg" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" - [[package]] name = "version_check" version = "0.9.4" @@ -1701,6 +1742,17 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d5b2c62b4012a3e1eca5a7e077d13b3bf498c4073e33ccd58626607748ceeca" +[[package]] +name = "walkdir" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +dependencies = [ + "same-file", + "winapi", + "winapi-util", +] + [[package]] name = "want" version = "0.3.0" @@ -1793,6 +1845,25 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" +dependencies = [ + "webpki", +] + [[package]] name = "wepoll-ffi" version = "0.1.2" @@ -1900,5 +1971,7 @@ version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2" dependencies = [ + "futures 0.1.31", "lzma-sys", + "tokio-io", ] diff --git a/Cargo.toml b/Cargo.toml index a74dc03..03e52ce 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -29,3 +29,5 @@ glob = "0.3.0" xz2 = { version = "0.1.7", features = ["static", "tokio"] } bytes = "1.2.1" tar = "0.4.38" +nix = { version = "0.25.0", features = ["user", "fs"], default-features = false } +walkdir = "2.3.2" diff --git a/flake.nix b/flake.nix index 9078f56..46f97b5 100644 --- a/flake.nix +++ b/flake.nix @@ -109,6 +109,7 @@ ]); doCheck = true; + RUSTFLAGS = "--cfg tracing_unstable"; override = { preBuild ? "", ... }: { preBuild = preBuild + '' diff --git a/src/cli/arg/channel_value.rs b/src/cli/arg/channel_value.rs new file mode 100644 index 0000000..9cd30ee --- /dev/null +++ b/src/cli/arg/channel_value.rs @@ -0,0 +1,37 @@ +use reqwest::Url; + +#[derive(Debug, Clone)] +pub struct ChannelValue(pub String, pub Url); + +impl clap::builder::ValueParserFactory for ChannelValue { + type Parser = ChannelValueParser; + fn value_parser() -> Self::Parser { + ChannelValueParser + } +} + +#[derive(Clone, Debug)] +pub struct ChannelValueParser; +impl clap::builder::TypedValueParser for ChannelValueParser { + type Value = ChannelValue; + + fn parse_ref( + &self, + _cmd: &clap::Command, + _arg: Option<&clap::Arg>, + value: &std::ffi::OsStr, + ) -> Result { + let buf = value.to_str().ok_or_else(|| { + clap::Error::raw(clap::ErrorKind::InvalidValue, "Should be all UTF-8") + })?; + let (name, url) = buf.split_once('=').ok_or_else(|| { + clap::Error::raw( + clap::ErrorKind::InvalidValue, + "Should be formatted `name=url`", + ) + })?; + let name = name.to_owned(); + let url = url.parse().unwrap(); + Ok(ChannelValue(name, url)) + } +} diff --git a/src/cli/arg/instrumentation.rs b/src/cli/arg/instrumentation.rs index ed7de3c..13efbb4 100644 --- a/src/cli/arg/instrumentation.rs +++ b/src/cli/arg/instrumentation.rs @@ -6,7 +6,7 @@ use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilte use valuable::Valuable; #[derive(clap::Args, Debug, Valuable)] -pub(crate) struct Instrumentation { +pub struct Instrumentation { /// Enable debug logs, -vv for trace #[clap( short = 'v', diff --git a/src/cli/arg/mod.rs b/src/cli/arg/mod.rs index c00d041..5e74712 100644 --- a/src/cli/arg/mod.rs +++ b/src/cli/arg/mod.rs @@ -1,2 +1,4 @@ mod instrumentation; -pub(crate) use instrumentation::Instrumentation; +pub use instrumentation::Instrumentation; +mod channel_value; +pub use channel_value::ChannelValue; diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 141142d..9f43ebd 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -1,10 +1,9 @@ pub(crate) mod arg; pub(crate) mod subcommand; -use crate::interaction; -use clap::Parser; +use crate::{cli::arg::ChannelValue, interaction}; +use clap::{ArgAction, Parser}; use harmonic::Harmonic; -use reqwest::Url; use std::process::ExitCode; #[async_trait::async_trait] @@ -15,10 +14,16 @@ pub(crate) trait CommandExecute { #[derive(Debug, Parser)] #[clap(version)] pub(crate) struct HarmonicCli { + #[clap(long, action(ArgAction::SetTrue), default_value = "false")] + pub(crate) dry_run: bool, #[clap(flatten)] pub(crate) instrumentation: arg::Instrumentation, - #[clap(long, default_value = "https://nixos.org/channels/nixpkgs-unstable")] - pub(crate) channels: Vec, + #[clap( + long, + value_parser, + default_value = "nixpkgs=https://nixos.org/channels/nixpkgs-unstable" + )] + pub(crate) channels: Vec, #[clap(long)] pub(crate) no_modify_profile: bool, #[clap(long, default_value = "32")] @@ -30,12 +35,14 @@ pub(crate) struct HarmonicCli { #[async_trait::async_trait] impl CommandExecute for HarmonicCli { #[tracing::instrument(skip_all, fields( - channels = %self.channels.iter().map(ToString::to_string).collect::>().join(", "), + channels = %self.channels.iter().map(|ChannelValue(name, url)| format!("{name} {url}")).collect::>().join(", "), daemon_user_count = %self.daemon_user_count, no_modify_profile = %self.no_modify_profile, + dry_run = %self.dry_run, ))] async fn execute(self) -> eyre::Result { let Self { + dry_run, instrumentation: _, daemon_user_count, channels, @@ -50,8 +57,13 @@ impl CommandExecute for HarmonicCli { let mut harmonic = Harmonic::default(); + harmonic.dry_run(dry_run); harmonic.daemon_user_count(daemon_user_count); - harmonic.channels(channels); + harmonic.channels( + channels + .into_iter() + .map(|ChannelValue(name, url)| (name, url)), + ); harmonic.modify_profile(!no_modify_profile); if !interaction::confirm("Are you ready to continue?").await? { @@ -67,6 +79,8 @@ impl CommandExecute for HarmonicCli { harmonic.setup_default_profile().await?; harmonic.place_nix_configuration().await?; + harmonic.configure_nix_daemon_service().await?; + Ok(ExitCode::SUCCESS) } } diff --git a/src/error.rs b/src/error.rs index 34c1171..a4ae4ca 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,45 +1,29 @@ #[derive(thiserror::Error, Debug)] pub enum HarmonicError { - #[error("Downloading Nix")] - DownloadingNix(#[from] reqwest::Error), - #[error("Unpacking Nix")] - UnpackingNix(std::io::Error), - #[error("Running `groupadd`")] - GroupAddSpawn(std::io::Error), - #[error("`groupadd` returned failure")] - GroupAddFailure(std::process::ExitStatus), - #[error("Running `useradd`")] - UserAddSpawn(std::io::Error), - #[error("`useradd` returned failure")] - UserAddFailure(std::process::ExitStatus), - #[error("Creating directory")] - CreateDirectory(std::io::Error), - #[error("Placing channel configuration")] - PlaceChannelConfiguration(std::io::Error), - #[error("Opening file `{0}`")] - OpeningFile(std::path::PathBuf, std::io::Error), - #[error("Writing to file `{0}`")] - WritingFile(std::path::PathBuf, std::io::Error), - #[error("Getting tempdir")] - GettingTempDir(std::io::Error), - #[error("Installing fetched Nix into the new store")] - InstallNixIntoStore(std::io::Error), - #[error("Installing fetched nss-cacert into the new store")] - InstallNssCacertIntoStore(std::io::Error), - #[error("Updating the Nix channel")] - UpdatingNixChannel(std::io::Error), - #[error("Globbing pattern error")] - GlobPatternError(glob::PatternError), - #[error("Could not find nss-cacert")] + #[error("Reqest error")] + Reqwest(#[from] reqwest::Error), + #[error("Unarchiving error")] + Unarchive(std::io::Error), + #[error("Getting temporary directory")] + TempDir(std::io::Error), + #[error("Glob pattern error")] + GlobPatternError(#[from] glob::PatternError), + #[error("Glob globbing error")] + GlobGlobError(#[from] glob::GlobError), + #[error("Symlinking from `{0}` to `{1}`")] + Symlink(std::path::PathBuf, std::path::PathBuf, std::io::Error), + #[error("Renaming from `{0}` to `{1}`")] + Rename(std::path::PathBuf, std::path::PathBuf, std::io::Error), + #[error("Unarchived Nix store did not appear to include a `nss-cacert` location")] NoNssCacert, - #[error("Creating /etc/nix/nix.conf")] - CreatingNixConf(std::io::Error), - #[error("No supported init syustem found")] + #[error("No supported init system found")] InitNotSupported, - #[error("Linking `{0}` to `{1}`")] - Linking(std::path::PathBuf, std::path::PathBuf, std::io::Error), - #[error("Running `systemd-tmpfiles`")] - SystemdTmpfiles(std::io::Error), + #[error("Creating directory `{0}`")] + CreateDirectory(std::path::PathBuf, std::io::Error), + #[error("Walking directory `{0}`")] + WalkDirectory(std::path::PathBuf, walkdir::Error), + #[error("Setting permissions `{0}`")] + SetPermissions(std::path::PathBuf, std::io::Error), #[error("Command `{0}` failed to execute")] CommandFailedExec(String, std::io::Error), // TODO(@Hoverbear): This should capture the stdout. @@ -47,4 +31,20 @@ pub enum HarmonicError { CommandFailedStatus(String), #[error("Join error")] JoinError(#[from] tokio::task::JoinError), + #[error("Opening file `{0}` for writing")] + OpenFile(std::path::PathBuf, std::io::Error), + #[error("Opening file `{0}` for writing")] + WriteFile(std::path::PathBuf, std::io::Error), + #[error("Seeking file `{0}` for writing")] + SeekFile(std::path::PathBuf, std::io::Error), + #[error("Changing ownership of `{0}`")] + Chown(std::path::PathBuf, nix::errno::Errno), + #[error("Getting uid for user `{0}`")] + UserId(String, nix::errno::Errno), + #[error("Getting user `{0}`")] + NoUser(String), + #[error("Getting gid for group `{0}`")] + GroupId(String, nix::errno::Errno), + #[error("Getting group `{0}`")] + NoGroup(String), } diff --git a/src/interaction.rs b/src/interaction.rs index 78fdfc4..a828e7b 100644 --- a/src/interaction.rs +++ b/src/interaction.rs @@ -10,7 +10,7 @@ pub(crate) async fn confirm(question: impl AsRef) -> eyre::Result { "\ {question}\n\ \n\ - {are_you_sure} ({yes}/{no})\ + {are_you_sure} ({yes}/{no}): \ ", question = question.as_ref(), are_you_sure = "Are you sure?".bright_white().bold(), diff --git a/src/lib.rs b/src/lib.rs index 60d9a01..e1039a4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,8 @@ mod error; use std::{ + ffi::OsStr, fs::Permissions, + io::SeekFrom, os::unix::fs::PermissionsExt, path::{Path, PathBuf}, process::ExitStatus, @@ -16,9 +18,9 @@ pub use nixos::NixOs; use bytes::Buf; use glob::glob; use reqwest::Url; +use tempdir::TempDir; use tokio::{ - fs::{create_dir, create_dir_all, set_permissions, symlink, OpenOptions}, - io::AsyncWriteExt, + io::{AsyncSeekExt, AsyncWriteExt}, process::Command, task::spawn_blocking, }; @@ -26,8 +28,9 @@ use tokio::{ // This uses a Rust builder pattern #[derive(Debug)] pub struct Harmonic { + dry_run: bool, daemon_user_count: usize, - channels: Vec, + channels: Vec<(String, Url)>, modify_profile: bool, nix_build_group_name: String, nix_build_group_id: usize, @@ -36,12 +39,16 @@ pub struct Harmonic { } impl Harmonic { + pub fn dry_run(&mut self, dry_run: bool) -> &mut Self { + self.dry_run = dry_run; + self + } pub fn daemon_user_count(&mut self, count: usize) -> &mut Self { self.daemon_user_count = count; self } - pub fn channels(&mut self, channels: impl IntoIterator) -> &mut Self { + pub fn channels(&mut self, channels: impl IntoIterator) -> &mut Self { self.channels = channels.into_iter().collect(); self } @@ -53,61 +60,64 @@ impl Harmonic { } impl Harmonic { + #[tracing::instrument(skip_all)] pub async fn fetch_nix(&self) -> Result<(), HarmonicError> { // TODO(@hoverbear): architecture specific download // TODO(@hoverbear): hash check - let res = reqwest::get( + // TODO(@hoverbear): custom url + let tempdir = TempDir::new("nix").map_err(HarmonicError::TempDir)?; + fetch_url_and_unpack_xz( "https://releases.nixos.org/nix/nix-2.11.0/nix-2.11.0-x86_64-linux.tar.xz", + tempdir.path(), + self.dry_run, ) - .await - .map_err(HarmonicError::DownloadingNix)?; - let bytes = res.bytes().await.map_err(HarmonicError::DownloadingNix)?; - // TODO(@Hoverbear): Pick directory - let handle: Result<(), HarmonicError> = spawn_blocking(|| { - let decoder = xz2::read::XzDecoder::new(bytes.reader()); - let mut archive = tar::Archive::new(decoder); - let destination = "/nix/install"; - archive - .unpack(destination) - .map_err(HarmonicError::UnpackingNix)?; - tracing::debug!(%destination, "Downloaded & extracted Nix"); - Ok(()) - }) .await?; - handle?; + let found_nix_path = if !self.dry_run { + // TODO(@Hoverbear): I would like to make this less awful + let found_nix_paths = glob::glob(&format!("{}/nix-*", tempdir.path().display()))? + .collect::, _>>()?; + assert_eq!( + found_nix_paths.len(), + 1, + "Did not expect to find multiple nix paths, please report this" + ); + found_nix_paths.into_iter().next().unwrap() + } else { + PathBuf::from("/nix/nix-*") + }; + rename(found_nix_path.join("store"), "/nix/store", self.dry_run).await?; Ok(()) } + #[tracing::instrument(skip_all)] pub async fn create_group(&self) -> Result<(), HarmonicError> { - let status = Command::new("groupadd") - .arg("-g") - .arg(self.nix_build_group_id.to_string()) - .arg("--system") - .arg(&self.nix_build_group_name) - .status() - .await - .map_err(HarmonicError::GroupAddSpawn)?; - if !status.success() { - Err(HarmonicError::GroupAddFailure(status)) - } else { - Ok(()) - } + execute_command( + Command::new("groupadd") + .arg("-g") + .arg(self.nix_build_group_id.to_string()) + .arg("--system") + .arg(&self.nix_build_group_name), + self.dry_run, + ) + .await?; + Ok(()) } + #[tracing::instrument(skip_all)] pub async fn create_users(&self) -> Result<(), HarmonicError> { for index in 1..=self.daemon_user_count { let user_name = format!("{}{index}", self.nix_build_user_prefix); let user_id = self.nix_build_user_id_base + index; - let status = Command::new("useradd") - .args([ + execute_command( + Command::new("useradd").args([ "--home-dir", "/var/empty", "--comment", &format!("\"Nix build user {user_id}\""), "--gid", - &self.nix_build_group_id.to_string(), + &self.nix_build_group_name.to_string(), "--groups", &self.nix_build_group_name.to_string(), "--no-user-group", @@ -119,24 +129,33 @@ impl Harmonic { "--password", "\"!\"", &user_name.to_string(), - ]) - .status() - .await - .map_err(HarmonicError::UserAddSpawn)?; - if !status.success() { - return Err(HarmonicError::UserAddFailure(status)); - } + ]), + self.dry_run, + ) + .await?; } Ok(()) } + + #[tracing::instrument(skip_all)] pub async fn create_directories(&self) -> Result<(), HarmonicError> { - let permissions = Permissions::from_mode(0o755); - let paths = [ + create_directory("/nix", self.dry_run).await?; + set_permissions( "/nix", + None, + Some("root".to_string()), + Some(self.nix_build_group_name.clone()), + self.dry_run, + ) + .await?; + + let permissions = Permissions::from_mode(0o0755); + let paths = [ "/nix/var", "/nix/var/log", "/nix/var/log/nix", "/nix/var/log/nix/drvs", + "/nix/var/nix", "/nix/var/nix/db", "/nix/var/nix/gcroots", "/nix/var/nix/gcroots/per-user", @@ -146,35 +165,54 @@ impl Harmonic { "/nix/var/nix/userpool", "/nix/var/nix/daemon-socket", ]; + for path in paths { // We use `create_dir` over `create_dir_all` to ensure we always set permissions right - create_dir_with_permissions(path, permissions.clone()) - .await - .map_err(HarmonicError::CreateDirectory)?; + create_directory(path, self.dry_run).await?; + set_permissions(path, Some(permissions.clone()), None, None, self.dry_run).await?; } + create_directory("/nix/store", self.dry_run).await?; + set_permissions( + "/nix/store", + Some(Permissions::from_mode(0o1775)), + None, + Some(self.nix_build_group_name.clone()), + self.dry_run, + ) + .await?; + create_directory("/etc/nix", self.dry_run).await?; + set_permissions( + "/etc/nix", + Some(Permissions::from_mode(0o0555)), + None, + None, + self.dry_run, + ) + .await?; Ok(()) } + #[tracing::instrument(skip_all)] pub async fn place_channel_configuration(&self) -> Result<(), HarmonicError> { - let mut file = OpenOptions::new() - .create(true) - .write(true) - .read(true) - .open("/root/.nix-channels") // TODO(@hoverbear): We should figure out the actual root dir - .await - .map_err(HarmonicError::PlaceChannelConfiguration)?; - let buf = self .channels .iter() - .map(ToString::to_string) + .map(|(name, url)| format!("{} {}", url, name)) .collect::>() .join("\n"); - file.write_all(buf.as_bytes()) - .await - .map_err(HarmonicError::PlaceChannelConfiguration)?; - Ok(()) + + create_file_if_not_exists("/root/.nix-channels", buf, self.dry_run).await?; + set_permissions( + "/root/.nix-channels", + Some(Permissions::from_mode(0o0664)), + None, + None, + self.dry_run, + ) + .await } + + #[tracing::instrument(skip_all)] pub async fn configure_shell_profile(&self) -> Result<(), HarmonicError> { const PROFILE_TARGETS: &[&str] = &[ "/etc/bashrc", @@ -191,86 +229,105 @@ impl Harmonic { # Nix\n\ if [ -e '{PROFILE_NIX_FILE}' ]; then\n\ . '{PROFILE_NIX_FILE}'\n\ - fi\n + fi\n\ # End Nix\n \n", ); - if path.exists() { - // TODO(@Hoverbear): Backup - // TODO(@Hoverbear): See if the line already exists, skip setting it - tracing::trace!("TODO"); - } else if let Some(parent) = path.parent() { - create_dir_all(parent).await.unwrap() - } - let mut file = OpenOptions::new() - .create(true) - .read(true) - .write(true) - .truncate(false) - .open(profile_target) - .await - .map_err(|e| HarmonicError::OpeningFile(path.to_owned(), e))?; - file.write_all(buf.as_bytes()) - .await - .map_err(|e| HarmonicError::WritingFile(path.to_owned(), e))?; + create_or_append_file(path, buf, self.dry_run).await?; } Ok(()) } + #[tracing::instrument(skip_all)] pub async fn setup_default_profile(&self) -> Result<(), HarmonicError> { - Command::new("/nix/install/bin/nix-env") - .arg("-i") - .arg("/nix/install") - .status() - .await - .map_err(HarmonicError::InstallNixIntoStore)?; - // Find an `nss-cacert` package, add it too. - let mut found_nss_ca_cert = None; - for entry in - glob("/nix/install/store/*-nss-cacert").map_err(HarmonicError::GlobPatternError)? - { - match entry { - Ok(path) => { - // TODO(@Hoverbear): Should probably ensure is unique - found_nss_ca_cert = Some(path); - break; - } - Err(_) => continue, /* Ignore it */ - }; - } - if let Some(nss_ca_cert) = found_nss_ca_cert { - let status = Command::new("/nix/install/bin/nix-env") - .arg("-i") - .arg(&nss_ca_cert) - .status() - .await - .map_err(HarmonicError::InstallNssCacertIntoStore)?; - if !status.success() { - // TODO(@Hoverbear): report + // Find an `nix` package + let nix_pkg_glob = "/nix/store/*-nix-*"; + let found_nix_pkg = if !self.dry_run { + let mut found_pkg = None; + for entry in glob(nix_pkg_glob).map_err(HarmonicError::GlobPatternError)? { + match entry { + Ok(path) => { + // TODO(@Hoverbear): Should probably ensure is unique + found_pkg = Some(path); + break; + } + Err(_) => continue, /* Ignore it */ + }; } - std::env::set_var("NIX_SSL_CERT_FILE", &nss_ca_cert); + found_pkg + } else { + // This is a mock for dry running. + Some(PathBuf::from(nix_pkg_glob)) + }; + let nix_pkg = if let Some(nix_pkg) = found_nix_pkg { + nix_pkg } else { return Err(HarmonicError::NoNssCacert); - } + }; + + execute_command( + Command::new(nix_pkg.join("bin/nix-env")) + .arg("-i") + .arg(&nix_pkg), + self.dry_run, + ) + .await?; + + // Find an `nss-cacert` package, add it too. + let nss_ca_cert_pkg_glob = "/nix/store/*-nss-cacert-*"; + let found_nss_ca_cert_pkg = if !self.dry_run { + let mut found_pkg = None; + for entry in glob(nss_ca_cert_pkg_glob).map_err(HarmonicError::GlobPatternError)? { + match entry { + Ok(path) => { + // TODO(@Hoverbear): Should probably ensure is unique + found_pkg = Some(path); + break; + } + Err(_) => continue, /* Ignore it */ + }; + } + found_pkg + } else { + // This is a mock for dry running. + Some(PathBuf::from(nss_ca_cert_pkg_glob)) + }; + + if let Some(nss_ca_cert_pkg) = found_nss_ca_cert_pkg { + execute_command( + Command::new(nix_pkg.join("bin/nix-env")) + .arg("-i") + .arg(&nss_ca_cert_pkg), + self.dry_run, + ) + .await?; + set_env( + "NIX_SSL_CERT_FILE", + "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt", + self.dry_run, + ); + nss_ca_cert_pkg + } else { + return Err(HarmonicError::NoNssCacert); + }; if !self.channels.is_empty() { - status_failure_as_error( - Command::new("/nix/install/bin/nix-channel") + execute_command( + Command::new(nix_pkg.join("bin/nix-channel")) .arg("--update") - .arg("nixpkgs"), + .arg("nixpkgs") + .env( + "NIX_SSL_CERT_FILE", + "/nix/var/nix/profiles/default/etc/ssl/certs/ca-bundle.crt", + ), + self.dry_run, ) .await?; } Ok(()) } + #[tracing::instrument(skip_all)] pub async fn place_nix_configuration(&self) -> Result<(), HarmonicError> { - let mut nix_conf = OpenOptions::new() - .create_new(true) - .write(true) - .read(true) - .open("/etc/nix/nix.conf") - .await - .map_err(HarmonicError::CreatingNixConf)?; let buf = format!( "\ {extra_conf}\n\ @@ -279,14 +336,10 @@ impl Harmonic { extra_conf = "", // TODO(@Hoverbear): populate me build_group_name = self.nix_build_group_name, ); - nix_conf - .write_all(buf.as_bytes()) - .await - .map_err(HarmonicError::CreatingNixConf)?; - - Ok(()) + create_file_if_not_exists("/etc/nix/nix.conf", buf, self.dry_run).await } + #[tracing::instrument(skip_all)] pub async fn configure_nix_daemon_service(&self) -> Result<(), HarmonicError> { if Path::new("/run/systemd/system").exists() { const SERVICE_SRC: &str = @@ -299,30 +352,38 @@ impl Harmonic { "/nix/var/nix/profiles/default//lib/tmpfiles.d/nix-daemon.conf"; const TMPFILES_DEST: &str = "/etc/tmpfiles.d/nix-daemon.conf"; - symlink(TMPFILES_SRC, TMPFILES_DEST).await.map_err(|e| { - HarmonicError::Linking(PathBuf::from(TMPFILES_SRC), PathBuf::from(TMPFILES_DEST), e) - })?; - status_failure_as_error( + symlink(TMPFILES_SRC, TMPFILES_DEST, self.dry_run).await?; + execute_command( Command::new("systemd-tmpfiles") .arg("--create") .arg("--prefix=/nix/var/nix"), + self.dry_run, + ) + .await?; + execute_command( + Command::new("systemctl").arg("link").arg(SERVICE_SRC), + self.dry_run, + ) + .await?; + execute_command( + Command::new("systemctl").arg("enable").arg(SOCKET_SRC), + self.dry_run, ) .await?; - status_failure_as_error(Command::new("systemctl").arg("link").arg(SERVICE_SRC)).await?; - status_failure_as_error(Command::new("systemctl").arg("enable").arg(SOCKET_SRC)) - .await?; // TODO(@Hoverbear): Handle proxy vars - status_failure_as_error(Command::new("systemctl").arg("daemon-reload")).await?; - status_failure_as_error( + execute_command(Command::new("systemctl").arg("daemon-reload"), self.dry_run).await?; + execute_command( Command::new("systemctl") .arg("start") .arg("nix-daemon.socket"), + self.dry_run, ) .await?; - status_failure_as_error( + execute_command( Command::new("systemctl") .arg("restart") .arg("nix-daemon.service"), + self.dry_run, ) .await?; } else { @@ -335,9 +396,13 @@ impl Harmonic { impl Default for Harmonic { fn default() -> Self { Self { - channels: vec!["https://nixos.org/channels/nixpkgs-unstable" - .parse::() - .unwrap()], + dry_run: true, + channels: vec![( + "nixpkgs".to_string(), + "https://nixos.org/channels/nixpkgs-unstable" + .parse::() + .unwrap(), + )], daemon_user_count: 32, modify_profile: true, nix_build_group_name: String::from("nixbld"), @@ -348,24 +413,256 @@ impl Default for Harmonic { } } -async fn create_dir_with_permissions( +#[tracing::instrument(skip_all, fields( + path = %path.as_ref().display(), + permissions = tracing::field::valuable(&permissions.clone().map(|v| format!("{:#o}", v.mode()))), + owner = tracing::field::valuable(&owner), + group = tracing::field::valuable(&group), +))] +async fn set_permissions( path: impl AsRef, - permissions: Permissions, -) -> Result<(), std::io::Error> { - let path = path.as_ref(); - create_dir(path).await?; - set_permissions(path, permissions).await?; + permissions: Option, + owner: Option, + group: Option, + dry_run: bool, +) -> Result<(), HarmonicError> { + use nix::unistd::{chown, Group, User}; + use walkdir::WalkDir; + if !dry_run { + let path = path.as_ref(); + let uid = if let Some(owner) = owner { + let uid = User::from_name(owner.as_str()) + .map_err(|e| HarmonicError::UserId(owner.clone(), e))? + .ok_or(HarmonicError::NoUser(owner))? + .uid; + Some(uid) + } else { + None + }; + let gid = if let Some(group) = group { + let gid = Group::from_name(group.as_str()) + .map_err(|e| HarmonicError::GroupId(group.clone(), e))? + .ok_or(HarmonicError::NoGroup(group))? + .gid; + Some(gid) + } else { + None + }; + for child in WalkDir::new(path) { + let entry = child.map_err(|e| HarmonicError::WalkDirectory(path.to_owned(), e))?; + if let Some(ref perms) = permissions { + tokio::fs::set_permissions(path, perms.clone()) + .await + .map_err(|e| HarmonicError::SetPermissions(path.to_owned(), e))?; + } + chown(entry.path(), uid, gid) + .map_err(|e| HarmonicError::Chown(entry.path().to_owned(), e))?; + } + } else { + tracing::info!("Dry run: Would recursively set permissions/ownership"); + } + Ok(()) } -async fn status_failure_as_error(command: &mut Command) -> Result { - let command_str = format!("{:?}", command.as_std()); - let status = command - .status() - .await - .map_err(|e| HarmonicError::CommandFailedExec(command_str.clone(), e))?; - match status.success() { - true => Ok(status), - false => Err(HarmonicError::CommandFailedStatus(command_str)), +#[tracing::instrument(skip_all, fields( + path = %path.as_ref().display(), +))] +async fn create_directory(path: impl AsRef, dry_run: bool) -> Result<(), HarmonicError> { + use tokio::fs::create_dir; + if !dry_run { + let path = path.as_ref(); + create_dir(path) + .await + .map_err(|e| HarmonicError::CreateDirectory(path.to_owned(), e))?; + } else { + tracing::info!("Dry run: Would create directory"); + } + + Ok(()) +} + +#[tracing::instrument(skip_all, fields(command = %format!("{:?}", command.as_std())))] +async fn execute_command( + command: &mut Command, + dry_run: bool, +) -> Result { + if !dry_run { + let command_str = format!("{:?}", command.as_std()); + let status = command + .status() + .await + .map_err(|e| HarmonicError::CommandFailedExec(command_str.clone(), e))?; + match status.success() { + true => Ok(status), + false => Err(HarmonicError::CommandFailedStatus(command_str)), + } + } else { + tracing::info!("Dry run: Would execute"); + // You cannot conjure "good" exit status in Rust without breaking the rules + // So... we conjure one from `true` + Command::new("true") + .status() + .await + .map_err(|e| HarmonicError::CommandFailedExec(String::from("true"), e)) + } +} + +#[tracing::instrument(skip_all, fields( + path = %path.as_ref().display(), + buf = %format!("```{}```", buf.as_ref()), +))] +async fn create_or_append_file( + path: impl AsRef, + buf: impl AsRef, + dry_run: bool, +) -> Result<(), HarmonicError> { + use tokio::fs::{create_dir_all, OpenOptions}; + let path = path.as_ref(); + let buf = buf.as_ref(); + if !dry_run { + if let Some(parent) = path.parent() { + create_dir_all(parent) + .await + .map_err(|e| HarmonicError::CreateDirectory(parent.to_owned(), e))?; + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .read(true) + .open(&path) + .await + .map_err(|e| HarmonicError::OpenFile(path.to_owned(), e))?; + + file.seek(SeekFrom::End(0)) + .await + .map_err(|e| HarmonicError::SeekFile(path.to_owned(), e))?; + file.write_all(buf.as_bytes()) + .await + .map_err(|e| HarmonicError::WriteFile(path.to_owned(), e))?; + } else { + tracing::info!("Dry run: Would create or append"); + } + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + path = %path.as_ref().display(), + buf = %format!("`{}`", buf.as_ref()), +))] +async fn create_file_if_not_exists( + path: impl AsRef, + buf: impl AsRef, + dry_run: bool, +) -> Result<(), HarmonicError> { + use tokio::fs::{create_dir_all, OpenOptions}; + let path = path.as_ref(); + let buf = buf.as_ref(); + if !dry_run { + if let Some(parent) = path.parent() { + create_dir_all(parent) + .await + .map_err(|e| HarmonicError::CreateDirectory(parent.to_owned(), e))?; + } + let mut file = OpenOptions::new() + .create(true) + .write(true) + .read(true) + .open(&path) + .await + .map_err(|e| HarmonicError::OpenFile(path.to_owned(), e))?; + + file.write_all(buf.as_bytes()) + .await + .map_err(|e| HarmonicError::WriteFile(path.to_owned(), e))?; + } else { + tracing::info!("Dry run: Would create (or error if exists)"); + } + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + src = %src.as_ref().display(), + dest = %dest.as_ref().display(), +))] +async fn symlink( + src: impl AsRef, + dest: impl AsRef, + dry_run: bool, +) -> Result<(), HarmonicError> { + let src = src.as_ref(); + let dest = dest.as_ref(); + if !dry_run { + tokio::fs::symlink(src, dest) + .await + .map_err(|e| HarmonicError::Symlink(src.to_owned(), dest.to_owned(), e))?; + } else { + tracing::info!("Dry run: Would symlink",); + } + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + src = %src.as_ref().display(), + dest = %dest.as_ref().display(), +))] +async fn rename( + src: impl AsRef, + dest: impl AsRef, + dry_run: bool, +) -> Result<(), HarmonicError> { + let src = src.as_ref(); + let dest = dest.as_ref(); + if !dry_run { + tokio::fs::rename(src, dest) + .await + .map_err(|e| HarmonicError::Rename(src.to_owned(), dest.to_owned(), e))?; + } else { + tracing::info!("Dry run: Would rename",); + } + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + url = %url.as_ref(), + dest = %dest.as_ref().display(), +))] +async fn fetch_url_and_unpack_xz( + url: impl AsRef, + dest: impl AsRef, + dry_run: bool, +) -> Result<(), HarmonicError> { + let url = url.as_ref(); + let dest = dest.as_ref().to_owned(); + if !dry_run { + let res = reqwest::get(url).await.map_err(HarmonicError::Reqwest)?; + let bytes = res.bytes().await.map_err(HarmonicError::Reqwest)?; + // TODO(@Hoverbear): Pick directory + let handle: Result<(), HarmonicError> = spawn_blocking(move || { + let decoder = xz2::read::XzDecoder::new(bytes.reader()); + let mut archive = tar::Archive::new(decoder); + archive.unpack(&dest).map_err(HarmonicError::Unarchive)?; + tracing::debug!(dest = %dest.display(), "Downloaded & extracted Nix"); + Ok(()) + }) + .await?; + + handle?; + } else { + tracing::info!("Dry run: Would fetch and unpack xz tarball"); + } + + Ok(()) +} + +#[tracing::instrument(skip_all, fields( + k = %k.as_ref().to_string_lossy(), + v = %v.as_ref().to_string_lossy(), +))] +fn set_env(k: impl AsRef, v: impl AsRef, dry_run: bool) { + if !dry_run { + std::env::set_var(k.as_ref(), v.as_ref()); + } else { + tracing::info!("Dry run: Would set env"); } }