diff --git a/.github/workflows/check-and-lint.yaml b/.github/workflows/check-and-lint.yaml index 6f2155a9..de30d19d 100644 --- a/.github/workflows/check-and-lint.yaml +++ b/.github/workflows/check-and-lint.yaml @@ -4,66 +4,88 @@ on: branches: - main +name: CI -name: Check and Lint +env: + RUST_VER: '1.60.0' + CROSS_VER: '0.2.4' + CARGO_NET_RETRY: 3 jobs: - check: - name: Check - strategy: - matrix: - platform: [ ubuntu-latest, macos-latest, windows-latest ] - runs-on: ${{ matrix.platform }} - steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - uses: actions-rs/cargo@v1 - with: - command: check - fmt: name: Rustfmt - strategy: - matrix: - platform: [ ubuntu-latest, macos-latest, windows-latest ] - runs-on: ${{ matrix.platform }} + runs-on: ubuntu-20.04 steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 - with: - profile: minimal - toolchain: stable - override: true - - run: rustup component add rustfmt - - uses: actions-rs/cargo@v1 - with: - command: fmt - args: --all -- --check + - name: Checkout code + uses: actions/checkout@v3 - clippy: - name: Clippy + - name: Setup Rust + uses: dtolnay/rust-toolchain@master + with: + toolchain: '${{ env.RUST_VER }}' + targets: ${{ matrix.target }} + components: clippy, rustfmt + + - name: Run cargo fmt + run: | + cargo fmt --all -- --check + + main: + needs: fmt + name: ${{ matrix.target_name }} (check, clippy) + runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: - platform: [ ubuntu-latest, macos-latest, windows-latest ] - runs-on: ${{ matrix.platform }} + include: + - target: x86_64-linux-android + target_name: Android + use_cross: true + os: ubuntu-20.04 + + - target: x86_64-unknown-freebsd + target_name: FreeBSD + use_cross: true + os: ubuntu-20.04 + + - target: x86_64-unknown-linux-gnu + target_name: Linux + os: ubuntu-20.04 + + - target: x86_64-apple-darwin + target_name: macOS + os: macos-11 + + - target: x86_64-unknown-netbsd + target_name: NetBSD + use_cross: true + os: ubuntu-20.04 + + - target: x86_64-pc-windows-msvc + target_name: Windows + os: windows-2019 steps: - - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - name: Checkout code + uses: actions/checkout@v3 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@master with: - toolchain: stable - components: clippy - override: true - - uses: actions-rs/cargo@v1.0.1 + toolchain: '${{ env.RUST_VER }}' + targets: ${{ matrix.target }} + components: clippy, rustfmt + + - name: Setup Rust Cache + uses: Swatinem/rust-cache@v2 with: - command: clippy - args: --all-targets --locked -- -D warnings - name: Clippy Output - - uses: actions-rs/cargo@v1.0.1 - with: - command: clippy - args: --all-targets --locked --all-features -- -D warnings - name: Clippy (All features) Output + prefix-key: ${{ matrix.target }} + + - name: Setup cross + if: matrix.use_cross == true + run: curl -fL --retry 3 https://github.com/cross-rs/cross/releases/download/v${{ env.CROSS_VER }}/cross-x86_64-unknown-linux-musl.tar.gz | tar vxz -C /usr/local/bin + + - name: Run cargo check + run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} check --locked --target ${{ matrix.target }} + + - name: Run cargo clippy + run: ${{ matrix.use_cross == true && 'cross' || 'cargo' }} clippy --locked --target ${{ matrix.target }} --all-features -- -D warnings diff --git a/Cargo.lock b/Cargo.lock index 0aa46ae7..81de8e5e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 3 +[[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" @@ -21,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "0.7.19" +version = "0.7.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4f55bd91a0978cbfd91c457a164bab8b4001c833b7f323132c0a4e1922dd44e" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" dependencies = [ "memchr", ] @@ -37,12 +46,6 @@ dependencies = [ "libc", ] -[[package]] -name = "anyhow" -version = "1.0.66" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" - [[package]] name = "async-broadcast" version = "0.4.1" @@ -60,22 +63,22 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e14485364214912d3b19cc3435dde4df66065127f05fa0d75c712f36f12c2f28" dependencies = [ - "concurrent-queue", + "concurrent-queue 1.2.4", "event-listener", "futures-core", ] [[package]] name = "async-executor" -version = "1.4.1" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "871f9bb5e0a22eeb7e8cf16641feb87c9dc67032ccf8ff49e772eb9941d3a965" +checksum = "17adb73da160dfb475c183343c8cccd80721ea5a605d3eb57125f0a7b7a92d0b" dependencies = [ + "async-lock", "async-task", - "concurrent-queue", + "concurrent-queue 2.0.0", "fastrand", "futures-lite", - "once_cell", "slab", ] @@ -87,7 +90,7 @@ checksum = "e8121296a9f05be7f34aa4196b1747243b3b62e048bb7906f644f3fbfc490cf7" dependencies = [ "async-lock", "autocfg", - "concurrent-queue", + "concurrent-queue 1.2.4", "futures-lite", "libc", "log", @@ -154,6 +157,21 @@ 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.1" @@ -186,9 +204,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.2.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" +checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" [[package]] name = "cache-padded" @@ -198,9 +216,9 @@ checksum = "c1db59621ec70f09c5e9b597b220c7a2b43611f4710dc03ceb8748637775692c" [[package]] name = "cc" -version = "1.0.74" +version = "1.0.77" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" +checksum = "e9f73505338f7d905b19d18738976aae232eb46b8efc15554ffc56deb5d9ebe4" [[package]] name = "cfg-if" @@ -210,9 +228,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", @@ -272,6 +290,33 @@ dependencies = [ "unicode-width", ] +[[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", +] + +[[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 = "concurrent-queue" version = "1.2.4" @@ -281,6 +326,15 @@ dependencies = [ "cache-padded", ] +[[package]] +name = "concurrent-queue" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7bef69dc86e3c610e4e7aed41035e2a7ed12e72dd7530f61327a6579a4390b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "console" version = "0.15.2" @@ -312,18 +366,18 @@ dependencies = [ [[package]] name = "crossbeam-utils" -version = "0.8.12" +version = "0.8.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edbafec5fa1f196ca66527c1b12c2ec4745ca14b50f1ad8f9f6f720b55d11fac" +checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" dependencies = [ "cfg-if", ] [[package]] name = "cxx" -version = "1.0.80" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" +checksum = "d4a41a86530d0fe7f5d9ea779916b7cadd2d4f9add748b99c2c029cbbdfaf453" dependencies = [ "cc", "cxxbridge-flags", @@ -333,9 +387,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.80" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" +checksum = "06416d667ff3e3ad2df1cd8cd8afae5da26cf9cec4d0825040f88b5ca659a2f0" dependencies = [ "cc", "codespan-reporting", @@ -348,15 +402,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.80" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" +checksum = "820a9a2af1669deeef27cb271f476ffd196a2c4b6731336011e0ba63e2c7cf71" [[package]] name = "cxxbridge-macro" -version = "1.0.80" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" +checksum = "a08a6e2fcc370a089ad3b4aaf54db3b1b4cee38ddabce5896b33eb693275f470" dependencies = [ "proc-macro2", "quote", @@ -472,25 +526,22 @@ dependencies = [ "syn", ] -[[package]] -name = "env_logger" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" -dependencies = [ - "atty", - "humantime", - "log", - "regex", - "termcolor", -] - [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[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" @@ -652,6 +703,12 @@ dependencies = [ "wasi 0.11.0+wasi-snapshot-preview1", ] +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" + [[package]] name = "glob" version = "0.3.0" @@ -759,20 +816,11 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" -[[package]] -name = "humantime" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" -dependencies = [ - "quick-error", -] - [[package]] name = "hyper" -version = "0.14.22" +version = "0.14.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "abfba89e19b959ca163c7752ba59d737c1ceea53a5d31a149c805446fc958064" +checksum = "034711faac9d2166cb1baf1a2fb0b60b1f277f8492fd72176c17f3515e1abd3c" dependencies = [ "bytes", "futures-channel", @@ -794,9 +842,9 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.23.0" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d87c48c02e0dc5e3b849a2041db3029fd066650f8f717c07bf8ed78ccb895cac" +checksum = "59df7c4e19c950e6e0e868dcc0a300b09a9b88e9ec55bd879ca819087a77355d" dependencies = [ "http", "hyper", @@ -840,10 +888,16 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.9.1" +name = "indenter" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10a35a97730320ffe8e2d410b5d3b69279b98d2c14bdb8b70ea89ecf7888d41e" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" dependencies = [ "autocfg", "hashbrown", @@ -872,9 +926,9 @@ dependencies = [ [[package]] name = "ipnet" -version = "2.5.0" +version = "2.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +checksum = "f88c5561171189e69df9d98bcf18fd5f9558300f7ea7b801eb8a0fd748bd8745" [[package]] name = "itoa" @@ -941,7 +995,7 @@ dependencies = [ "dirs-next", "objc-foundation", "objc_id", - "time 0.3.16", + "time 0.3.17", ] [[package]] @@ -953,6 +1007,15 @@ dependencies = [ "libc", ] +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata", +] + [[package]] name = "memchr" version = "2.5.0" @@ -1053,6 +1116,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1074,23 +1147,14 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "number_prefix" version = "0.4.0" @@ -1126,6 +1190,15 @@ dependencies = [ "objc", ] +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" version = "1.16.0" @@ -1154,9 +1227,21 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.3.1" +version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" +checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "owo-colors" +version = "3.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f" [[package]] name = "parking" @@ -1234,19 +1319,9 @@ dependencies = [ [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "pretty_env_logger" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" -dependencies = [ - "env_logger", - "log", -] +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-crate" @@ -1292,12 +1367,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "quick-error" -version = "1.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" - [[package]] name = "quick-xml" version = "0.22.0" @@ -1387,10 +1456,19 @@ dependencies = [ ] [[package]] -name = "regex-syntax" -version = "0.6.27" +name = "regex-automata" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "remove_dir_all" @@ -1403,9 +1481,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.12" +version = "0.11.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "431949c384f4e2ae07605ccaa56d1d9d2ecdb5cadd4f9577ccfab29f2e5149fc" +checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c" dependencies = [ "base64", "bytes", @@ -1465,6 +1543,12 @@ dependencies = [ "ordered-multimap", ] +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + [[package]] name = "rustls" version = "0.20.7" @@ -1578,9 +1662,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.87" +version = "1.0.89" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" +checksum = "020ff22c755c2ed3f8cf162dbb41a7268d934702f3ed3631656ea597e08fc3db" dependencies = [ "itoa", "ryu", @@ -1625,6 +1709,15 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" +[[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 = "shell-words" version = "1.1.0" @@ -1746,16 +1839,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "sys-info" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" -dependencies = [ - "cc", - "libc", -] - [[package]] name = "tar" version = "0.4.38" @@ -1813,9 +1896,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.1" +version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d" [[package]] name = "thiserror" @@ -1837,6 +1920,15 @@ dependencies = [ "syn", ] +[[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 = "time" version = "0.1.44" @@ -1850,13 +1942,11 @@ dependencies = [ [[package]] name = "time" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fab5c8b9980850e06d92ddbe3ab839c062c801f3927c0fb8abd6fc8e918fbca" +checksum = "a561bf4617eebd33bca6434b988f39ed798e527f51a1e797d0ee4f61c0a38376" dependencies = [ "itoa", - "libc", - "num_threads", "serde", "time-core", "time-macros", @@ -1870,9 +1960,9 @@ checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" [[package]] name = "time-macros" -version = "0.2.5" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bb801831d812c562ae7d2bfb531f26e66e4e1f6b17307ba4149c5064710e5b" +checksum = "d967f99f534ca7e495c575c62638eebc2898a8c84c119b89e250477bc4ba16b2" dependencies = [ "time-core", ] @@ -1946,23 +2036,22 @@ dependencies = [ [[package]] name = "topgrade" -version = "10.1.2" +version = "10.2.0" dependencies = [ - "anyhow", "cfg-if", "chrono", "clap", + "color-eyre", "console", "directories", "futures", "glob", "home", "lazy_static", - "log", + "libc", "nix 0.24.2", "notify-rust", "parselnk", - "pretty_env_logger", "regex", "rust-ini", "self_update", @@ -1971,11 +2060,12 @@ dependencies = [ "shell-words", "shellexpand", "strum 0.24.1", - "sys-info", "tempfile", "thiserror", "tokio", "toml", + "tracing", + "tracing-subscriber", "walkdir", "which", "winapi", @@ -1994,6 +2084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" dependencies = [ "cfg-if", + "log", "pin-project-lite", "tracing-attributes", "tracing-core", @@ -2017,6 +2108,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" 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.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "time 0.3.17", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -2085,6 +2217,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 = "version_check" version = "0.9.4" @@ -2454,9 +2592,9 @@ dependencies = [ [[package]] name = "zbus_names" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a408fd8a352695690f53906dc7fd036be924ec51ea5e05666ff42685ed0af5" +checksum = "d69bb79b44e1901ed8b217e485d0f01991aec574479b68cb03415f142bc7ae67" dependencies = [ "serde", "static_assertions", @@ -2473,14 +2611,14 @@ dependencies = [ "crc32fast", "crossbeam-utils", "flate2", - "time 0.3.16", + "time 0.3.17", ] [[package]] name = "zvariant" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b794fb7f59af4105697b0449ba31731ee5dbb3e773a17dbdf3d36206ea1b1644" +checksum = "5c817f416f05fcbc833902f1e6064b72b1778573978cfeac54731451ccc9e207" dependencies = [ "byteorder", "enumflags2", @@ -2492,9 +2630,9 @@ dependencies = [ [[package]] name = "zvariant_derive" -version = "3.7.1" +version = "3.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd58d4b6c8e26d3dd2149c8c40c6613ef6451b9885ff1296d1ac86c388351a54" +checksum = "fdd24fffd02794a76eb10109de463444064c88f5adb9e9d1a78488adc332bfef" dependencies = [ "proc-macro-crate", "proc-macro2", diff --git a/Cargo.toml b/Cargo.toml index 63c321ce..7ae19233 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,9 +3,10 @@ name = "topgrade" description = "Upgrade all the things" categories = ["os"] keywords = ["upgrade", "update"] -license-file = "LICENSE" +license = "GPL-3.0" +# license-file = "LICENSE" repository = "https://github.com/topgrade-rs/topgrade" -version = "10.1.2" +version = "10.2.0" authors = ["Roey Darwish Dror ", "Thomas Schönauer "] exclude = ["doc/screenshot.gif"] edition = "2021" @@ -27,29 +28,38 @@ toml = "0.5" which_crate = { version = "~4.1", package = "which" } shellexpand = "~2.1" clap = { version = "~3.1", features = ["cargo", "derive"] } -log = "~0.4" walkdir = "~2.3" console = "~0.15" lazy_static = "~1.4" chrono = "~0.4" -pretty_env_logger = "~0.4" glob = "~0.3" strum = { version = "~0.24", features = ["derive"] } thiserror = "~1.0" -anyhow = "~1.0" tempfile = "~3.2" cfg-if = "~1.0" tokio = { version = "~1.5", features = ["process", "rt-multi-thread"] } futures = "~0.3" regex = "~1.5" -sys-info = "~0.9" semver = "~1.0" shell-words = "~1.1" +color-eyre = "0.6.2" +tracing = { version = "0.1.37", features = ["attributes", "log"] } +tracing-subscriber = { version = "0.3.16", features = ["env-filter", "time"] } [target.'cfg(target_os = "macos")'.dependencies] notify-rust = "~4.5" +[package.metadata.generate-rpm] +assets = [{source = "target/release/topgrade", dest="/usr/bin/topgrade"}] + +[package.metadata.generate-rpm.requires] +git = "*" + +[package.metadata.deb] +depends = "$auto,git" + [target.'cfg(unix)'.dependencies] +libc = "~0.2" nix = "~0.24" rust-ini = "~0.18" self_update_crate = { version = "~0.30", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] } diff --git a/Cross.toml b/Cross.toml new file mode 100644 index 00000000..33be5cdd --- /dev/null +++ b/Cross.toml @@ -0,0 +1,4 @@ +# Workaround for: https://github.com/cross-rs/cross/issues/1100 +# TODO: Remove this file altogether once a new version of cross (after v0.2.4) is released. +[target.x86_64-unknown-freebsd.env] +passthrough = ["AR_x86_64_unknown_freebsd=x86_64-unknown-freebsd12-ar"] diff --git a/README.md b/README.md index aa5bedf1..db8f5cfe 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ To remedy this, **Topgrade** detects which tools you use and runs the appropriat Other systems users can either use `cargo install` or the compiled binaries from the release page. The compiled binaries contain a self-upgrading feature. -Topgrade requires Rust 1.51 or above. +Topgrade requires Rust 1.60 or above. ## Usage diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 00000000..cc1ff16c --- /dev/null +++ b/clippy.toml @@ -0,0 +1,5 @@ +disallowed-methods = [ + { path = "std::process::Command::output", reason = "Use `output_checked[_with][_utf8]`" }, + { path = "std::process::Command::spawn", reason = "Use `spawn_checked`" }, + { path = "std::process::Command::status", reason = "Use `status_checked`" }, +] diff --git a/src/command.rs b/src/command.rs new file mode 100644 index 00000000..d8a87e25 --- /dev/null +++ b/src/command.rs @@ -0,0 +1,244 @@ +//! Utilities for running commands and providing user-friendly error messages. + +use std::fmt::Display; +use std::process::Child; +use std::process::{Command, ExitStatus, Output}; + +use color_eyre::eyre; +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; + +use crate::error::TopgradeError; + +/// Like [`Output`], but UTF-8 decoded. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Utf8Output { + pub status: ExitStatus, + pub stdout: String, + pub stderr: String, +} + +impl TryFrom for Utf8Output { + type Error = eyre::Error; + + fn try_from(Output { status, stdout, stderr }: Output) -> Result { + let stdout = String::from_utf8(stdout).map_err(|err| { + eyre!( + "Stdout contained invalid UTF-8: {}", + String::from_utf8_lossy(err.as_bytes()) + ) + })?; + let stderr = String::from_utf8(stderr).map_err(|err| { + eyre!( + "Stderr contained invalid UTF-8: {}", + String::from_utf8_lossy(err.as_bytes()) + ) + })?; + + Ok(Utf8Output { status, stdout, stderr }) + } +} + +impl TryFrom<&Output> for Utf8Output { + type Error = eyre::Error; + + fn try_from(Output { status, stdout, stderr }: &Output) -> Result { + let stdout = String::from_utf8(stdout.to_vec()).map_err(|err| { + eyre!( + "Stdout contained invalid UTF-8: {}", + String::from_utf8_lossy(err.as_bytes()) + ) + })?; + let stderr = String::from_utf8(stderr.to_vec()).map_err(|err| { + eyre!( + "Stderr contained invalid UTF-8: {}", + String::from_utf8_lossy(err.as_bytes()) + ) + })?; + let status = *status; + + Ok(Utf8Output { status, stdout, stderr }) + } +} + +impl Display for Utf8Output { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.stdout) + } +} + +/// Extension trait for [`Command`], adding helpers to gather output while checking the exit +/// status. +/// +/// These also give us significantly better error messages, which include: +/// +/// 1. The command and arguments that were executed, escaped with familiar `sh` syntax. +/// 2. The exit status of the command or the signal that killed it. +/// 3. If we were capturing the output of the command, rather than forwarding it to the user's +/// stdout/stderr, the error message includes the command's stdout and stderr output. +/// +/// Additionally, executing commands with these methods will log the command at debug-level, +/// useful when gathering error reports. +pub trait CommandExt { + type Child; + + /// Like [`Command::output`], but checks the exit status and provides nice error messages. + /// + /// Returns an `Err` if the command failed to execute or returned a non-zero exit code. + #[track_caller] + fn output_checked(&mut self) -> eyre::Result { + self.output_checked_with(|output: &Output| if output.status.success() { Ok(()) } else { Err(()) }) + } + + /// Like [`output_checked`], but also decodes Stdout and Stderr as UTF-8. + /// + /// Returns an `Err` if the command failed to execute, returned a non-zero exit code, or if the + /// output contains invalid UTF-8. + #[track_caller] + fn output_checked_utf8(&mut self) -> eyre::Result { + let output = self.output_checked()?; + output.try_into() + } + + /// Like [`output_checked`] but a closure determines if the command failed instead of + /// [`ExitStatus::success`]. + /// + /// Returns an `Err` if the command failed to execute or if `succeeded` returns an `Err`. + /// (This lets the caller substitute their own notion of "success" instead of assuming + /// non-zero exit codes indicate success.) + #[track_caller] + fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result; + + /// Like [`output_checked_with`], but also decodes Stdout and Stderr as UTF-8. + /// + /// Returns an `Err` if the command failed to execute, if `succeeded` returns an `Err`, or if + /// the output contains invalid UTF-8. + #[track_caller] + fn output_checked_with_utf8( + &mut self, + succeeded: impl Fn(&Utf8Output) -> Result<(), ()>, + ) -> eyre::Result { + // This decodes the Stdout and Stderr as UTF-8 twice... + let output = + self.output_checked_with(|output| output.try_into().map_err(|_| ()).and_then(|o| succeeded(&o)))?; + output.try_into() + } + + /// Like [`Command::status`], but gives a nice error message if the status is unsuccessful + /// rather than returning the [`ExitStatus`]. + /// + /// Returns `Ok` if the command executes successfully, returns `Err` if the command fails to + /// execute or returns a non-zero exit code. + #[track_caller] + fn status_checked(&mut self) -> eyre::Result<()> { + self.status_checked_with(|status| if status.success() { Ok(()) } else { Err(()) }) + } + + /// Like [`status_checked`], but gives a nice error message if the status is unsuccessful + /// rather than returning the [`ExitStatus`]. + /// + /// Returns `Ok` if the command executes successfully, returns `Err` if the command fails to + /// execute or if `succeeded` returns an `Err`. + /// (This lets the caller substitute their own notion of "success" instead of assuming + /// non-zero exit codes indicate success.) + #[track_caller] + fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()>; + + /// Like [`Command::spawn`], but gives a nice error message if the command fails to + /// execute. + #[track_caller] + fn spawn_checked(&mut self) -> eyre::Result; +} + +impl CommandExt for Command { + type Child = Child; + + fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result { + let command = log(self); + + // This is where we implement `output_checked`, which is what we prefer to use instead of + // `output`, so we allow `Command::output` here. + #[allow(clippy::disallowed_methods)] + let output = self + .output() + .with_context(|| format!("Failed to execute `{command}`"))?; + + if succeeded(&output).is_ok() { + Ok(output) + } else { + let mut message = format!("Command failed: `{command}`"); + let stderr = String::from_utf8_lossy(&output.stderr); + let stdout = String::from_utf8_lossy(&output.stdout); + + let stdout_trimmed = stdout.trim(); + if !stdout_trimmed.is_empty() { + message.push_str(&format!("\n\nStdout:\n{stdout_trimmed}")); + } + let stderr_trimmed = stderr.trim(); + if !stderr_trimmed.is_empty() { + message.push_str(&format!("\n\nStderr:\n{stderr_trimmed}")); + } + + let (program, _) = get_program_and_args(self); + let err = TopgradeError::ProcessFailedWithOutput(program, output.status, stderr.into_owned()); + + let ret = Err(err).with_context(|| message); + tracing::debug!("Command failed: {ret:?}"); + ret + } + } + + fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()> { + let command = log(self); + let message = format!("Failed to execute `{command}`"); + + // This is where we implement `status_checked`, which is what we prefer to use instead of + // `status`, so we allow `Command::status` here. + #[allow(clippy::disallowed_methods)] + let status = self.status().with_context(|| message.clone())?; + + if succeeded(status).is_ok() { + Ok(()) + } else { + let (program, _) = get_program_and_args(self); + let err = TopgradeError::ProcessFailed(program, status); + let ret = Err(err).with_context(|| format!("Command failed: `{command}`")); + tracing::debug!("Command failed: {ret:?}"); + ret + } + } + + fn spawn_checked(&mut self) -> eyre::Result { + let command = log(self); + let message = format!("Failed to execute `{command}`"); + + // This is where we implement `spawn_checked`, which is what we prefer to use instead of + // `spawn`, so we allow `Command::spawn` here. + #[allow(clippy::disallowed_methods)] + { + self.spawn().with_context(|| message.clone()) + } + } +} + +fn get_program_and_args(cmd: &Command) -> (String, String) { + // We're not doing anything weird with commands that are invalid UTF-8 so this is fine. + let program = cmd.get_program().to_string_lossy().into_owned(); + let args = shell_words::join(cmd.get_args().map(|arg| arg.to_string_lossy())); + (program, args) +} + +fn format_program_and_args(cmd: &Command) -> String { + let (program, args) = get_program_and_args(cmd); + if args.is_empty() { + program + } else { + format!("{program} {args}") + } +} + +fn log(cmd: &Command) -> String { + let command = format_program_and_args(cmd); + tracing::debug!("Executing command `{command}`"); + command +} diff --git a/src/config.rs b/src/config.rs index f032c2e2..b98910d9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,21 +1,24 @@ #![allow(dead_code)] -use anyhow::Context; -use anyhow::Result; -use clap::{ArgEnum, Parser}; -use directories::BaseDirs; -use log::debug; -use regex::Regex; -use serde::Deserialize; use std::collections::BTreeMap; use std::fs::write; use std::path::PathBuf; use std::process::Command; use std::{env, fs}; + +use clap::{ArgEnum, Parser}; +use color_eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; +use directories::BaseDirs; +use regex::Regex; +use serde::Deserialize; use strum::{EnumIter, EnumString, EnumVariantNames, IntoEnumIterator}; -use sys_info::hostname; +use tracing::debug; use which_crate::which; -use super::utils::editor; +use crate::command::CommandExt; + +use super::utils::{editor, hostname}; pub static EXAMPLE_CONFIG: &str = include_str!("../config.example.toml"); @@ -348,12 +351,12 @@ impl ConfigFile { }; let contents = fs::read_to_string(&config_path).map_err(|e| { - log::error!("Unable to read {}", config_path.display()); + tracing::error!("Unable to read {}", config_path.display()); e })?; let mut result: Self = toml::from_str(&contents).map_err(|e| { - log::error!("Failed to deserialize {}", config_path.display()); + tracing::error!("Failed to deserialize {}", config_path.display()); e })?; @@ -389,9 +392,8 @@ impl ConfigFile { Command::new(command) .args(args) .arg(config_path) - .spawn() - .and_then(|mut p| p.wait())?; - Ok(()) + .status_checked() + .context("Failed to open configuration file editor") } } @@ -439,7 +441,7 @@ pub struct CommandLineArgs { #[clap(long = "env", value_name = "NAME=VALUE", multiple_values = true)] env: Vec, - /// Output logs + /// Output debug logs. Alias for `--log-filter debug`. #[clap(short = 'v', long = "verbose")] pub verbose: bool, @@ -477,6 +479,12 @@ pub struct CommandLineArgs { /// Show the reason for skipped steps #[clap(long = "show-skipped")] show_skipped: bool, + + /// Tracing filter directives. + /// + /// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/struct.EnvFilter.html + #[clap(long, default_value = "info")] + pub log_filter: String, } impl CommandLineArgs { @@ -491,6 +499,14 @@ impl CommandLineArgs { pub fn env_variables(&self) -> &Vec { &self.env } + + pub fn tracing_filter_directives(&self) -> String { + if self.verbose { + "debug".into() + } else { + self.log_filter.clone() + } + } } /// Represents the application configuration @@ -514,11 +530,11 @@ impl Config { ConfigFile::read(base_dirs, opt.config.clone()).unwrap_or_else(|e| { // Inform the user about errors when loading the configuration, // but fallback to the default config to at least attempt to do something - log::error!("failed to load configuration: {}", e); + tracing::error!("failed to load configuration: {}", e); ConfigFile::default() }) } else { - log::debug!("Configuration directory {} does not exist", config_directory.display()); + tracing::debug!("Configuration directory {} does not exist", config_directory.display()); ConfigFile::default() }; @@ -633,7 +649,7 @@ impl Config { } /// Extra Tmux arguments - pub fn tmux_arguments(&self) -> anyhow::Result> { + pub fn tmux_arguments(&self) -> eyre::Result> { let args = &self.config_file.tmux_arguments.as_deref().unwrap_or_default(); shell_words::split(args) // The only time the parse failed is in case of a missing close quote. diff --git a/src/ctrlc/windows.rs b/src/ctrlc/windows.rs index b5148ee7..b2cf9e15 100644 --- a/src/ctrlc/windows.rs +++ b/src/ctrlc/windows.rs @@ -16,6 +16,6 @@ extern "system" fn handler(ctrl_type: DWORD) -> BOOL { pub fn set_handler() { if 0 == unsafe { SetConsoleCtrlHandler(Some(handler), TRUE) } { - log::error!("Cannot set a control C handler") + tracing::error!("Cannot set a control C handler") } } diff --git a/src/error.rs b/src/error.rs index 5ec6ce75..def711d7 100644 --- a/src/error.rs +++ b/src/error.rs @@ -4,11 +4,11 @@ use thiserror::Error; #[derive(Error, Debug, PartialEq, Eq)] pub enum TopgradeError { - #[error("{0}")] - ProcessFailed(ExitStatus), + #[error("`{0}` failed: {1}")] + ProcessFailed(String, ExitStatus), - #[error("{0}: {1}")] - ProcessFailedWithOutput(ExitStatus, String), + #[error("`{0}` failed: {1}")] + ProcessFailedWithOutput(String, ExitStatus, String), #[error("Sudo is required for this step")] #[allow(dead_code)] diff --git a/src/execution_context.rs b/src/execution_context.rs index 6fe89e7a..661e8bc8 100644 --- a/src/execution_context.rs +++ b/src/execution_context.rs @@ -3,9 +3,10 @@ use crate::executor::RunType; use crate::git::Git; use crate::utils::require_option; use crate::{config::Config, executor::Executor}; -use anyhow::Result; +use color_eyre::eyre::Result; use directories::BaseDirs; use std::path::{Path, PathBuf}; +use std::sync::Mutex; pub struct ExecutionContext<'a> { run_type: RunType, @@ -13,6 +14,10 @@ pub struct ExecutionContext<'a> { git: &'a Git, config: &'a Config, base_dirs: &'a BaseDirs, + /// Name of a tmux session to execute commands in, if any. + /// This is used in `./steps/remote/ssh.rs`, where we want to run `topgrade` in a new + /// tmux window for each remote. + tmux_session: Mutex>, } impl<'a> ExecutionContext<'a> { @@ -22,13 +27,14 @@ impl<'a> ExecutionContext<'a> { git: &'a Git, config: &'a Config, base_dirs: &'a BaseDirs, - ) -> ExecutionContext<'a> { - ExecutionContext { + ) -> Self { + Self { run_type, sudo, git, config, base_dirs, + tmux_session: Mutex::new(None), } } @@ -67,4 +73,12 @@ impl<'a> ExecutionContext<'a> { pub fn base_dirs(&self) -> &BaseDirs { self.base_dirs } + + pub fn set_tmux_session(&self, session_name: String) { + self.tmux_session.lock().unwrap().replace(session_name); + } + + pub fn get_tmux_session(&self) -> Option { + self.tmux_session.lock().unwrap().clone() + } } diff --git a/src/executor.rs b/src/executor.rs index b68b30a4..a403a65e 100644 --- a/src/executor.rs +++ b/src/executor.rs @@ -1,11 +1,14 @@ //! Utilities for command execution -use crate::error::{DryRun, TopgradeError}; -use crate::utils::{Check, CheckWithCodes}; -use anyhow::Result; -use log::{debug, trace}; use std::ffi::{OsStr, OsString}; use std::path::Path; -use std::process::{Child, Command, ExitStatus}; +use std::process::{Child, Command, ExitStatus, Output}; + +use color_eyre::eyre; +use color_eyre::eyre::Result; +use tracing::debug; + +use crate::command::CommandExt; +use crate::error::DryRun; /// An enum telling whether Topgrade should perform dry runs or actually perform the steps. #[derive(Clone, Copy, Debug)] @@ -56,6 +59,16 @@ pub enum Executor { } impl Executor { + /// Get the name of the program being run. + /// + /// Will give weird results for non-UTF-8 programs; see `to_string_lossy()`. + pub fn get_program(&self) -> String { + match self { + Executor::Wet(c) => c.get_program().to_string_lossy().into_owned(), + Executor::Dry(c) => c.program.to_string_lossy().into_owned(), + } + } + /// See `std::process::Command::arg` pub fn arg>(&mut self, arg: S) -> &mut Executor { match self { @@ -139,7 +152,7 @@ impl Executor { let result = match self { Executor::Wet(c) => { debug!("Running {:?}", c); - c.spawn().map(ExecutorChild::Wet)? + c.spawn_checked().map(ExecutorChild::Wet)? } Executor::Dry(c) => { c.dry_run(); @@ -153,7 +166,7 @@ impl Executor { /// See `std::process::Command::output` pub fn output(&mut self) -> Result { match self { - Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output()?)), + Executor::Wet(c) => Ok(ExecutorOutput::Wet(c.output_checked()?)), Executor::Dry(c) => { c.dry_run(); Ok(ExecutorOutput::Dry) @@ -161,23 +174,28 @@ impl Executor { } } - /// A convinence method for `spawn().wait().check()`. - /// Returns an error if something went wrong during the execution or if the - /// process exited with failure. - pub fn check_run(&mut self) -> Result<()> { - self.spawn()?.wait()?.check() - } - - /// An extension of `check_run` that allows you to set a sequence of codes + /// An extension of `status_checked` that allows you to set a sequence of codes /// that can indicate success of a script - #[allow(dead_code)] - pub fn check_run_with_codes(&mut self, codes: &[i32]) -> Result<()> { - self.spawn()?.wait()?.check_with_codes(codes) + #[cfg_attr(windows, allow(dead_code))] + pub fn status_checked_with_codes(&mut self, codes: &[i32]) -> Result<()> { + match self { + Executor::Wet(c) => c.status_checked_with(|status| { + if status.success() || status.code().as_ref().map(|c| codes.contains(c)).unwrap_or(false) { + Ok(()) + } else { + Err(()) + } + }), + Executor::Dry(c) => { + c.dry_run(); + Ok(()) + } + } } } pub enum ExecutorOutput { - Wet(std::process::Output), + Wet(Output), Dry, } @@ -214,78 +232,33 @@ pub enum ExecutorChild { Dry, } -impl ExecutorChild { - /// See `std::process::Child::wait` - pub fn wait(&mut self) -> Result { - let result = match self { - ExecutorChild::Wet(c) => c.wait().map(ExecutorExitStatus::Wet)?, - ExecutorChild::Dry => ExecutorExitStatus::Dry, - }; - - Ok(result) - } -} - -/// The Result of wait. Contains an actual `std::process::ExitStatus` if executed by a wet command. -pub enum ExecutorExitStatus { - Wet(ExitStatus), - Dry, -} - -impl CheckWithCodes for ExecutorExitStatus { - fn check_with_codes(self, codes: &[i32]) -> Result<()> { - match self { - ExecutorExitStatus::Wet(e) => e.check_with_codes(codes), - ExecutorExitStatus::Dry => Ok(()), - } - } -} - -/// Extension methods for `std::process::Command` -pub trait CommandExt { - /// Run the command, wait for it to complete, check the return code and decode the output as UTF-8. - fn check_output(&mut self) -> Result; - fn string_output(&mut self) -> Result; -} - -impl CommandExt for Command { - fn check_output(&mut self) -> Result { - let output = self.output()?; - trace!("Output of {:?}: {:?}", self, output); - let status = output.status; - if !status.success() { - let stderr = String::from_utf8(output.stderr).unwrap_or_default(); - return Err(TopgradeError::ProcessFailedWithOutput(status, stderr).into()); - } - Ok(String::from_utf8(output.stdout)?) - } - - fn string_output(&mut self) -> Result { - let output = self.output()?; - trace!("Output of {:?}: {:?}", self, output); - Ok(String::from_utf8(output.stdout)?) - } -} - impl CommandExt for Executor { - fn check_output(&mut self) -> Result { - let output = match self.output()? { - ExecutorOutput::Wet(output) => output, - ExecutorOutput::Dry => return Err(DryRun().into()), - }; - let status = output.status; - if !status.success() { - let stderr = String::from_utf8(output.stderr).unwrap_or_default(); - return Err(TopgradeError::ProcessFailedWithOutput(status, stderr).into()); + type Child = ExecutorChild; + + // TODO: It might be nice to make `output_checked_with` return something that has a + // variant for wet/dry runs. + + fn output_checked_with(&mut self, succeeded: impl Fn(&Output) -> Result<(), ()>) -> eyre::Result { + match self { + Executor::Wet(c) => c.output_checked_with(succeeded), + Executor::Dry(c) => { + c.dry_run(); + Err(DryRun().into()) + } } - Ok(String::from_utf8(output.stdout)?) } - fn string_output(&mut self) -> Result { - let output = match self.output()? { - ExecutorOutput::Wet(output) => output, - ExecutorOutput::Dry => return Err(DryRun().into()), - }; - Ok(String::from_utf8(output.stdout)?) + fn status_checked_with(&mut self, succeeded: impl Fn(ExitStatus) -> Result<(), ()>) -> eyre::Result<()> { + match self { + Executor::Wet(c) => c.status_checked_with(succeeded), + Executor::Dry(c) => { + c.dry_run(); + Ok(()) + } + } + } + + fn spawn_checked(&mut self) -> eyre::Result { + self.spawn() } } diff --git a/src/main.rs b/src/main.rs index d08d207e..f4dbcecf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -4,12 +4,11 @@ use std::env; use std::io; use std::process::exit; -use anyhow::{anyhow, Result}; use clap::{crate_version, Parser}; +use color_eyre::eyre::Context; +use color_eyre::eyre::{eyre, Result}; use console::Key; -use log::debug; -use log::LevelFilter; -use pretty_env_logger::formatted_timed_builder; +use tracing::debug; use self::config::{CommandLineArgs, Config, Step}; use self::error::StepFailed; @@ -18,6 +17,7 @@ use self::error::Upgraded; use self::steps::{remote::*, *}; use self::terminal::*; +mod command; mod config; mod ctrlc; mod error; @@ -34,12 +34,15 @@ mod terminal; mod utils; fn run() -> Result<()> { + color_eyre::install()?; ctrlc::set_handler(); - let base_dirs = directories::BaseDirs::new().ok_or_else(|| anyhow!("No base directories"))?; + let base_dirs = directories::BaseDirs::new().ok_or_else(|| eyre!("No base directories"))?; let opt = CommandLineArgs::parse(); + install_tracing(&opt.tracing_filter_directives())?; + for env in opt.env_variables() { let mut splitted = env.split('='); let var = splitted.next().unwrap(); @@ -47,14 +50,6 @@ fn run() -> Result<()> { env::set_var(var, value); } - let mut builder = formatted_timed_builder(); - - if opt.verbose { - builder.filter(Some("topgrade"), LevelFilter::Trace); - } - - builder.init(); - if opt.edit_config() { Config::edit(&base_dirs)?; return Ok(()); @@ -79,7 +74,8 @@ fn run() -> Result<()> { if config.run_in_tmux() && env::var("TOPGRADE_INSIDE_TMUX").is_err() { #[cfg(unix)] { - tmux::run_in_tmux(config.tmux_arguments()?); + tmux::run_in_tmux(config.tmux_arguments()?)?; + return Ok(()); } } @@ -206,7 +202,7 @@ fn run() -> Result<()> { #[cfg(target_os = "freebsd")] runner.execute(Step::Pkg, "FreeBSD Packages", || { - freebsd::upgrade_packages(sudo.as_ref(), run_type) + freebsd::upgrade_packages(&ctx, sudo.as_ref(), run_type) })?; #[cfg(target_os = "openbsd")] @@ -296,7 +292,7 @@ fn run() -> Result<()> { runner.execute(Step::Shell, "zi", || zsh::run_zi(&base_dirs, run_type))?; runner.execute(Step::Shell, "zim", || zsh::run_zim(&base_dirs, run_type))?; runner.execute(Step::Shell, "oh-my-zsh", || zsh::run_oh_my_zsh(&ctx))?; - runner.execute(Step::Shell, "fisher", || unix::run_fisher(&base_dirs, run_type))?; + runner.execute(Step::Shell, "fisher", || unix::run_fisher(run_type))?; runner.execute(Step::Shell, "bash-it", || unix::run_bashit(&ctx))?; runner.execute(Step::Shell, "oh-my-fish", || unix::run_oh_my_fish(&ctx))?; runner.execute(Step::Shell, "fish-plug", || unix::run_fish_plug(&ctx))?; @@ -327,7 +323,8 @@ fn run() -> Result<()> { runner.execute(Step::Choosenim, "choosenim", || generic::run_choosenim(&ctx))?; runner.execute(Step::Cargo, "cargo", || generic::run_cargo_update(&ctx))?; runner.execute(Step::Flutter, "Flutter", || generic::run_flutter_upgrade(run_type))?; - runner.execute(Step::Go, "Go", || generic::run_go(run_type))?; + runner.execute(Step::Go, "go-global-update", || go::run_go_global_update(run_type))?; + runner.execute(Step::Go, "gup", || go::run_go_gup(run_type))?; runner.execute(Step::Emacs, "Emacs", || emacs.upgrade(&ctx))?; runner.execute(Step::Opam, "opam", || generic::run_opam_update(&ctx))?; runner.execute(Step::Vcpkg, "vcpkg", || generic::run_vcpkg_update(run_type))?; @@ -471,10 +468,10 @@ fn run() -> Result<()> { loop { match get_key() { Ok(Key::Char('s')) | Ok(Key::Char('S')) => { - run_shell(); + run_shell().context("Failed to execute shell")?; } Ok(Key::Char('r')) | Ok(Key::Char('R')) => { - reboot(); + reboot().context("Failed to reboot")?; } Ok(Key::Char('q')) | Ok(Key::Char('Q')) => (), _ => { @@ -524,7 +521,7 @@ fn main() { .is_some()); if !skip_print { - // The `Debug` implementation of `anyhow::Result` prints a multi-line + // The `Debug` implementation of `eyre::Result` prints a multi-line // error message that includes all the 'causes' added with // `.with_context(...)` calls. println!("Error: {:?}", error); @@ -533,3 +530,26 @@ fn main() { } } } + +pub fn install_tracing(filter_directives: &str) -> Result<()> { + use tracing_subscriber::fmt; + use tracing_subscriber::fmt::format::FmtSpan; + use tracing_subscriber::layer::SubscriberExt; + use tracing_subscriber::util::SubscriberInitExt; + use tracing_subscriber::EnvFilter; + + let env_filter = EnvFilter::try_new(filter_directives) + .or_else(|_| EnvFilter::try_from_default_env()) + .or_else(|_| EnvFilter::try_new("info"))?; + + let fmt_layer = fmt::layer() + .with_target(false) + .with_span_events(FmtSpan::NEW | FmtSpan::CLOSE) + .without_time(); + + let registry = tracing_subscriber::registry(); + + registry.with(env_filter).with(fmt_layer).init(); + + Ok(()) +} diff --git a/src/runner.rs b/src/runner.rs index 2636a88a..99175bda 100644 --- a/src/runner.rs +++ b/src/runner.rs @@ -2,11 +2,12 @@ use crate::ctrlc; use crate::error::{DryRun, SkipStep}; use crate::execution_context::ExecutionContext; use crate::report::{Report, StepResult}; +use crate::terminal::print_error; use crate::{config::Step, terminal::should_retry}; -use anyhow::Result; -use log::debug; +use color_eyre::eyre::Result; use std::borrow::Cow; use std::fmt::Debug; +use tracing::debug; pub struct Runner<'a> { ctx: &'a ExecutionContext<'a>, @@ -55,7 +56,12 @@ impl<'a> Runner<'a> { let ignore_failure = self.ctx.config().ignore_failure(step); let should_ask = interrupted || !(self.ctx.config().no_retry() || ignore_failure); - let should_retry = should_ask && should_retry(interrupted, key.as_ref())?; + let should_retry = if should_ask { + print_error(&key, format!("{e:?}")); + should_retry(interrupted, key.as_ref())? + } else { + false + }; if !should_retry { self.report.push_result(Some(( diff --git a/src/self_renamer.rs b/src/self_renamer.rs index 16186bac..4a26b672 100644 --- a/src/self_renamer.rs +++ b/src/self_renamer.rs @@ -1,8 +1,8 @@ #![cfg(windows)] -use anyhow::Result; -use log::{debug, error}; +use color_eyre::eyre::Result; use std::{env::current_exe, fs, path::PathBuf}; +use tracing::{debug, error}; pub struct SelfRenamer { exe_path: PathBuf, diff --git a/src/self_update.rs b/src/self_update.rs index 9488fb58..95274e99 100644 --- a/src/self_update.rs +++ b/src/self_update.rs @@ -1,13 +1,15 @@ +use std::env; +#[cfg(unix)] +use std::os::unix::process::CommandExt as _; +use std::process::Command; + +use color_eyre::eyre::{bail, Result}; +use self_update_crate::backends::github::Update; +use self_update_crate::update::UpdateStatus; + use super::terminal::*; #[cfg(windows)] use crate::error::Upgraded; -use anyhow::{bail, Result}; -use self_update_crate::backends::github::Update; -use self_update_crate::update::UpdateStatus; -use std::env; -#[cfg(unix)] -use std::os::unix::process::CommandExt; -use std::process::Command; pub fn self_update() -> Result<()> { print_separator("Self update"); @@ -49,7 +51,8 @@ pub fn self_update() -> Result<()> { #[cfg(windows)] { - let status = command.spawn().and_then(|mut c| c.wait())?; + #[allow(clippy::disallowed_methods)] + let status = command.status()?; bail!(Upgraded(status)); } } diff --git a/src/steps/containers.rs b/src/steps/containers.rs index 3ad4b53d..811cf7de 100644 --- a/src/steps/containers.rs +++ b/src/steps/containers.rs @@ -1,13 +1,16 @@ -use anyhow::Result; - -use crate::error::{self, TopgradeError}; -use crate::executor::CommandExt; -use crate::terminal::print_separator; -use crate::{execution_context::ExecutionContext, utils::require}; -use log::{debug, error, warn}; use std::path::Path; use std::process::Command; +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; +use tracing::{debug, error, warn}; + +use crate::command::CommandExt; +use crate::error::{self, TopgradeError}; +use crate::terminal::print_separator; +use crate::{execution_context::ExecutionContext, utils::require}; + // A string found in the output of docker for containers that weren't found in // the docker registry. We use this to gracefully handle and skip containers // that cannot be pulled, likely because they don't exist in the registry in @@ -24,11 +27,10 @@ fn list_containers(crt: &Path) -> Result> { ); let output = Command::new(crt) .args(["image", "ls", "--format", "{{.Repository}}:{{.Tag}}"]) - .output()?; - let output_str = String::from_utf8(output.stdout)?; + .output_checked_with_utf8(|_| Ok(()))?; let mut retval = vec![]; - for line in output_str.lines() { + for line in output.stdout.lines() { if line.starts_with("localhost") { // Don't know how to update self-built containers debug!("Skipping self-built container '{}'", line); @@ -60,7 +62,7 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { print_separator("Containers"); let mut success = true; - let containers = list_containers(&crt)?; + let containers = list_containers(&crt).context("Failed to list Docker containers")?; debug!("Containers to inspect: {:?}", containers); for container in containers.iter() { @@ -68,7 +70,7 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { let args = vec!["pull", &container[..]]; let mut exec = ctx.run_type().execute(&crt); - if let Err(e) = exec.args(&args).check_run() { + if let Err(e) = exec.args(&args).status_checked() { error!("Pulling container '{}' failed: {}", container, e); // Find out if this is 'skippable' @@ -77,10 +79,10 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { // practical consequence that all containers, whether self-built, created by // docker-compose or pulled from the docker hub, look exactly the same to us. We can // only find out what went wrong by manually parsing the output of the command... - if match exec.check_output() { - Ok(s) => s.contains(NONEXISTENT_REPO), + if match exec.output_checked_utf8() { + Ok(s) => s.stdout.contains(NONEXISTENT_REPO) || s.stderr.contains(NONEXISTENT_REPO), Err(e) => match e.downcast_ref::() { - Some(TopgradeError::ProcessFailedWithOutput(_, stderr)) => stderr.contains(NONEXISTENT_REPO), + Some(TopgradeError::ProcessFailedWithOutput(_, _, stderr)) => stderr.contains(NONEXISTENT_REPO), _ => false, }, } { @@ -95,7 +97,12 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { if ctx.config().cleanup() { // Remove dangling images debug!("Removing dangling images"); - if let Err(e) = ctx.run_type().execute(&crt).args(["image", "prune", "-f"]).check_run() { + if let Err(e) = ctx + .run_type() + .execute(&crt) + .args(["image", "prune", "-f"]) + .status_checked() + { error!("Removing dangling images failed: {}", e); success = false; } @@ -104,6 +111,6 @@ pub fn run_containers(ctx: &ExecutionContext) -> Result<()> { if success { Ok(()) } else { - Err(anyhow::anyhow!(error::StepFailed)) + Err(eyre!(error::StepFailed)) } } diff --git a/src/steps/emacs.rs b/src/steps/emacs.rs index bbd80bb3..89e04885 100644 --- a/src/steps/emacs.rs +++ b/src/steps/emacs.rs @@ -2,9 +2,10 @@ use std::env; use std::path::{Path, PathBuf}; -use anyhow::Result; +use color_eyre::eyre::Result; use directories::BaseDirs; +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::terminal::print_separator; use crate::utils::{require, require_option, PathExt}; @@ -73,7 +74,7 @@ impl Emacs { command.args(["upgrade"]); - command.check_run() + command.status_checked() } pub fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> { @@ -105,6 +106,6 @@ impl Emacs { #[cfg(not(unix))] command.arg(EMACS_UPGRADE); - command.check_run() + command.status_checked() } } diff --git a/src/steps/generic.rs b/src/steps/generic.rs index 7694eada..1646b0be 100644 --- a/src/steps/generic.rs +++ b/src/steps/generic.rs @@ -5,13 +5,16 @@ use std::process::Command; use std::{env, path::Path}; use std::{fs, io::Write}; -use anyhow::Result; +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; use directories::BaseDirs; -use log::debug; use tempfile::tempfile_in; +use tracing::debug; +use crate::command::{CommandExt, Utf8Output}; use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, ExecutorOutput, RunType}; +use crate::executor::{ExecutorOutput, RunType}; use crate::terminal::{print_separator, shell}; use crate::utils::{self, require_option, PathExt}; use crate::{ @@ -53,28 +56,14 @@ pub fn run_cargo_update(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(cargo_update) .args(["install-update", "--git", "--all"]) - .check_run() + .status_checked() } pub fn run_flutter_upgrade(run_type: RunType) -> Result<()> { let flutter = utils::require("flutter")?; print_separator("Flutter"); - run_type.execute(flutter).arg("upgrade").check_run() -} - -pub fn run_go(run_type: RunType) -> Result<()> { - let go = utils::require("go")?; - let go_output = run_type.execute(go).args(["env", "GOPATH"]).check_output()?; - let gopath = go_output.trim(); - - let go_global_update = utils::require("go-global-update") - .unwrap_or_else(|_| PathBuf::from(gopath).join("bin/go-global-update")) - .require()?; - - print_separator("Go"); - - run_type.execute(go_global_update).check_run() + run_type.execute(flutter).arg("upgrade").status_checked() } pub fn run_gem(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -91,14 +80,15 @@ pub fn run_gem(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { command.arg("--user-install"); } - command.check_run() + command.status_checked() } pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> { let haxelib = utils::require("haxelib")?; let haxelib_dir = - PathBuf::from(std::str::from_utf8(&Command::new(&haxelib).arg("config").output()?.stdout)?.trim()).require()?; + PathBuf::from(std::str::from_utf8(&Command::new(&haxelib).arg("config").output_checked()?.stdout)?.trim()) + .require()?; let directory_writable = tempfile_in(&haxelib_dir).is_ok(); debug!("{:?} writable: {}", haxelib_dir, directory_writable); @@ -115,7 +105,7 @@ pub fn run_haxelib_update(ctx: &ExecutionContext) -> Result<()> { c }; - command.arg("update").check_run() + command.arg("update").status_checked() } pub fn run_sheldon(ctx: &ExecutionContext) -> Result<()> { @@ -123,7 +113,10 @@ pub fn run_sheldon(ctx: &ExecutionContext) -> Result<()> { print_separator("Sheldon"); - ctx.run_type().execute(sheldon).args(["lock", "--update"]).check_run() + ctx.run_type() + .execute(sheldon) + .args(["lock", "--update"]) + .status_checked() } pub fn run_fossil(run_type: RunType) -> Result<()> { @@ -131,7 +124,7 @@ pub fn run_fossil(run_type: RunType) -> Result<()> { print_separator("Fossil"); - run_type.execute(fossil).args(["all", "sync"]).check_run() + run_type.execute(fossil).args(["all", "sync"]).status_checked() } pub fn run_micro(run_type: RunType) -> Result<()> { @@ -139,13 +132,17 @@ pub fn run_micro(run_type: RunType) -> Result<()> { print_separator("micro"); - let stdout = run_type.execute(micro).args(["-plugin", "update"]).string_output()?; + let stdout = run_type + .execute(micro) + .args(["-plugin", "update"]) + .output_checked_utf8()? + .stdout; std::io::stdout().write_all(stdout.as_bytes())?; if stdout.contains("Nothing to install / update") || stdout.contains("One or more plugins installed") { Ok(()) } else { - Err(anyhow::anyhow!("micro output does not indicate success: {}", stdout)) + Err(eyre!("micro output does not indicate success: {}", stdout)) } } @@ -160,7 +157,10 @@ pub fn run_apm(run_type: RunType) -> Result<()> { print_separator("Atom Package Manager"); - run_type.execute(apm).args(["upgrade", "--confirm=false"]).check_run() + run_type + .execute(apm) + .args(["upgrade", "--confirm=false"]) + .status_checked() } pub fn run_rustup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -169,10 +169,10 @@ pub fn run_rustup(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("rustup"); if rustup.canonicalize()?.is_descendant_of(base_dirs.home_dir()) { - run_type.execute(&rustup).args(["self", "update"]).check_run()?; + run_type.execute(&rustup).args(["self", "update"]).status_checked()?; } - run_type.execute(&rustup).arg("update").check_run() + run_type.execute(&rustup).arg("update").status_checked() } pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> { @@ -181,8 +181,8 @@ pub fn run_choosenim(ctx: &ExecutionContext) -> Result<()> { print_separator("choosenim"); let run_type = ctx.run_type(); - run_type.execute(&choosenim).args(["update", "self"]).check_run()?; - run_type.execute(&choosenim).args(["update", "stable"]).check_run() + run_type.execute(&choosenim).args(["update", "self"]).status_checked()?; + run_type.execute(&choosenim).args(["update", "stable"]).status_checked() } pub fn run_krew_upgrade(run_type: RunType) -> Result<()> { @@ -190,7 +190,7 @@ pub fn run_krew_upgrade(run_type: RunType) -> Result<()> { print_separator("Krew"); - run_type.execute(krew).args(["upgrade"]).check_run() + run_type.execute(krew).args(["upgrade"]).status_checked() } pub fn run_gcloud_components_update(run_type: RunType) -> Result<()> { @@ -204,7 +204,7 @@ pub fn run_gcloud_components_update(run_type: RunType) -> Result<()> { run_type .execute(gcloud) .args(["components", "update", "--quiet"]) - .check_run() + .status_checked() } } @@ -213,7 +213,7 @@ pub fn run_jetpack(run_type: RunType) -> Result<()> { print_separator("Jetpack"); - run_type.execute(jetpack).args(["global", "update"]).check_run() + run_type.execute(jetpack).args(["global", "update"]).status_checked() } pub fn run_rtcl(ctx: &ExecutionContext) -> Result<()> { @@ -221,7 +221,7 @@ pub fn run_rtcl(ctx: &ExecutionContext) -> Result<()> { print_separator("rtcl"); - ctx.run_type().execute(rupdate).check_run() + ctx.run_type().execute(rupdate).status_checked() } pub fn run_opam_update(ctx: &ExecutionContext) -> Result<()> { @@ -229,11 +229,11 @@ pub fn run_opam_update(ctx: &ExecutionContext) -> Result<()> { print_separator("OCaml Package Manager"); - ctx.run_type().execute(&opam).arg("update").check_run()?; - ctx.run_type().execute(&opam).arg("upgrade").check_run()?; + ctx.run_type().execute(&opam).arg("update").status_checked()?; + ctx.run_type().execute(&opam).arg("upgrade").status_checked()?; if ctx.config().cleanup() { - ctx.run_type().execute(&opam).arg("clean").check_run()?; + ctx.run_type().execute(&opam).arg("clean").status_checked()?; } Ok(()) @@ -243,14 +243,17 @@ pub fn run_vcpkg_update(run_type: RunType) -> Result<()> { let vcpkg = utils::require("vcpkg")?; print_separator("vcpkg"); - run_type.execute(vcpkg).args(["upgrade", "--no-dry-run"]).check_run() + run_type + .execute(vcpkg) + .args(["upgrade", "--no-dry-run"]) + .status_checked() } pub fn run_pipx_update(run_type: RunType) -> Result<()> { let pipx = utils::require("pipx")?; print_separator("pipx"); - run_type.execute(pipx).arg("upgrade-all").check_run() + run_type.execute(pipx).arg("upgrade-all").status_checked() } pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> { @@ -258,10 +261,9 @@ pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> { let output = Command::new("conda") .args(["config", "--show", "auto_activate_base"]) - .output()?; - let string_output = String::from_utf8(output.stdout)?; - debug!("Conda output: {}", string_output); - if string_output.contains("False") { + .output_checked_utf8()?; + debug!("Conda output: {}", output.stdout); + if output.stdout.contains("False") { return Err(SkipStep("auto_activate_base is set to False".to_string()).into()); } @@ -270,14 +272,14 @@ pub fn run_conda_update(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(conda) .args(["update", "--all", "-y"]) - .check_run() + .status_checked() } pub fn run_pip3_update(run_type: RunType) -> Result<()> { let python3 = utils::require("python3")?; Command::new(&python3) .args(["-m", "pip"]) - .check_output() + .output_checked_utf8() .map_err(|_| SkipStep("pip does not exists".to_string()))?; print_separator("pip3"); @@ -289,7 +291,7 @@ pub fn run_pip3_update(run_type: RunType) -> Result<()> { run_type .execute(&python3) .args(["-m", "pip", "install", "--upgrade", "--user", "pip"]) - .check_run() + .status_checked() } pub fn run_stack_update(run_type: RunType) -> Result<()> { @@ -303,14 +305,14 @@ pub fn run_stack_update(run_type: RunType) -> Result<()> { let stack = utils::require("stack")?; print_separator("stack"); - run_type.execute(stack).arg("upgrade").check_run() + run_type.execute(stack).arg("upgrade").status_checked() } pub fn run_ghcup_update(run_type: RunType) -> Result<()> { let ghcup = utils::require("ghcup")?; print_separator("ghcup"); - run_type.execute(ghcup).arg("upgrade").check_run() + run_type.execute(ghcup).arg("upgrade").status_checked() } pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> { @@ -326,13 +328,11 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> { let kpsewhich = utils::require("kpsewhich")?; let tlmgr_directory = { let mut d = PathBuf::from( - std::str::from_utf8( - &Command::new(kpsewhich) - .arg("-var-value=SELFAUTOPARENT") - .output()? - .stdout, - )? - .trim(), + &Command::new(kpsewhich) + .arg("-var-value=SELFAUTOPARENT") + .output_checked_utf8()? + .stdout + .trim(), ); d.push("tlpkg"); d @@ -355,7 +355,7 @@ pub fn run_tlmgr_update(ctx: &ExecutionContext) -> Result<()> { }; command.args(["update", "--self", "--all"]); - command.check_run() + command.status_checked() } pub fn run_chezmoi_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -364,7 +364,7 @@ pub fn run_chezmoi_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> print_separator("chezmoi"); - run_type.execute(chezmoi).arg("update").check_run() + run_type.execute(chezmoi).arg("update").status_checked() } pub fn run_myrepos_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -378,27 +378,27 @@ pub fn run_myrepos_update(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> .arg("--directory") .arg(base_dirs.home_dir()) .arg("checkout") - .check_run()?; + .status_checked()?; run_type .execute(&myrepos) .arg("--directory") .arg(base_dirs.home_dir()) .arg("update") - .check_run() + .status_checked() } pub fn run_custom_command(name: &str, command: &str, ctx: &ExecutionContext) -> Result<()> { print_separator(name); - ctx.run_type().execute(shell()).arg("-c").arg(command).check_run() + ctx.run_type().execute(shell()).arg("-c").arg(command).status_checked() } pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> { let composer = utils::require("composer")?; let composer_home = Command::new(&composer) .args(["global", "config", "--absolute", "--quiet", "home"]) - .check_output() + .output_checked_utf8() .map_err(|e| (SkipStep(format!("Error getting the composer directory: {}", e)))) - .map(|s| PathBuf::from(s.trim()))? + .map(|s| PathBuf::from(s.stdout.trim()))? .require()?; if !composer_home.is_descendant_of(ctx.base_dirs().home_dir()) { @@ -425,26 +425,22 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> { .execute(ctx.sudo().as_ref().unwrap()) .arg(&composer) .arg("self-update") - .check_run()?; + .status_checked()?; } } else { - ctx.run_type().execute(&composer).arg("self-update").check_run()?; + ctx.run_type().execute(&composer).arg("self-update").status_checked()?; } } } - let output = Command::new(&composer).args(["global", "update"]).output()?; - let status = output.status; - if !status.success() { - return Err(TopgradeError::ProcessFailed(status).into()); - } - let stdout = String::from_utf8(output.stdout)?; - let stderr = String::from_utf8(output.stderr)?; - print!("{}\n{}", stdout, stderr); - - if stdout.contains("valet") || stderr.contains("valet") { - if let Some(valet) = utils::which("valet") { - ctx.run_type().execute(valet).arg("install").check_run()?; + let output = ctx.run_type().execute(&composer).args(["global", "update"]).output()?; + if let ExecutorOutput::Wet(output) = output { + let output: Utf8Output = output.try_into()?; + print!("{}\n{}", output.stdout, output.stderr); + if output.stdout.contains("valet") || output.stderr.contains("valet") { + if let Some(valet) = utils::which("valet") { + ctx.run_type().execute(valet).arg("install").status_checked()?; + } } } @@ -454,18 +450,15 @@ pub fn run_composer_update(ctx: &ExecutionContext) -> Result<()> { pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> { let dotnet = utils::require("dotnet")?; - let output = Command::new(dotnet).args(["tool", "list", "--global"]).output()?; + let output = Command::new(dotnet) + .args(["tool", "list", "--global"]) + .output_checked_utf8()?; - if !output.status.success() { - return Err(SkipStep(format!("dotnet failed with exit code {:?}", output.status)).into()); - } - - let output = String::from_utf8(output.stdout)?; - if !output.starts_with("Package Id") { + if !output.stdout.starts_with("Package Id") { return Err(SkipStep(String::from("dotnet did not output packages")).into()); } - let mut packages = output.split('\n').skip(2).filter(|line| !line.is_empty()).peekable(); + let mut packages = output.stdout.lines().skip(2).filter(|line| !line.is_empty()).peekable(); if packages.peek().is_none() { return Err(SkipStep(String::from("No dotnet global tools installed")).into()); @@ -478,7 +471,8 @@ pub fn run_dotnet_upgrade(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute("dotnet") .args(["tool", "update", package_name, "--global"]) - .check_run()?; + .status_checked() + .with_context(|| format!("Failed to update .NET package {package_name}"))?; } Ok(()) @@ -489,26 +483,26 @@ pub fn run_raco_update(run_type: RunType) -> Result<()> { print_separator("Racket Package Manager"); - run_type.execute(raco).args(["pkg", "update", "--all"]).check_run() + run_type.execute(raco).args(["pkg", "update", "--all"]).status_checked() } pub fn bin_update(ctx: &ExecutionContext) -> Result<()> { let bin = utils::require("bin")?; print_separator("Bin"); - ctx.run_type().execute(bin).arg("update").check_run() + ctx.run_type().execute(bin).arg("update").status_checked() } pub fn spicetify_upgrade(ctx: &ExecutionContext) -> Result<()> { let spicetify = utils::require("spicetify")?; print_separator("Spicetify"); - ctx.run_type().execute(spicetify).arg("upgrade").check_run() + ctx.run_type().execute(spicetify).arg("upgrade").status_checked() } pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> { let gh = utils::require("gh")?; - let result = Command::new(&gh).args(["extensions", "list"]).check_output(); + let result = Command::new(&gh).args(["extensions", "list"]).output_checked_utf8(); if result.is_err() { debug!("GH result {:?}", result); return Err(SkipStep(String::from("GH failed")).into()); @@ -518,7 +512,7 @@ pub fn run_ghcli_extensions_upgrade(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(&gh) .args(["extension", "upgrade", "--all"]) - .check_run() + .status_checked() } pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> { @@ -529,5 +523,5 @@ pub fn update_julia_packages(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(julia) .args(["-e", "using Pkg; Pkg.update()"]) - .check_run() + .status_checked() } diff --git a/src/steps/git.rs b/src/steps/git.rs index 190b3552..4bd0cbad 100644 --- a/src/steps/git.rs +++ b/src/steps/git.rs @@ -3,17 +3,18 @@ use std::io; use std::path::{Path, PathBuf}; use std::process::{Command, Output, Stdio}; -use anyhow::{anyhow, Result}; +use color_eyre::eyre::{eyre, Result}; use console::style; use futures::stream::{iter, FuturesUnordered}; use futures::StreamExt; use glob::{glob_with, MatchOptions}; -use log::{debug, error}; use tokio::process::Command as AsyncCommand; use tokio::runtime; +use tracing::{debug, error}; +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, RunType}; +use crate::executor::RunType; use crate::terminal::print_separator; use crate::utils::{which, PathExt}; use crate::{error::SkipStep, terminal::print_warning}; @@ -33,10 +34,10 @@ pub struct Repositories<'a> { bad_patterns: Vec, } -fn check_output(output: Output) -> Result<()> { +fn output_checked_utf8(output: Output) -> Result<()> { if !(output.status.success()) { let stderr = String::from_utf8(output.stderr).unwrap(); - Err(anyhow!(stderr)) + Err(eyre!(stderr)) } else { Ok(()) } @@ -66,7 +67,7 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) - .stdin(Stdio::null()) .output() .await?; - let result = check_output(pull_output).and_then(|_| check_output(submodule_output)); + let result = output_checked_utf8(pull_output).and_then(|_| output_checked_utf8(submodule_output)); if let Err(message) = &result { println!("{} pulling {}", style("Failed").red().bold(), &repo); @@ -88,10 +89,7 @@ async fn pull_repository(repo: String, git: &Path, ctx: &ExecutionContext<'_>) - "--oneline", &format!("{}..{}", before, after), ]) - .spawn() - .unwrap() - .wait() - .unwrap(); + .status_checked()?; println!(); } _ => { @@ -108,8 +106,8 @@ fn get_head_revision(git: &Path, repo: &str) -> Option { .stdin(Stdio::null()) .current_dir(repo) .args(["rev-parse", "HEAD"]) - .check_output() - .map(|output| output.trim().to_string()) + .output_checked_utf8() + .map(|output| output.stdout.trim().to_string()) .map_err(|e| { error!("Error getting revision for {}: {}", repo, e); @@ -123,8 +121,8 @@ fn has_remotes(git: &Path, repo: &str) -> Option { .stdin(Stdio::null()) .current_dir(repo) .args(["remote", "show"]) - .check_output() - .map(|output| output.lines().count() > 0) + .output_checked_utf8() + .map(|output| output.stdout.lines().count() > 0) .map_err(|e| { error!("Error getting remotes for {}: {}", repo, e); e @@ -166,9 +164,9 @@ impl Git { .stdin(Stdio::null()) .current_dir(path) .args(["rev-parse", "--show-toplevel"]) - .check_output() + .output_checked_utf8() .ok() - .map(|output| output.trim().to_string()); + .map(|output| output.stdout.trim().to_string()); return output; } } diff --git a/src/steps/go.rs b/src/steps/go.rs new file mode 100644 index 00000000..a80ee032 --- /dev/null +++ b/src/steps/go.rs @@ -0,0 +1,45 @@ +use std::path::PathBuf; +use std::process::Command; + +use color_eyre::eyre::Result; + +use crate::command::CommandExt; +use crate::executor::RunType; +use crate::terminal::print_separator; +use crate::utils; +use crate::utils::PathExt; + +/// +pub fn run_go_global_update(run_type: RunType) -> Result<()> { + let go_global_update = require_go_bin("go-global-update")?; + + print_separator("go-global-update"); + + run_type.execute(go_global_update).status_checked() +} + +/// +pub fn run_go_gup(run_type: RunType) -> Result<()> { + let gup = require_go_bin("gup")?; + + print_separator("gup"); + + run_type.execute(gup).arg("update").status_checked() +} + +/// Get the path of a Go binary. +fn require_go_bin(name: &str) -> Result { + utils::require(name).or_else(|_| { + let go = utils::require("go")?; + // TODO: Does this work? `go help gopath` says that: + // > The GOPATH environment variable lists places to look for Go code. + // > On Unix, the value is a colon-separated string. + // > On Windows, the value is a semicolon-separated string. + // > On Plan 9, the value is a list. + // Should we also fallback to the env variable? + let gopath_output = Command::new(go).args(["env", "GOPATH"]).output_checked_utf8()?; + let gopath = gopath_output.stdout.trim(); + + PathBuf::from(gopath).join("bin").join(name).require() + }) +} diff --git a/src/steps/kakoune.rs b/src/steps/kakoune.rs index d2b5014b..d1b955aa 100644 --- a/src/steps/kakoune.rs +++ b/src/steps/kakoune.rs @@ -1,10 +1,8 @@ -use crate::error::TopgradeError; use crate::terminal::print_separator; use crate::utils::require; -use anyhow::Result; +use color_eyre::eyre::Result; use crate::execution_context::ExecutionContext; -use crate::executor::ExecutorOutput; const UPGRADE_KAK: &str = include_str!("upgrade.kak"); @@ -13,19 +11,13 @@ pub fn upgrade_kak_plug(ctx: &ExecutionContext) -> Result<()> { print_separator("Kakoune"); - let mut command = ctx.run_type().execute(kak); - command.args(["-ui", "dummy", "-e", UPGRADE_KAK]); + // TODO: Why supress output for this command? + ctx.run_type() + .execute(kak) + .args(["-ui", "dummy", "-e", UPGRADE_KAK]) + .output()?; - let output = command.output()?; - - if let ExecutorOutput::Wet(output) = output { - let status = output.status; - if !status.success() { - return Err(TopgradeError::ProcessFailed(status).into()); - } else { - println!("Plugins upgraded") - } - } + println!("Plugins upgraded"); Ok(()) } diff --git a/src/steps/mod.rs b/src/steps/mod.rs index b1b7e9ec..9255dd81 100644 --- a/src/steps/mod.rs +++ b/src/steps/mod.rs @@ -2,6 +2,7 @@ pub mod containers; pub mod emacs; pub mod generic; pub mod git; +pub mod go; pub mod kakoune; pub mod node; pub mod os; diff --git a/src/steps/node.rs b/src/steps/node.rs index c702076a..4909867b 100644 --- a/src/steps/node.rs +++ b/src/steps/node.rs @@ -1,20 +1,20 @@ -#![allow(unused_imports)] - use std::fmt::Display; -#[cfg(unix)] -use std::os::unix::prelude::MetadataExt; +#[cfg(target_os = "linux")] +use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::process::Command; -use anyhow::Result; -use directories::BaseDirs; -use log::debug; -#[cfg(unix)] +use crate::utils::require_option; +use color_eyre::eyre::Result; +#[cfg(target_os = "linux")] use nix::unistd::Uid; use semver::Version; +use tracing::debug; -use crate::executor::{CommandExt, RunType}; +use crate::command::CommandExt; +use crate::executor::RunType; use crate::terminal::print_separator; +use crate::utils::sudo; use crate::utils::{require, PathExt}; use crate::{error::SkipStep, execution_context::ExecutionContext}; @@ -24,13 +24,6 @@ enum NPMVariant { } impl NPMVariant { - const fn long_name(&self) -> &str { - match self { - NPMVariant::Npm => "Node Package Manager", - NPMVariant::Pnpm => "PNPM", - } - } - const fn short_name(&self) -> &str { match self { NPMVariant::Npm => "npm", @@ -85,25 +78,26 @@ impl NPM { let args = ["root", self.global_location_arg()]; Command::new(&self.command) .args(args) - .check_output() - .map(|s| PathBuf::from(s.trim())) + .output_checked_utf8() + .map(|s| PathBuf::from(s.stdout.trim())) } fn version(&self) -> Result { let version_str = Command::new(&self.command) .args(["--version"]) - .check_output() - .map(|s| s.trim().to_owned()); + .output_checked_utf8() + .map(|s| s.stdout.trim().to_owned()); Version::parse(&version_str?).map_err(|err| err.into()) } fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { - print_separator(self.variant.long_name()); let args = ["update", self.global_location_arg()]; if use_sudo { - run_type.execute("sudo").args(args).check_run()?; + let sudo_option = sudo(); + let sudo = require_option(sudo_option, String::from("sudo is not installed"))?; + run_type.execute(sudo).arg(&self.command).args(args).status_checked()?; } else { - run_type.execute(&self.command).args(args).check_run()?; + run_type.execute(&self.command).args(args).status_checked()?; } Ok(()) @@ -142,9 +136,9 @@ impl Yarn { // // As “yarn dlx” don't need to “upgrade”, we // ignore the whole task if Yarn is 2.x or above. - let version = Command::new(&self.command).args(["--version"]).check_output(); + let version = Command::new(&self.command).args(["--version"]).output_checked_utf8(); - matches!(version, Ok(ver) if ver.starts_with('1') || ver.starts_with('0')) + matches!(version, Ok(ver) if ver.stdout.starts_with('1') || ver.stdout.starts_with('0')) } #[cfg(target_os = "linux")] @@ -152,12 +146,11 @@ impl Yarn { let args = ["global", "dir"]; Command::new(&self.command) .args(args) - .check_output() - .map(|s| PathBuf::from(s.trim())) + .output_checked_utf8() + .map(|s| PathBuf::from(s.stdout.trim())) } fn upgrade(&self, run_type: RunType, use_sudo: bool) -> Result<()> { - print_separator("Yarn Package Manager"); let args = ["global", "upgrade"]; if use_sudo { @@ -165,9 +158,9 @@ impl Yarn { .execute("sudo") .arg(self.yarn.as_ref().unwrap_or(&self.command)) .args(args) - .check_run()?; + .status_checked()?; } else { - run_type.execute(&self.command).args(args).check_run()?; + run_type.execute(&self.command).args(args).status_checked()?; } Ok(()) @@ -218,6 +211,8 @@ fn should_use_sudo_yarn(yarn: &Yarn, ctx: &ExecutionContext) -> Result { pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> { let npm = require("npm").map(|b| NPM::new(b, NPMVariant::Npm))?; + print_separator("Node Package Manager"); + #[cfg(target_os = "linux")] { npm.upgrade(ctx.run_type(), should_use_sudo(&npm, ctx)?) @@ -232,6 +227,8 @@ pub fn run_npm_upgrade(ctx: &ExecutionContext) -> Result<()> { pub fn run_pnpm_upgrade(ctx: &ExecutionContext) -> Result<()> { let pnpm = require("pnpm").map(|b| NPM::new(b, NPMVariant::Pnpm))?; + print_separator("Node Package Manager"); + #[cfg(target_os = "linux")] { pnpm.upgrade(ctx.run_type(), should_use_sudo(&pnpm, ctx)?) @@ -251,6 +248,8 @@ pub fn run_yarn_upgrade(ctx: &ExecutionContext) -> Result<()> { return Ok(()); } + print_separator("Yarn Package Manager"); + #[cfg(target_os = "linux")] { yarn.upgrade(ctx.run_type(), should_use_sudo_yarn(&yarn, ctx)?) @@ -272,5 +271,5 @@ pub fn deno_upgrade(ctx: &ExecutionContext) -> Result<()> { } print_separator("Deno"); - ctx.run_type().execute(&deno).arg("upgrade").check_run() + ctx.run_type().execute(&deno).arg("upgrade").status_checked() } diff --git a/src/steps/os/android.rs b/src/steps/os/android.rs index e6706f9c..76ed5468 100644 --- a/src/steps/os/android.rs +++ b/src/steps/os/android.rs @@ -1,8 +1,10 @@ +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; use crate::terminal::print_separator; use crate::utils::require; +use crate::utils::which; use crate::Step; -use anyhow::Result; +use color_eyre::eyre::Result; pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { //let pkg = require("pkg")?; @@ -10,7 +12,7 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { print_separator("Termux Packages"); - let is_nala = pkg.end_with("nala"); + let is_nala = pkg.ends_with("nala"); let mut command = ctx.run_type().execute(&pkg); command.arg("upgrade"); @@ -18,20 +20,18 @@ pub fn upgrade_packages(ctx: &ExecutionContext) -> Result<()> { if ctx.config().yes(Step::System) { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; - if !is_nala { - if ctx.config().cleanup() { - ctx.run_type().execute(&pkg).arg("clean").check_run()?; + if !is_nala && ctx.config().cleanup() { + ctx.run_type().execute(&pkg).arg("clean").status_checked()?; - let apt = require("apt")?; - let mut command = ctx.run_type().execute(&apt); - command.arg("autoremove"); - if ctx.config().yes(Step::System) { - command.arg("-y"); - } - command.check_run()?; + let apt = require("apt")?; + let mut command = ctx.run_type().execute(&apt); + command.arg("autoremove"); + if ctx.config().yes(Step::System) { + command.arg("-y"); } + command.status_checked()?; } Ok(()) diff --git a/src/steps/os/archlinux.rs b/src/steps/os/archlinux.rs index 892a8994..7c4e938c 100644 --- a/src/steps/os/archlinux.rs +++ b/src/steps/os/archlinux.rs @@ -3,9 +3,11 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Command; -use anyhow::Result; +use color_eyre::eyre; +use color_eyre::eyre::Result; use walkdir::WalkDir; +use crate::command::CommandExt; use crate::error::TopgradeError; use crate::execution_context::ExecutionContext; use crate::utils::which; @@ -29,11 +31,7 @@ pub struct YayParu { impl ArchPackageManager for YayParu { fn upgrade(&self, ctx: &ExecutionContext) -> Result<()> { if ctx.config().show_arch_news() { - Command::new(&self.executable) - .arg("-Pw") - .spawn() - .and_then(|mut p| p.wait()) - .ok(); + Command::new(&self.executable).arg("-Pw").status_checked()?; } let mut command = ctx.run_type().execute(&self.executable); @@ -48,7 +46,7 @@ impl ArchPackageManager for YayParu { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; if ctx.config().cleanup() { let mut command = ctx.run_type().execute(&self.executable); @@ -56,7 +54,7 @@ impl ArchPackageManager for YayParu { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; } Ok(()) @@ -88,7 +86,7 @@ impl ArchPackageManager for Trizen { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; if ctx.config().cleanup() { let mut command = ctx.run_type().execute(&self.executable); @@ -96,7 +94,7 @@ impl ArchPackageManager for Trizen { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; } Ok(()) @@ -126,7 +124,7 @@ impl ArchPackageManager for Pacman { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; if ctx.config().cleanup() { let mut command = ctx.run_type().execute(&self.sudo); @@ -134,7 +132,7 @@ impl ArchPackageManager for Pacman { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; } Ok(()) @@ -175,7 +173,7 @@ impl ArchPackageManager for Pikaur { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; if ctx.config().cleanup() { let mut command = ctx.run_type().execute(&self.executable); @@ -183,7 +181,7 @@ impl ArchPackageManager for Pikaur { if ctx.config().yes(Step::System) { command.arg("--noconfirm"); } - command.check_run()?; + command.status_checked()?; } Ok(()) @@ -214,7 +212,7 @@ impl ArchPackageManager for Pamac { command.arg("--no-confirm"); } - command.check_run()?; + command.status_checked()?; if ctx.config().cleanup() { let mut command = ctx.run_type().execute(&self.executable); @@ -222,7 +220,7 @@ impl ArchPackageManager for Pamac { if ctx.config().yes(Step::System) { command.arg("--no-confirm"); } - command.check_run()?; + command.status_checked()?; } Ok(()) @@ -257,7 +255,7 @@ impl ArchPackageManager for Aura { aur_update.arg("--noconfirm"); } - aur_update.check_run()?; + aur_update.status_checked()?; } else { println!("Aura requires sudo installed to work with AUR packages") } @@ -270,7 +268,7 @@ impl ArchPackageManager for Aura { if ctx.config().yes(Step::System) { pacman_update.arg("--noconfirm"); } - pacman_update.check_run()?; + pacman_update.status_checked()?; Ok(()) } @@ -304,7 +302,7 @@ pub fn get_arch_package_manager(ctx: &ExecutionContext) -> Option Result<()> { let package_manager = - get_arch_package_manager(ctx).ok_or_else(|| anyhow::Error::from(TopgradeError::FailedGettingPackageManager))?; + get_arch_package_manager(ctx).ok_or_else(|| eyre::Report::from(TopgradeError::FailedGettingPackageManager))?; package_manager.upgrade(ctx) } diff --git a/src/steps/os/dragonfly.rs b/src/steps/os/dragonfly.rs index 79003263..b314a47f 100644 --- a/src/steps/os/dragonfly.rs +++ b/src/steps/os/dragonfly.rs @@ -1,26 +1,26 @@ +use crate::command::CommandExt; use crate::executor::RunType; use crate::terminal::print_separator; use crate::utils::require_option; -use anyhow::Result; +use color_eyre::eyre::Result; use std::path::PathBuf; use std::process::Command; pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; - print_separator("DrgaonFly BSD Packages"); + print_separator("DragonFly BSD Packages"); run_type .execute(sudo) - .args(&["/usr/local/sbin/pkg", "upgrade"]) - .check_run() + .args(["/usr/local/sbin/pkg", "upgrade"]) + .status_checked() } pub fn audit_packages(sudo: &Option) -> Result<()> { if let Some(sudo) = sudo { println!(); Command::new(sudo) - .args(&["/usr/local/sbin/pkg", "audit", "-Fr"]) - .spawn()? - .wait()?; + .args(["/usr/local/sbin/pkg", "audit", "-Fr"]) + .status_checked()?; } Ok(()) } diff --git a/src/steps/os/freebsd.rs b/src/steps/os/freebsd.rs index bdd6ba76..8109057a 100644 --- a/src/steps/os/freebsd.rs +++ b/src/steps/os/freebsd.rs @@ -1,7 +1,10 @@ +use crate::command::CommandExt; +use crate::execution_context::ExecutionContext; use crate::executor::RunType; use crate::terminal::print_separator; use crate::utils::require_option; -use anyhow::Result; +use crate::Step; +use color_eyre::eyre::Result; use std::path::PathBuf; use std::process::Command; @@ -10,23 +13,29 @@ pub fn upgrade_freebsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> print_separator("FreeBSD Update"); run_type .execute(sudo) - .args(&["/usr/sbin/freebsd-update", "fetch", "install"]) - .check_run() + .args(["/usr/sbin/freebsd-update", "fetch", "install"]) + .status_checked() } -pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { +pub fn upgrade_packages(ctx: &ExecutionContext, sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; print_separator("FreeBSD Packages"); - run_type.execute(sudo).args(&["/usr/sbin/pkg", "upgrade"]).check_run() + + let mut command = run_type.execute(sudo); + + command.args(["/usr/sbin/pkg", "upgrade"]); + if ctx.config().yes(Step::System) { + command.arg("-y"); + } + command.status_checked() } pub fn audit_packages(sudo: &Option) -> Result<()> { if let Some(sudo) = sudo { println!(); Command::new(sudo) - .args(&["/usr/sbin/pkg", "audit", "-Fr"]) - .spawn()? - .wait()?; + .args(["/usr/sbin/pkg", "audit", "-Fr"]) + .status_checked()?; } Ok(()) } diff --git a/src/steps/os/linux.rs b/src/steps/os/linux.rs index 20f791dd..25a40c0a 100644 --- a/src/steps/os/linux.rs +++ b/src/steps/os/linux.rs @@ -1,13 +1,14 @@ use std::path::{Path, PathBuf}; use std::process::Command; -use anyhow::Result; +use color_eyre::eyre::Result; use ini::Ini; -use log::{debug, warn}; +use tracing::{debug, warn}; +use crate::command::CommandExt; use crate::error::{SkipStep, TopgradeError}; use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, RunType}; +use crate::executor::RunType; use crate::steps::os::archlinux; use crate::terminal::{print_separator, print_warning}; use crate::utils::{require, require_option, which, PathExt}; @@ -127,11 +128,10 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> { ctx.run_type().execute(sudo).args(["brl", "update"]); - let output = Command::new("brl").arg("list").output()?; + let output = Command::new("brl").arg("list").output_checked_utf8()?; debug!("brl list: {:?} {:?}", output.stdout, output.stderr); - let parsed_output = String::from_utf8(output.stdout).unwrap(); - for distribution in parsed_output.trim().split('\n') { + for distribution in output.stdout.trim().lines() { debug!("Bedrock distribution {}", distribution); match distribution { "arch" => archlinux::upgrade_arch_linux(ctx)?, @@ -148,7 +148,7 @@ fn update_bedrock(ctx: &ExecutionContext) -> Result<()> { } fn is_wsl() -> Result { - let output = Command::new("uname").arg("-r").check_output()?; + let output = Command::new("uname").arg("-r").output_checked_utf8()?.stdout; debug!("Uname output: {}", output); Ok(output.contains("microsoft")) } @@ -157,8 +157,8 @@ fn upgrade_alpine_linux(ctx: &ExecutionContext) -> Result<()> { let apk = require("apk")?; let sudo = ctx.sudo().as_ref().unwrap(); - ctx.run_type().execute(sudo).arg(&apk).arg("update").check_run()?; - ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").check_run() + ctx.run_type().execute(sudo).arg(&apk).arg("update").status_checked()?; + ctx.run_type().execute(sudo).arg(&apk).arg("upgrade").status_checked() } fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> { @@ -166,7 +166,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> { if ctx.config().rpm_ostree() { let mut command = ctx.run_type().execute(ostree); command.arg("upgrade"); - return command.check_run(); + return command.status_checked(); } }; @@ -188,7 +188,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -198,7 +198,7 @@ fn upgrade_redhat(ctx: &ExecutionContext) -> Result<()> { fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = ctx.sudo() { - ctx.run_type().execute(sudo).args(["brl", "update"]).check_run()?; + ctx.run_type().execute(sudo).args(["brl", "update"]).status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -208,12 +208,15 @@ fn upgrade_bedrock_strata(ctx: &ExecutionContext) -> Result<()> { fn upgrade_suse(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = ctx.sudo() { - ctx.run_type().execute(sudo).args(["zypper", "refresh"]).check_run()?; + ctx.run_type() + .execute(sudo) + .args(["zypper", "refresh"]) + .status_checked()?; ctx.run_type() .execute(sudo) .args(["zypper", "dist-upgrade"]) - .check_run()?; + .status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -235,7 +238,7 @@ fn upgrade_openmandriva(ctx: &ExecutionContext) -> Result<()> { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -250,14 +253,14 @@ fn upgrade_void(ctx: &ExecutionContext) -> Result<()> { if ctx.config().yes(Step::System) { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; let mut command = ctx.run_type().execute(sudo); command.args(["xbps-install", "-u"]); if ctx.config().yes(Step::System) { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -270,7 +273,11 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = &ctx.sudo() { if let Some(layman) = which("layman") { - run_type.execute(sudo).arg(layman).args(["-s", "ALL"]).check_run()?; + run_type + .execute(sudo) + .arg(layman) + .args(["-s", "ALL"]) + .status_checked()?; } println!("Syncing portage"); @@ -283,10 +290,10 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> { .map(|s| s.split_whitespace().collect()) .unwrap_or_else(|| vec!["-q"]), ) - .check_run()?; + .status_checked()?; if let Some(eix_update) = which("eix-update") { - run_type.execute(sudo).arg(eix_update).check_run()?; + run_type.execute(sudo).arg(eix_update).status_checked()?; } run_type @@ -298,7 +305,7 @@ fn upgrade_gentoo(ctx: &ExecutionContext) -> Result<()> { .map(|s| s.split_whitespace().collect()) .unwrap_or_else(|| vec!["-uDNa", "--with-bdeps=y", "world"]), ) - .check_run()?; + .status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -314,7 +321,7 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> { let is_nala = apt.ends_with("nala"); if !is_nala { - ctx.run_type().execute(sudo).arg(&apt).arg("update").check_run()?; + ctx.run_type().execute(sudo).arg(&apt).arg("update").status_checked()?; } let mut command = ctx.run_type().execute(sudo); @@ -330,17 +337,17 @@ fn upgrade_debian(ctx: &ExecutionContext) -> Result<()> { if let Some(args) = ctx.config().apt_arguments() { command.args(args.split_whitespace()); } - command.check_run()?; + command.status_checked()?; if ctx.config().cleanup() { - ctx.run_type().execute(sudo).arg(&apt).arg("clean").check_run()?; + ctx.run_type().execute(sudo).arg(&apt).arg("clean").status_checked()?; let mut command = ctx.run_type().execute(sudo); command.arg(&apt).arg("autoremove"); if ctx.config().yes(Step::System) { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; } } else { print_warning("No sudo detected. Skipping system upgrade"); @@ -354,11 +361,11 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> { print_separator("deb-get"); - ctx.execute_elevated(&deb_get, false)?.arg("update").check_run()?; - ctx.execute_elevated(&deb_get, false)?.arg("upgrade").check_run()?; + ctx.execute_elevated(&deb_get, false)?.arg("update").status_checked()?; + ctx.execute_elevated(&deb_get, false)?.arg("upgrade").status_checked()?; if ctx.config().cleanup() { - ctx.execute_elevated(&deb_get, false)?.arg("clean").check_run()?; + ctx.execute_elevated(&deb_get, false)?.arg("clean").status_checked()?; } Ok(()) @@ -366,7 +373,10 @@ pub fn run_deb_get(ctx: &ExecutionContext) -> Result<()> { fn upgrade_solus(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = ctx.sudo() { - ctx.run_type().execute(sudo).args(["eopkg", "upgrade"]).check_run()?; + ctx.run_type() + .execute(sudo) + .args(["eopkg", "upgrade"]) + .status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -379,10 +389,10 @@ pub fn run_pacdef(ctx: &ExecutionContext) -> Result<()> { print_separator("pacdef"); - ctx.run_type().execute(&pacdef).arg("sync").check_run()?; + ctx.run_type().execute(&pacdef).arg("sync").status_checked()?; println!(); - ctx.run_type().execute(&pacdef).arg("review").check_run() + ctx.run_type().execute(&pacdef).arg("review").status_checked() } pub fn run_pacstall(ctx: &ExecutionContext) -> Result<()> { @@ -390,13 +400,16 @@ pub fn run_pacstall(ctx: &ExecutionContext) -> Result<()> { print_separator("Pacstall"); - ctx.run_type().execute(&pacstall).arg("-U").check_run()?; - ctx.run_type().execute(pacstall).arg("-Up").check_run() + ctx.run_type().execute(&pacstall).arg("-U").status_checked()?; + ctx.run_type().execute(pacstall).arg("-Up").status_checked() } fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = &ctx.sudo() { - ctx.run_type().execute(sudo).args(["swupd", "update"]).check_run()?; + ctx.run_type() + .execute(sudo) + .args(["swupd", "update"]) + .status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -406,26 +419,29 @@ fn upgrade_clearlinux(ctx: &ExecutionContext) -> Result<()> { fn upgrade_exherbo(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = ctx.sudo() { - ctx.run_type().execute(sudo).args(["cave", "sync"]).check_run()?; + ctx.run_type().execute(sudo).args(["cave", "sync"]).status_checked()?; ctx.run_type() .execute(sudo) .args(["cave", "resolve", "world", "-c1", "-Cs", "-km", "-Km", "-x"]) - .check_run()?; + .status_checked()?; if ctx.config().cleanup() { - ctx.run_type().execute(sudo).args(["cave", "purge", "-x"]).check_run()?; + ctx.run_type() + .execute(sudo) + .args(["cave", "purge", "-x"]) + .status_checked()?; } ctx.run_type() .execute(sudo) .args(["cave", "fix-linkage", "-x", "--", "-Cs"]) - .check_run()?; + .status_checked()?; ctx.run_type() .execute(sudo) .args(["eclectic", "config", "interactive"]) - .check_run()?; + .status_checked()?; } else { print_warning("No sudo detected. Skipping system upgrade"); } @@ -438,13 +454,13 @@ fn upgrade_nixos(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(sudo) .args(["/run/current-system/sw/bin/nixos-rebuild", "switch", "--upgrade"]) - .check_run()?; + .status_checked()?; if ctx.config().cleanup() { ctx.run_type() .execute(sudo) .args(["/run/current-system/sw/bin/nix-collect-garbage", "-d"]) - .check_run()?; + .status_checked()?; } } else { print_warning("No sudo detected. Skipping system upgrade"); @@ -462,7 +478,11 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> { if let Some(sudo) = &ctx.sudo() { let pkcon = which("pkcon").unwrap(); // pkcon ignores update with update and refresh provided together - ctx.run_type().execute(sudo).arg(&pkcon).arg("refresh").check_run()?; + ctx.run_type() + .execute(sudo) + .arg(&pkcon) + .arg("refresh") + .status_checked()?; let mut exe = ctx.run_type().execute(sudo); let cmd = exe.arg(&pkcon).arg("update"); if ctx.config().yes(Step::System) { @@ -472,7 +492,7 @@ fn upgrade_neon(ctx: &ExecutionContext) -> Result<()> { cmd.arg("--autoremove"); } // from pkcon man, exit code 5 is 'Nothing useful was done.' - cmd.check_run_with_codes(&[5])?; + cmd.status_checked_with_codes(&[5])?; } Ok(()) @@ -489,7 +509,7 @@ pub fn run_needrestart(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> print_separator("Check for needed restarts"); - run_type.execute(sudo).arg(needrestart).check_run()?; + run_type.execute(sudo).arg(needrestart).status_checked()?; Ok(()) } @@ -506,7 +526,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(&fwupdmgr) .arg("refresh") - .check_run_with_codes(&[2])?; + .status_checked_with_codes(&[2])?; let mut updmgr = ctx.run_type().execute(&fwupdmgr); @@ -518,7 +538,7 @@ pub fn run_fwupdmgr(ctx: &ExecutionContext) -> Result<()> { } else { updmgr.arg("get-updates"); } - updmgr.check_run_with_codes(&[2]) + updmgr.status_checked_with_codes(&[2]) } pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> { @@ -533,14 +553,14 @@ pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> { if yes { update_args.push("-y"); } - run_type.execute(&flatpak).args(&update_args).check_run()?; + run_type.execute(&flatpak).args(&update_args).status_checked()?; if cleanup { let mut cleanup_args = vec!["uninstall", "--user", "--unused"]; if yes { cleanup_args.push("-y"); } - run_type.execute(&flatpak).args(&cleanup_args).check_run()?; + run_type.execute(&flatpak).args(&cleanup_args).status_checked()?; } print_separator("Flatpak System Packages"); @@ -549,26 +569,34 @@ pub fn flatpak_update(ctx: &ExecutionContext) -> Result<()> { if yes { update_args.push("-y"); } - run_type.execute(sudo).arg(&flatpak).args(&update_args).check_run()?; + run_type + .execute(sudo) + .arg(&flatpak) + .args(&update_args) + .status_checked()?; if cleanup { let mut cleanup_args = vec!["uninstall", "--system", "--unused"]; if yes { cleanup_args.push("-y"); } - run_type.execute(sudo).arg(flatpak).args(&cleanup_args).check_run()?; + run_type + .execute(sudo) + .arg(flatpak) + .args(&cleanup_args) + .status_checked()?; } } else { let mut update_args = vec!["update", "--system"]; if yes { update_args.push("-y"); } - run_type.execute(&flatpak).args(&update_args).check_run()?; + run_type.execute(&flatpak).args(&update_args).status_checked()?; if cleanup { let mut cleanup_args = vec!["uninstall", "--system", "--unused"]; if yes { cleanup_args.push("-y"); } - run_type.execute(flatpak).args(&cleanup_args).check_run()?; + run_type.execute(flatpak).args(&cleanup_args).status_checked()?; } } @@ -584,7 +612,7 @@ pub fn run_snap(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { } print_separator("snap"); - run_type.execute(sudo).arg(snap).arg("refresh").check_run() + run_type.execute(sudo).arg(snap).arg("refresh").status_checked() } pub fn run_pihole_update(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { @@ -594,7 +622,7 @@ pub fn run_pihole_update(sudo: Option<&PathBuf>, run_type: RunType) -> Result<() print_separator("pihole"); - run_type.execute(sudo).arg(pihole).arg("-up").check_run() + run_type.execute(sudo).arg(pihole).arg("-up").status_checked() } pub fn run_protonup_update(ctx: &ExecutionContext) -> Result<()> { @@ -602,7 +630,7 @@ pub fn run_protonup_update(ctx: &ExecutionContext) -> Result<()> { print_separator("protonup"); - ctx.run_type().execute(protonup).check_run()?; + ctx.run_type().execute(protonup).status_checked()?; Ok(()) } @@ -628,8 +656,7 @@ pub fn run_distrobox_update(ctx: &ExecutionContext) -> Result<()> { (r, true) => r.arg("--root"), (r, false) => r, } - .check_run()?; - Ok(()) + .status_checked() } pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> { @@ -640,14 +667,14 @@ pub fn run_config_update(ctx: &ExecutionContext) -> Result<()> { if let Ok(etc_update) = require("etc-update") { print_separator("Configuration update"); - ctx.run_type().execute(sudo).arg(etc_update).check_run()?; + ctx.run_type().execute(sudo).arg(etc_update).status_checked()?; } else if let Ok(pacdiff) = require("pacdiff") { if std::env::var("DIFFPROG").is_err() { require("vim")?; } print_separator("Configuration update"); - ctx.execute_elevated(&pacdiff, false)?.check_run()?; + ctx.execute_elevated(&pacdiff, false)?.status_checked()?; } Ok(()) diff --git a/src/steps/os/macos.rs b/src/steps/os/macos.rs index dab80d4b..f4265f4b 100644 --- a/src/steps/os/macos.rs +++ b/src/steps/os/macos.rs @@ -1,26 +1,30 @@ +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, RunType}; +use crate::executor::RunType; use crate::terminal::{print_separator, prompt_yesno}; -use crate::{error::TopgradeError, utils::require, Step}; -use anyhow::Result; -use log::debug; +use crate::{utils::require, Step}; +use color_eyre::eyre::Result; use std::fs; use std::process::Command; +use tracing::debug; pub fn run_macports(ctx: &ExecutionContext) -> Result<()> { require("port")?; let sudo = ctx.sudo().as_ref().unwrap(); print_separator("MacPorts"); - ctx.run_type().execute(sudo).args(["port", "selfupdate"]).check_run()?; + ctx.run_type() + .execute(sudo) + .args(["port", "selfupdate"]) + .status_checked()?; ctx.run_type() .execute(sudo) .args(["port", "-u", "upgrade", "outdated"]) - .check_run()?; + .status_checked()?; if ctx.config().cleanup() { ctx.run_type() .execute(sudo) .args(["port", "-N", "reclaim"]) - .check_run()?; + .status_checked()?; } Ok(()) @@ -30,7 +34,7 @@ pub fn run_mas(run_type: RunType) -> Result<()> { let mas = require("mas")?; print_separator("macOS App Store"); - run_type.execute(mas).arg("upgrade").check_run() + run_type.execute(mas).arg("upgrade").status_checked() } pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> { @@ -58,20 +62,15 @@ pub fn upgrade_macos(ctx: &ExecutionContext) -> Result<()> { command.arg("--no-scan"); } - command.check_run() + command.status_checked() } fn system_update_available() -> Result { - let output = Command::new("softwareupdate").arg("--list").output()?; + let output = Command::new("softwareupdate").arg("--list").output_checked_utf8()?; + debug!("{:?}", output); - let status = output.status; - if !status.success() { - return Err(TopgradeError::ProcessFailed(status).into()); - } - let string_output = String::from_utf8(output.stderr)?; - debug!("{:?}", string_output); - Ok(!string_output.contains("No new software available")) + Ok(!output.stderr.contains("No new software available")) } pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> { @@ -83,12 +82,12 @@ pub fn run_sparkle(ctx: &ExecutionContext) -> Result<()> { let probe = Command::new(&sparkle) .args(["--probe", "--application"]) .arg(application.path()) - .check_output(); + .output_checked_utf8(); if probe.is_ok() { let mut command = ctx.run_type().execute(&sparkle); command.args(["bundle", "--check-immediately", "--application"]); command.arg(application.path()); - command.spawn()?.wait()?; + command.status_checked()?; } } Ok(()) diff --git a/src/steps/os/openbsd.rs b/src/steps/os/openbsd.rs index dec83d22..b32c3e1e 100644 --- a/src/steps/os/openbsd.rs +++ b/src/steps/os/openbsd.rs @@ -1,17 +1,23 @@ use crate::executor::RunType; use crate::terminal::print_separator; use crate::utils::require_option; -use anyhow::Result; +use color_eyre::eyre::Result; use std::path::PathBuf; pub fn upgrade_openbsd(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; print_separator("OpenBSD Update"); - run_type.execute(sudo).args(&["/usr/sbin/sysupgrade", "-n"]).check_run() + run_type + .execute(sudo) + .args(&["/usr/sbin/sysupgrade", "-n"]) + .status_checked() } pub fn upgrade_packages(sudo: Option<&PathBuf>, run_type: RunType) -> Result<()> { let sudo = require_option(sudo, String::from("No sudo detected"))?; print_separator("OpenBSD Packages"); - run_type.execute(sudo).args(&["/usr/sbin/pkg_add", "-u"]).check_run() + run_type + .execute(sudo) + .args(&["/usr/sbin/pkg_add", "-u"]) + .status_checked() } diff --git a/src/steps/os/unix.rs b/src/steps/os/unix.rs index 6f20d17d..9f89df5b 100644 --- a/src/steps/os/unix.rs +++ b/src/steps/os/unix.rs @@ -1,33 +1,43 @@ -use crate::error::{SkipStep, TopgradeError}; -use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, Executor, ExecutorExitStatus, RunType}; -use crate::terminal::print_separator; -#[cfg(not(target_os = "macos"))] -use crate::utils::require_option; -use crate::utils::{require, PathExt}; -use crate::Step; -use anyhow::Result; -use directories::BaseDirs; -use home; -use ini::Ini; -use log::debug; use std::fs; use std::os::unix::fs::MetadataExt; use std::path::PathBuf; use std::process::Command; use std::{env, path::Path}; +use crate::command::CommandExt; +use crate::Step; +use color_eyre::eyre::Result; +use directories::BaseDirs; +use home; +use ini::Ini; +use tracing::debug; + +use crate::error::SkipStep; +use crate::execution_context::ExecutionContext; +#[cfg(any(target_os = "linux", target_os = "macos"))] +use crate::executor::Executor; +use crate::executor::RunType; +use crate::terminal::print_separator; +#[cfg(not(any(target_os = "android", target_os = "macos")))] +use crate::utils::require_option; +use crate::utils::{require, PathExt}; + +#[cfg(any(target_os = "linux", target_os = "macos"))] const INTEL_BREW: &str = "/usr/local/bin/brew"; + +#[cfg(any(target_os = "linux", target_os = "macos"))] const ARM_BREW: &str = "/opt/homebrew/bin/brew"; #[derive(Copy, Clone, Debug)] #[allow(dead_code)] +#[cfg(any(target_os = "linux", target_os = "macos"))] pub enum BrewVariant { Path, MacIntel, MacArm, } +#[cfg(any(target_os = "linux", target_os = "macos"))] impl BrewVariant { fn binary_name(self) -> &'static str { match self { @@ -77,30 +87,36 @@ impl BrewVariant { } } -pub fn run_fisher(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { +pub fn run_fisher(run_type: RunType) -> Result<()> { let fish = require("fish")?; - if env::var("fisher_path").is_err() { - base_dirs - .home_dir() - .join(".config/fish/functions/fisher.fish") - .require()?; - } + Command::new(&fish) + .args(["-c", "type -t fisher"]) + .output_checked_utf8() + .map(|_| ()) + .map_err(|_| SkipStep("`fisher` is not defined in `fish`".to_owned()))?; + + Command::new(&fish) + .args(["-c", "echo \"$__fish_config_dir/fish_plugins\""]) + .output_checked_utf8() + .and_then(|output| Path::new(&output.stdout.trim()).require().map(|_| ())) + .map_err(|err| SkipStep(format!("`fish_plugins` path doesn't exist: {err}")))?; print_separator("Fisher"); let version_str = run_type .execute(&fish) .args(["-c", "fisher --version"]) - .check_output()?; + .output_checked_utf8()? + .stdout; debug!("Fisher version: {}", version_str); if version_str.starts_with("fisher version 3.") { // v3 - see https://github.com/topgrade-rs/topgrade/pull/37#issuecomment-1283844506 - run_type.execute(&fish).args(["-c", "fisher"]).check_run() + run_type.execute(&fish).args(["-c", "fisher"]).status_checked() } else { // v4 - run_type.execute(&fish).args(["-c", "fisher update"]).check_run() + run_type.execute(&fish).args(["-c", "fisher update"]).status_checked() } } @@ -112,7 +128,7 @@ pub fn run_bashit(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute("bash") .args(["-lic", &format!("bash-it update {}", ctx.config().bashit_branch())]) - .check_run() + .status_checked() } pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> { @@ -124,7 +140,7 @@ pub fn run_oh_my_fish(ctx: &ExecutionContext) -> Result<()> { print_separator("oh-my-fish"); - ctx.run_type().execute(fish).args(["-c", "omf update"]).check_run() + ctx.run_type().execute(fish).args(["-c", "omf update"]).status_checked() } pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> { @@ -135,14 +151,14 @@ pub fn run_pkgin(ctx: &ExecutionContext) -> Result<()> { if ctx.config().yes(Step::Pkgin) { command.arg("-y"); } - command.check_run()?; + command.status_checked()?; let mut command = ctx.run_type().execute(ctx.sudo().as_ref().unwrap()); command.arg(&pkgin).arg("upgrade"); if ctx.config().yes(Step::Pkgin) { command.arg("-y"); } - command.check_run() + command.status_checked() } pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> { @@ -154,7 +170,10 @@ pub fn run_fish_plug(ctx: &ExecutionContext) -> Result<()> { print_separator("fish-plug"); - ctx.run_type().execute(fish).args(["-c", "plug update"]).check_run() + ctx.run_type() + .execute(fish) + .args(["-c", "plug update"]) + .status_checked() } /// Upgrades `fundle` and `fundle` plugins. @@ -171,7 +190,7 @@ pub fn run_fundle(ctx: &ExecutionContext) -> Result<()> { ctx.run_type() .execute(fish) .args(["-c", "fundle self-update && fundle update"]) - .check_run() + .status_checked() } #[cfg(not(any(target_os = "android", target_os = "macos")))] @@ -192,10 +211,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> { "--method", "org.freedesktop.DBus.ListActivatableNames", ]) - .check_output()?; + .output_checked_utf8()?; debug!("Checking for gnome extensions: {}", output); - if !output.contains("org.gnome.Shell.Extensions") { + if !output.stdout.contains("org.gnome.Shell.Extensions") { return Err(SkipStep(String::from("Gnome shell extensions are unregistered in DBus")).into()); } @@ -213,9 +232,10 @@ pub fn upgrade_gnome_extensions(ctx: &ExecutionContext) -> Result<()> { "--method", "org.gnome.Shell.Extensions.CheckForUpdates", ]) - .check_run() + .status_checked() } +#[cfg(any(target_os = "linux", target_os = "macos"))] pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> { #[allow(unused_variables)] let binary_name = require(variant.binary_name())?; @@ -230,18 +250,18 @@ pub fn run_brew_formula(ctx: &ExecutionContext, variant: BrewVariant) -> Result< print_separator(variant.step_title()); let run_type = ctx.run_type(); - variant.execute(run_type).arg("update").check_run()?; + variant.execute(run_type).arg("update").status_checked()?; variant .execute(run_type) .args(["upgrade", "--ignore-pinned", "--formula"]) - .check_run()?; + .status_checked()?; if ctx.config().cleanup() { - variant.execute(run_type).arg("cleanup").check_run()?; + variant.execute(run_type).arg("cleanup").status_checked()?; } if ctx.config().brew_autoremove() { - variant.execute(run_type).arg("autoremove").check_run()?; + variant.execute(run_type).arg("autoremove").status_checked()?; } Ok(()) @@ -259,8 +279,8 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> let cask_upgrade_exists = variant .execute(RunType::Wet) .args(["--repository", "buo/cask-upgrade"]) - .check_output() - .map(|p| Path::new(p.trim()).exists())?; + .output_checked_utf8() + .map(|p| Path::new(p.stdout.trim()).exists())?; let mut brew_args = vec![]; @@ -276,10 +296,10 @@ pub fn run_brew_cask(ctx: &ExecutionContext, variant: BrewVariant) -> Result<()> } } - variant.execute(run_type).args(&brew_args).check_run()?; + variant.execute(run_type).args(&brew_args).status_checked()?; if ctx.config().cleanup() { - variant.execute(run_type).arg("cleanup").check_run()?; + variant.execute(run_type).arg("cleanup").status_checked()?; } Ok(()) @@ -290,7 +310,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> { let run_type = ctx.run_type(); - let output = Command::new(&guix).arg("pull").check_output(); + let output = Command::new(&guix).arg("pull").output_checked_utf8(); debug!("guix pull output: {:?}", output); let should_upgrade = output.is_ok(); debug!("Can Upgrade Guix: {:?}", should_upgrade); @@ -298,7 +318,7 @@ pub fn run_guix(ctx: &ExecutionContext) -> Result<()> { print_separator("Guix"); if should_upgrade { - return run_type.execute(&guix).args(["package", "-u"]).check_run(); + return run_type.execute(&guix).args(["package", "-u"]).status_checked(); } Err(SkipStep(String::from("Guix Pull Failed, Skipping")).into()) } @@ -314,7 +334,7 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> { debug!("nix profile: {:?}", profile_path); let manifest_json_path = profile_path.join("manifest.json"); - let output = Command::new(&nix_env).args(["--query", "nix"]).check_output(); + let output = Command::new(&nix_env).args(["--query", "nix"]).output_checked_utf8(); debug!("nix-env output: {:?}", output); let should_self_upgrade = output.is_ok(); @@ -346,13 +366,13 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> { if should_self_upgrade { if multi_user { - ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").check_run()?; + ctx.execute_elevated(&nix, true)?.arg("upgrade-nix").status_checked()?; } else { - run_type.execute(&nix).arg("upgrade-nix").check_run()?; + run_type.execute(&nix).arg("upgrade-nix").status_checked()?; } } - run_type.execute(nix_channel).arg("--update").check_run()?; + run_type.execute(nix_channel).arg("--update").status_checked()?; if std::path::Path::new(&manifest_json_path).exists() { run_type @@ -360,9 +380,9 @@ pub fn run_nix(ctx: &ExecutionContext) -> Result<()> { .arg("profile") .arg("upgrade") .arg(".*") - .check_run() + .status_checked() } else { - run_type.execute(&nix_env).arg("--upgrade").check_run() + run_type.execute(&nix_env).arg("--upgrade").status_checked() } } @@ -371,42 +391,40 @@ pub fn run_yadm(ctx: &ExecutionContext) -> Result<()> { print_separator("yadm"); - ctx.run_type().execute(yadm).arg("pull").check_run() + ctx.run_type().execute(yadm).arg("pull").status_checked() } pub fn run_asdf(run_type: RunType) -> Result<()> { let asdf = require("asdf")?; print_separator("asdf"); - let exit_status = run_type.execute(&asdf).arg("update").spawn()?.wait()?; + run_type.execute(&asdf).arg("update").status_checked_with_codes(&[42])?; - if let ExecutorExitStatus::Wet(e) = exit_status { - if !(e.success() || e.code().map(|c| c == 42).unwrap_or(false)) { - return Err(TopgradeError::ProcessFailed(e).into()); - } - } - run_type.execute(&asdf).args(["plugin", "update", "--all"]).check_run() + run_type + .execute(&asdf) + .args(["plugin", "update", "--all"]) + .status_checked() } pub fn run_home_manager(run_type: RunType) -> Result<()> { let home_manager = require("home-manager")?; print_separator("home-manager"); - run_type.execute(home_manager).arg("switch").check_run() + run_type.execute(home_manager).arg("switch").status_checked() } pub fn run_tldr(run_type: RunType) -> Result<()> { let tldr = require("tldr")?; print_separator("TLDR"); - run_type.execute(tldr).arg("--update").check_run() + run_type.execute(tldr).arg("--update").status_checked() } pub fn run_pearl(run_type: RunType) -> Result<()> { let pearl = require("pearl")?; print_separator("pearl"); - run_type.execute(pearl).arg("update").check_run() + run_type.execute(pearl).arg("update").status_checked() } pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Result<()> { @@ -440,27 +458,33 @@ pub fn run_sdkman(base_dirs: &BaseDirs, cleanup: bool, run_type: RunType) -> Res run_type .execute(&bash) .args(["-c", cmd_selfupdate.as_str()]) - .check_run()?; + .status_checked()?; } let cmd_update = format!("source {} && sdk update", &sdkman_init_path); - run_type.execute(&bash).args(["-c", cmd_update.as_str()]).check_run()?; + run_type + .execute(&bash) + .args(["-c", cmd_update.as_str()]) + .status_checked()?; let cmd_upgrade = format!("source {} && sdk upgrade", &sdkman_init_path); - run_type.execute(&bash).args(["-c", cmd_upgrade.as_str()]).check_run()?; + run_type + .execute(&bash) + .args(["-c", cmd_upgrade.as_str()]) + .status_checked()?; if cleanup { let cmd_flush_archives = format!("source {} && sdk flush archives", &sdkman_init_path); run_type .execute(&bash) .args(["-c", cmd_flush_archives.as_str()]) - .check_run()?; + .status_checked()?; let cmd_flush_temp = format!("source {} && sdk flush temp", &sdkman_init_path); run_type .execute(&bash) .args(["-c", cmd_flush_temp.as_str()]) - .check_run()?; + .status_checked()?; } Ok(()) @@ -471,7 +495,7 @@ pub fn run_bun(ctx: &ExecutionContext) -> Result<()> { print_separator("Bun"); - ctx.run_type().execute(bun).arg("upgrade").check_run() + ctx.run_type().execute(bun).arg("upgrade").status_checked() } /// Update dotfiles with `rcm(7)`. @@ -481,10 +505,10 @@ pub fn run_rcm(ctx: &ExecutionContext) -> Result<()> { let rcup = require("rcup")?; print_separator("rcm"); - ctx.run_type().execute(rcup).arg("-v").check_run() + ctx.run_type().execute(rcup).arg("-v").status_checked() } -pub fn reboot() { +pub fn reboot() -> Result<()> { print!("Rebooting..."); - Command::new("sudo").arg("reboot").spawn().unwrap().wait().unwrap(); + Command::new("sudo").arg("reboot").status_checked() } diff --git a/src/steps/os/windows.rs b/src/steps/os/windows.rs index 14d57595..909847c9 100644 --- a/src/steps/os/windows.rs +++ b/src/steps/os/windows.rs @@ -2,11 +2,12 @@ use std::convert::TryFrom; use std::path::Path; use std::{ffi::OsStr, process::Command}; -use anyhow::Result; -use log::debug; +use color_eyre::eyre::Result; +use tracing::debug; +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, RunType}; +use crate::executor::RunType; use crate::terminal::{print_separator, print_warning}; use crate::utils::require; use crate::{error::SkipStep, steps::git::Repositories}; @@ -34,7 +35,7 @@ pub fn run_chocolatey(ctx: &ExecutionContext) -> Result<()> { command.arg("--yes"); } - command.check_run() + command.status_checked() } pub fn run_winget(ctx: &ExecutionContext) -> Result<()> { @@ -47,7 +48,10 @@ pub fn run_winget(ctx: &ExecutionContext) -> Result<()> { return Err(SkipStep(String::from("Winget is disabled by default")).into()); } - ctx.run_type().execute(&winget).args(["upgrade", "--all"]).check_run() + ctx.run_type() + .execute(&winget) + .args(["upgrade", "--all"]) + .status_checked() } pub fn run_scoop(cleanup: bool, run_type: RunType) -> Result<()> { @@ -55,18 +59,18 @@ pub fn run_scoop(cleanup: bool, run_type: RunType) -> Result<()> { print_separator("Scoop"); - run_type.execute(&scoop).args(["update"]).check_run()?; - run_type.execute(&scoop).args(["update", "*"]).check_run()?; + run_type.execute(&scoop).args(["update"]).status_checked()?; + run_type.execute(&scoop).args(["update", "*"]).status_checked()?; if cleanup { - run_type.execute(&scoop).args(["cleanup", "*"]).check_run()?; + run_type.execute(&scoop).args(["cleanup", "*"]).status_checked()?; } Ok(()) } fn get_wsl_distributions(wsl: &Path) -> Result> { - let output = Command::new(wsl).args(["--list", "-q"]).check_output()?; + let output = Command::new(wsl).args(["--list", "-q"]).output_checked_utf8()?.stdout; Ok(output .lines() .filter(|s| !s.is_empty()) @@ -77,7 +81,7 @@ fn get_wsl_distributions(wsl: &Path) -> Result> { fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> Result<()> { let topgrade = Command::new(wsl) .args(["-d", dist, "bash", "-lc", "which topgrade"]) - .check_output() + .output_checked_utf8() .map_err(|_| SkipStep(String::from("Could not find Topgrade installed in WSL")))?; let mut command = ctx.run_type().execute(wsl); @@ -89,7 +93,7 @@ fn upgrade_wsl_distribution(wsl: &Path, dist: &str, ctx: &ExecutionContext) -> R command.arg("-y"); } - command.check_run() + command.status_checked() } pub fn run_wsl_topgrade(ctx: &ExecutionContext) -> Result<()> { @@ -129,12 +133,17 @@ pub fn windows_update(ctx: &ExecutionContext) -> Result<()> { print_separator("Windows Update"); println!("Running Windows Update. Check the control panel for progress."); - ctx.run_type().execute(&usoclient).arg("ScanInstallWait").check_run()?; - ctx.run_type().execute(&usoclient).arg("StartInstall").check_run() + ctx.run_type() + .execute(&usoclient) + .arg("ScanInstallWait") + .status_checked()?; + ctx.run_type().execute(&usoclient).arg("StartInstall").status_checked() } -pub fn reboot() { - Command::new("shutdown").args(["/R", "/T", "0"]).spawn().ok(); +pub fn reboot() -> Result<()> { + // If this works, it won't return, but if it doesn't work, it may return a useful error + // message. + Command::new("shutdown").args(["/R", "/T", "0"]).status_checked() } pub fn insert_startup_scripts(ctx: &ExecutionContext, git_repos: &mut Repositories) -> Result<()> { diff --git a/src/steps/powershell.rs b/src/steps/powershell.rs index 3d60ea8d..f6fe50e3 100644 --- a/src/steps/powershell.rs +++ b/src/steps/powershell.rs @@ -3,10 +3,10 @@ use std::path::Path; use std::path::PathBuf; use std::process::Command; -use anyhow::Result; +use color_eyre::eyre::Result; +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; -use crate::executor::CommandExt; use crate::terminal::{is_dumb, print_separator}; use crate::utils::{require_option, which, PathExt}; use crate::Step; @@ -27,8 +27,8 @@ impl Powershell { let profile = path.as_ref().and_then(|path| { Command::new(path) .args(["-NoProfile", "-Command", "Split-Path $profile"]) - .check_output() - .map(|output| PathBuf::from(output.trim())) + .output_checked_utf8() + .map(|output| PathBuf::from(output.stdout.trim())) .and_then(|p| p.require()) .ok() }); @@ -52,8 +52,8 @@ impl Powershell { "-Command", &format!("Get-Module -ListAvailable {}", command), ]) - .check_output() - .map(|result| !result.is_empty()) + .output_checked_utf8() + .map(|result| !result.stdout.is_empty()) .unwrap_or(false) } @@ -81,7 +81,7 @@ impl Powershell { .execute(powershell) // This probably doesn't need `shell_words::join`. .args(["-NoProfile", "-Command", &cmd.join(" ")]) - .check_run() + .status_checked() } #[cfg(windows)] @@ -119,6 +119,6 @@ impl Powershell { } ), ]) - .check_run() + .status_checked() } } diff --git a/src/steps/remote/ssh.rs b/src/steps/remote/ssh.rs index c94552fb..c274766d 100644 --- a/src/steps/remote/ssh.rs +++ b/src/steps/remote/ssh.rs @@ -1,6 +1,8 @@ -use anyhow::Result; +use color_eyre::eyre::Result; -use crate::{error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils}; +use crate::{ + command::CommandExt, error::SkipStep, execution_context::ExecutionContext, terminal::print_separator, utils, +}; fn prepare_async_ssh_command(args: &mut Vec<&str>) { args.insert(0, "ssh"); @@ -24,7 +26,7 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { #[cfg(unix)] { prepare_async_ssh_command(&mut args); - crate::tmux::run_command(ctx, &shell_words::join(args))?; + crate::tmux::run_command(ctx, hostname, &shell_words::join(args))?; Err(SkipStep(String::from("Remote Topgrade launched in Tmux")).into()) } @@ -47,6 +49,6 @@ pub fn ssh_step(ctx: &ExecutionContext, hostname: &str) -> Result<()> { print_separator(format!("Remote ({})", hostname)); println!("Connecting to {}...", hostname); - ctx.run_type().execute(ssh).args(&args).check_run() + ctx.run_type().execute(ssh).args(&args).status_checked() } } diff --git a/src/steps/remote/vagrant.rs b/src/steps/remote/vagrant.rs index 712a2be9..b7fbfb18 100644 --- a/src/steps/remote/vagrant.rs +++ b/src/steps/remote/vagrant.rs @@ -2,13 +2,13 @@ use std::path::{Path, PathBuf}; use std::process::Command; use std::{fmt::Display, rc::Rc, str::FromStr}; -use anyhow::Result; -use log::{debug, error}; +use color_eyre::eyre::Result; use regex::Regex; use strum::EnumString; +use tracing::{debug, error}; +use crate::command::CommandExt; use crate::execution_context::ExecutionContext; -use crate::executor::CommandExt; use crate::terminal::print_separator; use crate::{error::SkipStep, utils, Step}; @@ -61,10 +61,11 @@ impl Vagrant { let output = Command::new(&self.path) .arg("status") .current_dir(directory) - .check_output()?; + .output_checked_utf8()?; debug!("Vagrant output in {}: {}", directory, output); let boxes = output + .stdout .split('\n') .skip(2) .take_while(|line| !(line.is_empty() || line.starts_with('\r'))) @@ -115,7 +116,7 @@ impl<'a> TemporaryPowerOn<'a> { .execute(vagrant) .args([subcommand, &vagrant_box.name]) .current_dir(vagrant_box.path.clone()) - .check_run()?; + .status_checked()?; Ok(TemporaryPowerOn { vagrant, vagrant_box, @@ -142,7 +143,7 @@ impl<'a> Drop for TemporaryPowerOn<'a> { .execute(self.vagrant) .args([subcommand, &self.vagrant_box.name]) .current_dir(self.vagrant_box.path.clone()) - .check_run() + .status_checked() .ok(); } } @@ -199,7 +200,7 @@ pub fn topgrade_vagrant_box(ctx: &ExecutionContext, vagrant_box: &VagrantBox) -> .execute(&vagrant.path) .current_dir(&vagrant_box.path) .args(["ssh", "-c", &command]) - .check_run() + .status_checked() } pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { @@ -208,12 +209,12 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { let outdated = Command::new(&vagrant) .args(["box", "outdated", "--global"]) - .check_output()?; + .output_checked_utf8()?; let re = Regex::new(r"\* '(.*?)' for '(.*?)' is outdated").unwrap(); let mut found = false; - for ele in re.captures_iter(&outdated) { + for ele in re.captures_iter(&outdated.stdout) { found = true; let _ = ctx .run_type() @@ -222,13 +223,16 @@ pub fn upgrade_vagrant_boxes(ctx: &ExecutionContext) -> Result<()> { .arg(ele.get(1).unwrap().as_str()) .arg("--provider") .arg(ele.get(2).unwrap().as_str()) - .check_run(); + .status_checked(); } if !found { println!("No outdated boxes") } else { - ctx.run_type().execute(&vagrant).args(["box", "prune"]).check_run()?; + ctx.run_type() + .execute(&vagrant) + .args(["box", "prune"]) + .status_checked()?; } Ok(()) diff --git a/src/steps/tmux.rs b/src/steps/tmux.rs index 388b65b4..61d3bb46 100644 --- a/src/steps/tmux.rs +++ b/src/steps/tmux.rs @@ -1,16 +1,22 @@ +use std::env; +use std::path::PathBuf; +use std::process::Command; + +use color_eyre::eyre::eyre; +use color_eyre::eyre::Context; +use color_eyre::eyre::Result; +use directories::BaseDirs; + +use crate::command::CommandExt; use crate::executor::RunType; use crate::terminal::print_separator; use crate::{ execution_context::ExecutionContext, - utils::{which, Check, PathExt}, + utils::{which, PathExt}, }; -use anyhow::Result; -use directories::BaseDirs; -use std::env; -use std::io; -use std::os::unix::process::CommandExt; -use std::path::PathBuf; -use std::process::{exit, Command}; + +#[cfg(unix)] +use std::os::unix::process::CommandExt as _; pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { let tpm = base_dirs @@ -20,7 +26,7 @@ pub fn run_tpm(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("tmux plugins"); - run_type.execute(tpm).arg("all").check_run() + run_type.execute(tpm).arg("all").status_checked() } struct Tmux { @@ -44,73 +50,130 @@ impl Tmux { command } - fn has_session(&self, session_name: &str) -> Result { + fn has_session(&self, session_name: &str) -> Result { Ok(self .build() .args(["has-session", "-t", session_name]) - .output()? + .output_checked_with(|_| Ok(()))? .status .success()) } - fn new_session(&self, session_name: &str) -> Result { - Ok(self + /// Create a new tmux session with the given name, running the given command. + /// The command is passed to `sh` (see "shell-command arguments are sh(1) commands" in the + /// `tmux(1)` man page). + fn new_session(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> { + let _ = self .build() - .args(["new-session", "-d", "-s", session_name, "-n", "dummy"]) - .spawn()? - .wait()? - .success()) + // `-d`: initial size comes from the global `default-size` option (instead + // of passing `-x` and `-y` arguments. + // (What do those even do?) + // `-s`: session name + // `-n`: window name (always `topgrade`) + .args(["new-session", "-d", "-s", session_name, "-n", window_name, command]) + .output_checked()?; + Ok(()) } - fn run_in_session(&self, command: &str) -> Result<()> { - self.build() - .args(["new-window", "-t", "topgrade", command]) - .spawn()? - .wait()? - .check()?; + /// Like [`new_session`] but it appends a digit to the session name (if necessary) to + /// avoid duplicate session names. + /// + /// The session name is returned. + fn new_unique_session(&self, session_name: &str, window_name: &str, command: &str) -> Result { + let mut session = session_name.to_owned(); + for i in 1.. { + if !self + .has_session(&session) + .context("Error determining if a tmux session exists")? + { + self.new_session(&session, window_name, command) + .context("Error running Topgrade in tmux")?; + return Ok(session); + } + session = format!("{session_name}-{i}"); + } + unreachable!() + } - Ok(()) + /// Create a new window in the given tmux session, running the given command. + fn new_window(&self, session_name: &str, window_name: &str, command: &str) -> Result<()> { + self.build() + // `-d`: initial size comes from the global `default-size` option (instead + // of passing `-x` and `-y` arguments. + // (What do those even do?) + // `-s`: session name + // `-n`: window name + .args([ + "new-window", + "-a", + "-t", + &format!("{session_name}:{window_name}"), + "-n", + window_name, + command, + ]) + .env_remove("TMUX") + .status_checked() + } + + fn window_indices(&self, session_name: &str) -> Result> { + self.build() + .args(["list-windows", "-F", "#{window_index}", "-t", session_name]) + .output_checked_utf8()? + .stdout + .lines() + .map(|l| l.parse()) + .collect::, _>>() + .context("Failed to compute tmux windows") } } -pub fn run_in_tmux(args: Vec) -> ! { +pub fn run_in_tmux(args: Vec) -> Result<()> { let command = { let mut command = vec![ String::from("env"), String::from("TOPGRADE_KEEP_END=1"), String::from("TOPGRADE_INSIDE_TMUX=1"), ]; + // TODO: Should we use `topgrade` instead of the first argument here, which may be + // a local path? command.extend(env::args()); shell_words::join(command) }; let tmux = Tmux::new(args); - if !tmux.has_session("topgrade").expect("Error detecting a tmux session") { - tmux.new_session("topgrade").expect("Error creating a tmux session"); - } - - tmux.run_in_session(&command).expect("Error running Topgrade in tmux"); - tmux.build() - .args(["kill-window", "-t", "topgrade:dummy"]) - .output() - .expect("Error killing the dummy tmux window"); + // Find an unused session and run `topgrade` in it with the current command's arguments. + let session_name = "topgrade"; + let window_name = "topgrade"; + let session = tmux.new_unique_session(session_name, window_name, &command)?; + // Only attach to the newly-created session if we're not currently in a tmux session. if env::var("TMUX").is_err() { - let err = tmux.build().args(["attach", "-t", "topgrade"]).exec(); - panic!("{:?}", err); + let err = tmux.build().args(["attach-session", "-t", &session]).exec(); + Err(eyre!("{err}")).context("Failed to `execvp(3)` tmux") } else { println!("Topgrade launched in a new tmux session"); - exit(0); + Ok(()) } } -pub fn run_command(ctx: &ExecutionContext, command: &str) -> Result<()> { - Tmux::new(ctx.config().tmux_arguments()?) - .build() - .args(["new-window", "-a", "-t", "topgrade:1", command]) - .env_remove("TMUX") - .spawn()? - .wait()? - .check() +pub fn run_command(ctx: &ExecutionContext, window_name: &str, command: &str) -> Result<()> { + let tmux = Tmux::new(ctx.config().tmux_arguments()?); + + match ctx.get_tmux_session() { + Some(session_name) => { + let indices = tmux.window_indices(&session_name)?; + let last_window = indices + .iter() + .last() + .ok_or_else(|| eyre!("tmux session {session_name} has no windows"))?; + tmux.new_window(&session_name, &format!("{last_window}"), command)?; + } + None => { + let name = tmux.new_unique_session("topgrade", window_name, command)?; + ctx.set_tmux_session(name); + } + } + Ok(()) } diff --git a/src/steps/toolbx.rs b/src/steps/toolbx.rs index fcf6b247..1c8a7583 100644 --- a/src/steps/toolbx.rs +++ b/src/steps/toolbx.rs @@ -1,17 +1,20 @@ -use anyhow::Result; +use color_eyre::eyre::Result; +use crate::command::CommandExt; use crate::config::Step; use crate::terminal::print_separator; use crate::{execution_context::ExecutionContext, utils::require}; -use log::debug; use std::path::Path; use std::{path::PathBuf, process::Command}; +use tracing::debug; fn list_toolboxes(toolbx: &Path) -> Result> { - let output = Command::new(toolbx).args(["list", "--containers"]).output()?; - let output_str = String::from_utf8(output.stdout)?; + let output = Command::new(toolbx) + .args(["list", "--containers"]) + .output_checked_utf8()?; - let proc: Vec = output_str + let proc: Vec = output + .stdout .lines() // Skip the first line since that contains only status information .skip(1) @@ -54,7 +57,7 @@ pub fn run_toolbx(ctx: &ExecutionContext) -> Result<()> { args.push("--yes"); } - let _output = ctx.run_type().execute(&toolbx).args(&args).check_run(); + ctx.run_type().execute(&toolbx).args(&args).status_checked()?; } Ok(()) diff --git a/src/steps/upgrade.vim b/src/steps/upgrade.vim index 28e3df9f..5eac813e 100644 --- a/src/steps/upgrade.vim +++ b/src/steps/upgrade.vim @@ -38,6 +38,14 @@ if exists(":CocUpdateSync") CocUpdateSync endif +" TODO: Should this be after `PackerSync`? +" Not sure how to sequence this after Packer without doing something weird +" with that `PackerComplete` autocommand. +if exists(":TSUpdate") + echo "TreeSitter Update" + TSUpdate +endif + if exists(':PackerSync') echo "Packer" autocmd User PackerComplete quitall diff --git a/src/steps/vim.rs b/src/steps/vim.rs index 4a7538fc..42b1a389 100644 --- a/src/steps/vim.rs +++ b/src/steps/vim.rs @@ -1,19 +1,20 @@ +use crate::command::CommandExt; use crate::error::{SkipStep, TopgradeError}; -use anyhow::Result; +use color_eyre::eyre::Result; -use crate::executor::{CommandExt, Executor, ExecutorOutput, RunType}; +use crate::executor::{Executor, ExecutorOutput, RunType}; use crate::terminal::print_separator; use crate::{ execution_context::ExecutionContext, utils::{require, PathExt}, }; use directories::BaseDirs; -use log::debug; use std::path::PathBuf; use std::{ io::{self, Write}, process::Command, }; +use tracing::debug; const UPGRADE_VIM: &str = include_str!("upgrade.vim"); @@ -63,7 +64,7 @@ fn upgrade(command: &mut Executor, ctx: &ExecutionContext) -> Result<()> { } if !status.success() { - return Err(TopgradeError::ProcessFailed(status).into()); + return Err(TopgradeError::ProcessFailed(command.get_program(), status).into()); } else { println!("Plugins upgraded") } @@ -84,22 +85,22 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> { .execute(&git) .current_dir(&config_dir) .args(["reset", "--hard"]) - .check_run()?; + .status_checked()?; ctx.run_type() .execute(&git) .current_dir(&config_dir) .args(["clean", "-d", "--force"]) - .check_run()?; + .status_checked()?; ctx.run_type() .execute(&git) .current_dir(&config_dir) .args(["pull", "--rebase"]) - .check_run()?; + .status_checked()?; ctx.run_type() .execute(python) .current_dir(config_dir) .arg(update_plugins) - .check_run()?; + .status_checked()?; Ok(()) } @@ -107,8 +108,8 @@ pub fn upgrade_ultimate_vimrc(ctx: &ExecutionContext) -> Result<()> { pub fn upgrade_vim(base_dirs: &BaseDirs, ctx: &ExecutionContext) -> Result<()> { let vim = require("vim")?; - let output = Command::new(&vim).arg("--version").check_output()?; - if !output.starts_with("VIM") { + let output = Command::new(&vim).arg("--version").output_checked_utf8()?; + if !output.stdout.starts_with("VIM") { return Err(SkipStep(String::from("vim binary might be actually nvim")).into()); } @@ -147,5 +148,5 @@ pub fn run_voom(_base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("voom"); - run_type.execute(voom).arg("update").check_run() + run_type.execute(voom).arg("update").status_checked() } diff --git a/src/steps/zsh.rs b/src/steps/zsh.rs index 9787c259..4ffdcc30 100644 --- a/src/steps/zsh.rs +++ b/src/steps/zsh.rs @@ -1,16 +1,19 @@ -use crate::execution_context::ExecutionContext; -use crate::executor::{CommandExt, RunType}; -use crate::git::Repositories; -use crate::terminal::print_separator; -use crate::utils::{require, PathExt}; -use anyhow::Result; -use directories::BaseDirs; -use log::debug; use std::env; use std::path::{Path, PathBuf}; use std::process::Command; + +use color_eyre::eyre::Result; +use directories::BaseDirs; +use tracing::debug; use walkdir::WalkDir; +use crate::command::CommandExt; +use crate::execution_context::ExecutionContext; +use crate::executor::RunType; +use crate::git::Repositories; +use crate::terminal::print_separator; +use crate::utils::{require, PathExt}; + pub fn run_zr(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { let zsh = require("zsh")?; @@ -19,7 +22,7 @@ pub fn run_zr(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("zr"); let cmd = format!("source {} && zr --update", zshrc(base_dirs).display()); - run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run() + run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked() } pub fn zshrc(base_dirs: &BaseDirs) -> PathBuf { @@ -34,7 +37,7 @@ pub fn run_antibody(run_type: RunType) -> Result<()> { print_separator("antibody"); - run_type.execute(antibody).arg("update").check_run() + run_type.execute(antibody).arg("update").status_checked() } pub fn run_antigen(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -48,7 +51,7 @@ pub fn run_antigen(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("antigen"); let cmd = format!("source {} && (antigen selfupdate ; antigen update)", zshrc.display()); - run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run() + run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked() } pub fn run_zgenom(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -62,7 +65,7 @@ pub fn run_zgenom(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("zgenom"); let cmd = format!("source {} && zgenom selfupdate && zgenom update", zshrc.display()); - run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).check_run() + run_type.execute(zsh).args(["-l", "-c", cmd.as_str()]).status_checked() } pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -76,7 +79,10 @@ pub fn run_zplug(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("zplug"); - run_type.execute(zsh).args(["-i", "-c", "zplug update"]).check_run() + run_type + .execute(zsh) + .args(["-i", "-c", "zplug update"]) + .status_checked() } pub fn run_zinit(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -91,7 +97,7 @@ pub fn run_zinit(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("zinit"); let cmd = format!("source {} && zinit self-update && zinit update --all", zshrc.display(),); - run_type.execute(zsh).args(["-i", "-c", cmd.as_str()]).check_run() + run_type.execute(zsh).args(["-i", "-c", cmd.as_str()]).status_checked() } pub fn run_zi(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -103,7 +109,7 @@ pub fn run_zi(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { print_separator("zi"); let cmd = format!("source {} && zi self-update && zi update --all", zshrc.display(),); - run_type.execute(zsh).args(["-i", "-c", &cmd]).check_run() + run_type.execute(zsh).args(["-i", "-c", &cmd]).status_checked() } pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { @@ -111,8 +117,10 @@ pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { env::var("ZIM_HOME") .or_else(|_| { Command::new("zsh") + // TODO: Should these be quoted? .args(["-c", "[[ -n ${ZIM_HOME} ]] && print -n ${ZIM_HOME}"]) - .check_output() + .output_checked_utf8() + .map(|o| o.stdout) }) .map(PathBuf::from) .unwrap_or_else(|_| base_dirs.home_dir().join(".zim")) @@ -123,7 +131,7 @@ pub fn run_zim(base_dirs: &BaseDirs, run_type: RunType) -> Result<()> { run_type .execute(zsh) .args(["-i", "-c", "zimfw upgrade && zimfw update"]) - .check_run() + .status_checked() } pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> { @@ -135,8 +143,10 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> { let custom_dir = env::var::<_>("ZSH_CUSTOM") .or_else(|_| { Command::new("zsh") + // TODO: Should these be quoted? .args(["-c", "test $ZSH_CUSTOM && echo -n $ZSH_CUSTOM"]) - .check_output() + .output_checked_utf8() + .map(|o| o.stdout) }) .map(PathBuf::from) .unwrap_or_else(|e| { @@ -168,5 +178,5 @@ pub fn run_oh_my_zsh(ctx: &ExecutionContext) -> Result<()> { .execute("zsh") .env("ZSH", &oh_my_zsh) .arg(&oh_my_zsh.join("tools/upgrade.sh")) - .check_run_with_codes(&[80]) + .status_checked_with_codes(&[80]) } diff --git a/src/terminal.rs b/src/terminal.rs index b6273a8a..8d17b3ad 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -8,14 +8,17 @@ use std::sync::Mutex; use std::time::Duration; use chrono::{Local, Timelike}; +use color_eyre::eyre; +use color_eyre::eyre::Context; use console::{style, Key, Term}; use lazy_static::lazy_static; -use log::{debug, error}; #[cfg(target_os = "macos")] use notify_rust::{Notification, Timeout}; +use tracing::{debug, error}; #[cfg(windows)] use which_crate::which; +use crate::command::CommandExt; use crate::report::StepResult; #[cfg(target_os = "linux")] use crate::utils::which; @@ -34,13 +37,8 @@ pub fn shell() -> &'static str { which("pwsh").map(|_| "pwsh").unwrap_or("powershell") } -pub fn run_shell() { - Command::new(shell()) - .env("IN_TOPGRADE", "1") - .spawn() - .unwrap() - .wait() - .unwrap(); +pub fn run_shell() -> eyre::Result<()> { + Command::new(shell()).env("IN_TOPGRADE", "1").status_checked() } struct Terminal { @@ -106,7 +104,9 @@ impl Terminal { } command.args(["-a", "Topgrade", "Topgrade"]); command.arg(message.as_ref()); - command.output().ok(); + if let Err(err) = command.output_checked() { + tracing::error!("{err:?}"); + } } } } @@ -163,6 +163,19 @@ impl Terminal { } } + #[allow(dead_code)] + fn print_error, Q: AsRef>(&mut self, key: Q, message: P) { + let key = key.as_ref(); + let message = message.as_ref(); + self.term + .write_fmt(format_args!( + "{} {}", + style(format!("{} failed:", key)).red().bold(), + message + )) + .ok(); + } + #[allow(dead_code)] fn print_warning>(&mut self, message: P) { let message = message.as_ref(); @@ -214,7 +227,7 @@ impl Terminal { } } #[allow(unused_variables)] - fn should_retry(&mut self, interrupted: bool, step_name: &str) -> Result { + fn should_retry(&mut self, interrupted: bool, step_name: &str) -> eyre::Result { if self.width.is_none() { return Ok(false); } @@ -225,29 +238,31 @@ impl Terminal { self.notify_desktop(format!("{} failed", step_name), None); - self.term - .write_fmt(format_args!( - "\n{}", - style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix)) - .yellow() - .bold() - )) - .ok(); + let prompt_inner = style(format!("{}Retry? (y)es/(N)o/(s)hell/(q)uit", self.prefix)) + .yellow() + .bold(); + + self.term.write_fmt(format_args!("\n{}", prompt_inner)).ok(); let answer = loop { match self.term.read_key() { Ok(Key::Char('y')) | Ok(Key::Char('Y')) => break Ok(true), Ok(Key::Char('s')) | Ok(Key::Char('S')) => { println!("\n\nDropping you to shell. Fix what you need and then exit the shell.\n"); - run_shell(); - break Ok(true); + if let Err(err) = run_shell().context("Failed to run shell") { + self.term.write_fmt(format_args!("{err:?}\n{}", prompt_inner)).ok(); + } else { + break Ok(true); + } } Ok(Key::Char('n')) | Ok(Key::Char('N')) | Ok(Key::Enter) => break Ok(false), Err(e) => { error!("Error reading from terminal: {}", e); break Ok(false); } - Ok(Key::Char('q')) | Ok(Key::Char('Q')) => return Err(io::Error::from(io::ErrorKind::Interrupted)), + Ok(Key::Char('q')) | Ok(Key::Char('Q')) => { + return Err(io::Error::from(io::ErrorKind::Interrupted)).context("Quit from user input") + } _ => (), } }; @@ -268,7 +283,7 @@ impl Default for Terminal { } } -pub fn should_retry(interrupted: bool, step_name: &str) -> Result { +pub fn should_retry(interrupted: bool, step_name: &str) -> eyre::Result { TERMINAL.lock().unwrap().should_retry(interrupted, step_name) } @@ -276,6 +291,11 @@ pub fn print_separator>(message: P) { TERMINAL.lock().unwrap().print_separator(message) } +#[allow(dead_code)] +pub fn print_error, Q: AsRef>(key: Q, message: P) { + TERMINAL.lock().unwrap().print_error(key, message) +} + #[allow(dead_code)] pub fn print_warning>(message: P) { TERMINAL.lock().unwrap().print_warning(message) diff --git a/src/utils.rs b/src/utils.rs index 3ea1fb3f..326e4ad6 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,46 +1,11 @@ -use crate::error::{SkipStep, TopgradeError}; -use anyhow::Result; +use crate::error::SkipStep; +use color_eyre::eyre::Result; -use log::{debug, error}; use std::env; use std::ffi::OsStr; use std::fmt::Debug; use std::path::{Path, PathBuf}; -use std::process::{ExitStatus, Output}; - -pub trait Check { - fn check(self) -> Result<()>; -} - -impl Check for Output { - fn check(self) -> Result<()> { - self.status.check() - } -} - -pub trait CheckWithCodes { - fn check_with_codes(self, codes: &[i32]) -> Result<()>; -} - -// Anything that implements CheckWithCodes also implements check -// if check_with_codes is given an empty array of codes to check -impl Check for T { - fn check(self) -> Result<()> { - self.check_with_codes(&[]) - } -} - -impl CheckWithCodes for ExitStatus { - fn check_with_codes(self, codes: &[i32]) -> Result<()> { - // Set the default to be -1 because the option represents a signal termination - let code = self.code().unwrap_or(-1); - if self.success() || codes.contains(&code) { - Ok(()) - } else { - Err(TopgradeError::ProcessFailed(self).into()) - } - } -} +use tracing::{debug, error}; pub trait PathExt where @@ -142,3 +107,55 @@ pub fn require_option(option: Option, cause: String) -> Result { Err(SkipStep(cause).into()) } } + +/* sys-info-rs + * + * Copyright (c) 2015 Siyu Wang + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +#[cfg(target_family = "unix")] +pub fn hostname() -> Result { + use std::ffi; + extern crate libc; + + unsafe { + let buf_size = libc::sysconf(libc::_SC_HOST_NAME_MAX) as usize; + let mut buf = Vec::::with_capacity(buf_size + 1); + + if libc::gethostname(buf.as_mut_ptr() as *mut libc::c_char, buf_size) < 0 { + return Err(SkipStep(format!("Failed to get hostname: {}", std::io::Error::last_os_error())).into()); + } + let hostname_len = libc::strnlen(buf.as_ptr() as *const libc::c_char, buf_size); + buf.set_len(hostname_len); + + Ok(ffi::CString::new(buf).unwrap().into_string().unwrap()) + } +} + +#[cfg(target_family = "windows")] +pub fn hostname() -> Result { + use crate::command::CommandExt; + use std::process::Command; + + Command::new("hostname") + .output_checked_utf8() + .map_err(|err| SkipStep(format!("Failed to get hostname: {}", err)).into()) + .map(|output| output.stdout.trim().to_owned()) +}