Repository: cross-rs/cross
Pull request #837 (2022-06-22 21:41:30)

check tag with package version early and determine if it's the latest version


Patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index cb48eb5..19aa17d 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 }}
@@ -82,7 +82,16 @@ 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:
@@ -182,15 +191,16 @@ 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 }
             - { target: x86_64-unknown-netbsd,            os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
-            - { target: sparcv9-sun-solaris,              os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
-            - { target: x86_64-sun-solaris,               os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
+            # Disabled, see #534
+            #-{ target: sparcv9-sun-solaris,              os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
+            #-{ target: x86_64-sun-solaris,               os: ubuntu-latest,  cpp: 1, dylib: 1, std: 1 }
             - { target: thumbv6m-none-eabi,               os: ubuntu-latest,                    std: 1 }
             - { target: thumbv7em-none-eabi,              os: ubuntu-latest,                    std: 1 }
             - { target: thumbv7em-none-eabihf,            os: ubuntu-latest,                    std: 1 }
@@ -199,7 +209,7 @@ jobs:
   build:
     name: target (${{ matrix.pretty }},${{ matrix.os }})
     runs-on: ${{ matrix.os }}
-    needs: [shellcheck, test, generate-matrix]
+    needs: [shellcheck, test, generate-matrix, check]
     if: github.event_name == 'push'
     strategy:
       fail-fast: false
@@ -249,6 +259,7 @@ 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
@@ -295,10 +306,11 @@ 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]
+    needs: [build, check]
     runs-on: ubuntu-latest
     steps:
       - uses: actions/checkout@v3
@@ -309,7 +321,7 @@ jobs:
           github-token: ${{ secrets.GITHUB_TOKEN }}
 
   conclusion:
-    needs: [shellcheck, fmt, clippy, test, generate-matrix, build, publish]
+    needs: [shellcheck, fmt, clippy, test, generate-matrix, build, publish, check]
     if: always()
     runs-on: ubuntu-latest
     steps:
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 d36c8af..c6c1279 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,13 +12,11 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 - #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`.
@@ -33,7 +31,6 @@ This project adheres to [Semantic Versioning](http://semver.org/).
 
 ### Changed
 
-- #838 - re-enabled the solaris targets.
 - #807 - update Qemu to 6.1.0 on images using Ubuntu 18.04+ with python3.6+.
 - #775 - forward Cargo exit code to host
 - #762 - re-enabled `x86_64-unknown-dragonfly` target.
diff --git a/Cargo.lock b/Cargo.lock
index 0d10599..3bdc619 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -156,8 +156,6 @@ dependencies = [
  "clap",
  "color-eyre",
  "const-sha1",
- "ctrlc",
- "directories",
  "dunce",
  "eyre",
  "home",
@@ -171,7 +169,6 @@ dependencies = [
  "serde_json",
  "shell-escape",
  "shell-words",
- "tempfile",
  "thiserror",
  "toml",
  "walkdir",
@@ -179,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"
@@ -231,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"
@@ -303,15 +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 = "itoa"
 version = "1.0.2"
@@ -437,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"
@@ -474,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"
@@ -609,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"
@@ -747,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"
@@ -804,6 +693,7 @@ dependencies = [
  "cross",
  "eyre",
  "once_cell",
+ "semver",
  "serde",
  "serde_json",
  "serde_yaml",
diff --git a/Cargo.toml b/Cargo.toml
index a24c533..d260c51 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -43,10 +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"
 
 [target.'cfg(not(windows))'.dependencies]
 nix = { version = "0.24", default-features = false, features = ["user"] }
@@ -98,8 +94,3 @@ file = "CHANGELOG.md"
 search = "<!-- next-url -->"
 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 }.{ archive-format }"
-bin-dir = "{ bin }{ binary-ext }"
-pkg-fmt = "tgz"
diff --git a/README.md b/README.md
index dd90ea7..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.10 | 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]       | 1.2.2  | 3.1.10  | ✓   | 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/docker/Dockerfile.wasm32-unknown-emscripten b/docker/Dockerfile.wasm32-unknown-emscripten
index 962f48c..8b458cb 100644
--- a/docker/Dockerfile.wasm32-unknown-emscripten
+++ b/docker/Dockerfile.wasm32-unknown-emscripten
@@ -16,5 +16,4 @@ RUN apt-get update && apt-get install --assume-yes --no-install-recommends \
   python
 
 ENV CARGO_TARGET_WASM32_UNKNOWN_EMSCRIPTEN_RUNNER=node \
-  BINDGEN_EXTRA_CLANG_ARGS_wasm32_unknown_emscripten="--sysroot=/emsdk/upstream/emscripten/cache/sysroot" \
-  EMCC_CFLAGS="-s ERROR_ON_UNDEFINED_SYMBOLS=0 --no-entry ${EMCC_CFLAGS}"
+  BINDGEN_EXTRA_CLANG_ARGS_wasm32_unknown_emscripten="--sysroot=/emsdk/upstream/emscripten/cache/sysroot"
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/docker/solaris.sh b/docker/solaris.sh
index 7f88ec3..e3c10d9 100755
--- a/docker/solaris.sh
+++ b/docker/solaris.sh
@@ -10,7 +10,7 @@ main() {
     local arch="${1}"
 
     local binutils=2.28.1 \
-          gcc=8.4.0 \
+          gcc=6.4.0 \
           target="${arch}-sun-solaris2.10"
 
     install_packages bzip2 \
@@ -58,21 +58,18 @@ main() {
     add-apt-repository -y 'deb http://apt.dilos.org/dilos dilos2-testing main'
     dpkg --add-architecture "${apt_arch}"
     apt-get update
-    # shellcheck disable=SC2046
-    apt-get download $(apt-cache depends --recurse --no-replaces \
-      "libc:${apt_arch}"            \
-      "liblgrp-dev:${apt_arch}"     \
-      "liblgrp:${apt_arch}"         \
-      "libm-dev:${apt_arch}"        \
-      "libpthread:${apt_arch}"      \
-      "libresolv:${apt_arch}"       \
-      "librt:${apt_arch}"           \
-      "libsendfile-dev:${apt_arch}" \
-      "libsendfile:${apt_arch}"     \
-      "libsocket:${apt_arch}"       \
-      "system-crt:${apt_arch}"      \
-      "system-header:${apt_arch}"   \
-      | grep "^\w")
+    apt-cache depends --recurse --no-replaces \
+      "libc:${apt_arch}"           \
+      "libdl-dev:${apt_arch}"      \
+      "libm-dev:${apt_arch}"       \
+      "libnsl-dev:${apt_arch}"     \
+      "libpthread-dev:${apt_arch}" \
+      "libresolv-dev:${apt_arch}"  \
+      "librt:${apt_arch}"          \
+      "libsocket-dev:${apt_arch}"  \
+      "system-crt:${apt_arch}"     \
+      "system-header:${apt_arch}"  \
+      | grep "^\w" | xargs apt-get download
 
     for deb in *"${apt_arch}.deb"; do
       dpkg -x "${deb}" "${td}/solaris"
@@ -105,8 +102,6 @@ EOF
     ln -s usr/include "${destdir}/sys-include"
     ln -s usr/include "${destdir}/include"
 
-    # note: solaris2.10 is obsolete, so we can't upgrade to GCC 10 till then.
-    # for gcc 9.4.0, need `--enable-obsolete`
     cd gcc-build
     ../gcc/configure \
         --disable-libada \
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 fec49a6..0000000
--- a/src/bin/commands/clean.rs
+++ /dev/null
@@ -1,77 +0,0 @@
-use std::fs;
-
-use super::containers::*;
-use super::images::*;
-use clap::Args;
-
-#[derive(Args, Debug)]
-pub struct Clean {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// 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 tempdir = cross::temp::dir()?;
-        match self.execute {
-            true => {
-                if tempdir.exists() {
-                    fs::remove_dir_all(tempdir)?;
-                }
-            }
-            false => println!(
-                "fs::remove_dir_all({})",
-                cross::pretty_path(&tempdir, |_| false)
-            ),
-        }
-
-        // containers -> images -> volumes -> prune to ensure no conflicts.
-        let remove_containers = RemoveAllContainers {
-            verbose: self.verbose,
-            force: self.force,
-            execute: self.execute,
-            engine: None,
-        };
-        remove_containers.run(engine.clone())?;
-
-        let remove_images = RemoveImages {
-            targets: vec![],
-            verbose: self.verbose,
-            force: self.force,
-            local: self.local,
-            execute: self.execute,
-            engine: None,
-        };
-        remove_images.run(engine.clone())?;
-
-        let remove_volumes = RemoveAllVolumes {
-            verbose: self.verbose,
-            force: self.force,
-            execute: self.execute,
-            engine: None,
-        };
-        remove_volumes.run(engine.clone())?;
-
-        let prune_volumes = PruneVolumes {
-            verbose: self.verbose,
-            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 fefda50..0000000
--- a/src/bin/commands/containers.rs
+++ /dev/null
@@ -1,473 +0,0 @@
-use atty::Stream;
-use clap::{Args, Subcommand};
-use cross::{docker, CommandExt};
-
-#[derive(Args, Debug)]
-pub struct ListVolumes {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// 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,
-    /// 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,
-    /// 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,
-    /// 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,
-    /// 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,
-        }
-    }
-}
-
-#[derive(Args, Debug)]
-pub struct ListContainers {
-    /// Provide verbose diagnostic output.
-    #[clap(short, long)]
-    pub verbose: bool,
-    /// 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,
-    /// 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,
-        }
-    }
-}
-
-fn get_cross_volumes(engine: &docker::Engine, verbose: bool) -> 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(verbose)?;
-
-    let mut volumes: Vec<String> = stdout.lines().map(|s| s.to_string()).collect();
-    volumes.sort();
-
-    Ok(volumes)
-}
-
-pub fn list_volumes(
-    ListVolumes { verbose, .. }: ListVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    get_cross_volumes(engine, verbose)?
-        .iter()
-        .for_each(|line| println!("{}", line));
-
-    Ok(())
-}
-
-pub fn remove_all_volumes(
-    RemoveAllVolumes {
-        verbose,
-        force,
-        execute,
-        ..
-    }: RemoveAllVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let volumes = get_cross_volumes(engine, verbose)?;
-
-    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(verbose, false).map_err(Into::into)
-    } else {
-        eprintln!("Note: this is a dry run. to remove the volumes, pass the `--execute` flag.");
-        command.print_verbose(true);
-        Ok(())
-    }
-}
-
-pub fn prune_volumes(
-    PruneVolumes {
-        verbose, execute, ..
-    }: PruneVolumes,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let mut command = docker::subcommand(engine, "volume");
-    command.args(&["prune", "--force"]);
-    if execute {
-        command.run(verbose, false).map_err(Into::into)
-    } else {
-        eprintln!("Note: this is a dry run. to prune the volumes, pass the `--execute` flag.");
-        command.print_verbose(true);
-        Ok(())
-    }
-}
-
-pub fn create_persistent_volume(
-    CreateVolume {
-        docker_in_docker,
-        copy_registry,
-        verbose,
-        ..
-    }: CreateVolume,
-    engine: &docker::Engine,
-    channel: Option<&str>,
-) -> cross::Result<()> {
-    // 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, verbose)?;
-    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, verbose)? {
-        eyre::bail!("Error: volume {volume} already exists.");
-    }
-
-    docker::subcommand(engine, "volume")
-        .args(&["create", &volume])
-        .run_and_get_status(verbose, false)?;
-
-    // stop the container if it's already running
-    let state = docker::remote::container_state(engine, &container, verbose)?;
-    if !state.is_stopped() {
-        eprintln!("Warning: container {container} was running.");
-        docker::remote::container_stop(engine, &container, verbose)?;
-    }
-    if state.exists() {
-        eprintln!("Warning: container {container} was exited.");
-        docker::remote::container_rm(engine, &container, verbose)?;
-    }
-
-    // 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 atty::is(Stream::Stdin) && atty::is(Stream::Stdout) && atty::is(Stream::Stderr) {
-        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(verbose, false)?;
-
-    docker::remote::copy_volume_container_xargo(
-        engine,
-        &container,
-        &dirs.xargo,
-        &target,
-        mount_prefix.as_ref(),
-        verbose,
-    )?;
-    docker::remote::copy_volume_container_cargo(
-        engine,
-        &container,
-        &dirs.cargo,
-        mount_prefix.as_ref(),
-        copy_registry,
-        verbose,
-    )?;
-    docker::remote::copy_volume_container_rust(
-        engine,
-        &container,
-        &dirs.sysroot,
-        &target,
-        mount_prefix.as_ref(),
-        true,
-        verbose,
-    )?;
-
-    docker::remote::container_stop(engine, &container, verbose)?;
-    docker::remote::container_rm(engine, &container, verbose)?;
-
-    Ok(())
-}
-
-pub fn remove_persistent_volume(
-    RemoveVolume {
-        target,
-        docker_in_docker,
-        verbose,
-        ..
-    }: RemoveVolume,
-    engine: &docker::Engine,
-    channel: Option<&str>,
-) -> cross::Result<()> {
-    let (_, _, dirs) =
-        docker::get_package_info(engine, &target, channel, docker_in_docker, verbose)?;
-    let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
-
-    if !docker::remote::volume_exists(engine, &volume, verbose)? {
-        eyre::bail!("Error: volume {volume} does not exist.");
-    }
-
-    docker::remote::volume_rm(engine, &volume, verbose)?;
-
-    Ok(())
-}
-
-fn get_cross_containers(engine: &docker::Engine, verbose: bool) -> 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(verbose)?;
-
-    let mut containers: Vec<String> = stdout.lines().map(|s| s.to_string()).collect();
-    containers.sort();
-
-    Ok(containers)
-}
-
-pub fn list_containers(
-    ListContainers { verbose, .. }: ListContainers,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    get_cross_containers(engine, verbose)?
-        .iter()
-        .for_each(|line| println!("{}", line));
-
-    Ok(())
-}
-
-pub fn remove_all_containers(
-    RemoveAllContainers {
-        verbose,
-        force,
-        execute,
-        ..
-    }: RemoveAllContainers,
-    engine: &docker::Engine,
-) -> cross::Result<()> {
-    let containers = get_cross_containers(engine, verbose)?;
-    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(verbose, false)?;
-        }
-    } else {
-        eprintln!("Note: this is a dry run. to remove the containers, pass the `--execute` flag.");
-        for command in commands {
-            command.print_verbose(true);
-        }
-    }
-
-    Ok(())
-}
diff --git a/src/bin/commands/images.rs b/src/bin/commands/images.rs
index e361c01..860f0e6 100644
--- a/src/bin/commands/images.rs
+++ b/src/bin/commands/images.rs
@@ -1,14 +1,9 @@
-use std::collections::{BTreeMap, BTreeSet};
-
 use clap::{Args, Subcommand};
