Repository: cross-rs/cross
Pull request #829 (2022-06-21 16:20:28)

enable coverage
Implements coverage via lcov artifacts and uploads them to https://app.codecov.io/gh/cross-rs/cross

Emilgardis: bors try
bors[bot]: ## try

Build succeeded:
  * [conclusion](https://github.com/cross-rs/cross/runs/6996372503?check_suite_focus=true)
Alexhuszagh: Fix the changelog and r=me whenever.
Alexhuszagh: bors r+
bors[bot]: Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7097455830?check_suite_focus=true)

Patch:
diff --git a/.github/actions/cargo-install-upload-artifacts/action.yml b/.github/actions/cargo-install-upload-artifacts/action.yml
index 2486c71..07bd34f 100644
--- a/.github/actions/cargo-install-upload-artifacts/action.yml
+++ b/.github/actions/cargo-install-upload-artifacts/action.yml
@@ -40,6 +40,8 @@ runs:
           --root ${{ steps.metadata.outputs.out-dir }}
           --bins
         use-cross: true
+      env:
+        RUSTFLAGS: "" # Make sure to unset RUSTFLAGS
 
     - name: Archive artifacts
       id: archive
diff --git a/.github/actions/cargo-llvm-cov/action.yml b/.github/actions/cargo-llvm-cov/action.yml
new file mode 100644
index 0000000..49eb4dd
--- /dev/null
+++ b/.github/actions/cargo-llvm-cov/action.yml
@@ -0,0 +1,67 @@
+name: llvm coverage
+description: Sets up everything that is needed for coverage. Makes artifacts available for processing later, prefixed with _coverage-
+inputs:
+  name:
+    description: 'the name of the artifact'
+    required: true
+outputs:
+  artifact-name:
+    description: 'the name of the artifact'
+    value: ${{ steps.cov.outputs.artifact-name }}
+runs:
+  using: composite
+  steps:
+    - name: Install cargo-llvm-cov
+      uses: taiki-e/install-action@v1
+      with:
+        tool: cargo-llvm-cov
+    - run: rustup component add llvm-tools-preview
+      shell: bash
+    - name: LLVM instrument coverage
+      id: cov
+      uses: ./.github/actions/post
+      if: always()
+      with:
+        main: |
+          pwd=$(pwd)
+          if which cygpath; then
+            pwd="$(cygpath -w "$(pwd)")"
+          fi
+          echo RUSTFLAGS=" -C instrument-coverage --remap-path-prefix ${pwd}=" >> $GITHUB_ENV
+          echo LLVM_PROFILE_FILE="${pwd}/target/cross-%m.profraw" >> $GITHUB_ENV
+          echo CARGO_INCREMENTAL="0" >> $GITHUB_ENV
+          echo RUST_TEST_THREADS="1" >> $GITHUB_ENV
+          echo "::set-output name=artifact-name::_coverage-${name}"
+        post: |
+          # XXX(emilgardis): Upload early?
+          pwd=$(pwd)
+          if which cygpath; then
+            pwd="$(cygpath -w "$(pwd)")"
+          fi
+          # No pwd needed here, we're in the root
+          export LLVM_PROFILE_FILE="${pwd}/target/cross-%m.profraw"
+          export CARGO_LLVM_COV_TARGET_DIR="${pwd}/target"
+          mkdir coverage
+          echo $(ls target)
+          cargo llvm-cov --no-run --remap-path-prefix --lcov --output-path "coverage/lcov.${name}.info" -vv || ( echo "::error title=Coverage merge failed::" && exit 0 )
+          rm target/*.profraw
+          npm install @actions/artifact
+          npm install glob
+
+          cat <<-EOT | node - || ( echo "::error title=Coverage upload failed::" && exit 0 )
+          (async function main() {
+              var artifact = require('@actions/artifact');
+              var glob = require('glob')
+              const artifactClient = artifact.create();
+              const artifactName = '_coverage-' + process.env.name;
+              const files = glob.sync("coverage/*");
+              if (!files.length) {
+                process.exit(0);
+              }
+              console.log("${files}")
+              const options = { retentionDays: 2 };
+              const upload = await artifactClient.uploadArtifact(artifactName, files, "coverage", options);
+              })()
+          EOT
+      env:
+        name: ${{ inputs.name }}
diff --git a/.github/actions/cargo-publish/action.yml b/.github/actions/cargo-publish/action.yml
index 83d8544..bcba680 100644
--- a/.github/actions/cargo-publish/action.yml
+++ b/.github/actions/cargo-publish/action.yml
@@ -40,7 +40,7 @@ runs:
       shell: bash
       env:
         GITHUB_TOKEN: ${{ inputs.github-token }}
-        
+
     - name: Create GitHub release
       if: >
         github.event_name == 'push' && (
@@ -54,7 +54,7 @@ runs:
         prerelease: ${{ startsWith(github.ref, 'refs/tags/v') && contains(github.ref_name, '-') }}
         draft: ${{ !startsWith(github.ref, 'refs/tags/v') && steps.changelog-reader.outputs.status == 'unreleased' }}
         files: |
-          ${{ steps.download-artifacts.outputs.download-path }}/*/*
+          ${{ steps.download-artifacts.outputs.download-path }}/cross-*/*
 
     - name: Publish crate
       uses: actions-rs/cargo@v1
diff --git a/.github/actions/post/action.yml b/.github/actions/post/action.yml
new file mode 100644
index 0000000..d8db192
--- /dev/null
+++ b/.github/actions/post/action.yml
@@ -0,0 +1,14 @@
+# adapted from https://github.com/actions/runner/issues/1478
+name: With post step
+description: 'Generic JS Action to execute a main command and set a command in a post step.'
+inputs:
+  main:
+    description: 'Main command/script.'
+    required: true
+  post:
+    description: 'Post command/script.'
+    required: true
+runs:
+  using: 'node16'
+  main: 'main.js'
+  post: 'main.js'
diff --git a/.github/actions/post/main.js b/.github/actions/post/main.js
new file mode 100644
index 0000000..94bcec6
--- /dev/null
+++ b/.github/actions/post/main.js
@@ -0,0 +1,26 @@
+// adapted from https://github.com/actions/runner/issues/1478
+const { exec } = require("child_process");
+
+function run(cmd) {
+  exec(cmd, { shell: "bash" }, (error, stdout, stderr) => {
+    if (stdout.length != 0) {
+      console.log(`${stdout}`);
+    }
+    if (stderr.length != 0) {
+      console.error(`${stderr}`);
+    }
+    if (error) {
+      process.exitCode = error.code;
+      console.error(`${error}`);
+    }
+  });
+}
+
+if (process.env[`STATE_POST`] != undefined) {
+  // Are we in the 'post' step?
+  run(process.env.INPUT_POST);
+} else {
+  // Otherwise, this is the main step
+  console.log(`::save-state name=POST::true`);
+  run(process.env.INPUT_MAIN);
+}
diff --git a/.github/codecov.yml b/.github/codecov.yml
new file mode 100644
index 0000000..3067768
--- /dev/null
+++ b/.github/codecov.yml
@@ -0,0 +1,14 @@
+comment: false
+codecov:
+  branch: main
+coverage:
+  status:
+    project:
+      default:
+        informational: true
+    patch:
+      default:
+        informational: true
+flag_management:
+  default_rules: # the rules that will be followed for any flag added, generally
+    carryforward: true
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 3083e4c..5c7db32 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -61,7 +61,7 @@ jobs:
         uses: actions-rs/cargo@v1
         with:
           command: clippy
-          args: --locked --all-targets --all-features --workspace -- -D warnings
+          args: --locked --all-targets --all-features -- -D warnings
 
   test:
     runs-on: ${{ matrix.os }}
@@ -75,6 +75,9 @@ jobs:
       - uses: actions/checkout@v3
 
       - uses: ./.github/actions/setup-rust
+      - uses: ./.github/actions/cargo-llvm-cov
+        with:
+          name: test-${{matrix.os}}
 
       - name: Run unit tests
         uses: actions-rs/cargo@v1
@@ -82,16 +85,7 @@ jobs:
           command: test
           args: --locked --all-targets --workspace --all-features
         timeout-minutes: 10
-  check:
-    runs-on: ubuntu-latest
-    outputs:
-      is-latest: ${{ steps.check.outputs.is-latest }}
-    steps:
-      - uses: actions/checkout@v3
-      - uses: ./.github/actions/setup-rust
-      - run: cargo xtask ci-job check
-        id: check
-        shell: bash
+
   generate-matrix:
     runs-on: ubuntu-latest
     outputs:
@@ -191,9 +185,9 @@ jobs:
             - { target: x86_64-linux-android,             os: ubuntu-latest,  cpp: 1,           std: 1, run: 1, runners: qemu-user }
             - { target: x86_64-pc-windows-gnu,            os: ubuntu-latest,  cpp: 1,           std: 1, run: 1 }
             - { target: i686-pc-windows-gnu,              os: ubuntu-latest,  cpp: 1,           std: 1, run: 1 }
-            # Disabled for now, see https://github.com/rust-lang/rust/issues/98216
+            # Disabled for now, see https://github.com/rust-lang/rust/issues/85821
             #- { target: asmjs-unknown-emscripten,         os: ubuntu-latest,  cpp: 1,           std: 1, run: 1 }
-            - { target: wasm32-unknown-emscripten,        os: ubuntu-latest,  cpp: 1,           std: 1, run: 1 }
+            #- { target: wasm32-unknown-emscripten,        os: ubuntu-latest,  cpp: 1,           std: 1, run: 1 }
             - { target: x86_64-unknown-dragonfly,         os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1, build-std: 1 }
             - { target: i686-unknown-freebsd,             os: ubuntu-latest,          dylib: 1, std: 1 }
             - { target: x86_64-unknown-freebsd,           os: ubuntu-latest,          dylib: 1, std: 1 }
@@ -208,7 +202,7 @@ jobs:
   build:
     name: target (${{ matrix.pretty }},${{ matrix.os }})
     runs-on: ${{ matrix.os }}
-    needs: [shellcheck, test, generate-matrix, check]
+    needs: [shellcheck, test, generate-matrix]
     if: github.event_name == 'push'
     strategy:
       fail-fast: false
@@ -217,6 +211,7 @@ jobs:
     outputs:
       has-image: ${{ steps.prepare-meta.outputs.has-image }}
       images: ${{ steps.build-docker-image.outputs.images && fromJSON(steps.build-docker-image.outputs.images)  }}
+      coverage-artifact: ${{ steps.cov.outputs.artifact-name }}
     steps:
       - uses: actions/checkout@v3
 
@@ -226,12 +221,9 @@ jobs:
         if: runner.os == 'Linux'
         uses: docker/setup-buildx-action@v1
 
-      - name: Install cross
-        if: matrix.deploy
-        run: cargo install --path . --force
-
       - name: Build xtask
         run: cargo build -p xtask
+
       - name: Prepare Meta
         id: prepare-meta
         timeout-minutes: 60
@@ -240,6 +232,18 @@ jobs:
           TARGET: ${{ matrix.target }}
           SUB: ${{ matrix.sub }}
         shell: bash
+
+      - name: LLVM instrument coverage
+        id: cov
+        uses: ./.github/actions/cargo-llvm-cov
+        if: steps.prepare-meta.outputs.has-image
+        with:
+          name: cross-${{matrix.pretty}}
+
+      - name: Install cross
+        if: matrix.deploy
+        run: cargo install --path . --force --debug
+
       - name: Docker Meta
         if: steps.prepare-meta.outputs.has-image
         id: docker-meta
@@ -258,7 +262,6 @@ jobs:
           TARGET: ${{ matrix.target }}
           SUB: ${{ matrix.sub }}
           LABELS: ${{ steps.docker-meta.outputs.labels }}
-          LATEST: ${{ needs.check.outputs.is-latest || 'false' }}
         shell: bash
       - name: Set Docker image for test
         if: steps.prepare-meta.outputs.has-image
@@ -305,11 +308,10 @@ jobs:
           TARGET: ${{ matrix.target }}
           SUB: ${{ matrix.sub }}
           LABELS: ${{ steps.docker-meta.outputs.labels }}
-          LATEST: ${{ needs.check.outputs.is-latest || 'false' }}
         shell: bash
 
   publish:
-    needs: [build, check]
+    needs: [build]
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
@@ -320,7 +322,7 @@ jobs:
           github-token: ${{ secrets.GITHUB_TOKEN }}
 
   conclusion:
-    needs: [shellcheck, fmt, clippy, test, generate-matrix, build, publish, check]
+    needs: [shellcheck, fmt, clippy, test, generate-matrix, build, publish]
     if: always()
     runs-on: ubuntu-latest
     steps:
@@ -332,3 +334,39 @@ jobs:
           "$(jq -r 'all(.result as $result | (["success", "skipped"] | contains([$result])))' <<< "${needs}")"
         env:
           needs: ${{ toJson(needs) }}
+
+  code-cov:
+    name: Coverage
+    needs: [test, build, conclusion, generate-matrix]
+    # should check that there are any artifacts, if not skip
+    if: always() && (needs.build.result == 'success' || needs.build.result == 'skipped') && needs.test.result == 'success'
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ./.github/actions/setup-rust
+      - uses: actions/download-artifact@v3
+        with:
+          path: ${{ runner.temp }}/artifacts
+      - name: Grab PR number
+        run: echo "::set-output name=pr::"$(echo $commit_message | sed -ne  's/.*\#\(.*\):/\1/p')
+        id: pr-number
+        if: ${{ !github.event.pull_request.number }}
+        env:
+          commit_message: >
+            ${{
+              ((
+                startsWith(github.event.head_commit.message, 'Try #') &&
+                github.event.head_commit.author.username == 'bors[bot]'
+              ) && github.event.head_commit.message) || ''
+            }}
+      - name: Upload to codecov.io
+        run: |
+          set -x
+          curl -Os https://uploader.codecov.io/latest/linux/codecov
+          chmod +x codecov
+          sha_rev=$(git rev-parse HEAD)
+          find ${artifacts} -name "lcov.*.info" -exec ./codecov -F $(echo {} | sed -n 's/lcov\.\(.*\)\.info/\1/p') \
+            ${pr:+-P ${pr}} -f {} --sha ${sha_rev} -n $(echo {} | sed -n 's/lcov\.\(.*\)\.info/\1/p') \;
+        env:
+          pr: ${{  steps.pr-number.outputs.pr || '' }}
+          artifacts: ${{ runner.temp }}/artifacts
diff --git a/.github/workflows/weekly.yml b/.github/workflows/weekly.yml
index f6f5cc5..0676951 100644
--- a/.github/workflows/weekly.yml
+++ b/.github/workflows/weekly.yml
@@ -12,7 +12,6 @@ env:
 jobs:
   weekly:
     name: Check All Targets - No Cache
-    timeout-minutes: 1440
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4b46ba7..85bd1d3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,30 +7,16 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ## [Unreleased] - ReleaseDate
 
-### Changed
-
-- #869 - ensure cargo configuration environment variable flags are passed to the docker container.
-- #859 - added color diagnostic output and error messages.
-
-### Fixed
-
-- #868 - ignore the `CARGO` environment variable.
-- #867 - fixed parsing of `build.env.passthrough` config values.
-
-## [v0.2.2] - 2022-06-24
-
 ### Added
 
 - #803 - added `CROSS_CUSTOM_TOOLCHAIN` to disable automatic installation of components for use with tools like `cargo-bisect-rustc`
 - #795 - added images for additional toolchains maintained by cross-rs.
 - #792 - added `CROSS_CONTAINER_IN_CONTAINER` environment variable to replace `CROSS_DOCKER_IN_DOCKER`.
-- #785 - added support for remote container engines through data volumes through setting the `CROSS_REMOTE` environment variable. also adds in utility commands to create and remove persistent data volumes.
 - #782 - added `build-std` config option, which builds the rust standard library from source if enabled.
 - #678 - Add optional `target.{target}.dockerfile[.file]`, `target.{target}.dockerfile.context` and `target.{target}.dockerfile.build-args` to invoke docker/podman build before using an image.
 - #678 - Add `target.{target}.pre-build` config for running commands before building the image.
 - #772 - added `CROSS_CONTAINER_OPTS` environment variable to replace `DOCKER_OPTS`.
 - #767, #788 - added the `cross-util` and `xtask` commands.
-- #842 - Add `Cargo.toml` as configuration source
 - #745 - added `thumbv7neon-*` targets.
 - #741 - added `armv7-unknown-linux-gnueabi` and `armv7-unknown-linux-musleabi` targets.
 - #721 - add support for running doctests on nightly if `CROSS_UNSTABLE_ENABLE_DOCTESTS=true`.
@@ -89,7 +75,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Internal
 
-- #856 - remove use of external wslpath and create internal helper that properly handles UNC paths.
 - #828 - assume paths are Unicode and provide better error messages for path encoding errors.
 - #787 - add installer for git hooks.
 - #786, #791 - Migrate build script to rust: `cargo build-docker-image $TARGET`
@@ -336,9 +321,7 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 <!-- prettier-ignore-start -->
 <!-- next-url -->
 
-[Unreleased]: https://github.com/cross-rs/cross/compare/v0.2.2...HEAD
-
-[v0.2.2]: https://github.com/cross-rs/cross/compare/v0.2.1...v0.2.2
+[Unreleased]: https://github.com/cross-rs/cross/compare/v0.2.1...HEAD
 [v0.2.1]: https://github.com/cross-rs/cross/compare/v0.2.0...v0.2.1
 [v0.2.0]: https://github.com/cross-rs/cross/compare/v0.1.16...v0.2.0
 [v0.1.16]: https://github.com/cross-rs/cross/compare/v0.1.15...v0.1.16
diff --git a/Cargo.lock b/Cargo.lock
index e2e058a..0c663fd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -150,21 +150,18 @@ checksum = "fb58b6451e8c2a812ad979ed1d83378caa5e927eef2622017a45f251457c2c9d"
 
 [[package]]
 name = "cross"
-version = "0.2.2"
+version = "0.2.1"
 dependencies = [
  "atty",
  "clap",
  "color-eyre",
  "const-sha1",
- "ctrlc",
- "directories",
  "dunce",
  "eyre",
  "home",
  "libc",
  "nix",
  "once_cell",
- "owo-colors",
  "regex",
  "rustc_version",
  "serde",
@@ -172,7 +169,6 @@ dependencies = [
  "serde_json",
  "shell-escape",
  "shell-words",
- "tempfile",
  "thiserror",
  "toml",
  "walkdir",
@@ -180,36 +176,6 @@ dependencies = [
  "winapi",
 ]
 
-[[package]]
-name = "ctrlc"
-version = "3.2.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b37feaa84e6861e00a1f5e5aa8da3ee56d605c9992d33e082786754828e20865"
-dependencies = [
- "nix",
- "winapi",
-]
-
-[[package]]
-name = "directories"
-version = "4.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f51c5d4ddabd36886dd3e1438cb358cdcb0d7c499cb99cb4ac2e38e18b5cb210"
-dependencies = [
- "dirs-sys",
-]
-
-[[package]]
-name = "dirs-sys"
-version = "0.3.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b1d1d91c932ef41c0f2663aa8b0ca0342d444d842c06914aa0a7e352d0bada6"
-dependencies = [
- "libc",
- "redox_users",
- "winapi",
-]
-
 [[package]]
 name = "dunce"
 version = "1.0.2"
@@ -232,26 +198,6 @@ dependencies = [
  "once_cell",
 ]
 
-[[package]]
-name = "fastrand"
-version = "1.7.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf"
-dependencies = [
- "instant",
-]
-
-[[package]]
-name = "getrandom"
-version = "0.2.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6"
-dependencies = [
- "cfg-if",
- "libc",
- "wasi",
-]
-
 [[package]]
 name = "gimli"
 version = "0.26.1"
@@ -304,21 +250,6 @@ dependencies = [
  "hashbrown",
 ]
 
-[[package]]
-name = "instant"
-version = "0.1.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
-dependencies = [
- "cfg-if",
-]
-
-[[package]]
-name = "is_ci"
-version = "1.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
-
 [[package]]
 name = "itoa"
 version = "1.0.2"
@@ -395,9 +326,6 @@ name = "owo-colors"
 version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "decf7381921fea4dcb2549c5667eda59b3ec297ab7e2b5fc33eac69d2e7da87b"
-dependencies = [
- "supports-color",
-]
 
 [[package]]
 name = "pin-project-lite"
@@ -447,26 +375,6 @@ dependencies = [
  "proc-macro2",
 ]
 
-[[package]]
-name = "redox_syscall"
-version = "0.2.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42"
-dependencies = [
- "bitflags",
-]
-
-[[package]]
-name = "redox_users"
-version = "0.4.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b"
-dependencies = [
- "getrandom",
- "redox_syscall",
- "thiserror",
-]
-
 [[package]]
 name = "regex"
 version = "1.5.6"
@@ -484,15 +392,6 @@ version = "0.6.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64"
 
-[[package]]
-name = "remove_dir_all"
-version = "0.5.3"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7"
-dependencies = [
- "winapi",
-]
-
 [[package]]
 name = "rustc-demangle"
 version = "0.1.21"
@@ -608,16 +507,6 @@ version = "0.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
-[[package]]
-name = "supports-color"
-version = "1.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4872ced36b91d47bae8a214a683fe54e7078875b399dfa251df346c9b547d1f9"
-dependencies = [
- "atty",
- "is_ci",
-]
-
 [[package]]
 name = "syn"
 version = "1.0.96"
@@ -629,20 +518,6 @@ dependencies = [
  "unicode-ident",
 ]
 
-[[package]]
-name = "tempfile"
-version = "3.3.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4"
-dependencies = [
- "cfg-if",
- "fastrand",
- "libc",
- "redox_syscall",
- "remove_dir_all",
- "winapi",
-]
-
 [[package]]
 name = "termcolor"
 version = "1.1.3"
@@ -767,12 +642,6 @@ dependencies = [
  "winapi-util",
 ]
 
-[[package]]
-name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
-
 [[package]]
 name = "which"
 version = "4.2.5"
@@ -824,7 +693,6 @@ dependencies = [
  "cross",
  "eyre",
  "once_cell",
- "semver",
  "serde",
  "serde_json",
  "serde_yaml",
diff --git a/Cargo.toml b/Cargo.toml
index 53492a4..d260c51 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -6,7 +6,7 @@ keywords = ["cross", "compilation", "testing", "tool"]
 license = "MIT OR Apache-2.0"
 name = "cross"
 repository = "https://github.com/cross-rs/cross"
-version = "0.2.2"
+version = "0.2.1"
 edition = "2021"
 include = [
   "src/**/*",
@@ -43,11 +43,6 @@ serde_json = "1"
 serde_ignored = "0.1.2"
 shell-words = "1.1.0"
 const-sha1 = "0.2.0"
-ctrlc = { version = "3.2.2", features = ["termination"] }
-directories = "4.0.1"
-walkdir = { version = "2", optional = true }
-tempfile = "3.3.0"
-owo-colors = { version = "3.4.0", features = ["supports-colors"] }
 
 [target.'cfg(not(windows))'.dependencies]
 nix = { version = "0.24", default-features = false, features = ["user"] }
@@ -80,7 +75,7 @@ replace = "v{{version}}"
 [[package.metadata.release.pre-release-replacements]]
 file = "CHANGELOG.md"
 search = "\\.\\.\\.HEAD"
-replace = "...v{{version}}"
+replace = "...{{tag_name}}"
 exactly = 1
 
 [[package.metadata.release.pre-release-replacements]]
@@ -97,10 +92,5 @@ exactly = 1
 [[package.metadata.release.pre-release-replacements]]
 file = "CHANGELOG.md"
 search = "<!-- next-url -->"
-replace = "<!-- next-url -->\n\n[Unreleased]: https://github.com/cross-rs/{{crate_name}}/compare/v{{version}}...HEAD"
+replace = "<!-- next-url -->\n\n[Unreleased]: https://github.com/cross-rs/{{crate_name}}/compare/{{tag_name}}...HEAD"
 exactly = 1
