diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 7e10aea..5e5a40b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -2,9 +2,9 @@ name: Run checks and tests on: workflow_dispatch: push: - branches: [ main, develop, feature/gh-actions ] + branches: [ main, development, feature/gh-actions ] pull_request: - branches: [ main, develop ] + branches: [ main, development ] env: CARGO_TERM_COLOR: always diff --git a/Cargo.lock b/Cargo.lock index 1c2440b..c22b6c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,41 +4,78 @@ version = 3 [[package]] name = "Amethyst" -version = "3.6.0" +version = "4.0.0" dependencies = [ - "chrono", + "async-recursion", + "aur-rpc", "clap", "clap_complete", + "color-eyre", "colored", + "console", "crossterm", + "dialoguer", + "directories", + "futures", + "fuzzy-matcher", + "indicatif", + "lazy-regex", + "lazy_static", "libc", - "mimalloc", "native-tls", + "parking_lot", "regex", - "rm_rf", "serde", - "spinoff", "textwrap", + "tokio", "toml", - "ureq", + "tracing", + "tracing-error", + "tracing-subscriber", ] +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +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.4" +name = "ansi_term" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" dependencies = [ - "libc", + "winapi", +] + +[[package]] +name = "async-recursion" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cda8f4bcc10624c4e85bc66b3f452cca98cfa5ca002dc83a16aad2367641bea" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] @@ -52,12 +89,39 @@ dependencies = [ "winapi", ] +[[package]] +name = "aur-rpc" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4edbe8eb8dde1e70c5969f980a5a51a6b267d86c49d99c4bd42b7d26a8bb39c5" +dependencies = [ + "reqwest", + "serde", + "thiserror", + "tracing", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[package]] +name = "backtrace" +version = "0.3.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cab84319d616cfb654d03394f38ab7e6f0919e181b1b57e1fd15e7fb4077d9a7" +dependencies = [ + "addr2line", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", +] + [[package]] name = "base64" version = "0.13.0" @@ -76,6 +140,12 @@ version = "3.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +[[package]] +name = "bytes" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" + [[package]] name = "cc" version = "1.0.73" @@ -88,31 +158,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "chrono" -version = "0.4.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-integer", - "num-traits", - "wasm-bindgen", - "winapi", -] - -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "clap" -version = "3.2.17" +version = "3.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29e724a68d9319343bb3328c9cc2dfde263f4b3142ee1059a9980580171c954b" +checksum = "23b71c3ce99b7611011217b366d923f1d0a7e07a92bb2dbf1e84508c673ca3bd" dependencies = [ "atty", "bitflags", @@ -137,9 +187,9 @@ dependencies = [ [[package]] name = "clap_derive" -version = "3.2.17" +version = "3.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13547f7012c01ab4a0e8f8967730ada8f9fdf419e8b6c792788f39cf4e46eefa" +checksum = "ea0c8bce528c4be4da13ea6fead8965e95b6073585a2f05204bd8f4119f82a65" dependencies = [ "heck", "proc-macro-error", @@ -157,6 +207,34 @@ dependencies = [ "os_str_bytes", ] +[[package]] +name = "color-eyre" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a667583cca8c4f8436db8de46ea8233c42a7d9ae424a82d338f2e4675229204" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", + "url", +] + +[[package]] +name = "color-spantrace" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba75b3d9449ecdccb27ecbc479fdc0b87fa2dd43d2f8298f9bf0e59aacc8dce" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colored" version = "2.0.0" @@ -168,6 +246,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "console" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89eab4d20ce20cea182308bca13088fecea9c05f6776cf287205d41a0ed3c847" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "terminal_size", + "unicode-width", + "winapi", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -209,6 +301,63 @@ dependencies = [ "winapi", ] +[[package]] +name = "dialoguer" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a92e7e37ecef6857fdc0c0c5d42fd5b0938e46590c2183cc92dd310a6d078eb1" +dependencies = [ + "console", + "fuzzy-matcher", + "tempfile", + "zeroize", +] + +[[package]] +name = "directories" +version = "4.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6" +dependencies = [ + "libc", + "redox_users", + "winapi", +] + +[[package]] +name = "encode_unicode" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" + +[[package]] +name = "encoding_rs" +version = "0.8.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fastrand" version = "1.8.0" @@ -218,6 +367,18 @@ dependencies = [ "instant", ] +[[package]] +name = "fixedbitset" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -243,6 +404,140 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f21eda599937fba36daeb58a22e8f5cee2d14c4a17b5b7739c7c8e5e3b8230c" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30bdd20c28fadd505d0fd6712cdfcb0d4b5648baf45faef7f852afb2399bb050" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" + +[[package]] +name = "futures-executor" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbf4d2a7a308fd4578637c0b17c7e1c7ba127b8f6ba00b29f717e9655d85eb68" + +[[package]] +name = "futures-macro" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42cd15d1c7456c04dbdf7e88bcd69760d74f3a798d6444e16974b505b0e62f17" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "futures-sink" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b20ba5a92e727ba30e72834706623d94ac93a725410b6a6b6fbc1b07f7ba56" + +[[package]] +name = "futures-task" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" + +[[package]] +name = "futures-util" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "fuzzy-matcher" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" +dependencies = [ + "thread_local", +] + +[[package]] +name = "getrandom" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + +[[package]] +name = "h2" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ca32592cf21ac7ccab1825cd87f6c9b3d9022c44d086172ed0966bec8af30be" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http", + "indexmap", + "slab", + "tokio", + "tokio-util", + "tracing", +] + [[package]] name = "hashbrown" version = "0.12.3" @@ -265,16 +560,74 @@ dependencies = [ ] [[package]] -name = "iana-time-zone" -version = "0.1.46" +name = "http" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2bfd338099682614d3ee3fe0cd72e0b6a41ca6a87f6a74a3bd593c91650501" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ - "android_system_properties", - "core-foundation-sys", - "js-sys", - "wasm-bindgen", - "winapi", + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" +dependencies = [ + "bytes", + "http", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" + +[[package]] +name = "httpdate" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" + +[[package]] +name = "hyper" +version = "0.14.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper", + "native-tls", + "tokio", + "tokio-native-tls", ] [[package]] @@ -288,6 +641,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.1" @@ -298,6 +657,18 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "indicatif" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc42b206e70d86ec03285b123e65a5458c92027d1fb2ae3555878b8113b3ddf" +dependencies = [ + "console", + "number_prefix", + "tokio", + "unicode-width", +] + [[package]] name = "instant" version = "0.1.12" @@ -307,6 +678,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "itoa" version = "1.0.3" @@ -322,6 +699,29 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "lazy-regex" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6b12f2eb6ed7d39405c5eb25a034b4c106a9ad87a6d9be3298de6c5f32fd57d" +dependencies = [ + "lazy-regex-proc_macros", + "once_cell", + "regex", +] + +[[package]] +name = "lazy-regex-proc_macros" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2496e5264069bc726ccf37eb76b9cd89406ae110d836c3f76729f99c8a23293" +dependencies = [ + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -334,20 +734,11 @@ version = "0.2.132" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8371e4e5341c3a96db127eb2465ac681ced4c433e01dd0e938adbef26ba93ba5" -[[package]] -name = "libmimalloc-sys" -version = "0.1.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11ca136052550448f55df7898c6dbe651c6b574fe38a0d9ea687a9f8088a2e2c" -dependencies = [ - "cc", -] - [[package]] name = "lock_api" -version = "0.4.7" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" +checksum = "9f80bf5aacaf25cbfc8210d1cfb718f2bf3b11c4c54e5afe36c236853a8ec390" dependencies = [ "autocfg", "scopeguard", @@ -363,10 +754,13 @@ dependencies = [ ] [[package]] -name = "maplit" -version = "1.0.2" +name = "matchers" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] [[package]] name = "matches" @@ -381,12 +775,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] -name = "mimalloc" -version = "0.1.29" +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f64ad83c969af2e732e907564deb0d0ed393cec4af80776f77dd77a1a427698" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ - "libmimalloc-sys", + "adler", ] [[package]] @@ -420,29 +820,35 @@ dependencies = [ ] [[package]] -name = "num-integer" -version = "0.1.45" +name = "num_cpus" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ - "autocfg", - "num-traits", + "hermit-abi", + "libc", ] [[package]] -name = "num-traits" -version = "0.2.15" +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + +[[package]] +name = "object" +version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" dependencies = [ - "autocfg", + "memchr", ] [[package]] name = "once_cell" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "2f7254b99e31cad77da24b08ebf628882739a608578bb1bcdfc1f9c21260d7c0" [[package]] name = "openssl" @@ -495,6 +901,12 @@ version = "6.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" + [[package]] name = "parking_lot" version = "0.12.1" @@ -511,10 +923,13 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ + "backtrace", "cfg-if", "libc", + "petgraph", "redox_syscall", "smallvec", + "thread-id", "windows-sys", ] @@ -524,6 +939,28 @@ version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" +[[package]] +name = "petgraph" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" +dependencies = [ + "fixedbitset", + "indexmap", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "pkg-config" version = "0.3.25" @@ -563,15 +1000,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "psm" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f446d0a6efba22928558c4fb4ce0b3fd6c89b0061343e390bf01a703742b8125" -dependencies = [ - "cc", -] - [[package]] name = "quote" version = "1.0.21" @@ -590,6 +1018,17 @@ dependencies = [ "bitflags", ] +[[package]] +name = "redox_users" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" +dependencies = [ + "getrandom", + "redox_syscall", + "thiserror", +] + [[package]] name = "regex" version = "1.6.0" @@ -601,6 +1040,15 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.6.27" @@ -617,19 +1065,47 @@ dependencies = [ ] [[package]] -name = "rm_rf" -version = "0.6.2" +name = "reqwest" +version = "0.11.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3443b7a35aa12ed2e99edfc0ecbefe6a53b4848305cc83e29981dfa1aea1f71e" +checksum = "b75aa69a3f06bbcc66ede33af2af253c6f7a86b1ca0033f60c580a27074fbf92" dependencies = [ - "stacker", + "base64", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-tls", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "native-tls", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", ] [[package]] -name = "rustversion" -version = "1.0.9" +name = "rustc-demangle" +version = "0.1.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" [[package]] name = "ryu" @@ -707,6 +1183,27 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + [[package]] name = "signal-hook" version = "0.3.14" @@ -737,6 +1234,15 @@ dependencies = [ "libc", ] +[[package]] +name = "slab" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] + [[package]] name = "smallvec" version = "1.9.0" @@ -750,27 +1256,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] -name = "spinoff" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c139aa6a2b4ed01ef761dfd593eb5b02218dbf35a3a0f10940b72f5bfe70426" -dependencies = [ - "colored", - "maplit", - "once_cell", - "strum", -] - -[[package]] -name = "stacker" -version = "0.1.15" +name = "socket2" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c886bd4480155fd3ef527d45e9ac8dd7118a898a46530b7b94c3e21866259fce" +checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" dependencies = [ - "cc", - "cfg-if", "libc", - "psm", "winapi", ] @@ -780,28 +1271,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "strum" -version = "0.24.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" -dependencies = [ - "strum_macros", -] - -[[package]] -name = "strum_macros" -version = "0.24.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "rustversion", - "syn", -] - [[package]] name = "syn" version = "1.0.99" @@ -858,6 +1327,46 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "thiserror" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d0a539a918745651435ac7db7a18761589a94cd7e94cd56999f828bf73c8a57" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c251e90f708e16c49a16f4917dc2131e75222b72edfa9cb7f7c58ae56aae0c09" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread-id" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fdfe0627923f7411a43ec9ec9c39c3a9b4151be313e0922042581fb6c9b717f" +dependencies = [ + "libc", + "redox_syscall", + "winapi", +] + +[[package]] +name = "thread_local" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +dependencies = [ + "once_cell", +] + [[package]] name = "tinyvec" version = "1.6.0" @@ -873,6 +1382,62 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +[[package]] +name = "tokio" +version = "1.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89797afd69d206ccd11fb0ea560a44bbb87731d020670e79416d442919257d42" +dependencies = [ + "autocfg", + "bytes", + "libc", + "memchr", + "mio", + "num_cpus", + "once_cell", + "pin-project-lite", + "signal-hook-registry", + "socket2", + "tokio-macros", + "tracing", + "winapi", +] + +[[package]] +name = "tokio-macros" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9724f9a975fb987ef7a3cd9be0350edcbe130698af5b8f7a631e23d42d052484" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.9" @@ -882,6 +1447,90 @@ dependencies = [ "serde", ] +[[package]] +name = "tower-service" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" + +[[package]] +name = "tracing" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11c75893af559bc8e10716548bdef5cb2b983f8e637db9d0e15126b61b484ee2" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-error" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d686ec1c0f384b1277f097b2f279a2ecc11afe8c133c1aabf036a27cb4cd206e" +dependencies = [ + "tracing", + "tracing-subscriber", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60db860322da191b40952ad9affe65ea23e7dd6a5c442c2c42865810c6ab8e6b" +dependencies = [ + "ansi_term", + "matchers", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" + [[package]] name = "unicode-bidi" version = "0.3.8" @@ -918,22 +1567,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" -[[package]] -name = "ureq" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" -dependencies = [ - "base64", - "chunked_transfer", - "log", - "native-tls", - "once_cell", - "serde", - "serde_json", - "url", -] - [[package]] name = "url" version = "2.2.2" @@ -946,6 +1579,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -958,6 +1597,16 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "want" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +dependencies = [ + "log", + "try-lock", +] + [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -989,6 +1638,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.82" @@ -1018,6 +1679,16 @@ version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +[[package]] +name = "web-sys" +version = "0.3.59" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "winapi" version = "0.3.9" @@ -1091,3 +1762,18 @@ name = "windows_x86_64_msvc" version = "0.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +dependencies = [ + "winapi", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" diff --git a/Cargo.toml b/Cargo.toml index d59267f..dc4a413 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,14 +1,13 @@ [package] name = "Amethyst" -version = "3.6.0" -authors = ["michal ", "axtlos "] +version = "4.0.0" +authors = ["michal ", "axtlos ", "trivernis "] edition = "2021" description = "A fast and efficient AUR helper" -repository = "https://github.com/crystal-linux/amethyst" license-file = "LICENSE.md" +default-run = "ame" keywords = ["aur", "crystal-linux", "pacman", "aur-helper"] categories = ["command-line-utilities"] -default-run = "ame" [[bin]] name = "ame" @@ -20,19 +19,36 @@ debug = false lto = "fat" codegen-units = 1 +[profile.dev] +opt-level = 0 + [dependencies] -mimalloc = { version = "0.1.29", default-features = false } -clap = { version = "3.2.8", features = [ "derive", "wrap_help" ] } -clap_complete = "3.2.4" -regex = { version = "1.5.6", default-features = false, features = [ "std", "unicode-perl" ] } +clap = { version = "3.2.17", features = [ "derive", "wrap_help" ] } +regex = { version = "1.6.0", default-features = false, features = [ "std", "unicode-perl" ] } colored = "2.0.0" -ureq = { version = "2.4.0", default-features = false, features = [ "native-tls", "json" ] } serde = { version = "1.0.144", default-features = false, features = [ "derive", "serde_derive" ] } native-tls = { version = "0.2.10", default-features = false } -libc = { version = "0.2.126", default-features = false } -rm_rf = { version = "0.6.2", default-features = false } -spinoff = { version = "0.5.2", default-features = false } -textwrap = { version = "0.15.0", features = [ "terminal_size", "smawk" ] } -chrono = { version = "0.4.22", default-features = false, features = [ "clock", "std", "wasmbind" ] } -toml = { version = "0.5.9", default-features = false } -crossterm = { version = "0.25.0", default-features = false } \ No newline at end of file +libc = { version = "0.2.132", default-features = false } +async-recursion = "1.0.0" +aur-rpc = "0.1.3" +futures = "0.3.23" +tracing = "0.1.36" +tracing-subscriber = { version = "0.3.15", features = ["env-filter"] } +textwrap = "0.15.0" +crossterm = "0.25.0" +toml = "0.5.9" +clap_complete = "3.2.4" +color-eyre = { version = "0.6.2", features = ["issue-url", "url"] } +indicatif = { version = "0.17.0", features = ["tokio"] } +lazy_static = "1.4.0" +parking_lot = { version = "0.12.1", features = ["deadlock_detection"] } +dialoguer = { version = "0.10.2", features = ["fuzzy-select"] } +lazy-regex = "2.3.0" +directories = "4.0.1" +console = "0.15.1" +tracing-error = "0.2.0" +fuzzy-matcher = "0.3.7" + +[dependencies.tokio] +version = "1.21.0" +features = ["rt", "rt-multi-thread", "io-std", "io-util", "process", "time", "macros", "tracing", "fs"] diff --git a/Containerfile b/Containerfile new file mode 100644 index 0000000..20cbd38 --- /dev/null +++ b/Containerfile @@ -0,0 +1,33 @@ +ARG BASE_IMAGE=docker.io/archlinux:latest +FROM ${BASE_IMAGE} as build_base +RUN pacman -Syu --noconfirm +RUN pacman -S --noconfirm base-devel curl bash +RUN curl https://sh.rustup.rs -sSf | bash -s -- -y +ENV PATH="/root/.cargo/bin:${PATH}" + +FROM build_base as builder +WORKDIR /usr/src +RUN cargo new amethyst +WORKDIR /usr/src/amethyst +COPY Cargo.toml Cargo.lock ./ +RUN mkdir target +RUN cargo fetch +COPY src ./src +RUN cargo build --frozen +RUN mkdir /tmp/ame +RUN cp target/debug/ame /tmp/ame/ + +FROM ${BASE_IMAGE} as runtime +RUN pacman -Syu --noconfirm +RUN pacman -S --noconfirm base-devel zsh wget vim git binutils fakeroot pacman-contrib sudo +RUN useradd -r -d /home/ame -p $(echo "ame" | openssl passwd -1 -stdin) ame -G wheel +RUN echo '%wheel ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +RUN mkdir /home/ame +RUN chown ame:ame /home/ame +COPY --from=builder /tmp/ame/ame /usr/bin/ +RUN rm -f $(pacdiff -o -f) +USER ame +RUN mkdir -p /home/ame/.local/share +RUN touch /home/ame/.zshrc +ENV AME_LOG=debug,hyper=info,mio=info,want=info +ENTRYPOINT ["zsh"] \ No newline at end of file diff --git a/run-isolated.sh b/run-isolated.sh new file mode 100755 index 0000000..a62c0cb --- /dev/null +++ b/run-isolated.sh @@ -0,0 +1,7 @@ +#!/bin/bash +podman build . -t ame-debug + +if [ $? -eq 0 ]; then + podman container exists ame-debug && podman container rm -f ame-debug + podman run -i -t --name ame-debug ame-debug +fi \ No newline at end of file diff --git a/src/args.rs b/src/args.rs index a21df05..6bf68d7 100644 --- a/src/args.rs +++ b/src/args.rs @@ -1,5 +1,6 @@ #![allow(clippy::module_name_repetitions)] +use crate::operations::SearchBy; use clap::{Parser, Subcommand, ValueHint}; #[derive(Debug, Clone, Parser)] @@ -28,19 +29,19 @@ pub struct Args { #[derive(Debug, Clone, Subcommand)] pub enum Operation { /// Installs a package from either the AUR or the Pacman-defined repositories - #[clap(bin_name = "ame", name = "install", visible_aliases = & ["-S"], aliases = & ["-Sa", "-Sr"])] + #[clap(bin_name = "ame", name = "install", visible_aliases = & ["-S", "i"], aliases = & ["-Sa", "-Sr"])] Install(InstallArgs), /// Removes a previously installed package - #[clap(bin_name = "ame", name = "remove", visible_aliases = & ["rm", "-Rs"])] + #[clap(bin_name = "ame", name = "remove", visible_aliases = & ["rm", "r", "-Rs"])] Remove(RemoveArgs), /// Searches for packages matching a regex-supported pattern in the AUR and/or the repos - #[clap(bin_name = "ame", name = "search", visible_aliases = & ["-Ss"], aliases = & ["-Ssa", "-Ssr"])] + #[clap(bin_name = "ame", name = "search", visible_aliases = & ["-Ss", "s"], aliases = & ["-Ssa", "-Ssr"])] Search(SearchArgs), /// Queries installed packages - #[clap(bin_name = "ame", name = "query", visible_aliases = & ["-Q"], aliases = & ["-Qa", "-Qr", "-Qm", "-Qn"])] + #[clap(bin_name = "ame", name = "query", visible_aliases = & ["-Q", "q"], aliases = & ["-Qa", "-Qr", "-Qm", "-Qn"])] Query(QueryArgs), /// Gets info about a package @@ -48,7 +49,7 @@ pub enum Operation { Info(InfoArgs), /// Upgrades locally installed packages to their latest versions (Default) - #[clap(bin_name = "ame", name = "upgrade", visible_aliases = & ["-Syu"])] + #[clap(bin_name = "ame", name = "upgrade", visible_aliases = & ["-Syu", "u"])] Upgrade(UpgradeArgs), /// Generates shell completions for supported shells (bash, fish, elvish, pwsh) @@ -56,7 +57,7 @@ pub enum Operation { GenComp(GenCompArgs), /// Removes all orphaned packages - #[clap(bin_name = "ame", name = "clean", visible_aliases = & ["-Sc"])] + #[clap(bin_name = "ame", name = "clean", visible_aliases = & ["-Sc", "c"])] Clean, /// Runs pacdiff @@ -104,7 +105,11 @@ pub struct SearchArgs { /// The string the package must match in the search #[clap(required = true)] - pub search: Vec, + pub search: String, + + /// Searches by a specific field + #[clap(long, short)] + pub by: Option, } #[derive(Default, Debug, Clone, Parser)] diff --git a/src/builder/git.rs b/src/builder/git.rs new file mode 100644 index 0000000..69b0267 --- /dev/null +++ b/src/builder/git.rs @@ -0,0 +1,69 @@ +use std::path::{Path, PathBuf}; + +use crate::internal::{ + commands::ShellCommand, + error::{AppError, AppResult}, +}; + +#[derive(Debug, Default)] +pub struct GitCloneBuilder { + url: String, + directory: PathBuf, +} + +impl GitCloneBuilder { + pub fn url(mut self, url: S) -> Self { + self.url = url.to_string(); + + self + } + + pub fn directory>(mut self, path: P) -> Self { + self.directory = path.as_ref().into(); + + self + } + + pub async fn clone(self) -> AppResult<()> { + let result = ShellCommand::git() + .arg("clone") + .arg(self.url) + .arg(self.directory) + .wait_with_output() + .await?; + + if result.status.success() { + Ok(()) + } else { + Err(AppError::Other(result.stderr)) + } + } +} + +#[derive(Debug, Default)] +pub struct GitPullBuilder { + directory: PathBuf, +} + +impl GitPullBuilder { + pub fn directory>(mut self, path: P) -> Self { + self.directory = path.as_ref().into(); + + self + } + + pub async fn pull(self) -> AppResult<()> { + let result = ShellCommand::git() + .arg("-C") + .arg(self.directory) + .arg("pull") + .wait_with_output() + .await?; + + if result.status.success() { + Ok(()) + } else { + Err(AppError::Other(result.stderr)) + } + } +} diff --git a/src/builder/makepkg.rs b/src/builder/makepkg.rs new file mode 100644 index 0000000..dd01ca7 --- /dev/null +++ b/src/builder/makepkg.rs @@ -0,0 +1,155 @@ +use std::fmt::Debug; +use std::path::{Path, PathBuf}; + +use tokio::process::Child; + +use crate::internal::{ + commands::ShellCommand, + error::{AppError, AppResult}, +}; + +#[derive(Default, Debug, Clone)] +pub struct MakePkgBuilder { + directory: PathBuf, + clean: bool, + no_deps: bool, + install: bool, + no_build: bool, + no_confirm: bool, + as_deps: bool, + skip_pgp: bool, + needed: bool, + no_prepare: bool, + force: bool, +} + +impl MakePkgBuilder { + /// Sets the working directory + pub fn directory>(mut self, dir: D) -> Self { + self.directory = dir.as_ref().into(); + + self + } + + pub fn clean(mut self, clean: bool) -> Self { + self.clean = clean; + + self + } + + pub fn no_deps(mut self, no_deps: bool) -> Self { + self.no_deps = no_deps; + + self + } + + pub fn no_build(mut self, no_build: bool) -> Self { + self.no_build = no_build; + + self + } + + pub fn no_prepare(mut self, no_prepare: bool) -> Self { + self.no_prepare = no_prepare; + + self + } + + /// Mark packages as non-explicitly installed + #[allow(clippy::wrong_self_convention)] + pub fn as_deps(mut self, as_deps: bool) -> Self { + self.as_deps = as_deps; + + self + } + + /// Skip PGP signature checks + pub fn skip_pgp(mut self, skip: bool) -> Self { + self.skip_pgp = skip; + + self + } + + /// Do not reinstall up to date packages + pub fn needed(mut self, needed: bool) -> Self { + self.needed = needed; + + self + } + + pub fn force(mut self, force: bool) -> Self { + self.force = force; + + self + } + + pub async fn run(self) -> AppResult<()> { + let output = self.build().wait_with_output().await?; + + if output.status.success() { + Ok(()) + } else { + Err(AppError::MakePkg(output.stderr)) + } + } + + pub fn spawn(self) -> AppResult { + self.build().spawn(true) + } + + /// Executes the makepkg command + #[tracing::instrument(level = "trace")] + fn build(self) -> ShellCommand { + let mut command = ShellCommand::makepkg().working_dir(self.directory); + + if self.clean { + command = command.arg("-c"); + } + if self.no_deps { + command = command.arg("-d") + } + if self.install { + command = command.arg("-c"); + } + if self.no_build { + command = command.arg("-o"); + } + if self.no_confirm { + command = command.arg("--noconfirm") + } + if self.as_deps { + command = command.arg("--asdeps") + } + if self.skip_pgp { + command = command.arg("--skippgp") + } + if self.needed { + command = command.arg("--needed"); + } + if self.no_prepare { + command = command.arg("--noprepare") + } + if self.force { + command = command.arg("-f") + } + + command + } + + #[tracing::instrument(level = "trace")] + pub async fn package_list + Debug>(dir: D) -> AppResult> { + let result = ShellCommand::makepkg() + .working_dir(dir.as_ref()) + .arg("--packagelist") + .wait_with_output() + .await?; + + if result.status.success() { + let packages = result.stdout.lines().map(PathBuf::from).collect(); + + Ok(packages) + } else { + Err(AppError::Other(result.stderr)) + } + } +} diff --git a/src/builder/mod.rs b/src/builder/mod.rs new file mode 100644 index 0000000..37d4240 --- /dev/null +++ b/src/builder/mod.rs @@ -0,0 +1,4 @@ +pub mod git; +pub mod makepkg; +pub mod pacman; +pub mod pager; diff --git a/src/builder/pacman.rs b/src/builder/pacman.rs new file mode 100644 index 0000000..f0fa34f --- /dev/null +++ b/src/builder/pacman.rs @@ -0,0 +1,267 @@ +use std::path::{Path, PathBuf}; + +use crate::internal::{commands::ShellCommand, error::AppResult, structs::Options}; + +#[derive(Debug, Default)] +pub struct PacmanInstallBuilder { + packages: Vec, + files: Vec, + as_deps: bool, + no_confirm: bool, + needed: bool, +} + +impl PacmanInstallBuilder { + pub fn from_options(options: Options) -> Self { + Self::default() + .as_deps(options.asdeps) + .no_confirm(options.noconfirm) + } + + pub fn packages, S: ToString>(mut self, packages: I) -> Self { + let mut packages = packages.into_iter().map(|p| p.to_string()).collect(); + self.packages.append(&mut packages); + + self + } + + pub fn files, T: AsRef>(mut self, files: I) -> Self { + let mut files = files.into_iter().map(|f| f.as_ref().into()).collect(); + self.files.append(&mut files); + + self + } + + pub fn no_confirm(mut self, no_confirm: bool) -> Self { + self.no_confirm = no_confirm; + + self + } + + #[allow(clippy::wrong_self_convention)] + pub fn as_deps(mut self, as_deps: bool) -> Self { + self.as_deps = as_deps; + + self + } + + pub fn needed(mut self, needed: bool) -> Self { + self.needed = needed; + + self + } + + #[tracing::instrument(level = "debug")] + pub async fn install(self) -> AppResult<()> { + let mut command = ShellCommand::pacman().elevated(); + + if !self.packages.is_empty() { + command = command.arg("-S"); + } else if !self.files.is_empty() { + command = command.arg("-U"); + } + + if self.no_confirm { + command = command.arg("--noconfirm") + } + + if self.as_deps { + command = command.arg("--asdeps") + } + if self.needed { + command = command.arg("--needed") + } + + command + .args(self.packages) + .args(self.files) + .wait_success() + .await + } +} + +#[derive(Debug)] +pub struct PacmanQueryBuilder { + query_type: PacmanQueryType, + color: PacmanColor, + packages: Vec, +} + +#[derive(Debug)] +enum PacmanQueryType { + Foreign, + All, + Info, + Native, +} + +#[derive(Clone, Copy, Debug)] +pub enum PacmanColor { + #[allow(dead_code)] + Always, + Auto, + Never, +} + +impl Default for PacmanColor { + fn default() -> Self { + Self::Auto + } +} + +impl PacmanQueryBuilder { + fn new(query_type: PacmanQueryType) -> Self { + Self { + query_type, + color: PacmanColor::default(), + packages: Vec::new(), + } + } + + pub fn all() -> Self { + Self::new(PacmanQueryType::All) + } + + pub fn foreign() -> Self { + Self::new(PacmanQueryType::Foreign) + } + + pub fn native() -> Self { + Self::new(PacmanQueryType::Native) + } + + pub fn info() -> Self { + Self::new(PacmanQueryType::Info) + } + + pub fn package(mut self, package: String) -> Self { + self.packages.push(package); + + self + } + + #[allow(dead_code)] + pub fn packages>(mut self, packages: I) -> Self { + let mut packages = packages.into_iter().collect::>(); + self.packages.append(&mut packages); + + self + } + + pub fn color(mut self, color: PacmanColor) -> Self { + self.color = color; + + self + } + + #[tracing::instrument(level = "trace")] + pub async fn query(self) -> AppResult<()> { + self.build_command().wait_success().await + } + + #[tracing::instrument(level = "trace")] + pub async fn query_with_output(self) -> AppResult> { + let output = self.build_command().wait_with_output().await?; + let packages = output + .stdout + .split('\n') + .filter(|p| !p.is_empty()) + .filter_map(|p| p.split_once(' ')) + .map(|(name, version)| BasicPackageInfo { + name: name.to_string(), + version: version.to_string(), + }) + .collect(); + tracing::debug!("Query result: {packages:?}"); + + Ok(packages) + } + + fn build_command(self) -> ShellCommand { + let mut command = ShellCommand::pacman().arg("-Q").arg("--color").arg("never"); + + command = match self.query_type { + PacmanQueryType::Foreign => command.arg("-m"), + PacmanQueryType::Info => command.arg("-i"), + PacmanQueryType::Native => command.arg("-n"), + PacmanQueryType::All => command, + }; + + command = command.arg("--color"); + command = match self.color { + PacmanColor::Always => command.arg("always"), + PacmanColor::Auto => command.arg("auto"), + PacmanColor::Never => command.arg("never"), + }; + + command.args(self.packages) + } +} + +#[derive(Clone, Debug)] +pub struct BasicPackageInfo { + pub name: String, + pub version: String, +} + +#[derive(Default)] +pub struct PacmanSearchBuilder { + query: String, +} + +impl PacmanSearchBuilder { + pub fn query>(mut self, query: S) -> Self { + if !self.query.is_empty() { + self.query.push(' '); + } + self.query.push_str(query.as_ref()); + + self + } + + /// Searches and returns if the execution result was ok + pub async fn search(self) -> AppResult { + let result = self.build_command().wait_with_output().await?; + + Ok(result.status.success()) + } + + fn build_command(self) -> ShellCommand { + ShellCommand::pacman().arg("-Ss").arg(self.query) + } +} + +#[derive(Default, Debug, Clone)] +pub struct PacmanUninstallBuilder { + packages: Vec, + no_confirm: bool, +} + +impl PacmanUninstallBuilder { + pub fn packages, S: ToString>(mut self, packages: I) -> Self { + let mut packages = packages.into_iter().map(|p| p.to_string()).collect(); + self.packages.append(&mut packages); + + self + } + + pub fn no_confirm(mut self, no_confirm: bool) -> Self { + self.no_confirm = no_confirm; + + self + } + + #[tracing::instrument(level = "trace")] + pub async fn uninstall(self) -> AppResult<()> { + let mut command = ShellCommand::pacman() + .elevated() + .arg("-R") + .args(self.packages); + + if self.no_confirm { + command = command.arg("--noconfirm"); + } + + command.wait_success().await + } +} diff --git a/src/builder/pager.rs b/src/builder/pager.rs new file mode 100644 index 0000000..f21f530 --- /dev/null +++ b/src/builder/pager.rs @@ -0,0 +1,23 @@ +use std::path::{Path, PathBuf}; + +use crate::{ + internal::{commands::ShellCommand, error::AppResult}, + with_suspended_output, +}; + +#[derive(Default)] +pub struct PagerBuilder { + path: PathBuf, +} + +impl PagerBuilder { + pub fn path>(mut self, path: P) -> Self { + self.path = path.as_ref().into(); + + self + } + + pub async fn open(self) -> AppResult<()> { + with_suspended_output!({ ShellCommand::pager().arg(self.path).wait_success().await }) + } +} diff --git a/src/interact/macros.rs b/src/interact/macros.rs new file mode 100644 index 0000000..fd4f88d --- /dev/null +++ b/src/interact/macros.rs @@ -0,0 +1,123 @@ +#[macro_export] +/// Macro for prompting the user with a yes/no question. +macro_rules! prompt { + (default yes, $($arg:tt)+) => { + $crate::interact::Interact::interact($crate::interact::AmePrompt::new(format!($($arg)+)).default_yes()) + }; + (default no, $($arg:tt)+) => { + $crate::interact::Interact::interact($crate::interact::AmePrompt::new(format!($($arg)+)).default_no()) + }; + (no default, $($arg:tt)+) => { + $crate::interact::Interact::interact($crate::interact::AmePrompt::new(format!($($arg)+))) + } +} + +#[macro_export] +/// Macro for prompting the user with a multi select +macro_rules! multi_select { + ($items:expr, $($arg:tt)+) => { + $crate::interact::Interact::interact($crate::interact::AmeMultiSelect::new(format!($($arg)+)).items($items)) + } +} + +#[macro_export] +macro_rules! select_opt { + ($items:expr, $($arg:tt)+) => { + $crate::interact::InteractOpt::interact_opt($crate::interact::AmeFuzzySelect::new(format!($($arg)+)).items($items)) + }; +} + +#[macro_export] +/// Returns a singular or plural expression depending on the given len +/// Usage: +/// ```rust +/// let some_list = vec!["a", "b", "c"]; +/// format!("The list has {}", numeric!(some_list.len(), "element"["s"])); +/// // result: The list has 3 elements +/// +/// let some_other_list = vec!["a"]; +/// format!("The list has {}", numeric!(some_other_list.len(), "element"["s"])); +/// // result: The list has 1 element +/// ``` +macro_rules! numeric { + ($len:expr, $sin:literal[$plu:literal]) => { + if $len == 1 { + format!("{} {}", $len, $sin) + } else { + format!("{} {}{}", $len, $sin, $plu) + } + }; + ($len:expr, $sin:literal or $plu:literal) => { + if $len == 1 { + format!("{} {}", $len, $sin) + } else { + format!("{} {}", $len, plu) + } + }; +} + +#[macro_export] +/// Creates a new multiprogress bar +macro_rules! multi_progress { + () => { + $crate::logging::get_logger().new_multi_progress(); + }; +} + +#[macro_export] +/// Creates a new progress spinner +macro_rules! spinner { + () => { + $crate::logging::get_logger().new_progress_spinner() + }; + ($($arg:tt)+) => { + { + let spinner = $crate::spinner!(); + spinner.set_message(format!($($arg)+)); + spinner + } + } +} + +#[macro_export] +/// Resets the output to normal text output (erases all progress bars and spinners) +macro_rules! normal_output { + () => { + $crate::logging::get_logger().reset_output_type(); + }; +} + +#[macro_export] +/// Suspends the output so that nothing is being written to stdout/stderr +/// Returns a handle that unsuspend the output when it's dropped +macro_rules! suspend_output { + () => { + $crate::logging::get_logger().suspend() + }; +} + +#[macro_export] +/// Unsuspends the output and writes everything buffered to stdout/stderr +macro_rules! unsuspend_output { + () => { + $crate::logging::get_logger().unsuspend(); + }; +} + +#[macro_export] +/// Suspend all output logging inside the given block +/// Note: This only works as long as the block itself doesn't unsuspend +/// the output +macro_rules! with_suspended_output { + ($expr:block) => {{ + let _handle = $crate::suspend_output!(); + $expr + }}; +} + +#[macro_export] +macro_rules! newline { + () => { + $crate::logging::get_logger().print_newline(); + }; +} diff --git a/src/interact/mod.rs b/src/interact/mod.rs new file mode 100644 index 0000000..3ebcf7a --- /dev/null +++ b/src/interact/mod.rs @@ -0,0 +1,19 @@ +pub mod macros; +mod multi_select; +mod prompt; +mod select; +mod theme; + +pub use multi_select::AmeMultiSelect; +pub use prompt::AmePrompt; +pub use select::AmeFuzzySelect; + +pub trait Interact { + type Result; + + fn interact(&mut self) -> Self::Result; +} + +pub trait InteractOpt: Interact { + fn interact_opt(&mut self) -> Option; +} diff --git a/src/interact/multi_select.rs b/src/interact/multi_select.rs new file mode 100644 index 0000000..ae04259 --- /dev/null +++ b/src/interact/multi_select.rs @@ -0,0 +1,41 @@ +use std::mem; + +use crate::with_suspended_output; + +use super::{theme::AmeTheme, Interact}; + +pub struct AmeMultiSelect { + prompt: String, + items: Vec, +} + +impl AmeMultiSelect { + /// Creates a new multi select prompt + pub fn new(prompt: S) -> Self { + Self { + prompt: prompt.to_string(), + items: Vec::new(), + } + } + + /// Adds/replaces the items of this multi select + pub fn items, S: ToString>(&mut self, items: I) -> &mut Self { + self.items = items.into_iter().map(|i| i.to_string()).collect(); + + self + } +} + +impl Interact for AmeMultiSelect { + type Result = Vec; + + fn interact(&mut self) -> Self::Result { + with_suspended_output!({ + dialoguer::MultiSelect::with_theme(AmeTheme::get()) + .with_prompt(mem::take(&mut self.prompt)) + .items(&self.items) + .interact() + .unwrap() + }) + } +} diff --git a/src/interact/prompt.rs b/src/interact/prompt.rs new file mode 100644 index 0000000..202ade1 --- /dev/null +++ b/src/interact/prompt.rs @@ -0,0 +1,51 @@ +use std::mem; + +use crate::with_suspended_output; + +use super::{theme::AmeTheme, Interact}; + +pub struct AmePrompt { + question: String, + default_yes: Option, +} + +impl AmePrompt { + /// Creates a new prompt + pub fn new(question: Q) -> Self { + Self { + question: question.to_string(), + default_yes: None, + } + } + + /// Sets the prompt to default to yes + pub fn default_yes(&mut self) -> &mut Self { + self.default_yes = Some(true); + + self + } + + /// Sets the prompt to default to yes + pub fn default_no(&mut self) -> &mut Self { + self.default_yes = Some(false); + + self + } +} + +impl Interact for AmePrompt { + type Result = bool; + + fn interact(&mut self) -> Self::Result { + let mut dialog = dialoguer::Confirm::with_theme(AmeTheme::get()); + + if let Some(def) = self.default_yes.take() { + dialog.default(def); + } + + dialog + .with_prompt(mem::take(&mut self.question)) + .wait_for_newline(true); + with_suspended_output!({ dialog.interact().unwrap() }) + } +} diff --git a/src/interact/select.rs b/src/interact/select.rs new file mode 100644 index 0000000..5794b74 --- /dev/null +++ b/src/interact/select.rs @@ -0,0 +1,51 @@ +use std::mem; + +use crate::with_suspended_output; + +use super::{theme::AmeTheme, Interact, InteractOpt}; + +pub struct AmeFuzzySelect { + prompt: String, + items: Vec, +} + +impl AmeFuzzySelect { + /// Creates a new multi select prompt + pub fn new(prompt: S) -> Self { + Self { + prompt: prompt.to_string(), + items: Vec::new(), + } + } + + /// Adds/replaces the items of this multi select + pub fn items, S: ToString>(&mut self, items: I) -> &mut Self { + self.items = items.into_iter().map(|i| i.to_string()).collect(); + + self + } + + fn build(&mut self) -> dialoguer::FuzzySelect { + let mut select = dialoguer::FuzzySelect::with_theme(AmeTheme::get()); + select + .with_prompt(mem::take(&mut self.prompt)) + .items(&self.items) + .default(0); + + select + } +} + +impl Interact for AmeFuzzySelect { + type Result = usize; + + fn interact(&mut self) -> Self::Result { + with_suspended_output!({ self.build().interact().unwrap() }) + } +} + +impl InteractOpt for AmeFuzzySelect { + fn interact_opt(&mut self) -> Option { + with_suspended_output!({ self.build().interact_opt().unwrap() }) + } +} diff --git a/src/interact/theme.rs b/src/interact/theme.rs new file mode 100644 index 0000000..c019921 --- /dev/null +++ b/src/interact/theme.rs @@ -0,0 +1,261 @@ +use crossterm::style::Stylize; +use dialoguer::theme::Theme; +use fuzzy_matcher::{skim::SkimMatcherV2, FuzzyMatcher}; + +use crate::internal::utils::wrap_text; +const ERR_SYMBOL: &str = "X"; +const PROMPT_SYMBOL: &str = "?"; + +pub struct AmeTheme; + +impl AmeTheme { + pub fn get() -> &'static Self { + static AME_THEME: AmeTheme = AmeTheme; + &AME_THEME + } +} + +impl Theme for AmeTheme { + fn format_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result { + let prompt = wrap_text(prompt).join("\n "); + write!(f, "{} {}:", PROMPT_SYMBOL.magenta(), prompt.bold()) + } + + fn format_error(&self, f: &mut dyn std::fmt::Write, err: &str) -> std::fmt::Result { + write!(f, "{} error: {}", ERR_SYMBOL.red(), err) + } + + fn format_confirm_prompt( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + default: Option, + ) -> std::fmt::Result { + let prompt = wrap_text(prompt).join("\n "); + if !prompt.is_empty() { + write!(f, "{} {} ", PROMPT_SYMBOL.magenta(), &prompt.bold())?; + } + match default { + None => write!(f, "[y/n] ")?, + Some(true) => write!(f, "[{}/n] ", "Y".bold())?, + Some(false) => write!(f, "[y/{}] ", "N".bold())?, + } + Ok(()) + } + + fn format_confirm_prompt_selection( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + selection: Option, + ) -> std::fmt::Result { + let prompt = wrap_text(prompt).join("\n "); + let selection = selection.map(|b| if b { "yes" } else { "no" }); + + match selection { + Some(selection) if prompt.is_empty() => { + write!(f, "{}", selection.italic()) + } + Some(selection) => { + write!(f, "{} {}", &prompt.bold(), selection.italic()) + } + None if prompt.is_empty() => Ok(()), + None => { + write!(f, "{}", &prompt.bold()) + } + } + } + + fn format_input_prompt( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + default: Option<&str>, + ) -> std::fmt::Result { + match default { + Some(default) if prompt.is_empty() => { + write!(f, "{} [{}]: ", PROMPT_SYMBOL.magenta(), default) + } + Some(default) => write!( + f, + "{} {} [{}]: ", + PROMPT_SYMBOL.magenta(), + prompt.bold(), + default + ), + None => write!(f, "{} {}: ", PROMPT_SYMBOL.magenta(), prompt.bold()), + } + } + + fn format_input_prompt_selection( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + sel: &str, + ) -> std::fmt::Result { + write!( + f, + "{} {}: {}", + PROMPT_SYMBOL.magenta(), + prompt.bold(), + sel.italic() + ) + } + + fn format_password_prompt( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + ) -> std::fmt::Result { + self.format_input_prompt(f, prompt, None) + } + + fn format_password_prompt_selection( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + ) -> std::fmt::Result { + self.format_input_prompt_selection(f, prompt, "[hidden]") + } + + fn format_select_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result { + self.format_prompt(f, prompt) + } + + fn format_select_prompt_selection( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + sel: &str, + ) -> std::fmt::Result { + self.format_input_prompt_selection(f, prompt, sel) + } + + fn format_multi_select_prompt( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + ) -> std::fmt::Result { + self.format_prompt(f, prompt) + } + + fn format_sort_prompt(&self, f: &mut dyn std::fmt::Write, prompt: &str) -> std::fmt::Result { + self.format_prompt(f, prompt) + } + + fn format_multi_select_prompt_selection( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + selections: &[&str], + ) -> std::fmt::Result { + write!(f, "{}: ", prompt.bold())?; + if selections.is_empty() { + write!(f, "{}", "No selections".italic())?; + } else { + for (idx, sel) in selections.iter().enumerate() { + write!(f, "{}{}", if idx == 0 { "" } else { ", " }, sel)?; + } + } + Ok(()) + } + + fn format_sort_prompt_selection( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + selections: &[&str], + ) -> std::fmt::Result { + self.format_multi_select_prompt_selection(f, prompt, selections) + } + + fn format_select_prompt_item( + &self, + f: &mut dyn std::fmt::Write, + text: &str, + active: bool, + ) -> std::fmt::Result { + write!(f, "{} {}", if active { ">" } else { " " }, text) + } + + fn format_multi_select_prompt_item( + &self, + f: &mut dyn std::fmt::Write, + text: &str, + checked: bool, + active: bool, + ) -> std::fmt::Result { + let active_symbol = if active { ">" } else { " " }; + let checked_symbol = if checked { "x" } else { " " }.magenta(); + write!(f, "{active_symbol} [{checked_symbol}] {text}") + } + + fn format_sort_prompt_item( + &self, + f: &mut dyn std::fmt::Write, + text: &str, + picked: bool, + active: bool, + ) -> std::fmt::Result { + write!( + f, + "{} {}", + match (picked, active) { + (true, true) => "> [x]", + (false, true) => "> [ ]", + (_, false) => " [ ]", + }, + text + ) + } + + fn format_fuzzy_select_prompt( + &self, + f: &mut dyn std::fmt::Write, + prompt: &str, + search_term: &str, + cursor_pos: usize, + ) -> std::fmt::Result { + if !prompt.is_empty() { + write!(f, "{} {} ", PROMPT_SYMBOL.magenta(), prompt.bold())?; + } + + if cursor_pos < search_term.len() { + let st_head = search_term[0..cursor_pos].to_string(); + let st_tail = search_term[cursor_pos..search_term.len()].to_string(); + let st_cursor = "|".to_string(); + write!(f, "{}{}{}", st_head, st_cursor, st_tail) + } else { + let cursor = "|".to_string(); + write!(f, "{}{}", search_term, cursor) + } + } + + fn format_fuzzy_select_prompt_item( + &self, + f: &mut dyn std::fmt::Write, + text: &str, + active: bool, + highlight_matches: bool, + matcher: &SkimMatcherV2, + search_term: &str, + ) -> std::fmt::Result { + write!(f, "{} ", if active { ">" } else { " " }.magenta().bold())?; + + if highlight_matches { + if let Some((_score, indices)) = matcher.fuzzy_indices(text, search_term) { + for (idx, c) in text.chars().into_iter().enumerate() { + if indices.contains(&idx) { + write!(f, "{}", c.bold())?; + } else { + write!(f, "{}", c)?; + } + } + + return Ok(()); + } + } + + write!(f, "{}", text) + } +} diff --git a/src/internal/clean.rs b/src/internal/clean.rs index 5a45d51..465d996 100644 --- a/src/internal/clean.rs +++ b/src/internal/clean.rs @@ -1,27 +1,16 @@ -use regex::Regex; - -use crate::{log, Options}; - /// Strips packages from versioning and other extraneous information. -pub fn clean(a: &[String], options: Options) -> Vec { +pub fn clean(a: &[String]) -> Vec { // Strip versioning from package names - let r = Regex::new(r"(\S+)((?:>=|<=|>|<|=\W)\S+$)").unwrap(); - let mut cleaned: Vec = vec![]; - let verbosity = options.verbosity; - - // Push cleaned package names to vector - for b in a { - if r.captures_iter(b).count() > 0 { - let c = r.captures(b).unwrap().get(1).map_or("", |m| m.as_str()); - cleaned.push(c.to_string()); - } else { - cleaned.push(b.to_string()); - } - } + let cleaned = a + .iter() + .map(|name| { + name.split_once('=') + .map(|n| n.0.to_string()) + .unwrap_or_else(|| name.to_string()) + }) + .collect(); - if verbosity >= 1 { - log!("Cleaned: {:?}\nInto: {:?}", a, cleaned); - } + tracing::debug!("Cleaned: {:?}\nInto: {:?}", a, cleaned); cleaned } diff --git a/src/internal/commands.rs b/src/internal/commands.rs index f16d3f8..f443aac 100644 --- a/src/internal/commands.rs +++ b/src/internal/commands.rs @@ -1,8 +1,9 @@ +use std::env; use std::ffi::{OsStr, OsString}; -use std::fs; -use std::process::{Child, Command, ExitStatus, Stdio}; +use std::path::{Path, PathBuf}; +use std::process::{ExitStatus, Stdio}; +use tokio::process::{Child, Command}; -use crate::internal::config; use crate::internal::error::{AppError, AppResult}; use crate::internal::is_tty; @@ -12,22 +13,18 @@ pub struct StringOutput { pub status: ExitStatus, } -/// A wrapper around [`std::process::Command`] with predefined +/// A wrapper around [std::process::Command] with predefined /// commands used in this project as well as elevated access. pub struct ShellCommand { command: String, args: Vec, elevated: bool, + working_dir: Option, } impl ShellCommand { pub fn pacman() -> Self { - let config = config::read(); - let pacman_cmd = if config.base.powerpill && fs::metadata("/usr/bin/powerpill").is_ok() { - Self::new("powerpill") - } else { - Self::new("pacman") - }; + let pacman_cmd = Self::new("pacman"); if is_tty() { pacman_cmd.arg("--color=always") @@ -56,11 +53,22 @@ impl ShellCommand { Self::new("sudo") } - fn new(command: &str) -> Self { + pub fn rm() -> Self { + Self::new("rm") + } + + pub fn pager() -> Self { + let pager = env::var("PAGER").unwrap_or_else(|_| String::from("less")); + + Self::new(pager) + } + + fn new(command: S) -> Self { Self { command: command.to_string(), args: Vec::new(), elevated: false, + working_dir: None, } } @@ -83,16 +91,22 @@ impl ShellCommand { self } + pub fn working_dir>(mut self, dir: D) -> Self { + self.working_dir = Some(dir.as_ref().into()); + + self + } + /// Runs the command with sudo - pub const fn elevated(mut self) -> Self { + pub fn elevated(mut self) -> Self { self.elevated = true; self } /// Waits for the child to exit but returns an error when it exists with a non-zero status code - pub fn wait_success(self) -> AppResult<()> { - let status = self.wait()?; + pub async fn wait_success(self) -> AppResult<()> { + let status = self.wait().await?; if status.success() { Ok(()) } else { @@ -101,17 +115,17 @@ impl ShellCommand { } /// Waits for the child to exit and returns the output status - pub fn wait(self) -> AppResult { + pub async fn wait(self) -> AppResult { let mut child = self.spawn(false)?; - child.wait().map_err(AppError::from) + child.wait().await.map_err(AppError::from) } /// Waits with output until the program completed and /// returns the string output object - pub fn wait_with_output(self) -> AppResult { + pub async fn wait_with_output(self) -> AppResult { let child = self.spawn(true)?; - let output = child.wait_with_output()?; + let output = child.wait_with_output().await?; let stdout = String::from_utf8(output.stdout).map_err(|e| AppError::from(e.to_string()))?; let stderr = String::from_utf8(output.stderr).map_err(|e| AppError::from(e.to_string()))?; @@ -122,26 +136,32 @@ impl ShellCommand { }) } - fn spawn(self, piped: bool) -> AppResult { + pub fn spawn(self, piped: bool) -> AppResult { + tracing::debug!("Running {} {:?}", self.command, self.args); + let (stdout, stderr) = if piped { (Stdio::piped(), Stdio::piped()) } else { (Stdio::inherit(), Stdio::inherit()) }; - let child = if self.elevated { - Command::new("sudo") - .arg(self.command) - .args(self.args) - .stdout(stdout) - .stderr(stderr) - .spawn()? + let mut command = if self.elevated { + let mut cmd = Command::new("sudo"); + cmd.arg(self.command); + + cmd } else { Command::new(self.command) - .args(self.args) - .stdout(stdout) - .stderr(stderr) - .spawn()? }; + if let Some(dir) = self.working_dir { + command.current_dir(dir); + } + + let child = command + .args(self.args) + .stdout(stdout) + .stderr(stderr) + .kill_on_drop(true) + .spawn()?; Ok(child) } diff --git a/src/internal/config.rs b/src/internal/config.rs index 97377a5..e4865a6 100644 --- a/src/internal/config.rs +++ b/src/internal/config.rs @@ -1,9 +1,7 @@ #![allow(clippy::module_name_repetitions)] use serde::Deserialize; -use std::{env, fs}; - -use crate::{crash, AppExitCode}; +use std::{env, fs, path::PathBuf}; #[derive(Debug, Deserialize)] pub struct Config { @@ -43,23 +41,9 @@ impl Default for Config { } pub fn read() -> Config { - let file = fs::read_to_string(format!( - "{}/{}", - env::var("HOME").unwrap(), - ".config/ame/config.toml" - )) - .unwrap_or_else(|e| { - crash!( - AppExitCode::ConfigParseError, - "Couldn't find config file: {}", - e - ); - }); - toml::from_str(&file).unwrap_or_else(|e| { - crash!( - AppExitCode::ConfigParseError, - "Could not parse config file: {}", - e - ); - }) + let config_path = PathBuf::from(env::var("HOME").unwrap()).join(".config/ame/config.toml"); + match fs::read_to_string(config_path) { + Ok(contents) => toml::from_str(&contents).expect("Could not parse the config file"), + Err(_) => Config::default(), + } } diff --git a/src/internal/dependencies.rs b/src/internal/dependencies.rs new file mode 100644 index 0000000..e0a3abd --- /dev/null +++ b/src/internal/dependencies.rs @@ -0,0 +1,283 @@ +use std::collections::HashSet; + +use aur_rpc::PackageInfo; +use futures::future; + +use crate::builder::pacman::{PacmanQueryBuilder, PacmanSearchBuilder}; + +use super::error::{AppError, AppResult}; +use lazy_regex::regex; + +#[derive(Clone, Debug)] +pub struct DependencyInformation { + pub depends: DependencyCollection, + pub make_depends: DependencyCollection, +} + +#[derive(Clone, Debug, Default)] +pub struct DependencyCollection { + pub aur: Vec, + pub repo: Vec, + pub not_found: Vec, +} + +#[derive(Clone, Debug)] +pub struct Dependency { + pub name: String, + #[allow(unused)] + pub condition: Option, + #[allow(unused)] + pub version: Option, +} + +#[derive(Clone, Debug)] +pub enum Condition { + Gt, + Ge, + Eq, + Le, + Lt, +} + +impl Condition { + pub fn try_from_str(s: &str) -> Option { + match s { + "=" => Some(Self::Eq), + "<=" => Some(Self::Le), + ">=" => Some(Self::Ge), + ">" => Some(Self::Gt), + "<" => Some(Self::Lt), + _ => None, + } + } +} + +impl DependencyInformation { + /// Resolves all dependency information for a given package + #[tracing::instrument(level = "trace")] + pub async fn for_package(package: &PackageInfo) -> AppResult { + let make_depends = Self::resolve_make_depends(package).await?; + let depends = Self::resolve_depends(package).await?; + + Ok(Self { + depends, + make_depends, + }) + } + + /// Resolves all make dependencies for a package + #[tracing::instrument(level = "trace")] + async fn resolve_make_depends(package: &PackageInfo) -> AppResult { + let mut packages_to_resolve: HashSet = package + .make_depends + .iter() + .filter_map(|d| Self::map_dep_to_name(d)) + .collect(); + + Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?; + let mut already_searched = HashSet::new(); + already_searched.insert(package.metadata.name.to_owned()); + let mut dependencies = DependencyCollection::default(); + + while !packages_to_resolve.is_empty() { + already_searched.extend(packages_to_resolve.iter().cloned()); + Self::extend_by_repo_packages(&mut packages_to_resolve, &mut dependencies).await?; + + let mut aur_packages = aur_rpc::info(&packages_to_resolve).await.map_err(|_| { + AppError::MissingDependencies(packages_to_resolve.iter().cloned().collect()) + })?; + aur_packages.iter().for_each(|p| { + packages_to_resolve.remove(&p.metadata.name); + }); + let not_found = std::mem::take(&mut packages_to_resolve); + + dependencies + .not_found + .append(&mut not_found.into_iter().collect()); + + packages_to_resolve = Self::get_filtered_make_depends(&aur_packages, &already_searched); + Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?; + dependencies.aur.append(&mut aur_packages); + } + + Ok(dependencies) + } + + /// Resolves all dependencies for a package + #[tracing::instrument(level = "trace")] + async fn resolve_depends(package: &PackageInfo) -> AppResult { + let mut packages_to_resolve: HashSet = package + .depends + .iter() + .filter_map(|d| Self::map_dep_to_name(d)) + .collect(); + + Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?; + let mut already_searched = HashSet::new(); + already_searched.insert(package.metadata.name.to_owned()); + let mut dependencies = DependencyCollection::default(); + + while !packages_to_resolve.is_empty() { + already_searched.extend(packages_to_resolve.iter().cloned()); + Self::extend_by_repo_packages(&mut packages_to_resolve, &mut dependencies).await?; + + let mut aur_packages = aur_rpc::info(&packages_to_resolve).await?; + aur_packages.iter().for_each(|p| { + packages_to_resolve.remove(&p.metadata.name); + }); + let not_found = std::mem::take(&mut packages_to_resolve); + + dependencies + .not_found + .append(&mut not_found.into_iter().collect()); + + packages_to_resolve = Self::get_filtered_depends(&aur_packages, &already_searched); + Self::filter_fulfilled_dependencies(&mut packages_to_resolve).await?; + dependencies.aur.append(&mut aur_packages); + } + + Ok(dependencies) + } + + async fn extend_by_repo_packages( + to_resolve: &mut HashSet, + dependencies: &mut DependencyCollection, + ) -> AppResult<()> { + let repo_deps = Self::find_repo_packages(to_resolve.clone()).await?; + to_resolve.retain(|p| !repo_deps.contains(p)); + dependencies + .repo + .append(&mut repo_deps.into_iter().collect()); + + Ok(()) + } + + fn get_filtered_make_depends( + aur_packages: &[PackageInfo], + searched: &HashSet, + ) -> HashSet { + aur_packages + .iter() + .flat_map(|p| { + p.make_depends + .iter() + .filter_map(|d| Self::map_dep_to_name(d)) + }) + .filter(|d| !searched.contains(d)) + .collect() + } + + fn get_filtered_depends( + aur_packages: &[PackageInfo], + searched: &HashSet, + ) -> HashSet { + aur_packages + .iter() + .flat_map(|p| p.depends.iter().filter_map(|d| Self::map_dep_to_name(d))) + .filter(|d| !searched.contains(d)) + .collect() + } + + async fn filter_fulfilled_dependencies(deps: &mut HashSet) -> AppResult<()> { + let mut fulfilled = HashSet::new(); + + for dep in deps.iter() { + if get_dependency_fulfilled(dep.clone()).await? { + fulfilled.insert(dep.clone()); + } + } + + deps.retain(|pkg| !fulfilled.contains(pkg)); + + Ok(()) + } + + fn map_dep_to_name(dep: &str) -> Option { + Dependency::try_from_str(dep).map(|d| d.name) + } + + #[tracing::instrument(level = "trace")] + async fn find_repo_packages(pkg_names: HashSet) -> AppResult> { + let repo_searches = pkg_names.iter().cloned().map(|p| async { + let search_result = PacmanSearchBuilder::default().query(&p).search().await?; + AppResult::Ok((p, search_result)) + }); + let repo_deps = future::try_join_all(repo_searches).await?; + let repo_deps: HashSet = repo_deps + .into_iter() + .filter_map(|(p, found)| if found { Some(p) } else { None }) + .collect(); + + Ok(repo_deps) + } + + pub fn make_depends(&self) -> HashSet<&str> { + let depends = self.depends(); + self.make_depends + .aur + .iter() + .map(|p| p.metadata.name.as_str()) + .chain(self.make_depends.repo.iter().map(String::as_str)) + .filter(|d| !depends.contains(d)) + .collect() + } + + pub fn depends(&self) -> HashSet<&str> { + self.depends + .aur + .iter() + .map(|d| d.metadata.name.as_str()) + .chain(self.depends.repo.iter().map(String::as_str)) + .collect() + } + + pub fn all_aur_depends(&self) -> Vec<&PackageInfo> { + self.make_depends + .aur + .iter() + .chain(self.depends.aur.iter()) + .collect() + } + + pub fn all_repo_depends(&self) -> Vec<&str> { + self.make_depends + .repo + .iter() + .chain(self.depends.repo.iter()) + .map(String::as_str) + .collect() + } +} + +impl Dependency { + #[tracing::instrument(level = "trace")] + pub fn try_from_str(s: &str) -> Option { + let r = + regex!(r#"^(?P[\w\-]+)((?P<=|=|>=|>|<)(?P\d+(\.\d+)*))?$"#); + let caps = r.captures(s)?; + let name = caps["name"].to_string(); + let condition = caps + .name("condition") + .map(|c| c.as_str()) + .and_then(Condition::try_from_str); + let version = caps.name("version").map(|v| v.as_str().into()); + tracing::debug!("Parsed dependency to {name} {condition:?} {version:?}"); + + Some(Dependency { + name, + condition, + version, + }) + } +} + +#[tracing::instrument(level = "trace")] +async fn get_dependency_fulfilled(name: String) -> AppResult { + let not_found = PacmanQueryBuilder::all() + .package(name) + .query_with_output() + .await? + .is_empty(); + + Ok(!not_found) +} diff --git a/src/internal/detect.rs b/src/internal/detect.rs index c6fc56f..d2d295f 100644 --- a/src/internal/detect.rs +++ b/src/internal/detect.rs @@ -1,13 +1,19 @@ +use crossterm::style::Stylize; + use crate::internal::commands::ShellCommand; use crate::internal::config; use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; -use crate::{prompt, spinner, warn}; +use crate::logging::get_logger; +use crate::prompt; + +use super::prompt_sudo_single; /// Searches the filesystem for .pacnew files and helps the user deal with them. -pub fn detect() { - // Start spinner - let sp = spinner!("Scanning for pacnew files"); +pub async fn detect() { + prompt_sudo_single().await.expect("Sudo prompt failed"); + let pb = get_logger().new_progress_spinner(); + pb.set_message("Scanning for pacnew files"); let mut pacnew = vec![]; @@ -16,6 +22,7 @@ pub fn detect() { .args(&["-o", "-f"]) .elevated() .wait_with_output() + .await .silent_unwrap(AppExitCode::PacmanError); let find_lines = find.stdout.split('\n'); for line in find_lines { @@ -26,26 +33,36 @@ pub fn detect() { // If pacnew files are found, warn the user and prompt to pacdiff if pacnew.is_empty() { - sp.stop_bold("No pacnew files found"); + pb.finish_with_message("No .pacnew files found".bold().to_string()); + get_logger().reset_output_type(); } else { - sp.stop_bold("It appears that at least one program you have installed / upgraded has installed a .pacnew config file. These are created when you have modified a program's configuration, and a package upgrade could not automatically merge the new file."); + pb.finish_with_message("pacnew files found".bold().to_string()); + get_logger().reset_output_type(); + tracing::info!( + "It appears that at least one program you have installed / upgraded has installed a .pacnew config file. \ + These are created when you have modified a program's configuration, and a package upgrade could not automatically merge the new file. \ + You can deal with those files by running {}.", + "sudo pacdiff".reset().magenta() + ); - let choice = prompt!(default false, "Would you like Amethyst to run pacdiff to deal with this? You can always deal with this later by running `sudo pacdiff`"); + let choice = prompt!(default no, "Would you like to run pacdiff now?"); if choice { let config = config::read(); if config.base.pacdiff_warn { ShellCommand::pacdiff() .elevated() .wait() + .await .silent_unwrap(AppExitCode::PacmanError); } else { - warn!("Pacdiff uses vimdiff by default to edit files for merging. You can focus panes by mousing over them and pressing left click, and scroll up and down using your mouse's scroll wheel (or the arrow keys). To exit vimdiff, press the following key combination: ESC, :qa!, ENTER"); - warn!("You can surpress this warning in the future by setting `pacdiff_warn` to \"false\" in ~/.config/ame/config.toml"); - let cont = prompt!(default false, "Continue?"); - if cont { + tracing::warn!("Pacdiff uses vimdiff by default to edit files for merging. You can focus panes by mousing over them and pressing left click, and scroll up and down using your mouse's scroll wheel (or the arrow keys). To exit vimdiff, press the following key combination: ESC, :qa!, ENTER"); + tracing::warn!("You can surpress this warning in the future by setting `pacdiff_warn` to \"false\" in ~/.config/ame/config.toml"); + + if prompt!(default no, "Continue?") { ShellCommand::pacdiff() .elevated() .wait() + .await .silent_unwrap(AppExitCode::PacmanError); } } diff --git a/src/internal/error.rs b/src/internal/error.rs index 2a46f24..cde2eab 100644 --- a/src/internal/error.rs +++ b/src/internal/error.rs @@ -12,15 +12,29 @@ pub type AppResult = Result; pub enum AppError { Io(std::io::Error), Other(String), + Rpc(aur_rpc::error::RPCError), NonZeroExit, + BuildStepViolation, + BuildError { pkg_name: String }, + UserCancellation, + MissingDependencies(Vec), + MakePkg(String), } impl Display for AppError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::Io(io) => Display::fmt(io, f), - Self::Other(s) => Display::fmt(s, f), - Self::NonZeroExit => Display::fmt("Exited with non-zero exit code", f), + AppError::Io(io) => Display::fmt(io, f), + AppError::Rpc(e) => Display::fmt(e, f), + AppError::Other(s) => Display::fmt(s, f), + AppError::NonZeroExit => Display::fmt("exited with non zero code", f), + AppError::BuildStepViolation => Display::fmt("AUR build violated build steps", f), + AppError::BuildError { pkg_name } => write!(f, "Failed to build package {pkg_name}"), + AppError::UserCancellation => write!(f, "Cancelled by user"), + AppError::MissingDependencies(deps) => { + write!(f, "Missing dependencies {}", deps.join(", ")) + } + AppError::MakePkg(msg) => write!(f, "Failed to ru makepkg {msg}"), } } } @@ -33,6 +47,12 @@ impl From for AppError { } } +impl From for AppError { + fn from(e: aur_rpc::error::RPCError) -> Self { + Self::Rpc(e) + } +} + impl From for AppError { fn from(string: String) -> Self { Self::Other(string) @@ -53,7 +73,10 @@ impl SilentUnwrap for AppResult { fn silent_unwrap(self, exit_code: AppExitCode) -> T { match self { Ok(val) => val, - Err(_) => crash!(exit_code, "An error occurred"), + Err(e) => { + tracing::debug!("{e}"); + crash!(exit_code, "An error occurred") + } } } } diff --git a/src/internal/exit_code.rs b/src/internal/exit_code.rs index b3aaa8f..ec7b781 100644 --- a/src/internal/exit_code.rs +++ b/src/internal/exit_code.rs @@ -6,8 +6,7 @@ pub enum AppExitCode { MissingDeps = 3, UserCancellation = 4, PacmanError = 5, - GitError = 6, MakePkgError = 7, - ConfigParseError = 8, + RpcError = 9, Other = 63, } diff --git a/src/internal/fs_utils.rs b/src/internal/fs_utils.rs new file mode 100644 index 0000000..c011c69 --- /dev/null +++ b/src/internal/fs_utils.rs @@ -0,0 +1,49 @@ +use std::{ + collections::VecDeque, + path::{Path, PathBuf}, +}; + +use futures::future; +use tokio::fs; + +#[tracing::instrument(level = "trace")] +pub async fn rmdir_recursive(path: &Path) -> std::io::Result<()> { + let mut files: Vec = Vec::new(); + let mut folders: Vec = Vec::new(); + + if path.is_dir() { + folders.push(path.into()); + } else { + files.push(path.into()); + } + + let mut folders_to_scan: VecDeque<_> = folders.clone().into(); + + while let Some(path) = folders_to_scan.pop_front() { + let mut dir_content = fs::read_dir(&path).await?; + + while let Some(entry) = dir_content.next_entry().await? { + let entry = entry.path(); + + if entry.is_dir() { + folders_to_scan.push_back(entry.clone()); + folders.push(entry); + } else { + files.push(entry); + } + } + } + + tracing::debug!("Deleting {} files", files.len()); + future::try_join_all(files.into_iter().map(fs::remove_file)).await?; + + tracing::debug!("Deleting {} folders", folders.len()); + + folders.reverse(); + for folder in folders { + tracing::trace!("Deleting {folder:?}"); + fs::remove_dir(folder).await?; + } + + Ok(()) +} diff --git a/src/internal/initialise.rs b/src/internal/initialise.rs deleted file mode 100644 index eaba826..0000000 --- a/src/internal/initialise.rs +++ /dev/null @@ -1,83 +0,0 @@ -use std::env; -use std::path::Path; - -use crate::{crash, internal::exit_code::AppExitCode, log, Options}; - -/// Ensure all required directories and files exist. -pub fn init(options: Options) { - // Initialise variables - let verbosity = options.verbosity; - let homedir = env::var("HOME").unwrap(); - - // If stateful dir doesn't exist, create it - if !Path::new(&format!("{}/.local/share/ame/", homedir)).exists() { - if verbosity >= 1 { - log!("Initialising stateful directory"); - } - std::fs::create_dir_all(format!("{}/.local/share/ame", homedir)).unwrap_or_else(|e| { - crash!( - AppExitCode::FailedCreatingPaths, - "Couldn't create path: {}/.local/share/ame: {}", - homedir, - e, - ); - }); - } - - // If cache dir doesn't exist, create it - if !Path::new(&format!("{}/.cache/ame", homedir)).exists() { - if verbosity >= 1 { - log!("Initialising cache directory"); - } - std::fs::create_dir_all(format!("{}/.cache/ame", homedir)).unwrap_or_else(|e| { - crash!( - AppExitCode::FailedCreatingPaths, - "Couldn't create path: {}/.cache/ame: {}", - homedir, - e, - ); - }); - } - - // If config dir doesn't exist, create it - if !Path::new(&format!("{}/.config/ame/", homedir)).exists() { - if verbosity >= 1 { - log!("Initialising config directory"); - } - std::fs::create_dir_all(format!("{}/.config/ame", homedir)).unwrap_or_else(|e| { - crash!( - AppExitCode::FailedCreatingPaths, - "Couldn't create path: {}/.config/ame: {}", - homedir, - e, - ); - }); - } - - // If config file doesn't exist, create it - let config = "\ -[base] -pacdiff_warn = true -highlight_optdepends = true -powerpill = false - -[extra] -review_user_shell = false -"; - - if !Path::new(&format!("{}/.config/ame/config.toml", homedir)).exists() { - if verbosity >= 1 { - log!("Initialising config file"); - } - std::fs::write(format!("{}/.config/ame/config.toml", homedir), config).unwrap_or_else( - |e| { - crash!( - AppExitCode::FailedCreatingPaths, - "Couldn't create path: {}/.config/ame/config.toml: {}", - homedir, - e, - ); - }, - ); - } -} diff --git a/src/internal/mod.rs b/src/internal/mod.rs index d49b897..81b1d0c 100644 --- a/src/internal/mod.rs +++ b/src/internal/mod.rs @@ -1,19 +1,17 @@ pub use clean::*; pub use clean::*; pub use detect::*; -pub use initialise::*; -pub use initialise::*; -pub use sort::*; pub use sort::*; pub use sudoloop::*; mod clean; pub mod commands; pub mod config; +pub mod dependencies; mod detect; pub mod error; pub mod exit_code; -mod initialise; +pub mod fs_utils; pub mod rpc; mod sort; pub mod structs; @@ -43,11 +41,6 @@ pub fn uwu_enabled() -> bool { config.extra.uwu.unwrap_or(false) } -pub fn uwu_debug_enabled() -> bool { - let config = config::read(); - config.extra.uwu_debug.unwrap_or(false) -} - /// Checks if we're running in a tty. If we do we can assume that /// the output can safely be colorized. pub fn is_tty() -> bool { diff --git a/src/internal/rpc.rs b/src/internal/rpc.rs index 81ca890..c8bc14d 100644 --- a/src/internal/rpc.rs +++ b/src/internal/rpc.rs @@ -1,97 +1,23 @@ -use std::sync::Arc; - -#[derive(serde::Deserialize, Debug, Clone)] -/// Struct for deserializing RPC results. -pub struct Package { - #[serde(rename = "Name")] - pub name: String, - #[serde(rename = "Version")] - pub version: String, - #[serde(rename = "Description")] - pub description: Option, - #[serde(rename = "Depends")] - #[serde(default)] - pub depends: Vec, - #[serde(rename = "MakeDepends")] - #[serde(default)] - pub make_depends: Vec, - #[serde(rename = "OptDepends")] - #[serde(default)] - pub opt_depends: Vec, - #[serde(rename = "OutOfDate")] - #[serde(default)] - pub out_of_date: Option, -} - -#[derive(serde::Deserialize)] -/// Struct for retreiving search results from the AUR. -pub struct SearchResults { - pub resultcount: u32, - pub results: Vec, -} - -#[derive(Clone)] -/// Struct for retreiving package information from the AUR. -pub struct InfoResults { - pub found: bool, - pub package: Option, -} +use aur_rpc::{PackageInfo, PackageMetadata, SearchField}; +use super::error::AppResult; pub const URL: &str = "https://aur.archlinux.org/"; -/// Return a struct of type [`InfoResults`] from the AUR. -pub fn rpcinfo(pkg: &str) -> InfoResults { - // Initialise TLS connector - let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap()); +pub async fn rpcinfo(pkg: &str) -> AppResult> { + let packages = aur_rpc::info(vec![pkg]).await?; - // Build request agent - let agent = ureq::AgentBuilder::new() - .tls_connector(tls_connector) - .build(); - - // Send request and parse results into json - let res: SearchResults = agent - .get(&format!( - "https://aur.archlinux.org/rpc/?v=5&type=info&arg={}", - pkg - )) - .call() - .unwrap() - .into_json() - .unwrap(); - - // Check if package was found - if res.results.is_empty() { - InfoResults { - found: false, - package: None, - } - } else { - InfoResults { - found: true, - package: Some(res.results[0].clone()), - } - } + Ok(packages.into_iter().next()) } -/// Return a struct of type [`SearchResults`] from the AUR. -pub fn rpcsearch(pkg: &str) -> SearchResults { - // Initialise TLS connector - let tls_connector = Arc::new(native_tls::TlsConnector::new().unwrap()); - - // Build request agent - let agent = ureq::AgentBuilder::new() - .tls_connector(tls_connector) - .build(); +pub async fn rpcsearch( + query: String, + by_field: Option, +) -> AppResult> { + let search_results = if let Some(field) = by_field { + aur_rpc::search_by(field, query).await? + } else { + aur_rpc::search(query).await? + }; - // Send request and parse results into json - agent - .get(&format!( - "https://aur.archlinux.org/rpc/?v=5&type=search&arg={}", - pkg - )) - .call() - .unwrap() - .into_json::() - .unwrap() + Ok(search_results) } diff --git a/src/internal/sort.rs b/src/internal/sort.rs index f0f1834..1f55bb0 100644 --- a/src/internal/sort.rs +++ b/src/internal/sort.rs @@ -1,52 +1,44 @@ use std::process::{Command, Stdio}; use crate::internal::{clean, rpc, structs}; -use crate::{log, Options}; +use crate::Options; -/// Sorts the given packages into an [`crate::internal::structs::Sorted`] -pub fn sort(input: &[String], options: Options) -> structs::Sorted { - // Initialise variables - let mut repo: Vec = vec![]; - let mut aur: Vec = vec![]; - let mut nf: Vec = vec![]; - let verbosity = options.verbosity; +use super::error::SilentUnwrap; +use super::exit_code::AppExitCode; - // Sanitise all packages passed in - let a = clean(input, options); +#[tracing::instrument(level = "trace")] +pub async fn sort(input: &[String], options: Options) -> structs::Sorted { + let mut repo_packages: Vec = vec![]; + let mut aur_packages: Vec = vec![]; + let mut missing_packages: Vec = vec![]; - if verbosity >= 1 { - log!("Sorting: {:?}", a.join(" ")); - } + let packages = clean(input); + + tracing::debug!("Sorting: {:?}", packages.join(" ")); - for b in a { - // Check if package is in the repos + for package in packages { let rs = Command::new("pacman") .arg("-Ss") - .arg(format!("^{}$", &b)) + .arg(format!("^{}$", &package)) .stdout(Stdio::null()) .status() .expect("Something has gone wrong"); - if rs.code() == Some(0) { - // If it is, add it to the repo vector - if verbosity >= 1 { - log!("{} found in repos", b); - } - repo.push(b.to_string()); - } else if rpc::rpcinfo(&b).found { - // Otherwise, check if it is in the AUR, if it is, add it to the AUR vector - if verbosity >= 1 { - log!("{} found in AUR", b); - } - aur.push(b.to_string()); + if let Some(0) = rs.code() { + tracing::debug!("{} found in repos", package); + repo_packages.push(package.to_string()); + } else if rpc::rpcinfo(&package) + .await + .silent_unwrap(AppExitCode::RpcError) + .is_some() + { + tracing::debug!("{} found in AUR", package); + aur_packages.push(package.to_string()); } else { - // Otherwise, add it to the not found vector - if verbosity >= 1 { - log!("{} not found", b); - } - nf.push(b.to_string()); + tracing::debug!("{} not found", package); + missing_packages.push(package.to_string()); } } - structs::Sorted::new(repo, aur, nf) + structs::Sorted::new(repo_packages, aur_packages, missing_packages) } diff --git a/src/internal/structs.rs b/src/internal/structs.rs index 62e0764..9c3f075 100644 --- a/src/internal/structs.rs +++ b/src/internal/structs.rs @@ -15,10 +15,9 @@ impl Sorted { } } -#[derive(Clone, Copy)] +#[derive(Clone, Debug, Copy)] /// Options to be passed down to internal functions pub struct Options { - pub verbosity: usize, pub noconfirm: bool, pub asdeps: bool, } diff --git a/src/internal/sudoloop.rs b/src/internal/sudoloop.rs index dd40c2b..d2a7a4b 100644 --- a/src/internal/sudoloop.rs +++ b/src/internal/sudoloop.rs @@ -1,18 +1,27 @@ -use std::thread; use std::time::Duration; use crate::ShellCommand; -/// Loop sudo so longer builds don't time out -#[allow(clippy::module_name_repetitions)] -pub fn start_sudoloop() { - prompt_sudo(); - std::thread::spawn(|| loop { - prompt_sudo(); - thread::sleep(Duration::from_secs(3 * 60)); +use super::error::AppResult; + +/// Loop sudo so it doesn't time out +#[tracing::instrument(level = "trace")] +pub async fn start_sudoloop() { + prompt_sudo().await; + tokio::task::spawn(async move { + loop { + prompt_sudo().await; + tokio::time::sleep(Duration::from_secs(3 * 60)).await; + } }); } -fn prompt_sudo() { - while ShellCommand::sudo().arg("-v").wait_success().is_err() {} +#[tracing::instrument(level = "trace")] +async fn prompt_sudo() { + while prompt_sudo_single().await.is_err() {} +} + +#[tracing::instrument(level = "trace")] +pub async fn prompt_sudo_single() -> AppResult<()> { + ShellCommand::sudo().arg("-v").wait_success().await } diff --git a/src/internal/utils.rs b/src/internal/utils.rs index 6490488..2d1b640 100644 --- a/src/internal/utils.rs +++ b/src/internal/utils.rs @@ -1,36 +1,14 @@ -use colored::Colorize; -use std::io; -use std::io::Write; -use std::process::{exit, Command, Stdio}; -use std::time::UNIX_EPOCH; -use textwrap::wrap; - -use crate::internal::exit_code::AppExitCode; -use crate::{internal, uwu}; +use std::fs; +use std::path::Path; +use std::process::exit; -const OK_SYMBOL: &str = "❖"; -const ERR_SYMBOL: &str = "X"; -const WARN_SYMBOL: &str = "!"; -const PROMPT_SYMBOL: &str = "?"; - -const PROMPT_YN_DEFAULT_TRUE: &str = "[Y/n]"; -const PROMPT_YN_DEFAULT_FALSE: &str = "[y/N]"; +use directories::ProjectDirs; +use textwrap::wrap; -#[macro_export] -/// Macro for printing a message to stdout. -macro_rules! info { - ($($arg:tt)+) => { - $crate::internal::utils::log_info(format!($($arg)+)) - } -} +use crate::{internal::exit_code::AppExitCode, logging::get_logger}; +use lazy_static::lazy_static; -#[macro_export] -/// Macro for printing a warning message non-destructively. -macro_rules! warn { - ($($arg:tt)+) => { - $crate::internal::utils::log_warn(format!($($arg)+)) - } -} +use super::error::{AppError, SilentUnwrap}; #[macro_export] /// Macro for printing a message and destructively exiting @@ -41,199 +19,58 @@ macro_rules! crash { } #[macro_export] -/// Macro for logging to stderr -macro_rules! log { - ($($arg:tt)+) => { - $crate::internal::utils::log_debug(format!($($arg)+)) - } -} - -#[macro_export] -/// Macro for prompting the user with a yes/no question. -macro_rules! prompt { - (default $default:expr, $($arg:tt)+) => { - $crate::internal::utils::prompt_yn(format!($($arg)+), $default) - } -} - -#[macro_export] -/// Macro for creating a spinner. -macro_rules! spinner { - ($($arg:tt)+) => { - $crate::internal::utils::spinner_fn(format!($($arg)+)) - } -} - -/// Print a formatted message to stdout. -pub fn log_info(msg: String) { - let msg = if internal::uwu_enabled() { - uwu!(&msg) - } else { - msg +/// Cancelles the process +macro_rules! cancelled { + () => { + crash!( + $crate::internal::exit_code::AppExitCode::UserCancellation, + "Installation cancelled" + ) }; - - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - - println!( - "{} {}", - OK_SYMBOL.purple(), - wrap(&msg, opts).join("\n").bold() - ); -} - -/// Print a non-destructive warning message -pub fn log_warn(msg: String) { - let msg = if internal::uwu_enabled() { - uwu!(&msg) - } else { - msg - }; - - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - - println!( - "{} {}", - WARN_SYMBOL.yellow(), - wrap(&msg, opts).join("\n").yellow().bold() - ); } /// Logs a message and exits the program with the given exit code. pub fn log_and_crash(msg: String, exit_code: AppExitCode) -> ! { - let msg = if internal::uwu_enabled() { - uwu!(&msg) - } else { - msg - }; - - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - - println!( - "{} {}", - ERR_SYMBOL.red().bold(), - wrap(&msg, opts).join("\n").red().bold() - ); + get_logger().reset_output_type(); + get_logger().log_error(msg); + get_logger().flush(); exit(exit_code as i32); } -/// Logs a message to stderr with timestamp -pub fn log_debug(msg: String) { - let msg = if internal::uwu_enabled() && internal::uwu_debug_enabled() { - uwu!(&msg) - } else { - msg - }; +pub fn get_cache_dir() -> &'static Path { + lazy_static! { + static ref CACHE_DIR: &'static Path = create_if_not_exist(get_directories().cache_dir()); + } - eprintln!( - "{} {}", - std::time::SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap() - .as_secs(), - msg - ); + *CACHE_DIR } -/// Prompts the user for a yes/no answer. -pub fn prompt_yn(question: String, default_true: bool) -> bool { - let yn_prompt = if default_true { - PROMPT_YN_DEFAULT_TRUE - } else { - PROMPT_YN_DEFAULT_FALSE - }; - - let question = if internal::uwu_enabled() { - uwu!(&question) - } else { - question - }; - - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - - print!( - "{} {} {}: ", - PROMPT_SYMBOL.purple(), - wrap(&question, opts).join("\n").bold(), - yn_prompt - ); - - let mut yn: String = String::new(); - - io::stdout().flush().ok(); - io::stdin().read_line(&mut yn).unwrap(); - - if yn.trim().to_lowercase() == "n" || yn.trim().to_lowercase() == "no" { - false - } else if yn.trim().to_lowercase() == "y" || yn.trim().to_lowercase() == "yes" { - true - } else { - default_true +fn get_directories() -> &'static ProjectDirs { + lazy_static! { + static ref DIRECTORIES: ProjectDirs = ProjectDirs::from("com", "crystal", "ame").unwrap(); } -} -pub struct Spinner { - spinner: spinoff::Spinner, + &*DIRECTORIES } -impl Spinner { - pub fn stop_bold(self, text: &str) { - let text = if internal::uwu_enabled() { - uwu!(text) - } else { - text.to_string() - }; - - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - - let symbol = format!("{}", OK_SYMBOL.purple()); - let text = format!("{}", wrap(&text, opts).join("\n").bold()); - - self.spinner.stop_and_persist(&symbol, &text); +fn create_if_not_exist(dir: &Path) -> &Path { + if !dir.exists() { + fs::create_dir_all(dir) + .map_err(AppError::from) + .silent_unwrap(AppExitCode::FailedCreatingPaths) } -} - -/// Returns a spinner that can be used to display progress. -pub fn spinner_fn(text: String) -> Spinner { - let text = if internal::uwu_enabled() { - uwu!(&text) - } else { - text - }; - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) - .subsequent_indent(" "); - - Spinner { - spinner: spinoff::Spinner::new( - spinoff::Spinners::Line, - format!("{}", wrap(&text, opts).join("\n").bold()), - spinoff::Color::Magenta, - ), - } + dir } -/// Opens a String in `less`. -pub fn pager(text: &String) -> io::Result<()> { - let text = if internal::uwu_enabled() { - uwu!(text) - } else { - text.to_string() - }; - - let mut pager = Command::new("less") - .arg("-R") - .stdin(Stdio::piped()) - .spawn()?; - - let stdin = pager.stdin.as_mut().unwrap(); - stdin.write_all(text.as_bytes())?; - stdin.flush()?; - pager.wait()?; +pub fn wrap_text>(s: S) -> Vec { + wrap(s.as_ref(), get_wrap_options()) + .into_iter() + .map(String::from) + .collect() +} - Ok(()) +fn get_wrap_options() -> textwrap::Options<'static> { + textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 2) + .subsequent_indent(" ") } diff --git a/src/logging/fmt_layer.rs b/src/logging/fmt_layer.rs new file mode 100644 index 0000000..d573904 --- /dev/null +++ b/src/logging/fmt_layer.rs @@ -0,0 +1,146 @@ +use colored::Colorize; +use std::collections::HashMap; +use std::sync::Arc; +use tracing_subscriber::registry::LookupSpan; + +use tracing::field::Visit; +use tracing::{span, Level, Metadata, Subscriber}; +use tracing_subscriber::Layer; + +use super::handler::LogHandler; +use super::Verbosity; + +const ENABLED_MODULES: &[&str] = &["ame"]; + +pub struct AmeFormatLayer { + logger: Arc, +} + +impl AmeFormatLayer { + pub fn new(logger: Arc) -> Self { + Self { logger } + } + + fn is_level_loggable(&self, level: &Level) -> bool { + self.logger.is_loggable(Verbosity::from_level(level)) + } + + fn is_enabled(&self, metadata: &Metadata) -> bool { + let level = metadata.level(); + if !self.is_level_loggable(level) { + false + } else if let Some(module_path) = metadata.module_path() { + ENABLED_MODULES.iter().any(|m| module_path.starts_with(m)) + } else { + false + } + } + + fn log(&self, msg: String, level: &Level) { + match Verbosity::from_level(level) { + Verbosity::Error => self.logger.log_error(msg), + Verbosity::Warning => self.logger.log_warning(msg), + Verbosity::Info => self.logger.log_info(msg), + Verbosity::Debug => self.logger.log_debug(msg), + Verbosity::Trace => self.logger.log_trace(msg), + } + } +} + +impl LookupSpan<'a>> Layer for AmeFormatLayer { + /// When entering a span + fn on_new_span( + &self, + attrs: &span::Attributes<'_>, + _id: &span::Id, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let metadata = attrs.metadata(); + if self.is_enabled(metadata) { + let mut visitor = ValueDebugStorage::default(); + attrs.record(&mut visitor); + let fields: Vec = visitor + .values + .into_iter() + .map(|(k, v)| format!("{k} = {v}")) + .collect(); + let mut fields_str = fields.join("\n "); + + if !fields_str.is_empty() { + fields_str = format!("\n {fields_str}"); + } + + if let Some(module) = metadata.module_path() { + self.log( + format!( + "{} {}::{} {}", + "ENTER".italic(), + module, + metadata.name(), + fields_str.dimmed() + ), + metadata.level(), + ) + } else { + self.log( + format!( + "{} {} {}", + "ENTER".italic(), + metadata.name(), + fields_str.dimmed() + ), + metadata.level(), + ) + } + } + } + + fn on_close(&self, id: span::Id, ctx: tracing_subscriber::layer::Context<'_, S>) { + let span = ctx.span(&id).unwrap(); + let metadata = span.metadata(); + + if self.is_enabled(metadata) { + if let Some(module) = metadata.module_path() { + self.log( + format!("{} {}::{}", "EXIT".italic(), module, metadata.name(),), + metadata.level(), + ); + } else { + self.log( + format!("{} {}", "EXIT".italic(), metadata.name()), + metadata.level(), + ); + } + } + } + + fn on_event( + &self, + event: &tracing::Event<'_>, + _ctx: tracing_subscriber::layer::Context<'_, S>, + ) { + let metadata = event.metadata(); + + if self.is_enabled(metadata) { + let mut visitor = ValueDebugStorage::default(); + event.record(&mut visitor); + let mut values = visitor.values; + + if let Some(msg) = values.remove("message") { + self.log(msg, metadata.level()) + } + } + } +} + +#[derive(Default)] +pub struct ValueDebugStorage { + pub values: HashMap, +} + +impl Visit for ValueDebugStorage { + fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) { + self.values + .insert(field.name().to_string(), format!("{:?}", value)); + } +} diff --git a/src/logging/handler.rs b/src/logging/handler.rs new file mode 100644 index 0000000..0e9ff6b --- /dev/null +++ b/src/logging/handler.rs @@ -0,0 +1,249 @@ +use colored::Colorize; +use indicatif::{MultiProgress, ProgressBar, ProgressDrawTarget}; +use parking_lot::{Mutex, RwLock}; +use std::{ + fmt::Display, + io::{self, Write}, + mem, + sync::{atomic::AtomicBool, Arc}, + time::Duration, +}; + +use crate::{internal::utils::wrap_text, uwu}; + +use super::{get_logger, Verbosity}; + +const OK_SYMBOL: &str = "❖"; +const ERR_SYMBOL: &str = "X"; +const WARN_SYMBOL: &str = "!"; +const DEBUG_SYMBOL: &str = "⌘"; +const TRACE_SYMBOL: &str = "🗲"; + +pub struct LogHandler { + level: Arc>, + output_type: Arc>, + uwu_enabled: Arc, +} + +impl Default for LogHandler { + fn default() -> Self { + Self { + level: Arc::new(RwLock::new(Verbosity::Info)), + output_type: Arc::new(RwLock::new(OutputType::Stderr)), + uwu_enabled: Arc::new(AtomicBool::new(false)), + } + } +} + +#[allow(unused)] +pub enum OutputType { + Stdout, + Stderr, + MultiProgress(Arc), + Progress(Arc), + Buffer { + buffer: Arc>>, + suspended: Box, + }, +} + +pub struct SuspendHandle; + +impl LogHandler { + pub fn log_error(&self, msg: String) { + if self.is_loggable(Verbosity::Error) { + let msg = self.preformat_msg(msg); + let msg = format!("{} {}", ERR_SYMBOL.red().bold(), msg.bold().red()); + self.log(msg); + } + } + + pub fn log_warning(&self, msg: String) { + if self.is_loggable(Verbosity::Warning) { + let msg = self.preformat_msg(msg); + let msg = format!("{} {}", WARN_SYMBOL.yellow(), msg.yellow().bold()); + self.log(msg); + } + } + + pub fn log_info(&self, msg: String) { + if self.is_loggable(Verbosity::Info) { + let msg = self.preformat_msg(msg); + let msg = format!("{} {}", OK_SYMBOL.purple(), msg.bold()); + self.log(msg); + } + } + + pub fn log_debug(&self, msg: String) { + if self.is_loggable(Verbosity::Debug) { + let msg = self.preformat_msg(msg); + let msg = format!("{} {}", DEBUG_SYMBOL.blue(), msg); + + self.log(msg); + } + } + + pub fn log_trace(&self, msg: String) { + if self.is_loggable(Verbosity::Trace) { + let msg = self.preformat_msg(msg); + let msg = format!("{} {}", TRACE_SYMBOL.cyan(), msg.dimmed()); + self.log(msg); + } + } + + pub fn print_list, T: Display>(&self, list: I, separator: &str) { + let lines = list + .into_iter() + .map(|l| self.preformat_msg(l.to_string())) + .fold(String::new(), |acc, line| { + format!("{}{}{}", acc, separator, line) + }); + + let lines = wrap_text(lines).join("\n"); + self.log(lines) + } + + pub fn print_newline(&self) { + self.log(String::from("")) + } + + pub fn set_verbosity(&self, level: Verbosity) { + (*self.level.write()) = level; + } + + pub fn reset_output_type(&self) { + self.set_output_type(OutputType::Stdout); + } + + #[must_use] + pub fn suspend(&self) -> SuspendHandle { + let mut output_type = self.output_type.write(); + let mut old_output_type = OutputType::Stdout; + mem::swap(&mut *output_type, &mut old_output_type); + + (*output_type) = OutputType::Buffer { + buffer: Arc::new(Mutex::new(Vec::new())), + suspended: Box::new(old_output_type), + }; + + SuspendHandle + } + + pub fn unsuspend(&self) { + let mut buffered = Vec::new(); + { + let mut output_type = self.output_type.write(); + let mut old_output_type = OutputType::Stdout; + mem::swap(&mut *output_type, &mut old_output_type); + + if let OutputType::Buffer { buffer, suspended } = old_output_type { + (*output_type) = *suspended; + buffered = mem::take(&mut *buffer.lock()); + } + } + + buffered.into_iter().for_each(|msg| self.log(msg)); + } + + /// Creates a new progress spinner and registers it on the log handler + pub fn new_progress_spinner(&self) -> Arc { + let pb = ProgressBar::new_spinner(); + pb.enable_steady_tick(Duration::from_millis(250)); + + let mut output_type = self.output_type.write(); + + if let OutputType::MultiProgress(mp) = &*output_type { + Arc::new(mp.add(pb)) + } else { + let pb = Arc::new(pb); + *output_type = OutputType::Progress(pb.clone()); + + pb + } + } + + pub fn new_multi_progress(&self) -> Arc { + let mp = Arc::new(MultiProgress::new()); + self.set_output_type(OutputType::MultiProgress(mp.clone())); + + mp + } + + /// Sets the output type of the log handler to either stdout/stderr or a progress bar + pub fn set_output_type(&self, mut output: OutputType) { + { + let mut output_type = self.output_type.write(); + mem::swap(&mut *output_type, &mut output); + } + + match &mut output { + OutputType::MultiProgress(mp) => mp.set_draw_target(ProgressDrawTarget::hidden()), + OutputType::Progress(p) => p.set_draw_target(ProgressDrawTarget::hidden()), + OutputType::Buffer { + buffer, + suspended: _, + } => { + let buffered = mem::take(&mut *buffer.lock()); + buffered.into_iter().for_each(|c| self.log(c)); + } + _ => {} + } + } + + #[tracing::instrument(level = "trace", skip_all)] + pub fn set_uwu_enabled(&self, enabled: bool) { + self.uwu_enabled + .store(enabled, std::sync::atomic::Ordering::Relaxed); + } + + pub(crate) fn is_loggable(&self, level: Verbosity) -> bool { + (*self.level.read()) >= level + } + + /// Flushes the output buffer + pub fn flush(&self) { + let output = self.output_type.read(); + match &*output { + OutputType::Stdout => io::stdout().flush().unwrap(), + OutputType::Stderr => io::stderr().flush().unwrap(), + OutputType::Progress(p) => p.tick(), + _ => {} + } + } + + fn preformat_msg(&self, msg: String) -> String { + let msg = self.apply_uwu(msg); + + wrap_text(msg).join("\n") + } + + fn apply_uwu(&self, msg: String) -> String { + if self.uwu_enabled.load(std::sync::atomic::Ordering::Relaxed) { + uwu!(msg) + } else { + msg + } + } + + fn log(&self, msg: String) { + let output_type = self.output_type.read(); + match &*output_type { + OutputType::Stdout => println!("{}", msg), + OutputType::Stderr => eprintln!("{}", msg), + OutputType::MultiProgress(m) => { + let _ = m.println(msg); + } + OutputType::Progress(p) => p.println(msg), + OutputType::Buffer { + buffer, + suspended: _, + } => buffer.lock().push(msg), + }; + } +} + +impl Drop for SuspendHandle { + fn drop(&mut self) { + get_logger().unsuspend(); + } +} diff --git a/src/logging/mod.rs b/src/logging/mod.rs new file mode 100644 index 0000000..16bf270 --- /dev/null +++ b/src/logging/mod.rs @@ -0,0 +1,74 @@ +use std::sync::Arc; + +use lazy_static::lazy_static; +use tracing::Level; +use tracing_error::ErrorLayer; +use tracing_subscriber::prelude::__tracing_subscriber_SubscriberExt; +use tracing_subscriber::Registry; + +mod fmt_layer; +use fmt_layer::AmeFormatLayer; + +use crate::internal::uwu_enabled; + +use self::handler::LogHandler; +pub mod handler; +pub mod output; +pub mod piped_stdio; + +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub enum Verbosity { + #[allow(dead_code)] + Error = 0, + #[allow(dead_code)] + Warning = 1, + Info = 2, + Debug = 3, + Trace = 4, +} + +impl From for Verbosity { + fn from(num_verbosity: usize) -> Self { + match num_verbosity { + 0 => Self::Info, + 1 => Self::Debug, + 2 => Self::Trace, + _ => Self::Info, + } + } +} + +impl Verbosity { + fn from_level(l: &Level) -> Self { + match *l { + Level::ERROR => Self::Error, + Level::WARN => Self::Warning, + Level::INFO => Self::Info, + Level::DEBUG => Self::Debug, + Level::TRACE => Self::Trace, + } + } +} + +/// Initializes the tracing logger +/// Can be used for debug purposes _or_ verbose output +pub fn init_logger(verbosity: Verbosity) { + let logger = get_logger(); + logger.set_verbosity(verbosity); + logger.set_uwu_enabled(uwu_enabled()); + let ame_layer = AmeFormatLayer::new(logger); + + let subscriber = Registry::default() + .with(ErrorLayer::default()) + .with(ame_layer); + tracing::subscriber::set_global_default(subscriber).unwrap(); +} + +/// Returns the global logger instance +pub fn get_logger() -> Arc { + lazy_static! { + static ref LOGGER: Arc = Arc::new(LogHandler::default()); + } + + Arc::clone(&LOGGER) +} diff --git a/src/logging/output.rs b/src/logging/output.rs new file mode 100644 index 0000000..224a713 --- /dev/null +++ b/src/logging/output.rs @@ -0,0 +1,101 @@ +use std::collections::{HashMap, HashSet}; + +use aur_rpc::PackageInfo; +use console::Alignment; +use crossterm::style::Stylize; + +use crate::{builder::pacman::PacmanQueryBuilder, internal::dependencies::DependencyInformation}; + +use super::get_logger; + +pub async fn print_dependency_list(dependencies: &[DependencyInformation]) -> bool { + let (mut deps_repo, mut makedeps_repo, deps_aur, makedeps_aur) = dependencies + .iter() + .map(|d| { + ( + d.depends.repo.iter().collect(), + d.make_depends.repo.iter().collect(), + d.depends.aur.iter().collect(), + d.make_depends.aur.iter().collect(), + ) + }) + .fold( + (Vec::new(), Vec::new(), Vec::new(), Vec::new()), + |mut acc, mut deps| { + acc.0.append(&mut deps.0); + acc.1.append(&mut deps.1); + acc.2.append(&mut deps.2); + acc.3.append(&mut deps.3); + + acc + }, + ); + deps_repo.dedup(); + makedeps_repo.dedup(); + + let mut empty = true; + if !deps_repo.is_empty() { + tracing::info!("Repo dependencies"); + get_logger().print_list(&deps_repo, " "); + empty = false; + get_logger().print_newline(); + } + if !deps_aur.is_empty() { + tracing::info!("AUR dependencies"); + print_aur_package_list(&deps_aur).await; + empty = false; + get_logger().print_newline(); + } + + if !makedeps_repo.is_empty() { + tracing::info!("Repo make dependencies"); + get_logger().print_list(&makedeps_repo, " "); + empty = false; + get_logger().print_newline(); + } + + if !makedeps_aur.is_empty() { + tracing::info!("AUR make dependencies"); + print_aur_package_list(&makedeps_aur).await; + empty = false; + get_logger().print_newline(); + } + + empty +} + +pub async fn print_aur_package_list(packages: &[&PackageInfo]) -> bool { + let pkgs = packages + .iter() + .map(|p| p.metadata.name.clone()) + .collect::>(); + let installed = PacmanQueryBuilder::all() + .query_with_output() + .await + .unwrap() + .into_iter() + .filter(|p| pkgs.contains(&p.name)) + .map(|p| (p.name.clone(), p)) + .collect::>(); + + get_logger().print_list( + packages.iter().map(|pkg| { + format!( + "{} version {} ({} votes) {}", + console::pad_str(&pkg.metadata.name, 30, Alignment::Left, Some("...")).bold(), + pkg.metadata.version.clone().dim(), + pkg.metadata.num_votes, + if installed.contains_key(&pkg.metadata.name) { + "(Installed)" + } else { + "" + } + .bold() + .magenta() + ) + }), + "\n ", + ); + + !installed.is_empty() +} diff --git a/src/logging/piped_stdio.rs b/src/logging/piped_stdio.rs new file mode 100644 index 0000000..11343da --- /dev/null +++ b/src/logging/piped_stdio.rs @@ -0,0 +1,57 @@ +use std::mem; + +use tokio::{ + io::{AsyncRead, AsyncReadExt}, + process::{ChildStderr, ChildStdout}, +}; + +use crate::internal::error::{AppError, AppResult}; + +pub struct StdioReader { + stdout: ChildStdout, + stderr: ChildStderr, + stdout_line: Vec, + stderr_line: Vec, +} + +impl StdioReader { + pub fn new(stdout: ChildStdout, stderr: ChildStderr) -> Self { + Self { + stdout, + stderr, + stdout_line: Vec::new(), + stderr_line: Vec::new(), + } + } + + pub async fn read_line(&mut self) -> AppResult { + let line = tokio::select! { + l = Self::read_stdio(&mut self.stdout, &mut self.stdout_line) => {l?} + l = Self::read_stdio(&mut self.stderr, &mut self.stderr_line) => {l?} + }; + + Ok(line) + } + + pub async fn read_stdio( + reader: &mut R, + buf: &mut Vec, + ) -> AppResult { + while let Ok(ch) = reader.read_u8().await { + if ch == b'\n' { + if !buf.is_empty() { + break; + } + } else { + buf.push(ch); + } + } + + let line = mem::take(buf); + if line.is_empty() { + Err(AppError::from("stdio exhausted")) + } else { + Ok(String::from_utf8(line).unwrap()) + } + } +} diff --git a/src/main.rs b/src/main.rs index 5cc42d7..70a703f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,145 +1,78 @@ -#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)] -#![allow(clippy::too_many_lines)] - -use args::Args; -use clap::{CommandFactory, Parser}; -use clap_complete::{Generator, Shell}; +use args::{Args, GenCompArgs, InfoArgs}; +use builder::pacman::{PacmanColor, PacmanQueryBuilder}; +use clap::Parser; use internal::commands::ShellCommand; use internal::error::SilentUnwrap; -use std::env; -use std::fs; -use std::path::Path; -use std::str::FromStr; -use crate::args::{ - GenCompArgs, InfoArgs, InstallArgs, Operation, QueryArgs, RemoveArgs, SearchArgs, UpgradeArgs, -}; +use crate::args::{InstallArgs, Operation, QueryArgs, RemoveArgs, SearchArgs}; +use crate::internal::detect; use crate::internal::exit_code::AppExitCode; -use crate::internal::utils::pager; -use crate::internal::{detect, init, sort, start_sudoloop, structs::Options}; - -#[global_allocator] -static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc; +use crate::internal::{sort, start_sudoloop, structs::Options}; +use clap_complete::{Generator, Shell}; +use std::str::FromStr; mod args; +mod builder; +mod interact; mod internal; +mod logging; mod operations; +use logging::init_logger; -fn main() { - // Break if we are running as root +#[tokio::main] +async fn main() { + color_eyre::install().unwrap(); if unsafe { libc::geteuid() } == 0 { crash!( AppExitCode::RunAsRoot, "Running amethyst as root is disallowed as it can lead to system breakage. Instead, amethyst will prompt you when it needs superuser permissions"); } - // Parse arguments let args: Args = Args::parse(); + init_logger(args.verbose.into()); - // Initialize variables - let verbosity = args.verbose; let noconfirm = args.no_confirm; - // Get options struct let options = Options { - verbosity, noconfirm, asdeps: false, }; - // Ensure amethyst is initialized - init(options); - - // Start sudoloop if specified if args.sudoloop { - start_sudoloop(); - } - - let cachedir = if args.cachedir.is_none() { - "".to_string() - } else { - // Create cache directory if it doesn't exist - if fs::metadata(&args.cachedir.as_ref().unwrap()).is_err() { - fs::create_dir(&args.cachedir.as_ref().unwrap()).unwrap_or_else(|err| { - crash!( - AppExitCode::FailedCreatingPaths, - "Could not create cache directory: {}", - err - ); - }); - } - Path::new(&args.cachedir.unwrap()) - .canonicalize() - .unwrap() - .to_str() - .unwrap() - .to_string() - }; - - // List of possible options - let opers = vec![ - "install", "remove", "upgrade", "search", "query", "info", "clean", "diff", "gencomp", - ]; - - // If arg is completely unrecognized, attempt to pass it to pacman - if let Some((ext, ext_m)) = args::Args::command().get_matches().subcommand() { - if !opers.contains(&ext) { - let mut m = ext_m - .values_of("") - .unwrap_or_default() - .collect::>(); - m.insert(0, ext); - - info!("Passing unrecognized flags \"{}\" to pacman", m.join(" ")); - - let child = ShellCommand::pacman() - .args(m) - .elevated() - .wait() - .silent_unwrap(AppExitCode::PacmanError); - std::process::exit(child.code().unwrap_or(1)); - } + start_sudoloop().await; } - // Match args match args.subcommand.unwrap_or_default() { - Operation::Install(install_args) => cmd_install(install_args, options, &cachedir), - Operation::Remove(remove_args) => cmd_remove(remove_args, options), - Operation::Search(search_args) => cmd_search(&search_args, options), - Operation::Query(query_args) => cmd_query(&query_args), - Operation::Info(info_args) => cmd_info(info_args), - Operation::Upgrade(upgrade_args) => cmd_upgrade(upgrade_args, options, &cachedir), - Operation::Clean => { - info!("Removing orphaned packages"); - operations::clean(options); + Operation::Install(install_args) => cmd_install(install_args, options).await, + Operation::Remove(remove_args) => cmd_remove(remove_args, options).await, + Operation::Search(search_args) => cmd_search(search_args, options).await, + Operation::Query(query_args) => cmd_query(query_args).await, + Operation::Upgrade(upgrade_args) => { + tracing::info!("Performing system upgrade"); + operations::upgrade(upgrade_args, options).await; } - Operation::Diff => { - info!("Running pacdiff"); - detect(); - } - Operation::GenComp(gencomp_args) => { - info!("Generating shell completions for {}. Please pipe `stderr` to a file to get completions as a file, e.g. `ame gencomp fish 2> file.fish`", gencomp_args.shell); - cmd_gencomp(&gencomp_args); + Operation::Clean => { + tracing::info!("Removing orphaned packages"); + operations::clean(options).await; } + Operation::Info(info_args) => cmd_info(info_args).await, + Operation::GenComp(gen_args) => cmd_gencomp(&gen_args), + Operation::Diff => todo!(), } + + detect().await; } -fn cmd_install(args: InstallArgs, options: Options, cachedir: &str) { - // Initialise variables +#[tracing::instrument(level = "trace")] +async fn cmd_install(args: InstallArgs, options: Options) { let packages = args.packages; + let sorted = sort(&packages, options).await; - if args.aur && args.repo { - crash!(AppExitCode::Other, "Cannot specify both --aur and --repo"); + if !sorted.repo.is_empty() { + operations::install(sorted.repo, options).await; + } + if !sorted.aur.is_empty() { + operations::aur_install(sorted.aur, options).await; } - - let aur = args.aur || env::args().collect::>()[1] == "-Sa"; - let repo = args.repo || env::args().collect::>()[1] == "-Sr"; - - let sorted = sort(&packages, options); - let config = internal::config::read(); - - info!("Attempting to install packages: {}", packages.join(", ")); - if !sorted.nf.is_empty() { - // If some packages are not found, crash crash!( AppExitCode::PacmanError, "Couldn't find packages: {} in repos or the AUR", @@ -147,149 +80,80 @@ fn cmd_install(args: InstallArgs, options: Options, cachedir: &str) { ); } - if !repo && !aur && !sorted.repo.is_empty() || repo && !sorted.repo.is_empty() { - // If repo packages found, install them - operations::install(&sorted.repo, options); - } - if !repo && !aur && !sorted.aur.is_empty() || aur && !sorted.aur.is_empty() { - // If AUR packages found, install them - operations::aur_install(sorted.aur, options, cachedir); - } - - // Show optional dependencies for installed packages - if packages.len() > 1 && config.base.highlight_optdepends { - info!("Showing optional dependencies for installed packages"); - for p in packages { - let out = std::process::Command::new("expac") - .args(&["-Q", "-l", "\n ", " %O", &p]) - .output() - .unwrap() - .stdout; - let out = String::from_utf8(out).unwrap().trim().to_string(); - if !out.is_empty() { - info!("{}:", p); - println!(" {}", out); - } - } + let bash_output = ShellCommand::bash() + .arg("-c") + .arg("sudo find /etc -name *.pacnew") + .wait_with_output() + .await + .silent_unwrap(AppExitCode::Other) + .stdout; + + if !bash_output.is_empty() { + let pacnew_files = bash_output + .split_whitespace() + .collect::>() + .join(", "); + tracing::info!("You have .pacnew files in /etc ({pacnew_files}) that you haven't removed or acted upon, it is recommended you do that now" ); } } -fn cmd_remove(args: RemoveArgs, options: Options) { - // Initialise variables +#[tracing::instrument(level = "trace")] +async fn cmd_remove(args: RemoveArgs, options: Options) { let packages = args.packages; - - info!("Uninstalling packages: {}", &packages.join(", ")); - - // Remove packages - operations::uninstall(&packages, options); + tracing::info!("Uninstalling packages: {}", &packages.join(", ")); + operations::uninstall(packages, options).await; } -fn cmd_search(args: &SearchArgs, options: Options) { - // Initialise variables - let query_string = args.search.join(" "); - - // Logic for searching - let repo = args.repo || env::args().collect::>()[1] == "-Ssr"; - let aur = args.aur || env::args().collect::>()[1] == "-Ssa"; - let both = !repo && !aur; - - // Start repo spinner - let repo_results = if repo || both { - let rsp = spinner!("Searching repos for {}", query_string); - - // Search repos - let ret = operations::search(&query_string, options); - rsp.stop_bold("Repo search complete"); - - ret - } else { - "".to_string() - }; - - // Start AUR spinner - let aur_results = if aur || both { - // Strip query of any non-alphanumeric characters - let query_string = query_string.replace(|c: char| !c.is_alphanumeric() && c != '-', ""); - - let asp = spinner!("Searching AUR for {}", query_string); - - // Search AUR - let ret = operations::aur_search(&query_string, options); - asp.stop_bold("AUR search complete"); +#[tracing::instrument(level = "trace")] +async fn cmd_search(args: SearchArgs, options: Options) { + let query_string = args.search; - ret - } else { - "".to_string() - }; - - let results = repo_results + "\n" + &aur_results; - - // Print results either way, so that the user can see the results after they exit `less` - let text = if internal::uwu_enabled() { - uwu!(results.trim()) - } else { - results.trim().to_string() - }; - - println!("{}", text); + if args.aur { + tracing::info!("Searching AUR for {}", &query_string); + operations::aur_search(&query_string, args.by, options).await; + } + if args.repo { + tracing::info!("Searching repos for {}", &query_string); + operations::search(&query_string, options).await; + } - // Check if results are longer than terminal height - if results.lines().count() > crossterm::terminal::size().unwrap().1 as usize { - // If so, paginate results - #[allow(clippy::let_underscore_drop)] - let _ = pager(&results.trim().to_string()); + if !args.aur && !args.repo { + tracing::info!("Searching AUR and repos for {}", &query_string); + operations::search(&query_string, options).await; + operations::aur_search(&query_string, args.by, options).await; } } -fn cmd_query(args: &QueryArgs) { - let aur = args.aur - || env::args().collect::>()[1] == "-Qa" - || env::args().collect::>()[1] == "-Qm"; - let repo = args.repo - || env::args().collect::>()[1] == "-Qr" - || env::args().collect::>()[1] == "-Qn"; - let both = !aur && !repo; - - if aur { - // If AUR query, query AUR - ShellCommand::pacman() - .arg("-Qm") - .wait_success() - .silent_unwrap(AppExitCode::PacmanError); - } - if repo { - // If repo query, query repos - ShellCommand::pacman() - .arg("-Qn") - .wait_success() +#[tracing::instrument(level = "trace")] +async fn cmd_query(args: QueryArgs) { + if args.repo || !args.aur { + tracing::info!("Installed Repo Packages: "); + PacmanQueryBuilder::native() + .color(PacmanColor::Always) + .query() + .await .silent_unwrap(AppExitCode::PacmanError); } - if both { - // If no query type specified, query both - ShellCommand::pacman() - .arg("-Qn") - .wait_success() - .silent_unwrap(AppExitCode::PacmanError); - ShellCommand::pacman() - .arg("-Qm") - .wait_success() + if args.aur || !args.repo { + tracing::info!("Installed AUR Packages: "); + PacmanQueryBuilder::foreign() + .color(PacmanColor::Always) + .query() + .await .silent_unwrap(AppExitCode::PacmanError); } } -fn cmd_info(args: InfoArgs) { - ShellCommand::pacman() - .arg("-Qi") - .arg(args.package) - .wait() +#[tracing::instrument(level = "trace")] +async fn cmd_info(args: InfoArgs) { + PacmanQueryBuilder::info() + .package(args.package) + .query() + .await .silent_unwrap(AppExitCode::PacmanError); } -fn cmd_upgrade(args: UpgradeArgs, options: Options, cachedir: &str) { - info!("Performing system upgrade"); - operations::upgrade(options, args, cachedir); -} - +#[tracing::instrument(level = "trace")] fn cmd_gencomp(args: &GenCompArgs) { let shell: Shell = Shell::from_str(&args.shell).unwrap_or_else(|e| { crash!(AppExitCode::Other, "Invalid shell: {}", e); diff --git a/src/operations/aur_install.rs b/src/operations/aur_install.rs deleted file mode 100644 index b64b23f..0000000 --- a/src/operations/aur_install.rs +++ /dev/null @@ -1,427 +0,0 @@ -use chrono::{Local, TimeZone}; -use std::env::set_current_dir; -use std::path::Path; -use std::process::Command; -use std::{env, fs}; - -use crate::internal::commands::ShellCommand; -use crate::internal::config; -use crate::internal::error::SilentUnwrap; -use crate::internal::exit_code::AppExitCode; -use crate::internal::rpc::rpcinfo; -use crate::internal::sort; -use crate::operations::install; -use crate::{crash, info, log, prompt, warn, Options}; - -const AUR_CACHE: &str = ".cache/ame"; - -/// Return a list of all files/dirs in a directory. -fn list(dir: &str) -> Vec { - let dirs = fs::read_dir(Path::new(&dir)).unwrap(); - let dirs: Vec = dirs - .map(|dir| { - (*dir - .unwrap() - .path() - .to_str() - .unwrap() - .split('/') - .collect::>() - .last() - .unwrap()) - .to_string() - }) - .collect(); - dirs -} - -/// Returns and creates a temporary directory for amethyst to use -fn mktemp() -> String { - let tempdir = Command::new("mktemp") - .args(&["-d", "/tmp/ame.XXXXXX.tmp"]) - .output() - .unwrap() - .stdout; - - String::from_utf8(tempdir).unwrap().trim().to_string() -} - -/// Help the user review and/or edit an AUR package before installing -fn review(cachedir: &str, pkg: &str, orig_cachedir: &str) { - // Prompt user to view PKGBUILD - let p0 = prompt!(default false, "Would you like to review and/or edit {}'s PKGBUILD (and any adjacent build files if present)?", pkg); - if p0 { - info!("This will drop you into a standard `bash` shell (unless set otherwise in the config) in the package's cache directory. If any changes are made, you will be prompted whether to save them to your home directory. To stop reviewing/editing, just run `exit`"); - let p1 = prompt!(default true, - "Continue?" - ); - - if p1 { - let config = config::read(); - let cdir = env::current_dir().unwrap().to_str().unwrap().to_string(); - set_current_dir(Path::new(&format!("{}/{}", &cachedir, pkg))).unwrap(); - - if config.extra.review_user_shell { - Command::new(&env::var("SHELL").unwrap()) - .spawn() - .unwrap() - .wait() - .unwrap(); - } else { - ShellCommand::bash().wait().unwrap(); - } - - set_current_dir(Path::new(&cdir)).unwrap(); - - // Prompt user to save changes - let p2 = prompt!(default false, - "Save changes to package {}?", - pkg - ); - if p2 { - // Save changes to ~/.local/share - let dest = format!( - "{}-saved-{}", - pkg, - chrono::Local::now() - .naive_local() - .format("%Y-%m-%d_%H-%M-%S") - ); - Command::new("cp") - .arg("-r") - .arg(format!("{}/{}", cachedir, pkg)) - .arg(format!( - "{}/.local/share/ame/{}", - env::var("HOME").unwrap(), - dest - )) - .spawn() - .unwrap() - .wait() - .unwrap(); - - // Alert user - info!("Saved changes to ~/.local/share/ame/{}", dest); - }; - } - } - - // Prompt user to continue - let p = prompt!(default true, "Would you still like to install {}?", pkg); - if !p { - // If not, crash - if orig_cachedir.is_empty() { - fs::remove_dir_all(format!("{}/{}", cachedir, pkg)).unwrap(); - } - crash!(AppExitCode::UserCancellation, "Not proceeding"); - }; -} - -/// Finalize a build/install process -fn finish(cachedir: &str, pkg: &str, options: &Options) { - // Install all packages from cachedir except `pkg` using --asdeps - let dirs = list(cachedir); - - // Get a list of packages in cachedir - if dirs.len() > 1 { - info!("Installing AUR dependencies for {}", pkg); - let cmd = std::process::Command::new("bash") - .args(&[ - "-cO", - "extglob", - format!( - "sudo pacman -U --asdeps {}/!({})/*.pkg.tar.* {}", - cachedir, - pkg, - if options.noconfirm { "--noconfirm" } else { "" } - ) - .as_str(), - ]) - .spawn() - .unwrap() - .wait() - .unwrap(); - if cmd.success() { - info!("All AUR dependencies for package {} installed", pkg); - } else { - crash!( - AppExitCode::PacmanError, - "AUR dependencies failed to install" - ); - } - } - - // Install package explicitly - info!("Installing {}", pkg); - let cmd = std::process::Command::new("bash") - .args(&[ - "-c", - format!( - "sudo pacman -U {}/{}/*.pkg.tar.* {}", - cachedir, - pkg, - if options.noconfirm { "--noconfirm" } else { "" } - ) - .as_str(), - ]) - .spawn() - .unwrap() - .wait() - .unwrap(); - if cmd.success() { - info!("{} installed!", pkg); - } else { - crash!(AppExitCode::PacmanError, "{} failed to install", pkg); - } -} - -/// Clone a package from the AUR -fn clone(pkg: &String, pkgcache: &str, options: &Options) { - let url = crate::internal::rpc::URL; - - // See if package is already cloned to AUR_CACHE - let dirs = list(pkgcache); - if dirs.contains(pkg) { - // Enter directory and git pull - if options.verbosity > 1 { - log!("Updating cached PKGBUILD for {}", pkg); - } - info!("Updating cached package source"); - set_current_dir(Path::new(&format!( - "{}/{}/{}", - env::var("HOME").unwrap(), - AUR_CACHE, - pkg - ))) - .unwrap(); - ShellCommand::git() - .arg("pull") - .wait() - .silent_unwrap(AppExitCode::GitError); - } else { - // Clone package into cachedir - if options.verbosity >= 1 { - log!("Cloning {} into cachedir", pkg); - } - info!("Cloning package source"); - set_current_dir(Path::new(&pkgcache)).unwrap(); - ShellCommand::git() - .arg("clone") - .arg(format!("{}/{}", url, pkg)) - .wait() - .silent_unwrap(AppExitCode::GitError); - // Enter directory and `makepkg -o` to fetch sources - if options.verbosity > 1 { - log!("Fetching sources for {}", pkg); - } - info!("Fetching sources"); - set_current_dir(Path::new(&format!( - "{}/{}/{}", - env::var("HOME").unwrap(), - AUR_CACHE, - pkg - ))) - .unwrap(); - ShellCommand::makepkg() - .arg("-od") - .wait() - .silent_unwrap(AppExitCode::MakePkgError); - } -} - -/// General function to handle installing AUR packages. -pub fn aur_install(a: Vec, options: Options, orig_cachedir: &str) { - // Initialise variables - let cachedir = if options.asdeps || !orig_cachedir.is_empty() { - orig_cachedir.to_string() - } else { - mktemp() - }; - let pkgcache = format!("{}/{}", env::var("HOME").unwrap(), AUR_CACHE); - let verbosity = options.verbosity; - let noconfirm = options.noconfirm; - - if verbosity >= 1 { - log!("Installing from AUR: {:?}", &a); - } - - info!("Installing packages {} from the AUR", a.join(", ")); - - let mut failed: Vec = vec![]; - - for package in a { - // Don't process packages if they are already in the cachedir - let dirs = list(&cachedir); - if dirs.contains(&package) { - continue; - } - - // Query AUR for package info - let rpcres = rpcinfo(&package); - if !rpcres.found { - // If package isn't found, break - break; - } - - // Get package name - let pkg = &rpcres.package.as_ref().unwrap().name; - let ood = rpcres.package.as_ref().unwrap().out_of_date; - - // If package is out of date, warn user - if ood.is_some() { - warn!( - "Package {} is marked as out of date since [{}], it might be broken, not install or not build properly", - pkg, - Local.timestamp(ood.unwrap().try_into().unwrap(), 0).date_naive() - ); - let p = prompt!(default false, "Would you like to continue?"); - if !p { - break; - } - } - - // Clone package into cachedir - clone(pkg, &pkgcache, &options); - - // Copy package from AUR_CACHE to cachedir - Command::new("cp") - .arg("-r") - .arg(format!( - "{}/{}/{}", - env::var("HOME").unwrap(), - AUR_CACHE, - pkg - )) - .arg(format!("{}/{}", cachedir, pkg)) - .spawn() - .unwrap() - .wait() - .unwrap(); - - // Sort dependencies and makedepends - if verbosity >= 1 { - log!("Sorting dependencies and makedepends"); - } - let mut sorted = sort(&rpcres.package.as_ref().unwrap().depends, options); - let mut md_sorted = sort(&rpcres.package.as_ref().unwrap().make_depends, options); - if verbosity >= 1 { - log!("Sorted dependencies for {} are:\n{:?}", pkg, &sorted); - log!("Sorted makedepends for {} are:\n{:?}", pkg, &md_sorted); - } - - // If any dependencies are not found in AUR or repos, crash - if !sorted.nf.is_empty() || !md_sorted.nf.is_empty() { - crash!( - AppExitCode::MissingDeps, - "Could not find dependencies {} for package {}, aborting", - sorted.nf.join(", "), - pkg, - ); - } - - // Create newopts struct for installing dependencies - let newopts = Options { - verbosity, - noconfirm, - asdeps: true, - }; - - // Get a list of installed packages - let installed = ShellCommand::pacman() - .elevated() - .args(&["-Qq"]) - .wait_with_output() - .silent_unwrap(AppExitCode::PacmanError) - .stdout - .split_whitespace() - .collect::>() - .iter() - .map(|s| (*s).to_string()) - .collect::>(); - - // Remove installed packages from sorted dependencies and makedepends - if verbosity >= 1 { - log!("Removing installed packages from sorted dependencies and makedepends"); - } - sorted.aur.retain(|x| !installed.contains(x)); - sorted.repo.retain(|x| !installed.contains(x)); - md_sorted.aur.retain(|x| !installed.contains(x)); - md_sorted.repo.retain(|x| !installed.contains(x)); - - // Prompt user to review/edit PKGBUILD - if !noconfirm { - review(&cachedir, pkg, orig_cachedir); - } - - // Install dependencies and makedepends - info!("Moving on to install dependencies"); - if !sorted.repo.is_empty() { - install(&sorted.repo, newopts); - } - if !sorted.aur.is_empty() { - aur_install(sorted.aur, newopts, &cachedir.clone()); - } - if !md_sorted.repo.is_empty() { - install(&md_sorted.repo, newopts); - } - if !md_sorted.aur.is_empty() { - aur_install(md_sorted.aur, newopts, &cachedir.clone()); - } - - // Build makepkg args - let mut makepkg_args = vec!["-rcd", "--skippgp", "--needed"]; - if options.asdeps { - makepkg_args.push("--asdeps"); - } - if options.noconfirm { - makepkg_args.push("--noconfirm"); - } - - // Enter cachedir and build package - info!("Building time!"); - set_current_dir(format!("{}/{}", cachedir, pkg)).unwrap(); - let status = ShellCommand::makepkg() - .args(makepkg_args) - .wait() - .silent_unwrap(AppExitCode::MakePkgError); - if !status.success() { - // If build failed, push to failed vec - failed.push(pkg.clone()); - return; - } - - // Return to cachedir - set_current_dir(&cachedir).unwrap(); - - // Finish installation process - if !options.asdeps { - finish(&cachedir, pkg, &options); - } - } - - // If any packages failed to build, warn user with failed packages - if !failed.is_empty() { - let failed_str = format!("{}.failed", cachedir); - warn!( - "Failed to build packages {}, keeping cache directory at {} for manual inspection", - failed.join(", "), - if orig_cachedir.is_empty() { - &cachedir - } else { - &failed_str - } - ); - if orig_cachedir.is_empty() { - Command::new("mv") - .args(&[&cachedir, &format!("{}.failed", cachedir)]) - .spawn() - .unwrap() - .wait() - .unwrap(); - } - } else if !options.asdeps && orig_cachedir.is_empty() { - rm_rf::remove(&cachedir).unwrap_or_else(|e| - crash!(AppExitCode::Other, "Could not remove cache directory at {}: {}. This could be a permissions issue with fakeroot, try running `sudo rm -rf {}`", cachedir, e, cachedir) - ); - } -} diff --git a/src/operations/aur_install/aur_dependency_installation.rs b/src/operations/aur_install/aur_dependency_installation.rs new file mode 100644 index 0000000..602cf07 --- /dev/null +++ b/src/operations/aur_install/aur_dependency_installation.rs @@ -0,0 +1,75 @@ +use aur_rpc::PackageInfo; +use futures::future; + +use crate::{ + builder::{makepkg::MakePkgBuilder, pacman::PacmanInstallBuilder}, + internal::{dependencies::DependencyInformation, error::AppResult}, + multi_progress, normal_output, numeric, + operations::{ + aur_install::common::{build_and_install, create_dependency_batches, download_aur_source}, + BuildContext, + }, +}; + +use super::aur_package_install::AurPackageInstall; + +pub struct AurDependencyInstallation { + pub options: crate::internal::structs::Options, + pub dependencies: Vec, + pub contexts: Vec, +} + +impl AurDependencyInstallation { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn install_aur_dependencies(self) -> AppResult { + let aur_dependencies: Vec<&PackageInfo> = self + .dependencies + .iter() + .flat_map(DependencyInformation::all_aur_depends) + .collect(); + + if !aur_dependencies.is_empty() { + tracing::info!( + "Installing {} from the aur", + numeric!(aur_dependencies.len(), "package"["s"]) + ); + let batches = create_dependency_batches(aur_dependencies); + tracing::debug!("aur install batches: {batches:?}"); + + for batch in batches { + self.install(batch).await.unwrap(); + } + } + + Ok(AurPackageInstall { + options: self.options, + dependencies: self.dependencies, + contexts: self.contexts, + }) + } + + #[tracing::instrument(level = "trace", skip(self))] + async fn install(&self, deps: Vec<&PackageInfo>) -> AppResult<()> { + multi_progress!(); + + let dep_contexts = future::try_join_all( + deps.into_iter() + .map(BuildContext::from) + .map(download_aur_source), + ) + .await?; + + normal_output!(); + + build_and_install( + dep_contexts, + MakePkgBuilder::default().as_deps(true), + PacmanInstallBuilder::default() + .no_confirm(self.options.noconfirm) + .as_deps(true), + ) + .await?; + + Ok(()) + } +} diff --git a/src/operations/aur_install/aur_download.rs b/src/operations/aur_install/aur_download.rs new file mode 100644 index 0000000..b5e15dc --- /dev/null +++ b/src/operations/aur_install/aur_download.rs @@ -0,0 +1,44 @@ +use aur_rpc::PackageInfo; + +use futures::future; + +use crate::{ + internal::{dependencies::DependencyInformation, error::AppResult, structs::Options}, + multi_progress, normal_output, + operations::BuildContext, +}; + +use super::aur_review::AurReview; + +pub struct AurDownload { + pub options: Options, + pub package_infos: Vec, + pub packages: Vec, + pub dependencies: Vec, +} + +impl AurDownload { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn download_sources(self) -> AppResult { + tracing::info!("Downloading sources"); + multi_progress!(); + + let contexts = future::try_join_all( + self.package_infos + .into_iter() + .map(BuildContext::from) + .map(super::common::download_aur_source), + ) + .await?; + + normal_output!(); + tracing::info!("All sources are ready."); + + Ok(AurReview { + options: self.options, + packages: self.packages, + dependencies: self.dependencies, + contexts, + }) + } +} diff --git a/src/operations/aur_install/aur_fetch.rs b/src/operations/aur_install/aur_fetch.rs new file mode 100644 index 0000000..ba708f2 --- /dev/null +++ b/src/operations/aur_install/aur_fetch.rs @@ -0,0 +1,76 @@ +use crossterm::style::Stylize; +use futures::future; + +use crate::{ + internal::{ + dependencies::DependencyInformation, + error::{AppError, AppResult}, + structs::Options, + }, + logging::output::{print_aur_package_list, print_dependency_list}, + normal_output, prompt, spinner, +}; + +use super::aur_download::AurDownload; + +pub struct AurFetch { + pub options: Options, + pub packages: Vec, +} + +impl AurFetch { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn fetch_package_info(self) -> AppResult { + let pb = spinner!("Fetching package information"); + + let package_infos = aur_rpc::info(&self.packages).await?; + + tracing::debug!("package info = {package_infos:?}"); + + if package_infos.len() != self.packages.len() { + pb.finish_with_message("Couldn't find all packages".red().to_string()); + let mut not_found = self.packages.clone(); + package_infos + .iter() + .for_each(|pkg| not_found.retain(|p| pkg.metadata.name != *p)); + return Err(AppError::MissingDependencies(not_found)); + } + + pb.finish_with_message("All packages found".green().to_string()); + normal_output!(); + + if print_aur_package_list(&package_infos.iter().collect::>()).await + && !self.options.noconfirm + && !prompt!(default yes, "Some packages are already installed. Continue anyway?") + { + return Err(AppError::UserCancellation); + } + + let pb = spinner!("Fetching package information"); + + let dependencies = future::try_join_all( + package_infos + .iter() + .map(|pkg| async { DependencyInformation::for_package(pkg).await }), + ) + .await?; + + pb.finish_and_clear(); + normal_output!(); + + print_dependency_list(&dependencies).await; + + if !self.options.noconfirm + && !prompt!(default yes, "Do you want to install these packages and package dependencies?") + { + Err(AppError::UserCancellation) + } else { + Ok(AurDownload { + options: self.options, + packages: self.packages, + package_infos, + dependencies, + }) + } + } +} diff --git a/src/operations/aur_install/aur_package_install.rs b/src/operations/aur_install/aur_package_install.rs new file mode 100644 index 0000000..1d6e642 --- /dev/null +++ b/src/operations/aur_install/aur_package_install.rs @@ -0,0 +1,37 @@ +use crate::{ + builder::{makepkg::MakePkgBuilder, pacman::PacmanInstallBuilder}, + internal::{dependencies::DependencyInformation, error::AppResult, structs::Options}, + numeric, + operations::aur_install::{ + common::build_and_install, make_dependency_removal::MakeDependencyRemoval, + }, +}; + +use super::BuildContext; + +pub struct AurPackageInstall { + pub options: Options, + pub dependencies: Vec, + pub contexts: Vec, +} + +impl AurPackageInstall { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn install_packages(self) -> AppResult { + tracing::info!( + "Installing {}", + numeric!(self.contexts.len(), "package"["s"]) + ); + build_and_install( + self.contexts, + MakePkgBuilder::default(), + PacmanInstallBuilder::default().no_confirm(self.options.noconfirm), + ) + .await?; + + Ok(MakeDependencyRemoval { + options: self.options, + dependencies: self.dependencies, + }) + } +} diff --git a/src/operations/aur_install/aur_review.rs b/src/operations/aur_install/aur_review.rs new file mode 100644 index 0000000..589140c --- /dev/null +++ b/src/operations/aur_install/aur_review.rs @@ -0,0 +1,77 @@ +use tokio::fs; + +use crate::{ + builder::pager::PagerBuilder, + internal::{ + dependencies::DependencyInformation, + error::{AppError, AppResult}, + structs::Options, + utils::get_cache_dir, + }, + multi_select, newline, prompt, select_opt, +}; + +use super::{repo_dependency_installation::RepoDependencyInstallation, BuildContext}; + +pub struct AurReview { + pub options: Options, + pub packages: Vec, + pub dependencies: Vec, + pub contexts: Vec, +} + +impl AurReview { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn review_pkgbuild(self) -> AppResult { + if !self.options.noconfirm { + let to_review = multi_select!(&self.packages, "Select packages to review"); + + for pkg in to_review.into_iter().filter_map(|i| self.packages.get(i)) { + self.review_single_package(pkg).await?; + } + if !prompt!(default yes, "Do you still want to install those packages?") { + return Err(AppError::UserCancellation); + } + } + Ok(RepoDependencyInstallation { + options: self.options, + dependencies: self.dependencies, + contexts: self.contexts, + }) + } + + async fn review_single_package(&self, pkg: &str) -> AppResult<()> { + newline!(); + tracing::info!("Reviewing {pkg}"); + let mut files_iter = fs::read_dir(get_cache_dir().join(pkg)).await?; + let mut files = Vec::new(); + + while let Some(file) = files_iter.next_entry().await? { + let path = file.path(); + + if path.is_file() { + files.push(file.path()); + } + } + + let file_names = files + .iter() + .map(|f| f.file_name().unwrap()) + .map(|f| f.to_string_lossy()) + .collect::>(); + + while let Some(selection) = select_opt!(&file_names, "Select a file to review") { + if let Some(path) = files.get(selection) { + if let Err(e) = PagerBuilder::default().path(path).open().await { + tracing::debug!("Pager error {e}"); + } + } else { + break; + } + } + + tracing::info!("Done reviewing {pkg}"); + + Ok(()) + } +} diff --git a/src/operations/aur_install/common.rs b/src/operations/aur_install/common.rs new file mode 100644 index 0000000..2f7c27f --- /dev/null +++ b/src/operations/aur_install/common.rs @@ -0,0 +1,294 @@ +use std::{collections::HashMap, path::Path, sync::Arc}; + +use aur_rpc::PackageInfo; +use crossterm::style::Stylize; +use futures::future; +use indicatif::ProgressBar; +use tokio::{ + fs::OpenOptions, + io::{AsyncWriteExt, BufWriter}, + process::{ChildStderr, ChildStdout}, + task, +}; + +use crate::{ + builder::{ + git::{GitCloneBuilder, GitPullBuilder}, + makepkg::MakePkgBuilder, + pacman::PacmanInstallBuilder, + pager::PagerBuilder, + }, + internal::{ + error::{AppError, AppResult}, + utils::{get_cache_dir, wrap_text}, + }, + logging::piped_stdio::StdioReader, + multi_progress, normal_output, numeric, + operations::PackageArchives, + prompt, spinner, +}; + +use super::{BuildContext, BuildPath, BuildStep}; + +#[tracing::instrument(level = "trace", skip_all)] +pub async fn download_aur_source(mut ctx: BuildContext) -> AppResult { + let pkg_name = &ctx.package.metadata.name; + let base_pkg = &ctx.package.metadata.package_base; + let pb = spinner!("{}: Downloading sources", pkg_name.clone().bold()); + + let cache_dir = get_cache_dir(); + let pkg_dir = cache_dir.join(&pkg_name); + + if pkg_dir.exists() { + pb.set_message(format!( + "{}: Pulling latest changes", + pkg_name.clone().bold() + )); + GitPullBuilder::default().directory(&pkg_dir).pull().await?; + } else { + let aur_url = crate::internal::rpc::URL; + let repository_url = format!("{aur_url}/{base_pkg}"); + pb.set_message(format!( + "{}: Cloning aur repository", + pkg_name.clone().bold() + )); + + GitCloneBuilder::default() + .url(repository_url) + .directory(&pkg_dir) + .clone() + .await?; + + pb.set_message(format!( + "{}: Downloading and extracting files", + pkg_name.clone().bold() + )); + + MakePkgBuilder::default() + .directory(&pkg_dir) + .no_build(true) + .no_deps(true) + .no_prepare(true) + .skip_pgp(true) + .run() + .await?; + } + pb.finish_with_message(format!( + "{}: {}", + pkg_name.clone().bold(), + "Downloaded!".green() + )); + ctx.step = BuildStep::Build(BuildPath(pkg_dir)); + + Ok(ctx) +} + +#[tracing::instrument(level = "trace")] +pub fn create_dependency_batches(deps: Vec<&PackageInfo>) -> Vec> { + let mut deps: HashMap = deps + .into_iter() + .map(|d| (d.metadata.name.clone(), d)) + .collect(); + let mut batches = Vec::new(); + let mut relaxed = false; + + while !deps.is_empty() { + let mut current_batch = HashMap::new(); + + for (key, info) in deps.clone() { + let contains_make_dep = info + .make_depends + .iter() + .any(|d| current_batch.contains_key(d) || deps.contains_key(d)); + + let contains_dep = info + .depends + .iter() + .any(|d| current_batch.contains_key(d) || deps.contains_key(d)); + + if (!contains_dep || relaxed) && contains_make_dep { + deps.remove(&key); + current_batch.insert(key, info); + if relaxed { + break; + } + } + } + + if current_batch.is_empty() { + relaxed = true; + } else { + batches.push(current_batch.into_iter().map(|(_, v)| v).collect()); + relaxed = false; + } + } + + batches +} + +#[tracing::instrument(level = "trace")] +pub async fn build_and_install( + ctxs: Vec, + make_opts: MakePkgBuilder, + install_opts: PacmanInstallBuilder, +) -> AppResult<()> { + tracing::info!("Building packages"); + multi_progress!(); + let results = future::join_all( + ctxs.into_iter() + .map(|ctx| build_package(ctx, make_opts.clone())), + ) + .await; + normal_output!(); + let mut ctxs = Vec::new(); + for result in results { + match result { + Ok(ctx) => ctxs.push(ctx), + Err(e) => handle_build_error(e).await?, + } + } + + tracing::info!("Built {}", numeric!(ctxs.len(), "package"["s"])); + tracing::info!("Installing packages"); + + install_packages(ctxs, install_opts).await?; + + Ok(()) +} + +#[tracing::instrument(level = "trace")] +async fn build_package( + mut ctx: BuildContext, + make_opts: MakePkgBuilder, +) -> AppResult { + let pkg_name = &ctx.package.metadata.name; + let build_path = ctx.build_path()?; + let pb = spinner!("{}: Building Package", pkg_name.as_str().bold()); + + let mut child = make_opts + .directory(build_path) + .clean(true) + .no_deps(true) + .skip_pgp(true) + .needed(true) + .force(true) + .spawn()?; + + let stderr = child.stderr.take().unwrap(); + let stdout = child.stdout.take().unwrap(); + let handle = task::spawn({ + let pb = pb.clone(); + let pkg_name = pkg_name.clone(); + async move { show_and_log_stdio(stdout, stderr, pb, pkg_name).await } + }); + + let exit_status = child.wait().await?; + handle.abort(); + + if !exit_status.success() { + pb.finish_with_message(format!( + "{}: {}", + pkg_name.as_str().bold(), + "Build failed!".red(), + )); + return Err(AppError::BuildError { + pkg_name: pkg_name.to_owned(), + }); + } + + let mut packages = MakePkgBuilder::package_list(build_path).await?; + let match_version = ctx + .package + .metadata + .version + .rsplit_once('_') + .map(|v| v.0) + .unwrap_or(&ctx.package.metadata.version); + let match_name = format!("{pkg_name}-{match_version}"); + tracing::debug!("Match name {match_name}"); + packages.retain(|name| { + name.file_name() + .and_then(|n| n.to_str()) + .unwrap() + .starts_with(&match_name) + }); + tracing::debug!("Archives: {packages:?}"); + pb.finish_with_message(format!("{}: {}", pkg_name.clone().bold(), "Built!".green())); + ctx.step = BuildStep::Install(PackageArchives(packages)); + + Ok(ctx) +} + +#[tracing::instrument(level = "trace")] +async fn install_packages( + mut ctxs: Vec, + install_opts: PacmanInstallBuilder, +) -> AppResult> { + let mut packages = Vec::new(); + + for ctx in &mut ctxs { + packages.append(&mut ctx.packages()?.clone()); + ctx.step = BuildStep::Done; + } + + install_opts.files(packages).needed(false).install().await?; + + Ok(ctxs) +} + +#[tracing::instrument(level = "trace")] +async fn show_and_log_stdio( + stdout: ChildStdout, + stderr: ChildStderr, + pb: Arc, + package_name: String, +) -> AppResult<()> { + let mut reader = StdioReader::new(stdout, stderr); + let out_file = get_cache_dir().join(format!("{package_name}-build.log")); + let mut out_writer = BufWriter::new( + OpenOptions::new() + .create(true) + .write(true) + .open(out_file) + .await?, + ); + + while let Ok(line) = reader.read_line().await { + let _ = out_writer.write(line.as_bytes()).await?; + let _ = out_writer.write(&[b'\n']).await?; + tracing::trace!("{package_name}: {line}"); + let line = format!("{}: {}", package_name.clone().bold(), line); + let lines = wrap_text(line); + let line = lines.into_iter().next().unwrap(); + pb.set_message(line); + } + out_writer.flush().await?; + + Ok(()) +} + +#[tracing::instrument(level = "trace", skip_all)] +async fn handle_build_error>(err: E) -> AppResult<()> { + normal_output!(); + let err = err.into(); + + match err { + AppError::BuildError { pkg_name } => { + tracing::error!("Failed to build package {pkg_name}!"); + let log_path = get_cache_dir().join(format!("{pkg_name}-build.log")); + review_build_log(&log_path).await?; + + Ok(()) + } + e => Err(e), + } +} + +#[tracing::instrument(level = "trace")] +async fn review_build_log(log_file: &Path) -> AppResult<()> { + if prompt!(default yes, "Do you want to review the build log?") { + PagerBuilder::default().path(log_file).open().await?; + } + + Ok(()) +} diff --git a/src/operations/aur_install/make_dependency_removal.rs b/src/operations/aur_install/make_dependency_removal.rs new file mode 100644 index 0000000..cb92cbc --- /dev/null +++ b/src/operations/aur_install/make_dependency_removal.rs @@ -0,0 +1,35 @@ +use crate::{ + builder::pacman::PacmanUninstallBuilder, + internal::{dependencies::DependencyInformation, error::AppResult, structs::Options}, + prompt, +}; + +pub struct MakeDependencyRemoval { + pub options: Options, + pub dependencies: Vec, +} + +impl MakeDependencyRemoval { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn remove_make_deps(self) -> AppResult<()> { + let make_depends = self + .dependencies + .iter() + .flat_map(DependencyInformation::make_depends) + .collect::>(); + if !make_depends.is_empty() + && !self.options.noconfirm + && prompt!(default yes, "Do you want to remove the installed make dependencies?") + { + PacmanUninstallBuilder::default() + .packages(make_depends) + .no_confirm(true) + .uninstall() + .await?; + } + + tracing::info!("Done!"); + + Ok(()) + } +} diff --git a/src/operations/aur_install/mod.rs b/src/operations/aur_install/mod.rs new file mode 100644 index 0000000..eb12fa9 --- /dev/null +++ b/src/operations/aur_install/mod.rs @@ -0,0 +1,142 @@ +use aur_rpc::PackageInfo; + +use std::path::{Path, PathBuf}; + +use crate::internal::error::{AppError, AppResult}; + +use crate::internal::exit_code::AppExitCode; +use crate::{cancelled, crash, Options}; + +use self::aur_fetch::AurFetch; + +mod aur_dependency_installation; +mod aur_download; +mod aur_fetch; +mod aur_package_install; +mod aur_review; +mod common; +mod make_dependency_removal; +mod repo_dependency_installation; + +#[derive(Debug)] +pub struct BuildContext { + pub package: PackageInfo, + pub step: BuildStep, +} + +#[derive(Debug)] +pub enum BuildStep { + Download, + Build(BuildPath), + Install(PackageArchives), + Done, +} + +#[derive(Debug)] +pub struct BuildPath(pub PathBuf); + +#[derive(Debug)] +pub struct PackageArchives(pub Vec); + +impl From for BuildContext { + fn from(package: PackageInfo) -> Self { + Self { + package, + step: BuildStep::Download, + } + } +} + +impl From<&PackageInfo> for BuildContext { + fn from(p: &PackageInfo) -> Self { + Self::from(p.to_owned()) + } +} + +impl BuildContext { + pub fn build_path(&self) -> AppResult<&Path> { + if let BuildStep::Build(path) = &self.step { + Ok(&path.0) + } else { + Err(AppError::BuildStepViolation) + } + } + + pub fn packages(&self) -> AppResult<&Vec> { + if let BuildStep::Install(pkgs) = &self.step { + Ok(&pkgs.0) + } else { + Err(AppError::BuildStepViolation) + } + } +} + +pub struct AurInstall { + options: Options, + packages: Vec, +} + +impl AurInstall { + pub fn new(options: Options, packages: Vec) -> Self { + Self { options, packages } + } + + pub fn start(self) -> AurFetch { + tracing::debug!("Installing from AUR: {:?}", &self.packages); + AurFetch { + options: self.options, + packages: self.packages, + } + } +} + +/// Installs a given list of packages from the aur +#[tracing::instrument(level = "trace")] +pub async fn aur_install(packages: Vec, options: Options) { + if let Err(e) = aur_install_internal(AurInstall::new(options, packages)).await { + match e { + AppError::Rpc(e) => { + crash!(AppExitCode::RpcError, "AUR RPC Call failed with {e}") + } + AppError::BuildStepViolation => { + crash!(AppExitCode::MakePkgError, "Failed to build") + } + AppError::BuildError { pkg_name } => { + crash!(AppExitCode::MakePkgError, "Failed to build {pkg_name}") + } + AppError::UserCancellation => { + cancelled!(); + } + AppError::MissingDependencies(deps) => { + crash!( + AppExitCode::MissingDeps, + "Missing dependencies {}", + deps.join(", ") + ) + } + AppError::MakePkg(msg) => { + crash!(AppExitCode::MakePkgError, "makepgk failed {msg}") + } + _ => crash!(AppExitCode::Other, "Unknown error"), + } + } +} + +async fn aur_install_internal(install: AurInstall) -> AppResult<()> { + install + .start() + .fetch_package_info() + .await? + .download_sources() + .await? + .review_pkgbuild() + .await? + .install_repo_dependencies() + .await? + .install_aur_dependencies() + .await? + .install_packages() + .await? + .remove_make_deps() + .await +} diff --git a/src/operations/aur_install/repo_dependency_installation.rs b/src/operations/aur_install/repo_dependency_installation.rs new file mode 100644 index 0000000..99780b3 --- /dev/null +++ b/src/operations/aur_install/repo_dependency_installation.rs @@ -0,0 +1,40 @@ +use std::collections::HashSet; + +use crate::{ + builder::pacman::PacmanInstallBuilder, + internal::{dependencies::DependencyInformation, error::AppResult, structs::Options}, +}; + +use super::{aur_dependency_installation::AurDependencyInstallation, BuildContext}; + +pub struct RepoDependencyInstallation { + pub options: Options, + pub dependencies: Vec, + pub contexts: Vec, +} + +impl RepoDependencyInstallation { + #[tracing::instrument(level = "trace", skip_all)] + pub async fn install_repo_dependencies(self) -> AppResult { + let repo_dependencies: HashSet<&str> = self + .dependencies + .iter() + .flat_map(DependencyInformation::all_repo_depends) + .collect(); + + if !repo_dependencies.is_empty() { + tracing::info!("Installing repo dependencies"); + PacmanInstallBuilder::default() + .as_deps(true) + .packages(repo_dependencies) + .no_confirm(self.options.noconfirm) + .install() + .await?; + } + Ok(AurDependencyInstallation { + options: self.options, + dependencies: self.dependencies, + contexts: self.contexts, + }) + } +} diff --git a/src/operations/clean.rs b/src/operations/clean.rs index 982d5bd..0ee0744 100644 --- a/src/operations/clean.rs +++ b/src/operations/clean.rs @@ -1,38 +1,40 @@ -use std::process::Command; +use tokio::process::Command; use crate::crash; -use crate::info; use crate::internal::commands::ShellCommand; + use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; -use crate::log; + +use crate::internal::utils::get_cache_dir; use crate::prompt; use crate::Options; -/// Help the user in clearing orphaned packages and pacman cache. -pub fn clean(options: Options) { - let verbosity = options.verbosity; +/// Removes orphaned packages and cache +#[tracing::instrument(level = "trace")] +pub async fn clean(options: Options) { let noconfirm = options.noconfirm; // Check for orphaned packages let orphaned_packages = ShellCommand::pacman() .arg("-Qdtq") .wait_with_output() + .await .silent_unwrap(AppExitCode::PacmanError); if orphaned_packages.stdout.as_str().is_empty() { // If no orphaned packages found, do nothing - info!("No orphaned packages found"); + tracing::info!("No orphaned packages found"); } else { // Prompt users whether to remove orphaned packages - info!( + tracing::info!( "Removing orphans would uninstall the following packages: \n{}", &orphaned_packages.stdout ); - let cont = prompt!(default false, "Continue?"); + let cont = prompt!(default no, "Continue?"); if !cont { // If user doesn't want to continue, break - info!("Exiting"); + tracing::info!("Exiting"); std::process::exit(AppExitCode::PacmanError as i32); } @@ -50,20 +52,19 @@ pub fn clean(options: Options) { } } - if verbosity >= 1 { - log!("Removing orphans: {:?}", orphaned_packages_vec); - } + tracing::debug!("Removing orphans: {:?}", orphaned_packages_vec); // Remove orphaned packages let pacman_result = ShellCommand::pacman() .elevated() .args(pacman_args) .wait() + .await .silent_unwrap(AppExitCode::PacmanError); if pacman_result.success() { // If pacman succeeded, notify user - info!("Successfully removed orphans"); + tracing::info!("Successfully removed orphans"); } else { // If pacman failed, crash crash!(AppExitCode::PacmanError, "Failed to remove orphans",); @@ -71,15 +72,16 @@ pub fn clean(options: Options) { } // Prompt the user whether to clear the Amethyst cache - let clear_ame_cache = prompt!(default false, "Clear Amethyst's internal PKGBUILD cache?"); + let clear_ame_cache = prompt!(default no, "Clear Amethyst's internal PKGBUILD cache?"); if clear_ame_cache { - // Remove ~/.cache/ame - Command::new("rm") - .arg("-rf") - .arg("~/.cache/ame") - .spawn() - .unwrap() - .wait() + let cache_dir = get_cache_dir(); + ShellCommand::rm() + .arg(cache_dir) + .arg("-r") + .arg("-f") + .elevated() + .wait_success() + .await .unwrap(); } @@ -87,7 +89,7 @@ pub fn clean(options: Options) { let clear_pacman_cache = if noconfirm { true } else { - prompt!(default false, "Also clear pacman's package cache?") + prompt!(default no, "Also clear pacman's package cache?") }; if clear_pacman_cache { @@ -103,9 +105,7 @@ pub fn clean(options: Options) { paccache_args.push("--noconfirm"); } - if verbosity >= 1 { - log!("Clearing using `paccache -r`"); - } + tracing::debug!("Clearing using `paccache -r`"); // Clear pacman's cache (keeping latest 3 versions of installed packages) Command::new("sudo") @@ -120,22 +120,22 @@ pub fn clean(options: Options) { ) }) .wait() + .await .unwrap(); - if verbosity >= 1 { - log!("Clearing using `pacman -Sc`"); - } + tracing::debug!("Clearing using `pacman -Sc`"); // Clear pacman's cache (keeping only installed packages) let pacman_result = ShellCommand::pacman() .elevated() .args(pacman_args) .wait() + .await .silent_unwrap(AppExitCode::PacmanError); if pacman_result.success() { // If pacman succeeded, notify user - info!("Successfully cleared package cache"); + tracing::info!("Successfully cleared package cache"); } else { // If pacman failed, crash crash!(AppExitCode::PacmanError, "Failed to clear package cache",); diff --git a/src/operations/install.rs b/src/operations/install.rs index 2873e33..2e21039 100644 --- a/src/operations/install.rs +++ b/src/operations/install.rs @@ -1,36 +1,20 @@ -use crate::internal::commands::ShellCommand; -use crate::internal::error::SilentUnwrap; +use crate::builder::pacman::PacmanInstallBuilder; use crate::internal::exit_code::AppExitCode; -use crate::{crash, info, log, Options}; +use crate::{crash, Options}; -/// Help the user install a package from the pacman repos -pub fn install(packages: &[String], options: Options) { - info!("Installing packages {} from repos", &packages.join(", ")); - - // Build pacman args - let mut opers = vec!["-S", "--needed"]; - if options.noconfirm { - opers.push("--noconfirm"); - } - if options.asdeps { - opers.push("--asdeps"); - } - let verbosity = options.verbosity; +#[tracing::instrument(level = "trace")] +pub async fn install(packages: Vec, options: Options) { + tracing::info!("Installing packages {} from repos", &packages.join(", ")); if !packages.is_empty() { - if verbosity >= 1 { - log!("Installing from repos: {:?}", &packages); - } + tracing::debug!("Installing from repos: {:?}", &packages); + + let result = PacmanInstallBuilder::from_options(options) + .packages(packages.clone()) + .install() + .await; - // Install packages - let status = ShellCommand::pacman() - .elevated() - .args(opers) - .args(packages) - .wait() - .silent_unwrap(AppExitCode::PacmanError); - if !status.success() { - // If pacman failed, crash + if result.is_err() { crash!( AppExitCode::PacmanError, "An error occured while installing packages: {}, aborting", @@ -38,8 +22,6 @@ pub fn install(packages: &[String], options: Options) { ); } - if verbosity >= 1 { - log!("Installing packages: {:?} was successful", &packages); - } + tracing::debug!("Installing packages: {:?} was successful", &packages); } } diff --git a/src/operations/mod.rs b/src/operations/mod.rs index ef348bb..0843999 100644 --- a/src/operations/mod.rs +++ b/src/operations/mod.rs @@ -1,7 +1,7 @@ pub use aur_install::*; pub use clean::*; pub use install::*; -pub use search::{aur_search, repo_search as search}; +pub use search::{aur_search, repo_search as search, SearchBy}; pub use uninstall::*; pub use upgrade::*; diff --git a/src/operations/search.rs b/src/operations/search.rs index d1f9dee..4165533 100644 --- a/src/operations/search.rs +++ b/src/operations/search.rs @@ -1,146 +1,97 @@ -use chrono::{Local, TimeZone}; -use colored::Colorize; -use textwrap::wrap; +use std::str::FromStr; use crate::internal::commands::ShellCommand; use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; use crate::internal::rpc::rpcsearch; -use crate::{log, Options}; - -#[allow(clippy::module_name_repetitions)] -/// Searches for packages from the AUR and returns wrapped results -pub fn aur_search(query: &str, options: Options) -> String { - // Query AUR for package info - let res = rpcsearch(query); - - // Get verbosity - let verbosity = options.verbosity; - - // Format output - let mut results_vec = vec![]; - for package in &res.results { - // Define wrapping options - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) - .subsequent_indent(" "); - - let result = format!( - "{}{} {} {}\n {}", - "aur/".cyan().bold(), - package.name.bold(), - package.version.green().bold(), - if package.out_of_date.is_some() { - format!( - "[out of date: since {}]", - Local - .timestamp(package.out_of_date.unwrap().try_into().unwrap(), 0) - .date_naive() - ) - .red() - .bold() - } else { - "".bold() - }, - wrap( - package - .description - .as_ref() - .unwrap_or(&"No description".to_string()), - opts, - ) - .join("\n"), - ); - results_vec.push(result); +use crate::Options; +use aur_rpc::SearchField; + +#[tracing::instrument(level = "trace")] +pub async fn aur_search(query: &str, by_field: Option, options: Options) { + let packages = rpcsearch(query.to_string(), by_field.map(SearchBy::into)) + .await + .silent_unwrap(AppExitCode::RpcError); + let total_results = packages.len(); + + for package in &packages { + println!( + "aur/{} {}\n {}", + package.name, package.version, package.description + ) } - if verbosity > 1 { - log!( - "Found {} results for \"{}\" in the AUR", - res.results.len(), - query - ); - } - - results_vec.join("\n") -} - -struct SearchResult { - repo: String, - name: String, - version: String, - description: String, + tracing::debug!("Found {total_results} resuls for \"{query}\" in AUR",); } -#[allow(clippy::module_name_repetitions)] -/// Searches for packages from the repos and returns wrapped results -pub fn repo_search(query: &str, options: Options) -> String { - // Initialise variables - let verbosity = options.verbosity; - - // Query pacman for package info - let output = ShellCommand::bash() - .args(&["-c", &format!("expac -Ss '%r\\\\%n\\\\%v\\\\%d' {}", query)]) +#[tracing::instrument(level = "trace")] +pub async fn repo_search(query: &str, options: Options) { + let output = ShellCommand::pacman() + .arg("-Ss") .arg(query) .wait_with_output() + .await .silent_unwrap(AppExitCode::PacmanError) .stdout; - // Split output into lines - let lines = output.trim().split('\n'); + tracing::debug!( + "Found {} results for \"{}\" in repos", + &output.split('\n').count() / 2, + &query + ); - // Initialise results vector - let mut results_vec: Vec = vec![]; + println!("{}", output) +} - let clone = lines.clone().collect::>(); - if clone.len() == 1 && clone[0].is_empty() { - // If no results, return empty string - return "".to_string(); - } +/// Represents a field to search by +#[derive(Debug, Clone, Copy)] +pub enum SearchBy { + /// Searches by name + Name, + /// Searches name and description + NameDesc, + /// Searches by package maintainer + Maintainer, + /// Searches for packages that depend on the given keywods + Depends, + /// Searches for packages that require the given keywords to be build + MakeDepends, + /// Searches for packages that optionally depend on the given keywods + OptDepends, + /// Searches for packages that require the given keywods to be present + CheckDepends, +} - // Iterate over lines - for line in lines { - let parts: Vec<&str> = line.split('\\').collect(); - let res = SearchResult { - repo: parts[0].to_string(), - name: parts[1].to_string(), - version: parts[2].to_string(), - description: parts[3].to_string(), +impl FromStr for SearchBy { + type Err = String; + + fn from_str(s: &str) -> Result { + let arg = match s { + "name" => Self::Name, + "name-desc" => Self::NameDesc, + "maintainer" => Self::Maintainer, + "depends" => Self::Depends, + "makedepends" | "make-depends" => Self::MakeDepends, + "optdepends" | "opt-depends" => Self::OptDepends, + "checkdepends" | "check-depends" => Self::CheckDepends, + directive => return Err(format!("Invalid search by directive '{directive}'")), }; - results_vec.push(res); - } - if verbosity >= 1 { - log!( - "Found {} results for \"{}\" in repos", - &results_vec.len(), - &query - ); + Ok(arg) } +} - // Format output - let results_vec = results_vec - .into_iter() - .map(|res| { - let opts = textwrap::Options::new(crossterm::terminal::size().unwrap().0 as usize - 4) - .subsequent_indent(" "); - format!( - "{}{}{} {}\n {}", - res.repo.purple().bold(), - "/".purple().bold(), - res.name.bold(), - res.version.green().bold(), - if res.description.is_empty() { - "No description".to_string() - } else { - wrap(&res.description, opts).join("\n") - }, - ) - }) - .collect::>(); - - if output.trim().is_empty() { - "".to_string() - } else { - results_vec.join("\n") +#[allow(clippy::from_over_into)] +impl Into for SearchBy { + fn into(self) -> SearchField { + match self { + SearchBy::Name => SearchField::Name, + SearchBy::NameDesc => SearchField::NameDesc, + SearchBy::Maintainer => SearchField::Maintainer, + SearchBy::Depends => SearchField::Depends, + SearchBy::MakeDepends => SearchField::MakeDepends, + SearchBy::OptDepends => SearchField::OptDepends, + SearchBy::CheckDepends => SearchField::CheckDepends, + } } } diff --git a/src/operations/uninstall.rs b/src/operations/uninstall.rs index 7abe2bd..fe89e3d 100644 --- a/src/operations/uninstall.rs +++ b/src/operations/uninstall.rs @@ -1,29 +1,47 @@ +use std::env; +use std::path::Path; +use tokio::fs; + use crate::internal::commands::ShellCommand; use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; -use crate::{log, Options}; +use crate::Options; -/// Helps the user in uninstalling installed packages. -pub fn uninstall(packages: &[String], options: Options) { - // Build pacman args +#[tracing::instrument(level = "trace")] +pub async fn uninstall(packages: Vec, options: Options) { let mut pacman_args = vec!["-Rs"]; - pacman_args.append(&mut packages.iter().map(String::as_str).collect()); + pacman_args.append(&mut packages.iter().map(|s| s.as_str()).collect()); + if options.noconfirm { pacman_args.push("--noconfirm"); } - let verbosity = options.verbosity; - if verbosity >= 1 { - log!("Uninstalling: {:?}", &packages); - } + tracing::debug!("Uninstalling: {:?}", &packages); - // Uninstall packages ShellCommand::pacman() .elevated() .args(pacman_args) .wait_success() + .await .silent_unwrap(AppExitCode::PacmanError); - if verbosity >= 1 { - log!("Uninstalling packages: {:?} exited with code 0", &packages); + tracing::debug!("Uninstalling packages: {:?} exited with code 0", &packages); + + for package in packages { + if Path::new(&format!( + "{}/.cache/ame/{}", + env::var("HOME").unwrap(), + package + )) + .exists() + { + tracing::debug!("Old cache directory found, deleting"); + fs::remove_dir_all(Path::new(&format!( + "{}/.cache/ame/{}", + env::var("HOME").unwrap(), + package + ))) + .await + .unwrap(); + } } } diff --git a/src/operations/upgrade.rs b/src/operations/upgrade.rs index 8b05d5c..1dae44c 100644 --- a/src/operations/upgrade.rs +++ b/src/operations/upgrade.rs @@ -1,152 +1,93 @@ use crate::args::UpgradeArgs; +use crate::builder::pacman::{PacmanColor, PacmanQueryBuilder}; use crate::internal::commands::ShellCommand; -use crate::internal::detect; use crate::internal::error::SilentUnwrap; use crate::internal::exit_code::AppExitCode; use crate::internal::rpc::rpcinfo; use crate::operations::aur_install::aur_install; -use crate::{info, log, prompt, spinner, warn, Options}; +use crate::{prompt, Options}; -#[derive(Debug)] -struct QueriedPackage { - pub name: String, - pub version: String, +/// Upgrades all installed packages +#[tracing::instrument(level = "trace")] +pub async fn upgrade(args: UpgradeArgs, options: Options) { + if args.repo { + upgrade_repo(options).await; + } + if args.aur { + upgrade_aur(options).await; + } + if !args.aur && !args.repo { + upgrade_repo(options).await; + upgrade_aur(options).await; + } } -/// Helps the user upgrade installed packages, repo and AUR. -pub fn upgrade(options: Options, args: UpgradeArgs, cachedir: &str) { - // Initialise variables - let verbosity = options.verbosity; +#[tracing::instrument(level = "trace")] +async fn upgrade_repo(options: Options) { let noconfirm = options.noconfirm; - let args = if !args.aur && !args.repo { - UpgradeArgs { - aur: true, - repo: true, - } - } else { - args - }; - - if args.repo { - // Build pacman args - let mut pacman_args = vec!["-Syu"]; - if noconfirm { - pacman_args.push("--noconfirm"); - } - - if verbosity >= 1 { - log!("Upgrading repo packages"); - } + let mut pacman_args = vec!["-Syu"]; + if noconfirm { + pacman_args.push("--noconfirm"); + } - // Upgrade repo packages - let pacman_result = ShellCommand::pacman() - .elevated() - .args(pacman_args) - .wait() - .silent_unwrap(AppExitCode::PacmanError); + tracing::debug!("Upgrading repo packages"); - if pacman_result.success() { - // If pacman was successful, notify user - info!("Successfully upgraded repo packages"); - } else { - // Otherwise warn user - warn!("Failed to upgrade repo packages.",); - } - } + let pacman_result = ShellCommand::pacman() + .elevated() + .args(pacman_args) + .wait() + .await + .silent_unwrap(AppExitCode::PacmanError); - if args.repo && args.aur { - let cont = prompt!(default true, "Continue to upgrade AUR packages?"); - if !cont { - // If user doesn't want to continue, break - info!("Exiting"); + if pacman_result.success() { + tracing::info!("Successfully upgraded repo packages"); + } else { + let continue_upgrading = prompt!(default no, + "Failed to upgrade repo packages, continue to upgrading AUR packages?", + ); + if !continue_upgrading { + tracing::info!("Exiting"); std::process::exit(AppExitCode::PacmanError as i32); } } +} - if args.aur { - if verbosity >= 1 { - log!("Checking AUR upgrades..."); - } - - // Start spinner - let sp = spinner!("Checking AUR upgrades..."); - - // List non-native packages using `pacman -Qm` and collect to a Vec - let non_native = ShellCommand::pacman() - .arg("-Qm") - .args(&["--color", "never"]) - .wait_with_output() - .silent_unwrap(AppExitCode::PacmanError); - - // Collect by lines to a Vec - let mut non_native = non_native.stdout.split('\n').collect::>(); - - // Remove last element, which is an empty line - non_native.pop(); - - // Parse non-native packages into a Vec - let mut parsed_non_native: Vec = vec![]; - for pkg in non_native { - // Split by space - let split = pkg.split(' ').collect::>(); - if verbosity >= 1 { - log!("{:?}", split); - } - // Create QueriedPackage and push it to parsed_non_native - let name = split[0].to_string(); - let version = split[1].to_string(); - parsed_non_native.push(QueriedPackage { name, version }); - } - - if verbosity >= 1 { - log!("{:?}", &parsed_non_native); - } - - // Check if AUR package versions are the same as installed - let mut aur_upgrades = vec![]; - for pkg in parsed_non_native { - // Query AUR - let rpc_result = rpcinfo(&pkg.name); - - if !rpc_result.found { - // If package not found, skip - continue; - } - - // Run `vercmp` to compare versions - let vercmp_result = std::process::Command::new("vercmp") - .arg(&pkg.version) - .arg(&rpc_result.package.unwrap().version) - .output() - .unwrap(); - let vercmp_result = String::from_utf8(vercmp_result.stdout).unwrap(); - if verbosity >= 1 { - log!("Vercmp returned {:?}", vercmp_result); - } - - // If versions differ, push to a vector - if vercmp_result.trim() == "-1" { +#[tracing::instrument(level = "trace")] +async fn upgrade_aur(options: Options) { + tracing::debug!("Upgrading AUR packages"); + + let non_native_pkgs = PacmanQueryBuilder::foreign() + .color(PacmanColor::Never) + .query_with_output() + .await + .silent_unwrap(AppExitCode::PacmanError); + + tracing::debug!("aur packages: {non_native_pkgs:?}"); + let mut aur_upgrades = vec![]; + + for pkg in non_native_pkgs { + let remote_package = rpcinfo(&pkg.name) + .await + .silent_unwrap(AppExitCode::RpcError); + + if let Some(remote_package) = remote_package { + if remote_package.metadata.version != pkg.version { + tracing::debug!( + "local version: {}, remote version: {}", + pkg.version, + remote_package.metadata.version + ); aur_upgrades.push(pkg.name); } - } - - sp.stop_bold("Finished!"); - - // If vector isn't empty, prompt to install AUR packages from vector, effectively upgrading - if aur_upgrades.is_empty() { - info!("No upgrades available for installed AUR packages"); } else { - let cont = prompt!(default true, - "AUR packages {} have new versions available, upgrade?", - aur_upgrades.join(", "), - ); - if cont { - aur_install(aur_upgrades, options, cachedir); - }; + tracing::warn!("Could not find the remote package for {}", pkg.name); } } - // Check for .pacnew files - detect(); + if !aur_upgrades.is_empty() { + aur_install(aur_upgrades, options).await; + } else { + tracing::info!("No upgrades available for installed AUR packages"); + } }