diff --git a/.drone/drone.jsonnet b/.drone/drone.jsonnet index d7da58e..f4eaee7 100644 --- a/.drone/drone.jsonnet +++ b/.drone/drone.jsonnet @@ -10,7 +10,7 @@ local deploy() = { commands: [ "sudo chown 'makedeb:makedeb' ./ -R", ".drone/scripts/setup-pbmpr.sh", - "sudo apt-get install cargo libssl-dev pkg-config libapt-pkg-dev -y", + "sudo apt-get install rustup libssl-dev pkg-config libapt-pkg-dev -y", "cargo fmt --check", "cargo clippy -- -D warnings" ] diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 3170bfd..a954dce 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -18,7 +18,7 @@ jobs: run: | sudo chown 'makedeb:makedeb' ./ -R .drone/scripts/setup-pbmpr.sh - sudo apt-get install cargo libssl-dev pkg-config libapt-pkg-dev -y + sudo apt-get install rustup libssl-dev pkg-config libapt-pkg-dev -y cargo fmt --check cargo clippy -- -D warnings diff --git a/CHANGELOG.md b/CHANGELOG.md index c0f3072..e927a34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.9.0] - 2022-08-15 +### Added +- Added `install` command. +- Added `upgrade` command. +- Added `remove` command. +- Added `list` command. + +### Removed +- Removed `info` command. + ## [0.8.0] - 2022-08-04 ### Changed - Renamed project back to `Mist`. diff --git a/Cargo.lock b/Cargo.lock index b6eed8f..b338d54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10,13 +10,22 @@ checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] name = "aho-corasick" -version = "0.7.18" +version = "0.7.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" dependencies = [ "memchr", ] +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_colours" version = "1.1.1" @@ -66,23 +75,17 @@ checksum = "fd1212d80800b3d7614b3725e0b2ee3b45b2b7484805d54b5660c8fa6f706305" dependencies = [ "ansi_colours", "ansi_term", - "atty", "bincode", - "bugreport", "bytesize", - "clap 2.34.0", "clircle", "console", "content_inspector", - "dirs-next", "encoding", "flate2", - "git2", "globset", "grep-cli", "once_cell", "path_abs", - "regex", "semver", "serde", "serde_yaml", @@ -90,8 +93,6 @@ dependencies = [ "syntect", "thiserror", "unicode-width", - "walkdir", - "wild", ] [[package]] @@ -103,6 +104,21 @@ dependencies = [ "serde", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitflags" version = "1.3.2" @@ -120,28 +136,17 @@ dependencies = [ "regex-automata", ] -[[package]] -name = "bugreport" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "535120b8182547808081a66f1f77a64533c780b23da26763e0ee34dfb94f98c9" -dependencies = [ - "git-version", - "shell-escape", - "sys-info", -] - [[package]] name = "bumpalo" -version = "3.10.0" +version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" +checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" [[package]] name = "bytemuck" -version = "1.11.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5377c8865e74a160d21f29c2d40669f53286db6eab59b88540cbb12ffc8b835" +checksum = "2f5715e491b5a1598fc2bef5a606847b5dc1d48ea625bd3c02c00de8285591da" [[package]] name = "bytes" @@ -160,9 +165,6 @@ name = "cc" version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" -dependencies = [ - "jobserver", -] [[package]] name = "cfg-if" @@ -172,47 +174,33 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.19" +version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" +checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" dependencies = [ - "libc", + "iana-time-zone", + "js-sys", "num-integer", "num-traits", - "time 0.1.44", + "time", + "wasm-bindgen", "winapi 0.3.9", ] [[package]] name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "term_size", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - -[[package]] -name = "clap" -version = "3.2.16" +version = "3.2.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" +checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" dependencies = [ "atty", "bitflags", "clap_lex", "indexmap", "once_cell", - "strsim 0.10.0", + "strsim", "termcolor", - "textwrap 0.15.0", + "textwrap", ] [[package]] @@ -306,9 +294,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.73" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "873c2e83af70859af2aaecd1f5d862f3790b747b1f4f50fb45a931d000ac0422" +checksum = "fd0492b760d2a9b9c5d1e2132db2dcbba07a2045df6f14694b136eec5b2053ef" dependencies = [ "cc", "cxxbridge-flags", @@ -318,9 +306,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.73" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49edea7163bbc7a39e3d829b4b0b66a9d30486973152842b7413f2c7b5632bf" +checksum = "fc65e68d05a2bf20230ea07fff3f26aaa15d057d8361438859f0fe66a89ce288" dependencies = [ "cc", "codespan-reporting", @@ -333,15 +321,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.73" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f46b787c15af80277db5c88c6ac6c502ae545e622f010e06f95e540d34931acf" +checksum = "2ebfd09b0a600cf97b53c16eec6591893cbcaffe3f98c17c4e89b3b3a672c33a" [[package]] name = "cxxbridge-macro" -version = "1.0.73" +version = "1.0.76" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ba3f3a7efa46626878fb5d324fabca4d19d2956b6ae97ce43044ef4515f5abc" +checksum = "f1ccac323f5afe0678c7129b8346c2be2d2213d8c58507facfb4987d109abf1b" dependencies = [ "proc-macro2", "quote", @@ -357,16 +345,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -378,17 +356,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi 0.3.9", -] - [[package]] name = "edit" version = "0.1.4" @@ -401,9 +368,9 @@ dependencies = [ [[package]] name = "either" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f107b87b6afc2a64fd13cac55fe06d6c8859f12d4b14cbcdd2c67d0976781be" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" [[package]] name = "encode_unicode" @@ -490,6 +457,16 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de853764b47027c2e862a995c34978ffa63c1501f2e15f987ba11bd4f9bba193" +[[package]] +name = "fancy-regex" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d6b8560a05112eb52f04b00e5d3790c0dd75d9d980eb8a122fb23b92a623ccf" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -532,52 +509,51 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.0.1" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" dependencies = [ - "matches", "percent-encoding", ] [[package]] name = "futures-channel" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" dependencies = [ "futures-core", ] [[package]] name = "futures-core" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" [[package]] name = "futures-io" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" [[package]] name = "futures-sink" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" [[package]] name = "futures-task" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" [[package]] name = "futures-util" -version = "0.3.21" +version = "0.3.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" dependencies = [ "futures-core", "futures-io", @@ -599,47 +575,6 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] -[[package]] -name = "git-version" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6b0decc02f4636b9ccad390dcbe77b722a77efedfa393caf8379a51d5c61899" -dependencies = [ - "git-version-macro", - "proc-macro-hack", -] - -[[package]] -name = "git-version-macro" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe69f1cbdb6e28af2bac214e943b99ce8a0a06b447d15d3e61161b0423139f3f" -dependencies = [ - "proc-macro-hack", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "git2" -version = "0.14.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0155506aab710a86160ddb504a480d2964d7ab5b9e62419be69e0032bc5931c" -dependencies = [ - "bitflags", - "libc", - "libgit2-sys", - "log", - "url", -] - -[[package]] -name = "glob" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" - [[package]] name = "globset" version = "0.4.9" @@ -672,9 +607,9 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.13" +version = "0.3.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" dependencies = [ "bytes", "fnv", @@ -728,9 +663,9 @@ dependencies = [ [[package]] name = "httparse" -version = "1.7.1" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" @@ -775,13 +710,25 @@ dependencies = [ "tokio-native-tls", ] +[[package]] +name = "iana-time-zone" +version = "0.1.49" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3bbaead50122b06e9a973ac20bc7445074d99ad9a0a0654934876908a9cec82c" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "js-sys", + "wasm-bindgen", + "winapi 0.3.9", +] + [[package]] name = "idna" -version = "0.2.3" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -817,20 +764,11 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c8af84674fe1f223a982c933a0ee1086ac4d4052aa0fb8060c12c6ad838e754" -[[package]] -name = "jobserver" -version = "0.1.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" -dependencies = [ - "libc", -] - [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "49409df3e3bf0856b916e2ceaca09ee28e6871cf7d9ce97a692cacfdb2a25a47" dependencies = [ "wasm-bindgen", ] @@ -853,48 +791,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.127" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "505e71a4706fa491e9b1b55f51b95d4037d0821ee40131190475f692b35b009b" - -[[package]] -name = "libgit2-sys" -version = "0.13.4+1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fa6563431ede25f5cc7f6d803c6afbc1c5d3ad3d4925d12c882bf2b526f5d1" -dependencies = [ - "cc", - "libc", - "libz-sys", - "pkg-config", -] - -[[package]] -name = "libz-sys" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" -dependencies = [ - "cc", - "libc", - "pkg-config", - "vcpkg", -] - -[[package]] -name = "line-wrap" -version = "0.1.1" +version = "0.2.133" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9" -dependencies = [ - "safemem", -] +checksum = "c0f80d65747a3e43d1596c7c5492d95d5edddaabd45a7fcdb02b95f644164966" [[package]] name = "link-cplusplus" -version = "1.0.6" +version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8cae2cd7ba2f3f63938b9c724475dfb7b9861b545a90324476324ed21dbc8c8" +checksum = "9272ab7b96c9046fbc5bc56c06c117cb639fe2d509df0c421cad82d2915cf369" dependencies = [ "cc", ] @@ -915,10 +820,13 @@ dependencies = [ ] [[package]] -name = "matches" -version = "0.1.9" +name = "makedeb-srcinfo" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" +checksum = "6b01c135d0545ba393141008c9d8d7e4fed274f1651ead6c5999c17013a2c7e6" +dependencies = [ + "regex", +] [[package]] name = "memchr" @@ -934,9 +842,9 @@ checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] name = "miniz_oxide" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" +checksum = "96590ba8f175222643a85693f33d26e9c8a015f599c216509b1a6894af675d34" dependencies = [ "adler", ] @@ -959,13 +867,14 @@ version = "0.8.0" dependencies = [ "bat", "chrono", - "clap 3.2.16", + "clap", "colored", "dirs", "edit", "exitcode", "flate2", "lazy_static", + "makedeb-srcinfo", "quit", "regex", "reqwest", @@ -973,6 +882,9 @@ dependencies = [ "serde", "serde_json", "tempfile", + "termsize", + "users", + "which", ] [[package]] @@ -1022,15 +934,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "numtoa" version = "0.1.0" @@ -1039,31 +942,9 @@ checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" [[package]] name = "once_cell" -version = "1.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" - -[[package]] -name = "onig" -version = "6.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1eb3502504c9c8b06634b38bfdda86a9a8cef6277f3dec4d8b17c115110dd2a3" -dependencies = [ - "bitflags", - "lazy_static", - "libc", - "onig_sys", -] - -[[package]] -name = "onig_sys" -version = "69.8.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8bf3fbc9b931b6c9af85d219c7943c274a6ad26cff7488a2210215edd5f49bf8" -dependencies = [ - "cc", - "pkg-config", -] +checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" [[package]] name = "openssl" @@ -1112,9 +993,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.2.0" +version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" +checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" [[package]] name = "path_abs" @@ -1127,9 +1008,9 @@ dependencies = [ [[package]] name = "percent-encoding" -version = "2.1.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" [[package]] name = "pin-project-lite" @@ -1149,26 +1030,6 @@ version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" -[[package]] -name = "plist" -version = "1.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd39bc6cdc9355ad1dc5eeedefee696bb35c34caf21768741e81826c0bbd7225" -dependencies = [ - "base64", - "indexmap", - "line-wrap", - "serde", - "time 0.3.12", - "xml-rs", -] - -[[package]] -name = "proc-macro-hack" -version = "0.5.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" - [[package]] name = "proc-macro2" version = "1.0.43" @@ -1265,9 +1126,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.11" +version = "0.11.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" +checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" dependencies = [ "base64", "bytes", @@ -1281,10 +1142,10 @@ dependencies = [ "hyper-tls", "ipnet", "js-sys", - "lazy_static", "log", "mime", "native-tls", + "once_cell", "percent-encoding", "pin-project-lite", "serde", @@ -1302,18 +1163,17 @@ dependencies = [ [[package]] name = "rgb" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b221de559e4a29df3b957eec92bc0de6bc8eaf6ca9cfed43e5e1d67ff65a34" +checksum = "3603b7d71ca82644f79b5a06d1220e9a58ede60bd32255f698cb1af8838b8db3" dependencies = [ "bytemuck", ] [[package]] name = "rust-apt" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b2a2fd6635cd0029ce40610b3b6b10928d2b7bbbdda8fcd9eff229fc47c2bbe" +version = "0.4.1" +source = "git+https://gitlab.com/volian/rust-apt?branch=fix/tagfile-parsing#7750594be3ca30ee29804bf46cf76896dd61655b" dependencies = [ "cxx", "cxx-build", @@ -1327,12 +1187,6 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09" -[[package]] -name = "safemem" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072" - [[package]] name = "same-file" version = "1.0.6" @@ -1360,9 +1214,9 @@ checksum = "9c8132065adcfd6e02db789d9285a0deb2f3fcb04002865ab67d5fb103533898" [[package]] name = "security-framework" -version = "2.6.1" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" +checksum = "2bc1bb97804af6631813c55739f771071e0f2ed33ee20b68c86ec505d906356c" dependencies = [ "bitflags", "core-foundation", @@ -1383,24 +1237,24 @@ dependencies = [ [[package]] name = "semver" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93f6841e709003d68bb2deee8c343572bf446003ec20a583e76f7b15cebf3711" +checksum = "e25dfac463d778e353db5be2449d1cce89bd6fd23c9f1ea21310ce6e5a1b29c4" [[package]] name = "serde" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e590c437916fb6b221e1d00df6e3294f3fccd70ca7e92541c475d6ed6ef5fee2" +checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.142" +version = "1.0.145" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34b5b8d809babe02f538c2cfec6f2c1ed10804c0e5a6a041a049a4f5588ccc2e" +checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" dependencies = [ "proc-macro2", "quote", @@ -1409,9 +1263,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.83" +version = "1.0.85" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38dd04e3c8279e75b31ef29dbdceebfe5ad89f4d0937213c53f7d49d01b3d5a7" +checksum = "e55a28e3aaef9d5ce0506d0a14dbba8054ddc7e499ef522dd8b26859ec9d4a44" dependencies = [ "itoa", "ryu", @@ -1442,12 +1296,6 @@ dependencies = [ "yaml-rust", ] -[[package]] -name = "shell-escape" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45bb67a18fa91266cc7807181f62f9178a6873bfad7dc788c42e6430db40184f" - [[package]] name = "shell-words" version = "1.1.0" @@ -1465,9 +1313,9 @@ dependencies = [ [[package]] name = "socket2" -version = "0.4.4" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ "libc", "winapi 0.3.9", @@ -1479,12 +1327,6 @@ version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8207e78455ffdf55661170876f88daf85356e4edd54e0a3dbc79586ca1e50cbe" -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - [[package]] name = "strsim" version = "0.10.0" @@ -1493,9 +1335,9 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "syn" -version = "1.0.99" +version = "1.0.100" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58dbef6ec655055e20b86b15a8cc6d439cca19b667537ac6a1369572d151ab13" +checksum = "52205623b1b0f064a4e71182c3b18ae902267282930c6d5462c91b859668426e" dependencies = [ "proc-macro2", "quote", @@ -1510,29 +1352,17 @@ checksum = "c6c454c27d9d7d9a84c7803aaa3c50cd088d2906fe3c6e42da3209aa623576a8" dependencies = [ "bincode", "bitflags", + "fancy-regex", "flate2", "fnv", "lazy_static", "once_cell", - "onig", - "plist", "regex-syntax", "serde", "serde_derive", "serde_json", "thiserror", "walkdir", - "yaml-rust", -] - -[[package]] -name = "sys-info" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" -dependencies = [ - "cc", - "libc", ] [[package]] @@ -1549,16 +1379,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "term_size" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" -dependencies = [ - "libc", - "winapi 0.3.9", -] - [[package]] name = "termcolor" version = "1.1.3" @@ -1605,34 +1425,24 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "term_size", - "unicode-width", -] - -[[package]] -name = "textwrap" -version = "0.15.0" +version = "0.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1141d4d61095b28419e22cb0bbf02755f5e54e0526f97f1e3d1d160e60885fb" +checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" [[package]] name = "thiserror" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5f6586b7f764adc0231f4c79be7b920e766bb2f3e51b3661cdb263828f19994" +checksum = "c53f98874615aea268107765aa1ed8f6116782501d18e53d08b471733bea6c85" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.32" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12bafc5b54507e0149cdf1b145a5d80ab80a90bcd9275df43d4fff68460f6c21" +checksum = "f8b463991b4eab2d801e724172285ec4195c650e8ec79b149e6c2a8e6dd3f783" dependencies = [ "proc-macro2", "quote", @@ -1650,18 +1460,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "time" -version = "0.3.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74b7cc93fc23ba97fde84f7eea56c55d1ba183f495c6715defdfc7b9cb8c870f" -dependencies = [ - "itoa", - "js-sys", - "libc", - "num_threads", -] - [[package]] name = "tinyvec" version = "1.6.0" @@ -1679,9 +1477,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.20.1" +version = "1.21.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" +checksum = "0020c875007ad96677dcc890298f4b942882c5d4eb7cc8f439fc3bf813dc9c95" dependencies = [ "autocfg", "bytes", @@ -1707,9 +1505,9 @@ dependencies = [ [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "0bb2e075f03b3d66d8d8785356224ba688d2906a371015e225beeb65ca92c740" dependencies = [ "bytes", "futures-core", @@ -1759,48 +1557,51 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" +checksum = "dcc811dc4066ac62f84f11307873c4850cb653bfa9b1719cee2bd2204a4bc5dd" [[package]] name = "unicode-normalization" -version = "0.1.21" +version = "0.1.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854cbdc4f7bc6ae19c820d44abdc3277ac3e1b2b93db20a636825d9322fb60e6" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" dependencies = [ "tinyvec", ] [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "url" -version = "2.2.2" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] [[package]] -name = "vcpkg" -version = "0.2.15" +name = "users" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032" +dependencies = [ + "libc", + "log", +] [[package]] -name = "vec_map" -version = "0.8.2" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "walkdir" @@ -1837,9 +1638,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "eaf9f5aceeec8be17c128b2e93e031fb8a4d469bb9c4ae2d7dc1888b26887268" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1847,9 +1648,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "4c8ffb332579b0557b52d268b91feab8df3615f265d5270fec2a8c95b17c1142" dependencies = [ "bumpalo", "log", @@ -1862,9 +1663,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.32" +version = "0.4.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +checksum = "23639446165ca5a5de86ae1d8896b737ae80319560fbaa4c2887b7da6e7ebd7d" dependencies = [ "cfg-if", "js-sys", @@ -1874,9 +1675,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "052be0f94026e6cbc75cdefc9bae13fd6052cdcaf532fa6c45e7ae33a1e6c810" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1884,9 +1685,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "07bc0c051dc5f23e307b13285f9d75df86bfdf816c5721e573dec1f9b8aa193c" dependencies = [ "proc-macro2", "quote", @@ -1897,15 +1698,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.83" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "1c38c045535d93ec4f0b4defec448e4291638ee608530863b1e2ba115d4fff7f" [[package]] name = "web-sys" -version = "0.3.59" +version = "0.3.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +checksum = "bcda906d8be16e728fd5adc5b729afad4e444e106ab28cd1c7256e54fa61510f" dependencies = [ "js-sys", "wasm-bindgen", @@ -1913,22 +1714,13 @@ dependencies = [ [[package]] name = "which" -version = "4.2.5" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +checksum = "1c831fbbee9e129a8cf93e7747a82da9d95ba8e16621cae60ec2cdc849bacb7b" dependencies = [ "either", - "lazy_static", "libc", -] - -[[package]] -name = "wild" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "035793abb854745033f01a07647a79831eba29ec0be377205f2a25b0aa830020" -dependencies = [ - "glob", + "once_cell", ] [[package]] @@ -2026,12 +1818,6 @@ dependencies = [ "winapi 0.3.9", ] -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - [[package]] name = "yaml-rust" version = "0.4.5" diff --git a/Cargo.toml b/Cargo.toml index ebe81de..6e6bce0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "mist" -version = "0.8.0" +version = "0.10.0" authors = ["Hunter Wittenborn Mist needs the latest version of the Rust compiler toolchain in order to build. It may work with older releases, but they're not tested against and aren't guaranteed to work. If you're system's repositories don't contain the latest release, the Rust toolchain can be installed from the [MPR](https://mpr.makedeb.org/packages/rustc) or the Prebuilt-MPR. +> If you install from the MPR and omit `-H 'MPR-Package: yes'`, Mist will be **unable to update itself**. + +> Mist currently requires the nightly version of the Rust compiler toolchain in order to build. To build it locally, it's recommended to use [rustup](https://rustup.rs), which will automatically manage and update the nightly toolchain on your local system. If preferred, rustup can be installed from the [MPR](https://mpr.makedeb.org/packages/rustup) or the Prebuilt-MPR. ## Contributing If there's something you want added/fixed in Mist, feel free to open a pull request. There aren't many guidelines on what you should do quite yet, so just submit your changes and we can figure out what to do from there! diff --git a/completions/mist.bash b/completions/mist.bash index 5ee6989..71b82ca 100644 --- a/completions/mist.bash +++ b/completions/mist.bash @@ -1,5 +1,11 @@ _mist_get_pkglist() { - mapfile -t opts < <("${words[0]}" pkglist) + if ! printf '%s\n' "${@}" "${words[@]}" | grep -q -- '--mpr-only' || ! printf '%s\n' "${opts[@]}" | grep -q -- '--mpr-only'; then + mapfile -t COMPREPLY < <(apt-cache --no-generate pkgnames "${@: -1}") + fi + + if ([[ -f '/var/cache/mist/pkglist.gz' ]] && ! printf '%s\n' "${@}" "${words[@]}" | grep -q -- '--apt-only') || ! printf '%s\n' "${opts[@]}" | grep -q -- '--apt-only'; then + mapfile -O "${#COMPREPLY[@]}" -t COMPREPLY < <(gzip -cd '/var/cache/mist/pkglist.gz' | grep "^${@: -1}") + fi } _mist_gen_compreply() { @@ -10,8 +16,7 @@ _mist_pkg_specified_check() { if [[ "${#nonopts[@]}" -gt 3 ]]; then _mist_gen_compreply '${opts[@]}' "${cur}" else - _mist_get_pkglist - _mist_gen_compreply '${opts[@]}' "${cur}" + _mist_get_pkglist "${@}" fi } @@ -23,17 +28,16 @@ _mist() { 'clone' 'comment' 'help' - 'info' + 'install' + 'list' 'list-comments' + 'remove' 'search' 'update' + 'upgrade' 'whoami' ) - local opts=( - '--mpr-url' - '--token' - ) - + # Get a list of arguments that are nonoptions. mapfile -t nonopts < <(printf '%s\n' "${words[@]}" | grep -v '^-') @@ -43,9 +47,11 @@ _mist() { fi case "${nonopts[1]}" in - clone|info) + clone) + opts=('--mpr-url') + case "${prev}" in - --token|--mpr-url) + --mpr-url) return ;; esac @@ -56,26 +62,27 @@ _mist() { return ;; *) - _mist_pkg_specified_check + _mist_pkg_specified_check "${cur}" return ;; esac ;; comment) + opts=('--mpr-url' '--msg' '--token') + case "${prev}" in --token|--mpr-url|--msg) return ;; esac - opts+=('--msg') case "${cur}" in -*) _mist_gen_compreply '${opts[@]}' "${cur}" return ;; *) - _mist_pkg_specified_check + _mist_pkg_specified_check "${cur}" return ;; esac @@ -83,9 +90,31 @@ _mist() { help) return ;; + install) + opts=('--mpr-url') + + case "${prev}" in + --mpr-url) + return + ;; + esac + + case "${cur}" in + -*) + _mist_gen_compreply '${opts[@]}' "${cur}" + return + ;; + *) + _mist_pkg_specified_check "${cur}" + return + ;; + esac + ;; list-comments) + opts=('--mpr-url' '--paging') + case "${prev}" in - --token|--mpr-url) + --mpr-url) return ;; --paging) @@ -95,47 +124,79 @@ _mist() { ;; esac - opts+=('--paging') case "${cur}" in -*) _mist_gen_compreply '${opts[@]}' "${cur}" return ;; *) - _mist_pkg_specified_check + _mist_pkg_specified_check "${cur}" return ;; esac ;; - search) + remove) + opts=('--autoremove' '--purge') + + case "${cur}" in + -*) + _mist_gen_compreply '${opts[@]}' "${cur}" + return + ;; + *) + _mist_get_pkglist '--apt-only' "${cur}" + return + ;; + esac + ;; + + search|list) + opts=('--mpr-url' '--apt-only' '--mpr-only' '--name-only' '--installed') + case "${prev}" in - --token|--mpr-url) + --mpr-url) return ;; esac - opts+=('--apt-only' '--mpr-only') case "${cur}" in -*) _mist_gen_compreply '${opts[@]}' "${cur}" return ;; *) - _mist_pkg_specified_check + _mist_pkg_specified_check "${cur}" return ;; esac ;; update) + opts=('--mpr-url') + case "${prev}" in - --token|--mpr-url) + --mpr-url) return ;; esac _mist_gen_compreply '${opts[@]}' "${cur}" + return + ;; + upgrade) + opts=('--apt-only' '--mpr-only' '--mpr-url') + + case "${prev}" in + --mpr-url) + return + ;; + esac + + _mist_gen_compreply '${opts[@]}' "${cur}" + return ;; whoami) + opts=('--token' '--mpr-url') + case "${prev}" in --token|--mpr-url) return diff --git a/justfile b/justfile new file mode 100755 index 0000000..1dd42d4 --- /dev/null +++ b/justfile @@ -0,0 +1,35 @@ +#!/usr/bin/env -S just --justfile +set positional-arguments +export CARGO_RELEASE := "" + +default: + @just --list + +build *ARGS: + #!/usr/bin/env bash + set -e + + # Set Cargo args. + if [[ "${CARGO_RELEASE:+x}" == 'x' ]]; then + set -- "${@}" --release + BIN_PATH='target/release/mist' + else + BIN_PATH='target/debug/mist' + fi + + cargo build "${@}" + + if [[ "${NO_SUDO:+x}" == 'x' ]]; then + sudo_cmd='' + else + sudo_cmd='sudo' + fi + + ${sudo_cmd} chown 'root:root' "${BIN_PATH}" + ${sudo_cmd} chmod a+s "${BIN_PATH}" + +run *ARGS: + #!/usr/bin/env bash + set -e + just build + target/debug/mist "${@}" diff --git a/makedeb/PKGBUILD b/makedeb/PKGBUILD index 3db9894..6031b0d 100644 --- a/makedeb/PKGBUILD +++ b/makedeb/PKGBUILD @@ -2,18 +2,19 @@ pkgname=mist pkgver=0.8.0 pkgrel=1 -pkgdesc='The official helper for the makedeb Package Repository' +pkgdesc='The official command-line interface for the makedeb Package Repository' arch=('any') depends=( 'libapt-pkg-dev' 'libssl-dev' + 'sudo' ) optdepends=( 'r!less' ) makedepends=( 'asciidoctor' - 'cargo' + 'rustup' 'pkg-config' ) license=('GPL3') diff --git a/man/mist.1.adoc b/man/mist.1.adoc index 125bf75..a8a8f2a 100644 --- a/man/mist.1.adoc +++ b/man/mist.1.adoc @@ -5,15 +5,18 @@ :mansource: Git == NAME -mist - The official helper for the makedeb Package Repository +mist - The official command-line interface for the makedeb Package Repository == SYNOPSIS *mist* clone _pkgbase_ [_options_] ... *mist* comment _pkgbase_ [_options_] ... -*mist* info _pkgname_ [_options_] ... +*mist* install _pkg_ ... [_options_] ... +*mist* list _pkg_ [_options_] ... *mist* list-comments _pkgbase_ [_options_] ... +*mist* remove _pkgname_ ... [_options_] ... *mist* search _query_ ... [_options_] ... -*mist* update +*mist* update [_options_] ... +*mist* upgrade [_options_] ... *mist* whoami [_options_] ... == DESCRIPTION @@ -27,8 +30,8 @@ Clone the build files for a package base from the MPR. *comment*:: Comment on a package base's page on the MPR. -*info*:: -Get information about a package on the MPR. +*list*:: +Get information about APT or MPR packages. *list-comments*:: List comments of a package base on the MPR. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..271800c --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,2 @@ +[toolchain] +channel = "nightly" \ No newline at end of file diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 0000000..606e292 --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +wrap_comments = true \ No newline at end of file diff --git a/src/cache.rs b/src/cache.rs index ebe7136..024f38e 100644 --- a/src/cache.rs +++ b/src/cache.rs @@ -1,14 +1,32 @@ -use crate::message; -use flate2::{read::GzDecoder, write::GzEncoder, Compression}; -use regex::Regex; -use rust_apt::cache::{Cache as AptCache, PackageSort}; +use crate::{ + install_util, message, + progress::{MistAcquireProgress, MistInstallProgress}, + style::Colorize, + util, +}; +use flate2::read::GzDecoder; +use rust_apt::{ + cache::{Cache as AptCache, PackageSort}, + progress::{AcquireProgress, InstallProgress}, + tagfile::TagSection, +}; use serde::{Deserialize, Serialize}; use std::io::prelude::*; -use std::{collections::HashMap, fs, time::SystemTime}; +use std::{collections::HashMap, env, fs, io}; /////////////////////////// // Stuff for MPR caches. // /////////////////////////// +#[derive(Deserialize, Serialize, PartialEq, Eq)] +pub struct MprDependencyGroup { + #[serde(rename = "Distro")] + distro: Option, + #[serde(rename = "Arch")] + arch: Option, + #[serde(rename = "Packages")] + packages: Vec, +} + #[derive(Deserialize, Serialize, PartialEq)] pub struct MprPackage { #[serde(rename = "Name")] @@ -27,212 +45,200 @@ pub struct MprPackage { pub popularity: f32, #[serde(rename = "OutOfDate")] pub ood: Option, + #[serde(rename = "Depends")] + pub depends: Vec, + #[serde(rename = "MakeDepends")] + pub makedepends: Vec, + #[serde(rename = "CheckDepends")] + pub checkdepends: Vec, + #[serde(rename = "Conflicts")] + pub conflicts: Vec, + #[serde(rename = "Provides")] + pub provides: Vec, +} + +impl MprPackage { + fn get_pkg_group( + &self, + distro: Option<&str>, + arch: Option<&str>, + dep_groups: &Vec, + ) -> Option> { + let distro = distro.map(|distro| distro.to_owned()); + let arch = arch.map(|arch| arch.to_owned()); + + for dep_group in dep_groups { + if dep_group.distro == distro && dep_group.arch == arch { + return Some(dep_group.packages.clone()); + } + } + + None + } + + /// Get a list of depends packages for a specific distro/arch pair. + pub fn get_depends(&self, distro: Option<&str>, arch: Option<&str>) -> Option> { + self.get_pkg_group(distro, arch, &self.depends) + } + + /// Get a list of makedepends packages for a specific distro/arch pair. + pub fn get_makedepends(&self, distro: Option<&str>, arch: Option<&str>) -> Option> { + self.get_pkg_group(distro, arch, &self.makedepends) + } + + /// Get a list of checkdepends packages for a specific distro/arch pair. + pub fn get_checkdepends( + &self, + distro: Option<&str>, + arch: Option<&str>, + ) -> Option> { + self.get_pkg_group(distro, arch, &self.checkdepends) + } + + /// Get a list of conflicts packages for a specific distro/arch pair. + pub fn get_conflicts(&self, distro: Option<&str>, arch: Option<&str>) -> Option> { + self.get_pkg_group(distro, arch, &self.conflicts) + } + + /// Get a list of provides packages for a specific distro/arch pair. + pub fn get_provides(&self, distro: Option<&str>, arch: Option<&str>) -> Option> { + self.get_pkg_group(distro, arch, &self.provides) + } + + /// Get one of the above dependency vectors, looping through the order of + /// specificity for distro-architecture variables used by makedeb. + fn get_system_pkgs, Option<&str>) -> Option>>( + &self, + f: F, + distro: &str, + arch: &str, + ) -> Option> { + if let Some(deps) = f(self, Some(distro), Some(arch)) { + Some(deps) + } else if let Some(deps) = f(self, Some(distro), None) { + Some(deps) + } else if let Some(deps) = f(self, None, Some(arch)) { + Some(deps) + } else { + f(self, None, None) + } + } + + /// Get the `depends` values of this package, looping through the order of + /// specificity for distro-architecture variables used by makedeb. + pub fn get_system_depends(&self, distro: &str, arch: &str) -> Option> { + self.get_system_pkgs(Self::get_depends, distro, arch) + } + + /// Get the `makedepends` values of this package, looping through the order + /// of specificity for distro-architecture variables used by makedeb. + pub fn get_system_makedepends(&self, distro: &str, arch: &str) -> Option> { + self.get_system_pkgs(Self::get_makedepends, distro, arch) + } + /// Get the `checkdepends` values of this package, looping through the order + /// of specificity for distro-architecture variables used by makedeb. + pub fn get_system_checkdepends(&self, distro: &str, arch: &str) -> Option> { + self.get_system_pkgs(Self::get_checkdepends, distro, arch) + } + + /// Get the `conflicts` values of this package, looping through the order of + /// specificity for distro-architecture variables used by makedeb. + pub fn get_system_conflicts(&self, distro: &str, arch: &str) -> Option> { + self.get_system_pkgs(Self::get_conflicts, distro, arch) + } + + /// Get the `provides` values of this package, looping through the order of + /// specificity for distro-architecture variables used by makedeb. + pub fn get_system_provides(&self, distro: &str, arch: &str) -> Option> { + self.get_system_pkgs(Self::get_provides, distro, arch) + } } pub struct MprCache { - pub packages: Vec, + packages: HashMap, } impl MprCache { - pub fn new(mpr_url: &str) -> MprCache { - // Get the XDG cache directory. - let cache_dir = match dirs::cache_dir() { - Some(dir) => dir, - None => { - message::error("Unable to find the xdg cache directory."); - quit::with_code(exitcode::UNAVAILABLE); - } - }; + // Convert a Vector of MPR packages (the way they're stored on the MPR itself) + // into a HashMap that's accessible via key-value pairs. + fn vec_to_map(packages: Vec) -> HashMap { + let mut map = HashMap::new(); + + for pkg in packages { + let pkgname = pkg.pkgname.clone(); + map.insert(pkgname, pkg); + } - // Make sure the directory exists. - let mut mpr_cache_dir = cache_dir; - mpr_cache_dir.push("mpr-cli"); + map + } - if !mpr_cache_dir.exists() { - match fs::create_dir_all(mpr_cache_dir.clone()) { - Ok(()) => (), - Err(err) => { - message::error(&format!( - "Encountered an unknown error while creating the cache directory. [{}]", - err - )); - quit::with_code(exitcode::UNAVAILABLE); - } - } - } else if !mpr_cache_dir.is_dir() { - message::error(&format!( - "Cache path '{}' isn't a directory.", - mpr_cache_dir.display() - )); - quit::with_code(exitcode::OSERR); + pub fn validate_data(data: &[u8]) -> Result { + let mut file_gz = GzDecoder::new(data); + let mut file_json = String::new(); + + match file_gz.read_to_string(&mut file_json) { + Ok(_) => (), + Err(_) => return Err(()), } - // Try reading the cache file. If it doesn't exist or it's older than five minutes, we have to - // update the cache file. - let mut mpr_cache_file = mpr_cache_dir; - mpr_cache_file.push("cache.gz"); - - let mut update_cache = false; - - match fs::metadata(mpr_cache_file.clone()) { - // The file exists. Make sure it's been updated in the last five minutes. - Ok(metadata) => { - let five_minutes = 60 * 5; // The MPR updates package archives every five minutes. - let current_time = SystemTime::now() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - let file_last_modified = metadata - .modified() - .unwrap() - .duration_since(SystemTime::UNIX_EPOCH) - .unwrap() - .as_secs(); - - if (current_time - file_last_modified) > five_minutes { - update_cache = true; - }; - } - // The file doesn't exist. We need to create it. - Err(err) => { - if err.raw_os_error().unwrap() != 2 { - message::error(&format!( - "Encountered an unknown error while reading cache. [{}]", - err - )); - quit::with_code(exitcode::OSFILE); - } else { - update_cache = true; - - match fs::File::create(mpr_cache_file.clone()) { - Ok(_) => (), - Err(err) => { - message::error(&format!( - "Encountered an unknown error while reading cache. [{}]", - err - )); - quit::with_code(exitcode::OSFILE); - } - } - } - } + let cache = match serde_json::from_str::>(&file_json) { + Ok(json) => json, + Err(_) => return Err(()), }; - // If we need to, update the cache file. - if update_cache { - // Download the archive. - let resp = - match reqwest::blocking::get(format!("{}/packages-meta-ext-v2.json.gz", mpr_url)) { - Ok(resp) => resp, - Err(err) => { - message::error(&format!("Unable to make request. [{}]", err)); - quit::with_code(exitcode::UNAVAILABLE); - } - }; + Ok(Self { + packages: Self::vec_to_map(cache), + }) + } - if !resp.status().is_success() { - message::error(&format!( - "Failed to download package archive from the MPR. [{}]", - resp.status() - )); - quit::with_code(exitcode::TEMPFAIL); - } + pub fn new() -> Self { + // Try reading the cache file. If it doesn't exist or it's older than five + // minutes, we have to update the cache file. + let mut cache_file_path = util::xdg::get_global_cache_dir(); + cache_file_path.push("cache.gz"); - // Decompress the archive. - let cache = match valid_archive(resp) { + match fs::read(cache_file_path.clone()) { + Ok(file) => match Self::validate_data(&file) { Ok(cache) => cache, - Err(num) => { - if num == 1 { - message::error("Failed to decompress package archive from the MPR."); - quit::with_code(exitcode::TEMPFAIL); - } else { - message::error( - "Failed to verify integrity of package archive from the MPR.", - ); - quit::with_code(exitcode::TEMPFAIL); - } - } - }; - - // Now that the JSON has been verified, let's write out the archive to the cache file. - let mut config_compressor = GzEncoder::new(Vec::new(), Compression::default()); - config_compressor - .write_all(serde_json::to_string(&cache).unwrap().as_bytes()) - .unwrap(); - let config_gz = config_compressor.finish().unwrap(); - - match fs::write(mpr_cache_file, config_gz) { - Ok(()) => (), - Err(err) => { - message::error(&format!( - "Failed to write updated package archive. [{}]", - err - )); - quit::with_code(exitcode::IOERR); - } - } - - // Return the new cache object. - MprCache { packages: cache } - } else { - // The cache is less than 5 minutes old. We still need to validate that the cache is valid - // though. - let cache_file = match fs::File::open(mpr_cache_file.clone()) { - Ok(file) => file, - Err(err) => { + Err(_) => { message::error(&format!( - "Failed to write updated package archive. [{}]", - err + "There was an issue parsing the cache archive. Try running '{}'.\n", + "mist update".bold().green() )); - quit::with_code(exitcode::IOERR); - } - }; - - match valid_archive(cache_file) { - Ok(file) => MprCache { packages: file }, - Err(_) => { - // On an error, let's just remove the cache file and regenerate it by recalling - // this function. - fs::remove_file(mpr_cache_file).unwrap(); - self::MprCache::new(mpr_url) + quit::with_code(exitcode::UNAVAILABLE); } + }, + Err(err) => { + message::error(&format!( + "There was an issue reading the cache archive. Try running '{}' [{}].\n", + "mist update".bold().green(), + err.to_string().bold() + )); + quit::with_code(exitcode::UNAVAILABLE); } } } -} - -fn valid_archive(file: impl Read) -> Result, u32> { - let mut resp_gz = GzDecoder::new(file); - let mut resp_json = String::new(); - match resp_gz.read_to_string(&mut resp_json) { - Ok(_) => (), - Err(_) => return Err(1), + pub fn packages(&self) -> &HashMap { + &self.packages } - - // Feed the JSON into our struct. - let cache = match serde_json::from_str::>(&resp_json) { - Ok(json) => json, - Err(_) => return Err(2), - }; - - Ok(cache) } ///////////////////////////////////////////// // Stuff to handled shared APT/MPR caches. // ///////////////////////////////////////////// // -// Some of these fields only make sense to one type of package, but this kind of cache allows us to -// combine both types when needed, such as when providing search results. +// Some of these fields only make sense to one type of package, but this kind of +// cache allows us to combine both types when needed, such as when providing +// search results. -#[derive(PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub enum CachePackageSource { Apt, Mpr, } -#[derive(PartialEq)] +#[derive(Clone, PartialEq)] pub struct CachePackage { pub pkgname: String, pub pkgbase: Option, @@ -243,49 +249,46 @@ pub struct CachePackage { pub num_votes: Option, pub popularity: Option, pub ood: Option, - pub current_state: Option, pub source: CachePackageSource, - pub is_installed: Option, } pub struct Cache { - pub packages: Vec, - _initialized: bool, + /// The underlying APT cache struct. + apt_cache: AptCache, + /// The underlying MPR cache struct. + mpr_cache: MprCache, + /// A combined list of all packages in the cache. + //pkglist: Vec, + /// A map for getting all packages with a certain pkgname. Can be quicker + /// than looping over [`Self::pkglist`]. + pkgmap: HashMap>, } -// Create a new cache. impl Cache { - pub fn new(apt_cache: &AptCache, mpr_cache: &MprCache) -> Self { - let mut packages: Vec = Vec::new(); - - // Add APT packages. - let re = Regex::new(r":.*$").unwrap(); + /// Create a new cache. + pub fn new(apt_cache: AptCache, mpr_cache: MprCache) -> Self { + // Package list. + let mut pkglist = Vec::new(); - for pkg in apt_cache.packages(&PackageSort::default().names()) { - // Foreign architecture have ':{arch}' appended to the package name, but we don't want - // that since pkg.arch() contains that needed information anyway. - let pkgname = re.replace(&pkg.name(), "").to_string(); - let version = pkg.candidate().unwrap(); + for pkg in apt_cache.packages(&PackageSort::default()) { + let candidate = pkg.candidate().unwrap(); - packages.push(CachePackage { - pkgname, + pkglist.push(CachePackage { + pkgname: pkg.name(), pkgbase: None, - version: version.version(), - pkgdesc: Some(version.summary()), - arch: Some(version.arch()), + version: candidate.version(), + pkgdesc: candidate.summary(), + arch: Some(pkg.arch()), maintainer: None, num_votes: None, popularity: None, ood: None, - current_state: Some(pkg.current_state()), - is_installed: Some(pkg.is_installed()), source: CachePackageSource::Apt, }); } - // Add MPR packages. - for pkg in &mpr_cache.packages { - packages.push(CachePackage { + for pkg in mpr_cache.packages().values() { + pkglist.push(CachePackage { pkgname: pkg.pkgname.clone(), pkgbase: Some(pkg.pkgbase.clone()), version: pkg.version.clone(), @@ -295,89 +298,416 @@ impl Cache { num_votes: Some(pkg.num_votes), popularity: Some(pkg.popularity), ood: pkg.ood, - current_state: None, - is_installed: None, source: CachePackageSource::Mpr, }); } - Cache { - packages, - _initialized: true, + // Package map. + let mut pkgmap: HashMap> = HashMap::new(); + + for pkg in &pkglist { + let pkgname = pkg.pkgname.clone(); + + #[allow(clippy::map_entry)] + if pkgmap.contains_key(&pkgname) { + pkgmap.get_mut(&pkgname).unwrap().push(pkg.clone()); + } else { + pkgmap.insert(pkgname, vec![pkg.clone()]); + } } + + Self { + apt_cache, + mpr_cache, + //pkglist, + pkgmap, + } + } + + /// Get a reference to the APT cache passed into this function. + pub fn apt_cache(&self) -> &AptCache { + &self.apt_cache + } + + /// Get a reference to the MPR cache passed into this function. + pub fn mpr_cache(&self) -> &MprCache { + &self.mpr_cache } - // Get a list of unique pkgnames - if a package exists in both APT repos and the MPR, they'll - // be duplicated in the 'Cache.packages' list otherwise. - pub fn get_unique_pkgnames(&self) -> Vec<&String> { - let mut packages: Vec<&String> = Vec::new(); + /// Run a transaction. + /// `mpr_pkgs` is the list of MPR packages to install. + pub fn commit(&self, mpr_pkgs: &Vec>, mpr_url: &str) { + let mut to_install: Vec = Vec::new(); + let mut to_remove: Vec = Vec::new(); + let mut to_purge: Vec = Vec::new(); + let mut to_upgrade: Vec = Vec::new(); + let mut to_downgrade: Vec = Vec::new(); + + // Report APT packages. + for pkg in self.apt_cache().packages(&PackageSort::default()) { + let pkgname = pkg.name(); + let apt_string = format!("{}{}", "apt/".to_string().green(), &pkgname); + + if pkg.marked_install() { + to_install.push(apt_string); + } else if pkg.marked_delete() { + to_remove.push(apt_string); + } else if pkg.marked_purge() { + to_purge.push(apt_string); + } else if pkg.marked_upgrade() { + to_upgrade.push(apt_string); + } else if pkg.marked_downgrade() { + to_downgrade.push(apt_string); + } + } - for pkg in &self.packages { - packages.push(&pkg.pkgname); + // Report MPR packages. + for pkg in mpr_pkgs.iter().flatten() { + let mpr_string = format!("{}{}", "mpr/".to_owned().green(), pkg); + to_install.push(mpr_string); } - packages.sort_unstable(); - packages.dedup(); - packages - } + // Print out the transaction. + if to_install.is_empty() + && to_remove.is_empty() + && to_purge.is_empty() + && to_upgrade.is_empty() + && to_downgrade.is_empty() + { + println!("{}", "Nothing to do, quitting.".bold()); + quit::with_code(exitcode::OK); + }; + + if !to_install.is_empty() { + println!("{}", "The following packages will be installed:".bold()); + util::format_apt_pkglist(&to_install); + println!(); + } + + if !to_remove.is_empty() { + println!( + "{}", + format!("The following packages will be {}:", "removed".red()).bold() + ); + util::format_apt_pkglist(&to_remove); + println!(); + } + + if !to_purge.is_empty() { + println!( + "{}", + format!( + "The following packages (along with their configuration files) will be {}:", + "removed".red() + ) + .bold() + ); + util::format_apt_pkglist(&to_purge); + println!(); + } + + if !to_upgrade.is_empty() { + println!("{}", "The following packages will be upgraded:".bold()); + util::format_apt_pkglist(&to_upgrade); + println!(); + } + + if !to_downgrade.is_empty() { + println!("{}", "The following packages will be downgraded:".bold()); + util::format_apt_pkglist(&to_downgrade); + println!(); + } + + let (to_install_string, to_install_count) = { + let count = to_install.len(); + let string = match count { + 0 => "install".green(), + _ => "install".magenta(), + }; + (string, count) + }; + let (to_remove_string, to_remove_count) = { + let count = to_remove.len(); + let string = match count { + 0 => "remove".green(), + _ => "remove".magenta(), + }; + (string, count) + }; + let (to_upgrade_string, to_upgrade_count) = { + let count = to_upgrade.len(); + let string = match count { + 0 => "upgrade".green(), + _ => "upgrade".magenta(), + }; + (string, count) + }; + let (to_downgrade_string, to_downgrade_count) = { + let count = to_downgrade.len(); + let string = match count { + 0 => "downgrade".green(), + _ => "downgrade".magenta(), + }; + (string, count) + }; + + println!("{}", "Review:".bold()); + + println!( + "{}", + format!("- {} to {}", to_install_count, to_install_string).bold() + ); + println!( + "{}", + format!("- {} to {}", to_remove_count, to_remove_string).bold() + ); + println!( + "{}", + format!("- {} to {}", to_upgrade_count, to_upgrade_string).bold() + ); + println!( + "{}", + format!("- {} to {}", to_downgrade_count, to_downgrade_string).bold() + ); + + print!("{}", "\nWould you like to continue? [Y/n] ".bold()); + io::stdout().flush().unwrap(); + + let mut resp = String::new(); + io::stdin().read_line(&mut resp).unwrap(); + resp.pop(); + + if !util::is_yes(&resp, true) { + println!("{}", "Aborting...".bold()); + quit::with_code(exitcode::OK); + } + + println!(); + // Clone MPR packages. + // + // We should be able to flatten the `mpr_pkgs` list to get this variable, but I + // haven't gotten it to work yet. TODO: Make it work, duh. + let mut flattened_pkgnames = vec![]; + let mut flattened_pkgbases = vec![]; + let mpr_pkgbases = install_util::pkgnames_to_pkgbases(self, mpr_pkgs); + + for vec in mpr_pkgs { + for pkg in vec { + flattened_pkgnames.push(pkg.as_str()); + } + } + + for vec in &mpr_pkgbases { + for pkg in vec { + flattened_pkgbases.push(pkg.as_str()); + } + } + + install_util::clone_mpr_pkgs(&flattened_pkgbases, mpr_url); + + // Review MPR packages. + + // Get the editor to review package files with. + let editor = match edit::get_editor() { + Ok(editor) => editor.into_os_string().into_string().unwrap(), + Err(err) => { + message::error(&format!( + "Couldn't find an editor to review package files with. [{}]\n", + err + )); + + quit::with_code(exitcode::UNAVAILABLE); + } + }; + + for pkg in flattened_pkgbases { + println!(); + + loop { + message::question(&format!("Review files for '{}'? [Y/n] ", pkg.green())); + io::stdout().flush().unwrap(); - // Get a list of CachePackage objects that matche a certain pkgname. - pub fn package_map(&self) -> HashMap<&String, Vec<&CachePackage>> { - let mut packages: HashMap<&String, Vec<&CachePackage>> = HashMap::new(); + let mut resp = String::new(); + io::stdin().read_line(&mut resp).unwrap(); + resp.pop(); - for pkg in &self.packages { - match packages.get_mut(&&pkg.pkgname) { - Some(vec) => vec.push(pkg), - None => { - packages.insert(&pkg.pkgname, vec![pkg]); + if !util::is_yes(&resp, true) { + break; } + + let mut cache_dir = util::xdg::get_cache_dir(); + cache_dir.push("git-pkg"); + cache_dir.push(pkg); + + let mut cmd = util::sudo::run_as_normal_user(&editor); + cmd.arg("./"); + + let status = cmd.spawn().unwrap().wait().unwrap(); + util::check_exit_status(&cmd, &status) } } - packages - } + // Install APT packages. + let mut updater: Box = Box::new(MistAcquireProgress {}); + if self.apt_cache().get_archives(&mut updater).is_err() { + message::error("Failed to fetch needed archives\n"); + quit::with_code(exitcode::UNAVAILABLE); + } - // See if a package is available via APT. - // package_map is available from the package_map() function above. - pub fn available_apt( - &self, - package_map: &HashMap<&String, Vec<&CachePackage>>, - pkgname: &String, - ) -> bool { - match package_map.get(pkgname) { - Some(packages) => { - for pkg in packages { - match pkg.source { - CachePackageSource::Apt => return true, - _ => continue, + let mut installer: Box = Box::new(MistInstallProgress {}); + if let Err(err) = self.apt_cache().do_install(&mut installer) { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + // If we're not installing any MPR packages, go ahead and quit. + if mpr_pkgs.is_empty() { + quit::with_code(exitcode::OK); + } + + // Build and install MPR packages. + let current_dir = env::current_dir().unwrap(); + let mut cache_dir = util::xdg::get_cache_dir(); + cache_dir.push("git-pkg"); + + for pkg_group in mpr_pkgbases { + let mut debs = vec![]; + // The list of packages to install; A Vector containing pkgname/version pairs. + let mut install_list: Vec<[String; 2]> = vec![]; + + for pkg in pkg_group { + let mut git_dir = cache_dir.clone(); + git_dir.push(pkg.clone()); + env::set_current_dir(git_dir).unwrap(); + + // See this package has a control field value of 'MPR-Package'. If it does, + // don't add it to our arg list. TODO: We need to add this key + // to makedeb's .SRCINFO files. + let mpr_package_field = { + let mut cmd = util::sudo::run_as_normal_user("bash"); + cmd.arg("-c"); + cmd.arg("source PKGBUILD; printf '%s\n' \"${control_fields[@]}\" | grep -q '^MPR-Package:'"); + cmd.output().unwrap().status.success() + }; + + let mut cmd = util::sudo::run_as_normal_user("makedeb"); + + if !mpr_package_field { + cmd.arg("-H"); + cmd.arg("MPR-Package: yes"); + } + + message::info(&format!("Running makedeb for '{}'...\n", pkg.green())); + if !cmd.spawn().unwrap().wait().unwrap().success() { + message::error("Failed to run makedeb.\n"); + quit::with_code(exitcode::UNAVAILABLE); + } + + // Get the list of '.deb' files that were built. + for dir in fs::read_dir("./pkg").unwrap() { + let mut path = dir.unwrap().path(); + path.push("DEBIAN"); + path.push("control"); + let control_file = + TagSection::new(&fs::read_to_string(&path).unwrap()).unwrap(); + + // Only add this deb for installation if the user asked for it to be installed. + let pkgname = control_file.get("Package").unwrap(); + let version = control_file.get("Version").unwrap(); + let arch = control_file.get("Architecture").unwrap(); + + if flattened_pkgnames.contains(&pkgname.as_str()) { + debs.push(format!("{}_{}_{}.deb", pkgname, version, arch)); } + + install_list.push([ + pkgname.to_string(), + control_file.get("Version").unwrap().to_string(), + ]); } - false + env::set_current_dir(¤t_dir).unwrap(); + } + + // Convert the debs into the format required by the + // [`rust_apt::cache::Cache::debs`] initializer. + let mut debs_as_str = vec![]; + for deb in &debs { + debs_as_str.push(deb.as_str()); + } + + // Install the packages. + let deb_cache = AptCache::debs(&debs_as_str).unwrap(); + + for pkg in &install_list { + let cache_pkg = deb_cache.get(&pkg[0]).unwrap(); + let version = cache_pkg.get_version(&pkg[1]).unwrap(); + version.set_candidate(); + assert!(cache_pkg.mark_install(false, true)); + cache_pkg.protect(); + } + + if let Err(err) = deb_cache.resolve(true) { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + if deb_cache.get_archives(&mut updater).is_err() { + message::error("Failed to fetch needed archives\n"); + quit::with_code(exitcode::UNAVAILABLE); + } + + if let Err(err) = deb_cache.do_install(&mut installer) { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); } - None => false, } } - // See if a package is available on the MPR. - // package_map is available from the package_map() function above. - pub fn available_mpr( - &self, - package_map: &HashMap<&String, Vec<&CachePackage>>, - pkgname: &String, - ) -> bool { - match package_map.get(pkgname) { - Some(packages) => { - for pkg in packages { - match pkg.source { - CachePackageSource::Mpr => return true, - _ => continue, - } + /// Get a reference to the generated pkglist (contains a combined APT+MPR + /// cache). + /*pub fn pkglist(&self) -> &Vec { + &self.pkglist + }*/ + + /// Get a reference to the generated pkgmap (a key-value pair with keys of + /// pkgnames and values of lists of packages). Can be quicker than + /// [`Cache::pkglist`] if you're trying to lookup a package. + pub fn pkgmap(&self) -> &HashMap> { + &self.pkgmap + } + + // Get the APT variant of a package. + pub fn get_apt_pkg(&self, pkgname: &str) -> Option<&CachePackage> { + if let Some(pkglist) = self.pkgmap().get(&pkgname.to_owned()) { + for pkg in pkglist { + if let CachePackageSource::Apt = pkg.source { + return Some(pkg); } + } + } + None + } + + // Get the MPR variant of a package. + pub fn get_mpr_pkg(&self, pkgname: &str) -> Option<&CachePackage> { + if let Some(pkglist) = self.pkgmap().get(&pkgname.to_owned()) { + for pkg in pkglist { + if let CachePackageSource::Mpr = pkg.source { + return Some(pkg); + } + } + } + None + } - false + // Find the pkgbase of a given MPR package's pkgname. + pub fn find_pkgbase(&self, pkgname: &str) -> Option { + for pkg in self.mpr_cache().packages().values() { + if pkg.pkgname == pkgname { + return Some(pkg.pkgbase.clone()); } - None => false, } + None } } diff --git a/src/clone.rs b/src/clone.rs index 8990da7..e80cb49 100644 --- a/src/clone.rs +++ b/src/clone.rs @@ -1,39 +1,40 @@ -use crate::{cache::MprCache, message, util}; +use crate::{ + cache::{Cache, MprCache}, + message, util, +}; +use rust_apt::cache::Cache as AptCache; pub fn clone(args: &clap::ArgMatches) { let pkg: &String = args.get_one("pkg").unwrap(); let mpr_url: &String = args.get_one("mpr-url").unwrap(); - let mpr_cache = MprCache::new(mpr_url); - + let cache = Cache::new(AptCache::new(), MprCache::new()); let mut pkgbases: Vec<&String> = Vec::new(); // Get a list of package bases. - for pkg in &mpr_cache.packages { + for pkg in cache.mpr_cache().packages().values() { pkgbases.push(&pkg.pkgbase); } // Abort if the package base doesn't exist. if !pkgbases.contains(&pkg) { - message::error(&format!("Package base '{}' doesn't exist on the MPR.", pkg)); - - // If there's a pkgbase that builds this package, guide the user to clone that package - // instead. - let pkgbase = util::find_pkgbase(pkg, &mpr_cache); - - match pkgbase { - Some(pkgbase) => { - message::error( - &format!( - "Package base '{}' exists on the MPR though, which builds '{}'. You probably want to clone that instead:", - pkgbase, - &pkg - ) - ); - - message::error(&format!(" {} clone '{}'", clap::crate_name!(), pkgbase)); - } - - None => (), + message::error(&format!( + "Package base '{}' doesn't exist on the MPR.\n", + pkg + )); + + // If there's a pkgbase that builds this package, guide the user to clone that + // package instead. + if let Some(pkgbase) = cache.find_pkgbase(pkg) { + message::error(&format!( + "Package base '{}' exists on the MPR though, which builds '{}'. You probably want to clone that instead:\n", + pkgbase, + &pkg + )); + message::error(&format!( + " {} clone '{}'\n", + clap::crate_name!(), + pkgbase + )); } quit::with_code(exitcode::USAGE); @@ -41,15 +42,12 @@ pub fn clone(args: &clap::ArgMatches) { // Clone the package. let pkg_url = format!("{}/{}", mpr_url, pkg); - let cmd = util::CommandInfo { - args: &vec!["git", "clone", &pkg_url], - capture: false, - stdin: None, - }; - let exit_code = util::run_command(&cmd).exit_status; + let mut cmd = util::sudo::run_as_normal_user("git"); + cmd.args(["clone", &pkg_url]); + let exit_code = cmd.output().unwrap().status; if !exit_code.success() { - message::error("Failed to clone package."); + message::error("Failed to clone package.\n"); quit::with_code(exitcode::UNAVAILABLE); }; } diff --git a/src/color.rs b/src/color.rs deleted file mode 100644 index 41f3a5a..0000000 --- a/src/color.rs +++ /dev/null @@ -1,8 +0,0 @@ -pub use colored::Colorize; -use colored::CustomColor; -use lazy_static::lazy_static; - -lazy_static! { - pub static ref UBUNTU_ORANGE: CustomColor = CustomColor::new(255, 175, 0); - pub static ref UBUNTU_PURPLE: CustomColor = CustomColor::new(95, 95, 255); -} diff --git a/src/comment.rs b/src/comment.rs index d3a75ad..c960136 100644 --- a/src/comment.rs +++ b/src/comment.rs @@ -15,22 +15,22 @@ pub fn comment(args: &clap::ArgMatches) { let api_token: &String = match args.get_one("token") { Some(token) => token, None => { - message::error("No API token was provided."); + message::error("No API token was provided.\n"); quit::with_code(exitcode::USAGE); } }; // Get a list of packages. - let mpr_cache = MprCache::new(mpr_url); + let mpr_cache = MprCache::new(); let mut pkgnames: Vec<&String> = Vec::new(); - for pkg in &mpr_cache.packages { + for pkg in mpr_cache.packages().values() { pkgnames.push(&pkg.pkgname); } // Abort if the package base doesn't exist. if !pkgnames.contains(&pkg) { - message::error(&format!("Package '{}' doesn't exist on the MPR.", pkg)); + message::error(&format!("Package '{}' doesn't exist on the MPR.\n", pkg)); quit::with_code(exitcode::USAGE); } @@ -44,7 +44,7 @@ pub fn comment(args: &clap::ArgMatches) { Ok(editor) => editor.into_os_string().into_string().unwrap(), Err(err) => { message::error(&format!( - "Couldn't find an editor to write a comment with. [{}]", + "Couldn't find an editor to write a comment with. [{}]\n", err )); @@ -57,7 +57,7 @@ pub fn comment(args: &clap::ArgMatches) { Ok(file) => file.path().to_str().unwrap().to_owned(), Err(err) => { message::error(&format!( - "Failed to create temporary file to write comment in. [{}]", + "Failed to create temporary file to write comment in. [{}]\n", err )); quit::with_code(exitcode::UNAVAILABLE); @@ -65,14 +65,12 @@ pub fn comment(args: &clap::ArgMatches) { }; // Open the file in the editor. - message::info(&format!("Opening '{}' in '{}'...", &file, editor)); + message::info(&format!("Opening '{}' in '{}'...\n", &file, editor)); - let cmd = util::CommandInfo { - args: &vec![&editor, &file], - capture: false, - stdin: None, - }; - util::run_command(&cmd); + let mut cmd = util::sudo::run_as_normal_user(&editor); + cmd.arg(&file); + let status = cmd.spawn().unwrap().wait().unwrap(); + util::check_exit_status(&cmd, &status); // Read the changed file. let mut file_content = String::new(); @@ -91,5 +89,5 @@ pub fn comment(args: &clap::ArgMatches) { // Parse the message. let json = serde_json::from_str::(&resp_text).unwrap(); - message::info(&format!("Succesfully posted comment. [{}]", json.link)); + message::info(&format!("Succesfully posted comment. [{}]\n", json.link)); } diff --git a/src/info.rs b/src/info.rs deleted file mode 100644 index 02548bb..0000000 --- a/src/info.rs +++ /dev/null @@ -1,38 +0,0 @@ -use crate::{ - cache::{Cache, MprCache}, - message, search, util, -}; -use rust_apt::cache::Cache as AptCache; - -pub fn info(args: &clap::ArgMatches) { - let pkg: &String = args.get_one("pkg").unwrap(); - let mpr_url: &String = args.get_one("mpr-url").unwrap(); - let apt_cache = AptCache::new(); - let mpr_cache = MprCache::new(mpr_url); - let cache = Cache::new(&apt_cache, &mpr_cache); - let package_map = cache.package_map(); - - // Abort if the package base doesn't exist. - match package_map.get(&pkg) { - Some(_) => (), - None => { - message::error(&format!("Package '{}' doesn't exist on the MPR.", pkg)); - quit::with_code(exitcode::USAGE); - } - } - - // If the user wants to open the web browser page, go to that. - if args.contains_id("web") { - let pkg_url = format!("{}/packages/{}", mpr_url, pkg); - let cmd = util::CommandInfo { - args: &vec!["xdg-open", &pkg_url], - capture: false, - stdin: None, - }; - util::run_command(&cmd); - quit::with_code(exitcode::OK); - }; - - // Print the info for our package. - println!("{}", search::pkg_info(&package_map, pkg)); -} diff --git a/src/install.rs b/src/install.rs new file mode 100644 index 0000000..36e0043 --- /dev/null +++ b/src/install.rs @@ -0,0 +1,84 @@ +use crate::{ + cache::{Cache, MprCache}, + install_util, message, + style::Colorize, + util, +}; +use rust_apt::cache::Cache as AptCache; + +pub fn install(args: &clap::ArgMatches) { + let pkglist: Vec<&String> = args.get_many("pkg").unwrap().collect(); + let mpr_url: &String = args.get_one("mpr-url").unwrap(); + let cache = Cache::new(AptCache::new(), MprCache::new()); + + // Package sources. + let mut apt_pkgs: Vec<&str> = Vec::new(); + let mut mpr_pkgs: Vec<&str> = Vec::new(); + + // Check real quick for any packages that cannot be found. We don't want to ask + // the user anything else if there's packages that cannot be found, instead we + // should just show those packages and abort. + let mut unfindable = false; + + for pkg in &pkglist { + if cache.get_apt_pkg(pkg).is_none() && cache.get_mpr_pkg(pkg).is_none() { + message::error(&format!( + "Unable to find package '{}'.\n", + pkg.green().bold() + )); + unfindable = true; + } + } + + if unfindable { + quit::with_code(exitcode::USAGE); + } + + for pkg in &pkglist { + let apt_pkg = cache.get_apt_pkg(pkg); + let mpr_pkg = cache.get_mpr_pkg(pkg); + + if apt_pkg.is_some() && mpr_pkg.is_some() { + let resp = util::ask_question( + &format!("Package '{}' is available from multiple sources. Please select one to install:\n", pkg.green().bold()), + &vec!["APT", "MPR"], + false + ).remove(0); + println!(); + + if resp == "APT" { + apt_pkgs.push(pkg); + } else { + mpr_pkgs.push(pkg); + } + } else if apt_pkg.is_some() { + apt_pkgs.push(pkg); + } else if mpr_pkg.is_some() { + mpr_pkgs.push(pkg); + } + } + + // Mark any APT packages for installation. + for pkg in apt_pkgs { + let apt_pkg = cache.apt_cache().get(pkg).unwrap(); + + if !apt_pkg.mark_install(false, true) { + message::error(&format!( + "There was an issue marking '{}' for installation.\n", + pkg.green().bold() + )); + quit::with_code(exitcode::UNAVAILABLE); + } + } + + // Get the ordering for MPR package installation. + let mpr_install_order = install_util::order_mpr_packages(&cache, &mpr_pkgs); + + // Make sure any new marked APT packages are resolved properly. + if let Err(err) = cache.apt_cache().resolve(true) { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + cache.commit(&mpr_install_order, mpr_url); +} diff --git a/src/install_util.rs b/src/install_util.rs new file mode 100644 index 0000000..f27152f --- /dev/null +++ b/src/install_util.rs @@ -0,0 +1,369 @@ +use crate::{cache::Cache, message, style::Colorize, util}; +use rust_apt::{cache::Cache as AptCache, tagfile::TagSection}; +use std::{env, fs}; + +pub fn clone_mpr_pkgs(pkglist: &Vec<&str>, mpr_url: &str) { + let mut cache_dir = util::xdg::get_cache_dir(); + cache_dir.push("git-pkg"); + + // Lint checks for the cache dir. + if !cache_dir.exists() { + if fs::create_dir_all(&cache_dir).is_err() { + message::error(&format!( + "Failed to create directory for cache directory ({}).\n", + cache_dir + .into_os_string() + .into_string() + .unwrap() + .green() + .bold() + )); + quit::with_code(exitcode::UNAVAILABLE); + } + } else if !cache_dir.is_dir() { + message::error(&format!( + "Config directory path '{}' needs to be a directory, but it isn't.\n", + cache_dir + .into_os_string() + .into_string() + .unwrap() + .green() + .bold() + )); + quit::with_code(exitcode::UNAVAILABLE); + } + + // Check each package. + for pkg in pkglist { + let mut git_dir = cache_dir.clone(); + git_dir.push(pkg); + + // Clone the repository. + if !git_dir.exists() { + message::info(&format!( + "Cloning '{}' Git repository from the MPR...\n", + pkg.green().bold() + )); + + { + let mut cmd = util::sudo::run_as_normal_user("git"); + cmd.arg("clone"); + cmd.arg(format!("{}/{}", mpr_url, pkg)); + cmd.arg(git_dir.clone().into_os_string().into_string().unwrap()); + + let status = cmd.output().unwrap().status; + util::check_exit_status(&cmd, &status); + } + + env::set_current_dir(git_dir).unwrap(); + // Error out if it isn't a directory. + } else if !git_dir.is_dir() { + message::error(&format!( + "Path '{}' should be a folder, but is isn't.\n", + &git_dir + .into_os_string() + .into_string() + .unwrap() + .green() + .bold() + )); + quit::with_code(exitcode::UNAVAILABLE); + // Otherwise, make sure the repository is up to date. + } else { + env::set_current_dir(git_dir).unwrap(); + + message::info(&format!( + "Making sure Git repository for '{}' is up to date...\n", + pkg.green().bold() + )); + + // Checkout to the right branch. + { + let mut cmd = util::sudo::run_as_normal_user("git"); + cmd.args(["checkout", "master"]); + let status = cmd.output().unwrap().status; + util::check_exit_status(&cmd, &status); + } + + // Pull from the remote. + { + let mut cmd = util::sudo::run_as_normal_user("git"); + cmd.arg("pull"); + let status = cmd.output().unwrap().status; + util::check_exit_status(&cmd, &status); + } + } + } +} + +/// Order marked MPR packages for installation. +/// This function assumes all packages in `pkglist` actually exist and that all +/// changes have already been marked in the `cache` object. +pub fn order_mpr_packages(cache: &Cache, pkglist: &Vec<&str>) -> Vec> { + let mut cache_dir = util::xdg::get_global_cache_dir(); + cache_dir.push("deb-pkgs"); + env::set_current_dir(&cache_dir).unwrap(); + + // Get the list of MPR packages on this system. These are created in + // [`crate::update::update`]. + let mut debs_owned = vec![]; + + for path in fs::read_dir("./").unwrap() { + let filename = path.unwrap().file_name().into_string().unwrap(); + + if filename.ends_with(".deb") { + debs_owned.push(filename); + } + } + + let debs: Vec<&str> = debs_owned.iter().map(|s| s.as_str()).collect(); + + // Create a new cache object that we'll use to find what packages are to be + // installed from the MPR. + let new_cache = AptCache::debs(&debs).unwrap(); + + // Mirror the changes from the passed in cache into this one. + for pkg in cache.apt_cache().get_changes(false) { + let version = pkg.candidate().unwrap().version(); + let new_cache_pkg = new_cache.get(&pkg.name()).unwrap(); + new_cache_pkg.get_version(&version).unwrap().set_candidate(); + + if pkg.marked_install() + || pkg.marked_downgrade() + || pkg.marked_reinstall() + || pkg.marked_upgrade() + { + assert!(new_cache_pkg.mark_install(false, !pkg.is_auto_installed())); + } else if pkg.marked_delete() { + new_cache_pkg.mark_delete(false); + } else if pkg.marked_purge() { + new_cache_pkg.mark_delete(true); + } else if pkg.marked_keep() { + new_cache_pkg.mark_keep(); + } else { + unreachable!( + "Package '{}' is in an unknown state.", + pkg.name().bold().green() + ); + } + + new_cache_pkg.protect(); + } + + // Mark any MPR packages for installation. + for pkg_str in pkglist { + let pkg = new_cache.get(pkg_str).unwrap(); + + // Get the package's version in its control file. + let tagsection = + TagSection::new(&fs::read_to_string(pkg_str.to_string() + "/DEBIAN/control").unwrap()) + .unwrap(); + let version = tagsection.get("Version").unwrap(); + + pkg.get_version(version).unwrap().set_candidate(); + assert!(pkg.mark_install(false, true)); + pkg.protect(); + } + + // Resolve the cache. + if let Err(err) = new_cache.resolve(true) { + message::error("Couldn't resolve MPR packages.\n"); + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + // Get the list of changes for MPR packages. + let mut mpr_pkgs = vec![vec![]]; + let apt_cache = cache.apt_cache(); + + for pkg in new_cache.get_changes(false) { + let mut invalid_change: Option<&str> = None; + let mpr_pkg_change = { + if pkglist.contains(&pkg.name().as_str()) + && let Ok(string) = fs::read_to_string(pkg.name() + "/DEBIAN/control") + && let Ok(tagsection) = TagSection::new(&string) + && tagsection.get("Version").unwrap() == &pkg.candidate().unwrap().version() { + true + } else { + false + } + }; + + if !mpr_pkg_change { + let normal_pkg = apt_cache.get(&pkg.name()).unwrap(); + let normal_pkg_keep = normal_pkg.marked_keep(); + + // Mirror these changes back into the normal cache. + if pkg.marked_install() + || pkg.marked_downgrade() + || pkg.marked_reinstall() + || pkg.marked_upgrade() + { + if !normal_pkg_keep + && !normal_pkg.marked_install() + && pkg.marked_downgrade() + && pkg.marked_reinstall() + && pkg.marked_upgrade() + { + invalid_change = Some("install"); + } + (!mpr_pkg_change).then(|| assert!(normal_pkg.mark_install(false, true))); + } else if pkg.marked_delete() { + if !normal_pkg_keep && !normal_pkg.marked_delete() { + invalid_change = Some("delete"); + } + (!mpr_pkg_change).then(|| normal_pkg.mark_delete(false)); + } else if pkg.marked_purge() { + if !normal_pkg_keep && !normal_pkg.marked_purge() { + invalid_change = Some("purge"); + } + (!mpr_pkg_change).then(|| normal_pkg.mark_delete(true)); + } else if pkg.marked_keep() { + if !normal_pkg_keep { + invalid_change = Some("keep"); + } + (!mpr_pkg_change).then(|| normal_pkg.mark_keep()); + } + + if let Some(change) = invalid_change { + message::error(&format!( + "There was an issue marking '{}', as it was supposed to be marked for {} but wasn't.", + pkg.name().bold().green(), + change + )); + quit::with_code(exitcode::UNAVAILABLE); + } + + normal_pkg.protect(); + } else { + mpr_pkgs.first_mut().unwrap().push(pkg); + } + } + + // Order the MPR packages. + // + // The changed index. A tuple containing an array containing the element to + // remove (the vector's position, and the package position in that vector). + let mut changed_index: Option<[usize; 2]> = Some([0, 0]); + + while changed_index.is_some() { + let index_len = mpr_pkgs.len() - 1; + changed_index = None; + + 'main: for (vec_index, pkg_vec) in mpr_pkgs.iter().enumerate() { + for (pkg_index, pkg) in pkg_vec.iter().enumerate() { + // Get the dependencies of this package. + let dependencies: Vec = { + let mut deps = vec![]; + let version = pkg.candidate().unwrap(); + if let Some(dep_groups) = version.dependencies() { + for dep_grp in dep_groups { + for dep in &dep_grp.base_deps { + deps.push(dep.name().to_owned()); + } + } + } + + deps + }; + + // Loop over this vector and each one after this, and see if any of the + // packages it contains is a package from `dependencies`. If it + // is, this package needs to be moved to a vector after that + // package's vector. + if let Some(inner_pkg_vecs) = mpr_pkgs.get(vec_index..=index_len) { + for inner_pkg_vec in inner_pkg_vecs { + for inner_pkg in inner_pkg_vec { + // See if this package or any packages it provides are in the dependency + // list (i.e. 'lbrynet-bin' providing 'lbrynet' on the MPR). + let mut provides_list = inner_pkg.candidate().unwrap().provides_list(); + provides_list.push((inner_pkg.name(), None)); + + for (pkgname, _) in provides_list { + if dependencies.contains(&pkgname) { + changed_index = Some([vec_index, pkg_index]); + break 'main; + } + } + } + } + } + } + } + + if let Some(change) = changed_index { + let new_vec_position = change[0] + 1; + + // Remove the element from the specified position. + let pkg = mpr_pkgs.get_mut(change[0]).unwrap().remove(change[1]); + + if let Some(vec) = mpr_pkgs.get_mut(new_vec_position) { + vec.push(pkg); + } else { + mpr_pkgs.push(vec![pkg]); + } + } + } + + let mut returned_vec = vec![]; + + for vec in mpr_pkgs { + returned_vec.push(vec.iter().map(|pkg| pkg.name()).collect()); + } + + returned_vec +} + +#[allow(clippy::ptr_arg)] +// Convert a list of MPR packages (obtained from [`order_mpr_packages`]) into a +// list of MPR package bases. +pub fn pkgnames_to_pkgbases(cache: &Cache, pkglist: &Vec>) -> Vec> { + let mut returned_vec = vec![]; + + // Replace each entry in the list with its corresponding pkgbase. + for vec in pkglist.iter() { + let mut inner_vec = vec![]; + + for pkgname in vec { + let mpr_pkg = cache.mpr_cache().packages().get(pkgname).unwrap(); + inner_vec.push(mpr_pkg.pkgbase.clone()); + } + + returned_vec.push(inner_vec); + } + + // Remove any entries that are duplicates, considering the last valid option. + // I.e. a `Vec, Vec<"rustc", "toast">>` would turn into + // `Vec>`. + // Keep the element closest to the beginning of the main vector and discord any + // others in order to respect dependency installation order. + let mut removal_index: Option<[usize; 2]> = Some([0, 0]); + + while removal_index.is_some() { + removal_index = None; + let vec_len = returned_vec.len(); + + 'main: for (vec_index, vec) in returned_vec.iter().enumerate() { + for pkg in vec { + // Loop over each index after this one, and see if there's a matching package. + if let Some(inner_vecs) = returned_vec.get(vec_index..=vec_len) { + for (inner_vec_index, inner_vec) in inner_vecs.iter().enumerate() { + for (inner_pkg_index, inner_pkg) in inner_vec.iter().enumerate() { + if pkg == inner_pkg { + removal_index = Some([inner_vec_index, inner_pkg_index]); + break 'main; + } + } + } + } + } + } + + // If we found a matching package later in the index, remove it. + if let Some(index) = removal_index { + returned_vec.get_mut(index[0]).unwrap().remove(index[1]); + } + } + + returned_vec +} diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 0000000..2984018 --- /dev/null +++ b/src/list.rs @@ -0,0 +1,43 @@ +use crate::{ + cache::{Cache, CachePackage, MprCache}, + style, +}; +use rust_apt::cache::Cache as AptCache; + +pub fn list(args: &clap::ArgMatches) { + let pkglist: Vec<&String> = match args.get_many("pkg") { + Some(pkglist) => pkglist.collect(), + None => Vec::new(), + }; + let apt_only = args.is_present("apt-only"); + let mpr_only = args.is_present("mpr-only"); + let installed_only = args.is_present("installed-only"); + let name_only = args.is_present("name-only"); + + let cache = Cache::new(AptCache::new(), MprCache::new()); + let mut candidates: Vec<&Vec> = Vec::new(); + + if !pkglist.is_empty() { + for pkg in pkglist { + if let Some(pkg_group) = cache.pkgmap().get(pkg) { + candidates.push(pkg_group); + } + } + } else { + for pkg_group in cache.pkgmap().values() { + candidates.push(pkg_group); + } + } + + print!( + "{}", + style::generate_pkginfo_entries( + &candidates, + &cache, + apt_only, + mpr_only, + installed_only, + name_only + ) + ); +} diff --git a/src/list_comments.rs b/src/list_comments.rs index e1f3655..8e2fdf6 100644 --- a/src/list_comments.rs +++ b/src/list_comments.rs @@ -15,12 +15,12 @@ pub fn list_comments(args: &clap::ArgMatches) { let pkgbase: &String = args.get_one("pkg").unwrap(); let mpr_url: &String = args.get_one("mpr-url").unwrap(); let paging = args.get_one::("paging").unwrap().as_str(); - let mpr_cache = MprCache::new(mpr_url); + let mpr_cache = MprCache::new(); let mut pkgbases: Vec<&String> = Vec::new(); // Get a list of packages. - for pkg in &mpr_cache.packages { + for pkg in mpr_cache.packages().values() { pkgbases.push(&pkg.pkgbase); } diff --git a/src/main.rs b/src/main.rs index 22d9122..cd9bb78 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,43 +1,71 @@ +#![feature(let_chains)] mod cache; mod clone; -mod color; mod comment; -mod info; +mod install; +mod install_util; +mod list; mod list_comments; mod message; -mod pkglist; +mod progress; +mod remove; mod search; +mod style; mod update; +mod upgrade; mod util; mod whoami; use clap::{self, Arg, Command, PossibleValue}; +pub use rust_apt::util as apt_util; +use std::{ + env, + fs::File, + os::{linux::fs::MetadataExt, unix::fs::PermissionsExt}, +}; +use which::which; #[rustfmt::skip] fn get_cli() -> Command<'static> { + // Common arguments used in multiple commands. + let token_arg = Arg::new("token") + .help("The API token to authenticate to the MPR with") + .long("token") + .env("MPR_TOKEN") + .hide_env_values(true) + .takes_value(true) + .required(true); + + let mpr_url_arg = Arg::new("mpr-url") + .help("URL to access the MPR from") + .long("mpr-url") + .env("MPR_URL") + .hide_env_values(true) + .takes_value(true) + .default_value("https://mpr.makedeb.org"); + + let mpr_only_arg = Arg::new("mpr-only") + .help("Filter results to packages available on the MPR") + .long("mpr-only"); + + let apt_only_arg = Arg::new("apt-only") + .help("Filter results to packages available via APT") + .long("apt-only"); + + let installed_only_arg = Arg::new("installed-only") + .help("Filter results to installed packages") + .short('i') + .long("installed"); + + let name_only_arg = Arg::new("name-only") + .help("Output the package's name without any extra details") + .long("name-only"); + + // The CLI. Command::new(clap::crate_name!()) .version(clap::crate_version!()) .about(clap::crate_description!()) .arg_required_else_help(true) - .arg( - Arg::new("token") - .help("The API token to authenticate to the MPR with") - .long("token") - .env("MPR_TOKEN") - .hide_env_values(true) - .global(true) - .takes_value(true) - ) - .arg( - Arg::new("mpr-url") - .help("URL to access the MPR from") - .long("mpr-url") - .env("MPR_URL") - .hide_env_values(true) - .global(true) - .takes_value(true) - .default_value("https://mpr.makedeb.org") - ) .subcommand( Command::new("clone") .about("Clone a package base from the MPR") @@ -46,6 +74,7 @@ fn get_cli() -> Command<'static> { .help("The package to clone") .required(true) ) + .arg(mpr_url_arg.clone()) ) .subcommand( Command::new("comment") @@ -63,22 +92,32 @@ fn get_cli() -> Command<'static> { .short('m') .long("msg") ) + .arg(token_arg.clone()) + .arg(mpr_url_arg.clone()) ) .subcommand( - Command::new("info") - .arg_required_else_help(true) - .about("View information about an APT/MPR package") - .arg( - Arg::new("pkg") - .help("The package to view") - .required(true) - ) - .arg( - Arg::new("web") - .help("Open the page for the package in a web browser") - .short('w') - .long("web") - ) + Command::new("install") + .about("Install packages from APT and the MPR") + .arg( + Arg::new("pkg") + .help("The package(s) to install") + .multiple_values(true) + .required(true) + ) + .arg(mpr_url_arg.clone()) + ) + .subcommand( + Command::new("list") + .about("List packages available via APT and the MPR") + .arg( + Arg::new("pkg") + .help("The package(s) to get information for") + .multiple_values(true) + ) + .arg(mpr_only_arg.clone()) + .arg(apt_only_arg.clone()) + .arg(installed_only_arg.clone()) + .arg(name_only_arg.clone()) ) .subcommand( Command::new("list-comments") @@ -101,39 +140,61 @@ fn get_cli() -> Command<'static> { PossibleValue::new("never") ]) ) + .arg(mpr_url_arg.clone()) ) .subcommand( - Command::new("pkglist") - .hide(true) + Command::new("remove") + .about("Remove packages from the system") + .arg_required_else_help(true) + .arg( + Arg::new("pkg") + .help("The package(s) to remove") + .multiple_values(true) + ) + .arg( + Arg::new("purge") + .help("Remove configuration files along with the package(s)") + .long("purge") + ) + .arg( + Arg::new("autoremove") + .help("Automatically remove any unneeded packages") + .long("autoremove") + ) + .arg(mpr_url_arg.clone().hide(true)) ) .subcommand( Command::new("search") .about("Search for an APT/MPR package") .arg_required_else_help(true) .arg( - Arg::new("pkg") + Arg::new("query") .required(true) .help("The query to search for") .multiple_values(true) ) - .arg( - Arg::new("apt-only") - .help("Filter results to packages available via APT") - .long("apt-only") - ) - .arg( - Arg::new("mpr-only") - .help("Filter results to packages available on the MPR") - .long("mpr-only") - ) + .arg(mpr_only_arg.clone()) + .arg(apt_only_arg.clone()) + .arg(installed_only_arg.clone()) + .arg(name_only_arg.clone()) ) .subcommand( Command::new("update") .about("Update the APT cache on the system") + .arg(mpr_url_arg.clone()) + ) + .subcommand( + Command::new("upgrade") + .about("Upgrade the packages on the system") + .arg(Arg::new("apt-only").help("Only upgrade APT packages").long("apt-only").conflicts_with("mpr-only")) + .arg(Arg::new("mpr-only").help("Only upgrade MPR packages").long("mpr-only").conflicts_with("apt-only")) + .arg(mpr_url_arg.clone()) ) .subcommand( Command::new("whoami") .about("Show the currently authenticated user") + .arg(token_arg.clone()) + .arg(mpr_url_arg.clone()) ) } @@ -141,15 +202,59 @@ fn get_cli() -> Command<'static> { fn main() { let cmd_results = get_cli().get_matches(); + // Make sure that this executable has the `setuid` flag set and is owned by + // root. Parts of this program (intentionally) expect such behavior. + let cmd_name = { + let cmd = env::args().collect::>().remove(0); + if cmd.contains('/') { + cmd + } else { + which(cmd).unwrap().into_os_string().into_string().unwrap() + } + }; + + let cmd_metadata = File::open(cmd_name).unwrap().metadata().unwrap(); + + // Make sure `root` owns the executable. + if cmd_metadata.st_uid() != 0 { + message::error("This executable needs to be owned by `root` in order to run.\n"); + quit::with_code(exitcode::USAGE); + // Make sure the `setuid` bit flag is set. This appears to be third + // digit in the six-digit long mode returned. + } else if format!("{:o}", cmd_metadata.permissions().mode()) + .chars() + .nth(2) + .unwrap() + .to_string() + .parse::() + .unwrap() + < 4 + { + message::error( + "This executable needs to have the `setuid` bit flag set in order to run command.\n", + ); + quit::with_code(exitcode::USAGE); + } + + util::sudo::to_root(); + + // If we're running a command that should be permission-checked, then do so. + if vec!["install", "remove", "update", "upgrade"].contains(&cmd_results.subcommand().unwrap().0) + { + util::sudo::check_perms(); + } + match cmd_results.subcommand() { Some(("clone", args)) => clone::clone(args), Some(("comment", args)) => comment::comment(args), - Some(("info", args)) => info::info(args), + Some(("install", args)) => install::install(args), + Some(("list", args)) => list::list(args), Some(("list-comments", args)) => list_comments::list_comments(args), - Some(("pkglist", args)) => pkglist::pkglist(args), + Some(("remove", args)) => remove::remove(args), Some(("search", args)) => search::search(args), Some(("update", args)) => update::update(args), + Some(("upgrade", args)) => upgrade::upgrade(args), Some(("whoami", args)) => whoami::whoami(args), - _ => {} + _ => unreachable!(), }; } diff --git a/src/message.rs b/src/message.rs index db5d3ba..e5289fc 100644 --- a/src/message.rs +++ b/src/message.rs @@ -1,13 +1,17 @@ -use crate::color::Colorize; +use crate::style::Colorize; -pub fn info(str: &str) { - println!("{} {}", "Info:".cyan().bold(), str); +pub fn info(string: &str) { + print!("{} {}", "Info:".cyan().bold(), string); } -pub fn warning(str: &str) { - println!("{} {}", "Err:".yellow().bold(), str); +pub fn warning(string: &str) { + print!("{} {}", "Warning:".yellow().bold(), string); } -pub fn error(str: &str) { - println!("{} {}", "Err:".red().bold(), str); +pub fn error(string: &str) { + print!("{} {}", "Err:".red().bold(), string); +} + +pub fn question(string: &str) { + print!("{} {}", "Question:".magenta().bold(), string); } diff --git a/src/pkglist.rs b/src/pkglist.rs deleted file mode 100644 index addd802..0000000 --- a/src/pkglist.rs +++ /dev/null @@ -1,14 +0,0 @@ -use crate::cache::{Cache, MprCache}; -use rust_apt::cache::Cache as AptCache; - -pub fn pkglist(args: &clap::ArgMatches) { - let mpr_url = args.get_one::("mpr-url").unwrap(); - let apt_cache = AptCache::new(); - let mpr_cache = MprCache::new(mpr_url); - let cache = Cache::new(&apt_cache, &mpr_cache); - - // Print a list of packages. - for pkgname in cache.package_map().keys() { - println!("{}", pkgname); - } -} diff --git a/src/progress.rs b/src/progress.rs new file mode 100644 index 0000000..2341d4a --- /dev/null +++ b/src/progress.rs @@ -0,0 +1,151 @@ +use crate::{ + apt_util::{self, NumSys}, + style::Colorize, +}; +use rust_apt::progress::{AcquireProgress, InstallProgress, Worker}; +use std::io::{self, Write}; + +/// Acquire progress struct. +pub struct MistAcquireProgress {} + +impl AcquireProgress for MistAcquireProgress { + fn pulse_interval(&self) -> usize { + 500000 + } + + fn hit(&mut self, id: u32, description: String) { + println!( + "{}{} {}", + "Hit:".green().bold(), + id.to_string().green().bold(), + description + ); + } + + fn fetch(&mut self, id: u32, description: String, _file_size: u64) { + println!( + "{}{} {}", + "Get:".green().bold(), + id.to_string().green().bold(), + description + ); + } + + fn fail(&mut self, id: u32, description: String, status: u32, error_text: String) { + if status == 0 || status == 2 { + println!( + "{} {}", + format!("{}{} ({})", "Ign:", id, error_text).yellow().bold(), + description + ); + } else { + println!( + "{} {}", + format!("{}{} ({})", "Err:", id, error_text).red().bold(), + description + ); + } + } + + fn pulse( + &mut self, + _workers: Vec, + _percent: f32, + _total_bytes: u64, + _current_bytes: u64, + _current_cps: u64, + ) { + } + + fn done(&mut self) {} + + fn start(&mut self) {} + + fn stop( + &mut self, + fetched_bytes: u64, + elapsed_time: u64, + current_cps: u64, + _pending_errors: bool, + ) { + if fetched_bytes == 0 { + return; + } + + println!( + "{}", + format!( + "Fetched {} in {} ({}/s)", + apt_util::unit_str(fetched_bytes, NumSys::Decimal), + apt_util::time_str(elapsed_time), + apt_util::unit_str(current_cps, NumSys::Decimal) + ) + .bold() + ) + } +} + +/// Install progress struct. +pub struct MistInstallProgress {} + +impl InstallProgress for MistInstallProgress { + fn status_changed( + &mut self, + _pkgname: String, + steps_done: u64, + total_steps: u64, + _action: String, + ) { + // Get the terminal's width and height. + let term_height = apt_util::terminal_height(); + let term_width = apt_util::terminal_width(); + + // Save the current cursor position. + print!("\x1b7"); + + // Go to the progress reporting line. + print!("\x1b[{};0f", term_height); + io::stdout().flush().unwrap(); + + // Convert the float to a percentage string. + let percent = steps_done as f32 / total_steps as f32; + let mut percent_str = (percent * 100.0).round().to_string(); + + let percent_padding = match percent_str.len() { + 1 => " ", + 2 => " ", + 3 => "", + _ => unreachable!(), + }; + + percent_str = percent_padding.to_owned() + &percent_str; + + print!( + "{}", + format!("Progress: [{}{}] ", percent_str.blue(), "%".blue()).bold() + ); + + // The length of "Progress: [100%] ". + const PROGRESS_STR_LEN: usize = 17; + + // Print the progress bar. + // We should safely be able to convert the `usize`.try_into() into the `u32` + // needed by `get_apt_progress_string`, as usize ints only take up 8 bytes on a + // 64-bit processor. + print!( + "{}", + apt_util::get_apt_progress_string( + percent, + (term_width - PROGRESS_STR_LEN).try_into().unwrap() + ) + .bold() + ); + io::stdout().flush().unwrap(); + + // Finally, go back to the previous cursor position. + print!("\x1b8"); + io::stdout().flush().unwrap(); + } + + fn error(&mut self, _pkgname: String, _steps_done: u64, _total_steps: u64, _error: String) {} +} diff --git a/src/remove.rs b/src/remove.rs new file mode 100644 index 0000000..2c24a61 --- /dev/null +++ b/src/remove.rs @@ -0,0 +1,63 @@ +use crate::{ + apt_util, + cache::{Cache, MprCache}, + message, util, +}; +use rust_apt::cache::{Cache as AptCache, PackageSort}; + +pub fn remove(args: &clap::ArgMatches) { + let pkglist: Vec<&String> = { + if let Some(pkglist) = args.get_many("pkg") { + pkglist.collect() + } else { + Vec::new() + } + }; + let purge = args.is_present("purge"); + let autoremove = args.is_present("autoremove"); + let mpr_url: &String = args.get_one("mpr-url").unwrap(); + let cache = Cache::new(AptCache::new(), MprCache::new()); + + // Lock the cache. + if let Err(err) = apt_util::apt_lock() { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + // Remove the user requested packages. + for pkgname in pkglist { + if let Some(pkg) = cache.apt_cache().get(pkgname) { + if !pkg.is_installed() { + message::warning(&format!( + "Package '{}' isn't installed, so not removing.\n", + pkg.name(), + )); + continue; + } + + pkg.mark_delete(purge).then_some(()).unwrap(); + pkg.protect(); + } + } + + // Remove any packages that are no longer needed. + if autoremove { + for pkg in cache.apt_cache().packages(&PackageSort::default()) { + if pkg.is_auto_removable() { + pkg.mark_delete(purge).then_some(()).unwrap(); + pkg.protect(); + } + } + } + + if let Err(err) = cache.apt_cache().resolve(true) { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + // Unlock the cache so our transaction can complete. + apt_util::apt_unlock(); + + // Commit our changes. + cache.commit(&Vec::new(), mpr_url); +} diff --git a/src/search.rs b/src/search.rs index 27ccc94..cfb7297 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,204 +1,56 @@ use crate::{ - cache::{Cache, CachePackage, CachePackageSource, MprCache}, - color::{self, Colorize}, - message, + cache::{Cache, CachePackage, MprCache}, + style, }; -use chrono::{TimeZone, Utc}; use rust_apt::cache::Cache as AptCache; -use std::{collections::HashMap, fmt::Write}; pub fn search(args: &clap::ArgMatches) { - let pkglist: Vec<&String> = args.get_many("pkg").unwrap().collect(); - let mpr_url: &String = args.get_one("mpr-url").unwrap(); + let query_list: Vec<&String> = args.get_many("query").unwrap().collect(); let apt_only = args.is_present("apt-only"); let mpr_only = args.is_present("mpr-only"); - let apt_cache = AptCache::new(); - let mpr_cache = MprCache::new(mpr_url); - let cache = Cache::new(&apt_cache, &mpr_cache); + let installed_only = args.is_present("installed-only"); + let name_only = args.is_present("name-only"); - let mut matches: Vec<&String> = Vec::new(); - let package_map = cache.package_map(); + let cache = Cache::new(AptCache::new(), MprCache::new()); + let mut candidates: Vec<&Vec> = Vec::new(); - // Get matches. - for pkgname in cache.get_unique_pkgnames() { - // See if the package can be found in APT repos/the MPR if '--apt-only' or '--mpr-only' - // were passed in. - if ((apt_only && mpr_only) - && !(cache.available_apt(&package_map, pkgname) - && cache.available_mpr(&package_map, pkgname))) - || (apt_only && !cache.available_apt(&package_map, pkgname)) - || (mpr_only && !cache.available_mpr(&package_map, pkgname)) - { - continue; - } - - // We don't care what source the package is from, we just want the pkgname. - let pkg = package_map.get(pkgname).unwrap()[0]; + for query in query_list { + for (pkgname, pkg_group) in cache.pkgmap().iter() { + let mut pkgs = Vec::new(); + let apt_pkg = cache.get_apt_pkg(pkgname); + let mpr_pkg = cache.get_mpr_pkg(pkgname); - for arg in &pkglist { - if pkg.pkgname.contains(arg.as_str()) { - matches.push(&pkg.pkgname); + if let Some(pkg) = apt_pkg { + pkgs.push(pkg); + } + if let Some(pkg) = mpr_pkg { + pkgs.push(pkg); } - match &pkg.pkgdesc { - Some(pkgdesc) => { - if pkgdesc.to_lowercase().contains(arg.as_str()) { - matches.push(&pkg.pkgname); - } + for pkg in pkgs { + if (pkg.pkgname.contains(query) + || pkg + .pkgdesc + .as_ref() + .unwrap_or(&"".to_owned()) + .contains(query)) + && !candidates.contains(&pkg_group) + { + candidates.push(pkg_group); } - None => (), } } } - matches.sort_unstable(); - matches.dedup(); - - // Print matches. - // - // We'll be comparing this length against indexes, so subtract 1 so that it functions as if we - // started at 0. - // Also make sure to return if we got no matches, as we'll get an underflow then. - let matches_length = matches.len(); - - if matches_length == 0 { - message::info("No results."); - return; - } - - matches.sort_by_key(|a| a.to_lowercase()); - - let matches_length = matches_length - 1; - let mut result = String::new(); - - for (index, pkg) in matches.iter().enumerate() { - result.push_str(&pkg_info(&package_map, pkg)); - if index < matches_length { - result.push('\n'); - } - } - - print!("{}", result); -} - -pub fn pkg_info(package_map: &HashMap<&String, Vec<&CachePackage>>, pkg_str: &String) -> String { - let mut result = String::new(); - - // Get a list of sources for this package. - let packages = package_map.get(pkg_str).unwrap(); - // Create a sources vector to concatenate into a colored array-like string later (i.e. '[APT, MPR]'). - let mut sources: Vec<&str> = Vec::new(); - let pkg; - - if packages.len() == 2 { - sources.push("APT"); - sources.push("MPR"); - - // If the APT version of the package is installed and matches the MPR version, the APT - // version probably originated from the MPR, and we should use the MPR package version for - // search results. If the APT version is installed and its version doesn't match, it was - // probably installed from an APT repository and we should use the APT version. Otherwise, - // it's just not installed and we can show the MPR version. - let apt_pkg; - let mpr_pkg; - - match packages[0].source { - CachePackageSource::Apt => { - apt_pkg = packages[0]; - mpr_pkg = packages[1]; - } - _ => { - apt_pkg = packages[1]; - mpr_pkg = packages[2]; - } - }; - - if apt_pkg.is_installed.unwrap() && apt_pkg.version == mpr_pkg.version { - pkg = mpr_pkg; - } else if apt_pkg.is_installed.unwrap() { - pkg = apt_pkg; - } else { - pkg = mpr_pkg; - }; - } else { - pkg = packages[0]; - match pkg.source { - CachePackageSource::Apt => sources.push("APT"), - _ => sources.push("MPR"), - } - }; - - let mut sources_str = String::from("["); - - for source in sources { - write!( - sources_str, - "{}, ", - source.custom_color(*color::UBUNTU_PURPLE) + print!( + "{}", + style::generate_pkginfo_entries( + &candidates, + &cache, + apt_only, + mpr_only, + installed_only, + name_only ) - .unwrap(); - } - - // Remove the trailing ', ' at the end of the string. Then add the closing ']'. - sources_str.pop(); - sources_str.pop(); - sources_str.push(']'); - - // pkgname + version. - writeln!( - result, - "{}/{} {}", - pkg.pkgname.as_str().custom_color(*color::UBUNTU_ORANGE), - pkg.version, - sources_str - ) - .unwrap(); - - // pkgdesc. - match &pkg.pkgdesc { - Some(pkgdesc) => { - writeln!(result, "{} {}", "Description:".bold(), pkgdesc).unwrap(); - } - - None => (), - } - - // Maintainer. - match &pkg.maintainer { - Some(maintainer) => { - writeln!(result, "{} {}", "Maintainer:".bold(), maintainer).unwrap(); - } - - None => (), - } - - // Votes. - match &pkg.num_votes { - Some(num_votes) => writeln!(result, "{} {}", "Votes:".bold(), num_votes).unwrap(), - - None => (), - } - - // Popularity. - match &pkg.popularity { - Some(popularity) => writeln!(result, "{} {}", "Popularity:".bold(), popularity).unwrap(), - - None => (), - } - - // Out of date. - if let CachePackageSource::Mpr = pkg.source { - match &pkg.ood { - Some(ood) => { - let dt = Utc.timestamp(*ood as i64, 0).format("%Y-%m-%d").to_string(); - writeln!(result, "{} {}", "Out of Date:".bold(), dt).unwrap(); - } - - None => { - writeln!(result, "{} N/A", "Out of Date:".bold()).unwrap(); - } - } - } - - result + ); } diff --git a/src/style.rs b/src/style.rs new file mode 100644 index 0000000..1ad8456 --- /dev/null +++ b/src/style.rs @@ -0,0 +1,206 @@ +pub use colored::Colorize; +use colored::CustomColor; +use lazy_static::lazy_static; + +use chrono::{TimeZone, Utc}; + +use crate::{ + apt_util, + cache::{Cache, CachePackage}, +}; +use std::{cmp::Ordering, fmt::Write}; + +lazy_static! { + pub static ref UBUNTU_ORANGE: CustomColor = CustomColor::new(255, 175, 0); + pub static ref UBUNTU_PURPLE: CustomColor = CustomColor::new(95, 95, 255); +} + +/// Generate a colored package information entry. +/// If `name_only` is [`true`], the package name will be returned by itself. +pub fn generate_pkginfo_entry( + pkg_group: &[CachePackage], + cache: &Cache, + name_only: bool, +) -> String { + let pkgname = pkg_group.get(0).unwrap().pkgname.clone(); + + if name_only { + return pkgname; + } + + // Set up the string we'll return at the end of the function. + let mut return_string = String::new(); + + // Fancy colored pkgname to the max! :OOOOOOOOOOOOOOOOOO + write!(return_string, "{}", pkgname.custom_color(*UBUNTU_ORANGE)).unwrap(); + + // Get the APT and MPR packages. + let apt_pkg = cache.get_apt_pkg(&pkgname); + let mpr_pkg = cache.get_mpr_pkg(&pkgname); + + // Get the package sources. + let mut src_str = String::new(); + + if apt_pkg.is_some() && mpr_pkg.is_some() { + write!( + src_str, + "[{}, {}]", + "APT".custom_color(*UBUNTU_PURPLE), + "MPR".custom_color(*UBUNTU_PURPLE) + ) + .unwrap(); + } else if apt_pkg.is_some() { + write!(src_str, "[{}]", "APT".custom_color(*UBUNTU_PURPLE)).unwrap(); + } else if mpr_pkg.is_some() { + write!(src_str, "[{}]", "MPR".custom_color(*UBUNTU_PURPLE)).unwrap(); + } else { + unreachable!(); + } + + // Figure out what version and description to use, in this order: + // 1. APT if installed + // 2. MPR if present + // 3. APT + let pkgver: String; + let pkgdesc: Option; + + if let Some(apt_pkg_unwrapped) = apt_pkg && let Some(mpr_pkg_unwrapped) = mpr_pkg { + if cache.apt_cache().get(&apt_pkg_unwrapped.pkgname).unwrap().is_installed() { + pkgver = apt_pkg.unwrap().version.clone(); + pkgdesc = apt_pkg.unwrap().pkgdesc.clone(); + } else { + let apt_pkgver = &apt_pkg_unwrapped.version; + let mpr_pkgver = &mpr_pkg_unwrapped.version; + + match apt_util::cmp_versions(apt_pkgver, mpr_pkgver) { + Ordering::Greater | Ordering::Equal => { + pkgver = apt_pkgver.clone(); + pkgdesc = apt_pkg_unwrapped.pkgdesc.clone(); + } + Ordering::Less => { + pkgver = mpr_pkgver.clone(); + pkgdesc = mpr_pkg_unwrapped.pkgdesc.clone(); + } + } + } + } else if let Some(mpr_pkg_unwrapped) = mpr_pkg { + pkgver = mpr_pkg_unwrapped.version.clone(); + pkgdesc = mpr_pkg_unwrapped.pkgdesc.clone(); + } else { + let apt_pkg_unwrapped = apt_pkg.unwrap(); + pkgver = apt_pkg_unwrapped.version.clone(); + pkgdesc = apt_pkg_unwrapped.pkgdesc.clone(); + } + + // Add the first line and description, at long last. This string is the one + // we'll return at the end of the function. + write!(return_string, "/{} {}", pkgver, src_str).unwrap(); + + if let Some(unwrapped_pkgdesc) = pkgdesc { + write!( + return_string, + "\n{} {}", + "Description:".bold(), + unwrapped_pkgdesc + ) + .unwrap(); + } else { + write!(return_string, "\n{} N/A", "Description:".bold()).unwrap(); + } + + // If the MPR package exists, add some extra information about that. + if let Some(pkg) = mpr_pkg { + // Maintainer. + if let Some(maintainer) = &pkg.maintainer { + write!(return_string, "\n{} {}", "Maintainer:".bold(), maintainer).unwrap(); + } + + // Votes. + write!( + return_string, + "\n{} {}", + "Votes:".bold(), + &pkg.num_votes.unwrap() + ) + .unwrap(); + + // Popularity. + write!( + return_string, + "\n{} {}", + "Popularity:".bold(), + &pkg.popularity.unwrap() + ) + .unwrap(); + + // Out of Date. + let ood_date: String; + + if let Some(ood_epoch) = pkg.ood { + ood_date = Utc + .timestamp(ood_epoch as i64, 0) + .format("%Y-%m-%d") + .to_string(); + } else { + ood_date = "N/A".to_owned(); + } + + write!(return_string, "\n{} {}", "Popularity:".bold(), ood_date).unwrap(); + } + + return_string +} + +pub fn generate_pkginfo_entries( + pkgs: &Vec<&Vec>, + cache: &Cache, + apt_only: bool, + mpr_only: bool, + installed_only: bool, + name_only: bool, +) -> String { + let mut matches = Vec::new(); + let mut result_string = String::new(); + + for pkg_group in pkgs { + let pkgname = &pkg_group.get(0).unwrap().pkgname; + + // APT only. + if apt_only && cache.get_apt_pkg(pkgname).is_none() { + continue; + } + + // MPR only. + if mpr_only && cache.get_mpr_pkg(pkgname).is_none() { + continue; + } + + // Installed only. + if installed_only + && cache.get_apt_pkg(pkgname).is_some() + && !cache.apt_cache().get(pkgname).unwrap().is_installed() + { + continue; + } + + // Package be passed all the tests bro. We's be adding it to the vector now. + matches.push(pkg_group); + } + + let matches_len = matches.len(); + + for (index, pkg_group) in matches.iter().enumerate() { + if name_only { + result_string.push_str(&pkg_group.get(0).unwrap().pkgname); + result_string.push('\n'); + } else if index == matches_len - 1 { + result_string.push_str(&generate_pkginfo_entry(pkg_group, cache, name_only)); + result_string.push('\n'); + } else { + result_string.push_str(&generate_pkginfo_entry(pkg_group, cache, name_only)); + result_string.push_str("\n\n"); + } + } + + result_string +} diff --git a/src/update.rs b/src/update.rs index f9fb4a6..886a3a3 100644 --- a/src/update.rs +++ b/src/update.rs @@ -1,96 +1,233 @@ -use crate::{color::Colorize, message}; -use rust_apt::{ - cache::{time_str, unit_str, Cache as AptCache, NumSys}, - progress::UpdateProgress, - raw::apt::Worker, +use crate::{cache::MprCache, message, progress::MistAcquireProgress, style::Colorize, util}; +use makedeb_srcinfo::SplitDependency; +use rust_apt::{cache::Cache as AptCache, progress::AcquireProgress, tagfile::TagSection}; +use std::{ + env, fs, + io::{self, Write}, + path, + process::Command, }; -struct Update {} +pub fn update(args: &clap::ArgMatches) { + let mpr_url: &String = args.get_one("mpr-url").unwrap(); -impl UpdateProgress for Update { - fn pulse_interval(&self) -> usize { - 500000 - } + // For some reason we have to set our current UID to 0 instead of just the EUID + // when using setuid functionality. TODO: No clue why, but this fixes the + // issue for now. + users::switch::set_current_uid(0).unwrap(); - fn hit(&mut self, id: u32, description: String) { - println!( - "{}{} {}", - "Hit:".green().bold(), - id.to_string().green().bold(), - description - ); - } + // Update APT packages. + let cache = AptCache::new(); + let mut progress: Box = Box::new(MistAcquireProgress {}); - fn fetch(&mut self, id: u32, description: String, _file_size: u64) { - println!( - "{}{} {}", - "Get:".green().bold(), - id.to_string().green().bold(), - description - ); - } + if let Err(error) = cache.update(&mut progress) { + for msg in error.what().split(';') { + if msg.starts_with("E:") { + message::error(&format!("{}\n", msg.strip_prefix("E:").unwrap())); + } else if msg.starts_with("W:") { + message::warning(&format!("{}\n", msg.strip_prefix("W:").unwrap())); + }; + } + }; - fn fail(&mut self, id: u32, description: String, status: u32, error_text: String) { - if status == 0 || status == 2 { - println!( - "{} {}", - format!("{}{} ({})", "Ign:", id, error_text).yellow().bold(), - description - ); - } else { - println!( - "{} {}", - format!("{}{} ({})", "Err:", id, error_text).yellow().bold(), - description - ); + // Get the new MPR pkglist. + let client = reqwest::blocking::Client::new(); + match client.get(format!("{}/packages.gz", mpr_url)).send() { + Ok(resp) => { + let mut cache_dir = util::xdg::get_global_cache_dir(); + cache_dir.push("pkglist.gz"); + fs::write(&cache_dir, resp.bytes().unwrap()).unwrap(); } - } + Err(err) => { + message::error(&format!("Failed to make request [{}]\n", err)); + quit::with_code(exitcode::UNAVAILABLE); + } + }; - fn pulse( - &mut self, - _workers: Vec, - _percent: f32, - _total_bytes: u64, - _current_bytes: u64, - _current_cps: u64, - ) { + // Get the new MPR cache. + let resp = match client + .get(format!("{}/packages-meta-ext-v2.json.gz", mpr_url)) + .send() + { + Ok(resp) => resp.bytes().unwrap(), + Err(err) => { + message::error(&format!("Failed to make request [{}]\n", err)); + quit::with_code(exitcode::UNAVAILABLE); + } + }; + + let mpr_cache = match MprCache::validate_data(&resp) { + Ok(mpr_cache) => mpr_cache, + Err(_) => { + message::error("There was an issue validating the downloaded MPR cache archive."); + quit::with_code(exitcode::UNAVAILABLE); + } + }; + + // Create the '.deb' files for the packages in the MPR cache. + let mut cache_dir = util::xdg::get_global_cache_dir(); + cache_dir.push("deb-pkgs"); + + { + let dir_string = cache_dir.clone().into_os_string().into_string().unwrap(); + + util::fs::create_dir(&dir_string); + env::set_current_dir(&dir_string).unwrap(); } - fn done(&mut self) {} - - fn start(&mut self) {} - - fn stop( - &mut self, - fetched_bytes: u64, - elapsed_time: u64, - current_cps: u64, - _pending_errors: bool, - ) { - println!( - "{}", - format!( - "Fetched {} in {} ({}/s)", - unit_str(fetched_bytes, NumSys::Decimal), - time_str(elapsed_time), - unit_str(current_cps, NumSys::Decimal) - ) - .bold() - ) + let (system_distro, system_arch) = util::get_distro_arch_info(); + + // Get the list of packages we need to build. + let mut to_build: Vec = vec![]; + + for pkg in mpr_cache.packages().values() { + // If the deb doesn't exist, we have to build. + if !path::Path::new(&format!("{}.deb", pkg.pkgname)).exists() { + to_build.push(pkg.pkgname.clone()); + continue; + } + + let mut control_file_path = cache_dir.clone(); + control_file_path.push(&pkg.pkgname); + control_file_path.push("DEBIAN"); + control_file_path.push("control"); + + if path::Path::new(&control_file_path).exists() { + let control_file = + TagSection::new(&fs::read_to_string(&control_file_path).unwrap()).unwrap(); + + // If the version in the control file matches the current MPR package's version, + // then we don't need to update it. + if control_file.get("Version").unwrap() == &pkg.version { + continue; + } else { + to_build.push(pkg.pkgname.clone()); + } + } } -} -pub fn update(_args: &clap::ArgMatches) { - let cache = AptCache::new(); - let mut progress: Box = Box::new(Update {}); + let num_of_packages = to_build.len(); - if let Err(error) = cache.update(&mut progress) { - for msg in error.what().split(';') { - if msg.starts_with("E:") { - message::error(msg.strip_prefix("E:").unwrap()); - } else if msg.starts_with("W:") { - message::warning(msg.strip_prefix("W:").unwrap()); - }; + for (iter, pkg_string) in to_build.iter().enumerate() { + let pkg = mpr_cache.packages().get(pkg_string).unwrap(); + + // Generate the control file. + let mut control_file_str = String::new(); + control_file_str.push_str(&format!("Package: {}\n", pkg.pkgname)); + control_file_str.push_str(&format!("Version: {}\n", pkg.version)); + control_file_str.push_str("Architecture: all\n"); + control_file_str + .push_str("Description: Dummy description so 'dpkg-deb' doesn't complain.\n"); + + let mut depends = vec![]; + let mut predepends = vec![]; + + for dep_group in [ + pkg.get_system_depends(&system_distro, &system_arch), + pkg.get_system_makedepends(&system_distro, &system_arch), + pkg.get_system_checkdepends(&system_distro, &system_arch), + ] + .into_iter() + .flatten() + { + for dep in dep_group { + if let Some(no_prefix_string) = dep.strip_prefix("p!") { + predepends.push(no_prefix_string.to_string()); + } else { + depends.push(dep); + } + } } - }; + + if !depends.is_empty() { + let mut depends_items = String::new(); + for dep in depends { + depends_items.push_str(&SplitDependency::new(&dep).as_control()); + depends_items.push_str(", "); + } + depends_items.pop().unwrap(); + depends_items.pop().unwrap(); + + control_file_str.push_str(&format!("Depends: {}\n", &depends_items)); + } + + if !predepends.is_empty() { + let mut predepends_items = String::new(); + for predep in predepends { + predepends_items.push_str(&SplitDependency::new(&predep).as_control()); + predepends_items.push_str(", "); + } + predepends_items.pop().unwrap(); + predepends_items.pop().unwrap(); + + control_file_str.push_str(&format!("Pre-Depends: {}\n", &predepends_items)); + } + + if let Some(conflicts) = pkg.get_system_conflicts(&system_distro, &system_arch) { + let mut conflicts_items = String::new(); + + for conflict in conflicts { + conflicts_items.push_str(&SplitDependency::new(&conflict).as_control()); + conflicts_items.push_str(", "); + } + conflicts_items.pop().unwrap(); + conflicts_items.pop().unwrap(); + + control_file_str.push_str(&format!("Conflicts: {}\n", &conflicts_items)); + } + + if let Some(provides) = pkg.get_system_provides(&system_distro, &system_arch) { + let mut provides_items = String::new(); + + for provide in provides { + provides_items.push_str(&SplitDependency::new(&provide).as_control()); + provides_items.push_str(", "); + } + provides_items.pop().unwrap(); + provides_items.pop().unwrap(); + + control_file_str.push_str(&format!("Provides: {}\n", &provides_items)); + } + + // Write the control file. + let control_file_dir = pkg.pkgname.clone() + "/DEBIAN"; + util::fs::create_dir(&control_file_dir); + let mut control_file = util::fs::create_file(&(control_file_dir.clone() + "/control")); + control_file.write_all(control_file_str.as_bytes()).unwrap(); + + // Build the package. + let clear_line = || { + print!("\x1b[2K"); + io::stdout().flush().unwrap(); + print!("\x1b[0G"); + io::stdout().flush().unwrap(); + }; + clear_line(); + message::info(&format!( + "[{}/{}] Processing MPR package '{}'...", + iter + 1, + num_of_packages, + pkg.pkgname.bold().green(), + )); + io::stdout().flush().unwrap(); + + let cmd = Command::new("dpkg-deb") + .args(["-b", &pkg.pkgname]) + .output() + .unwrap(); + if !cmd.status.success() { + clear_line(); + message::error(&format!( + "Failed to process MPR package '{}'. The package won't be available to install from the MPR.\n", + pkg.pkgname.bold().green() + )); + } + } + + println!(); + + // Write the archive file. + cache_dir.pop(); + cache_dir.push("cache.gz"); + fs::write(&cache_dir, resp).unwrap(); } diff --git a/src/upgrade.rs b/src/upgrade.rs new file mode 100644 index 0000000..e1933da --- /dev/null +++ b/src/upgrade.rs @@ -0,0 +1,66 @@ +use crate::{ + cache::{Cache, MprCache}, + install_util, util, +}; +use rust_apt::{ + cache::{Cache as AptCache, PackageSort}, + tagfile, +}; +use std::{collections::HashMap, fs}; + +pub fn upgrade(args: &clap::ArgMatches) { + let apt_only = args.is_present("apt-only"); + let mpr_only = args.is_present("mpr-only"); + let mpr_url: &String = args.get_one("mpr-url").unwrap(); + + let cache = Cache::new(AptCache::new(), MprCache::new()); + + // Get the list of packages on this system. + let dpkg_pkgs = + tagfile::parse_tagfile(&fs::read_to_string("/var/lib/dpkg/status").unwrap()).unwrap(); + + // Convert it into a [`HashMap`] for easier access. + let mut dpkg_map = HashMap::new(); + + for pkg in dpkg_pkgs { + dpkg_map.insert(pkg.get("Package").unwrap().to_owned(), pkg); + } + + // The list of MPR packages we're going to update. + let mut mpr_pkgs = vec![]; + + // Check which APT packages need upgrading, and mark any for such if needed. + for pkg in cache.apt_cache().packages(&PackageSort::default()) { + let pkgname = pkg.name(); + + if !mpr_only && pkg.is_upgradable(false) && let Some(pkg_control) = dpkg_map.get(&pkgname) && pkg_control.get("MPR-Package").is_none() { + pkg.mark_install(false, !pkg.is_auto_installed()); + pkg.protect(); + } else if !apt_only && let Some(pkg_control) = dpkg_map.get(&pkgname) && pkg_control.get("MPR-Package").is_some() { + // See if the MPR version is more recent. If so, add the package for installation. + if crate::apt_util::cmp_versions( + dpkg_map.get(&pkgname).unwrap().get("Version").unwrap(), + &cache.mpr_cache().packages().get(&pkgname).unwrap().version, + ) + .is_lt() + { + mpr_pkgs.push(pkgname); + } + } + } + + // Get the ordering for MPR package installation. + let mpr_install_order = install_util::order_mpr_packages( + &cache, + &mpr_pkgs.iter().map(|pkg| pkg.as_str()).collect(), + ); + + // Make sure any new marked APT packages are resolved properly. + if let Err(err) = cache.apt_cache().resolve(true) { + util::handle_errors(&err); + quit::with_code(exitcode::UNAVAILABLE); + } + + crate::message::warning(&format!("{}\n", mpr_install_order.len())); + cache.commit(&mpr_install_order, mpr_url); +} diff --git a/src/util.rs b/src/util.rs index 2dd82e9..cb4f2a6 100644 --- a/src/util.rs +++ b/src/util.rs @@ -1,8 +1,14 @@ -use crate::{cache::MprCache, message}; +use crate::{apt_util, message, style::Colorize}; +use core::fmt::Display; +use lazy_static::lazy_static; +use regex::Regex; use serde::{Deserialize, Serialize}; use std::{ - io::Write, - process::{Command, ExitStatus, Stdio}, + ffi::OsStr, + fs as std_fs, + io::{self, Write}, + path, + process::{Command as ProcCommand, ExitStatus}, str, }; @@ -28,20 +34,20 @@ impl<'a> AuthenticatedRequest<'a> { let resp = match resp { Ok(resp) => resp, Err(err) => { - message::error(&format!("Failed to make request [{}]", err)); + message::error(&format!("Failed to make request [{}]\n", err)); quit::with_code(exitcode::UNAVAILABLE); } }; - // Check the response and see if we got a bad API token error. If we did, go ahead and - // abort the program. + // Check the response and see if we got a bad API token error. If we did, go + // ahead and abort the program. let resp_text = resp.text().unwrap(); if let Ok(json) = serde_json::from_str::(&resp_text) { - // TODO: We need to define a more suitable way for machine parsing of errors in the - // MPR. Maybe something like '{"err_type": "invalid_api_key"}'. + // TODO: We need to define a more suitable way for machine parsing of errors in + // the MPR. Maybe something like '{"err_type": "invalid_api_key"}'. if json.resp_type == "error" && json.code == "err_invalid_api_key" { - message::error("Invalid API key was passed in."); + message::error("Invalid API key was passed in.\n"); quit::with_code(exitcode::USAGE); } } @@ -73,80 +79,294 @@ impl<'a> AuthenticatedRequest<'a> { } } -// Structs and functions to run a command, and abort if it fails. -pub struct CommandInfo<'a> { - pub args: &'a Vec<&'a str>, - pub capture: bool, - pub stdin: Option<&'a str>, +/// Handle errors from APT. +pub fn handle_errors(err_str: &apt_util::Exception) { + for msg in err_str.what().split(';') { + if msg.starts_with("E:") { + message::error(&format!("{}\n", msg.strip_prefix("E:").unwrap())); + } else if msg.starts_with("W:") { + message::warning(&format!("{}\n", msg.strip_prefix("W:").unwrap())); + }; + } } -pub struct CommandResult { - pub stdout: Vec, - pub stderr: Vec, - pub exit_status: ExitStatus, +/// Run a command, and error out if it fails. +pub fn check_exit_status(cmd: &ProcCommand, status: &ExitStatus) { + if !status.success() { + let mut args = vec![cmd.get_program().to_str().unwrap().to_string()]; + for arg in cmd.get_args() { + args.push(arg.to_str().unwrap().to_string()); + } + + message::error(&format!("Failed to run command: {:?}\n", args)); + quit::with_code(exitcode::UNAVAILABLE); + } } -pub fn run_command(cmd: &CommandInfo) -> CommandResult { - let cmd_name = cmd.args[0]; - let cmd_args = &cmd.args[1..]; - // Functions like 'Command::stdin()' return references to the object created by - // 'Command::new()', which returns the object itself. - // We want to only interact with references to the object from hereon out. - let mut _result = Command::new(cmd_name); - let mut result = &mut _result; - result = result.args(cmd_args); - - // If we passed in stdin, set up the command to accept it. - if cmd.stdin.is_some() { - result = result.stdin(Stdio::piped()); - } - - // Take in stdout and stderr if needed. - if cmd.capture { - result = result.stdout(Stdio::piped()); - result = result.stderr(Stdio::piped()); - } - - // Start the subprocess. - let mut result = match result.spawn() { - Ok(child) => child, - Err(err) => { - message::error(&format!( - "Failed to run command. [{:?}] [{}]", - cmd.args, err - )); - quit::with_code(exitcode::UNAVAILABLE); +/// Format a list of package names in the way APT would. +pub fn format_apt_pkglist + Display>(pkgnames: &Vec) { + // All package lines always start with two spaces, so pretend like we have two + // less characters. + let term_width = apt_util::terminal_width() - 2; + let mut output = String::from(" "); + let mut current_width = 0; + + for _pkgname in pkgnames { + let pkgname = _pkgname.as_ref(); + output.push_str(pkgname); + current_width += pkgname.len(); + + if current_width > term_width { + output.push_str("\n "); + current_width = 0; + } else { + output.push(' '); + } + } + + println!("{}", output); +} + +/// Check if a response was a "yes" response. 'default' is what to return if +/// 'resp' is empty. +pub fn is_yes(resp: &str, default: bool) -> bool { + resp.to_lowercase() == "y" || (resp.is_empty() && default) +} + +/// Print out a question with options and get the result. +/// `multi_allowed` specifies if only a single option can be chosen. +pub fn ask_question(question: &str, options: &Vec<&str>, multi_allowed: bool) -> Vec { + let num_re = Regex::new("^[0-9]*-[0-9]*$|^[0-9]*$").unwrap(); + let options_len = options.len(); + message::question(question); + + // Panic if no options were passed in, there's nothing to work with there. This + // function should only be used internally anyway, so this just gives a heads up + // that it's being used incorrectly. + if options.is_empty() { + panic!("No values passed in for `options` parameter"); + } + + // Print the options. + let mut str_options: Vec = Vec::new(); + + for (index, item) in options.iter().enumerate() { + str_options.push(format!("[{}] {}", index, item)) + } + + format_apt_pkglist(&str_options); + + let print_question = || -> Option> { + let mut returned_items: Vec = Vec::new(); + + if multi_allowed { + print!( + "{}", + "Please enter a selection (i.e. `1-3 5`, defaults to `0`): ".bold() + ); + } else { + print!( + "{}", + "Please enter a selection (i.e. `1` or `6`, defaults to `0`): ".bold() + ); + } + io::stdout().flush().unwrap(); + let mut input = String::new(); + io::stdin().read_line(&mut input).unwrap(); + + // Pop off the leading newline. + input.pop(); + + // If no response was given, return the first item in the options. + if input.is_empty() { + returned_items.push(options.first().unwrap().to_string()); + return Some(returned_items); + } + + let matched_items: Vec<&str> = input.split(' ').collect(); + + if !multi_allowed + && (matched_items.len() > 1 || matched_items.first().unwrap().contains('-')) + { + message::error("Only one value is allowed to be specified.\n"); + return None; + } + + for item in &matched_items { + if !num_re.is_match(item) { + message::error(&format!( + "Error parsing item `{}`. Please make sure it is valid.\n", + item + )); + return None; + } + + if item.contains('-') { + let (num1_str, num2_str) = item.split_once('-').unwrap(); + let num1: usize = num1_str.parse().unwrap(); + let num2: usize = num2_str.parse().unwrap(); + + if num1 > options_len - 1 { + message::error(&format!("Number is too big: {}\n", num1)); + return None; + } else if num2 > options_len - 1 { + message::error(&format!("Number is too big: {}\n", num2)); + return None; + } + + for num in num1..num2 { + returned_items.push(options.get(num).unwrap().to_string()) + } + } else { + let num: usize = item.parse().unwrap(); + + if num > options_len - 1 { + message::error(&format!("Number is too big: {}\n", num)); + return None; + } + returned_items.push(options.get(num).unwrap().to_string()); + } } + + Some(returned_items) }; - // If we passed in stdin previously, pass in our stdin. - if let Some(stdin) = cmd.stdin { - result - .stdin - .take() - .unwrap() - .write_all(stdin.as_bytes()) - .unwrap(); + let mut result = print_question(); + while result.is_none() { + result = print_question(); } - // Wait for the command to exit. - let prog_exit = result.wait_with_output().unwrap(); + result.unwrap() +} + +/// Get the system's distro and architecture. The first value returned is the +/// distribution, and the second is the architecture. +pub fn get_distro_arch_info() -> (String, String) { + let mut distro_cmd = ProcCommand::new("lsb_release"); + distro_cmd.arg("-cs"); + let mut arch_cmd = ProcCommand::new("dpkg"); + arch_cmd.arg("--print-architecture"); + + let distro = std::str::from_utf8(&distro_cmd.output().unwrap().stdout) + .unwrap() + .to_owned(); + let arch = std::str::from_utf8(&arch_cmd.output().unwrap().stdout) + .unwrap() + .to_owned(); + + (distro, arch) +} + +/// XDG directory wrapper thingermabobers. +pub mod xdg { + /// Return the cache directory, which also creating it if it doesn't exist. + pub fn get_cache_dir() -> super::path::PathBuf { + let mut cache_dir = dirs::cache_dir().unwrap(); + cache_dir.push("mist"); - // Return the result. - CommandResult { - stdout: prog_exit.stdout, - stderr: prog_exit.stderr, - exit_status: prog_exit.status, + if !cache_dir.exists() { + if super::std_fs::create_dir_all(&cache_dir).is_err() { + super::message::error(&format!( + "Failed to create directory for cache directory ({}).", + cache_dir.display() + )); + } + } else if !cache_dir.is_dir() { + super::message::error(&format!( + "Config directory path '{}' needs to be a directory, but it isn't.", + cache_dir.display() + )); + } + + cache_dir + } + + /// Return the global cache directory, for use by all users. + pub fn get_global_cache_dir() -> super::path::PathBuf { + ["/var", "cache", "mist"].iter().collect() } } -// Function that finds the matching package base of a given package. -pub fn find_pkgbase<'a>(pkgname: &'a str, package_cache: &'a MprCache) -> Option<&'a str> { - for pkg in &package_cache.packages { - if pkg.pkgname == pkgname { - return Some(pkg.pkgbase.as_str()); +/// File/Folder wrappers for my joy. +pub mod fs { + use crate::style::Colorize; + + /// Create a folder, aborting if unable to or the specified path already + /// exists and isn't a folder. + pub fn create_dir(directory: &str) { + let path = super::path::Path::new(directory); + if !path.exists() { + if super::std_fs::create_dir_all(path).is_err() { + super::message::error(&format!( + "Failed to create directory ({}).\n", + directory.green().bold() + )); + quit::with_code(exitcode::UNAVAILABLE); + } + } else if !path.is_dir() { + super::message::error(&format!( + "Path '{}' needs to be a directory, but it isn't.\n", + directory.green().bold() + )); + quit::with_code(exitcode::UNAVAILABLE); } } - None + /// Create a file, aborting if unable to do so. + pub fn create_file(path: &str) -> super::std_fs::File { + match super::std_fs::File::create(path) { + Ok(file) => file, + Err(err) => { + super::message::error(&format!( + "Failed to create file '{}' [{}]\n", + path.bold().green(), + err.to_string().bold() + )); + quit::with_code(exitcode::UNAVAILABLE); + } + } + } +} + +/// Sudo user management stuff. +pub mod sudo { + super::lazy_static! { + static ref NORMAL_UID: u32 = users::get_current_uid(); + } + + /// Change the user to root. + pub fn to_root() { + // Make sure the deref is ran on `normal` uid so that it's properly registered. + let _ = *self::NORMAL_UID; + + users::switch::set_effective_uid(0).unwrap(); + users::switch::set_current_uid(0).unwrap(); + } + + pub fn check_perms() { + super::message::info("Obtaining root permissions...\n"); + + let mut cmd = self::run_as_normal_user("sudo"); + cmd.arg("true"); + + if !cmd.spawn().unwrap().wait().unwrap().success() { + super::message::error("Couldn't obtain root permissions.\n"); + quit::with_code(exitcode::USAGE); + } + } + + /// Change the user to the non-root user. + // pub fn to_normal() { + // users::switch::set_effective_uid(*self::NORMAL_UID).unwrap(); + // } + + // Run a command as the normal user declared by [`NORMAL_UID`]. + pub fn run_as_normal_user>(program: P) -> super::ProcCommand { + let mut cmd = super::ProcCommand::new("sudo"); + cmd.args(["-E", "-n"]); + cmd.arg(format!("-u#{}", *self::NORMAL_UID)); + cmd.arg("--"); + cmd.arg(program); + cmd + } } diff --git a/test b/test new file mode 100755 index 0000000..4265675 Binary files /dev/null and b/test differ diff --git a/test.rs b/test.rs new file mode 100644 index 0000000..a87b473 --- /dev/null +++ b/test.rs @@ -0,0 +1,6 @@ +use std::os::unix::fs::PermissionsExt; + +fn main() { + let file = std::fs::File::open("target/debug/mist").unwrap(); + println!("{:o}", file.metadata().unwrap().permissions().mode()); +}