-
-[package.metadata.binstall]
-pkg-url = "{ repo }/releases/download/v{ version }/{ name }-{ target }.tar.gz"
-bin-dir = "{ bin }{ binary-ext }"
-pkg-fmt = "tgz"
diff --git a/README.md b/README.md
index e1d7c95..b453c9b 100644
--- a/README.md
+++ b/README.md
@@ -84,27 +84,9 @@ Additional documentation can be found on the [wiki](https://github.com/cross-rs/
 
 ## Configuration
 
-You have three options to configure `cross`. All of these options use the TOML format for configuration and the possible configuration values are documented [here](docs/cross_toml.md).
-
-### Option 1: Configuring `cross` directly in your `Cargo.toml`
-
-You can directly set [configuration values](docs/cross_toml.md) in your `Cargo.toml` file, under the `[package.metadata.cross]` table, i.e. key prefix.
-An example config snippet would look like this:
-
-```
-[package.metadata.cross.target.aarch64-unknown-linux-gnu]
-xargo = false
-image = "test-image"
-runner = "custom-runner"
-```
-
-### Option 2: Configuring `cross` via a `Cross.toml` file
-
-You can put your [configuration](docs/cross_toml.md) inside a `Cross.toml` file in your project root directory.
-
-### Option 3: Using `CROSS_CONFIG` to specify the location of your configuration
-
-By setting the `CROSS_CONFIG` environment variable, you can tell `cross` where it should search for the config file. This way you are not limited to a `Cross.toml` file in the project root.
+You can place a `Cross.toml` file in the root of your Cargo project or use a
+`CROSS_CONFIG` environment variable to tweak `cross`'s behavior. The format
+of `Cross.toml` is documented in [docs/cross_toml.md](docs/cross_toml.md).
 
 ### Custom Docker images
 
@@ -355,24 +337,24 @@ terminate.
 | `riscv64gc-unknown-linux-gnu`        | 2.27   | 7.5.0   | ✓   | 6.1.0 |   ✓    |
 | `s390x-unknown-linux-gnu`            | 2.23   | 5.4.0   | ✓   | 5.1.0 |   ✓    |
 | `sparc64-unknown-linux-gnu`          | 2.23   | 5.4.0   | ✓   | 5.1.0 |   ✓    |
-| `sparcv9-sun-solaris`                | 1.22.7 | 8.4.0   | ✓   | N/A   |       |
 | `thumbv6m-none-eabi` [4]             | 2.2.0  | 4.9.3   |     | N/A   |       |
 | `thumbv7em-none-eabi` [4]            | 2.2.0  | 4.9.3   |     | N/A   |       |
 | `thumbv7em-none-eabihf` [4]          | 2.2.0  | 4.9.3   |     | N/A   |       |
 | `thumbv7m-none-eabi` [4]             | 2.2.0  | 4.9.3   |     | N/A   |       |
 | `thumbv7neon-linux-androideabi` [1]  | 9.0.8  | 9.0.8   | ✓   | 6.1.0 |   ✓    |
 | `thumbv7neon-unknown-linux-gnueabihf`| 2.23   | 5.4.0   | ✓   | 5.1.0 |   ✓    |
-| `wasm32-unknown-emscripten` [6]        | 3.1.14 | 15.0.0  | ✓   | N/A   |   ✓    |
 | `x86_64-linux-android` [1]           | 9.0.8  | 9.0.8   | ✓   | 6.1.0 |   ✓    |
 | `x86_64-pc-windows-gnu`              | N/A    | 7.3     | ✓   | N/A   |   ✓    |
-| `x86_64-sun-solaris`                 | 1.22.7 | 8.4.0   | ✓   | N/A   |       |
 | `x86_64-unknown-freebsd`             | 1.5    | 6.4.0   | ✓   | N/A   |       |
 | `x86_64-unknown-dragonfly` [2] [3]   | 6.0.1  | 5.3.0   | ✓   | N/A   |       |
 | `x86_64-unknown-linux-gnu`           | 2.23   | 5.4.0   | ✓   | 5.1.0 |   ✓    |
 | `x86_64-unknown-linux-gnu:centos` [5]  | 2.17   | 4.8.5   | ✓   | 4.2.1 |   ✓    |
 | `x86_64-unknown-linux-musl`          | 1.1.24  | 9.2.0   | ✓   | N/A   |   ✓    |
 | `x86_64-unknown-netbsd` [3]          | 9.2.0  | 9.4.0   | ✓   | N/A   |       |
-<!--| `asmjs-unknown-emscripten` [7]       | 3.1.14 | 15.0.0  | ✓   | N/A   |   ✓    |-->
+<!--| `asmjs-unknown-emscripten` [6]       | 1.2.2  | 3.1.10  | ✓   | N/A   |   ✓    |-->
+<!--| `wasm32-unknown-emscripten` [6]      | 1.2.2  | 3.1.10  | ✓   | N/A   |   ✓    |-->
+<!--| `sparcv9-sun-solaris` [3]            | 2.11   | 5.3.0   | ✓   | N/A   |        |-->
+<!--| `x86_64-sun-solaris` [3]             | 2.11   | 5.3.0   | ✓   | N/A   |        |-->
 
 [1] libc = bionic; Only works with native tests, that is, tests that do not depends on the
     Android Runtime. For i686 some tests may fails with the error `assertion
@@ -389,13 +371,10 @@ terminate.
 
 [5] Must change `image = "x86_64-unknown-linux-gnu:main-centos"` in `Cross.toml` for `[target.x86_64-unknown-linux-gnu]` to use the CentOS7-compatible target.
 
-[6] libc = emscripten and GCC = clang
-
-<!--[7] libc = emscripten and GCC = clang. The Docker images for these targets are currently not built automatically
-due to a [compiler bug](https://github.com/rust-lang/rust/issues/98216), you will have to build them yourself for now.-->
+<!--[6] libc = musl, gcc = emcc. The Docker images for these targets are currently not built automatically
+due to a [compiler bug](https://github.com/rust-lang/rust/issues/85821), you will have to build them yourself for now.-->
 
 Additional Dockerfiles for other targets can be found in [cross-toolchains](https://github.com/cross-rs/cross-toolchains).
-These include MSVC and Apple Darwin targets, which we cannot ship pre-built images of.
 
 ## Debugging
 
diff --git a/deny.toml b/deny.toml
index 3f77553..c2ef045 100644
--- a/deny.toml
+++ b/deny.toml
@@ -24,22 +24,11 @@ unknown-git = "deny"
 allow-git = []
 
 [licenses]
-# need this since to suppress errors in case we add crates with these allowed licenses
-unused-allowed-license = "allow"
 unlicensed = "deny"
 allow-osi-fsf-free = "neither"
 copyleft = "deny"
 confidence-threshold = 0.93
-allow = [
-  "Apache-2.0",
-  "MIT",
-  "CC0-1.0",
-  "ISC",
-  "0BSD",
-  "BSD-2-Clause",
-  "BSD-3-Clause",
-  "Unlicense"
-]
+allow = ["Apache-2.0", "MIT", "CC0-1.0"]
 
 [licenses.private]
 ignore = true
diff --git a/docker/Dockerfile.asmjs-unknown-emscripten b/docker/Dockerfile.asmjs-unknown-emscripten
index 31781fc..bb32d66 100644
--- a/docker/Dockerfile.asmjs-unknown-emscripten
+++ b/docker/Dockerfile.asmjs-unknown-emscripten
@@ -1,4 +1,4 @@
-FROM emscripten/emsdk:3.1.14
+FROM emscripten/emsdk:3.1.10
 WORKDIR /
 ARG DEBIAN_FRONTEND=noninteractive
 
diff --git a/docker/Dockerfile.wasm32-unknown-emscripten b/docker/Dockerfile.wasm32-unknown-emscripten
index 2d040a7..8b458cb 100644
--- a/docker/Dockerfile.wasm32-unknown-emscripten
+++ b/docker/Dockerfile.wasm32-unknown-emscripten
@@ -1,4 +1,4 @@
-FROM emscripten/emsdk:3.1.14
+FROM emscripten/emsdk:3.1.10
 WORKDIR /
 ARG DEBIAN_FRONTEND=noninteractive
 
diff --git a/docker/cross-toolchains b/docker/cross-toolchains
index 852e448..a49c817 160000
--- a/docker/cross-toolchains
+++ b/docker/cross-toolchains
@@ -1,1 +1,1 @@
-Subproject commit 852e448897beb8d6c3ca31cd7188f4d8b97799a4
+Subproject commit a49c8176cbcc0fa70e4690be9510722e7a11d0f2
diff --git a/docs/cross_toml.md b/docs/cross_toml.md
index 51f8783..bedfc49 100644
--- a/docs/cross_toml.md
+++ b/docs/cross_toml.md
@@ -1,6 +1,4 @@
-The `cross` configuration in the `Cross.toml` file, can contain the elements described below.
-
-If the configuration is given in the `Cargo.toml`, these table headers must be of the form `[package.metadata.cross.<KEY>]`.
+The `cross` configuration in the `Cross.toml` file, can contain the following elements:
 
 # `build`
 
diff --git a/src/bin/commands/clean.rs b/src/bin/commands/clean.rs
deleted file mode 100644
index 35c9db7..0000000
--- a/src/bin/commands/clean.rs
+++ /dev/null
@@ -1,96 +0,0 @@
-use std::fs;
-
-use super::containers::*;
-use super::images::*;
-use clap::Args;
-use cross::shell::{self, MessageInfo};
-
-#[derive(Args, Debug)]
-pub struct Clean {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Force removal of images.
-    #[clap(short, long)]
-    pub force: bool,
-    /// Remove local (development) images.
-    #[clap(short, long)]
-    pub local: bool,
-    /// Remove images. Default is a dry run.
-    #[clap(short, long)]
-    pub execute: bool,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl Clean {
-    pub fn run(self, engine: cross::docker::Engine) -> cross::Result<()> {
-        let msg_info = MessageInfo::create(self.verbose, self.quiet, self.color.as_deref())?;
-        let tempdir = cross::temp::dir()?;
-        match self.execute {
-            true => {
-                if tempdir.exists() {
-                    fs::remove_dir_all(tempdir)?;
-                }
-            }
-            false => shell::print(
-                format!(
-                    "fs::remove_dir_all({})",
-                    cross::pretty_path(&tempdir, |_| false)
-                ),
-                msg_info,
-            )?,
-        }
-
-        // containers -> images -> volumes -> prune to ensure no conflicts.
-        let remove_containers = RemoveAllContainers {
-            verbose: self.verbose,
-            quiet: self.quiet,
-            color: self.color.clone(),
-            force: self.force,
-            execute: self.execute,
-            engine: None,
-        };
-        remove_containers.run(engine.clone())?;
-
-        let remove_images = RemoveImages {
-            targets: vec![],
-            verbose: self.verbose,
-            quiet: self.quiet,
-            color: self.color.clone(),
-            force: self.force,
-            local: self.local,
-            execute: self.execute,
-            engine: None,
-        };
-        remove_images.run(engine.clone())?;
-
-        let remove_volumes = RemoveAllVolumes {
-            verbose: self.verbose,
-            quiet: self.quiet,
-            color: self.color.clone(),
-            force: self.force,
-            execute: self.execute,
-            engine: None,
-        };
-        remove_volumes.run(engine.clone())?;
-
-        let prune_volumes = PruneVolumes {
-            verbose: self.verbose,
-            quiet: self.quiet,
-            color: self.color.clone(),
-            execute: self.execute,
-            engine: None,
-        };
-        prune_volumes.run(engine)?;
-
-        Ok(())
-    }
-}
diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs
deleted file mode 100644
index fc0582e..0000000
--- a/src/bin/commands/containers.rs
+++ /dev/null
@@ -1,592 +0,0 @@
-use std::io;
-
-use clap::{Args, Subcommand};
-use cross::shell::{self, MessageInfo, Stream};
-use cross::{docker, CommandExt};
-
-#[derive(Args, Debug)]
-pub struct ListVolumes {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl ListVolumes {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        list_volumes(self, &engine)
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct RemoveAllVolumes {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Force removal of volumes.
-    #[clap(short, long)]
-    pub force: bool,
-    /// Remove volumes. Default is a dry run.
-    #[clap(short, long)]
-    pub execute: bool,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl RemoveAllVolumes {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        remove_all_volumes(self, &engine)
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct PruneVolumes {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Remove volumes. Default is a dry run.
-    #[clap(short, long)]
-    pub execute: bool,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl PruneVolumes {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        prune_volumes(self, &engine)
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct CreateVolume {
-    /// If cross is running inside a container.
-    #[clap(short, long)]
-    pub docker_in_docker: bool,
-    /// If we should copy the cargo registry to the volume.
-    #[clap(short, long)]
-    pub copy_registry: bool,
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl CreateVolume {
-    pub fn run(self, engine: docker::Engine, channel: Option<&str>) -> cross::Result<()> {
-        create_persistent_volume(self, &engine, channel)
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct RemoveVolume {
-    /// Triple for the target platform.
-    #[clap(long)]
-    pub target: String,
-    /// If cross is running inside a container.
-    #[clap(short, long)]
-    pub docker_in_docker: bool,
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl RemoveVolume {
-    pub fn run(self, engine: docker::Engine, channel: Option<&str>) -> cross::Result<()> {
-        remove_persistent_volume(self, &engine, channel)
-    }
-}
-
-#[derive(Subcommand, Debug)]
-pub enum Volumes {
-    /// List cross data volumes in local storage.
-    List(ListVolumes),
-    /// Remove cross data volumes in local storage.
-    RemoveAll(RemoveAllVolumes),
-    /// Prune volumes not used by any container.
-    Prune(PruneVolumes),
-    /// Create a persistent data volume for the current toolchain.
-    Create(CreateVolume),
-    /// Remove a persistent data volume for the current toolchain.
-    Remove(RemoveVolume),
-}
-
-impl Volumes {
-    pub fn run(self, engine: docker::Engine, toolchain: Option<&str>) -> cross::Result<()> {
-        match self {
-            Volumes::List(args) => args.run(engine),
-            Volumes::RemoveAll(args) => args.run(engine),
-            Volumes::Prune(args) => args.run(engine),
-            Volumes::Create(args) => args.run(engine, toolchain),
-            Volumes::Remove(args) => args.run(engine, toolchain),
-        }
-    }
-
-    pub fn engine(&self) -> Option<&str> {
-        match self {
-            Volumes::List(l) => l.engine.as_deref(),
-            Volumes::RemoveAll(l) => l.engine.as_deref(),
-            Volumes::Prune(l) => l.engine.as_deref(),
-            Volumes::Create(l) => l.engine.as_deref(),
-            Volumes::Remove(l) => l.engine.as_deref(),
-        }
-    }
-
-    pub fn verbose(&self) -> bool {
-        match self {
-            Volumes::List(l) => l.verbose,
-            Volumes::RemoveAll(l) => l.verbose,
-            Volumes::Prune(l) => l.verbose,
-            Volumes::Create(l) => l.verbose,
-            Volumes::Remove(l) => l.verbose,
-        }
-    }
-
-    pub fn quiet(&self) -> bool {
-        match self {
-            Volumes::List(l) => l.quiet,
-            Volumes::RemoveAll(l) => l.quiet,
-            Volumes::Prune(l) => l.quiet,
-            Volumes::Create(l) => l.quiet,
-            Volumes::Remove(l) => l.quiet,
-        }
-    }
-
-    pub fn color(&self) -> Option<&str> {
-        match self {
-            Volumes::List(l) => l.color.as_deref(),
-            Volumes::RemoveAll(l) => l.color.as_deref(),
-            Volumes::Prune(l) => l.color.as_deref(),
-            Volumes::Create(l) => l.color.as_deref(),
-            Volumes::Remove(l) => l.color.as_deref(),
-        }
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct ListContainers {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl ListContainers {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        list_containers(self, &engine)
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct RemoveAllContainers {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
-    /// Force removal of containers.
-    #[clap(short, long)]
-    pub force: bool,
-    /// Remove containers. Default is a dry run.
-    #[clap(short, long)]
-    pub execute: bool,
-    /// Container engine (such as docker or podman).
-    #[clap(long)]
-    pub engine: Option<String>,
-}
-
-impl RemoveAllContainers {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        remove_all_containers(self, &engine)
-    }
-}
-
-#[derive(Subcommand, Debug)]
-pub enum Containers {
-    /// List cross containers in local storage.
-    List(ListContainers),
-    /// Stop and remove cross containers in local storage.
-    RemoveAll(RemoveAllContainers),
-}
-
-impl Containers {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
-        match self {
-            Containers::List(args) => args.run(engine),
-            Containers::RemoveAll(args) => args.run(engine),
-        }
-    }
-
-    pub fn engine(&self) -> Option<&str> {
-        match self {
-            Containers::List(l) => l.engine.as_deref(),
-            Containers::RemoveAll(l) => l.engine.as_deref(),
-        }
-    }
-
-    pub fn verbose(&self) -> bool {
-        match self {
-            Containers::List(l) => l.verbose,
-            Containers::RemoveAll(l) => l.verbose,
-        }
-    }
-
-    pub fn quiet(&self) -> bool {
-        match self {
-            Containers::List(l) => l.quiet,
-            Containers::RemoveAll(l) => l.quiet,
-        }
-    }
-
-    pub fn color(&self) -> Option<&str> {
-        match self {
-            Containers::List(l) => l.color.as_deref(),
-            Containers::RemoveAll(l) => l.color.as_deref(),
-        }
-    }
-}
-
-fn get_cross_volumes(engine: &docker::Engine, msg_info: MessageInfo) -> cross::Result<Vec<String>> {
-    let stdout = docker::subcommand(engine, "volume")
-        .arg("list")
-        .args(&["--format", "{{.Name}}"])
-        // handles simple regex: ^ for start of line.
-        .args(&["--filter", "name=^cross-"])
-        .run_and_get_stdout(msg_info)?;
-
-    let mut volumes: Vec<String> = stdout.lines().map(|s| s.to_string()).collect();
-    volumes.sort();
-
-    Ok(volumes)
-}
-
-pub fn list_volumes(
-    ListVolumes {
-        verbose,
-        quiet,
-        color,
-        ..
-    }: ListVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    for line in get_cross_volumes(engine, msg_info)?.iter() {
-        shell::print(line, msg_info)?;
-    }
-
-    Ok(())
-}
-
-pub fn remove_all_volumes(
-    RemoveAllVolumes {
-        verbose,
-        quiet,
-        color,
-        force,
-        execute,
-        ..
-    }: RemoveAllVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let volumes = get_cross_volumes(engine, msg_info)?;
-
-    let mut command = docker::subcommand(engine, "volume");
-    command.arg("rm");
-    if force {
-        command.arg("--force");
-    }
-    command.args(&volumes);
-    if volumes.is_empty() {
-        Ok(())
-    } else if execute {
-        command.run(msg_info, false).map_err(Into::into)
-    } else {
-        shell::note(
-            "this is a dry run. to remove the volumes, pass the `--execute` flag.",
-            msg_info,
-        )?;
-        command.print(msg_info)?;
-        Ok(())
-    }
-}
-
-pub fn prune_volumes(
-    PruneVolumes {
-        verbose,
-        quiet,
-        color,
-        execute,
-        ..
-    }: PruneVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let mut command = docker::subcommand(engine, "volume");
-    command.args(&["prune", "--force"]);
-    if execute {
-        command.run(msg_info, false).map_err(Into::into)
-    } else {
-        shell::note(
-            "this is a dry run. to prune the volumes, pass the `--execute` flag.",
-            msg_info,
-        )?;
-        command.print(msg_info)?;
-        Ok(())
-    }
-}
-
-pub fn create_persistent_volume(
-    CreateVolume {
-        docker_in_docker,
-        copy_registry,
-        verbose,
-        quiet,
-        color,
-        ..
-    }: CreateVolume,
-    engine: &docker::Engine,
-    channel: Option<&str>,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    // we only need a triple that needs docker: the actual target doesn't matter.
-    let triple = cross::Host::X86_64UnknownLinuxGnu.triple();
-    let (target, metadata, dirs) =
-        docker::get_package_info(engine, triple, channel, docker_in_docker, msg_info)?;
-    let container = docker::remote::unique_container_identifier(&target, &metadata, &dirs)?;
-    let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
-
-    if docker::remote::volume_exists(engine, &volume, msg_info)? {
-        eyre::bail!("Error: volume {volume} already exists.");
-    }
-
-    docker::subcommand(engine, "volume")
-        .args(&["create", &volume])
-        .run_and_get_status(msg_info, false)?;
-
-    // stop the container if it's already running
-    let state = docker::remote::container_state(engine, &container, msg_info)?;
-    if !state.is_stopped() {
-        shell::warn("container {container} was running.", msg_info)?;
-        docker::remote::container_stop(engine, &container, msg_info)?;
-    }
-    if state.exists() {
-        shell::warn("container {container} was exited.", msg_info)?;
-        docker::remote::container_rm(engine, &container, msg_info)?;
-    }
-
-    // create a dummy running container to copy data over
-    let mount_prefix = docker::remote::MOUNT_PREFIX;
-    let mut docker = docker::subcommand(engine, "run");
-    docker.args(&["--name", &container]);
-    docker.args(&["-v", &format!("{}:{}", volume, mount_prefix)]);
-    docker.arg("-d");
-    if io::Stdin::is_atty() && io::Stdout::is_atty() && io::Stderr::is_atty() {
-        docker.arg("-t");
-    }
-    docker.arg(docker::UBUNTU_BASE);
-    // ensure the process never exits until we stop it
-    docker.args(&["sh", "-c", "sleep infinity"]);
-    docker.run_and_get_status(msg_info, false)?;
-
-    docker::remote::copy_volume_container_xargo(
-        engine,
-        &container,
-        &dirs.xargo,
-        &target,
-        mount_prefix.as_ref(),
-        msg_info,
-    )?;
-    docker::remote::copy_volume_container_cargo(
-        engine,
-        &container,
-        &dirs.cargo,
-        mount_prefix.as_ref(),
-        copy_registry,
-        msg_info,
-    )?;
-    docker::remote::copy_volume_container_rust(
-        engine,
-        &container,
-        &dirs.sysroot,
-        &target,
-        mount_prefix.as_ref(),
-        true,
-        msg_info,
-    )?;
-
-    docker::remote::container_stop(engine, &container, msg_info)?;
-    docker::remote::container_rm(engine, &container, msg_info)?;
-
-    Ok(())
-}
-
-pub fn remove_persistent_volume(
-    RemoveVolume {
-        target,
-        docker_in_docker,
-        verbose,
-        quiet,
-        color,
-        ..
-    }: RemoveVolume,
-    engine: &docker::Engine,
-    channel: Option<&str>,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let (_, _, dirs) =
-        docker::get_package_info(engine, &target, channel, docker_in_docker, msg_info)?;
-    let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
-
-    if !docker::remote::volume_exists(engine, &volume, msg_info)? {
-        eyre::bail!("Error: volume {volume} does not exist.");
-    }
-
-    docker::remote::volume_rm(engine, &volume, msg_info)?;
-
-    Ok(())
-}
-
-fn get_cross_containers(
-    engine: &docker::Engine,
-    msg_info: MessageInfo,
-) -> cross::Result<Vec<String>> {
-    let stdout = docker::subcommand(engine, "ps")
-        .arg("-a")
-        .args(&["--format", "{{.Names}}: {{.State}}"])
-        // handles simple regex: ^ for start of line.
-        .args(&["--filter", "name=^cross-"])
-        .run_and_get_stdout(msg_info)?;
-
-    let mut containers: Vec<String> = stdout.lines().map(|s| s.to_string()).collect();
-    containers.sort();
-
-    Ok(containers)
-}
-
-pub fn list_containers(
-    ListContainers {
-        verbose,
-        quiet,
-        color,
-        ..
-    }: ListContainers,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    for line in get_cross_containers(engine, msg_info)?.iter() {
-        shell::print(line, msg_info)?;
-    }
-
-    Ok(())
-}
-
-pub fn remove_all_containers(
-    RemoveAllContainers {
-        verbose,
-        quiet,
-        color,
-        force,
-        execute,
-        ..
-    }: RemoveAllContainers,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let containers = get_cross_containers(engine, msg_info)?;
-    let mut running = vec![];
-    let mut stopped = vec![];
-    for container in containers.iter() {
-        // cannot fail, formatted as {{.Names}}: {{.State}}
-        let (name, state) = container.split_once(':').unwrap();
-        let name = name.trim();
-        let state = docker::remote::ContainerState::new(state.trim())?;
-        if state.is_stopped() {
-            stopped.push(name);
-        } else {
-            running.push(name);
-        }
-    }
-
-    let mut commands = vec![];
-    if !running.is_empty() {
-        let mut stop = docker::subcommand(engine, "stop");
-        stop.args(&running);
-        commands.push(stop);
-    }
-
-    if !(stopped.is_empty() && running.is_empty()) {
-        let mut rm = docker::subcommand(engine, "rm");
-        if force {
-            rm.arg("--force");
-        }
-        rm.args(&running);
-        rm.args(&stopped);
-        commands.push(rm);
-    }
-    if execute {
-        for mut command in commands {
-            command.run(msg_info, false)?;
-        }
-    } else {
-        shell::note(
-            "this is a dry run. to remove the containers, pass the `--execute` flag.",
-            msg_info,
-        )?;
-        for command in commands {
-            command.print(msg_info)?;
-        }
-    }
-
-    Ok(())
-}
diff --git a/src/bin/commands/images.rs b/src/bin/commands/images.rs
index 46ebec3..860f0e6 100644
--- a/src/bin/commands/images.rs
+++ b/src/bin/commands/images.rs
@@ -1,13 +1,9 @@
-use std::collections::{BTreeMap, BTreeSet};
-
 use clap::{Args, Subcommand};
-use cross::docker::{self, CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX};
-use cross::shell::{self, MessageInfo, Verbosity};
-use cross::{CommandExt, TargetList};
+use cross::CommandExt;
 
 // known image prefixes, with their registry
 // the docker.io registry can also be implicit
-const GHCR_IO: &str = docker::CROSS_IMAGE;
+const GHCR_IO: &str = cross::docker::CROSS_IMAGE;
 const RUST_EMBEDDED: &str = "rustembedded/cross";
 const DOCKER_IO: &str = "docker.io/rustembedded/cross";
 const IMAGE_PREFIXES: &[&str] = &[GHCR_IO, DOCKER_IO, RUST_EMBEDDED];
@@ -17,21 +13,13 @@ pub struct ListImages {
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
     pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
     /// Container engine (such as docker or podman).
     #[clap(long)]
     pub engine: Option<String>,
-    /// Only list images for specific target(s). By default, list all targets.
-    pub targets: Vec<String>,
 }
 
 impl ListImages {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
+    pub fn run(self, engine: cross::docker::Engine) -> cross::Result<()> {
         list_images(self, &engine)
     }
 }
@@ -43,12 +31,6 @@ pub struct RemoveImages {
     /// Remove images matching provided targets.
     #[clap(short, long)]
     pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
     /// Force removal of images.
     #[clap(short, long)]
     pub force: bool,
@@ -64,7 +46,7 @@ pub struct RemoveImages {
 }
 
 impl RemoveImages {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
+    pub fn run(self, engine: cross::docker::Engine) -> cross::Result<()> {
         if self.targets.is_empty() {
             remove_all_images(self, &engine)
         } else {
@@ -82,7 +64,7 @@ pub enum Images {
 }
 
 impl Images {
-    pub fn run(self, engine: docker::Engine) -> cross::Result<()> {
+    pub fn run(self, engine: cross::docker::Engine) -> cross::Result<()> {
         match self {
             Images::List(args) => args.run(engine),
             Images::Remove(args) => args.run(engine),
@@ -102,20 +84,6 @@ impl Images {
             Images::Remove(l) => l.verbose,
         }
     }
-
-    pub fn quiet(&self) -> bool {
-        match self {
-            Images::List(l) => l.quiet,
-            Images::Remove(l) => l.quiet,
-        }
-    }
-
-    pub fn color(&self) -> Option<&str> {
-        match self {
-            Images::List(l) => l.color.as_deref(),
-            Images::Remove(l) => l.color.as_deref(),
-        }
-    }
 }
 
 #[derive(Debug, PartialOrd, Ord, PartialEq, Eq)]
@@ -126,16 +94,6 @@ struct Image {
     id: String,
 }
 
-impl std::fmt::Display for Image {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        if self.repository == "<none>" {
-            f.write_str(&self.id)
-        } else {
-            f.write_str(&self.name())
-        }
-    }
-}
-
 impl Image {
     fn name(&self) -> String {
         format!("{}:{}", self.repository, self.tag)
@@ -162,35 +120,24 @@ fn is_local_image(tag: &str) -> bool {
 }
 
 fn get_cross_images(
-    engine: &docker::Engine,
-    msg_info: MessageInfo,
+    engine: &cross::docker::Engine,
+    verbose: bool,
     local: bool,
 ) -> cross::Result<Vec<Image>> {
-    let mut images: BTreeSet<_> = cross::docker::subcommand(engine, "images")
-        .args(&["--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
-        .args(&[
-            "--filter",
-            &format!("label={}.for-cross-target", cross::CROSS_LABEL_DOMAIN),
-        ])
-        .run_and_get_stdout(msg_info)?
+    let stdout = cross::docker::subcommand(engine, "images")
+        .arg("--format")
+        .arg("{{.Repository}}:{{.Tag}} {{.ID}}")
+        .run_and_get_stdout(verbose)?;
+
+    let mut images: Vec<Image> = stdout
         .lines()
         .map(parse_image)
+        .filter(|image| is_cross_image(&image.repository))
+        .filter(|image| local || !is_local_image(&image.tag))
         .collect();
+    images.sort();
 
-    let stdout = cross::docker::subcommand(engine, "images")
-        .args(&["--format", "{{.Repository}}:{{.Tag}} {{.ID}}"])
-        .run_and_get_stdout(msg_info)?;
-    let ids: Vec<_> = images.iter().map(|i| i.id.to_string()).collect();
-    images.extend(
-        stdout
-            .lines()
-            .map(parse_image)
-            .filter(|i| !ids.iter().any(|id| id == &i.id))
-            .filter(|image| is_cross_image(&image.repository))
-            .filter(|image| local || !is_local_image(&image.tag)),
-    );
-
-    Ok(images.into_iter().collect())
+    Ok(images)
 }
 
 // the old rustembedded targets had the following format:
@@ -213,145 +160,48 @@ fn rustembedded_target(tag: &str) -> String {
     components.join("-")
 }
 
-fn get_image_target(
-    engine: &cross::docker::Engine,
-    image: &Image,
-    target_list: &TargetList,
-    msg_info: MessageInfo,
-) -> cross::Result<String> {
-    if let Some(stripped) = image.repository.strip_prefix(&format!("{GHCR_IO}/")) {
-        return Ok(stripped.to_string());
+fn get_image_target(image: &Image) -> cross::Result<String> {
+    if let Some(stripped) = image.repository.strip_prefix(GHCR_IO) {
+        Ok(stripped.to_string())
     } else if let Some(tag) = image.tag.strip_prefix(RUST_EMBEDDED) {
-        return Ok(rustembedded_target(tag));
+        Ok(rustembedded_target(tag))
     } else if let Some(tag) = image.tag.strip_prefix(DOCKER_IO) {
-        return Ok(rustembedded_target(tag));
-    } else if image
-        .repository
-        .starts_with(CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX)
-    {
-        if let Some(target) = target_list
-            .triples
-            .iter()
-            .find(|target| image.tag.starts_with(target.as_str()))
-            .cloned()
-        {
-            return Ok(target);
-        }
-    }
-    let mut command = cross::docker::subcommand(engine, "inspect");
-    command.args(&[
-        "--format",
-        &format!(
-            r#"{{{{index .Config.Labels "{}.for-cross-target"}}}}"#,
-            cross::CROSS_LABEL_DOMAIN
-        ),
-    ]);
-    command.arg(&image.id);
-
-    let target = command.run_and_get_stdout(msg_info)?;
-    if target.trim().is_empty() {
-        eyre::bail!("cannot get target for image {}", image)
+        Ok(rustembedded_target(tag))
+    } else {
+        eyre::bail!("cannot get target for image {}", image.name())
     }
-    Ok(target.trim().to_string())
 }
 
 pub fn list_images(
-    ListImages {
-        targets,
-        verbose,
-        quiet,
-        color,
-        ..
-    }: ListImages,
-    engine: &docker::Engine,
+    ListImages { verbose, .. }: ListImages,
+    engine: &cross::docker::Engine,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let cross_images = get_cross_images(engine, msg_info, true)?;
-    let target_list = cross::rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?;
-    let mut map: BTreeMap<String, Vec<Image>> = BTreeMap::new();
-    let mut max_target_len = 0;
-    let mut max_image_len = 0;
-    for image in cross_images {
-        let target = get_image_target(engine, &image, &target_list, msg_info)?;
-        if targets.is_empty() || targets.contains(&target) {
-            if !map.contains_key(&target) {
-                map.insert(target.clone(), vec![]);
-            }
-            max_target_len = target.len().max(max_target_len);
-            max_image_len = image.name().len().max(max_image_len);
-            map.get_mut(&target).expect("map must have key").push(image);
-        }
-    }
-    let mut keys: Vec<&str> = map.iter().map(|(k, _)| k.as_ref()).collect();
-    keys.sort_unstable();
-
-    let print_string = |col1: &str, col2: &str, fill: char| -> cross::Result<()> {
-        let mut row = String::new();
-        row.push('|');
-        row.push(fill);
-        row.push_str(col1);
-        let spaces = max_target_len.max(col1.len()) + 1 - col1.len();
-        for _ in 0..spaces {
-            row.push(fill);
-        }
-        row.push('|');
-        row.push(fill);
-        row.push_str(col2);
-        let spaces = max_image_len.max(col2.len()) + 1 - col2.len();
-        for _ in 0..spaces {
-            row.push(fill);
-        }
-        row.push('|');
-        shell::print(row, msg_info)
-    };
-
-    if targets.len() != 1 {
-        print_string("Targets", "Images", ' ')?;
-        print_string("-------", "------", '-')?;
-    }
-
-    let print_single =
-        |_: &str, image: &Image| -> cross::Result<()> { shell::print(image, msg_info) };
-    let print_table = |target: &str, image: &Image| -> cross::Result<()> {
-        let name = image.name();
-        print_string(target, &name, ' ')
-    };
-
-    for target in keys {
-        for image in map.get(target).expect("map must have key").iter() {
-            if targets.len() == 1 {
-                print_single(target, image)?;
-            } else {
-                print_table(target, image)?;
-            }
-        }
-    }
+    get_cross_images(engine, verbose, true)?
+        .iter()
+        .for_each(|line| println!("{}", line.name()));
 
     Ok(())
 }
 
 fn remove_images(
-    engine: &docker::Engine,
-    images: &[Image],
-    msg_info: MessageInfo,
+    engine: &cross::docker::Engine,
+    images: &[&str],
+    verbose: bool,
     force: bool,
     execute: bool,
 ) -> cross::Result<()> {
-    let mut command = docker::subcommand(engine, "rmi");
+    let mut command = cross::docker::subcommand(engine, "rmi");
     if force {
         command.arg("--force");
     }
-    command.args(images.iter().map(|i| &i.id));
+    command.args(images);
     if images.is_empty() {
         Ok(())
     } else if execute {
-        command.run(msg_info, false).map_err(Into::into)
+        command.run(verbose, false).map_err(Into::into)
     } else {
-        shell::note(
-            "this is a dry run. to remove the images, pass the `--execute` flag.",
-            msg_info,
-        )?;
-        command.print(msg_info)?;
+        eprintln!("note: this is a dry run. to remove the images, pass the `--execute` flag.");
+        command.print_verbose(true);
         Ok(())
     }
 }
@@ -359,44 +209,38 @@ fn remove_images(
 pub fn remove_all_images(
     RemoveImages {
         verbose,
-        quiet,
-        color,
         force,
         local,
         execute,
         ..
     }: RemoveImages,
-    engine: &docker::Engine,
+    engine: &cross::docker::Engine,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let images = get_cross_images(engine, msg_info, local)?;
-    remove_images(engine, &images, msg_info, force, execute)
+    let images = get_cross_images(engine, verbose, local)?;
+    let ids: Vec<&str> = images.iter().map(|i| i.id.as_ref()).collect();
+    remove_images(engine, &ids, verbose, force, execute)
 }
 
 pub fn remove_target_images(
     RemoveImages {
         targets,
         verbose,
-        quiet,
-        color,
         force,
         local,
         execute,
         ..
     }: RemoveImages,
-    engine: &docker::Engine,
+    engine: &cross::docker::Engine,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let cross_images = get_cross_images(engine, msg_info, local)?;
-    let target_list = cross::rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?;
-    let mut images = vec![];
-    for image in cross_images {
-        let target = dbg!(get_image_target(engine, &image, &target_list, msg_info)?);
+    let images = get_cross_images(engine, verbose, local)?;
+    let mut ids = vec![];
+    for image in images.iter() {
+        let target = get_image_target(image)?;
         if targets.contains(&target) {
-            images.push(image);
+            ids.push(image.id.as_ref());
         }
     }
-    remove_images(engine, &images, msg_info, force, execute)
+    remove_images(engine, &ids, verbose, force, execute)
 }
 
 #[cfg(test)]
diff --git a/src/bin/commands/mod.rs b/src/bin/commands/mod.rs
index 30a1c77..f2c4a67 100644
--- a/src/bin/commands/mod.rs
+++ b/src/bin/commands/mod.rs
@@ -1,7 +1,3 @@
-mod clean;
-mod containers;
 mod images;
 
-pub use self::clean::*;
-pub use self::containers::*;
 pub use self::images::*;
diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs
index e5be6ae..b5746fc 100644
--- a/src/bin/cross-util.rs
+++ b/src/bin/cross-util.rs
@@ -1,8 +1,6 @@
 #![deny(missing_debug_implementations, rust_2018_idioms)]
 
 use clap::{CommandFactory, Parser, Subcommand};
-use cross::docker;
-use cross::shell::MessageInfo;
 
 mod commands;
 
@@ -25,17 +23,9 @@ struct CliHidden {
 
 #[derive(Subcommand, Debug)]
 enum Commands {
-    /// Work with cross images in local storage.
+    /// List cross images in local storage.
     #[clap(subcommand)]
     Images(commands::Images),
-    /// Work with cross volumes in local storage.
-    #[clap(subcommand)]
-    Volumes(commands::Volumes),
-    /// Work with cross containers in local storage.
-    #[clap(subcommand)]
-    Containers(commands::Containers),
-    /// Clean all cross data in local storage.
-    Clean(commands::Clean),
 }
 
 fn is_toolchain(toolchain: &str) -> cross::Result<String> {
@@ -49,14 +39,14 @@ fn is_toolchain(toolchain: &str) -> cross::Result<String> {
 
 fn get_container_engine(
     engine: Option<&str>,
-    msg_info: MessageInfo,
-) -> cross::Result<docker::Engine> {
+    verbose: bool,
+) -> cross::Result<cross::docker::Engine> {
     let engine = if let Some(ce) = engine {
         which::which(ce)?
     } else {
-        docker::get_container_engine()?
+        cross::docker::get_container_engine()?
     };
-    docker::Engine::from_path(engine, None, msg_info)
+    cross::docker::Engine::from_path(engine, verbose)
 }
 
 pub fn main() -> cross::Result<()> {
@@ -64,23 +54,7 @@ pub fn main() -> cross::Result<()> {
     let cli = Cli::parse();
     match cli.command {
         Commands::Images(args) => {
-            let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?;
-            let engine = get_container_engine(args.engine(), msg_info)?;
-            args.run(engine)?;
-        }
-        Commands::Volumes(args) => {
-            let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?;
-            let engine = get_container_engine(args.engine(), msg_info)?;
-            args.run(engine, cli.toolchain.as_deref())?;
-        }
-        Commands::Containers(args) => {
-            let msg_info = MessageInfo::create(args.verbose(), args.quiet(), args.color())?;
-            let engine = get_container_engine(args.engine(), msg_info)?;
-            args.run(engine)?;
-        }
-        Commands::Clean(args) => {
-            let msg_info = MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
-            let engine = get_container_engine(args.engine.as_deref(), msg_info)?;
+            let engine = get_container_engine(args.engine(), args.verbose())?;
             args.run(engine)?;
         }
     }
diff --git a/src/bin/cross.rs b/src/bin/cross.rs
index f2f8294..3c8ab6c 100644
--- a/src/bin/cross.rs
+++ b/src/bin/cross.rs
@@ -2,8 +2,6 @@
 
 pub fn main() -> cross::Result<()> {
     cross::install_panic_hook()?;
-    cross::install_termination_hook()?;
-
     let status = cross::run()?;
     let code = status
         .code()
diff --git a/src/cargo.rs b/src/cargo.rs
index 8afe8ca..860c4be 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -4,8 +4,7 @@ use std::process::{Command, ExitStatus};
 
 use crate::cli::Args;
 use crate::errors::*;
-use crate::extensions::CommandExt;
-use crate::shell::MessageInfo;
+use crate::extensions::{env_program, CommandExt};
 
 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 pub enum Subcommand {
@@ -20,20 +19,11 @@ pub enum Subcommand {
     Clippy,
     Metadata,
     List,
-    Clean,
 }
 
 impl Subcommand {
-    pub fn needs_docker(self, is_remote: bool) -> bool {
-        match self {
-            Subcommand::Other | Subcommand::List => false,
-            Subcommand::Clean if !is_remote => false,
-            _ => true,
-        }
-    }
-
-    pub fn needs_host(self, is_remote: bool) -> bool {
-        self == Subcommand::Clean && is_remote
+    pub fn needs_docker(self) -> bool {
+        !matches!(self, Subcommand::Other | Subcommand::List)
     }
 
     pub fn needs_interpreter(self) -> bool {
@@ -50,7 +40,6 @@ impl<'a> From<&'a str> for Subcommand {
         match s {
             "b" | "build" => Subcommand::Build,
             "c" | "check" => Subcommand::Check,
-            "clean" => Subcommand::Clean,
             "doc" => Subcommand::Doc,
             "r" | "run" => Subcommand::Run,
             "rustc" => Subcommand::Rustc,
@@ -113,14 +102,14 @@ impl Package {
 }
 
 pub fn cargo_command() -> Command {
-    Command::new("cargo")
+    Command::new(env_program("CARGO", "cargo"))
 }
 
 /// Cargo metadata with specific invocation
 pub fn cargo_metadata_with_args(
     cd: Option<&Path>,
     args: Option<&Args>,
-    msg_info: MessageInfo,
+    verbose: bool,
 ) -> Result<Option<CargoMetadata>> {
     let mut command = cargo_command();
     command.arg("metadata").args(&["--format-version", "1"]);
@@ -140,7 +129,7 @@ pub fn cargo_metadata_with_args(
     if let Some(features) = args.map(|a| &a.features).filter(|v| !v.is_empty()) {
         command.args([String::from("--features"), features.join(",")]);
     }
-    let output = command.run_and_get_output(msg_info)?;
+    let output = command.run_and_get_output(verbose)?;
     if !output.status.success() {
         // TODO: logging
         return Ok(None);
@@ -159,13 +148,13 @@ pub fn cargo_metadata_with_args(
 }
 
 /// Pass-through mode
-pub fn run(args: &[String], msg_info: MessageInfo) -> Result<ExitStatus> {
+pub fn run(args: &[String], verbose: bool) -> Result<ExitStatus, CommandError> {
     cargo_command()
         .args(args)
-        .run_and_get_status(msg_info, false)
+        .run_and_get_status(verbose, false)
 }
 
 /// run cargo and get the output, does not check the exit status
-pub fn run_and_get_output(args: &[String], msg_info: MessageInfo) -> Result<std::process::Output> {
-    cargo_command().args(args).run_and_get_output(msg_info)
+pub fn run_and_get_output(args: &[String], verbose: bool) -> Result<std::process::Output> {
+    cargo_command().args(args).run_and_get_output(verbose)
 }
diff --git a/src/cli.rs b/src/cli.rs
index 7e8b69a..ad8d6fa 100644
--- a/src/cli.rs
+++ b/src/cli.rs
@@ -4,7 +4,6 @@ use crate::cargo::Subcommand;
 use crate::config::bool_from_envvar;
 use crate::errors::Result;
 use crate::rustc::TargetList;
-use crate::shell::{self, MessageInfo};
 use crate::Target;
 
 #[derive(Debug)]
@@ -18,8 +17,6 @@ pub struct Args {
     pub docker_in_docker: bool,
     pub enable_doctests: bool,
     pub manifest_path: Option<PathBuf>,
-    pub version: bool,
-    pub msg_info: MessageInfo,
 }
 
 // Fix for issue #581. target_dir must be absolute.
@@ -52,21 +49,16 @@ pub fn group_subcommands(stdout: &str) -> (Vec<&str>, Vec<&str>) {
     (cross, host)
 }
 
-pub fn fmt_subcommands(stdout: &str, msg_info: MessageInfo) -> Result<()> {
+pub fn fmt_subcommands(stdout: &str) {
     let (cross, host) = group_subcommands(stdout);
     if !cross.is_empty() {
-        shell::print("Cross Commands:", msg_info)?;
-        for line in cross.iter() {
-            shell::print(line, msg_info)?;
-        }
+        println!("Cross Commands:");
+        cross.iter().for_each(|line| println!("{}", line));
     }
     if !host.is_empty() {
-        shell::print("Host Commands:", msg_info)?;
-        for line in cross.iter() {
-            shell::print(line, msg_info)?;
-        }
+        println!("Host Commands:");
+        host.iter().for_each(|line| println!("{}", line));
     }
-    Ok(())
 }
 
 pub fn parse(target_list: &TargetList) -> Result<Args> {
@@ -77,11 +69,6 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
     let mut target_dir = None;
     let mut sc = None;
     let mut all: Vec<String> = Vec::new();
-    let mut version = false;
-    let mut quiet = false;
-    let mut verbose = false;
-    let mut color = None;
-    let default_msg_info = MessageInfo::default();
 
     {
         let mut args = env::args().skip(1);
@@ -89,20 +76,7 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
             if arg.is_empty() {
                 continue;
             }
-            if matches!(arg.as_str(), "--verbose" | "-v" | "-vv") {
-                verbose = true;
-            } else if matches!(arg.as_str(), "--version" | "-V") {
-                version = true;
-            } else if matches!(arg.as_str(), "--quiet" | "-q") {
-                quiet = true;
-            } else if arg == "--color" {
-                match args.next() {
-                    Some(arg) => color = Some(arg),
-                    None => {
-                        shell::fatal_usage("--color <WHEN>", default_msg_info, 1);
-                    }
-                }
-            } else if arg == "--manifest-path" {
+            if arg == "--manifest-path" {
                 all.push(arg);
                 if let Some(m) = args.next() {
                     let p = PathBuf::from(&m);
@@ -159,13 +133,11 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
         }
     }
 
-    let msg_info = shell::MessageInfo::create(verbose, quiet, color.as_deref())?;
     let docker_in_docker = if let Ok(value) = env::var("CROSS_CONTAINER_IN_CONTAINER") {
         if env::var("CROSS_DOCKER_IN_DOCKER").is_ok() {
-            shell::warn(
-                "using both `CROSS_CONTAINER_IN_CONTAINER` and `CROSS_DOCKER_IN_DOCKER`.",
-                msg_info,
-            )?;
+            eprintln!(
+                "Warning: using both `CROSS_CONTAINER_IN_CONTAINER` and `CROSS_DOCKER_IN_DOCKER`."
+            );
         }
         bool_from_envvar(&value)
     } else if let Ok(value) = env::var("CROSS_DOCKER_IN_DOCKER") {
@@ -189,7 +161,5 @@ pub fn parse(target_list: &TargetList) -> Result<Args> {
         docker_in_docker,
         enable_doctests,
         manifest_path,
-        version,
-        msg_info,
     })
 }
diff --git a/src/config.rs b/src/config.rs
index 4c2ae73..dcc39ed 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,3 @@
-use crate::shell::{self, MessageInfo};
 use crate::{CrossToml, Result, Target, TargetList};
 
 use std::collections::HashMap;
@@ -132,7 +131,7 @@ impl Config {
         }
     }
 
-    pub fn confusable_target(&self, target: &Target, msg_info: MessageInfo) -> Result<()> {
+    pub fn confusable_target(&self, target: &Target) {
         if let Some(keys) = self.toml.as_ref().map(|t| t.targets.keys()) {
             for mentioned_target in keys {
                 let mentioned_target_norm = mentioned_target
@@ -144,15 +143,11 @@ impl Config {
                     .replace(|c| c == '-' || c == '_', "")
                     .to_lowercase();
                 if mentioned_target != target && mentioned_target_norm == target_norm {
-                    shell::warn("a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\".", msg_info)?;
-                    shell::status(
-                        " > Is the target misspelled in the Cross configuration?",
-                        msg_info,
-                    )?;
+                    eprintln!("Warning: a target named \"{mentioned_target}\" is mentioned in the Cross configuration, but the current specified target is \"{target}\".");
+                    eprintln!(" > Is the target misspelled in the Cross configuration?");
                 }
             }
         }
-        Ok(())
     }
 
     fn bool_from_config(
@@ -433,36 +428,20 @@ mod tests {
     mod test_config {
 
         use super::*;
-
-        macro_rules! s {
-            ($x:literal) => {
-                $x.to_string()
-            };
-        }
+        use std::matches;
 
         fn toml(content: &str) -> Result<crate::CrossToml> {
-            Ok(CrossToml::parse_from_cross(content, MessageInfo::default())
-                .wrap_err("couldn't parse toml")?
-                .0)
+            Ok(CrossToml::parse(content).wrap_err("couldn't parse toml")?.0)
         }
 
         #[test]
         pub fn env_and_toml_build_xargo_then_use_env() -> Result<()> {
             let mut map = HashMap::new();
             map.insert("CROSS_BUILD_XARGO", "true");
-            map.insert(
-                "CROSS_BUILD_PRE_BUILD",
-                "apt-get update\napt-get install zlib-dev",
-            );
 
             let env = Environment::new(Some(map));
             let config = Config::new_with(Some(toml(TOML_BUILD_XARGO_FALSE)?), env);
-            assert_eq!(config.xargo(&target()), Some(true));
-            assert_eq!(config.build_std(&target()), None);
-            assert_eq!(
-                config.pre_build(&target())?,
-                Some(vec![s!("apt-get update"), s!("apt-get install zlib-dev")])
-            );
+            assert!(matches!(config.xargo(&target()), Some(true)));
 
             Ok(())
         }
@@ -475,9 +454,8 @@ mod tests {
             let env = Environment::new(Some(map));
 
             let config = Config::new_with(Some(toml(TOML_TARGET_XARGO_FALSE)?), env);
-            assert_eq!(config.xargo(&target()), Some(true));
-            assert_eq!(config.build_std(&target()), Some(true));
-            assert_eq!(config.pre_build(&target())?, None);
+            assert!(matches!(config.xargo(&target()), Some(true)));
+            assert!(matches!(config.build_std(&target()), Some(true)));
 
             Ok(())
         }
@@ -486,82 +464,9 @@ mod tests {
         pub fn env_target_and_toml_build_xargo_then_use_toml() -> Result<()> {
             let mut map = HashMap::new();
             map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "true");
-
             let env = Environment::new(Some(map));
             let config = Config::new_with(Some(toml(TOML_BUILD_XARGO_FALSE)?), env);
-            assert_eq!(config.xargo(&target()), Some(true));
-            assert_eq!(config.build_std(&target()), None);
-            assert_eq!(config.pre_build(&target())?, None);
-
-            Ok(())
-        }
-
-        #[test]
-        pub fn env_target_and_toml_build_pre_build_then_use_toml() -> Result<()> {
-            let mut map = HashMap::new();
-            map.insert(
-                "CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_PRE_BUILD",
-                "dpkg --add-architecture arm64",
-            );
-
-            let env = Environment::new(Some(map));
-            let config = Config::new_with(Some(toml(TOML_BUILD_PRE_BUILD)?), env);
-            assert_eq!(
-                config.pre_build(&target())?,
-                Some(vec![s!("dpkg --add-architecture arm64")])
-            );
-
-            Ok(())
-        }
-
-        #[test]
-        pub fn toml_build_passthrough_then_use_target_passthrough_both() -> Result<()> {
-            let map = HashMap::new();
-            let env = Environment::new(Some(map));
-            let config = Config::new_with(Some(toml(TOML_ARRAYS_BOTH)?), env);
-            assert_eq!(
-                config.env_passthrough(&target())?,
-                Some(vec![s!("VAR1"), s!("VAR2"), s!("VAR3"), s!("VAR4")])
-            );
-            assert_eq!(
-                config.env_volumes(&target())?,
-                Some(vec![s!("VOLUME3"), s!("VOLUME4")])
-            );
-            // TODO(ahuszagh) Need volumes
-
-            Ok(())
-        }
-
-        #[test]
-        pub fn toml_build_passthrough() -> Result<()> {
-            let map = HashMap::new();
-            let env = Environment::new(Some(map));
-            let config = Config::new_with(Some(toml(TOML_ARRAYS_BUILD)?), env);
-            assert_eq!(
-                config.env_passthrough(&target())?,
-                Some(vec![s!("VAR1"), s!("VAR2")])
-            );
-            assert_eq!(
-                config.env_volumes(&target())?,
-                Some(vec![s!("VOLUME1"), s!("VOLUME2")])
-            );
-
-            Ok(())
-        }
-
-        #[test]
-        pub fn toml_target_passthrough() -> Result<()> {
-            let map = HashMap::new();
-            let env = Environment::new(Some(map));
-            let config = Config::new_with(Some(toml(TOML_ARRAYS_TARGET)?), env);
-            assert_eq!(
-                config.env_passthrough(&target())?,
-                Some(vec![s!("VAR3"), s!("VAR4")])
-            );
-            assert_eq!(
-                config.env_volumes(&target())?,
-                Some(vec![s!("VOLUME3"), s!("VOLUME4")])
-            );
+            assert!(matches!(config.xargo(&target()), Some(true)));
 
             Ok(())
         }
@@ -603,7 +508,7 @@ mod tests {
         pub fn no_env_and_no_toml_default_target_then_none() -> Result<()> {
             let config = Config::new_with(None, Environment::new(None));
             let config_target = config.target(&target_list());
-            assert_eq!(config_target, None);
+            assert!(matches!(config_target, None));
 
             Ok(())
         }
@@ -616,7 +521,10 @@ mod tests {
             let config = Config::new_with(Some(toml(TOML_DEFAULT_TARGET)?), env);
 
             let config_target = config.target(&target_list()).unwrap();
-            assert_eq!(config_target.triple(), "armv7-unknown-linux-musleabihf");
+            assert!(matches!(
+                config_target.triple(),
+                "armv7-unknown-linux-musleabihf"
+            ));
 
             Ok(())
         }
@@ -627,7 +535,10 @@ mod tests {
             let config = Config::new_with(Some(toml(TOML_DEFAULT_TARGET)?), env);
 
             let config_target = config.target(&target_list()).unwrap();
-            assert_eq!(config_target.triple(), "aarch64-unknown-linux-gnu");
+            assert!(matches!(
+                config_target.triple(),
+                "aarch64-unknown-linux-gnu"
+            ));
 
             Ok(())
         }
@@ -635,11 +546,6 @@ mod tests {
         static TOML_BUILD_XARGO_FALSE: &str = r#"
     [build]
     xargo = false
-    "#;
-
-        static TOML_BUILD_PRE_BUILD: &str = r#"
-    [build]
-    pre-build = ["apt-get update && apt-get install zlib-dev"]
     "#;
 
         static TOML_TARGET_XARGO_FALSE: &str = r#"
@@ -652,28 +558,6 @@ mod tests {
     volumes = ["VOLUME3", "VOLUME4"]
     [target.aarch64-unknown-linux-gnu]
     xargo = false
-    "#;
-
-        static TOML_ARRAYS_BOTH: &str = r#"
-    [build.env]
-    passthrough = ["VAR1", "VAR2"]
-    volumes = ["VOLUME1", "VOLUME2"]
-
-    [target.aarch64-unknown-linux-gnu.env]
-    passthrough = ["VAR3", "VAR4"]
-    volumes = ["VOLUME3", "VOLUME4"]
-    "#;
-
-        static TOML_ARRAYS_BUILD: &str = r#"
-    [build.env]
-    passthrough = ["VAR1", "VAR2"]
-    volumes = ["VOLUME1", "VOLUME2"]
-    "#;
-
-        static TOML_ARRAYS_TARGET: &str = r#"
-    [target.aarch64-unknown-linux-gnu.env]
-    passthrough = ["VAR3", "VAR4"]
-    volumes = ["VOLUME3", "VOLUME4"]
     "#;
 
         static TOML_DEFAULT_TARGET: &str = r#"
diff --git a/src/cross_toml.rs b/src/cross_toml.rs
index 0eed162..2d07300 100644
--- a/src/cross_toml.rs
+++ b/src/cross_toml.rs
@@ -1,22 +1,20 @@
 #![doc = include_str!("../docs/cross_toml.md")]
 
-use crate::shell::{self, MessageInfo};
 use crate::{config, errors::*};
 use crate::{Target, TargetList};
-use serde::de::DeserializeOwned;
-use serde::{Deserialize, Deserializer, Serialize};
+use serde::Deserialize;
 use std::collections::{BTreeSet, HashMap};
 use std::str::FromStr;
 
 /// Environment configuration
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
+#[derive(Debug, Deserialize, PartialEq, Eq, Default)]
 pub struct CrossEnvConfig {
     volumes: Option<Vec<String>>,
     passthrough: Option<Vec<String>>,
 }
 
 /// Build configuration
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
+#[derive(Debug, Deserialize, PartialEq, Eq, Default)]
 #[serde(rename_all = "kebab-case")]
 pub struct CrossBuildConfig {
     #[serde(default)]
@@ -30,7 +28,7 @@ pub struct CrossBuildConfig {
 }
 
 /// Target configuration
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 pub struct CrossTargetConfig {
     xargo: Option<bool>,
@@ -45,7 +43,7 @@ pub struct CrossTargetConfig {
 }
 
 /// Dockerfile configuration
-#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 #[serde(rename_all = "kebab-case")]
 pub struct CrossTargetDockerfileConfig {
     file: String,
@@ -66,7 +64,7 @@ impl FromStr for CrossTargetDockerfileConfig {
 }
 
 /// Cross configuration
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)]
+#[derive(Debug, Deserialize, PartialEq, Eq)]
 pub struct CrossToml {
     #[serde(default, rename = "target")]
     pub targets: HashMap<Target, CrossTargetConfig>,
@@ -75,145 +73,26 @@ pub struct CrossToml {
 }
 
 impl CrossToml {
-    /// Parses the [`CrossToml`] from all of the config sources
-    pub fn parse(
-        cargo_toml: &str,
-        cross_toml: &str,
-        msg_info: MessageInfo,
-    ) -> Result<(Self, BTreeSet<String>)> {
-        let (cross_toml, mut unused) = Self::parse_from_cross(cross_toml, msg_info)?;
-
-        if let Some((cargo_toml, u_cargo)) = Self::parse_from_cargo(cargo_toml, msg_info)? {
-            unused.extend(u_cargo.into_iter());
-            Ok((cargo_toml.merge(cross_toml)?, unused))
-        } else {
-            Ok((cross_toml, unused))
-        }
-    }
-
     /// Parses the [`CrossToml`] from a string
-    pub fn parse_from_cross(
-        toml_str: &str,
-        msg_info: MessageInfo,
-    ) -> Result<(Self, BTreeSet<String>)> {
-        let mut tomld = toml::Deserializer::new(toml_str);
-        Self::parse_from_deserializer(&mut tomld, msg_info)
-    }
-
-    /// Parses the [`CrossToml`] from a string containing the Cargo.toml contents
-    pub fn parse_from_cargo(
-        cargo_toml_str: &str,
-        msg_info: MessageInfo,
-    ) -> Result<Option<(Self, BTreeSet<String>)>> {
-        let cargo_toml: toml::Value = toml::from_str(cargo_toml_str)?;
-        let cross_metadata_opt = cargo_toml
-            .get("package")
-            .and_then(|p| p.get("metadata"))
-            .and_then(|m| m.get("cross"));
-
-        if let Some(cross_meta) = cross_metadata_opt {
-            Ok(Some(Self::parse_from_deserializer(
-                cross_meta.clone(),
-                msg_info,
-            )?))
-        } else {
-            Ok(None)
-        }
-    }
+    pub fn parse(toml_str: &str) -> Result<(Self, BTreeSet<String>)> {
+        let tomld = &mut toml::Deserializer::new(toml_str);
 
-    /// Parses the [`CrossToml`] from a [`Deserializer`]
-    fn parse_from_deserializer<'de, D>(
-        deserializer: D,
-        msg_info: MessageInfo,
-    ) -> Result<(Self, BTreeSet<String>)>
-    where
-        D: Deserializer<'de>,
-        D::Error: Send + Sync + 'static,
-    {
         let mut unused = BTreeSet::new();
-        let cfg = serde_ignored::deserialize(deserializer, |path| {
+
+        let cfg = serde_ignored::deserialize(tomld, |path| {
             unused.insert(path.to_string());
         })?;
 
         if !unused.is_empty() {
-            shell::warn(
-                format!(
-                    "found unused key(s) in Cross configuration:\n > {}",
-                    unused.clone().into_iter().collect::<Vec<_>>().join(", ")
-                ),
-                msg_info,
-            )?;
+            eprintln!(
+                "Warning: found unused key(s) in Cross configuration:\n > {}",
+                unused.clone().into_iter().collect::<Vec<_>>().join(", ")
+            );
         }
 
         Ok((cfg, unused))
     }
 
-    /// Merges another [`CrossToml`] into `self` and returns a new merged one
-    pub fn merge(self, other: CrossToml) -> Result<CrossToml> {
-        type ValueMap = serde_json::Map<String, serde_json::Value>;
-
-        fn to_map<S: Serialize>(s: S) -> Result<ValueMap> {
-            if let Some(obj) = serde_json::to_value(s)
-                .wrap_err("could not convert CrossToml to serde_json::Value")?
-                .as_object()
-            {
-                Ok(obj.to_owned())
-            } else {
-                eyre::bail!("failed to serialize CrossToml as object");
-            }
-        }
-
-        fn from_map<D: DeserializeOwned>(map: ValueMap) -> Result<D> {
-            let value = serde_json::to_value(map)
-                .wrap_err("could not convert ValueMap to serde_json::Value")?;
-            serde_json::from_value(value)
-                .wrap_err("could not deserialize serde_json::Value to CrossToml")
-        }
-
-        // merge 2 objects. y has precedence over x.
-        fn merge_objects(x: &mut ValueMap, y: &ValueMap) -> Option<()> {
-            // we need to iterate over both keys, so we need a full deduplication
-            let keys: BTreeSet<String> = x.keys().chain(y.keys()).cloned().collect();
-            for key in keys {
-                let in_x = x.contains_key(&key);
-                let in_y = y.contains_key(&key);
-                if !in_x && in_y {
-                    let yk = y[&key].clone();
-                    x.insert(key, yk);
-                    continue;
-                } else if !in_y {
-                    continue;
-                }
-
-                let xk = x.get_mut(&key)?;
-                let yk = y.get(&key)?;
-                if xk.is_null() && !yk.is_null() {
-                    *xk = yk.clone();
-                    continue;
-                } else if yk.is_null() {
-                    continue;
-                }
-
-                // now we've filtered out missing keys and optional values
-                // all key/value pairs should be same type.
-                if xk.is_object() {
-                    merge_objects(xk.as_object_mut()?, yk.as_object()?)?;
-                } else {
-                    *xk = yk.clone();
-                }
-            }
-
-            Some(())
-        }
-
-        // Builds maps of objects
-        let mut self_map = to_map(&self)?;
-        let other_map = to_map(other)?;
-
-        merge_objects(&mut self_map, &other_map).ok_or_else(|| eyre::eyre!("could not merge"))?;
-        from_map(self_map)
-    }
-
     /// Returns the `target.{}.image` part of `Cross.toml`
     pub fn image(&self, target: &Target) -> Option<String> {
         self.get_string(target, |_| None, |t| t.image.as_ref())
@@ -279,11 +158,7 @@ impl CrossToml {
 
     /// Returns the list of environment variables to pass through for `build` and `target`
     pub fn env_passthrough(&self, target: &Target) -> (Option<&[String]>, Option<&[String]>) {
-        self.get_vec(
-            target,
-            |build| build.env.passthrough.as_deref(),
-            |t| t.env.passthrough.as_deref(),
-        )
+        self.get_vec(target, |_| None, |t| t.env.passthrough.as_deref())
     }
 
     /// Returns the list of environment variables to pass through for `build` and `target`
@@ -383,20 +258,6 @@ where
                 Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
             t.map(Some)
         }
-
-        fn visit_none<E>(self) -> Result<Self::Value, E>
-        where
-            E: de::Error,
-        {
-            Ok(None)
-        }
-
-        fn visit_unit<E>(self) -> Result<Self::Value, E>
-        where
-            E: de::Error,
-        {
-            Ok(None)
-        }
     }
 
     deserializer.deserialize_any(StringOrStruct(PhantomData))
@@ -405,16 +266,6 @@ where
 #[cfg(test)]
 mod tests {
     use super::*;
-    const MSG_INFO: MessageInfo = MessageInfo {
-        color_choice: shell::ColorChoice::Never,
-        verbosity: shell::Verbosity::Quiet,
-    };
-
-    macro_rules! s {
-        ($x:literal) => {
-            $x.to_string()
-        };
-    }
 
     #[test]
     pub fn parse_empty_toml() -> Result<()> {
@@ -422,7 +273,7 @@ mod tests {
             targets: HashMap::new(),
             build: CrossBuildConfig::default(),
         };
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross("", MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse("")?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -456,7 +307,7 @@ mod tests {
           volumes = ["VOL1_ARG", "VOL2_ARG"]
           passthrough = ["VAR1", "VAR2"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse(test_str)?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -500,7 +351,7 @@ mod tests {
             image = "test-image"
             pre-build = []
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse(test_str)?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -564,215 +415,11 @@ mod tests {
             [target.aarch64-unknown-linux-gnu.env]
             volumes = ["VOL"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, MSG_INFO)?;
+        let (parsed_cfg, unused) = CrossToml::parse(test_str)?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
 
         Ok(())
     }
-
-    #[test]
-    pub fn parse_from_empty_cargo_toml() -> Result<()> {
-        let test_str = r#"
-          [package]
-          name = "cargo_toml_test_package"
-          version = "0.1.0"
-
-          [dependencies]
-          cross = "1.2.3"
-        "#;
-
-        let res = CrossToml::parse_from_cargo(test_str, MSG_INFO)?;
-        assert!(res.is_none());
-
-        Ok(())
-    }
-
-    #[test]
-    pub fn parse_from_cargo_toml() -> Result<()> {
-        let cfg = CrossToml {
-            targets: HashMap::new(),
-            build: CrossBuildConfig {
-                env: CrossEnvConfig {
-                    passthrough: None,
-                    volumes: None,
-                },
-                build_std: None,
-                xargo: Some(true),
-                default_target: None,
-                pre_build: None,
-                dockerfile: None,
-            },
-        };
-
-        let test_str = r#"
-          [package]
-          name = "cargo_toml_test_package"
-          version = "0.1.0"
-
-          [dependencies]
-          cross = "1.2.3"
-
-          [package.metadata.cross.build]
-          xargo = true
-        "#;
-
-        if let Some((parsed_cfg, _unused)) = CrossToml::parse_from_cargo(test_str, MSG_INFO)? {
-            assert_eq!(parsed_cfg, cfg);
-        } else {
-            panic!("Parsing result is None");
-        }
-
-        Ok(())
-    }
-
-    #[test]
-    pub fn merge() -> Result<()> {
-        let cfg1_str = r#"
-            [target.aarch64-unknown-linux-gnu]
-            xargo = false
-            build-std = true
-            image = "test-image1"
-
-            [target.aarch64-unknown-linux-gnu.env]
-            volumes = ["VOL1_ARG"]
-            passthrough = ["VAR1"]
-
-            [target.target2]
-            xargo = false
-            build-std = true
-            image = "test-image2"
-
-            [target.target2.env]
-            volumes = ["VOL2_ARG"]
-            passthrough = ["VAR2"]
-
-            [build]
-            build-std = true
-            xargo = true
-
-            [build.env]
-            volumes = []
-            passthrough = ["VAR1", "VAR2"]
-        "#;
-
-        let cfg2_str = r#"
-            [target.target2]
-            xargo = false
-            build-std = false
-            image = "test-image2-precedence"
-
-            [target.target2.env]
-            volumes = ["VOL2_ARG_PRECEDENCE"]
-            passthrough = ["VAR2_PRECEDENCE"]
-
-            [target.target3]
-            xargo = false
-            build-std = true
-            image = "test-image3"
-
-            [target.target3.env]
-            volumes = ["VOL3_ARG"]
-            passthrough = ["VAR3"]
-
-            [build]
-            build-std = true
-            xargo = false
-            default-target = "aarch64-unknown-linux-gnu"
-
-            [build.env]
-            volumes = []
-            passthrough = ["VAR3", "VAR4"]
-
-        "#;
-
-        let cfg_expected_str = r#"
-            [target.aarch64-unknown-linux-gnu]
-            xargo = false
-            build-std = true
-            image = "test-image1"
-
-            [target.aarch64-unknown-linux-gnu.env]
-            volumes = ["VOL1_ARG"]
-            passthrough = ["VAR1"]
-
-            [target.target2]
-            xargo = false
-            build-std = false
-            image = "test-image2-precedence"
-
-            [target.target2.env]
-            volumes = ["VOL2_ARG_PRECEDENCE"]
-            passthrough = ["VAR2_PRECEDENCE"]
-
-            [target.target3]
-            xargo = false
-            build-std = true
-            image = "test-image3"
-
-            [target.target3.env]
-            volumes = ["VOL3_ARG"]
-            passthrough = ["VAR3"]
-
-            [build]
-            build-std = true
-            xargo = false
-            default-target = "aarch64-unknown-linux-gnu"
-
-            [build.env]
-            volumes = []
-            passthrough = ["VAR3", "VAR4"]
-        "#;
-
-        // Parses configs
-        let (cfg1, _) = CrossToml::parse_from_cross(cfg1_str, MSG_INFO)?;
-        let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str, MSG_INFO)?;
-        let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str, MSG_INFO)?;
-
-        // Merges config and compares
-        let cfg_merged = cfg1.merge(cfg2)?;
-        assert_eq!(cfg_expected, cfg_merged);
-
-        // need to test individual values. i've broken this down into
-        // tests on values for better error reporting
-        let build = &cfg_expected.build;
-        assert_eq!(build.build_std, Some(true));
-        assert_eq!(build.xargo, Some(false));
-        assert_eq!(build.default_target, Some(s!("aarch64-unknown-linux-gnu")));
-        assert_eq!(build.pre_build, None);
-        assert_eq!(build.dockerfile, None);
-        assert_eq!(build.env.passthrough, Some(vec![s!("VAR3"), s!("VAR4")]));
-        assert_eq!(build.env.volumes, Some(vec![]));
-
-        let targets = &cfg_expected.targets;
-        let aarch64 = &targets[&Target::new_built_in("aarch64-unknown-linux-gnu")];
-        assert_eq!(aarch64.build_std, Some(true));
-        assert_eq!(aarch64.xargo, Some(false));
-        assert_eq!(aarch64.image, Some(s!("test-image1")));
-        assert_eq!(aarch64.pre_build, None);
-        assert_eq!(aarch64.dockerfile, None);
-        assert_eq!(aarch64.env.passthrough, Some(vec![s!("VAR1")]));
-        assert_eq!(aarch64.env.volumes, Some(vec![s!("VOL1_ARG")]));
-
-        let target2 = &targets[&Target::new_custom("target2")];
-        assert_eq!(target2.build_std, Some(false));
-        assert_eq!(target2.xargo, Some(false));
-        assert_eq!(target2.image, Some(s!("test-image2-precedence")));
-        assert_eq!(target2.pre_build, None);
-        assert_eq!(target2.dockerfile, None);
-        assert_eq!(target2.env.passthrough, Some(vec![s!("VAR2_PRECEDENCE")]));
-        assert_eq!(target2.env.volumes, Some(vec![s!("VOL2_ARG_PRECEDENCE")]));
-
-        let target3 = &targets[&Target::new_custom("target3")];
-        assert_eq!(target3.build_std, Some(true));
-        assert_eq!(target3.xargo, Some(false));
-        assert_eq!(target3.image, Some(s!("test-image3")));
-        assert_eq!(target3.pre_build, None);
-        assert_eq!(target3.dockerfile, None);
-        assert_eq!(target3.env.passthrough, Some(vec![s!("VAR3")]));
-        assert_eq!(target3.env.volumes, Some(vec![s!("VOL3_ARG")]));
-
-        Ok(())
-    }
 }
diff --git a/src/docker/custom.rs b/src/docker/custom.rs
index 5079de5..6bdafed 100644
--- a/src/docker/custom.rs
+++ b/src/docker/custom.rs
@@ -2,13 +2,10 @@ use std::io::Write;
 use std::path::{Path, PathBuf};
 
 use crate::docker::Engine;
-use crate::shell::MessageInfo;
 use crate::{config::Config, docker, CargoMetadata, Target};
 use crate::{errors::*, file, CommandExt, ToUtf8};
 
-use super::{image_name, parse_docker_opts, path_hash};
-
-pub const CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX: &str = "cross-custom-";
+use super::{image_name, parse_docker_opts};
 
 #[derive(Debug, PartialEq, Eq)]
 pub enum Dockerfile<'a> {
@@ -32,7 +29,7 @@ impl<'a> Dockerfile<'a> {
         host_root: &Path,
         build_args: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
         target_triple: &Target,
-        msg_info: MessageInfo,
+        verbose: bool,
     ) -> Result<String> {
         let mut docker_build = docker::subcommand(engine, "build");
         docker_build.current_dir(host_root);
@@ -54,7 +51,7 @@ impl<'a> Dockerfile<'a> {
             ),
         ]);
 
-        let image_name = self.image_name(target_triple, metadata)?;
+        let image_name = self.image_name(target_triple, metadata);
         docker_build.args(["--tag", &image_name]);
 
         for (key, arg) in build_args.into_iter() {
@@ -101,30 +98,36 @@ impl<'a> Dockerfile<'a> {
             docker_build.arg(".");
         }
 
-        docker_build.run(msg_info, true)?;
+        docker_build.run(verbose, true)?;
         Ok(image_name)
     }
 
-    pub fn image_name(&self, target_triple: &Target, metadata: &CargoMetadata) -> Result<String> {
+    pub fn image_name(&self, target_triple: &Target, metadata: &CargoMetadata) -> String {
         match self {
             Dockerfile::File {
                 name: Some(name), ..
-            } => Ok(name.to_string()),
-            _ => Ok(format!(
-                "{}{package_name}:{target_triple}-{path_hash}{custom}",
-                CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX,
+            } => name.to_string(),
+            _ => format!(
+                "cross-custom-{package_name}:{target_triple}-{path_hash}{custom}",
                 package_name = metadata
                     .workspace_root
                     .file_name()
                     .expect("workspace_root can't end in `..`")
                     .to_string_lossy(),
-                path_hash = path_hash(&metadata.workspace_root)?,
+                path_hash = format!(
+                    "{}",
+                    const_sha1::sha1(&const_sha1::ConstBuffer::from_slice(
+                        metadata.workspace_root.to_string_lossy().as_bytes()
+                    ))
+                )
+                .get(..5)
+                .expect("sha1 is expected to be at least 5 characters long"),
                 custom = if matches!(self, Self::File { .. }) {
                     ""
                 } else {
                     "-pre-build"
                 }
-            )),
+            ),
         }
     }
 
diff --git a/src/docker/engine.rs b/src/docker/engine.rs
index 37a15dd..aa57710 100644
--- a/src/docker/engine.rs
+++ b/src/docker/engine.rs
@@ -2,10 +2,8 @@ use std::env;
 use std::path::{Path, PathBuf};
 use std::process::Command;
 
-use crate::config::bool_from_envvar;
 use crate::errors::*;
 use crate::extensions::CommandExt;
-use crate::shell::MessageInfo;
 
 pub const DOCKER: &str = "docker";
 pub const PODMAN: &str = "podman";
@@ -22,48 +20,28 @@ pub enum EngineType {
 pub struct Engine {
     pub kind: EngineType,
     pub path: PathBuf,
-    pub is_remote: bool,
 }
 
 impl Engine {
-    pub fn new(is_remote: Option<bool>, msg_info: MessageInfo) -> Result<Engine> {
+    pub fn new(verbose: bool) -> Result<Engine> {
         let path = get_container_engine()
             .map_err(|_| eyre::eyre!("no container engine found"))
             .with_suggestion(|| "is docker or podman installed?")?;
-        Self::from_path(path, is_remote, msg_info)
+        Self::from_path(path, verbose)
     }
 
-    pub fn from_path(
-        path: PathBuf,
-        is_remote: Option<bool>,
-        msg_info: MessageInfo,
-    ) -> Result<Engine> {
-        let kind = get_engine_type(&path, msg_info)?;
-        let is_remote = is_remote.unwrap_or_else(Self::is_remote);
-        Ok(Engine {
-            path,
-            kind,
-            is_remote,
-        })
-    }
-
-    pub fn needs_remote(&self) -> bool {
-        self.is_remote && self.kind == EngineType::Podman
-    }
-
-    pub fn is_remote() -> bool {
-        env::var("CROSS_REMOTE")
-            .map(|s| bool_from_envvar(&s))
-            .unwrap_or_default()
+    pub fn from_path(path: PathBuf, verbose: bool) -> Result<Engine> {
+        let kind = get_engine_type(&path, verbose)?;
+        Ok(Engine { path, kind })
     }
 }
 
 // determine if the container engine is docker. this fixes issues with
 // any aliases (#530), and doesn't fail if an executable suffix exists.
-fn get_engine_type(ce: &Path, msg_info: MessageInfo) -> Result<EngineType> {
+fn get_engine_type(ce: &Path, verbose: bool) -> Result<EngineType> {
     let stdout = Command::new(ce)
         .arg("--help")
-        .run_and_get_stdout(msg_info)?
+        .run_and_get_stdout(verbose)?
         .to_lowercase();
 
     if stdout.contains("podman-remote") {
diff --git a/src/docker/local.rs b/src/docker/local.rs
index 7282a32..d65987f 100644
--- a/src/docker/local.rs
+++ b/src/docker/local.rs
@@ -1,4 +1,3 @@
-use std::io;
 use std::path::Path;
 use std::process::ExitStatus;
 
@@ -7,32 +6,32 @@ use super::shared::*;
 use crate::cargo::CargoMetadata;
 use crate::errors::Result;
 use crate::extensions::CommandExt;
-use crate::file::{PathExt, ToUtf8};
-use crate::shell::{MessageInfo, Stream};
+use crate::file::ToUtf8;
 use crate::{Config, Target};
+use atty::Stream;
 use eyre::Context;
 
 #[allow(clippy::too_many_arguments)] // TODO: refactor
 pub(crate) fn run(
-    engine: &Engine,
     target: &Target,
     args: &[String],
     metadata: &CargoMetadata,
     config: &Config,
     uses_xargo: bool,
     sysroot: &Path,
-    msg_info: MessageInfo,
+    verbose: bool,
     docker_in_docker: bool,
     cwd: &Path,
 ) -> Result<ExitStatus> {
-    let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker)?;
+    let engine = Engine::new(verbose)?;
+    let dirs = Directories::create(&engine, metadata, cwd, sysroot, docker_in_docker, verbose)?;
 
     let mut cmd = cargo_safe_command(uses_xargo);
     cmd.args(args);
 
-    let mut docker = subcommand(engine, "run");
+    let mut docker = subcommand(&engine, "run");
     docker.args(&["--userns", "host"]);
-    docker_envvars(&mut docker, config, target, msg_info)?;
+    docker_envvars(&mut docker, config, target)?;
 
     let mount_volumes = docker_mount(
         &mut docker,
@@ -40,13 +39,14 @@ pub(crate) fn run(
         config,
         target,
         cwd,
-        |docker, val| mount(docker, val, ""),
+        verbose,
+        |docker, val, verbose| mount(docker, val, "", verbose),
         |_| {},
     )?;
 
     docker.arg("--rm");
 
-    docker_seccomp(&mut docker, engine.kind, target, metadata)?;
+    docker_seccomp(&mut docker, engine.kind, target, metadata, verbose)?;
     docker_user_id(&mut docker, engine.kind);
 
     docker
@@ -57,7 +57,11 @@ pub(crate) fn run(
     if mount_volumes {
         docker.args(&[
             "-v",
-            &format!("{}:{}:Z", dirs.host_root.to_utf8()?, dirs.mount_root),
+            &format!(
+                "{}:{}:Z",
+                dirs.host_root.to_utf8()?,
+                dirs.mount_root.to_utf8()?
+            ),
         ]);
     } else {
         docker.args(&["-v", &format!("{}:/project:Z", dirs.host_root.to_utf8()?)]);
@@ -72,25 +76,25 @@ pub(crate) fn run(
     if let Some(ref nix_store) = dirs.nix_store {
         docker.args(&[
             "-v",
-            &format!("{}:{}:Z", nix_store.to_utf8()?, nix_store.as_posix()?),
+            &format!("{}:{}:Z", nix_store.to_utf8()?, nix_store.to_utf8()?),
         ]);
     }
 
-    if io::Stdin::is_atty() {
+    if atty::is(Stream::Stdin) {
         docker.arg("-i");
-        if io::Stdout::is_atty() && io::Stderr::is_atty() {
+        if atty::is(Stream::Stdout) && atty::is(Stream::Stderr) {
             docker.arg("-t");
         }
     }
     let mut image = image_name(config, target)?;
     if needs_custom_image(target, config) {
-        image = custom_image_build(target, config, metadata, dirs, engine, msg_info)
+        image = custom_image_build(target, config, metadata, dirs, &engine, verbose)
             .wrap_err("when building custom image")?
     }
 
     docker
         .arg(&image)
         .args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)])
-        .run_and_get_status(msg_info, false)
+        .run_and_get_status(verbose, false)
         .map_err(Into::into)
 }
diff --git a/src/docker/mod.rs b/src/docker/mod.rs
index b341736..ab431d4 100644
--- a/src/docker/mod.rs
+++ b/src/docker/mod.rs
@@ -1,7 +1,6 @@
 mod custom;
 mod engine;
 mod local;
-pub mod remote;
 mod shared;
 
 pub use self::engine::*;
@@ -12,47 +11,29 @@ use std::process::ExitStatus;
 
 use crate::cargo::CargoMetadata;
 use crate::errors::*;
-use crate::shell::MessageInfo;
 use crate::{Config, Target};
 
 #[allow(clippy::too_many_arguments)] // TODO: refactor
 pub fn run(
-    engine: &Engine,
     target: &Target,
     args: &[String],
     metadata: &CargoMetadata,
     config: &Config,
     uses_xargo: bool,
     sysroot: &Path,
-    msg_info: MessageInfo,
+    verbose: bool,
     docker_in_docker: bool,
     cwd: &Path,
 ) -> Result<ExitStatus> {
-    if engine.is_remote {
-        remote::run(
-            engine,
-            target,
-            args,
-            metadata,
-            config,
-            uses_xargo,
-            sysroot,
-            msg_info,
-            docker_in_docker,
-            cwd,
-        )
-    } else {
-        local::run(
-            engine,
-            target,
-            args,
-            metadata,
-            config,
-            uses_xargo,
-            sysroot,
-            msg_info,
-            docker_in_docker,
-            cwd,
-        )
-    }
+    local::run(
+        target,
+        args,
+        metadata,
+        config,
+        uses_xargo,
+        sysroot,
+        verbose,
+        docker_in_docker,
+        cwd,
+    )
 }
diff --git a/src/docker/remote.rs b/src/docker/remote.rs
deleted file mode 100644
index 2167ed5..0000000
--- a/src/docker/remote.rs
+++ /dev/null
@@ -1,1006 +0,0 @@
-use std::collections::BTreeMap;
-use std::io::{self, BufRead, Read, Write};
-use std::path::{Path, PathBuf};
-use std::process::ExitStatus;
-use std::{env, fs, time};
-
-use super::engine::Engine;
-use super::shared::*;
-use crate::cargo::CargoMetadata;
-use crate::config::{bool_from_envvar, Config};
-use crate::errors::Result;
-use crate::extensions::CommandExt;
-use crate::file::{self, PathExt, ToUtf8};
-use crate::rustc::{self, VersionMetaExt};
-use crate::rustup;
-use crate::shell::{self, MessageInfo, Stream};
-use crate::temp;
-use crate::{Host, Target};
-
-// the mount directory for the data volume.
-pub const MOUNT_PREFIX: &str = "/cross";
-
-struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, MessageInfo);
-
-impl<'a> Drop for DeleteVolume<'a> {
-    fn drop(&mut self) {
-        if let VolumeId::Discard(id) = self.1 {
-            volume_rm(self.0, id, self.2).ok();
-        }
-    }
-}
-
-struct DeleteContainer<'a>(&'a Engine, &'a str, MessageInfo);
-
-impl<'a> Drop for DeleteContainer<'a> {
-    fn drop(&mut self) {
-        container_stop(self.0, self.1, self.2).ok();
-        container_rm(self.0, self.1, self.2).ok();
-    }
-}
-
-#[derive(Debug, PartialEq, Eq)]
-pub enum ContainerState {
-    Created,
-    Running,
-    Paused,
-    Restarting,
-    Dead,
-    Exited,
-    DoesNotExist,
-}
-
-impl ContainerState {
-    pub fn new(state: &str) -> Result<Self> {
-        match state {
-            "created" => Ok(ContainerState::Created),
-            "running" => Ok(ContainerState::Running),
-            "paused" => Ok(ContainerState::Paused),
-            "restarting" => Ok(ContainerState::Restarting),
-            "dead" => Ok(ContainerState::Dead),
-            "exited" => Ok(ContainerState::Exited),
-            "" => Ok(ContainerState::DoesNotExist),
-            _ => eyre::bail!("unknown container state: got {state}"),
-        }
-    }
-
-    pub fn is_stopped(&self) -> bool {
-        matches!(self, Self::Exited | Self::DoesNotExist)
-    }
-
-    pub fn exists(&self) -> bool {
-        !matches!(self, Self::DoesNotExist)
-    }
-}
-
-#[derive(Debug)]
-enum VolumeId {
-    Keep(String),
-    Discard(String),
-}
-
-impl VolumeId {
-    fn create(
-        engine: &Engine,
-        toolchain: &str,
-        container: &str,
-        msg_info: MessageInfo,
-    ) -> Result<Self> {
-        if volume_exists(engine, toolchain, msg_info)? {
-            Ok(Self::Keep(toolchain.to_string()))
-        } else {
-            Ok(Self::Discard(container.to_string()))
-        }
-    }
-}
-
-impl AsRef<str> for VolumeId {
-    fn as_ref(&self) -> &str {
-        match self {
-            Self::Keep(s) => s,
-            Self::Discard(s) => s,
-        }
-    }
-}
-
-fn create_volume_dir(
-    engine: &Engine,
-    container: &str,
-    dir: &Path,
-    msg_info: MessageInfo,
-) -> Result<ExitStatus> {
-    // make our parent directory if needed
-    subcommand(engine, "exec")
-        .arg(container)
-        .args(&["sh", "-c", &format!("mkdir -p '{}'", dir.as_posix()?)])
-        .run_and_get_status(msg_info, false)
-        .map_err(Into::into)
-}
-
-// copy files for a docker volume, for remote host support
-fn copy_volume_files(
-    engine: &Engine,
-    container: &str,
-    src: &Path,
-    dst: &Path,
-    msg_info: MessageInfo,
-) -> Result<ExitStatus> {
-    subcommand(engine, "cp")
-        .arg("-a")
-        .arg(src.to_utf8()?)
-        .arg(format!("{container}:{}", dst.as_posix()?))
-        .run_and_get_status(msg_info, false)
-        .map_err(Into::into)
-}
-
-fn is_cachedir_tag(path: &Path) -> Result<bool> {
-    let mut buffer = [b'0'; 43];
-    let mut file = fs::OpenOptions::new().read(true).open(path)?;
-    file.read_exact(&mut buffer)?;
-
-    Ok(&buffer == b"Signature: 8a477f597d28d172789f06886806bc55")
-}
-
-fn is_cachedir(entry: &fs::DirEntry) -> bool {
-    // avoid any cached directories when copying
-    // see https://bford.info/cachedir/
-    if entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
-        let path = entry.path().join("CACHEDIR.TAG");
-        path.exists() && is_cachedir_tag(&path).unwrap_or(false)
-    } else {
-        false
-    }
-}
-
-fn container_path_exists(
-    engine: &Engine,
-    container: &str,
-    path: &Path,
-    msg_info: MessageInfo,
-) -> Result<bool> {
-    Ok(subcommand(engine, "exec")
-        .arg(container)
-        .args(&["bash", "-c", &format!("[[ -d '{}' ]]", path.as_posix()?)])
-        .run_and_get_status(msg_info, true)?
-        .success())
-}
-
-// copy files for a docker volume, for remote host support
-fn copy_volume_files_nocache(
-    engine: &Engine,
-    container: &str,
-    src: &Path,
-    dst: &Path,
-    msg_info: MessageInfo,
-) -> Result<ExitStatus> {
-    // avoid any cached directories when copying
-    // see https://bford.info/cachedir/
-    // SAFETY: safe, single-threaded execution.
-    let tempdir = unsafe { temp::TempDir::new()? };
-    let temppath = tempdir.path();
-    copy_dir(src, temppath, 0, |e, _| is_cachedir(e))?;
-    copy_volume_files(engine, container, temppath, dst, msg_info)
-}
-
-pub fn copy_volume_container_xargo(
-    engine: &Engine,
-    container: &str,
-    xargo_dir: &Path,
-    target: &Target,
-    mount_prefix: &Path,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    // only need to copy the rustlib files for our current target.
-    let triple = target.triple();
-    let relpath = Path::new("lib").join("rustlib").join(&triple);
-    let src = xargo_dir.join(&relpath);
-    let dst = mount_prefix.join("xargo").join(&relpath);
-    if Path::new(&src).exists() {
-        create_volume_dir(engine, container, dst.parent().unwrap(), msg_info)?;
-        copy_volume_files(engine, container, &src, &dst, msg_info)?;
-    }
-
-    Ok(())
-}
-
-pub fn copy_volume_container_cargo(
-    engine: &Engine,
-    container: &str,
-    cargo_dir: &Path,
-    mount_prefix: &Path,
-    copy_registry: bool,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    let dst = mount_prefix.join("cargo");
-    let copy_registry = env::var("CROSS_REMOTE_COPY_REGISTRY")
-        .map(|s| bool_from_envvar(&s))
-        .unwrap_or(copy_registry);
-
-    if copy_registry {
-        copy_volume_files(engine, container, cargo_dir, &dst, msg_info)?;
-    } else {
-        // can copy a limit subset of files: the rest is present.
-        create_volume_dir(engine, container, &dst, msg_info)?;
-        for entry in fs::read_dir(cargo_dir)? {
-            let file = entry?;
-            let basename = file.file_name().to_utf8()?.to_string();
-            if !basename.starts_with('.') && !matches!(basename.as_ref(), "git" | "registry") {
-                copy_volume_files(engine, container, &file.path(), &dst, msg_info)?;
-            }
-        }
-    }
-
-    Ok(())
-}
-
-// recursively copy a directory into another
-fn copy_dir<Skip>(src: &Path, dst: &Path, depth: u32, skip: Skip) -> Result<()>
-where
-    Skip: Copy + Fn(&fs::DirEntry, u32) -> bool,
-{
-    for entry in fs::read_dir(src)? {
-        let file = entry?;
-        if skip(&file, depth) {
-            continue;
-        }
-
-        let src_path = file.path();
-        let dst_path = dst.join(file.file_name());
-        if file.file_type()?.is_file() {
-            fs::copy(&src_path, &dst_path)?;
-        } else {
-            fs::create_dir(&dst_path).ok();
-            copy_dir(&src_path, &dst_path, depth + 1, skip)?;
-        }
-    }
-
-    Ok(())
-}
-
-// copy over files needed for all targets in the toolchain that should never change
-fn copy_volume_container_rust_base(
-    engine: &Engine,
-    container: &str,
-    sysroot: &Path,
-    mount_prefix: &Path,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    // the rust toolchain is quite large, but most of it isn't needed
-    // we need the bin, libexec, and etc directories, and part of the lib directory.
-    let dst = mount_prefix.join("rust");
-    let rustlib = Path::new("lib").join("rustlib");
-    create_volume_dir(engine, container, &dst.join(&rustlib), msg_info)?;
-    for basename in ["bin", "libexec", "etc"] {
-        let file = sysroot.join(basename);
-        copy_volume_files(engine, container, &file, &dst, msg_info)?;
-    }
-
-    // the lib directories are rather large, so we want only a subset.
-    // now, we use a temp directory for everything else in the libdir
-    // we can pretty safely assume we don't have symlinks here.
-
-    // first, copy the shared libraries inside lib, all except rustlib.
-    // SAFETY: safe, single-threaded execution.
-    let tempdir = unsafe { temp::TempDir::new()? };
-    let temppath = tempdir.path();
-    fs::create_dir_all(&temppath.join(&rustlib))?;
-    copy_dir(&sysroot.join("lib"), &temppath.join("lib"), 0, |e, d| {
-        d == 0 && e.file_name() == "rustlib"
-    })?;
-
-    // next, copy the src/etc directories inside rustlib
-    copy_dir(
-        &sysroot.join(&rustlib),
-        &temppath.join(&rustlib),
-        0,
-        |e, d| d == 0 && !(e.file_name() == "src" || e.file_name() == "etc"),
-    )?;
-    copy_volume_files(engine, container, &temppath.join("lib"), &dst, msg_info)?;
-
-    Ok(())
-}
-
-fn copy_volume_container_rust_manifest(
-    engine: &Engine,
-    container: &str,
-    sysroot: &Path,
-    mount_prefix: &Path,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    // copy over all the manifest files in rustlib
-    // these are small text files containing names/paths to toolchains
-    let dst = mount_prefix.join("rust");
-    let rustlib = Path::new("lib").join("rustlib");
-
-    // SAFETY: safe, single-threaded execution.
-    let tempdir = unsafe { temp::TempDir::new()? };
-    let temppath = tempdir.path();
-    fs::create_dir_all(&temppath.join(&rustlib))?;
-    copy_dir(
-        &sysroot.join(&rustlib),
-        &temppath.join(&rustlib),
-        0,
-        |e, d| d != 0 || e.file_type().map(|t| !t.is_file()).unwrap_or(true),
-    )?;
-    copy_volume_files(engine, container, &temppath.join("lib"), &dst, msg_info)?;
-
-    Ok(())
-}
-
-// copy over the toolchain for a specific triple
-pub fn copy_volume_container_rust_triple(
-    engine: &Engine,
-    container: &str,
-    sysroot: &Path,
-    triple: &str,
-    mount_prefix: &Path,
-    skip_exists: bool,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    // copy over the files for a specific triple
-    let dst = mount_prefix.join("rust");
-    let rustlib = Path::new("lib").join("rustlib");
-    let dst_rustlib = dst.join(&rustlib);
-    let src_toolchain = sysroot.join(&rustlib).join(triple);
-    let dst_toolchain = dst_rustlib.join(triple);
-
-    // skip if the toolchain already exists. for the host toolchain
-    // or the first run of the target toolchain, we know it doesn't exist.
-    let mut skip = false;
-    if skip_exists {
-        skip = container_path_exists(engine, container, &dst_toolchain, msg_info)?;
-    }
-    if !skip {
-        copy_volume_files(engine, container, &src_toolchain, &dst_rustlib, msg_info)?;
-    }
-    if !skip && skip_exists {
-        // this means we have a persistent data volume and we have a
-        // new target, meaning we might have new manifests as well.
-        copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, msg_info)?;
-    }
-
-    Ok(())
-}
-
-pub fn copy_volume_container_rust(
-    engine: &Engine,
-    container: &str,
-    sysroot: &Path,
-    target: &Target,
-    mount_prefix: &Path,
-    skip_target: bool,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    let target_triple = target.triple();
-    let image_triple = Host::X86_64UnknownLinuxGnu.triple();
-
-    copy_volume_container_rust_base(engine, container, sysroot, mount_prefix, msg_info)?;
-    copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, msg_info)?;
-    copy_volume_container_rust_triple(
-        engine,
-        container,
-        sysroot,
-        image_triple,
-        mount_prefix,
-        false,
-        msg_info,
-    )?;
-    if !skip_target && target_triple != image_triple {
-        copy_volume_container_rust_triple(
-            engine,
-            container,
-            sysroot,
-            target_triple,
-            mount_prefix,
-            false,
-            msg_info,
-        )?;
-    }
-
-    Ok(())
-}
-
-type FingerprintMap = BTreeMap<String, time::SystemTime>;
-
-fn parse_project_fingerprint(path: &Path) -> Result<FingerprintMap> {
-    let epoch = time::SystemTime::UNIX_EPOCH;
-    let file = fs::OpenOptions::new().read(true).open(path)?;
-    let reader = io::BufReader::new(file);
-    let mut result = BTreeMap::new();
-    for line in reader.lines() {
-        let line = line?;
-        let (timestamp, relpath) = line
-            .split_once('\t')
-            .ok_or_else(|| eyre::eyre!("unable to parse fingerprint line '{line}'"))?;
-        let modified = epoch + time::Duration::from_millis(timestamp.parse::<u64>()?);
-        result.insert(relpath.to_string(), modified);
-    }
-
-    Ok(result)
-}
-
-fn write_project_fingerprint(path: &Path, fingerprint: &FingerprintMap) -> Result<()> {
-    let epoch = time::SystemTime::UNIX_EPOCH;
-    let mut file = fs::OpenOptions::new()
-        .write(true)
-        .truncate(true)
-        .create(true)
-        .open(path)?;
-    for (relpath, modified) in fingerprint {
-        let timestamp = modified.duration_since(epoch)?.as_millis() as u64;
-        writeln!(file, "{timestamp}\t{relpath}")?;
-    }
-
-    Ok(())
-}
-
-fn read_dir_fingerprint(
-    home: &Path,
-    path: &Path,
-    map: &mut FingerprintMap,
-    copy_cache: bool,
-) -> Result<()> {
-    let epoch = time::SystemTime::UNIX_EPOCH;
-    for entry in fs::read_dir(path)? {
-        let file = entry?;
-        let file_type = file.file_type()?;
-        // only parse known files types: 0 or 1 of these tests can pass.
-        if file_type.is_dir() {
-            if copy_cache || !is_cachedir(&file) {
-                read_dir_fingerprint(home, &path.join(file.file_name()), map, copy_cache)?;
-            }
-        } else if file_type.is_file() || file_type.is_symlink() {
-            // we're mounting to the same location, so this should fine
-            // we need to round the modified date to millis.
-            let modified = file.metadata()?.modified()?;
-            let millis = modified.duration_since(epoch)?.as_millis() as u64;
-            let rounded = epoch + time::Duration::from_millis(millis);
-            let relpath = file.path().strip_prefix(home)?.as_posix()?;
-            map.insert(relpath, rounded);
-        }
-    }
-
-    Ok(())
-}
-
-fn get_project_fingerprint(home: &Path, copy_cache: bool) -> Result<FingerprintMap> {
-    let mut result = BTreeMap::new();
-    read_dir_fingerprint(home, home, &mut result, copy_cache)?;
-    Ok(result)
-}
-
-fn get_fingerprint_difference<'a, 'b>(
-    previous: &'a FingerprintMap,
-    current: &'b FingerprintMap,
-) -> (Vec<&'b str>, Vec<&'a str>) {
-    // this can be added or updated
-    let changed: Vec<&str> = current
-        .iter()
-        .filter(|(ref k, ref v1)| {
-            previous
-                .get(&k.to_string())
-                .map(|ref v2| v1 != v2)
-                .unwrap_or(true)
-        })
-        .map(|(k, _)| k.as_str())
-        .collect();
-    let removed: Vec<&str> = previous
-        .iter()
-        .filter(|(ref k, _)| !current.contains_key(&k.to_string()))
-        .map(|(k, _)| k.as_str())
-        .collect();
-    (changed, removed)
-}
-
-// copy files for a docker volume, for remote host support
-// provides a list of files relative to src.
-fn copy_volume_file_list(
-    engine: &Engine,
-    container: &str,
-    src: &Path,
-    dst: &Path,
-    files: &[&str],
-    msg_info: MessageInfo,
-) -> Result<ExitStatus> {
-    // SAFETY: safe, single-threaded execution.
-    let tempdir = unsafe { temp::TempDir::new()? };
-    let temppath = tempdir.path();
-    for file in files {
-        let src_path = src.join(file);
-        let dst_path = temppath.join(file);
-        fs::create_dir_all(dst_path.parent().expect("must have parent"))?;
-        fs::copy(&src_path, &dst_path)?;
-    }
-    copy_volume_files(engine, container, temppath, dst, msg_info)
-}
-
-// removed files from a docker volume, for remote host support
-// provides a list of files relative to src.
-fn remove_volume_file_list(
-    engine: &Engine,
-    container: &str,
-    dst: &Path,
-    files: &[&str],
-    msg_info: MessageInfo,
-) -> Result<ExitStatus> {
-    const PATH: &str = "/tmp/remove_list";
-    let mut script = vec![];
-    if msg_info.verbose() {
-        script.push("set -x".to_string());
-    }
-    script.push(format!(
-        "cat \"{PATH}\" | while read line; do
-    rm -f \"${{line}}\"
-done
-
-rm \"{PATH}\"
-"
-    ));
-
-    // SAFETY: safe, single-threaded execution.
-    let mut tempfile = unsafe { temp::TempFile::new()? };
-    for file in files {
-        writeln!(tempfile.file(), "{}", dst.join(file).as_posix()?)?;
-    }
-
-    // need to avoid having hundreds of files on the command, so
-    // just provide a single file name.
-    subcommand(engine, "cp")
-        .arg(tempfile.path())
-        .arg(format!("{container}:{PATH}"))
-        .run_and_get_status(msg_info, true)?;
-
-    subcommand(engine, "exec")
-        .arg(container)
-        .args(&["sh", "-c", &script.join("\n")])
-        .run_and_get_status(msg_info, true)
-        .map_err(Into::into)
-}
-
-fn copy_volume_container_project(
-    engine: &Engine,
-    container: &str,
-    src: &Path,
-    dst: &Path,
-    volume: &VolumeId,
-    copy_cache: bool,
-    msg_info: MessageInfo,
-) -> Result<()> {
-    let copy_all = || {
-        if copy_cache {
-            copy_volume_files(engine, container, src, dst, msg_info)
-        } else {
-            copy_volume_files_nocache(engine, container, src, dst, msg_info)
-        }
-    };
-    match volume {
-        VolumeId::Keep(_) => {
-            let parent = temp::dir()?;
-            fs::create_dir_all(&parent)?;
-            let fingerprint = parent.join(container);
-            let current = get_project_fingerprint(src, copy_cache)?;
-            if fingerprint.exists() {
-                let previous = parse_project_fingerprint(&fingerprint)?;
-                let (changed, removed) = get_fingerprint_difference(&previous, &current);
-                write_project_fingerprint(&fingerprint, &current)?;
-
-                if !changed.is_empty() {
-                    copy_volume_file_list(engine, container, src, dst, &changed, msg_info)?;
-                }
-                if !removed.is_empty() {
-                    remove_volume_file_list(engine, container, dst, &removed, msg_info)?;
-                }
-            } else {
-                write_project_fingerprint(&fingerprint, &current)?;
-                copy_all()?;
-            }
-        }
-        VolumeId::Discard(_) => {
-            copy_all()?;
-        }
-    }
-
-    Ok(())
-}
-
-fn run_and_get_status(engine: &Engine, args: &[&str], msg_info: MessageInfo) -> Result<ExitStatus> {
-    command(engine)
-        .args(args)
-        .run_and_get_status(msg_info, true)
-        .map_err(Into::into)
-}
-
-pub fn volume_create(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["volume", "create", volume], msg_info)
-}
-
-pub fn volume_rm(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["volume", "rm", volume], msg_info)
-}
-
-pub fn volume_exists(engine: &Engine, volume: &str, msg_info: MessageInfo) -> Result<bool> {
-    command(engine)
-        .args(&["volume", "inspect", volume])
-        .run_and_get_output(msg_info)
-        .map(|output| output.status.success())
-        .map_err(Into::into)
-}
-
-pub fn container_stop(
-    engine: &Engine,
-    container: &str,
-    msg_info: MessageInfo,
-) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["stop", container], msg_info)
-}
-
-pub fn container_rm(engine: &Engine, container: &str, msg_info: MessageInfo) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["rm", container], msg_info)
-}
-
-pub fn container_state(
-    engine: &Engine,
-    container: &str,
-    msg_info: MessageInfo,
-) -> Result<ContainerState> {
-    let stdout = command(engine)
-        .args(&["ps", "-a"])
-        .args(&["--filter", &format!("name={container}")])
-        .args(&["--format", "{{.State}}"])
-        .run_and_get_stdout(msg_info)?;
-    ContainerState::new(stdout.trim())
-}
-
-pub fn unique_toolchain_identifier(sysroot: &Path) -> Result<String> {
-    // try to get the commit hash for the currently toolchain, if possible
-    // if not, get the default rustc and use the path hash for uniqueness
-    let commit_hash = if let Some(version) = rustup::rustc_version_string(sysroot)? {
-        rustc::hash_from_version_string(&version, 1)
-    } else {
-        rustc::version_meta()?.commit_hash()
-    };
-
-    let toolchain_name = sysroot.file_name().unwrap().to_utf8()?;
-    let toolchain_hash = path_hash(sysroot)?;
-    Ok(format!(
-        "cross-{toolchain_name}-{toolchain_hash}-{commit_hash}"
-    ))
-}
-
-// unique identifier for a given project
-pub fn unique_container_identifier(
-    target: &Target,
-    metadata: &CargoMetadata,
-    dirs: &Directories,
-) -> Result<String> {
-    let workspace_root = &metadata.workspace_root;
-    let package = metadata
-        .packages
-        .iter()
-        .find(|p| p.manifest_path.parent().unwrap() == workspace_root)
-        .unwrap_or_else(|| {
-            metadata
-                .packages
-                .get(0)
-                .expect("should have at least 1 package")
-        });
-
-    let name = &package.name;
-    let triple = target.triple();
-    let toolchain_id = unique_toolchain_identifier(&dirs.sysroot)?;
-    let project_hash = path_hash(&package.manifest_path)?;
-    Ok(format!("{toolchain_id}-{triple}-{name}-{project_hash}"))
-}
-
-fn mount_path(val: &Path) -> Result<String> {
-    let host_path = file::canonicalize(val)?;
-    canonicalize_mount_path(&host_path)
-}
-
-#[allow(clippy::too_many_arguments)] // TODO: refactor
-pub(crate) fn run(
-    engine: &Engine,
-    target: &Target,
-    args: &[String],
-    metadata: &CargoMetadata,
-    config: &Config,
-    uses_xargo: bool,
-    sysroot: &Path,
-    msg_info: MessageInfo,
-    docker_in_docker: bool,
-    cwd: &Path,
-) -> Result<ExitStatus> {
-    let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker)?;
-
-    let mount_prefix = MOUNT_PREFIX;
-
-    // the logic is broken into the following steps
-    // 1. get our unique identifiers and cleanup from a previous run.
-    // 2. if not using persistent volumes, create a data volume
-    // 3. start our container with the mounted data volume and all envvars
-    // 4. copy data into the data volume
-    //      with persistent data volumes, copy just copy crate data and
-    //      if not present, the toolchain for the current target.
-    //      otherwise, copy the entire toolchain, cargo, and crate data
-    //      if `CROSS_REMOTE_COPY_CACHE`, copy over the target dir as well
-    // 5. create symlinks for all mounted data
-    //      ensure the paths are the same as local cross
-    // 6. execute our cargo command inside the container
-    // 7. copy data from target dir back to host
-    // 8. stop container and delete data volume
-    //
-    // we use structs that wrap the resources to ensure they're dropped
-    // in the correct order even on error, to ensure safe cleanup
-
-    // 1. get our unique identifiers and cleanup from a previous run.
-    // this can happen if we didn't gracefully exit before
-    let toolchain_id = unique_toolchain_identifier(&dirs.sysroot)?;
-    let container = unique_container_identifier(target, metadata, &dirs)?;
-    let volume = VolumeId::create(engine, &toolchain_id, &container, msg_info)?;
-    let state = container_state(engine, &container, msg_info)?;
-    if !state.is_stopped() {
-        shell::warn("container {container} was running.", msg_info)?;
-        container_stop(engine, &container, msg_info)?;
-    }
-    if state.exists() {
-        shell::warn("container {container} was exited.", msg_info)?;
-        container_rm(engine, &container, msg_info)?;
-    }
-    if let VolumeId::Discard(ref id) = volume {
-        if volume_exists(engine, id, msg_info)? {
-            shell::warn("temporary volume {container} existed.", msg_info)?;
-            volume_rm(engine, id, msg_info)?;
-        }
-    }
-
-    // 2. create our volume to copy all our data over to
-    if let VolumeId::Discard(ref id) = volume {
-        volume_create(engine, id, msg_info)?;
-    }
-    let _volume_deletter = DeleteVolume(engine, &volume, msg_info);
-
-    // 3. create our start container command here
-    let mut docker = subcommand(engine, "run");
-    docker.args(&["--userns", "host"]);
-    docker.args(&["--name", &container]);
-    docker.args(&["-v", &format!("{}:{mount_prefix}", volume.as_ref())]);
-    docker_envvars(&mut docker, config, target, msg_info)?;
-
-    let mut volumes = vec![];
-    let mount_volumes = docker_mount(
-        &mut docker,
-        metadata,
-        config,
-        target,
-        cwd,
-        |_, val| mount_path(val),
-        |(src, dst)| volumes.push((src, dst)),
-    )?;
-
-    docker_seccomp(&mut docker, engine.kind, target, metadata)?;
-
-    // Prevent `bin` from being mounted inside the Docker container.
-    docker.args(&["-v", &format!("{mount_prefix}/cargo/bin")]);
-
-    // When running inside NixOS or using Nix packaging we need to add the Nix
-    // Store to the running container so it can load the needed binaries.
-    if let Some(ref nix_store) = dirs.nix_store {
-        let nix_string = nix_store.to_utf8()?;
-        volumes.push((nix_string.to_string(), nix_string.to_string()))
-    }
-
-    docker.arg("-d");
-    if io::Stdin::is_atty() && io::Stdout::is_atty() && io::Stderr::is_atty() {
-        docker.arg("-t");
-    }
-
-    docker
-        .arg(&image_name(config, target)?)
-        // ensure the process never exits until we stop it
-        .args(&["sh", "-c", "sleep infinity"])
-        .run_and_get_status(msg_info, true)?;
-    let _container_deletter = DeleteContainer(engine, &container, msg_info);
-
-    // 4. copy all mounted volumes over
-    let copy_cache = env::var("CROSS_REMOTE_COPY_CACHE")
-        .map(|s| bool_from_envvar(&s))
-        .unwrap_or_default();
-    let copy = |src, dst: &PathBuf| {
-        if copy_cache {
-            copy_volume_files(engine, &container, src, dst, msg_info)
-        } else {
-            copy_volume_files_nocache(engine, &container, src, dst, msg_info)
-        }
-    };
-    let mount_prefix_path = mount_prefix.as_ref();
-    if let VolumeId::Discard(_) = volume {
-        copy_volume_container_xargo(
-            engine,
-            &container,
-            &dirs.xargo,
-            target,
-            mount_prefix_path,
-            msg_info,
-        )?;
-        copy_volume_container_cargo(
-            engine,
-            &container,
-            &dirs.cargo,
-            mount_prefix_path,
-            false,
-            msg_info,
-        )?;
-        copy_volume_container_rust(
-            engine,
-            &container,
-            &dirs.sysroot,
-            target,
-            mount_prefix_path,
-            false,
-            msg_info,
-        )?;
-    } else {
-        // need to copy over the target triple if it hasn't been previously copied
-        copy_volume_container_rust_triple(
-            engine,
-            &container,
-            &dirs.sysroot,
-            target.triple(),
-            mount_prefix_path,
-            true,
-            msg_info,
-        )?;
-    }
-    let mount_root = if mount_volumes {
-        // cannot panic: absolute unix path, must have root
-        let rel_mount_root = dirs.mount_root.strip_prefix('/').unwrap();
-        let mount_root = mount_prefix_path.join(rel_mount_root);
-        if !rel_mount_root.is_empty() {
-            create_volume_dir(engine, &container, mount_root.parent().unwrap(), msg_info)?;
-        }
-        mount_root
-    } else {
-        mount_prefix_path.join("project")
-    };
-    copy_volume_container_project(
-        engine,
-        &container,
-        &dirs.host_root,
-        &mount_root,
-        &volume,
-        copy_cache,
-        msg_info,
-    )?;
-
-    let mut copied = vec![
-        (&dirs.xargo, mount_prefix_path.join("xargo")),
-        (&dirs.cargo, mount_prefix_path.join("cargo")),
-        (&dirs.sysroot, mount_prefix_path.join("rust")),
-        (&dirs.host_root, mount_root.clone()),
-    ];
-    let mut to_symlink = vec![];
-    let target_dir = file::canonicalize(&dirs.target)?;
-    let target_dir = if let Ok(relpath) = target_dir.strip_prefix(&dirs.host_root) {
-        mount_root.join(relpath)
-    } else {
-        // outside project, need to copy the target data over
-        // only do if we're copying over cached files.
-        let target_dir = mount_prefix_path.join("target");
-        if copy_cache {
-            copy(&dirs.target, &target_dir)?;
-        } else {
-            create_volume_dir(engine, &container, &target_dir, msg_info)?;
-        }
-
-        copied.push((&dirs.target, target_dir.clone()));
-        target_dir
-    };
-    for (src, dst) in volumes.iter() {
-        let src: &Path = src.as_ref();
-        if let Some((psrc, pdst)) = copied.iter().find(|(p, _)| src.starts_with(p)) {
-            // path has already been copied over
-            let relpath = src.strip_prefix(psrc).unwrap();
-            to_symlink.push((pdst.join(relpath), dst));
-        } else {
-            let rel_dst = dst.strip_prefix('/').unwrap();
-            let mount_dst = mount_prefix_path.join(rel_dst);
-            if !rel_dst.is_empty() {
-                create_volume_dir(engine, &container, mount_dst.parent().unwrap(), msg_info)?;
-            }
-            copy(src, &mount_dst)?;
-        }
-    }
-
-    // `clean` doesn't handle symlinks: it will just unlink the target
-    // directory, so we should just substitute it our target directory
-    // for it. we'll still have the same end behavior
-    let mut final_args = vec![];
-    let mut iter = args.iter().cloned();
-    let mut has_target_dir = false;
-    let target_dir_string = target_dir.as_posix()?;
-    while let Some(arg) = iter.next() {
-        if arg == "--target-dir" {
-            has_target_dir = true;
-            final_args.push(arg);
-            if iter.next().is_some() {
-                final_args.push(target_dir_string.clone());
-            }
-        } else if arg.starts_with("--target-dir=") {
-            has_target_dir = true;
-            if arg.split_once('=').is_some() {
-                final_args.push(format!("--target-dir={target_dir_string}"));
-            }
-        } else {
-            final_args.push(arg);
-        }
-    }
-    if !has_target_dir {
-        final_args.push("--target-dir".to_string());
-        final_args.push(target_dir_string);
-    }
-    let mut cmd = cargo_safe_command(uses_xargo);
-    cmd.args(final_args);
-
-    // 5. create symlinks for copied data
-    let mut symlink = vec!["set -e pipefail".to_string()];
-    if msg_info.verbose() {
-        symlink.push("set -x".to_string());
-    }
-    symlink.push(format!(
-        "chown -R {uid}:{gid} {mount_prefix}",
-        uid = user_id(),
-        gid = group_id(),
-    ));
-    // need a simple script to add symlinks, but not override existing files.
-    symlink.push(format!(
-        "prefix=\"{mount_prefix}\"
-
-symlink_recurse() {{
-    for f in \"${{1}}\"/*; do
-        dst=${{f#\"$prefix\"}}
-        if [ -f \"${{dst}}\" ]; then
-            echo \"invalid: got unexpected file at ${{dst}}\" 1>&2
-            exit 1
-        elif [ -d \"${{dst}}\" ]; then
-            symlink_recurse \"${{f}}\"
-        else
-            ln -s \"${{f}}\" \"${{dst}}\"
-        fi
-    done
-}}
-
-symlink_recurse \"${{prefix}}\"
-"
-    ));
-    for (src, dst) in to_symlink {
-        symlink.push(format!("ln -s \"{}\" \"{}\"", src.as_posix()?, dst));
-    }
-    subcommand(engine, "exec")
-        .arg(&container)
-        .args(&["sh", "-c", &symlink.join("\n")])
-        .run_and_get_status(msg_info, false)
-        .map_err::<eyre::ErrReport, _>(Into::into)?;
-
-    // 6. execute our cargo command inside the container
-    let mut docker = subcommand(engine, "exec");
-    docker_user_id(&mut docker, engine.kind);
-    docker_cwd(&mut docker, metadata, &dirs, cwd, mount_volumes)?;
-    docker.arg(&container);
-    docker.args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)]);
-    let status = docker
-        .run_and_get_status(msg_info, false)
-        .map_err(Into::into);
-
-    // 7. copy data from our target dir back to host
-    // this might not exist if we ran `clean`.
-    if container_path_exists(engine, &container, &target_dir, msg_info)? {
-        subcommand(engine, "cp")
-            .arg("-a")
-            .arg(&format!("{container}:{}", target_dir.as_posix()?))
-            .arg(&dirs.target.parent().unwrap())
-            .run_and_get_status(msg_info, false)
-            .map_err::<eyre::ErrReport, _>(Into::into)?;
-    }
-
-    status
-}
diff --git a/src/docker/shared.rs b/src/docker/shared.rs
index 8288f3e..ac3b51a 100644
--- a/src/docker/shared.rs
+++ b/src/docker/shared.rs
@@ -5,21 +5,15 @@ use std::{env, fs};
 
 use super::custom::Dockerfile;
 use super::engine::*;
-use crate::cargo::{cargo_metadata_with_args, CargoMetadata};
+use crate::cargo::CargoMetadata;
 use crate::config::Config;
 use crate::errors::*;
 use crate::extensions::{CommandExt, SafeCommand};
 use crate::file::{self, write_file, PathExt, ToUtf8};
 use crate::id;
-use crate::rustc::{self, VersionMetaExt};
-use crate::shell::{self, MessageInfo, Verbosity};
 use crate::Target;
 
-pub use super::custom::CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX;
-
 pub const CROSS_IMAGE: &str = "ghcr.io/cross-rs";
-// note: this is the most common base image for our images
-pub const UBUNTU_BASE: &str = "ubuntu:16.04";
 const DOCKER_IMAGES: &[&str] = &include!(concat!(env!("OUT_DIR"), "/docker-images.rs"));
 
 // secured profile based off the docker documentation for denied syscalls:
@@ -35,19 +29,20 @@ pub struct Directories {
     pub target: PathBuf,
     pub nix_store: Option<PathBuf>,
     pub host_root: PathBuf,
-    // both mount fields are WSL paths on windows: they already are POSIX paths
-    pub mount_root: String,
-    pub mount_cwd: String,
+    pub mount_root: PathBuf,
+    pub mount_cwd: PathBuf,
     pub sysroot: PathBuf,
 }
 
 impl Directories {
+    #[allow(unused_variables)]
     pub fn create(
         engine: &Engine,
         metadata: &CargoMetadata,
         cwd: &Path,
         sysroot: &Path,
         docker_in_docker: bool,
+        verbose: bool,
     ) -> Result<Self> {
         let mount_finder = if docker_in_docker {
             MountFinder::new(docker_read_mount_paths(engine)?)
@@ -81,28 +76,25 @@ impl Directories {
         });
 
         // root is either workspace_root, or, if we're outside the workspace root, the current directory
-        let mount_root: String;
+        let mount_root: PathBuf;
         #[cfg(target_os = "windows")]
         {
             // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path.
-            mount_root = host_root.as_wslpath()?;
+            mount_root = wslpath(&host_root, verbose)?;
         }
         #[cfg(not(target_os = "windows"))]
         {
-            mount_root = mount_finder
-                .find_mount_path(host_root.clone())
-                .to_utf8()?
-                .to_string();
+            mount_root = mount_finder.find_mount_path(host_root.clone());
         }
-        let mount_cwd: String;
+        let mount_cwd: PathBuf;
         #[cfg(target_os = "windows")]
         {
             // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path.
-            mount_cwd = cwd.as_wslpath()?;
+            mount_cwd = wslpath(cwd, verbose)?;
         }
         #[cfg(not(target_os = "windows"))]
         {
-            mount_cwd = mount_finder.find_mount_path(cwd).to_utf8()?.to_string();
+            mount_cwd = mount_finder.find_mount_path(cwd);
         }
         let sysroot = mount_finder.find_mount_path(sysroot);
 
@@ -138,12 +130,7 @@ fn create_target_dir(path: &Path) -> Result<()> {
 }
 
 pub fn command(engine: &Engine) -> Command {
-    let mut command = Command::new(&engine.path);
-    if engine.needs_remote() {
-        // if we're using podman and not podman-remote, need `--remote`.
-        command.arg("--remote");
-    }
-    command
+    Command::new(&engine.path)
 }
 
 pub fn subcommand(engine: &Engine, subcommand: &str) -> Command {
@@ -152,28 +139,8 @@ pub fn subcommand(engine: &Engine, subcommand: &str) -> Command {
     command
 }
 
-pub fn get_package_info(
-    engine: &Engine,
-    target: &str,
-    channel: Option<&str>,
-    docker_in_docker: bool,
-    msg_info: MessageInfo,
-) -> Result<(Target, CargoMetadata, Directories)> {
-    let target_list = rustc::target_list((msg_info.color_choice, Verbosity::Quiet).into())?;
-    let target = Target::from(target, &target_list);
-    let metadata = cargo_metadata_with_args(None, None, msg_info)?
-        .ok_or(eyre::eyre!("unable to get project metadata"))?;
-    let cwd = std::env::current_dir()?;
-    let host_meta = rustc::version_meta()?;
-    let host = host_meta.host();
-    let sysroot = rustc::get_sysroot(&host, &target, channel, msg_info)?.1;
-    let dirs = Directories::create(engine, &metadata, &cwd, &sysroot, docker_in_docker)?;
-
-    Ok((target, metadata, dirs))
-}
-
 /// Register binfmt interpreters
-pub(crate) fn register(engine: &Engine, target: &Target, msg_info: MessageInfo) -> Result<()> {
+pub(crate) fn register(target: &Target, verbose: bool) -> Result<()> {
     let cmd = if target.is_windows() {
         // https://www.kernel.org/doc/html/latest/admin-guide/binfmt-misc.html
         "mount binfmt_misc -t binfmt_misc /proc/sys/fs/binfmt_misc && \
@@ -183,13 +150,14 @@ pub(crate) fn register(engine: &Engine, target: &Target, msg_info: MessageInfo)
             binfmt-support qemu-user-static"
     };
 
-    subcommand(engine, "run")
+    let engine = Engine::new(verbose)?;
+    subcommand(&engine, "run")
         .args(&["--userns", "host"])
         .arg("--privileged")
         .arg("--rm")
-        .arg(UBUNTU_BASE)
+        .arg("ubuntu:16.04")
         .args(&["sh", "-c", cmd])
-        .run(msg_info, false)
+        .run(verbose, false)
         .map_err(Into::into)
 }
 
@@ -220,56 +188,22 @@ pub(crate) fn cargo_safe_command(uses_xargo: bool) -> SafeCommand {
     }
 }
 
-fn add_cargo_configuration_envvars(docker: &mut Command) {
-    let non_cargo_prefix = &[
-        "http_proxy",
-        "TERM",
-        "RUSTDOCFLAGS",
-        "RUSTFLAGS",
-        "BROWSER",
-        "HTTPS_PROXY",
-        "HTTP_TIMEOUT",
-        "https_proxy",
-    ];
-    let cargo_prefix_skip = &[
-        "CARGO_HOME",
-        "CARGO_TARGET_DIR",
-        "CARGO_BUILD_TARGET_DIR",
-        "CARGO_BUILD_RUSTC",
-        "CARGO_BUILD_RUSTC_WRAPPER",
-        "CARGO_BUILD_RUSTC_WORKSPACE_WRAPPER",
-        "CARGO_BUILD_RUSTDOC",
-    ];
-    let is_cargo_passthrough = |key: &str| -> bool {
-        non_cargo_prefix.contains(&key)
-            || key.starts_with("CARGO_") && !cargo_prefix_skip.contains(&key)
-    };
-
-    // also need to accept any additional flags used to configure
-    // cargo, but only pass what's actually present.
-    for (key, _) in env::vars() {
-        if is_cargo_passthrough(&key) {
-            docker.args(&["-e", &key]);
-        }
-    }
-}
-
-pub(crate) fn mount(docker: &mut Command, val: &Path, prefix: &str) -> Result<String> {
+pub(crate) fn mount(
+    docker: &mut Command,
+    val: &Path,
+    prefix: &str,
+    verbose: bool,
+) -> Result<PathBuf> {
     let host_path = file::canonicalize(val)?;
-    let mount_path = canonicalize_mount_path(&host_path)?;
+    let mount_path = canonicalize_mount_path(&host_path, verbose)?;
     docker.args(&[
         "-v",
-        &format!("{}:{prefix}{}", host_path.to_utf8()?, mount_path),
+        &format!("{}:{prefix}{}", host_path.to_utf8()?, mount_path.to_utf8()?),
     ]);
     Ok(mount_path)
 }
 
-pub(crate) fn docker_envvars(
-    docker: &mut Command,
-    config: &Config,
-    target: &Target,
-    msg_info: MessageInfo,
-) -> Result<()> {
+pub(crate) fn docker_envvars(docker: &mut Command, config: &Config, target: &Target) -> Result<()> {
     for ref var in config.env_passthrough(target)?.unwrap_or_default() {
         validate_env_var(var)?;
 
@@ -286,7 +220,6 @@ pub(crate) fn docker_envvars(
         .args(&["-e", "CARGO_HOME=/cargo"])
         .args(&["-e", "CARGO_TARGET_DIR=/target"])
         .args(&["-e", &cross_runner]);
-    add_cargo_configuration_envvars(docker);
 
     if let Some(username) = id::username().unwrap() {
         docker.args(&["-e", &format!("USER={username}")]);
@@ -302,10 +235,7 @@ pub(crate) fn docker_envvars(
 
     if let Ok(value) = env::var("CROSS_CONTAINER_OPTS") {
         if env::var("DOCKER_OPTS").is_ok() {
-            shell::warn(
-                "using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`.",
-                msg_info,
-            )?;
+            eprintln!("Warning: using both `CROSS_CONTAINER_OPTS` and `DOCKER_OPTS`.");
         }
         docker.args(&parse_docker_opts(&value)?);
     } else if let Ok(value) = env::var("DOCKER_OPTS") {
@@ -324,8 +254,8 @@ pub(crate) fn docker_cwd(
     mount_volumes: bool,
 ) -> Result<()> {
     if mount_volumes {
-        docker.args(&["-w", &dirs.mount_cwd]);
-    } else if dirs.mount_cwd == metadata.workspace_root.to_utf8()? {
+        docker.args(&["-w", dirs.mount_cwd.to_utf8()?]);
+    } else if dirs.mount_cwd == metadata.workspace_root {
         docker.args(&["-w", "/project"]);
     } else {
         // We do this to avoid clashes with path separators. Windows uses `\` as a path separator on Path::join
@@ -344,8 +274,9 @@ pub(crate) fn docker_mount(
     config: &Config,
     target: &Target,
     cwd: &Path,
-    mount_cb: impl Fn(&mut Command, &Path) -> Result<String>,
-    mut store_cb: impl FnMut((String, String)),
+    verbose: bool,
+    mount_cb: impl Fn(&mut Command, &Path, bool) -> Result<PathBuf>,
+    mut store_cb: impl FnMut((String, PathBuf)),
 ) -> Result<bool> {
     let mut mount_volumes = false;
     // FIXME(emilgardis 2022-04-07): This is a fallback so that if it's hard for us to do mounting logic, make it simple(r)
@@ -362,15 +293,15 @@ pub(crate) fn docker_mount(
         };
 
         if let Ok(val) = value {
-            let mount_path = mount_cb(docker, val.as_ref())?;
-            docker.args(&["-e", &format!("{}={}", var, mount_path)]);
+            let mount_path = mount_cb(docker, val.as_ref(), verbose)?;
+            docker.args(&["-e", &format!("{}={}", var, mount_path.to_utf8()?)]);
             store_cb((val, mount_path));
             mount_volumes = true;
         }
     }
 
     for path in metadata.path_dependencies() {
-        let mount_path = mount_cb(docker, path)?;
+        let mount_path = mount_cb(docker, path, verbose)?;
         store_cb((path.to_utf8()?.to_string(), mount_path));
         mount_volumes = true;
     }
@@ -378,15 +309,33 @@ pub(crate) fn docker_mount(
     Ok(mount_volumes)
 }
 
-pub(crate) fn canonicalize_mount_path(path: &Path) -> Result<String> {
+#[cfg(target_os = "windows")]
+fn wslpath(path: &Path, verbose: bool) -> Result<PathBuf> {
+    let wslpath = which::which("wsl.exe")
+        .map_err(|_| eyre::eyre!("could not find wsl.exe"))
+        .warning("usage of `env.volumes` requires WSL on Windows")
+        .suggestion("is WSL installed on the host?")?;
+
+    Command::new(wslpath)
+        .arg("-e")
+        .arg("wslpath")
+        .arg("-a")
+        .arg(path)
+        .run_and_get_stdout(verbose)
+        .wrap_err_with(|| format!("could not get linux compatible path for `{path:?}`"))
+        .map(|s| s.trim().into())
+}
+
+#[allow(unused_variables)]
+pub(crate) fn canonicalize_mount_path(path: &Path, verbose: bool) -> Result<PathBuf> {
     #[cfg(target_os = "windows")]
     {
         // On Windows, we can not mount the directory name directly. Instead, we use wslpath to convert the path to a linux compatible path.
-        path.as_wslpath()
+        wslpath(path, verbose)
     }
     #[cfg(not(target_os = "windows"))]
     {
-        path.to_utf8().map(|p| p.to_string())
+        Ok(path.to_path_buf())
     }
 }
 
@@ -405,12 +354,13 @@ pub(crate) fn docker_user_id(docker: &mut Command, engine_type: EngineType) {
     }
 }
 
-#[allow(unused_mut, clippy::let_and_return)]
+#[allow(unused_variables)]
 pub(crate) fn docker_seccomp(
     docker: &mut Command,
     engine_type: EngineType,
     target: &Target,
     metadata: &CargoMetadata,
+    verbose: bool,
 ) -> Result<()> {
     // docker uses seccomp now on all installations
     if target.needs_docker_seccomp() {
@@ -427,13 +377,12 @@ pub(crate) fn docker_seccomp(
             if !path.exists() {
                 write_file(&path, false)?.write_all(SECCOMP.as_bytes())?;
             }
-            let mut path_string = path.to_utf8()?.to_string();
             #[cfg(target_os = "windows")]
             if matches!(engine_type, EngineType::Podman | EngineType::PodmanRemote) {
                 // podman weirdly expects a WSL path here, and fails otherwise
-                path_string = path.as_wslpath()?;
+                path = wslpath(&path, verbose)?;
             }
-            path_string
+            path.to_utf8()?.to_string()
         };
 
         docker.args(&["--security-opt", &format!("seccomp={}", seccomp)]);
@@ -457,7 +406,7 @@ pub(crate) fn custom_image_build(
     metadata: &CargoMetadata,
     Directories { host_root, .. }: Directories,
     engine: &Engine,
-    msg_info: MessageInfo,
+    verbose: bool,
 ) -> Result<String> {
     let mut image = image_name(config, target)?;
 
@@ -479,7 +428,7 @@ pub(crate) fn custom_image_build(
                 &host_root,
                 config.dockerfile_build_args(target)?.unwrap_or_default(),
                 target,
-                msg_info,
+                verbose,
             )
             .wrap_err("when building dockerfile")?;
     }
@@ -504,11 +453,11 @@ pub(crate) fn custom_image_build(
                     &host_root,
                     Some(("CROSS_CMD", pre_build.join("\n"))),
                     target,
-                    msg_info,
+                    verbose,
                 )
                 .wrap_err("when pre-building")
                 .with_note(|| format!("CROSS_CMD={}", pre_build.join("\n")))?;
-            image = custom.image_name(target, metadata)?;
+            image = custom.image_name(target, metadata);
         }
     }
     Ok(image)
@@ -544,7 +493,7 @@ fn docker_read_mount_paths(engine: &Engine) -> Result<Vec<MountDetail>> {
         command
     };
 
-    let output = docker.run_and_get_stdout(Verbosity::Quiet.into())?;
+    let output = docker.run_and_get_stdout(false)?;
     let info = serde_json::from_str(&output).wrap_err("failed to parse docker inspect output")?;
     dockerinfo_parse_mounts(&info)
 }
@@ -632,19 +581,6 @@ impl MountFinder {
     }
 }
 
-fn path_digest(path: &Path) -> Result<const_sha1::Digest> {
-    let buffer = const_sha1::ConstBuffer::from_slice(path.to_utf8()?.as_bytes());
-    Ok(const_sha1::sha1(&buffer))
-}
-
-pub fn path_hash(path: &Path) -> Result<String> {
-    Ok(path_digest(path)?
-        .to_string()
-        .get(..5)
-        .expect("sha1 is expected to be at least 5 characters long")
-        .to_string())
-}
-
 #[cfg(test)]
 mod tests {
     use super::*;
diff --git a/src/errors.rs b/src/errors.rs
index 83970e7..5f80b43 100644
--- a/src/errors.rs
+++ b/src/errors.rs
@@ -1,39 +1,13 @@
-use crate::temp;
-
-use std::sync::atomic::{AtomicBool, Ordering};
-
 pub use color_eyre::Section;
 pub use eyre::Context;
 pub use eyre::Result;
 
-pub static mut TERMINATED: AtomicBool = AtomicBool::new(false);
-
 pub fn install_panic_hook() -> Result<()> {
     color_eyre::config::HookBuilder::new()
         .display_env_section(false)
         .install()
 }
 
-/// # Safety
-/// Safe as long as we have single-threaded execution.
-unsafe fn termination_handler() {
-    // we can't warn the user here, since locks aren't signal-safe.
-    // we can delete files, since fdopendir is thread-safe, and
-    // `openat`, `unlinkat`, and `lstat` are signal-safe.
-    //  https://man7.org/linux/man-pages/man7/signal-safety.7.html
-    if !TERMINATED.swap(true, Ordering::SeqCst) && temp::has_tempfiles() {
-        temp::clean();
-    }
-
-    // EOWNERDEAD, seems to be the same on linux, macos, and bash on windows.
-    std::process::exit(130);
-}
-
-pub fn install_termination_hook() -> Result<()> {
-    // SAFETY: safe since single-threaded execution.
-    ctrlc::set_handler(|| unsafe { termination_handler() }).map_err(Into::into)
-}
-
 #[derive(Debug, thiserror::Error)]
 pub enum CommandError {
     #[error("`{command}` failed with {status}")]
diff --git a/src/extensions.rs b/src/extensions.rs
index 64f194c..8948c1f 100644
--- a/src/extensions.rs
+++ b/src/extensions.rs
@@ -3,54 +3,32 @@ use std::fmt;
 use std::process::{Command, ExitStatus, Output};
 
 use crate::errors::*;
-use crate::shell::{self, MessageInfo, Verbosity};
 
 pub const STRIPPED_BINS: &[&str] = &[crate::docker::DOCKER, crate::docker::PODMAN, "cargo"];
 
 pub trait CommandExt {
-    fn fmt_message(&self, msg_info: MessageInfo) -> String;
-
-    fn print(&self, msg_info: MessageInfo) -> Result<()> {
-        shell::print(&self.fmt_message(msg_info), msg_info)
-    }
-
-    fn info(&self, msg_info: MessageInfo) -> Result<()> {
-        shell::info(&self.fmt_message(msg_info), msg_info)
-    }
-
-    fn debug(&self, msg_info: MessageInfo) -> Result<()> {
-        shell::debug(&self.fmt_message(msg_info), msg_info)
-    }
-
+    fn print_verbose(&self, verbose: bool);
     fn status_result(
         &self,
-        msg_info: MessageInfo,
+        verbose: bool,
         status: ExitStatus,
         output: Option<&Output>,
     ) -> Result<(), CommandError>;
-    fn run(&mut self, msg_info: MessageInfo, silence_stdout: bool) -> Result<()>;
+    fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError>;
     fn run_and_get_status(
         &mut self,
-        msg_info: MessageInfo,
+        verbose: bool,
         silence_stdout: bool,
-    ) -> Result<ExitStatus>;
-    fn run_and_get_stdout(&mut self, msg_info: MessageInfo) -> Result<String>;
-    fn run_and_get_output(&mut self, msg_info: MessageInfo) -> Result<std::process::Output>;
-    fn command_pretty(
-        &self,
-        msg_info: MessageInfo,
-        strip: impl for<'a> Fn(&'a str) -> bool,
-    ) -> String;
+    ) -> Result<ExitStatus, CommandError>;
+    fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String>;
+    fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output>;
+    fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String;
 }
 
 impl CommandExt for Command {
-    fn command_pretty(
-        &self,
-        msg_info: MessageInfo,
-        strip: impl for<'a> Fn(&'a str) -> bool,
-    ) -> String {
+    fn command_pretty(&self, verbose: bool, strip: impl for<'a> Fn(&'a str) -> bool) -> String {
         // a dummy implementor of display to avoid using unwraps
-        struct C<'c, F>(&'c Command, MessageInfo, F);
+        struct C<'c, F>(&'c Command, bool, F);
         impl<F> std::fmt::Display for C<'_, F>
         where
             F: for<'a> Fn(&'a str) -> bool,