-use cross::{
-    docker::{self, CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX},
-    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];
@@ -21,12 +16,10 @@ pub struct ListImages {
     /// 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)
     }
 }
@@ -53,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 {
@@ -71,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),
@@ -101,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)
@@ -137,35 +120,24 @@ fn is_local_image(tag: &str) -> bool {
 }
 
 fn get_cross_images(
-    engine: &docker::Engine,
+    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(verbose)?
+    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(verbose)?;
-    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:
@@ -188,138 +160,47 @@ fn rustembedded_target(tag: &str) -> String {
     components.join("-")
 }
 
-fn get_image_target(
-    engine: &cross::docker::Engine,
-    image: &Image,
-    target_list: &TargetList,
-) -> 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);
-
-    // TODO: verbosity = 3?
-    let target = command.run_and_get_stdout(true)?;
-    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, ..
-    }: ListImages,
-    engine: &docker::Engine,
+    ListImages { verbose, .. }: ListImages,
+    engine: &cross::docker::Engine,
 ) -> cross::Result<()> {
-    let cross_images = get_cross_images(engine, verbose, true)?;
-    let target_list = cross::rustc::target_list(false)?;
-    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)?;
-        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| {
-        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('|');
-        println!("{}", row);
-    };
-
-    if targets.len() != 1 {
-        print_string("Targets", "Images", ' ');
-        print_string("-------", "------", '-');
-    }
-
-    let print_single = |_: &str, image: &Image| println!("{}", image);
-    let print_table = |target: &str, image: &Image| {
-        let name = image.name();
-        print_string(target, &name, ' ');
-    };
-
-    keys.iter().for_each(|&target| {
-        map.get(target)
-            .expect("map must have key")
-            .iter()
-            .for_each(|image| {
-                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],
+    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(verbose, false).map_err(Into::into)
     } else {
-        eprintln!("Note: this is a dry run. to remove the images, pass the `--execute` flag.");
+        eprintln!("note: this is a dry run. to remove the images, pass the `--execute` flag.");
         command.print_verbose(true);
         Ok(())
     }
@@ -333,10 +214,11 @@ pub fn remove_all_images(
         execute,
         ..
     }: RemoveImages,
-    engine: &docker::Engine,
+    engine: &cross::docker::Engine,
 ) -> cross::Result<()> {
     let images = get_cross_images(engine, verbose, local)?;
-    remove_images(engine, &images, verbose, force, execute)
+    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(
@@ -348,18 +230,17 @@ pub fn remove_target_images(
         execute,
         ..
     }: RemoveImages,
-    engine: &docker::Engine,
+    engine: &cross::docker::Engine,
 ) -> cross::Result<()> {
-    let cross_images = get_cross_images(engine, verbose, local)?;
-    let target_list = cross::rustc::target_list(false)?;
-    let mut images = vec![];
-    for image in cross_images {
-        let target = dbg!(get_image_target(engine, &image, &target_list)?);
+    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, verbose, 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 5f3919f..b5746fc 100644
--- a/src/bin/cross-util.rs
+++ b/src/bin/cross-util.rs
@@ -1,7 +1,6 @@
 #![deny(missing_debug_implementations, rust_2018_idioms)]
 
 use clap::{CommandFactory, Parser, Subcommand};
-use cross::docker;
 
 mod commands;
 
@@ -24,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> {
@@ -46,13 +37,16 @@ fn is_toolchain(toolchain: &str) -> cross::Result<String> {
     }
 }
 
-fn get_container_engine(engine: Option<&str>, verbose: bool) -> cross::Result<docker::Engine> {
+fn get_container_engine(
+    engine: Option<&str>,
+    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, verbose)
+    cross::docker::Engine::from_path(engine, verbose)
 }
 
 pub fn main() -> cross::Result<()> {
@@ -63,18 +57,6 @@ pub fn main() -> cross::Result<()> {
             let engine = get_container_engine(args.engine(), args.verbose())?;
             args.run(engine)?;
         }
-        Commands::Volumes(args) => {
-            let engine = get_container_engine(args.engine(), args.verbose())?;
-            args.run(engine, cli.toolchain.as_deref())?;
-        }
-        Commands::Containers(args) => {
-            let engine = get_container_engine(args.engine(), args.verbose())?;
-            args.run(engine)?;
-        }
-        Commands::Clean(args) => {
-            let engine = get_container_engine(args.engine.as_deref(), args.verbose)?;
-            args.run(engine)?;
-        }
     }
 
     Ok(())
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 8001f34..860c4be 100644
--- a/src/cargo.rs
+++ b/src/cargo.rs
@@ -19,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 {
@@ -49,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,
diff --git a/src/config.rs b/src/config.rs
index bcb3d49..dcc39ed 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -431,9 +431,7 @@ mod tests {
         use std::matches;
 
         fn toml(content: &str) -> Result<crate::CrossToml> {
-            Ok(CrossToml::parse_from_cross(content)
-                .wrap_err("couldn't parse toml")?
-                .0)
+            Ok(CrossToml::parse(content).wrap_err("couldn't parse toml")?.0)
         }
 
         #[test]
diff --git a/src/cross_toml.rs b/src/cross_toml.rs
index eec270c..2d07300 100644
--- a/src/cross_toml.rs
+++ b/src/cross_toml.rs
@@ -2,20 +2,19 @@
 
 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)]
@@ -29,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>,
@@ -44,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,
@@ -65,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>,
@@ -74,47 +73,13 @@ pub struct CrossToml {
 }
 
 impl CrossToml {
-    /// Parses the [`CrossToml`] from all of the config sources
-    pub fn parse(cargo_toml: &str, cross_toml: &str) -> Result<(Self, BTreeSet<String>)> {
-        let (cross_toml, mut unused) = Self::parse_from_cross(cross_toml)?;
-
-        if let Some((cargo_toml, u_cargo)) = Self::parse_from_cargo(cargo_toml)? {
-            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) -> Result<(Self, BTreeSet<String>)> {
-        let mut tomld = toml::Deserializer::new(toml_str);
-        Self::parse_from_deserializer(&mut tomld)
-    }
-
-    /// Parses the [`CrossToml`] from a string containing the Cargo.toml contents
-    pub fn parse_from_cargo(cargo_toml_str: &str) -> 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())?))
-        } 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) -> 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());
         })?;
 
@@ -128,72 +93,6 @@ impl CrossToml {
         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())
@@ -359,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))
@@ -388,7 +273,7 @@ mod tests {
             targets: HashMap::new(),
             build: CrossBuildConfig::default(),
         };
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross("")?;
+        let (parsed_cfg, unused) = CrossToml::parse("")?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -422,7 +307,7 @@ mod tests {
           volumes = ["VOL1_ARG", "VOL2_ARG"]
           passthrough = ["VAR1", "VAR2"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?;
+        let (parsed_cfg, unused) = CrossToml::parse(test_str)?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -466,7 +351,7 @@ mod tests {
             image = "test-image"
             pre-build = []
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?;
+        let (parsed_cfg, unused) = CrossToml::parse(test_str)?;
 
         assert_eq!(parsed_cfg, cfg);
         assert!(unused.is_empty());
@@ -530,176 +415,11 @@ mod tests {
             [target.aarch64-unknown-linux-gnu.env]
             volumes = ["VOL"]
         "#;
-        let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str)?;
+        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)?;
-        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)? {
-            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)?;
-        let (cfg2, _) = CrossToml::parse_from_cross(cfg2_str)?;
-        let (cfg_expected, _) = CrossToml::parse_from_cross(cfg_expected_str)?;
-
-        // Merges config and compares
-        let cfg_merged = cfg1.merge(cfg2)?;
-        assert_eq!(cfg_expected, cfg_merged);
-
-        Ok(())
-    }
 }