@@ -61,7 +39,7 @@ impl CommandExt for Command {
                     f,
                     "{}",
                     // if verbose, never strip, if not, let user choose
-                    crate::file::pretty_path(cmd.get_program(), move |c| if self.1.verbose() {
+                    crate::file::pretty_path(cmd.get_program(), move |c| if self.1 {
                         false
                     } else {
                         self.2(c)
@@ -78,21 +56,23 @@ impl CommandExt for Command {
                 Ok(())
             }
         }
-        format!("{}", C(self, msg_info, strip))
+        format!("{}", C(self, verbose, strip))
     }
 
-    fn fmt_message(&self, msg_info: MessageInfo) -> String {
-        let verbose = (msg_info.color_choice, Verbosity::Verbose).into();
-        if let Some(cwd) = self.get_current_dir() {
-            format!("+ {:?} {}", cwd, self.command_pretty(verbose, |_| false))
-        } else {
-            format!("+ {}", self.command_pretty(verbose, |_| false))
+    fn print_verbose(&self, verbose: bool) {
+        // TODO: introduce verbosity levels, v = 1, strip cmd, v > 1, don't strip cmd
+        if verbose {
+            if let Some(cwd) = self.get_current_dir() {
+                println!("+ {:?} {}", cwd, self.command_pretty(true, |_| false));
+            } else {
+                println!("+ {}", self.command_pretty(true, |_| false));
+            }
         }
     }
 
     fn status_result(
         &self,
-        msg_info: MessageInfo,
+        verbose: bool,
         status: ExitStatus,
         output: Option<&Output>,
     ) -> Result<(), CommandError> {
@@ -102,7 +82,7 @@ impl CommandExt for Command {
             Err(CommandError::NonZeroExitCode {
                 status,
                 command: self
-                    .command_pretty(msg_info, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
+                    .command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
                 stderr: output.map(|out| out.stderr.clone()).unwrap_or_default(),
                 stdout: output.map(|out| out.stdout.clone()).unwrap_or_default(),
             })
@@ -110,35 +90,34 @@ impl CommandExt for Command {
     }
 
     /// Runs the command to completion
-    fn run(&mut self, msg_info: MessageInfo, silence_stdout: bool) -> Result<()> {
-        let status = self.run_and_get_status(msg_info, silence_stdout)?;
-        self.status_result(msg_info, status, None)
-            .map_err(Into::into)
+    fn run(&mut self, verbose: bool, silence_stdout: bool) -> Result<(), CommandError> {
+        let status = self.run_and_get_status(verbose, silence_stdout)?;
+        self.status_result(verbose, status, None)
     }
 
     /// Runs the command to completion
     fn run_and_get_status(
         &mut self,
-        msg_info: MessageInfo,
+        verbose: bool,
         silence_stdout: bool,
-    ) -> Result<ExitStatus> {
-        self.debug(msg_info)?;
-        if silence_stdout && !msg_info.verbose() {
+    ) -> Result<ExitStatus, CommandError> {
+        self.print_verbose(verbose);
+        if silence_stdout && !verbose {
             self.stdout(std::process::Stdio::null());
         }
         self.status()
             .map_err(|e| CommandError::CouldNotExecute {
                 source: Box::new(e),
                 command: self
-                    .command_pretty(msg_info, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
+                    .command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
             })
             .map_err(Into::into)
     }
 
     /// Runs the command to completion and returns its stdout
-    fn run_and_get_stdout(&mut self, msg_info: MessageInfo) -> Result<String> {
-        let out = self.run_and_get_output(msg_info)?;
-        self.status_result(msg_info, out.status, Some(&out))
+    fn run_and_get_stdout(&mut self, verbose: bool) -> Result<String> {
+        let out = self.run_and_get_output(verbose)?;
+        self.status_result(verbose, out.status, Some(&out))
             .map_err(CommandError::to_section_report)?;
         out.stdout().map_err(Into::into)
     }
@@ -148,13 +127,13 @@ impl CommandExt for Command {
     /// # Notes
     ///
     /// This command does not check the status.
-    fn run_and_get_output(&mut self, msg_info: MessageInfo) -> Result<std::process::Output> {
-        self.debug(msg_info)?;
+    fn run_and_get_output(&mut self, verbose: bool) -> Result<std::process::Output> {
+        self.print_verbose(verbose);
         self.output().map_err(|e| {
             CommandError::CouldNotExecute {
                 source: Box::new(e),
                 command: self
-                    .command_pretty(msg_info, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
+                    .command_pretty(verbose, |ref cmd| STRIPPED_BINS.iter().any(|f| f == cmd)),
             }
             .to_section_report()
         })
diff --git a/src/file.rs b/src/file.rs
index a81bfcd..7d369f3 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -2,8 +2,6 @@ use std::borrow::Cow;
 use std::ffi::OsStr;
 use std::fs::{self, File};
 use std::io::Read;
-#[cfg(target_family = "windows")]
-use std::path::Prefix;
 use std::path::{Component, Path, PathBuf};
 
 use crate::errors::*;
@@ -27,49 +25,18 @@ impl ToUtf8 for Path {
 
 pub trait PathExt {
     fn as_posix(&self) -> Result<String>;
-    #[cfg(target_family = "windows")]
-    fn as_wslpath(&self) -> Result<String>;
 }
 
-#[cfg(target_family = "windows")]
-fn format_prefix(prefix: &str) -> Result<String> {
-    match prefix {
-        "" => eyre::bail!("Error: got empty windows prefix"),
-        _ => Ok(format!("/mnt/{}", prefix.to_lowercase())),
-    }
-}
-
-#[cfg(target_family = "windows")]
-fn fmt_disk(disk: u8) -> String {
-    (disk as char).to_string()
-}
-
-#[cfg(target_family = "windows")]
-fn fmt_unc(server: &std::ffi::OsStr, volume: &std::ffi::OsStr) -> Result<String> {
-    let server = server.to_utf8()?;
-    let volume = volume.to_utf8()?;
-    let bytes = volume.as_bytes();
-    if server == "localhost"
-        && bytes.len() == 2
-        && bytes[1] == b'$'
-        && matches!(bytes[0], b'A'..=b'Z' | b'a'..=b'z')
-    {
-        Ok(fmt_disk(bytes[0]))
-    } else {
-        Ok(format!("{}/{}", server, volume))
+fn push_posix_path(path: &mut String, component: &str) {
+    if !path.is_empty() && path != "/" {
+        path.push('/');
     }
+    path.push_str(component);
 }
 
 impl PathExt for Path {
     fn as_posix(&self) -> Result<String> {
         if cfg!(target_os = "windows") {
-            let push = |p: &mut String, c: &str| {
-                if !p.is_empty() && p != "/" {
-                    p.push('/');
-                }
-                p.push_str(c);
-            };
-
             // iterate over components to join them
             let mut output = String::new();
             for component in self.components() {
@@ -78,9 +45,9 @@ impl PathExt for Path {
                         eyre::bail!("unix paths cannot handle windows prefix {prefix:?}.")
                     }
                     Component::RootDir => output = "/".to_string(),
-                    Component::CurDir => push(&mut output, "."),
-                    Component::ParentDir => push(&mut output, ".."),
-                    Component::Normal(path) => push(&mut output, path.to_utf8()?),
+                    Component::CurDir => push_posix_path(&mut output, "."),
+                    Component::ParentDir => push_posix_path(&mut output, ".."),
+                    Component::Normal(path) => push_posix_path(&mut output, path.to_utf8()?),
                 }
             }
             Ok(output)
@@ -88,52 +55,6 @@ impl PathExt for Path {
             self.to_utf8().map(|x| x.to_string())
         }
     }
-
-    // this is similar to as_posix, but it handles drive separators
-    // and doesn't assume a relative path.
-    #[cfg(target_family = "windows")]
-    fn as_wslpath(&self) -> Result<String> {
-        let path = canonicalize(self)?;
-
-        let push = |p: &mut String, c: &str, r: bool| {
-            if !r {
-                p.push('/');
-            }
-            p.push_str(c);
-        };
-        // iterate over components to join them
-        let mut output = String::new();
-        let mut root_prefix = String::new();
-        let mut was_root = false;
-        for component in path.components() {
-            match component {
-                Component::Prefix(prefix) => {
-                    root_prefix = match prefix.kind() {
-                        Prefix::Verbatim(verbatim) => verbatim.to_utf8()?.to_string(),
-                        Prefix::VerbatimUNC(server, volume) => fmt_unc(server, volume)?,
-                        // we should never get this, but it's effectively just
-                        // a root_prefix since we force absolute paths.
-                        Prefix::VerbatimDisk(disk) => fmt_disk(disk),
-                        Prefix::UNC(server, volume) => fmt_unc(server, volume)?,
-                        Prefix::DeviceNS(ns) => ns.to_utf8()?.to_string(),
-                        Prefix::Disk(disk) => fmt_disk(disk),
-                    }
-                }
-                Component::RootDir => output = format!("{}/", format_prefix(&root_prefix)?),
-                Component::CurDir => push(&mut output, ".", was_root),
-                Component::ParentDir => push(&mut output, "..", was_root),
-                Component::Normal(path) => push(&mut output, path.to_utf8()?, was_root),
-            }
-            was_root = component == Component::RootDir;
-        }
-
-        // remove trailing '/'
-        if was_root {
-            output.truncate(output.len() - 1);
-        }
-
-        Ok(output)
-    }
 }
 
 pub fn read<P>(path: P) -> Result<String>
@@ -154,7 +75,6 @@ fn read_(path: &Path) -> Result<String> {
 
 pub fn canonicalize(path: impl AsRef<Path>) -> Result<PathBuf> {
     _canonicalize(path.as_ref())
-        .wrap_err_with(|| format!("when canonicalizing path `{:?}`", path.as_ref()))
 }
 
 fn _canonicalize(path: &Path) -> Result<PathBuf> {
@@ -245,12 +165,6 @@ mod tests {
     use super::*;
     use std::fmt::Debug;
 
-    macro_rules! p {
-        ($path:expr) => {
-            Path::new($path)
-        };
-    }
-
     fn result_eq<T: PartialEq + Eq + Debug>(x: Result<T>, y: Result<T>) {
         match (x, y) {
             (Ok(x), Ok(y)) => assert_eq!(x, y),
@@ -260,11 +174,14 @@ mod tests {
 
     #[test]
     fn as_posix() {
-        result_eq(p!(".").join("..").as_posix(), Ok("./..".to_string()));
-        result_eq(p!(".").join("/").as_posix(), Ok("/".to_string()));
-        result_eq(p!("foo").join("bar").as_posix(), Ok("foo/bar".to_string()));
+        result_eq(Path::new(".").join("..").as_posix(), Ok("./..".to_string()));
+        result_eq(Path::new(".").join("/").as_posix(), Ok("/".to_string()));
+        result_eq(
+            Path::new("foo").join("bar").as_posix(),
+            Ok("foo/bar".to_string()),
+        );
         result_eq(
-            p!("/foo").join("bar").as_posix(),
+            Path::new("/foo").join("bar").as_posix(),
             Ok("/foo/bar".to_string()),
         );
     }
@@ -272,24 +189,8 @@ mod tests {
     #[test]
     #[cfg(target_family = "windows")]
     fn as_posix_prefix() {
-        assert_eq!(p!("C:").join(".."), p!("C:.."));
-        assert!(p!("C:").join("..").as_posix().is_err());
-    }
-
-    #[test]
-    #[cfg(target_family = "windows")]
-    fn as_wslpath() {
-        result_eq(p!(r"C:\").as_wslpath(), Ok("/mnt/c".to_string()));
-        result_eq(p!(r"C:\Users").as_wslpath(), Ok("/mnt/c/Users".to_string()));
-        result_eq(
-            p!(r"\\localhost\c$\Users").as_wslpath(),
-            Ok("/mnt/c/Users".to_string()),
-        );
-        result_eq(p!(r"\\.\C:\").as_wslpath(), Ok("/mnt/c".to_string()));
-        result_eq(
-            p!(r"\\.\C:\Users").as_wslpath(),
-            Ok("/mnt/c/Users".to_string()),
-        );
+        assert_eq!(Path::new("C:").join(".."), Path::new("C:.."));
+        assert!(Path::new("C:").join("..").as_posix().is_err());
     }
 
     #[test]
diff --git a/src/lib.rs b/src/lib.rs
index c3db395..55746e6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,10 +27,8 @@ mod extensions;
 mod file;
 mod id;
 mod interpreter;
-pub mod rustc;
+mod rustc;
 mod rustup;
-pub mod shell;
-pub mod temp;
 
 use std::env;
 use std::io::{self, Write};
@@ -39,17 +37,16 @@ use std::process::ExitStatus;
 
 use config::Config;
 use rustc_version::Channel;
-use serde::{Deserialize, Serialize, Serializer};
+use serde::Deserialize;
 
 pub use self::cargo::{cargo_command, cargo_metadata_with_args, CargoMetadata, Subcommand};
 use self::cross_toml::CrossToml;
 use self::errors::Context;
-use self::shell::{MessageInfo, Verbosity};
+use self::rustc::{TargetList, VersionMetaExt};
 
-pub use self::errors::{install_panic_hook, install_termination_hook, Result};
+pub use self::errors::{install_panic_hook, Result};
 pub use self::extensions::{CommandExt, OutputExt};
-pub use self::file::{pretty_path, ToUtf8};
-pub use self::rustc::{TargetList, VersionMetaExt};
+pub use self::file::ToUtf8;
 
 pub const CROSS_LABEL_DOMAIN: &str = "org.cross-rs";
 
@@ -118,7 +115,7 @@ impl Host {
     }
 
     /// Returns the [`Target`] as target triple string
-    pub fn triple(&self) -> &str {
+    fn triple(&self) -> &str {
         match self {
             Host::X86_64AppleDarwin => "x86_64-apple-darwin",
             Host::Aarch64AppleDarwin => "aarch64-apple-darwin",
@@ -148,7 +145,7 @@ impl<'a> From<&'a str> for Host {
 }
 
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
-#[serde(from = "String")]
+#[serde(from = "&str")]
 pub enum Target {
     BuiltIn { triple: String },
     Custom { triple: String },
@@ -317,7 +314,7 @@ impl std::fmt::Display for Target {
 }
 
 impl Target {
-    pub fn from(triple: &str, target_list: &TargetList) -> Target {
+    fn from(triple: &str, target_list: &TargetList) -> Target {
         if target_list.contains(triple) {
             Target::new_built_in(triple)
         } else {
@@ -336,74 +333,78 @@ impl From<Host> for Target {
             Host::Aarch64AppleDarwin => Target::new_built_in("aarch64-apple-darwin"),
             Host::Aarch64UnknownLinuxGnu => Target::new_built_in("aarch64-unknown-linux-gnu"),
             Host::Aarch64UnknownLinuxMusl => Target::new_built_in("aarch64-unknown-linux-musl"),
-            Host::Other(s) => Target::from(
-                s.as_str(),
-                &rustc::target_list(Verbosity::Quiet.into()).unwrap(),
-            ),
+            Host::Other(s) => Target::from(s.as_str(), &rustc::target_list(false).unwrap()),
         }
     }
 }
 
-impl From<String> for Target {
-    fn from(target_str: String) -> Target {
-        let target_host: Host = target_str.as_str().into();
+impl From<&str> for Target {
+    fn from(target_str: &str) -> Target {
+        let target_host: Host = target_str.into();
         target_host.into()
     }
 }
 
-impl Serialize for Target {
-    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        match self {
-            Target::BuiltIn { triple } => serializer.serialize_str(triple),
-            Target::Custom { triple } => serializer.serialize_str(triple),
-        }
-    }
-}
-
 pub fn run() -> Result<ExitStatus> {
-    let target_list = rustc::target_list(Verbosity::Quiet.into())?;
+    let target_list = rustc::target_list(false)?;
     let args = cli::parse(&target_list)?;
 
-    if args.version && args.subcommand.is_none() {
-        let commit_info = include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"));
-        shell::print(
-            format!(
-                concat!("cross ", env!("CARGO_PKG_VERSION"), "{}"),
-                commit_info
-            ),
-            args.msg_info,
-        )?;
+    if args.all.iter().any(|a| a == "--version" || a == "-V") && args.subcommand.is_none() {
+        println!(
+            concat!("cross ", env!("CARGO_PKG_VERSION"), "{}"),
+            include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"))
+        );
     }
 
-    let host_version_meta = rustc::version_meta()?;
+    let verbose = args
+        .all
+        .iter()
+        .any(|a| a == "--verbose" || a == "-v" || a == "-vv");
+
+    let host_version_meta =
+        rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version")?;
     let cwd = std::env::current_dir()?;
-    if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), args.msg_info)? {
+    if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), verbose)? {
         let host = host_version_meta.host();
-        let toml = toml(&metadata, args.msg_info)?;
+        let toml = toml(&metadata)?;
         let config = Config::new(toml);
         let target = args
             .target
             .or_else(|| config.target(&target_list))
             .unwrap_or_else(|| Target::from(host.triple(), &target_list));
-        config.confusable_target(&target, args.msg_info)?;
+        config.confusable_target(&target);
 
         let image_exists = match docker::image_name(&config, &target) {
             Ok(_) => true,
             Err(err) => {
-                shell::warn(err, args.msg_info)?;
+                eprintln!("Warning: {}", err);
                 false
             }
         };
 
         if image_exists && host.is_supported(Some(&target)) {
-            let (toolchain, sysroot) =
-                rustc::get_sysroot(&host, &target, args.channel.as_deref(), args.msg_info)?;
+            let mut sysroot = rustc::sysroot(&host, &target, verbose)?;
+            let default_toolchain = sysroot
+                .file_name()
+                .and_then(|file_name| file_name.to_str())
+                .ok_or_else(|| eyre::eyre!("couldn't get toolchain name"))?;
+            let toolchain = if let Some(channel) = args.channel {
+                [channel]
+                    .iter()
+                    .map(|c| c.as_str())
+                    .chain(default_toolchain.splitn(2, '-').skip(1))
+                    .collect::<Vec<_>>()
+                    .join("-")
+            } else {
+                default_toolchain.to_string()
+            };
+            sysroot.set_file_name(&toolchain);
             let mut is_nightly = toolchain.contains("nightly");
 
-            let installed_toolchains = rustup::installed_toolchains(args.msg_info)?;
+            let installed_toolchains = rustup::installed_toolchains(verbose)?;
 
             if !installed_toolchains.into_iter().any(|t| t == toolchain) {
-                rustup::install_toolchain(&toolchain, args.msg_info)?;
+                rustup::install_toolchain(&toolchain, verbose)?;
             }
             // TODO: Provide a way to pick/match the toolchain version as a consumer of `cross`.
             if let Some((rustc_version, channel, rustc_commit)) = rustup::rustc_version(&sysroot)? {
@@ -412,7 +413,6 @@ pub fn run() -> Result<ExitStatus> {
                     &toolchain,
                     &rustc_version,
                     &rustc_commit,
-                    args.msg_info,
                 )?;
                 is_nightly = channel == Channel::Nightly;
             }
@@ -423,7 +423,7 @@ pub fn run() -> Result<ExitStatus> {
             if std::env::var("CROSS_CUSTOM_TOOLCHAIN").is_err() {
                 // build-std overrides xargo, but only use it if it's a built-in
                 // tool but not an available target or doesn't have rust-std.
-                let available_targets = rustup::available_targets(&toolchain, args.msg_info)?;
+                let available_targets = rustup::available_targets(&toolchain, verbose)?;
 
                 if !is_nightly && uses_build_std {
                     eyre::bail!(
@@ -436,17 +436,17 @@ pub fn run() -> Result<ExitStatus> {
                     && !available_targets.is_installed(&target)
                     && available_targets.contains(&target)
                 {
-                    rustup::install(&target, &toolchain, args.msg_info)?;
-                } else if !rustup::component_is_installed("rust-src", &toolchain, args.msg_info)? {
-                    rustup::install_component("rust-src", &toolchain, args.msg_info)?;
+                    rustup::install(&target, &toolchain, verbose)?;
+                } else if !rustup::component_is_installed("rust-src", &toolchain, verbose)? {
+                    rustup::install_component("rust-src", &toolchain, verbose)?;
                 }
                 if args
                     .subcommand
                     .map(|sc| sc == Subcommand::Clippy)
                     .unwrap_or(false)
-                    && !rustup::component_is_installed("clippy", &toolchain, args.msg_info)?
+                    && !rustup::component_is_installed("clippy", &toolchain, verbose)?
                 {
-                    rustup::install_component("clippy", &toolchain, args.msg_info)?;
+                    rustup::install_component("clippy", &toolchain, verbose)?;
                 }
             }
 
@@ -492,54 +492,41 @@ pub fn run() -> Result<ExitStatus> {
                 filtered_args.push("-Zbuild-std".to_string());
             }
 
-            let is_remote = docker::Engine::is_remote();
-            let needs_docker = args
-                .subcommand
-                .map(|sc| sc.needs_docker(is_remote))
-                .unwrap_or(false);
-            if target.needs_docker() && needs_docker {
-                let engine = docker::Engine::new(Some(is_remote), args.msg_info)?;
+            if target.needs_docker() && args.subcommand.map(|sc| sc.needs_docker()).unwrap_or(false)
+            {
                 if host_version_meta.needs_interpreter()
                     && needs_interpreter
                     && target.needs_interpreter()
                     && !interpreter::is_registered(&target)?
                 {
-                    docker::register(&engine, &target, args.msg_info)?
+                    docker::register(&target, verbose)?
                 }
 
-                let status = docker::run(
-                    &engine,
+                return docker::run(
                     &target,
                     &filtered_args,
                     &metadata,
                     &config,
                     uses_xargo,
                     &sysroot,
-                    args.msg_info,
+                    verbose,
                     args.docker_in_docker,
                     &cwd,
-                )?;
-                let needs_host = args
-                    .subcommand
-                    .map(|sc| sc.needs_host(is_remote))
-                    .unwrap_or(false);
-                if !(status.success() && needs_host) {
-                    return Ok(status);
-                }
+                );
             }
         }
     }
 
     // if we fallback to the host cargo, use the same invocation that was made to cross
     let argv: Vec<String> = env::args().skip(1).collect();
-    shell::note("Falling back to `cargo` on the host.", args.msg_info)?;
+    eprintln!("Warning: Falling back to `cargo` on the host.");
     match args.subcommand {
         Some(Subcommand::List) => {
             // this won't print in order if we have both stdout and stderr.
-            let out = cargo::run_and_get_output(&argv, args.msg_info)?;
+            let out = cargo::run_and_get_output(&argv, verbose)?;
             let stdout = out.stdout()?;
             if out.status.success() && cli::is_subcommand_list(&stdout) {
-                cli::fmt_subcommands(&stdout, args.msg_info)?;
+                cli::fmt_subcommands(&stdout);
             } else {
                 // Not a list subcommand, which can happen with weird edge-cases.
                 print!("{}", stdout);
@@ -547,7 +534,7 @@ pub fn run() -> Result<ExitStatus> {
             }
             Ok(out.status)
         }
-        _ => cargo::run(&argv, args.msg_info).map_err(Into::into),
+        _ => cargo::run(&argv, verbose).map_err(Into::into),
     }
 }
 
@@ -564,7 +551,6 @@ pub(crate) fn warn_host_version_mismatch(
     toolchain: &str,
     rustc_version: &rustc_version::Version,
     rustc_commit: &str,
-    msg_info: MessageInfo,
 ) -> Result<VersionMatch> {
     let host_commit = (&host_version_meta.short_version_string)
         .splitn(3, ' ')
@@ -583,60 +569,40 @@ pub(crate) fn warn_host_version_mismatch(
             host_version_meta.short_version_string
         );
         if versions.is_lt() || (versions.is_eq() && dates.is_lt()) {
-            shell::warn(format!("using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`"), msg_info)?;
+            eprintln!("Warning: using older {rustc_warning}.\n > Update with `rustup update --force-non-host {toolchain}`");
             return Ok(VersionMatch::OlderTarget);
         } else if versions.is_gt() || (versions.is_eq() && dates.is_gt()) {
-            shell::warn(
-                format!("using newer {rustc_warning}.\n > Update with `rustup update`"),
-                msg_info,
-            )?;
+            eprintln!("Warning: using newer {rustc_warning}.\n > Update with `rustup update`");
             return Ok(VersionMatch::NewerTarget);
         } else {
-            shell::warn(format!("using {rustc_warning}."), msg_info)?;
+            eprintln!("Warning: using {rustc_warning}.");
             return Ok(VersionMatch::Different);
         }
     }
     Ok(VersionMatch::Same)
 }
 
-/// Obtains the [`CrossToml`] from one of the possible locations
-///
-/// These locations are checked in the following order:
-/// 1. If the `CROSS_CONFIG` variable is set, it tries to read the config from its value
-/// 2. Otherwise, the `Cross.toml` in the project root is used
-/// 3. Package metadata in the Cargo.toml
-///
-/// The values from `CROSS_CONFIG` or `Cross.toml` are concatenated with the package
-/// metadata in `Cargo.toml`, with `Cross.toml` having the highest priority.
-fn toml(metadata: &CargoMetadata, msg_info: MessageInfo) -> Result<Option<CrossToml>> {
-    let root = &metadata.workspace_root;
-    let cross_config_path = match env::var("CROSS_CONFIG") {
+/// Parses the `Cross.toml` at the root of the Cargo project or from the
+/// `CROSS_CONFIG` environment variable (if any exist in either location).
+fn toml(metadata: &CargoMetadata) -> Result<Option<CrossToml>> {
+    let path = match env::var("CROSS_CONFIG") {
         Ok(var) => PathBuf::from(var),
-        Err(_) => root.join("Cross.toml"),
+        Err(_) => metadata.workspace_root.join("Cross.toml"),
     };
 
-    // Attempts to read the cross config from the Cargo.toml
-    let cargo_toml_str =
-        file::read(root.join("Cargo.toml")).wrap_err("failed to read Cargo.toml")?;
-
-    if cross_config_path.exists() {
-        let cross_toml_str = file::read(&cross_config_path)
-            .wrap_err_with(|| format!("could not read file `{cross_config_path:?}`"))?;
+    if path.exists() {
+        let content =
+            file::read(&path).wrap_err_with(|| format!("could not read file `{path:?}`"))?;
 
-        let (config, _) = CrossToml::parse(&cargo_toml_str, &cross_toml_str, msg_info)
-            .wrap_err_with(|| format!("failed to parse file `{cross_config_path:?}` as TOML",))?;
+        let (config, _) = CrossToml::parse(&content)
+            .wrap_err_with(|| format!("failed to parse file `{path:?}` as TOML"))?;
 
         Ok(Some(config))
     } else {
         // Checks if there is a lowercase version of this file
-        if root.join("cross.toml").exists() {
-            shell::warn("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered.", msg_info)?;
-        }
-
-        if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str, msg_info)? {
-            Ok(Some(cfg))
-        } else {
-            Ok(None)
+        if metadata.workspace_root.join("cross.toml").exists() {
+            eprintln!("There's a file named cross.toml, instead of Cross.toml. You may want to rename it, or it won't be considered.");
         }
+        Ok(None)
     }
 }
diff --git a/src/rustc.rs b/src/rustc.rs
index fe5cb13..1cdfaf2 100644
--- a/src/rustc.rs
+++ b/src/rustc.rs
@@ -5,7 +5,6 @@ use rustc_version::{Version, VersionMeta};
 
 use crate::errors::*;
 use crate::extensions::{env_program, CommandExt};
-use crate::shell::MessageInfo;
 use crate::{Host, Target};
 
 #[derive(Debug)]
@@ -22,7 +21,6 @@ impl TargetList {
 pub trait VersionMetaExt {
     fn host(&self) -> Host;
     fn needs_interpreter(&self) -> bool;
-    fn commit_hash(&self) -> String;
 }
 
 impl VersionMetaExt for VersionMeta {
@@ -33,67 +31,25 @@ impl VersionMetaExt for VersionMeta {
     fn needs_interpreter(&self) -> bool {
         self.semver < Version::new(1, 19, 0)
     }
-
-    fn commit_hash(&self) -> String {
-        self.commit_hash
-            .as_ref()
-            .map(|x| short_commit_hash(x))
-            .unwrap_or_else(|| hash_from_version_string(&self.short_version_string, 2))
-    }
-}
-
-fn short_commit_hash(hash: &str) -> String {
-    // short version hashes are always 9 digits
-    //  https://github.com/rust-lang/cargo/pull/10579
-    const LENGTH: usize = 9;
-
-    hash.get(..LENGTH)
-        .unwrap_or_else(|| panic!("commit hash must be at least {LENGTH} characters long"))
-        .to_string()
-}
-
-pub fn hash_from_version_string(version: &str, index: usize) -> String {
-    let is_hash = |x: &str| x.chars().all(|c| c.is_ascii_hexdigit());
-    let is_date = |x: &str| x.chars().all(|c| matches!(c, '-' | '0'..='9'));
-
-    // the version can be one of two forms:
-    //   multirust channel string: `"1.61.0 (fe5b13d68 2022-05-18)"`
-    //   short version string: `"rustc 1.61.0 (fe5b13d68 2022-05-18)"`
-    // want to extract the commit hash if we can, if not, just hash the string.
-    if let Some((commit, date)) = version
-        .splitn(index + 1, ' ')
-        .nth(index)
-        .and_then(|meta| meta.strip_prefix('('))
-        .and_then(|meta| meta.strip_suffix(')'))
-        .and_then(|meta| meta.split_once(' '))
-    {
-        if is_hash(commit) && is_date(date) {
-            return short_commit_hash(commit);
-        }
-    }
-
-    // fallback: can't extract the hash. just create a hash of the version string.
-    let buffer = const_sha1::ConstBuffer::from_slice(version.as_bytes());
-    short_commit_hash(&const_sha1::sha1(&buffer).to_string())
 }
 
 pub fn rustc_command() -> Command {
     Command::new(env_program("RUSTC", "rustc"))
 }
 
-pub fn target_list(msg_info: MessageInfo) -> Result<TargetList> {
+pub fn target_list(verbose: bool) -> Result<TargetList> {
     rustc_command()
         .args(&["--print", "target-list"])
-        .run_and_get_stdout(msg_info)
+        .run_and_get_stdout(verbose)
         .map(|s| TargetList {
             triples: s.lines().map(|l| l.to_owned()).collect(),
         })
 }
 
-pub fn sysroot(host: &Host, target: &Target, msg_info: MessageInfo) -> Result<PathBuf> {
+pub fn sysroot(host: &Host, target: &Target, verbose: bool) -> Result<PathBuf> {
     let mut stdout = rustc_command()
         .args(&["--print", "sysroot"])
-        .run_and_get_stdout(msg_info)?
+        .run_and_get_stdout(verbose)?
         .trim()
         .to_owned();
 
@@ -104,50 +60,3 @@ pub fn sysroot(host: &Host, target: &Target, msg_info: MessageInfo) -> Result<Pa
 
     Ok(PathBuf::from(stdout))
 }
-
-pub fn get_sysroot(
-    host: &Host,
-    target: &Target,
-    channel: Option<&str>,
-    msg_info: MessageInfo,
-) -> Result<(String, PathBuf)> {
-    let mut sysroot = sysroot(host, target, msg_info)?;
-    let default_toolchain = sysroot
-        .file_name()
-        .and_then(|file_name| file_name.to_str())
-        .ok_or_else(|| eyre::eyre!("couldn't get toolchain name"))?;
-    let toolchain = if let Some(channel) = channel {
-        [channel]
-            .iter()
-            .cloned()
-            .chain(default_toolchain.splitn(2, '-').skip(1))
-            .collect::<Vec<_>>()
-            .join("-")
-    } else {
-        default_toolchain.to_string()
-    };
-    sysroot.set_file_name(&toolchain);
-
-    Ok((toolchain, sysroot))
-}
-
-pub fn version_meta() -> Result<rustc_version::VersionMeta> {
-    rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version")
-}
-
-#[cfg(test)]
-mod tests {
-    use super::*;
-
-    #[test]
-    fn hash_from_rustc() {
-        assert_eq!(
-            hash_from_version_string("1.61.0 (fe5b13d68 2022-05-18)", 1),
-            "fe5b13d68"
-        );
-        assert_eq!(
-            hash_from_version_string("rustc 1.61.0 (fe5b13d68 2022-05-18)", 2),
-            "fe5b13d68"
-        );
-    }
-}
diff --git a/src/rustup.rs b/src/rustup.rs
index 5825bb9..f4c93a7 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -1,11 +1,10 @@
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::process::Command;
 
 use rustc_version::{Channel, Version};
 
 use crate::errors::*;
 pub use crate::extensions::{CommandExt, OutputExt};
-use crate::shell::{MessageInfo, Verbosity};
 use crate::Target;
 
 #[derive(Debug)]
@@ -27,24 +26,10 @@ impl AvailableTargets {
     }
 }
 
-fn rustup_command(msg_info: MessageInfo) -> Command {
-    let mut cmd = Command::new("rustup");
-    match msg_info.verbosity {
-        Verbosity::Quiet => {
-            cmd.arg("--quiet");
-        }
-        Verbosity::Verbose => {
-            cmd.arg("--verbose");
-        }
-        _ => (),
-    }
-    cmd
-}
-
-pub fn installed_toolchains(msg_info: MessageInfo) -> Result<Vec<String>> {
+pub fn installed_toolchains(verbose: bool) -> Result<Vec<String>> {
     let out = Command::new("rustup")
         .args(&["toolchain", "list"])
-        .run_and_get_stdout(msg_info)?;
+        .run_and_get_stdout(verbose)?;
 
     Ok(out
         .lines()
@@ -57,11 +42,11 @@ pub fn installed_toolchains(msg_info: MessageInfo) -> Result<Vec<String>> {
         .collect())
 }
 
-pub fn available_targets(toolchain: &str, msg_info: MessageInfo) -> Result<AvailableTargets> {
+pub fn available_targets(toolchain: &str, verbose: bool) -> Result<AvailableTargets> {
     let mut cmd = Command::new("rustup");
     cmd.args(&["target", "list", "--toolchain", toolchain]);
     let output = cmd
-        .run_and_get_output(msg_info)
+        .run_and_get_output(verbose)
         .suggestion("is rustup installed?")?;
 
     if !output.status.success() {
@@ -69,7 +54,7 @@ pub fn available_targets(toolchain: &str, msg_info: MessageInfo) -> Result<Avail
             eyre::bail!("{toolchain} is a custom toolchain. To use it, you'll need to set the environment variable `CROSS_CUSTOM_TOOLCHAIN=1`")
         }
         return Err(cmd
-            .status_result(msg_info, output.status, Some(&output))
+            .status_result(verbose, output.status, Some(&output))
             .unwrap_err()
             .to_section_report());
     }
@@ -97,37 +82,33 @@ pub fn available_targets(toolchain: &str, msg_info: MessageInfo) -> Result<Avail
     })
 }
 
-pub fn install_toolchain(toolchain: &str, msg_info: MessageInfo) -> Result<()> {
-    rustup_command(msg_info)
+pub fn install_toolchain(toolchain: &str, verbose: bool) -> Result<()> {
+    Command::new("rustup")
         .args(&["toolchain", "add", toolchain, "--profile", "minimal"])
-        .run(msg_info, false)
+        .run(verbose, false)
         .wrap_err_with(|| format!("couldn't install toolchain `{toolchain}`"))
 }
 
-pub fn install(target: &Target, toolchain: &str, msg_info: MessageInfo) -> Result<()> {
+pub fn install(target: &Target, toolchain: &str, verbose: bool) -> Result<()> {
     let target = target.triple();
 
-    rustup_command(msg_info)
+    Command::new("rustup")
         .args(&["target", "add", target, "--toolchain", toolchain])
-        .run(msg_info, false)
+        .run(verbose, false)
         .wrap_err_with(|| format!("couldn't install `std` for {target}"))
 }
 
-pub fn install_component(component: &str, toolchain: &str, msg_info: MessageInfo) -> Result<()> {
-    rustup_command(msg_info)
+pub fn install_component(component: &str, toolchain: &str, verbose: bool) -> Result<()> {
+    Command::new("rustup")
         .args(&["component", "add", component, "--toolchain", toolchain])
-        .run(msg_info, false)
+        .run(verbose, false)
         .wrap_err_with(|| format!("couldn't install the `{component}` component"))
 }
 
-pub fn component_is_installed(
-    component: &str,
-    toolchain: &str,
-    msg_info: MessageInfo,
-) -> Result<bool> {
+pub fn component_is_installed(component: &str, toolchain: &str, verbose: bool) -> Result<bool> {
     Ok(Command::new("rustup")
         .args(&["component", "list", "--toolchain", toolchain])
-        .run_and_get_stdout(msg_info)?
+        .run_and_get_stdout(verbose)?
         .lines()
         .any(|l| l.starts_with(component) && l.contains("installed")))
 }
@@ -142,35 +123,25 @@ fn rustc_channel(version: &Version) -> Result<Channel> {
     }
 }
 
-fn multirust_channel_manifest_path(toolchain_path: &Path) -> PathBuf {
-    toolchain_path.join("lib/rustlib/multirust-channel-manifest.toml")
-}
-
-pub fn rustc_version_string(toolchain_path: &Path) -> Result<Option<String>> {
-    let path = multirust_channel_manifest_path(toolchain_path);
+pub fn rustc_version(toolchain_path: &Path) -> Result<Option<(Version, Channel, String)>> {
+    let path = toolchain_path.join("lib/rustlib/multirust-channel-manifest.toml");
     if path.exists() {
         let contents =
             std::fs::read(&path).wrap_err_with(|| format!("couldn't open file `{path:?}`"))?;
         let manifest: toml::value::Table = toml::from_slice(&contents)?;
-        return Ok(manifest
+        if let Some(rust_version) = manifest
             .get("pkg")
             .and_then(|pkg| pkg.get("rust"))
             .and_then(|rust| rust.get("version"))
             .and_then(|version| version.as_str())
-            .map(|version| version.to_string()));
-    }
-    Ok(None)
-}
-
-pub fn rustc_version(toolchain_path: &Path) -> Result<Option<(Version, Channel, String)>> {
-    let path = multirust_channel_manifest_path(toolchain_path);
-    if let Some(rust_version) = rustc_version_string(toolchain_path)? {
-        // Field is `"{version} ({commit} {date})"`
-        if let Some((version, meta)) = rust_version.split_once(' ') {
-            let version = Version::parse(version)
-                .wrap_err_with(|| format!("invalid rust version found in {path:?}"))?;
-            let channel = rustc_channel(&version)?;
-            return Ok(Some((version, channel, meta.to_owned())));
+        {
+            // Field is `"{version} ({commit} {date})"`
+            if let Some((version, meta)) = rust_version.split_once(' ') {
+                let version = Version::parse(version)
+                    .wrap_err_with(|| format!("invalid rust version found in {path:?}"))?;
+                let channel = rustc_channel(&version)?;
+                return Ok(Some((version, channel, meta.to_owned())));
+            }
         }
     }
     Ok(None)
diff --git a/src/shell.rs b/src/shell.rs
deleted file mode 100644
index 14af762..0000000
--- a/src/shell.rs
+++ /dev/null
@@ -1,310 +0,0 @@
-// This file was adapted from:
-//   https://github.com/rust-lang/cargo/blob/ca4edabb28fc96fdf2a1d56fe3851831ac166f8a/src/cargo/core/shell.rs
-
-use std::fmt;
-use std::io::{self, Write};
-
-use crate::errors::Result;
-use owo_colors::{self, OwoColorize};
-
-/// the requested verbosity of output.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum Verbosity {
-    Quiet,
-    Normal,
-    Verbose,
-}
-
-impl Verbosity {
-    pub fn verbose(self) -> bool {
-        match self {
-            Self::Verbose => true,
-            Self::Normal | Self::Quiet => false,
-        }
-    }
-}
-
-/// Whether messages should use color output
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub enum ColorChoice {
-    /// force color output
-    Always,
-    /// force disable color output
-    Never,
-    /// intelligently guess whether to use color output
-    Auto,
-}
-
-// Should simplify the APIs a lot.
-#[derive(Debug, Clone, Copy, PartialEq, Eq)]
-pub struct MessageInfo {
-    pub color_choice: ColorChoice,
-    pub verbosity: Verbosity,
-}
-
-impl MessageInfo {
-    pub fn create(verbose: bool, quiet: bool, color: Option<&str>) -> Result<MessageInfo> {
-        let color_choice = get_color_choice(color)?;
-        let verbosity = get_verbosity(color_choice, verbose, quiet)?;
-
-        Ok(MessageInfo {
-            color_choice,
-            verbosity,
-        })
-    }
-
-    pub fn verbose(self) -> bool {
-        self.verbosity.verbose()
-    }
-}
-
-impl Default for MessageInfo {
-    fn default() -> MessageInfo {
-        MessageInfo {
-            color_choice: ColorChoice::Auto,
-            verbosity: Verbosity::Normal,
-        }
-    }
-}
-
-impl From<ColorChoice> for MessageInfo {
-    fn from(color_choice: ColorChoice) -> MessageInfo {
-        MessageInfo {
-            color_choice,
-            verbosity: Verbosity::Normal,
-        }
-    }
-}
-
-impl From<Verbosity> for MessageInfo {
-    fn from(verbosity: Verbosity) -> MessageInfo {
-        MessageInfo {
-            color_choice: ColorChoice::Auto,
-            verbosity,
-        }
-    }
-}
-
-impl From<(ColorChoice, Verbosity)> for MessageInfo {
-    fn from(info: (ColorChoice, Verbosity)) -> MessageInfo {
-        MessageInfo {
-            color_choice: info.0,
-            verbosity: info.1,
-        }
-    }
-}
-
-// get the prefix for stderr messages
-macro_rules! cross_prefix {
-    ($s:literal) => {
-        concat!("[cross]", " ", $s)
-    };
-}
-
-// generate the color style
-macro_rules! write_style {
-    ($stream:ident, $msg_info:expr, $message:expr $(, $style:ident)* $(,)?) => {{
-        match $msg_info.color_choice {
-            ColorChoice::Always => write!($stream, "{}", $message $(.$style())*),
-            ColorChoice::Never => write!($stream, "{}", $message),
-            ColorChoice::Auto => write!(
-                $stream,
-                "{}",
-                $message $(.if_supports_color($stream.owo(), |text| text.$style()))*
-            ),
-        }?;
-    }};
-}
-
-// low-level interface for printing colorized messages
-macro_rules! message {
-    // write a status message, which has the following format:
-    //  "{status}: {message}"
-    // both status and ':' are bold.
-    (@status $stream:ident, $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{
-        write_style!($stream, $msg_info, $status, bold, $color);
-        write_style!($stream, $msg_info, ":", bold);
-        match $message {
-            Some(message) => writeln!($stream, " {}", message)?,
-            None => write!($stream, " ")?,
-        }
-
-        Ok(())
-    }};
-
-    (@status @name $name:ident, $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{
-        let mut stream = io::$name();
-        message!(@status stream, $status, $message, $color, $msg_info)
-    }};
-}
-
-// high-level interface to message
-macro_rules! status {
-    (@stderr $status:expr, $message:expr, $color:ident, $msg_info:expr $(,)?) => {{
-        message!(@status @name stderr, $status, $message, $color, $msg_info)
-    }};
-
-    (@stdout $status:expr, $message:expr, $color:ident, $msg_info:expr  $(,)?) => {{
-        message!(@status @name stdout, $status, $message, $color, $msg_info)
-    }};
-}
-
-/// prints a red 'error' message and terminates.
-pub fn fatal<T: fmt::Display>(message: T, msg_info: MessageInfo, code: i32) -> ! {
-    error(message, msg_info).unwrap();
-    std::process::exit(code);
-}
-
-/// prints a red 'error' message.
-pub fn error<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    status!(@stderr cross_prefix!("error"), Some(&message), red, msg_info)
-}
-
-/// prints an amber 'warning' message.
-pub fn warn<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => status!(@stderr
-            cross_prefix!("warning"),
-            Some(&message),
-            yellow,
-            msg_info,
-        ),
-    }
-}
-
-/// prints a cyan 'note' message.
-pub fn note<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => status!(@stderr cross_prefix!("note"), Some(&message), cyan, msg_info),
-    }
-}
-
-pub fn status<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => {
-            eprintln!("{}", message);
-            Ok(())
-        }
-    }
-}
-
-/// prints a high-priority message to stdout.
-pub fn print<T: fmt::Display>(message: T, _: MessageInfo) -> Result<()> {
-    println!("{}", message);
-    Ok(())
-}
-
-/// prints a normal message to stdout.
-pub fn info<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet => Ok(()),
-        _ => {
-            println!("{}", message);
-            Ok(())
-        }
-    }
-}
-
-/// prints a debugging message to stdout.
-pub fn debug<T: fmt::Display>(message: T, msg_info: MessageInfo) -> Result<()> {
-    match msg_info.verbosity {
-        Verbosity::Quiet | Verbosity::Normal => Ok(()),
-        _ => {
-            println!("{}", message);
-            Ok(())
-        }
-    }
-}
-
-pub fn fatal_usage<T: fmt::Display>(arg: T, msg_info: MessageInfo, code: i32) -> ! {
-    error_usage(arg, msg_info).unwrap();
-    std::process::exit(code);
-}
-
-fn error_usage<T: fmt::Display>(arg: T, msg_info: MessageInfo) -> Result<()> {
-    let mut stream = io::stderr();
-    write_style!(stream, msg_info, cross_prefix!("error"), bold, red);
-    write_style!(stream, msg_info, ":", bold);
-    write_style!(stream, msg_info, " The argument '");
-    write_style!(stream, msg_info, arg, yellow);
-    write_style!(
-        stream,
-        msg_info,
-        "' requires a value but none was supplied\n"
-    );
-    write_style!(stream, msg_info, "Usage:\n");
-    write_style!(
-        stream,
-        msg_info,
-        "    cross [+toolchain] [OPTIONS] [SUBCOMMAND]\n"
-    );
-    write_style!(stream, msg_info, "\n");
-    write_style!(stream, msg_info, "For more information try ");
-    write_style!(stream, msg_info, "--help", green);
-    write_style!(stream, msg_info, "\n");
-
-    stream.flush()?;
-
-    Ok(())
-}
-
-fn get_color_choice(color: Option<&str>) -> Result<ColorChoice> {
-    match color {
-        Some("always") => Ok(ColorChoice::Always),
-        Some("never") => Ok(ColorChoice::Never),
-        Some("auto") | None => Ok(ColorChoice::Auto),
-        Some(arg) => {
-            eyre::bail!("argument for --color must be auto, always, or never, but found `{arg}`")
-        }
-    }
-}
-
-fn get_verbosity(color_choice: ColorChoice, verbose: bool, quiet: bool) -> Result<Verbosity> {
-    match (verbose, quiet) {
-        (true, true) => {
-            let verbosity = Verbosity::Normal;
-            error(
-                "cannot set both --verbose and --quiet",
-                MessageInfo {
-                    color_choice,
-                    verbosity,
-                },
-            )?;
-            std::process::exit(101);
-        }
-        (true, false) => Ok(Verbosity::Verbose),
-        (false, true) => Ok(Verbosity::Quiet),
-        (false, false) => Ok(Verbosity::Normal),
-    }
-}
-
-pub trait Stream {
-    const TTY: atty::Stream;
-    const OWO: owo_colors::Stream;
-
-    fn is_atty() -> bool {
-        atty::is(Self::TTY)
-    }
-
-    fn owo(&self) -> owo_colors::Stream {
-        Self::OWO
-    }
-}
-
-impl Stream for io::Stdin {
-    const TTY: atty::Stream = atty::Stream::Stdin;
-    const OWO: owo_colors::Stream = owo_colors::Stream::Stdin;
-}
-
-impl Stream for io::Stdout {
-    const TTY: atty::Stream = atty::Stream::Stdout;
-    const OWO: owo_colors::Stream = owo_colors::Stream::Stdout;
-}
-
-impl Stream for io::Stderr {
-    const TTY: atty::Stream = atty::Stream::Stderr;
-    const OWO: owo_colors::Stream = owo_colors::Stream::Stderr;
-}
diff --git a/src/temp.rs b/src/temp.rs
deleted file mode 100644
index 12f24ac..0000000
--- a/src/temp.rs
+++ /dev/null
@@ -1,125 +0,0 @@
-use std::fs;
-use std::path::{Path, PathBuf};
-
-use crate::errors::Result;
-
-// open temporary directories and files so we ensure we cleanup on exit.
-static mut FILES: Vec<tempfile::NamedTempFile> = vec![];
-static mut DIRS: Vec<tempfile::TempDir> = vec![];
-
-fn data_dir() -> Option<PathBuf> {
-    directories::BaseDirs::new().map(|d| d.data_dir().to_path_buf())
-}
-
-pub fn dir() -> Result<PathBuf> {
-    data_dir()
-        .map(|p| p.join("cross-rs").join("tmp"))
-        .ok_or(eyre::eyre!("unable to get data directory"))
-}
-
-pub(crate) fn has_tempfiles() -> bool {
-    // SAFETY: safe, since we only check if the stack is empty.
-    unsafe { !FILES.is_empty() || !DIRS.is_empty() }
-}
-
-/// # Safety
-/// Safe as long as we have single-threaded execution.
-pub(crate) unsafe fn clean() {
-    // don't expose FILES or DIRS outside this module,
-    // so we can only add or remove from the stack using
-    // our wrappers, guaranteeing add/remove in order.
-    FILES.clear();
-    DIRS.clear();
-}
-
-/// # Safety
-/// Safe as long as we have single-threaded execution.
-unsafe fn push_tempfile() -> Result<&'static mut tempfile::NamedTempFile> {
-    let parent = dir()?;
-    fs::create_dir_all(&parent).ok();
-    let file = tempfile::NamedTempFile::new_in(&parent)?;
-    FILES.push(file);
-    Ok(FILES.last_mut().unwrap())
-}
-
-/// # Safety
-/// Safe as long as we have single-threaded execution.
-unsafe fn pop_tempfile() -> Option<tempfile::NamedTempFile> {
-    FILES.pop()
-}
-
-#[derive(Debug)]
-pub struct TempFile {
-    file: &'static mut tempfile::NamedTempFile,
-}
-
-impl TempFile {
-    /// # Safety
-    /// Safe as long as we have single-threaded execution.
-    pub unsafe fn new() -> Result<Self> {
-        Ok(Self {
-            file: push_tempfile()?,
-        })
-    }
-
-    pub fn file(&mut self) -> &mut tempfile::NamedTempFile {
-        self.file
-    }
-
-    pub fn path(&self) -> &Path {
-        self.file.path()
-    }
-}
-
-impl Drop for TempFile {
-    fn drop(&mut self) {
-        // SAFETY: safe if we only modify the stack via `TempFile`.
-        unsafe {
-            pop_tempfile();
-        }
-    }
-}
-
-/// # Safety
-/// Safe as long as we have single-threaded execution.
-unsafe fn push_tempdir() -> Result<&'static Path> {
-    let parent = dir()?;
-    fs::create_dir_all(&parent).ok();
-    let dir = tempfile::TempDir::new_in(&parent)?;
-    DIRS.push(dir);
-    Ok(DIRS.last().unwrap().path())
-}
-
-/// # Safety
-/// Safe as long as we have single-threaded execution.
-unsafe fn pop_tempdir() -> Option<tempfile::TempDir> {
-    DIRS.pop()
-}
-
-#[derive(Debug)]
-pub struct TempDir {
-    path: &'static Path,
-}
-
-impl TempDir {
-    /// # Safety
-    /// Safe as long as we have single-threaded execution.
-    pub unsafe fn new() -> Result<Self> {
-        Ok(Self {
-            path: push_tempdir()?,
-        })
-    }
-
-    pub fn path(&self) -> &'static Path {
-        self.path
-    }
-}
-
-impl Drop for TempDir {
-    fn drop(&mut self) {
-        // SAFETY: safe if we only modify the stack via `TempDir`.
-        unsafe {
-            pop_tempdir();
-        }
-    }
-}
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
index 8cafced..5539003 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -19,4 +19,3 @@ serde = { version = "1", features = ["derive"] }
 serde_yaml = "0.8"
 serde_json = "1.0"
 once_cell = "1.12"
-semver = "1"
diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs
index eacaf17..bfc4e1d 100644
--- a/xtask/src/build_docker_image.rs
+++ b/xtask/src/build_docker_image.rs
@@ -1,74 +1,62 @@
-use std::fmt::Write;
-use std::path::Path;
+use std::{path::Path, process::Command};
 
-use crate::util::{cargo_metadata, gha_error, gha_output, gha_print};
 use clap::Args;
-use cross::shell::{self, MessageInfo};
-use cross::{docker, CommandExt, ToUtf8};
+use color_eyre::Section;
+use cross::{CommandExt, ToUtf8};
+use std::fmt::Write;
 
 #[derive(Args, Debug)]
 pub struct BuildDockerImage {
     #[clap(long, hide = true, env = "GITHUB_REF_TYPE")]
-    pub ref_type: Option<String>,
+    ref_type: Option<String>,
     #[clap(long, hide = true, env = "GITHUB_REF_NAME")]
     ref_name: Option<String>,
-    #[clap(action, long = "latest", hide = true, env = "LATEST")]
-    is_latest: bool,
     /// Specify a tag to use instead of the derived one, eg `local`
     #[clap(long)]
-    pub tag: Option<String>,
+    tag: Option<String>,
     /// Repository name for image.
-    #[clap(long, default_value = docker::CROSS_IMAGE)]
-    pub repository: String,
+    #[clap(long, default_value = cross::docker::CROSS_IMAGE)]
+    repository: String,
     /// Newline separated labels
     #[clap(long, env = "LABELS")]
-    pub labels: Option<String>,
+    labels: Option<String>,
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
+    verbose: bool,
     /// Print but do not execute the build commands.
     #[clap(long)]
-    pub dry_run: bool,
+    dry_run: bool,
     /// Force a push when `--push` is set, but not `--tag`
     #[clap(long, hide = true)]
-    pub force: bool,
+    force: bool,
     /// Push build to registry.
     #[clap(short, long)]
-    pub push: bool,
+    push: bool,
     /// Set output to /dev/null
     #[clap(short, long)]
-    pub no_output: bool,
+    no_output: bool,
     /// Docker build progress output type.
     #[clap(
         long,
-        value_parser = clap::builder::PossibleValuesParser::new(["auto", "plain", "tty"]),
+        value_parser = clap::builder::PossibleValuesParser::new(["auto", "plain", "tty"]), 
         default_value = "auto"
     )]
-    pub progress: String,
+    progress: String,
     /// Do not load from cache when building the image.
     #[clap(long)]
-    pub no_cache: bool,
+    no_cache: bool,
     /// Continue building images even if an image fails to build.
     #[clap(long)]
-    pub no_fastfail: bool,
+    no_fastfail: bool,
     /// Container engine (such as docker or podman).
     #[clap(long)]
     pub engine: Option<String>,
     /// If no target list is provided, parse list from CI.
     #[clap(long)]
-    pub from_ci: bool,
-    /// Additional build arguments to pass to Docker.
-    #[clap(long)]
-    pub build_arg: Vec<String>,
+    from_ci: bool,
     /// Targets to build for
     #[clap()]
-    pub targets: Vec<crate::ImageTarget>,
+    targets: Vec<crate::ImageTarget>,
 }
 
 fn locate_dockerfile(
@@ -92,13 +80,10 @@ pub fn build_docker_image(
     BuildDockerImage {
         ref_type,
         ref_name,
-        is_latest,
         tag: tag_override,
         repository,
         labels,
         verbose,
-        quiet,
-        color,
         dry_run,
         force,
         push,
@@ -107,14 +92,17 @@ pub fn build_docker_image(
         no_cache,
         no_fastfail,
         from_ci,
-        build_arg,
         mut targets,
         ..
     }: BuildDockerImage,
-    engine: &docker::Engine,
+    engine: &Path,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let metadata = cargo_metadata(msg_info)?;
+    let metadata = cross::cargo_metadata_with_args(
+        Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
+        None,
+        verbose,
+    )?
+    .ok_or_else(|| eyre::eyre!("could not find cross workspace and its current version"))?;
     let version = metadata
         .get_package("cross")
         .expect("cross expected in workspace")
@@ -154,9 +142,9 @@ pub fn build_docker_image(
     let mut results = vec![];
     for (target, dockerfile) in &targets {
         if gha && targets.len() > 1 {
-            gha_print("::group::Build {target}");
+            println!("::group::Build {target}");
         }
-        let mut docker_build = docker::command(engine);
+        let mut docker_build = Command::new(engine);
         docker_build.args(&["buildx", "build"]);
         docker_build.current_dir(&docker_root);
 
@@ -176,7 +164,6 @@ pub fn build_docker_image(
                 &repository,
                 ref_type,
                 ref_name,
-                is_latest,
                 &version,
             )?),
             _ => {
@@ -233,18 +220,18 @@ pub fn build_docker_image(
         } else {
             docker_build.args(&["--progress", &progress]);
         }
-        for arg in &build_arg {
-            docker_build.args(&["--build-arg", arg]);
-        }
 
         docker_build.arg(".");
 
         if !dry_run && (force || !push || gha) {
-            let result = docker_build.run(msg_info, false);
+            let result = docker_build.run(verbose, false);
             if gha && targets.len() > 1 {
                 if let Err(e) = &result {
                     // TODO: Determine what instruction errorred, and place warning on that line with appropriate warning
-                    gha_error(&format!("file=docker/{dockerfile},title=Build failed::{e}"));
+                    println!(
+                        "::error file=docker/{},title=Build failed::{}",
+                        dockerfile, e
+                    )
                 }
             }
             results.push(
@@ -256,16 +243,19 @@ pub fn build_docker_image(
                 break;
             }
         } else {
-            docker_build.print(msg_info)?;
+            docker_build.print_verbose(true);
             if !dry_run {
-                shell::fatal("refusing to push, use --force to override", msg_info, 1);
+                panic!("refusing to push, use --force to override");
             }
         }
         if gha {
-            gha_output("image", &tags[0]);
-            gha_output("images", &format!("'{}'", serde_json::to_string(&tags)?));
+            println!("::set-output name=image::{}", &tags[0]);
+            println!(
+                "::set-output name=images::'{}'",
+                serde_json::to_string(&tags)?
+            );
             if targets.len() > 1 {
-                gha_print("::endgroup::");
+                println!("::endgroup::");
             }
         }
     }
@@ -273,10 +263,10 @@ pub fn build_docker_image(
         std::env::set_var("GITHUB_STEP_SUMMARY", job_summary(&results)?);
     }
     if results.iter().any(|r| r.is_err()) {
-        results
-            .into_iter()
-            .filter_map(Result::err)
-            .fold(Err(eyre::eyre!("encountered error(s)")), |_, e| Err(e.1))?;
+        results.into_iter().filter_map(Result::err).fold(
+            Err(eyre::eyre!("encountered error(s)")),
+            |report: Result<(), color_eyre::Report>, e| report.error(e.1),
+        )?;
     }
     Ok(())
 }
@@ -286,7 +276,6 @@ pub fn determine_image_name(
     repository: &str,
     ref_type: &str,
     ref_name: &str,
-    is_latest: bool,
     version: &str,
 ) -> cross::Result<Vec<String>> {
     let mut tags = vec![];
@@ -300,7 +289,8 @@ pub fn determine_image_name(
             }
             tags.push(target.image_name(repository, version));
             // Check for unstable releases, tag stable releases as `latest`
-            if is_latest {
+            if version.contains('-') {
+                // TODO: Don't tag if version is older than currently released version.
                 tags.push(target.image_name(repository, "latest"))
             }
         }
@@ -320,7 +310,7 @@ pub fn determine_image_name(
 }
 
 pub fn job_summary(
-    results: &[Result<crate::ImageTarget, (crate::ImageTarget, eyre::ErrReport)>],
+    results: &[Result<crate::ImageTarget, (crate::ImageTarget, cross::errors::CommandError)>],
 ) -> cross::Result<String> {
     let mut summary = "# SUMMARY\n\n".to_string();
     let success: Vec<_> = results.iter().filter_map(|r| r.as_ref().ok()).collect();
diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs
index d32b2c0..fa8edb1 100644
--- a/xtask/src/ci.rs
+++ b/xtask/src/ci.rs
@@ -1,11 +1,9 @@
-use crate::util::gha_output;
+use std::path::Path;
+
 use clap::Subcommand;
-use cross::shell::Verbosity;
-use cross::{cargo_command, CargoMetadata, CommandExt};
 
 #[derive(Subcommand, Debug)]
 pub enum CiJob {
-    /// Return needed metadata for building images
     PrepareMeta {
         // tag, branch
         #[clap(long, env = "GITHUB_REF_TYPE")]
@@ -15,28 +13,25 @@ pub enum CiJob {
         ref_name: String,
         target: crate::ImageTarget,
     },
-    /// Check workspace metadata.
-    Check {
-        // tag, branch
-        #[clap(long, env = "GITHUB_REF_TYPE")]
-        ref_type: String,
-        // main, v0.1.0
-        #[clap(long, env = "GITHUB_REF_NAME")]
-        ref_name: String,
-    },
 }
 
-pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> {
-    let cross_meta = metadata
-        .get_package("cross")
-        .expect("cross expected in workspace");
-
+pub fn ci(args: CiJob) -> cross::Result<()> {
     match args {
         CiJob::PrepareMeta {
             ref_type,
             ref_name,
             target,
         } => {
+            let metadata = cross::cargo_metadata_with_args(
+                Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
+                None,
+                true,
+            )?
+            .ok_or_else(|| eyre::eyre!("could not find cross workspace and its current version"))?;
+            let cross_meta = metadata
+                .get_package("cross")
+                .expect("cross expected in workspace");
+
             // Set labels
             let mut labels = vec![];
 
@@ -61,7 +56,6 @@ pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> {
                     cross::docker::CROSS_IMAGE,
                     &ref_type,
                     &ref_name,
-                    false,
                     &version,
                 )?[0],
             );
@@ -70,31 +64,15 @@ pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> {
                 gha_output("has-image", "true")
             }
         }
-        CiJob::Check { ref_type, ref_name } => {
-            let version = semver::Version::parse(&cross_meta.version)?;
-            if ref_type == "tag" {
-                if ref_name.starts_with('v') && ref_name != format!("v{version}") {
-                    eyre::bail!("a version tag was published, but the tag does not match the current version in Cargo.toml");
-                }
-                let search = cargo_command()
-                    .args(&["search", "--limit", "1"])
-                    .arg("cross")
-                    .run_and_get_stdout(Verbosity::Verbose.into())?;
-                let (cross, rest) = search
-                    .split_once(" = ")
-                    .ok_or_else(|| eyre::eyre!("cargo search failed"))?;
-                assert_eq!(cross, "cross");
-                // Note: this version includes pre-releases.
-                let latest_version = semver::Version::parse(
-                    rest.split('"')
-                        .nth(1)
-                        .ok_or_else(|| eyre::eyre!("cargo search returned unexpected data"))?,
-                )?;
-                if version >= latest_version && version.pre.is_empty() {
-                    gha_output("is-latest", "true")
-                }
-            }
-        }
     }
     Ok(())
 }
+
+#[track_caller]
+fn gha_output(tag: &str, content: &str) {
+    if content.contains('\n') {
+        // https://github.com/actions/toolkit/issues/403
+        panic!("output `{tag}` contains newlines, consider serializing with json and deserializing in gha with fromJSON()")
+    }
+    println!("::set-output name={tag}::{}", content)
+}
diff --git a/xtask/src/hooks.rs b/xtask/src/hooks.rs
index 35d3d96..b2b7028 100644
--- a/xtask/src/hooks.rs
+++ b/xtask/src/hooks.rs
@@ -3,9 +3,7 @@ use std::io::{BufRead, BufReader, ErrorKind};
 use std::path::Path;
 use std::process::Command;
 
-use crate::util::{cargo, get_channel_prefer_nightly};
 use clap::Args;
-use cross::shell::{self, MessageInfo};
 use cross::CommandExt;
 
 const CARGO_FLAGS: &[&str] = &["--all-features", "--all-targets", "--workspace"];
@@ -14,13 +12,7 @@ const CARGO_FLAGS: &[&str] = &["--all-features", "--all-targets", "--workspace"]
 pub struct Check {
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
+    verbose: bool,
     /// Run shellcheck on all files, not just staged files.
     #[clap(short, long)]
     all: bool,
@@ -30,36 +22,59 @@ pub struct Check {
 pub struct Test {
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
+    verbose: bool,
+}
+
+fn has_nightly(verbose: bool) -> cross::Result<bool> {
+    cross::cargo_command()
+        .arg("+nightly")
+        .run_and_get_output(verbose)
+        .map(|o| o.status.success())
+        .map_err(Into::into)
+}
+
+fn get_channel_prefer_nightly(
+    verbose: bool,
+    toolchain: Option<&str>,
+) -> cross::Result<Option<&str>> {
+    Ok(match toolchain {
+        Some(t) => Some(t),
+        None => match has_nightly(verbose)? {
+            true => Some("nightly"),
+            false => None,
+        },
+    })
 }
 
-fn cargo_fmt(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> {
+fn cargo(channel: Option<&str>) -> Command {
+    let mut command = cross::cargo_command();
+    if let Some(channel) = channel {
+        command.arg(&format!("+{channel}"));
+    }
+    command
+}
+
+fn cargo_fmt(verbose: bool, channel: Option<&str>) -> cross::Result<()> {
     cargo(channel)
         .args(&["fmt", "--", "--check"])
-        .run(msg_info, false)
+        .run(verbose, false)
         .map_err(Into::into)
 }
 
-fn cargo_clippy(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> {
+fn cargo_clippy(verbose: bool, channel: Option<&str>) -> cross::Result<()> {
     cargo(channel)
         .arg("clippy")
         .args(CARGO_FLAGS)
         .args(&["--", "--deny", "warnings"])
-        .run(msg_info, false)
+        .run(verbose, false)
         .map_err(Into::into)
 }
 
-fn cargo_test(msg_info: MessageInfo, channel: Option<&str>) -> cross::Result<()> {
+fn cargo_test(verbose: bool, channel: Option<&str>) -> cross::Result<()> {
     cargo(channel)
         .arg("test")
         .args(CARGO_FLAGS)
-        .run(msg_info, false)
+        .run(verbose, false)
         .map_err(Into::into)
 }
 
@@ -67,17 +82,17 @@ fn splitlines(string: String) -> Vec<String> {
     string.lines().map(|l| l.to_string()).collect()
 }
 
-fn staged_files(msg_info: MessageInfo) -> cross::Result<Vec<String>> {
+fn staged_files(verbose: bool) -> cross::Result<Vec<String>> {
     Command::new("git")
         .args(&["diff", "--cached", "--name-only", "--diff-filter=ACM"])
-        .run_and_get_stdout(msg_info)
+        .run_and_get_stdout(verbose)
         .map(splitlines)
 }
 
-fn all_files(msg_info: MessageInfo) -> cross::Result<Vec<String>> {
+fn all_files(verbose: bool) -> cross::Result<Vec<String>> {
     Command::new("git")
         .arg("ls-files")
-        .run_and_get_stdout(msg_info)
+        .run_and_get_stdout(verbose)
         .map(splitlines)
 }
 
@@ -100,11 +115,11 @@ fn is_shell_script(path: impl AsRef<Path>) -> cross::Result<bool> {
     }
 }
 
-fn shellcheck(all: bool, msg_info: MessageInfo) -> cross::Result<()> {
+fn shellcheck(all: bool, verbose: bool) -> cross::Result<()> {
     if which::which("shellcheck").is_ok() {
         let files = match all {
-            true => all_files(msg_info),
-            false => staged_files(msg_info),
+            true => all_files(verbose),
+            false => staged_files(verbose),
         }?;
         let mut scripts = vec![];
         for file in files {
@@ -115,47 +130,30 @@ fn shellcheck(all: bool, msg_info: MessageInfo) -> cross::Result<()> {
         if !scripts.is_empty() {
             Command::new("shellcheck")
                 .args(&scripts)
-                .run(msg_info, false)?;
+                .run(verbose, false)?;
         }
     }
 
     Ok(())
 }
 
-pub fn check(
-    Check {
-        verbose,
-        quiet,
-        color,
-        all,
-    }: Check,
-    toolchain: Option<&str>,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    shell::info("Running rustfmt, clippy, and shellcheck checks.", msg_info)?;
+pub fn check(Check { verbose, all }: Check, toolchain: Option<&str>) -> cross::Result<()> {
+    println!("Running rustfmt, clippy, and shellcheck checks.");
 
-    let channel = get_channel_prefer_nightly(msg_info, toolchain)?;
-    cargo_fmt(msg_info, channel)?;
-    cargo_clippy(msg_info, channel)?;
-    shellcheck(all, msg_info)?;
+    let channel = get_channel_prefer_nightly(verbose, toolchain)?;
+    cargo_fmt(verbose, channel)?;
+    cargo_clippy(verbose, channel)?;
+    shellcheck(all, verbose)?;
 
     Ok(())
 }
 
-pub fn test(
-    Test {
-        verbose,
-        quiet,
-        color,
-    }: Test,
-    toolchain: Option<&str>,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    shell::info("Running cargo fmt and tests", msg_info)?;
+pub fn test(Test { verbose }: Test, toolchain: Option<&str>) -> cross::Result<()> {
+    println!("Running cargo fmt and tests");
 
-    let channel = get_channel_prefer_nightly(msg_info, toolchain)?;
-    cargo_fmt(msg_info, channel)?;
-    cargo_test(msg_info, channel)?;
+    let channel = get_channel_prefer_nightly(verbose, toolchain)?;
+    cargo_fmt(verbose, channel)?;
+    cargo_test(verbose, channel)?;
 
     Ok(())
 }
diff --git a/xtask/src/install_git_hooks.rs b/xtask/src/install_git_hooks.rs
index 41c097e..60a190d 100644
--- a/xtask/src/install_git_hooks.rs
+++ b/xtask/src/install_git_hooks.rs
@@ -1,32 +1,23 @@
-use crate::util::project_dir;
 use clap::Args;
-use cross::shell::MessageInfo;
+
+use std::path::Path;
 
 #[derive(Args, Debug)]
 pub struct InstallGitHooks {
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
+    verbose: bool,
 }
 
-pub fn install_git_hooks(
-    InstallGitHooks {
+pub fn install_git_hooks(InstallGitHooks { verbose }: InstallGitHooks) -> cross::Result<()> {
+    let metadata = cross::cargo_metadata_with_args(
+        Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
+        None,
         verbose,
-        quiet,
-        color,
-    }: InstallGitHooks,
-) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
-    let root = project_dir(msg_info)?;
-    let git_hooks = root.join(".git").join("hooks");
-    let cross_dev = root.join("xtask").join("src");
-
+    )?
+    .ok_or_else(|| eyre::eyre!("could not find cross workspace"))?;
+    let git_hooks = metadata.workspace_root.join(".git").join("hooks");
+    let cross_dev = metadata.workspace_root.join("xtask").join("src");
     std::fs::copy(
         cross_dev.join("pre-commit.sh"),
         git_hooks.join("pre-commit"),
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 732cd7e..74a7dac 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -7,11 +7,11 @@ pub mod install_git_hooks;
 pub mod target_info;
 pub mod util;
 
+use std::path::PathBuf;
+
 use ci::CiJob;
 use clap::{CommandFactory, Parser, Subcommand};
-use cross::docker;
-use cross::shell::{MessageInfo, Verbosity};
-use util::{cargo_metadata, ImageTarget};
+use util::ImageTarget;
 
 use self::build_docker_image::BuildDockerImage;
 use self::hooks::{Check, Test};
@@ -66,13 +66,11 @@ pub fn main() -> cross::Result<()> {
     let cli = Cli::parse();
     match cli.command {
         Commands::TargetInfo(args) => {
-            let msg_info = MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
-            let engine = get_container_engine(args.engine.as_deref(), msg_info)?;
+            let engine = get_container_engine(args.engine.as_deref())?;
             target_info::target_info(args, &engine)?;
         }
         Commands::BuildDockerImage(args) => {
-            let msg_info = MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
-            let engine = get_container_engine(args.engine.as_deref(), msg_info)?;
+            let engine = get_container_engine(args.engine.as_deref())?;
             build_docker_image::build_docker_image(args, &engine)?;
         }
         Commands::InstallGitHooks(args) => {
@@ -84,23 +82,16 @@ pub fn main() -> cross::Result<()> {
         Commands::Test(args) => {
             hooks::test(args, cli.toolchain.as_deref())?;
         }
-        Commands::CiJob(args) => {
-            let metadata = cargo_metadata(Verbosity::Verbose.into())?;
-            ci::ci(args, metadata)?
-        }
+        Commands::CiJob(args) => ci::ci(args)?,
     }
 
     Ok(())
 }
 
-fn get_container_engine(
-    engine: Option<&str>,
-    msg_info: MessageInfo,
-) -> cross::Result<docker::Engine> {
-    let engine = if let Some(ce) = engine {
-        which::which(ce)?
+fn get_container_engine(engine: Option<&str>) -> Result<PathBuf, which::Error> {
+    if let Some(ce) = engine {
+        which::which(ce)
     } else {
-        docker::get_container_engine()?
-    };
-    docker::Engine::from_path(engine, None, msg_info)
+        cross::docker::get_container_engine()
+    }
 }
diff --git a/xtask/src/target_info.rs b/xtask/src/target_info.rs
index fa16991..8ed54d3 100644
--- a/xtask/src/target_info.rs
+++ b/xtask/src/target_info.rs
@@ -1,9 +1,12 @@
-use std::collections::BTreeMap;
+use std::{
+    collections::BTreeMap,
+    path::Path,
+    process::{Command, Stdio},
+};
 
 use crate::util::{format_repo, pull_image};
 use clap::Args;
-use cross::shell::MessageInfo;
-use cross::{docker, CommandExt};
+use cross::CommandExt;
 
 // Store raw text data in the binary so we don't need a data directory
 // when extracting all targets, or running our target info script.
@@ -12,43 +15,37 @@ const TARGET_INFO_SCRIPT: &str = include_str!("target_info.sh");
 #[derive(Args, Debug)]
 pub struct TargetInfo {
     /// If not provided, get info for all targets.
-    pub targets: Vec<crate::ImageTarget>,
+    targets: Vec<crate::ImageTarget>,
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
-    /// Do not print cross log messages.
-    #[clap(short, long)]
-    pub quiet: bool,
-    /// Whether messages should use color output.
-    #[clap(long)]
-    pub color: Option<String>,
+    verbose: bool,
     /// Image registry.
     #[clap(long, default_value_t = String::from("ghcr.io"))]
-    pub registry: String,
+    registry: String,
     /// Image repository.
     #[clap(long, default_value_t = String::from("cross-rs"))]
-    pub repository: String,
+    repository: String,
     /// Image tag.
     #[clap(long, default_value_t = String::from("main"))]
-    pub tag: String,
+    tag: String,
     /// Container engine (such as docker or podman).
     #[clap(long)]
     pub engine: Option<String>,
 }
 
 fn image_info(
-    engine: &docker::Engine,
+    engine: &Path,
     target: &crate::ImageTarget,
     image: &str,
     tag: &str,
-    msg_info: MessageInfo,
+    verbose: bool,
     has_test: bool,
 ) -> cross::Result<()> {
     if !tag.starts_with("local") {
-        pull_image(engine, image, msg_info)?;
+        pull_image(engine, image, verbose)?;
     }
 
-    let mut command = docker::command(engine);
+    let mut command = Command::new(engine);
     command.arg("run");
     command.arg("--rm");
     command.args(&["-e", &format!("TARGET={}", target.triplet)]);
@@ -59,25 +56,25 @@ fn image_info(
     }
     command.arg(image);
     command.args(&["bash", "-c", TARGET_INFO_SCRIPT]);
-    command
-        .run(msg_info, !msg_info.verbose())
-        .map_err(Into::into)
+
+    if !verbose {
+        // capture stderr to avoid polluting table
+        command.stderr(Stdio::null());
+    }
+    command.run(verbose, false).map_err(Into::into)
 }
 
 pub fn target_info(
     TargetInfo {
         mut targets,
         verbose,
-        quiet,
-        color,
         registry,
         repository,
         tag,
         ..
     }: TargetInfo,
-    engine: &docker::Engine,
+    engine: &Path,
 ) -> cross::Result<()> {
-    let msg_info = MessageInfo::create(verbose, quiet, color.as_deref())?;
     let matrix = crate::util::get_matrix();
     let test_map: BTreeMap<crate::ImageTarget, bool> = matrix
         .iter()
@@ -98,7 +95,7 @@ pub fn target_info(
             .get(&target)
             .cloned()
             .ok_or_else(|| eyre::eyre!("invalid target name {}", target))?;
-        image_info(engine, &target, &image, &tag, msg_info, has_test)?;
+        image_info(engine, &target, &image, &tag, verbose, has_test)?;
     }
 
     Ok(())
diff --git a/xtask/src/target_info.sh b/xtask/src/target_info.sh
index b6cc8e9..b8fa90f 100755
--- a/xtask/src/target_info.sh
+++ b/xtask/src/target_info.sh
@@ -62,48 +62,6 @@ max_glibc_version() {
     echo "${major}.${minor}"
 }
 
-max_solaris_libc_version() {
-    # solaris libc versions have the following format:
-    #  67: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  ABS SUNW_1.21.1
-    local major=0
-    local minor=0
-    local patch=0
-    local version
-    local x
-    local y
-    local z
-    local is_larger
-
-    for version in "${@}"; do
-        x=$(echo "${version}" | cut -d '.' -f 1)
-        y=$(echo "${version}" | cut -d '.' -f 2)
-        z=$(echo "${version}" | cut -d '.' -f 3)
-        is_larger=
-
-        if [ "${x}" -gt "${major}" ]; then
-            is_larger=1
-        elif [ "${x}" -eq "${major}" ] && [ "${y}" -gt "${minor}" ]; then
-            is_larger=1
-        elif [ -z "${patch}" ]; then
-            is_larger=1
-        elif [ -n "${z}" ] && [ "${x}" -eq "${major}" ] && [ "${y}" -eq "${minor}" ] && [ "${z}" -gt "${patch}" ]; then
-            is_larger=1
-        fi
-
-        if [ -n "${is_larger}" ]; then
-            major="${x}"
-            minor="${y}"
-            patch="${z}"
-        fi
-    done
-
-    result="${major}.${minor}"
-    if [ -n "${patch}" ]; then
-        result="${result}.${patch}"
-    fi
-    echo "${result}"
-}
-
 readelf_all() {
     # weirdly, readelf -a can produce a non-zero error code.
     set +e
@@ -146,12 +104,6 @@ case "${target}" in
      *-*-dragonfly)
         cc_regex=".*gcc \(GCC\) ([0-9]+.[0-9]+.[0-9]+).*"
         ;;
-    *-*-solaris)
-        cc_regex=".*gcc \(GCC\) ([0-9]+.[0-9]+.[0-9]+).*"
-        ;;
-    *-*-emscripten)
-        cc_regex="clang version ([0-9]+.[0-9]+.[0-9]+).*"
-        ;;
     *-none-*)
         cc_regex=".*gcc \(.*\) ([0-9]+.[0-9]+.[0-9]+).*"
         ;;
@@ -198,14 +150,11 @@ case "${target}" in
         cc_bin=arm-none-eabi-gcc
         cxx_bin=arm-none-eabi-g++
         ;;
-    *-*-emscripten)
-        cc_bin="${EMSDK}/upstream/bin/clang"
-        cxx_bin="${cc_bin}"
-        ;;
     *)
         cc_bin="${!cc_var}"
         cxx_bin="${!cxx_var}"
         ;;
+
 esac
 cc=$(extract_regex_version "${cc_bin}" "${cc_regex}" compiler)
 if command -v "${cxx_bin}" &>/dev/null; then
@@ -220,12 +169,6 @@ EOT
     if [[ "${target}" == *-none-* ]]; then
         cxx_flags=("${cxx_flags[@]}" "-nostartfiles")
     fi
-    if [[ "${target}" == *-*-emscripten ]]; then
-        # need to switch to the real c++ compiler here, not the wrapper
-        cxx_bin=emcc
-        # shellcheck disable=SC2206
-        cxx_flags=($EMCC_CFLAGS)
-    fi
     if "${cxx_bin}" "${cxx_flags[@]}" main.cc >/dev/null 2>&1; then
         cxx=1
     fi
@@ -344,25 +287,6 @@ case "${target}" in
             exit 1
         fi
         ;;
-    *-*-solaris)
-        # we can read the libc version from the libc symbols
-        # first, we need to use our compiler name to get the libdir
-        #    67: 0000000000000000     0 OBJECT  GLOBAL DEFAULT  ABS SUNW_1.21.1
-        # there will be many of these, so we want to grab the highest one.
-        prefix="${cc_bin//-gcc/}"
-        libdir="/usr/local/${prefix}/lib"
-        lines=$(readelf_all "${libdir}"/libc.so | grep 'ABS SUNW_')
-        lines=$(echo "${lines}" | grep -o 'ABS .*')
-        # shellcheck disable=SC2207
-        libc_versions=($(echo "$lines" | cut -d ' ' -f 2 | cut -d '_' -f 2))
-        libc=$(max_solaris_libc_version "${libc_versions[@]}")
-        ;;
-    *-*-emscripten)
-        # we want the emsdk version, which is the image version
-        libc_cmd=emcc
-        libc_regex="emcc \(.* GNU ld) ([0-9]+.[0-9]+.[0-9]+).*"
-        libc=$(extract_regex_version "${libc_cmd}" "${libc_regex}" libc)
-        ;;
     thumb*-none-eabi* | arm*-none-eabi*)
         # newlib kinda sucks. just query for the install package
         pkg=$(dpkg --get-selections | grep -v deinstall | grep newlib | head -n 1)
diff --git a/xtask/src/util.rs b/xtask/src/util.rs
index d454c32..598b68d 100644
--- a/xtask/src/util.rs
+++ b/xtask/src/util.rs
@@ -1,9 +1,7 @@
-use std::fs;
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::process::Command;
 
-use cross::shell::MessageInfo;
-use cross::{docker, CommandExt, ToUtf8};
+use cross::CommandExt;
 use once_cell::sync::OnceCell;
 use serde::Deserialize;
 
@@ -86,15 +84,12 @@ pub fn format_repo(registry: &str, repository: &str) -> String {
     output
 }
 
-pub fn pull_image(
-    engine: &docker::Engine,
-    image: &str,
-    msg_info: MessageInfo,
-) -> cross::Result<()> {
-    let mut command = docker::subcommand(engine, "pull");
+pub fn pull_image(engine: &Path, image: &str, verbose: bool) -> cross::Result<()> {
+    let mut command = Command::new(engine);
+    command.arg("pull");
     command.arg(image);
-    let out = command.run_and_get_output(msg_info)?;
-    command.status_result(msg_info, out.status, Some(&out))?;
+    let out = command.run_and_get_output(verbose)?;
+    command.status_result(verbose, out.status, Some(&out))?;
     Ok(())
 }
 
@@ -157,125 +152,3 @@ impl std::fmt::Display for ImageTarget {
         }
     }
 }
-
-pub fn has_nightly(msg_info: MessageInfo) -> cross::Result<bool> {
-    cross::cargo_command()
-        .arg("+nightly")
-        .run_and_get_output(msg_info)
-        .map(|o| o.status.success())
-        .map_err(Into::into)
-}
-
-pub fn get_channel_prefer_nightly(
-    msg_info: MessageInfo,
-    toolchain: Option<&str>,
-) -> cross::Result<Option<&str>> {
-    Ok(match toolchain {
-        Some(t) => Some(t),
-        None => match has_nightly(msg_info)? {
-            true => Some("nightly"),
-            false => None,
-        },
-    })
-}
-
-pub fn cargo(channel: Option<&str>) -> Command {
-    let mut command = cross::cargo_command();
-    if let Some(channel) = channel {
-        command.arg(&format!("+{channel}"));
-    }
-    command
-}
-
-pub fn cargo_metadata(msg_info: MessageInfo) -> cross::Result<cross::CargoMetadata> {
-    cross::cargo_metadata_with_args(Some(Path::new(env!("CARGO_MANIFEST_DIR"))), None, msg_info)?
-        .ok_or_else(|| eyre::eyre!("could not find cross workspace"))
-}
-
-pub fn project_dir(msg_info: MessageInfo) -> cross::Result<PathBuf> {
-    Ok(cargo_metadata(msg_info)?.workspace_root)
-}
-
-// note: for GHA actions we need to output these tags no matter the verbosity level
-pub fn gha_print(content: &str) {
-    println!("{}", content)
-}
-
-// note: for GHA actions we need to output these tags no matter the verbosity level
-pub fn gha_error(content: &str) {
-    println!("::error {}", content)
-}
-
-#[track_caller]
-pub fn gha_output(tag: &str, content: &str) {
-    if content.contains('\n') {
-        // https://github.com/actions/toolkit/issues/403
-        panic!("output `{tag}` contains newlines, consider serializing with json and deserializing in gha with fromJSON()")
-    }
-    println!("::set-output name={tag}::{}", content)
-}
-
-pub fn read_dockerfiles(msg_info: MessageInfo) -> cross::Result<Vec<String>> {
-    let root = project_dir(msg_info)?;
-    let docker = root.join("docker");
-    let mut dockerfiles = vec![];
-    for entry in fs::read_dir(docker)? {
-        let entry = entry?;
-        let file_type = entry.file_type()?;
-        let file_name = entry.file_name();
-        if file_type.is_file() && file_name.to_utf8()?.starts_with("Dockerfile") {
-            dockerfiles.push(fs::read_to_string(entry.path())?);
-        }
-    }
-
-    Ok(dockerfiles)
-}
-
-#[cfg(tests)]
-mod tests {
-    use super::*;
-
-    use crate::util::read_dockerfiles;
-    use cross::shell::Verbosity;
-    use std::collections::BTreeMap;
-
-    #[test]
-    fn check_ubuntu_base() -> cross::Result<()> {
-        // count all the entries of FROM for our images
-        let mut counts = BTreeMap::new();
-        let dockerfiles = read_dockerfiles(Verbosity::Verbose.into())?;
-        for dockerfile in dockerfiles {
-            let lines: Vec<&str> = dockerfile.lines().collect();
-            let index = lines
-                .iter()
-                .map(|x| x.trim())
-                .position(|x| x.to_lowercase().starts_with("from"))
-                .ok_or_else(|| {
-                    eyre::eyre!("unable to find FROM instruction for {:?}", entry.path())
-                })?;
-            let tag = lines[index]
-                .split_whitespace()
-                .nth(1)
-                .ok_or_else(|| eyre::eyre!("invalid FROM instruction, got {}", lines[index]))?;
-            if let Some(value) = counts.get_mut(tag) {
-                *value += 1;
-            } else {
-                counts.insert(tag.to_string(), 1);
-            }
-        }
-
-        // Now, get the most common and ensure our base is correct.
-        let actual_base = cross::docker::UBUNTU_BASE;
-        let max_base = counts
-            .iter()
-            .max_by(|x, y| x.1.cmp(y.1))
-            .map(|(k, _)| k)
-            .ok_or_else(|| eyre::eyre!("have no dockerfiles"))?;
-
-        if actual_base != max_base {
-            eyre::bail!("most common base image is {max_base} but source code has {actual_base}")
-        } else {
-            Ok(())
-        }
-    }
-}


Test patch:
diff --git a/ci/test.sh b/ci/test.sh
index d47a52a..b920429 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -41,7 +41,10 @@ main() {
     local td=
 
     retry cargo fetch
-    cargo install --force --path .
+    cargo install --force --path . --debug
+
+    # Unset RUSTFLAGS
+    export RUSTFLAGS=""
 
     export QEMU_STRACE=1
 
@@ -165,7 +168,7 @@ main() {
                     --depth 1 \
                     --recursive \
                     https://github.com/cross-rs/test-workspace "${td}"
-                
+
                 pushd "${td}"
                 TARGET="${TARGET}" workspace_test --manifest-path="./workspace/Cargo.toml"
                 pushd "workspace"
diff --git a/src/tests.rs b/src/tests.rs
index 98f30be..92b59ba 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -14,14 +14,10 @@ static WORKSPACE: OnceCell<PathBuf> = OnceCell::new();
 pub fn get_cargo_workspace() -> &'static Path {
     let manifest_dir = env!("CARGO_MANIFEST_DIR");
     WORKSPACE.get_or_init(|| {
-        crate::cargo_metadata_with_args(
-            Some(manifest_dir.as_ref()),
-            None,
-            crate::shell::Verbosity::Verbose.into(),
-        )
-        .unwrap()
-        .unwrap()
-        .workspace_root
+        crate::cargo_metadata_with_args(Some(manifest_dir.as_ref()), None, true)
+            .unwrap()
+            .unwrap()
+            .workspace_root
     })
 }
 
@@ -75,14 +71,7 @@ release: {version}
         let target_meta = dbg!(make_rustc_version(targ));
         assert_eq!(
             expected,
-            warn_host_version_mismatch(
-                &host_meta,
-                "xxxx",
-                &target_meta.0,
-                &target_meta.1,
-                crate::shell::MessageInfo::default()
-            )
-            .unwrap(),
+            warn_host_version_mismatch(&host_meta, "xxxx", &target_meta.0, &target_meta.1).unwrap(),
             "\nhost = {}\ntarg = {}",
             host,
             targ
diff --git a/src/tests/toml.rs b/src/tests/toml.rs
index 4501755..c7479f1 100644
--- a/src/tests/toml.rs
+++ b/src/tests/toml.rs
@@ -41,12 +41,9 @@ fn toml_check() -> Result<(), Box<dyn std::error::Error>> {
                 dir_entry.path(),
                 text_line_no(&contents, fence.range().start),
             );
-            assert!(crate::cross_toml::CrossToml::parse_from_cross(
-                fence.as_str(),
-                crate::shell::MessageInfo::default()
-            )?
-            .1
-            .is_empty());
+            assert!(crate::cross_toml::CrossToml::parse(fence.as_str())?
+                .1
+                .is_empty());
         }
     }
     Ok(())