diff --git a/src/docker/custom.rs b/src/docker/custom.rs
index 5e4b32d..6bdafed 100644
--- a/src/docker/custom.rs
+++ b/src/docker/custom.rs
@@ -5,9 +5,7 @@ use crate::docker::Engine;
 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> {
@@ -53,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() {
@@ -104,26 +102,32 @@ impl<'a> Dockerfile<'a> {
         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 7c1f692..aa57710 100644
--- a/src/docker/engine.rs
+++ b/src/docker/engine.rs
@@ -2,7 +2,6 @@ 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;
 
@@ -21,35 +20,19 @@ 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>, verbose: bool) -> 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, verbose)
+        Self::from_path(path, verbose)
     }
 
-    pub fn from_path(path: PathBuf, is_remote: Option<bool>, verbose: bool) -> Result<Engine> {
+    pub fn from_path(path: PathBuf, verbose: bool) -> Result<Engine> {
         let kind = get_engine_type(&path, verbose)?;
-        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()
+        Ok(Engine { path, kind })
     }
 }
 
diff --git a/src/docker/local.rs b/src/docker/local.rs
index 01c3240..d65987f 100644
--- a/src/docker/local.rs
+++ b/src/docker/local.rs
@@ -6,14 +6,13 @@ use super::shared::*;
 use crate::cargo::CargoMetadata;
 use crate::errors::Result;
 use crate::extensions::CommandExt;
-use crate::file::{PathExt, ToUtf8};
+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,
@@ -24,12 +23,13 @@ pub(crate) fn run(
     docker_in_docker: bool,
     cwd: &Path,
 ) -> Result<ExitStatus> {
-    let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker, verbose)?;
+    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)?;
 
@@ -76,7 +76,7 @@ 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()?),
         ]);
     }
 
@@ -88,7 +88,7 @@ pub(crate) fn run(
     }
     let mut image = image_name(config, target)?;
     if needs_custom_image(target, config) {
-        image = custom_image_build(target, config, metadata, dirs, engine, verbose)
+        image = custom_image_build(target, config, metadata, dirs, &engine, verbose)
             .wrap_err("when building custom image")?
     }
 
diff --git a/src/docker/mod.rs b/src/docker/mod.rs
index f55a978..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::*;
@@ -16,7 +15,6 @@ use crate::{Config, Target};
 
 #[allow(clippy::too_many_arguments)] // TODO: refactor
 pub fn run(
-    engine: &Engine,
     target: &Target,
     args: &[String],
     metadata: &CargoMetadata,
@@ -27,31 +25,15 @@ pub fn run(
     docker_in_docker: bool,
     cwd: &Path,
 ) -> Result<ExitStatus> {
-    if engine.is_remote {
-        remote::run(
-            engine,
-            target,
-            args,
-            metadata,
-            config,
-            uses_xargo,
-            sysroot,
-            verbose,
-            docker_in_docker,
-            cwd,
-        )
-    } else {
-        local::run(
-            engine,
-            target,
-            args,
-            metadata,
-            config,
-            uses_xargo,
-            sysroot,
-            verbose,
-            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 afd1eb9..0000000
--- a/src/docker/remote.rs
+++ /dev/null
@@ -1,993 +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::temp;
-use crate::{Host, Target};
-use atty::Stream;
-
-// the mount directory for the data volume.
-pub const MOUNT_PREFIX: &str = "/cross";
-
-struct DeleteVolume<'a>(&'a Engine, &'a VolumeId, bool);
-
-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, bool);
-
-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, verbose: bool) -> Result<Self> {
-        if volume_exists(engine, toolchain, verbose)? {
-            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,
-    verbose: bool,
-) -> 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(verbose, 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,
-    verbose: bool,
-) -> Result<ExitStatus> {
-    subcommand(engine, "cp")
-        .arg("-a")
-        .arg(src.to_utf8()?)
-        .arg(format!("{container}:{}", dst.as_posix()?))
-        .run_and_get_status(verbose, 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,
-    verbose: bool,
-) -> Result<bool> {
-    Ok(subcommand(engine, "exec")
-        .arg(container)
-        .args(&["bash", "-c", &format!("[[ -d '{}' ]]", path.as_posix()?)])
-        .run_and_get_status(verbose, 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,
-    verbose: bool,
-) -> 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, verbose)
-}
-
-pub fn copy_volume_container_xargo(
-    engine: &Engine,
-    container: &str,
-    xargo_dir: &Path,
-    target: &Target,
-    mount_prefix: &Path,
-    verbose: bool,
-) -> 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(), verbose)?;
-        copy_volume_files(engine, container, &src, &dst, verbose)?;
-    }
-
-    Ok(())
-}
-
-pub fn copy_volume_container_cargo(
-    engine: &Engine,
-    container: &str,
-    cargo_dir: &Path,
-    mount_prefix: &Path,
-    copy_registry: bool,
-    verbose: bool,
-) -> 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, verbose)?;
-    } else {
-        // can copy a limit subset of files: the rest is present.
-        create_volume_dir(engine, container, &dst, verbose)?;
-        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, verbose)?;
-            }
-        }
-    }
-
-    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,
-    verbose: bool,
-) -> 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), verbose)?;
-    for basename in ["bin", "libexec", "etc"] {
-        let file = sysroot.join(basename);
-        copy_volume_files(engine, container, &file, &dst, verbose)?;
-    }
-
-    // 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, verbose)?;
-
-    Ok(())
-}
-
-fn copy_volume_container_rust_manifest(
-    engine: &Engine,
-    container: &str,
-    sysroot: &Path,
-    mount_prefix: &Path,
-    verbose: bool,
-) -> 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, verbose)?;
-
-    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,
-    verbose: bool,
-) -> 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, verbose)?;
-    }
-    if !skip {
-        copy_volume_files(engine, container, &src_toolchain, &dst_rustlib, verbose)?;
-    }
-    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, verbose)?;
-    }
-
-    Ok(())
-}
-
-pub fn copy_volume_container_rust(
-    engine: &Engine,
-    container: &str,
-    sysroot: &Path,
-    target: &Target,
-    mount_prefix: &Path,
-    skip_target: bool,
-    verbose: bool,
-) -> Result<()> {
-    let target_triple = target.triple();
-    let image_triple = Host::X86_64UnknownLinuxGnu.triple();
-
-    copy_volume_container_rust_base(engine, container, sysroot, mount_prefix, verbose)?;
-    copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, verbose)?;
-    copy_volume_container_rust_triple(
-        engine,
-        container,
-        sysroot,
-        image_triple,
-        mount_prefix,
-        false,
-        verbose,
-    )?;
-    if !skip_target && target_triple != image_triple {
-        copy_volume_container_rust_triple(
-            engine,
-            container,
-            sysroot,
-            target_triple,
-            mount_prefix,
-            false,
-            verbose,
-        )?;
-    }
-
-    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],
-    verbose: bool,
-) -> 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, verbose)
-}
-
-// 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],
-    verbose: bool,
-) -> Result<ExitStatus> {
-    const PATH: &str = "/tmp/remove_list";
-    let mut script = vec![];
-    if 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(verbose, true)?;
-
-    subcommand(engine, "exec")
-        .arg(container)
-        .args(&["sh", "-c", &script.join("\n")])
-        .run_and_get_status(verbose, true)
-        .map_err(Into::into)
-}
-
-fn copy_volume_container_project(
-    engine: &Engine,
-    container: &str,
-    src: &Path,
-    dst: &Path,
-    volume: &VolumeId,
-    copy_cache: bool,
-    verbose: bool,
-) -> Result<()> {
-    let copy_all = || {
-        if copy_cache {
-            copy_volume_files(engine, container, src, dst, verbose)
-        } else {
-            copy_volume_files_nocache(engine, container, src, dst, verbose)
-        }
-    };
-    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, verbose)?;
-                }
-                if !removed.is_empty() {
-                    remove_volume_file_list(engine, container, dst, &removed, verbose)?;
-                }
-            } else {
-                write_project_fingerprint(&fingerprint, &current)?;
-                copy_all()?;
-            }
-        }
-        VolumeId::Discard(_) => {
-            copy_all()?;
-        }
-    }
-
-    Ok(())
-}
-
-fn run_and_get_status(engine: &Engine, args: &[&str], verbose: bool) -> Result<ExitStatus> {
-    command(engine)
-        .args(args)
-        .run_and_get_status(verbose, true)
-        .map_err(Into::into)
-}
-
-pub fn volume_create(engine: &Engine, volume: &str, verbose: bool) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["volume", "create", volume], verbose)
-}
-
-pub fn volume_rm(engine: &Engine, volume: &str, verbose: bool) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["volume", "rm", volume], verbose)
-}
-
-pub fn volume_exists(engine: &Engine, volume: &str, verbose: bool) -> Result<bool> {
-    command(engine)
-        .args(&["volume", "inspect", volume])
-        .run_and_get_output(verbose)
-        .map(|output| output.status.success())
-        .map_err(Into::into)
-}
-
-pub fn container_stop(engine: &Engine, container: &str, verbose: bool) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["stop", container], verbose)
-}
-
-pub fn container_rm(engine: &Engine, container: &str, verbose: bool) -> Result<ExitStatus> {
-    run_and_get_status(engine, &["rm", container], verbose)
-}
-
-pub fn container_state(engine: &Engine, container: &str, verbose: bool) -> Result<ContainerState> {
-    let stdout = command(engine)
-        .args(&["ps", "-a"])
-        .args(&["--filter", &format!("name={container}")])
-        .args(&["--format", "{{.State}}"])
-        .run_and_get_stdout(verbose)?;
-    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, verbose: bool) -> Result<PathBuf> {
-    let host_path = file::canonicalize(val)?;
-    canonicalize_mount_path(&host_path, verbose)
-}
-
-#[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,
-    verbose: bool,
-    docker_in_docker: bool,
-    cwd: &Path,
-) -> Result<ExitStatus> {
-    let dirs = Directories::create(engine, metadata, cwd, sysroot, docker_in_docker, verbose)?;
-
-    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, verbose)?;
-    let state = container_state(engine, &container, verbose)?;
-    if !state.is_stopped() {
-        eprintln!("Warning: container {container} was running.");
-        container_stop(engine, &container, verbose)?;
-    }
-    if state.exists() {
-        eprintln!("Warning: container {container} was exited.");
-        container_rm(engine, &container, verbose)?;
-    }
-    if let VolumeId::Discard(ref id) = volume {
-        if volume_exists(engine, id, verbose)? {
-            eprintln!("Warning: temporary volume {container} existed.");
-            volume_rm(engine, id, verbose)?;
-        }
-    }
-
-    // 2. create our volume to copy all our data over to
-    if let VolumeId::Discard(ref id) = volume {
-        volume_create(engine, id, verbose)?;
-    }
-    let _volume_deletter = DeleteVolume(engine, &volume, verbose);
-
-    // 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)?;
-
-    let mut volumes = vec![];
-    let mount_volumes = docker_mount(
-        &mut docker,
-        metadata,
-        config,
-        target,
-        cwd,
-        verbose,
-        |_, val, verbose| mount_path(val, verbose),
-        |(src, dst)| volumes.push((src, dst)),
-    )?;
-
-    docker_seccomp(&mut docker, engine.kind, target, metadata, verbose)?;
-
-    // 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 {
-        volumes.push((nix_store.to_utf8()?.to_string(), nix_store.to_path_buf()))
-    }
-
-    docker.arg("-d");
-    if atty::is(Stream::Stdin) && atty::is(Stream::Stdout) && atty::is(Stream::Stderr) {
-        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(verbose, true)?;
-    let _container_deletter = DeleteContainer(engine, &container, verbose);
-
-    // 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, verbose)
-        } else {
-            copy_volume_files_nocache(engine, &container, src, dst, verbose)
-        }
-    };
-    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,
-            verbose,
-        )?;
-        copy_volume_container_cargo(
-            engine,
-            &container,
-            &dirs.cargo,
-            mount_prefix_path,
-            false,
-            verbose,
-        )?;
-        copy_volume_container_rust(
-            engine,
-            &container,
-            &dirs.sysroot,
-            target,
-            mount_prefix_path,
-            false,
-            verbose,
-        )?;
-    } 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,
-            verbose,
-        )?;
-    }
-    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 != PathBuf::new() {
-            create_volume_dir(engine, &container, mount_root.parent().unwrap(), verbose)?;
-        }
-        mount_root
-    } else {
-        mount_prefix_path.join("project")
-    };
-    copy_volume_container_project(
-        engine,
-        &container,
-        &dirs.host_root,
-        &mount_root,
-        &volume,
-        copy_cache,
-        verbose,
-    )?;
-
-    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, verbose)?;
-        }
-
-        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.as_posix()?));
-        } else {
-            let rel_dst = dst.strip_prefix("/").unwrap();
-            let mount_dst = mount_prefix_path.join(rel_dst);
-            if rel_dst != PathBuf::new() {
-                create_volume_dir(engine, &container, mount_dst.parent().unwrap(), verbose)?;
-            }
-            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 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(verbose, 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(verbose, 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, verbose)? {
-        subcommand(engine, "cp")
-            .arg("-a")
-            .arg(&format!("{container}:{}", target_dir.as_posix()?))
-            .arg(&dirs.target.parent().unwrap())
-            .run_and_get_status(verbose, false)
-            .map_err::<eyre::ErrReport, _>(Into::into)?;
-    }
-
-    status
-}
diff --git a/src/docker/shared.rs b/src/docker/shared.rs
index 8c9a0d3..ac3b51a 100644
--- a/src/docker/shared.rs
+++ b/src/docker/shared.rs
@@ -5,20 +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::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:
@@ -34,7 +29,6 @@ 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: PathBuf,
     pub mount_cwd: PathBuf,
     pub sysroot: PathBuf,
@@ -136,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 {
@@ -150,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,
-    verbose: bool,
-) -> Result<(Target, CargoMetadata, Directories)> {
-    let target_list = rustc::target_list(false)?;
-    let target = Target::from(target, &target_list);
-    let metadata = cargo_metadata_with_args(None, None, verbose)?
-        .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, verbose)?.1;
-    let dirs = Directories::create(engine, &metadata, &cwd, &sysroot, docker_in_docker, verbose)?;
-
-    Ok((target, metadata, dirs))
-}
-
 /// Register binfmt interpreters
-pub(crate) fn register(engine: &Engine, target: &Target, verbose: bool) -> 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 && \
@@ -181,11 +150,12 @@ pub(crate) fn register(engine: &Engine, target: &Target, verbose: bool) -> Resul
             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(verbose, false)
         .map_err(Into::into)
@@ -487,7 +457,7 @@ pub(crate) fn custom_image_build(
                 )
                 .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)
@@ -611,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/file.rs b/src/file.rs
index b6e0df6..7d369f3 100644
--- a/src/file.rs
+++ b/src/file.rs
@@ -75,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> {
diff --git a/src/lib.rs b/src/lib.rs
index 08f2fe3..55746e6 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -27,9 +27,8 @@ mod extensions;
 mod file;
 mod id;
 mod interpreter;
-pub mod rustc;
+mod rustc;
 mod rustup;
-pub mod temp;
 
 use std::env;
 use std::io::{self, Write};
@@ -38,16 +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::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";
 
@@ -116,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",
@@ -146,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 },
@@ -315,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 {
@@ -339,22 +338,13 @@ impl From<Host> for Target {
     }
 }
 
-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(false)?;
     let args = cli::parse(&target_list)?;
@@ -371,7 +361,8 @@ pub fn run() -> Result<ExitStatus> {
         .iter()
         .any(|a| a == "--verbose" || a == "-v" || a == "-vv");
 
-    let host_version_meta = rustc::version_meta()?;
+    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), verbose)? {
         let host = host_version_meta.host();
@@ -392,8 +383,22 @@ pub fn run() -> Result<ExitStatus> {
         };
 
         if image_exists && host.is_supported(Some(&target)) {
-            let (toolchain, sysroot) =
-                rustc::get_sysroot(&host, &target, args.channel.as_deref(), verbose)?;
+            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(verbose)?;
@@ -487,23 +492,17 @@ 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), verbose)?;
+            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, verbose)?
+                    docker::register(&target, verbose)?
                 }
 
-                let status = docker::run(
-                    &engine,
+                return docker::run(
                     &target,
                     &filtered_args,
                     &metadata,
@@ -513,14 +512,7 @@ pub fn run() -> Result<ExitStatus> {
                     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);
-                }
+                );
             }
         }
     }
@@ -590,44 +582,27 @@ pub(crate) fn warn_host_version_mismatch(
     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.
+/// 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 root = &metadata.workspace_root;
-    let cross_config_path = match env::var("CROSS_CONFIG") {
+    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 path.exists() {
+        let content =
+            file::read(&path).wrap_err_with(|| format!("could not read file `{path:?}`"))?;
 
-    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:?}`"))?;
-
-        let (config, _) = CrossToml::parse(&cargo_toml_str, &cross_toml_str)
-            .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() {
+        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.");
         }
-
-        if let Some((cfg, _)) = CrossToml::parse_from_cargo(&cargo_toml_str)? {
-            Ok(Some(cfg))
-        } else {
-            Ok(None)
-        }
+        Ok(None)
     }
 }
diff --git a/src/rustc.rs b/src/rustc.rs
index 6613559..1cdfaf2 100644
--- a/src/rustc.rs
+++ b/src/rustc.rs
@@ -21,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 {
@@ -32,48 +31,6 @@ 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 {
@@ -103,50 +60,3 @@ pub fn sysroot(host: &Host, target: &Target, verbose: bool) -> Result<PathBuf> {
 
     Ok(PathBuf::from(stdout))
 }
-
-pub fn get_sysroot(
-    host: &Host,
-    target: &Target,
-    channel: Option<&str>,
-    verbose: bool,
-) -> Result<(String, PathBuf)> {
-    let mut sysroot = 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) = 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 ed54412..f4c93a7 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -1,4 +1,4 @@
-use std::path::{Path, PathBuf};
+use std::path::Path;
 use std::process::Command;
 
 use rustc_version::{Channel, Version};
@@ -123,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/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 5539003..8cafced 100644
--- a/xtask/Cargo.toml
+++ b/xtask/Cargo.toml
@@ -19,3 +19,4 @@ 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 cef4e4e..4a58623 100644
--- a/xtask/src/build_docker_image.rs
+++ b/xtask/src/build_docker_image.rs
@@ -1,8 +1,8 @@
-use std::path::Path;
+use std::{path::Path, process::Command};
 
 use clap::Args;
 use color_eyre::Section;
-use cross::{docker, CommandExt, ToUtf8};
+use cross::{CommandExt, ToUtf8};
 use std::fmt::Write;
 
 #[derive(Args, Debug)]
@@ -11,18 +11,20 @@ pub struct BuildDockerImage {
     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)]
     tag: Option<String>,
     /// Repository name for image.
-    #[clap(long, default_value = docker::CROSS_IMAGE)]
+    #[clap(long, default_value = cross::docker::CROSS_IMAGE)]
     repository: String,
     /// Newline separated labels
     #[clap(long, env = "LABELS")]
     labels: Option<String>,
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
+    verbose: bool,
     /// Print but do not execute the build commands.
     #[clap(long)]
     dry_run: bool,
@@ -38,7 +40,7 @@ pub struct BuildDockerImage {
     /// 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"
     )]
     progress: String,
@@ -54,9 +56,6 @@ pub struct BuildDockerImage {
     /// If no target list is provided, parse list from CI.
     #[clap(long)]
     from_ci: bool,
-    /// Additional build arguments to pass to Docker.
-    #[clap(long)]
-    build_arg: Vec<String>,
     /// Targets to build for
     #[clap()]
     targets: Vec<crate::ImageTarget>,
@@ -83,6 +82,7 @@ pub fn build_docker_image(
     BuildDockerImage {
         ref_type,
         ref_name,
+        is_latest,
         tag: tag_override,
         repository,
         labels,
@@ -95,11 +95,10 @@ pub fn build_docker_image(
         no_cache,
         no_fastfail,
         from_ci,
-        build_arg,
         mut targets,
         ..
     }: BuildDockerImage,
-    engine: &docker::Engine,
+    engine: &Path,
 ) -> cross::Result<()> {
     let metadata = cross::cargo_metadata_with_args(
         Some(Path::new(env!("CARGO_MANIFEST_DIR"))),
@@ -148,7 +147,7 @@ pub fn build_docker_image(
         if gha && targets.len() > 1 {
             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);
 
@@ -168,6 +167,7 @@ pub fn build_docker_image(
                 &repository,
                 ref_type,
                 ref_name,
+                is_latest,
                 &version,
             )?),
             _ => {
@@ -224,9 +224,6 @@ 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(".");
 
@@ -283,6 +280,7 @@ 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![];
@@ -296,8 +294,7 @@ pub fn determine_image_name(
             }
             tags.push(target.image_name(repository, version));
             // Check for unstable releases, tag stable releases as `latest`
-            if version.contains('-') {
-                // TODO: Don't tag if version is older than currently released version.
+            if is_latest {
                 tags.push(target.image_name(repository, "latest"))
             }
         }
diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs
index fa8edb1..7eb902c 100644
--- a/xtask/src/ci.rs
+++ b/xtask/src/ci.rs
@@ -1,9 +1,9 @@
-use std::path::Path;
-
 use clap::Subcommand;
+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")]
@@ -13,25 +13,28 @@ 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) -> cross::Result<()> {
+pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> {
+    let cross_meta = metadata
+        .get_package("cross")
+        .expect("cross expected in workspace");
+
     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![];
 
@@ -56,6 +59,7 @@ pub fn ci(args: CiJob) -> cross::Result<()> {
                     cross::docker::CROSS_IMAGE,
                     &ref_type,
                     &ref_name,
+                    false,
                     &version,
                 )?[0],
             );
@@ -64,6 +68,31 @@ pub fn ci(args: CiJob) -> 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(true)?;
+                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(())
 }
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index b593a7e..2214ddf 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -7,9 +7,10 @@ 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 util::ImageTarget;
 
 use self::build_docker_image::BuildDockerImage;
@@ -65,11 +66,11 @@ pub fn main() -> cross::Result<()> {
     let cli = Cli::parse();
     match cli.command {
         Commands::TargetInfo(args) => {
-            let engine = get_container_engine(args.engine.as_deref(), args.verbose)?;
+            let engine = get_container_engine(args.engine.as_deref())?;
             target_info::target_info(args, &engine)?;
         }
         Commands::BuildDockerImage(args) => {
-            let engine = get_container_engine(args.engine.as_deref(), args.verbose)?;
+            let engine = get_container_engine(args.engine.as_deref())?;
             build_docker_image::build_docker_image(args, &engine)?;
         }
         Commands::InstallGitHooks(args) => {
@@ -81,17 +82,24 @@ pub fn main() -> cross::Result<()> {
         Commands::Test(args) => {
             hooks::test(args, cli.toolchain.as_deref())?;
         }
-        Commands::CiJob(args) => ci::ci(args)?,
+        Commands::CiJob(args) => {
+            let metadata = cross::cargo_metadata_with_args(
+                Some(std::path::Path::new(env!("CARGO_MANIFEST_DIR"))),
+                None,
+                true,
+            )?
+            .ok_or_else(|| eyre::eyre!("could not find cross workspace"))?;
+            ci::ci(args, metadata)?
+        }
     }
 
     Ok(())
 }
 
-fn get_container_engine(engine: Option<&str>, verbose: bool) -> 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, verbose)
+        cross::docker::get_container_engine()
+    }
 }
diff --git a/xtask/src/target_info.rs b/xtask/src/target_info.rs
index d5493d0..8ed54d3 100644
--- a/xtask/src/target_info.rs
+++ b/xtask/src/target_info.rs
@@ -1,8 +1,12 @@
-use std::{collections::BTreeMap, process::Stdio};
+use std::{
+    collections::BTreeMap,
+    path::Path,
+    process::{Command, Stdio},
+};
 
 use crate::util::{format_repo, pull_image};
 use clap::Args;
-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.
@@ -14,7 +18,7 @@ pub struct TargetInfo {
     targets: Vec<crate::ImageTarget>,
     /// Provide verbose diagnostic output.
     #[clap(short, long)]
-    pub verbose: bool,
+    verbose: bool,
     /// Image registry.
     #[clap(long, default_value_t = String::from("ghcr.io"))]
     registry: String,
@@ -30,7 +34,7 @@ pub struct TargetInfo {
 }
 
 fn image_info(
-    engine: &docker::Engine,
+    engine: &Path,
     target: &crate::ImageTarget,
     image: &str,
     tag: &str,
@@ -41,7 +45,7 @@ fn image_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)]);
@@ -69,7 +73,7 @@ pub fn target_info(
         tag,
         ..
     }: TargetInfo,
-    engine: &docker::Engine,
+    engine: &Path,
 ) -> cross::Result<()> {
     let matrix = crate::util::get_matrix();
     let test_map: BTreeMap<crate::ImageTarget, bool> = matrix
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 1aa7e67..598b68d 100644
--- a/xtask/src/util.rs
+++ b/xtask/src/util.rs
@@ -1,4 +1,7 @@
-use cross::{docker, CommandExt};
+use std::path::Path;
+use std::process::Command;
+
+use cross::CommandExt;
 use once_cell::sync::OnceCell;
 use serde::Deserialize;
 
@@ -81,8 +84,9 @@ pub fn format_repo(registry: &str, repository: &str) -> String {
     output
 }
 
-pub fn pull_image(engine: &docker::Engine, image: &str, verbose: bool) -> 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(verbose)?;
     command.status_result(verbose, out.status, Some(&out))?;


Test patch:
diff --git a/src/tests/toml.rs b/src/tests/toml.rs
index da5369a..c7479f1 100644
--- a/src/tests/toml.rs
+++ b/src/tests/toml.rs
@@ -41,11 +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())?
-                    .1
-                    .is_empty()
-            );
+            assert!(crate::cross_toml::CrossToml::parse(fence.as_str())?
+                .1
+                .is_empty());
         }
     }
     Ok(())