Repository: cross-rs/cross
Pull request #817 (2022-06-18 03:49:44)

Enable usage of other toolchains in images
Makes the following work

```toml
# Cross.toml
[build]
default-target = "x86_64-unknown-linux-musl"

[target."x86_64-unknown-linux-musl"]
image.name = "alpine:edge"
image.toolchain = ["x86_64-unknown-linux-musl"]
pre-build = ["apk add --no-cache gcc musl-dev"]
```


```rust
// src/main.rs
fn main() {
    println!("Hello, world!");
}
```

```
> cross run
[..snip]
    Finished dev [unoptimized + debuginfo] target(s) in 0.24s
     Running `/target/x86_64-unknown-linux-musl/debug/cross-minimal`
Hello, world!
```

and also 

```toml
[target.x86_64-unknown-linux-gnu]
pre-build = [
    "apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross",
]

[target.x86_64-unknown-linux-gnu.env]
passthrough = [
    "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc",
    "CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc",
    "CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++",
]

[target.x86_64-unknown-linux-gnu.image]
name = "ubuntu:20.04"
toolchain = ["aarch64-unknown-linux-gnu"]
```


```
cross build --target x86_64-unknown-linux-gnu -v
+ cargo metadata --format-version 1 --filter-platform x86_64-unknown-linux-gnu
+ rustc --print sysroot
+ /usr/local/bin/docker
+ /usr/local/bin/docker version '-f "{{ .Server.Os }},{{ .Server.Arch }}"'
+ rustup --verbose toolchain list
+ rustup --verbose target list --toolchain stable-aarch64-unknown-linux-gnu
+ rustup --verbose component list --toolchain stable-aarch64-unknown-linux-gnu
+ "/Users/emil/workspace/cross-tests/basic" /usr/local/bin/docker build --platform linux/arm64 --label 'org.cross-rs.for-cross-target=x86_64-unknown-linux-gnu' --label 'org.cross-rs.runs-with=aarch64-unknown-linux-gnu' --label 'org.cross-rs.workspace_root=/Users/emil/workspace/cross-tests/basic' --tag cross-custom-basic:x86_64-unknown-linux-gnu-50dfa-pre-build --build-arg 'CROSS_CMD=apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross' --build-arg 'CROSS_DEB_ARCH=amd64' --file /Users/emil/workspace/cross-tests/basic/target/x86_64-unknown-linux-gnu/Dockerfile.x86_64-unknown-linux-gnu-custom .
[+] Building 10.0s (6/6) FINISHED                                                                               
 => [internal] load build definition from Dockerfile.x86_64-unknown-linux-gnu-custom                       0.0s
 => => transferring dockerfile: 166B                                                                       0.0s
 => [internal] load .dockerignore                                                                          0.0s
 => => transferring context: 2B                                                                            0.0s
 => [internal] load metadata for docker.io/library/ubuntu:20.04                                            0.6s
 => CACHED [1/2] FROM docker.io/library/ubuntu:20.04@sha256:fd92c36d3cb9b1d027c4d2a72c6bf0125da82425fc2ca  0.0s
 => [2/2] RUN eval "apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross  8.8s
 => exporting to image                                                                                     0.5s 
 => => exporting layers                                                                                    0.5s 
 => => writing image sha256:92f3cf51cc16c7b176c9bc09ce28ef83c029881e9c5017589eb8372bf6fff8d3               0.0s 
 => => naming to docker.io/library/cross-custom-basic:x86_64-unknown-linux-gnu-50dfa-pre-build             0.0s 
+ /usr/local/bin/docker run --userns host --platform linux/arm64 -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc' -e 'CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc' -e 'CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++' -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e TERM -e 'USER=emil' --rm --user 501:20 -v /Users/emil/.xargo:/xargo:Z -v /Users/emil/.cargo:/cargo:Z -v /cargo/bin -v /Users/emil/workspace/cross-tests/basic:/project:Z -v /Users/emil/.rustup/toolchains/stable-aarch64-unknown-linux-gnu:/rust:Z,ro -v /Users/emil/workspace/cross-tests/basic/target:/target:Z -w /project -i -t cross-custom-basic:x86_64-unknown-linux-gnu-50dfa-pre-build sh -c 'PATH=$PATH:/rust/bin cargo build --target x86_64-unknown-linux-gnu -v'
       Fresh basic v0.1.0 (/project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s

basic on master [?] is 📦 v0.1.0 via 🦀 v1.61.0 took 12s 
❯ file target/x86_64-unknown-linux-gnu/debug/basic
target/x86_64-unknown-linux-gnu/debug/basic: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5716ca6c6514e255e9a29eeeeac3d9cb4fb43c0a, for GNU/Linux 3.2.0, with debug_info, not stripped

basic on master [?] is 📦 v0.1.0 via 🦀 v1.61.0 
❯ uname -a
Darwin Emils-MacBook-Pro.local 21.5.0 Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:37 PDT 2022; root:xnu-8020.121.3~4/RELEASE_ARM64_T6000 arm64
```

also makes `cross +nightly-2022-01-01` possible.

this is a requirement for #751 

Emilgardis: need to add documentation, ill work on that
Emilgardis: Now this is possible!

```toml
[target.x86_64-unknown-linux-gnu]
pre-build = [
    "apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross",
]

[target.x86_64-unknown-linux-gnu.env]
passthrough = [
    "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc",
    "CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc",
    "CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++",
]

[target.x86_64-unknown-linux-gnu.image]
name = "ubuntu:20.04"
toolchain = ["aarch64-unknown-linux-gnu"]
```

```
❯ cross build --target x86_64-unknown-linux-gnu -v
+ cargo metadata --format-version 1 --filter-platform x86_64-unknown-linux-gnu
+ rustc --print sysroot
+ /usr/local/bin/docker
+ /usr/local/bin/docker version '-f "{{ .Server.Os }},{{ .Server.Arch }}"'
+ rustup --verbose toolchain list
+ rustup --verbose target list --toolchain stable-aarch64-unknown-linux-gnu
+ rustup --verbose component list --toolchain stable-aarch64-unknown-linux-gnu
+ "/Users/emil/workspace/cross-tests/basic" /usr/local/bin/docker build --platform linux/arm64 --label 'org.cross-rs.for-cross-target=x86_64-unknown-linux-gnu' --label 'org.cross-rs.runs-with=aarch64-unknown-linux-gnu' --label 'org.cross-rs.workspace_root=/Users/emil/workspace/cross-tests/basic' --tag cross-custom-basic:x86_64-unknown-linux-gnu-50dfa-pre-build --build-arg 'CROSS_CMD=apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross' --build-arg 'CROSS_DEB_ARCH=amd64' --file /Users/emil/workspace/cross-tests/basic/target/x86_64-unknown-linux-gnu/Dockerfile.x86_64-unknown-linux-gnu-custom .
[+] Building 10.0s (6/6) FINISHED                                                                               
 => [internal] load build definition from Dockerfile.x86_64-unknown-linux-gnu-custom                       0.0s
 => => transferring dockerfile: 166B                                                                       0.0s
 => [internal] load .dockerignore                                                                          0.0s
 => => transferring context: 2B                                                                            0.0s
 => [internal] load metadata for docker.io/library/ubuntu:20.04                                            0.6s
 => CACHED [1/2] FROM docker.io/library/ubuntu:20.04@sha256:fd92c36d3cb9b1d027c4d2a72c6bf0125da82425fc2ca  0.0s
 => [2/2] RUN eval "apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross  8.8s
 => exporting to image                                                                                     0.5s 
 => => exporting layers                                                                                    0.5s 
 => => writing image sha256:92f3cf51cc16c7b176c9bc09ce28ef83c029881e9c5017589eb8372bf6fff8d3               0.0s 
 => => naming to docker.io/library/cross-custom-basic:x86_64-unknown-linux-gnu-50dfa-pre-build             0.0s 
+ /usr/local/bin/docker run --userns host --platform linux/arm64 -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc' -e 'CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc' -e 'CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++' -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e TERM -e 'USER=emil' --rm --user 501:20 -v /Users/emil/.xargo:/xargo:Z -v /Users/emil/.cargo:/cargo:Z -v /cargo/bin -v /Users/emil/workspace/cross-tests/basic:/project:Z -v /Users/emil/.rustup/toolchains/stable-aarch64-unknown-linux-gnu:/rust:Z,ro -v /Users/emil/workspace/cross-tests/basic/target:/target:Z -w /project -i -t cross-custom-basic:x86_64-unknown-linux-gnu-50dfa-pre-build sh -c 'PATH=$PATH:/rust/bin cargo build --target x86_64-unknown-linux-gnu -v'
       Fresh basic v0.1.0 (/project)
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s

basic on master [?] is 📦 v0.1.0 via 🦀 v1.61.0 took 12s 
❯ file target/x86_64-unknown-linux-gnu/debug/basic
target/x86_64-unknown-linux-gnu/debug/basic: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=5716ca6c6514e255e9a29eeeeac3d9cb4fb43c0a, for GNU/Linux 3.2.0, with debug_info, not stripped

basic on master [?] is 📦 v0.1.0 via 🦀 v1.61.0 
❯ uname -a
Darwin Emils-MacBook-Pro.local 21.5.0 Darwin Kernel Version 21.5.0: Tue Apr 26 21:08:37 PDT 2022; root:xnu-8020.121.3~4/RELEASE_ARM64_T6000 arm64
```

Emilgardis: bors try

hopefully my rebase hasn't broken anything :))
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7267265125?check_suite_focus=true)
Emilgardis: seems to work, promising. The bisect issue is due to it being a new day
Emilgardis: bors try --target none
Emilgardis: bors try-

bors try aaaa
Emilgardis: bors try-
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7277077838?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build succeeded:
  * [conclusion](https://github.com/cross-rs/cross/runs/7278179686?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
Emilgardis: bors try --target aarch64-unknown-linux-gnu
Emilgardis: bors try-
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7279735009?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7280499477?check_suite_focus=true)
Emilgardis: I don't understand the error

<details>
<summary>curated logs</summary>

```log
2022-07-11T09:32:19.1952885Z + cargo build
2022-07-11T09:32:19.3232518Z    Compiling cross v0.2.4 (/home/runner/work/cross/cross)
2022-07-11T09:32:30.8018591Z     Finished dev [unoptimized + debuginfo] target(s) in 11.52s
2022-07-11T09:32:30.8057743Z + export CROSS=/home/runner/work/cross/cross/target/debug/cross
2022-07-11T09:32:30.8058177Z + CROSS=/home/runner/work/cross/cross/target/debug/cross
2022-07-11T09:32:30.8060817Z ++ mktemp -d
2022-07-11T09:32:30.8069598Z + td=/tmp/tmp.BEo18tca8q
2022-07-11T09:32:30.8070004Z + pushd /tmp/tmp.BEo18tca8q
2022-07-11T09:32:30.8070485Z + cargo init --bin --name foreign_toolchain
2022-07-11T09:32:30.8071678Z /tmp/tmp.BEo18tca8q ~/work/cross/cross
2022-07-11T09:32:32.1170183Z      Created binary (application) package
2022-07-11T09:32:32.1178414Z + echo '# Cross.toml
2022-07-11T09:32:32.1178802Z [build]
2022-07-11T09:32:32.1179755Z default-target = "x86_64-unknown-linux-musl"
2022-07-11T09:32:32.1180047Z 
2022-07-11T09:32:32.1180376Z [target."x86_64-unknown-linux-musl"]
2022-07-11T09:32:32.1180697Z image.name = "alpine:edge"
2022-07-11T09:32:32.1181304Z image.toolchain = ["x86_64-unknown-linux-musl"]
2022-07-11T09:32:32.1182118Z pre-build = ["apk add --no-cache gcc musl-dev"]'
2022-07-11T09:32:32.1182738Z + /home/runner/work/cross/cross/target/debug/cross run -v
2022-07-11T09:32:32.3043608Z + cargo metadata --format-version 1
2022-07-11T09:32:32.6643441Z + rustc --print sysroot
2022-07-11T09:32:32.7588418Z + /usr/bin/docker
2022-07-11T09:32:36.4783122Z + /usr/bin/docker version '-f "{{ .Server.Os }},{{ .Server.Arch }}"'
2022-07-11T09:32:36.5156592Z + rustup toolchain list
2022-07-11T09:32:36.5192492Z + rustup --verbose toolchain add stable-x86_64-unknown-linux-musl --profile minimal
2022-07-11T09:32:49.7281545Z + rustup target list --toolchain stable-x86_64-unknown-linux-musl
2022-07-11T09:32:50.3013816Z + rustup component list --toolchain stable-x86_64-unknown-linux-musl
2022-07-11T09:32:50.3436662Z + rustup --verbose component add rust-src --toolchain stable-x86_64-unknown-linux-musl
2022-07-11T09:32:50.9258630Z + "/tmp/tmp.BEo18tca8q" /usr/bin/docker build --platform linux/amd64 --label 'org.cross-rs.for-cross-target=x86_64-unknown-linux-musl' --label 'org.cross-rs.runs-with=x86_64-unknown-linux-musl' --label 'org.cross-rs.workspace_root=/tmp/tmp.BEo18tca8q' --tag cross-custom-tmp.beotcaq:x86_64-unknown-linux-musl-394df-pre-build --build-arg 'CROSS_CMD=apk add --no-cache gcc musl-dev' --build-arg 'CROSS_DEB_ARCH=musl-linux-amd64' --file /tmp/tmp.BEo18tca8q/target/x86_64-unknown-linux-musl/Dockerfile.x86_64-unknown-linux-musl-custom /tmp/tmp.BEo18tca8q
2022-07-11T09:33:02.0096604Z Successfully built ca051d390867
2022-07-11T09:33:02.0129336Z Successfully tagged cross-custom-tmp.beotcaq:x86_64-unknown-linux-musl-394df-pre-build
2022-07-11T09:33:02.0132414Z + /usr/bin/docker run --userns host --platform linux/amd64 -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e CARGO_HTTP_CHECK_REVOKE -e RUSTFLAGS -e CARGO_NET_RETRY -e CARGO_INCREMENTAL -e 'USER=runner' --rm --user 1001:121 -v /home/runner/.xargo:/xargo:Z -v /home/runner/.cargo:/cargo:Z -v /cargo/bin -v /tmp/tmp.BEo18tca8q:/project:Z -v /home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-musl:/rust:Z,ro -v /tmp/tmp.BEo18tca8q/target:/target:Z -w /project cross-custom-tmp.beotcaq:x86_64-unknown-linux-musl-394df-pre-build sh -c 'PATH=$PATH:/rust/bin cargo run -v --target x86_64-unknown-linux-musl'
2022-07-11T09:33:02.6285776Z    Compiling foreign_toolchain v0.1.0 (/project)
2022-07-11T09:33:02.6290174Z      Running `rustc --crate-name foreign_toolchain --edition=2021 src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type bin --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 -C metadata=5a103a4a40b3bbe2 -C extra-filename=-5a103a4a40b3bbe2 --out-dir /target/x86_64-unknown-linux-musl/debug/deps --target x86_64-unknown-linux-musl -L dependency=/target/x86_64-unknown-linux-musl/debug/deps -L dependency=/target/debug/deps -C instrument-coverage --remap-path-prefix /home/runner/work/cross/cross=`
2022-07-11T09:33:02.8704395Z     Finished dev [unoptimized + debuginfo] target(s) in 0.56s
2022-07-11T09:33:02.8710776Z      Running `/target/x86_64-unknown-linux-musl/debug/foreign_toolchain`
2022-07-11T09:33:02.8716476Z Hello, world!
2022-07-11T09:33:02.9949523Z + echo '# Cross.toml
2022-07-11T09:33:02.9949811Z [build]
2022-07-11T09:33:02.9950610Z default-target = "x86_64-unknown-linux-gnu"
2022-07-11T09:33:02.9951119Z 
2022-07-11T09:33:02.9951980Z [target.x86_64-unknown-linux-gnu]
2022-07-11T09:33:02.9952653Z pre-build = [
2022-07-11T09:33:02.9953160Z     "apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross",
2022-07-11T09:33:02.9953538Z ]
2022-07-11T09:33:02.9953680Z 
2022-07-11T09:33:02.9953890Z [target.x86_64-unknown-linux-gnu.env]
2022-07-11T09:33:02.9954169Z passthrough = [
2022-07-11T09:33:02.9954712Z     "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc",
2022-07-11T09:33:02.9955172Z     "CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc",
2022-07-11T09:33:02.9955602Z     "CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++",
2022-07-11T09:33:02.9956082Z ]
2022-07-11T09:33:02.9956218Z 
2022-07-11T09:33:02.9956467Z [target.x86_64-unknown-linux-gnu.image]
2022-07-11T09:33:02.9956742Z name = "ubuntu:20.04"
2022-07-11T09:33:02.9957118Z toolchain = ["aarch64-unknown-linux-gnu"]
2022-07-11T09:33:02.9957427Z     '
2022-07-11T09:33:02.9957767Z + /home/runner/work/cross/cross/target/debug/cross run -v
2022-07-11T09:33:03.1902924Z + cargo metadata --format-version 1
2022-07-11T09:33:03.5494476Z + rustc --print sysroot
2022-07-11T09:33:03.6432767Z + /usr/bin/docker
2022-07-11T09:33:03.7201337Z + /usr/bin/docker version '-f "{{ .Server.Os }},{{ .Server.Arch }}"'
2022-07-11T09:33:03.7540368Z + rustup toolchain list
2022-07-11T09:33:03.7574621Z + rustup --verbose toolchain add stable-aarch64-unknown-linux-gnu --profile minimal
2022-07-11T09:33:17.7266752Z + rustup target list --toolchain stable-aarch64-unknown-linux-gnu
2022-07-11T09:33:18.3021058Z + rustup --verbose target add x86_64-unknown-linux-gnu --toolchain stable-aarch64-unknown-linux-gnu
2022-07-11T09:33:22.3486964Z + "/tmp/tmp.BEo18tca8q" /usr/bin/docker build --platform linux/arm64 --label 'org.cross-rs.for-cross-target=x86_64-unknown-linux-gnu' --label 'org.cross-rs.runs-with=aarch64-unknown-linux-gnu' --label 'org.cross-rs.workspace_root=/tmp/tmp.BEo18tca8q' --tag cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build --build-arg 'CROSS_CMD=apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross' --build-arg 'CROSS_DEB_ARCH=amd64' --file /tmp/tmp.BEo18tca8q/target/x86_64-unknown-linux-gnu/Dockerfile.x86_64-unknown-linux-gnu-custom /tmp/tmp.BEo18tca8q
2022-07-11T09:33:46.0190465Z Successfully built ea5b8ec2e41f
2022-07-11T09:33:46.0209883Z Successfully tagged cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build
2022-07-11T09:33:46.0262099Z + /usr/bin/docker run --userns host --platform linux/arm64 -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc' -e 'CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc' -e 'CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++' -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e CARGO_HTTP_CHECK_REVOKE -e RUSTFLAGS -e CARGO_NET_RETRY -e CARGO_INCREMENTAL -e 'USER=runner' --rm --user 1001:121 -v /home/runner/.xargo:/xargo:Z -v /home/runner/.cargo:/cargo:Z -v /cargo/bin -v /tmp/tmp.BEo18tca8q:/project:Z -v /home/runner/.rustup/toolchains/stable-aarch64-unknown-linux-gnu:/rust:Z,ro -v /tmp/tmp.BEo18tca8q/target:/target:Z -w /project cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build sh -c 'PATH=$PATH:/rust/bin cargo run -v --target x86_64-unknown-linux-gnu'
2022-07-11T09:33:46.0546201Z Unable to find image 'cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build' locally
2022-07-11T09:33:46.7600044Z docker: Error response from daemon: pull access denied for cross-custom-tmp.beotcaq, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.
```

</details>

the gist of the error is

```
+ "/tmp/tmp.BEo18tca8q" /usr/bin/docker build --platform linux/arm64 --label 'org.cross-rs.for-cross-target=x86_64-unknown-linux-gnu' --label 'org.cross-rs.runs-with=aarch64-unknown-linux-gnu' --label 'org.cross-rs.workspace_root=/tmp/tmp.BEo18tca8q' --tag cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build --build-arg 'CROSS_CMD=apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross' --build-arg 'CROSS_DEB_ARCH=amd64' --file /tmp/tmp.BEo18tca8q/target/x86_64-unknown-linux-gnu/Dockerfile.x86_64-unknown-linux-gnu-custom /tmp/tmp.BEo18tca8q
2022-07-11T09:33:46.0190465Z Successfully built ea5b8ec2e41f
2022-07-11T09:33:46.0209883Z Successfully tagged cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build
2022-07-11T09:33:46.0262099Z + /usr/bin/docker run --userns host --platform linux/arm64 -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc' -e 'CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc' -e 'CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++' -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e CARGO_HTTP_CHECK_REVOKE -e RUSTFLAGS -e CARGO_NET_RETRY -e CARGO_INCREMENTAL -e 'USER=runner' --rm --user 1001:121 -v /home/runner/.xargo:/xargo:Z -v /home/runner/.cargo:/cargo:Z -v /cargo/bin -v /tmp/tmp.BEo18tca8q:/project:Z -v /home/runner/.rustup/toolchains/stable-aarch64-unknown-linux-gnu:/rust:Z,ro -v /tmp/tmp.BEo18tca8q/target:/target:Z -w /project cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build sh -c 'PATH=$PATH:/rust/bin cargo run -v --target x86_64-unknown-linux-gnu'
2022-07-11T09:33:46.0546201Z Unable to find image 'cross-custom-tmp.beotcaq:x86_64-unknown-linux-gnu-394df-pre-build' locally
2022-07-11T09:33:46.7600044Z docker: Error response from daemon: pull access denied for cross-custom-tmp.beotcaq, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.
```

but the previous one works

```
2022-07-11T09:32:50.9258630Z + "/tmp/tmp.BEo18tca8q" /usr/bin/docker build --platform linux/amd64 --label 'org.cross-rs.for-cross-target=x86_64-unknown-linux-musl' --label 'org.cross-rs.runs-with=x86_64-unknown-linux-musl' --label 'org.cross-rs.workspace_root=/tmp/tmp.BEo18tca8q' --tag cross-custom-tmp.beotcaq:x86_64-unknown-linux-musl-394df-pre-build --build-arg 'CROSS_CMD=apk add --no-cache gcc musl-dev' --build-arg 'CROSS_DEB_ARCH=musl-linux-amd64' --file /tmp/tmp.BEo18tca8q/target/x86_64-unknown-linux-musl/Dockerfile.x86_64-unknown-linux-musl-custom /tmp/tmp.BEo18tca8q
2022-07-11T09:33:02.0096604Z Successfully built ca051d390867
2022-07-11T09:33:02.0129336Z Successfully tagged cross-custom-tmp.beotcaq:x86_64-unknown-linux-musl-394df-pre-build
2022-07-11T09:33:02.0132414Z + /usr/bin/docker run --userns host --platform linux/amd64 -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e CARGO_HTTP_CHECK_REVOKE -e RUSTFLAGS -e CARGO_NET_RETRY -e CARGO_INCREMENTAL -e 'USER=runner' --rm --user 1001:121 -v /home/runner/.xargo:/xargo:Z -v /home/runner/.cargo:/cargo:Z -v /cargo/bin -v /tmp/tmp.BEo18tca8q:/project:Z -v /home/runner/.rustup/toolchains/stable-x86_64-unknown-linux-musl:/rust:Z,ro -v /tmp/tmp.BEo18tca8q/target:/target:Z -w /project cross-custom-tmp.beotcaq:x86_64-unknown-linux-musl-394df-pre-build sh -c 'PATH=$PATH:/rust/bin cargo run -v --target x86_64-unknown-linux-musl'
2022-07-11T09:33:02.6285776Z    Compiling foreign_toolchain v0.1.0 (/project)
2022-07-11T09:33:02.6290174Z      Running `rustc --crate-name foreign_toolchain --edition=2021 src/main.rs --error-format=json --json=diagnostic-rendered-ansi,artifacts,future-incompat --crate-type bin --emit=dep-info,link -C embed-bitcode=no -C debuginfo=2 -C metadata=5a103a4a40b3bbe2 -C extra-filename=-5a103a4a40b3bbe2 --out-dir /target/x86_64-unknown-linux-musl/debug/deps --target x86_64-unknown-linux-musl -L dependency=/target/x86_64-unknown-linux-musl/debug/deps -L dependency=/target/debug/deps -C instrument-coverage --remap-path-prefix /home/runner/work/cross/cross=`
2022-07-11T09:33:02.8704395Z     Finished dev [unoptimized + debuginfo] target(s) in 0.56s
```
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7283819414?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7284339949?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Already running a review
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7285103616?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build succeeded:
  * [conclusion](https://github.com/cross-rs/cross/runs/7285868123?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7289427262?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7290416075?check_suite_focus=true)
Emilgardis: unsure what happened between ddf77c682febc3b5408524b72cf8b2a2a3f02d70...37d42520c305d7982694728e3573673f8527adb3 that made this not work anymore
Emilgardis: found it, `!` was lost

bors try --target aarch64-unknown-linux-gnu
Emilgardis: bors try-

test is stuck on a random regression in bisect test due to some circumstance. Fixed by f5a17cf47cc39107e63cbe4b90204bb11a50200e
Alexhuszagh: Besides the one server/client issue with getting the Docker server architecture, and I have to test on macOS and Windows, everything looks good to me. I think we should have decent support for 3rd party clients if they have Docker compatibility, because as long as it's similar to Docker, it costs us nothing.
Alexhuszagh: So the only issue I think I see left is attempting to run it on an `x86_64-pc-windows-msvc` system, using Git Bash. If I have a previous Linux build in `$TARGET_DIR/debug/cross`, then it fails with a binary exec issue. Otherwise, I get the following (using Docker):

```bash
    Finished dev [unoptimized + debuginfo] target(s) in 0.18s
+ export CROSS=/c/Users/ahusz/git/cross/target/debug/cross
+ CROSS=/c/Users/ahusz/git/cross/target/debug/cross
++ mkcargotemp -d
++ local td=
++ local tmpdir=/c/Users/ahusz/git/cross/target/tmp
++ mkdir -p /c/Users/ahusz/git/cross/target/tmp
+++ command mktemp --tmpdir=/c/Users/ahusz/git/cross/target/tmp -d
+++ mktemp --tmpdir=/c/Users/ahusz/git/cross/target/tmp -d
++ td=/c/Users/ahusz/git/cross/target/tmp/tmp.lEJFBgEZAz
+++ basename /c/Users/ahusz/git/cross/target/tmp/tmp.lEJFBgEZAz
++ echo '# Cargo.toml
  [workspace]
  members = ["tmp.lEJFBgEZAz"]
   '
++ echo /c/Users/ahusz/git/cross/target/tmp/tmp.lEJFBgEZAz
+ td=/c/Users/ahusz/git/cross/target/tmp/tmp.lEJFBgEZAz
+ pushd /c/Users/ahusz/git/cross/target/tmp/tmp.lEJFBgEZAz
~/git/cross/target/tmp/tmp.lEJFBgEZAz ~/git/cross
+ cargo init --bin --name foreign_toolchain
     Created binary (application) package
+ echo '# Cross.toml
[build]
default-target = "x86_64-unknown-linux-musl"

[target."x86_64-unknown-linux-musl"]
image.name = "alpine:edge"
image.toolchain = ["x86_64-unknown-linux-musl"]
pre-build = ["apk add --no-cache gcc musl-dev"]'
+ /c/Users/ahusz/git/cross/target/debug/cross run -v
+ cargo metadata --format-version 1
+ rustc --print sysroot
+ "C:\Program Files\Docker\Docker\resources\bin\docker.EXE"
+ "C:\Program Files\Docker\Docker\resources\bin\docker.EXE" version -f '{{ .Server.Os }},,,{{ .Server.Arch }}'
+ rustup toolchain list
+ rustup target list --toolchain stable-x86_64-unknown-linux-gnu
+ rustup component list --toolchain stable-x86_64-unknown-linux-gnu
+ "C:\Program Files\Docker\Docker\resources\bin\docker.EXE" run --userns host -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e TERM -e 'USER=ahusz' --rm --user 1000:1000 -v 'C:\Users\ahusz\.xargo:/xargo:Z' -v 'C:\Users\ahusz\.cargo:/cargo:Z' -v /cargo/bin -v 'C:\Users\ahusz\git\cross\target\tmp:/mnt/c/Users/ahusz/git/cross/target/tmp:Z' -v 'C:\Users\ahusz\.rustup\toolchains\stable-x86_64-unknown-linux-gnu:/rust:Z,ro' -v 'C:\Users\ahusz\git\cross\target\tmp\target:/target:Z' -w /mnt/c/Users/ahusz/git/cross/target/tmp/tmp.lEJFBgEZAz -i -t ghcr.io/cross-rs/x86_64-pc-windows-msvc:main sh -c 'PATH=$PATH:/rust/bin cargo run -v --target x86_64-pc-windows-msvc'
Unable to find image 'ghcr.io/cross-rs/x86_64-pc-windows-msvc:main' locally
docker: Error response from daemon: Head "https://ghcr.io/v2/cross-rs/x86_64-pc-windows-msvc/manifests/main": denied.
See 'docker run --help'.
+ rustup component list --toolchain stable-x86_64-unknown-linux-gnu
```
Alexhuszagh: This is a more general issue when using the default toolchain, which no longer seems to be respected:

```bash
$ cross build
stdout="C:\\Users\\ahusz\\.rustup\\toolchains\\stable-x86_64-pc-windows-msvc"
target=BuiltIn { triple: X86_64PcWindowsMsvc }
Unable to find image 'ghcr.io/cross-rs/x86_64-pc-windows-msvc:main' locally
docker: Error response from daemon: Head "https://ghcr.io/v2/cross-rs/x86_64-pc-windows-msvc/manifests/main": denied.
See 'docker run --help'.
```
Alexhuszagh: So it turns out this is a bug I introduced in my own scripts. We should probably change `"${PROJECT_HOME}/target/debug/cross"` to `cargo run --bin cross --manifest-path "${PROJECT_HOME}/Cargo.toml"`, or handle binary extensions ourselves.
Emilgardis: > This is a more general issue when providing no toolchain with no default target, which no longer seems to be respected (annotations are print debugging symbols I added in).

```
❯ rustup show active-toolchain
nightly-x86_64-pc-windows-msvc (default)

❯ cargo run --bin cross aaa
    Finished dev [unoptimized + debuginfo] target(s) in 0.31s
     Running `target\debug\cross.exe aaa`
[cross] warning: `cross` does not provide a Docker image for target x86_64-pc-windows-msvc, specify a custom image in `Cross.toml`.
[cross] note: Falling back to `cargo` on the host.
error: no such subcommand: `aaa`

        Did you mean `add`?
error: process didn't exit successfully: `target\debug\cross.exe aaa` (exit code: 101)
```

fixed with 8ff9ef82b897539031211c7955971fe076bf8936
Emilgardis: > So it turns out this is a bug I introduced in my own scripts. We should probably change `"${PROJECT_HOME}/target/debug/cross"` to `cargo run --bin cross --manifest-path "${PROJECT_HOME}/Cargo.toml"`, or handle binary extensions ourselves. The reason why is Windows uses `cross` over `cross.exe` if the former is available, and it is if we've done a previous WSL2 build.
> 
> This is pretty low-priority though.

need https://rust-lang.zulipchat.com/#narrow/stream/122651-general/topic/make.20cargo.20always.20output.20as.20if.20--target.20was.20specified :)

unsure how to go about this, I don't like the manifest-path alternative. it is however as mentioned a minor, as the issue only presents in these test files.

One possible way to get out of this is to switch the tests to use xshell to be written in rust
Emilgardis: > Actually, the issue is the default-target doesn't seem to be respected. It's completely ignored.

I'm unable to get this behaviour, I think the actual issue was fixed with 8ff9ef82b897539031211c7955971fe076bf8936
Emilgardis: bors try --target aarch64-unknown
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7341046540?check_suite_focus=true)
Emilgardis: yay!
back to the issue I was having before, no wonder it dissapeared before with the tmp changes, it dissappeared because docker build was no longer used :D 

```
---> bb0f1b0374d6
Step 7/7 : LABEL org.cross-rs.workspace_root=/home/runner/work/cross/cross/target/tmp
 ---> Running in 691912c56dd3
Removing intermediate container 691912c56dd3
 ---> 3ce7618d0e19
Successfully built 3ce7618d0e19
Successfully tagged cross-custom-tmp:x86_64-unknown-linux-gnu-80868-pre-build
+ /usr/bin/docker run --userns host --platform linux/arm64 -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc' -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER=/qemu-runner x86_64' -e 'CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc' -e 'CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++' -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e CARGO_HTTP_CHECK_REVOKE -e CARGO_TMP_DIR -e RUSTFLAGS -e CARGO_NET_RETRY -e CARGO_INCREMENTAL -e 'USER=runner' --rm --user 1001:121 -v /home/runner/.xargo:/xargo:Z -v /home/runner/.cargo:/cargo:Z -v /cargo/bin -v /home/runner/work/cross/cross/target/tmp:/home/runner/work/cross/cross/target/tmp:Z -v /home/runner/.rustup/toolchains/stable-aarch64-unknown-linux-gnu:/rust:Z,ro -v /home/runner/work/cross/cross/target/tmp/target:/target:Z -w /home/runner/work/cross/cross/target/tmp/tmp.3sC8nXO1HX cross-custom-tmp:x86_64-unknown-linux-gnu-80868-pre-build sh -c 'PATH=$PATH:/rust/bin cargo build -v --target x86_64-unknown-linux-gnu'
Unable to find image 'cross-custom-tmp:x86_64-unknown-linux-gnu-80868-pre-build' locally
docker: Error response from daemon: pull access denied for cross-custom-tmp, repository does not exist or may require 'docker login': denied: requested access to the resource is denied.
See 'docker run --help'.
```
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7342211310?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7345001486?check_suite_focus=true)
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7345329654?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu
Alexhuszagh: > bors try --target aarch64-unknown-linux-gnu

Let me test on a fork :/
Emilgardis: I have no clue how to fix this error.

```
Removing intermediate container 6ba39688d361
 ---> e5b70b69a7b4
Successfully built e5b70b69a7b4
Successfully tagged localhost/cross-rs/cross-custom-tmp:x86_64-unknown-linux-gnu-80868-pre-build
+ /usr/bin/docker
REPOSITORY                            TAG                                        IMAGE ID       CREATED         SIZE
localhost/cross-rs/cross-custom-tmp   x86_64-unknown-linux-gnu-80868-pre-build   e5b70b69a7b4   1 second ago    328MB
node                                  14                                         0849acd0acd1   6 days ago      914MB
node                                  14-alpine                                  39e98037c459   6 days ago      119MB
node                                  16                                         e6b2a509415b   6 days ago      907MB
node                                  16-alpine                                  b48aa2e90c0a   6 days ago      112MB
buildpack-deps                        stretch                                    9d4f40bcbdd3   3 weeks ago     835MB
buildpack-deps                        buster                                     9ef0e2a2b02c   3 weeks ago     804MB
buildpack-deps                        bullseye                                   368c349ca3b0   3 weeks ago     834MB
debian                                9                                          662c05203bab   3 weeks ago     101MB
debian                                10                                         9d4d2232003a   3 weeks ago     114MB
debian                                11                                         d2780094a226   3 weeks ago     124MB
ubuntu                                20.04                                      20fffa419e3a   5 weeks ago     72.8MB
ubuntu                                18.04                                      ad080923604a   5 weeks ago     63.1MB
moby/buildkit                         latest                                     a2c9241854f2   2 months ago    142MB
node                                  12                                         6c8de432fc7f   2 months ago    918MB
node                                  12-alpine                                  bb6d28039b8c   3 months ago    91MB
alpine                                3.12                                       24c8ece58a1a   3 months ago    5.58MB
alpine                                3.13                                       20e452a0a81a   3 months ago    5.61MB
alpine                                3.14                                       e04c818066af   3 months ago    5.59MB
alpine                                edge                                       70b641350c4b   3 months ago    5.6MB
ubuntu                                16.04                                      b6f507652425   10 months ago   135MB
+ /usr/bin/docker run --userns host --platform linux/arm64 -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc' -e 'CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER=/qemu-runner x86_64' -e 'CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc' -e 'CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++' -e 'PKG_CONFIG_ALLOW_CROSS=1' -e 'XARGO_HOME=/xargo' -e 'CARGO_HOME=/cargo' -e 'CARGO_TARGET_DIR=/target' -e 'CROSS_RUNNER=' -e CARGO_HTTP_CHECK_REVOKE -e CARGO_TMP_DIR -e RUSTFLAGS -e CARGO_NET_RETRY -e CARGO_INCREMENTAL -e 'USER=runner' --rm --user 1001:121 -v /home/runner/.xargo:/xargo:Z -v /home/runner/.cargo:/cargo:Z -v /cargo/bin -v /home/runner/work/cross/cross/target/tmp:/home/runner/work/cross/cross/target/tmp:Z -v /home/runner/.rustup/toolchains/stable-aarch64-unknown-linux-gnu:/rust:Z,ro -v /home/runner/work/cross/cross/target/tmp/target:/target:Z -w /home/runner/work/cross/cross/target/tmp/tmp.lawR3lBwiz localhost/cross-rs/cross-custom-tmp:x86_64-unknown-linux-gnu-80868-pre-build sh -c 'PATH=$PATH:/rust/bin cargo build -v --target x86_64-unknown-linux-gnu'
Unable to find image 'localhost/cross-rs/cross-custom-tmp:x86_64-unknown-linux-gnu-80868-pre-build' locally
docker: Error response from daemon: Get "http://localhost/v2/": dial tcp [::1]:80: connect: connection refused.
See 'docker run --help'.
+ rustup component list --toolchain stable-aarch64-unknown-linux-gnu
Error: Process completed with exit code 125.
```

...
Alexhuszagh: I have no idea why it's trying to pull the image, and it doesn't do this locally :/. Is there a way we can disable pulling for this test only? Maybe via `CROSS_CONTAINER_OPTS`?
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7346964098?check_suite_focus=true)
Emilgardis: bors try --target aarch64-unknown-linux-gnu

it could be that `--platform linux/arm64` is not possible
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7347137930?check_suite_focus=true)
Emilgardis: Yep, I think this is actually a docker issue with how it reports the error. I remember seeing an issue about this, can't find it.

Anyway, should be solvable
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7347410696?check_suite_focus=true)
Emilgardis: ok, now both failed...

this is something to do with the buildx environment, since it worked with `docker build` for x86_64, but not for `docker buildx build`
Emilgardis: the issue is probably this:

https://github.com/docker/buildx/blob/master/docs/reference/buildx_create.md#docker-container-driver

```
Uses a BuildKit container that will be spawned via docker. With this driver, both building multi-platform images and exporting cache are supported.

Unlike docker driver, built images will not automatically appear in docker images and [build --load](https://github.com/docker/buildx/blob/master/docs/reference/buildx_build.md#load) needs to be used to achieve that.
```

We need to specify `--load` or `----output=type=docker`
Emilgardis: bors try --target aarch64-unknown-linux-gnu
Emilgardis: this is now breaking
Emilgardis: bors try --target aarch64-unknown-linux-gnu
bors[bot]: ## try

Build succeeded:
  * [conclusion](https://github.com/cross-rs/cross/runs/7349203006?check_suite_focus=true)
Alexhuszagh: Went over the changes since the last commit, everything looks good. I tested it and it works on:
- [x] Native Linux
- [x] macOS Docker
- [x] Windows Docker
- [ ] WSL2 Podman

It does not, however, work with `podman` 3.4.4 since the `--output` flag is not known to podman. This might be true for other engines as well. I'm going to test newer Podman installs.
Emilgardis: Seems podman build only supports --output since [2022-05-04](https://github.com/containers/buildah/blob/3a8223522f9cc9a997b059e2eab7d7ae517e9ffb/changelog.txt#L16)

I think we could just remove `--output` for podman, don't think it's needed for podman
Alexhuszagh: I've added the following diff which handles Podman returning partial data with a status error, better error handling on the fallback case, and not using `--output` if we're using Podman.

```diff
diff --git a/src/docker/custom.rs b/src/docker/custom.rs
index 89eb189..5e82910 100644
--- a/src/docker/custom.rs
+++ b/src/docker/custom.rs
@@ -2,7 +2,7 @@ use std::io::Write;
 use std::path::PathBuf;
 use std::str::FromStr;
 
-use crate::docker::{DockerOptions, DockerPaths};
+use crate::docker::{DockerOptions, DockerPaths, EngineType};
 use crate::shell::MessageInfo;
 use crate::{docker, CargoMetadata, TargetTriple};
 use crate::{errors::*, file, CommandExt, ToUtf8};
@@ -147,7 +147,7 @@ impl<'a> Dockerfile<'a> {
         let has_output = options.config.build_opts().map_or(false, |opts| {
             opts.contains("--load") || opts.contains("--output")
         });
-        if !has_output {
+        if options.engine.kind == EngineType::Docker && !has_output {
             docker_build.args(&["--output", "type=docker"]);
         };
 
diff --git a/src/docker/engine.rs b/src/docker/engine.rs
index 9bc37c4..d7095dd 100644
--- a/src/docker/engine.rs
+++ b/src/docker/engine.rs
@@ -3,9 +3,9 @@ use std::path::{Path, PathBuf};
 use std::process::Command;
 
 use crate::config::bool_from_envvar;
+use crate::errors::*;
 use crate::extensions::CommandExt;
 use crate::shell::MessageInfo;
-use crate::{errors::*, OutputExt};
 
 use super::{Architecture, ContainerOs};
 
@@ -20,6 +20,13 @@ pub enum EngineType {
     Other,
 }
 
+impl EngineType {
+    #[must_use]
+    pub fn is_podman(&self) -> bool {
+        matches!(self, EngineType::Podman | EngineType::PodmanRemote)
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Engine {
     pub kind: EngineType,
@@ -88,6 +95,11 @@ impl Engine {
         )
     }
 
+    #[must_use]
+    pub fn is_podman(&self) -> bool {
+        self.kind.is_podman()
+    }
+
     #[must_use]
     pub fn is_remote() -> bool {
         env::var("CROSS_REMOTE")
@@ -117,82 +129,71 @@ fn get_engine_info(
         EngineType::Other
     };
 
-    let mut cmd = Command::new(ce);
-    cmd.args(&["version", "-f", "{{ .Server.Os }},,,{{ .Server.Arch }}"]);
-
-    let out = cmd.run_and_get_output(msg_info)?;
-
-    let stdout = out.stdout()?.to_lowercase();
-
-    let osarch = stdout
-        .trim()
-        .split_once(",,,")
-        .map(|(os, arch)| -> Result<_> { Ok((ContainerOs::new(os)?, Architecture::new(arch)?)) })
-        .transpose();
-
-    let osarch = match (kind, osarch) {
-        (_, Ok(Some(osarch))) => Some(osarch),
-        (EngineType::PodmanRemote | EngineType::Podman, Ok(None)) => get_podman_info(ce, msg_info)?,
-        (_, Err(e)) => {
-            return Err(e.wrap_err(format!(
-                "command `{}` returned unexpected data",
-                cmd.command_pretty(msg_info, |_| false)
-            )));
-        }
-        (EngineType::Docker | EngineType::Other, Ok(None)) => None,
-    };
-
-    let osarch = if osarch.is_some() {
-        osarch
-    } else if !out.status.success() {
-        get_custom_info(ce, msg_info).with_error(|| {
-            cmd.status_result(msg_info, out.status, Some(&out))
-                .expect_err("status_result should error")
-        })?
-    } else {
-        get_custom_info(ce, msg_info)?
+    // this can fail: podman can give partial output
+    //   linux,,,Error: template: version:1:15: executing "version" at <.Arch>:
+    //   can't evaluate field Arch in type *define.Version
+    let osarch = engine_info(
+        ce,
+        &["version", "-f", "{{ .Server.Os }},,,{{ .Server.Arch }}"],
+        ",,,",
+        msg_info,
+    )
+    .unwrap_or(None);
+
+    let osarch = match osarch {
+        Some(osarch) => Some(osarch),
+        None if kind.is_podman() => get_podman_info(ce, msg_info)?,
+        None => get_custom_info(ce, msg_info)?,
     };
 
     let (os, arch) = osarch.map_or(<_>::default(), |(os, arch)| (Some(os), Some(arch)));
     Ok((kind, arch, os))
 }
 
-fn get_podman_info(
+fn engine_info(
     ce: &Path,
+    args: &[&str],
+    sep: &str,
     msg_info: &mut MessageInfo,
 ) -> Result<Option<(ContainerOs, Architecture)>> {
     let mut cmd = Command::new(ce);
-    cmd.args(&["info", "-f", "{{ .Version.OsArch }}"]);
+    cmd.args(args);
     cmd.run_and_get_stdout(msg_info)
         .map(|s| {
             s.to_lowercase()
                 .trim()
-                .split_once('/')
+                .split_once(sep)
                 .map(|(os, arch)| -> Result<_> {
                     Ok((ContainerOs::new(os)?, Architecture::new(arch)?))
                 })
         })
+        .wrap_err_with(|| {
+            format!(
+                "command `{}` returned unexpected data",
+                cmd.command_pretty(msg_info, |_| false)
+            )
+        })
         .wrap_err("could not determine os and architecture of vm")?
         .transpose()
 }
 
+fn get_podman_info(
+    ce: &Path,
+    msg_info: &mut MessageInfo,
+) -> Result<Option<(ContainerOs, Architecture)>> {
+    engine_info(ce, &["info", "-f", "{{ .Version.OsArch }}"], "/", msg_info)
+}
+
 fn get_custom_info(
     ce: &Path,
     msg_info: &mut MessageInfo,
 ) -> Result<Option<(ContainerOs, Architecture)>> {
-    let mut cmd = Command::new(ce);
-    cmd.args(&["info", "-f", "{{ .Client.Os }},,,{{ .Client.Arch }}"]);
-    cmd.run_and_get_stdout(msg_info)
-        .map(|s| {
-            s.to_lowercase()
-                .trim()
-                .split_once(",,,")
-                .map(|(os, arch)| -> Result<_> {
-                    Ok((ContainerOs::new(os)?, Architecture::new(arch)?))
-                })
-        })
-        .unwrap_or_default()
-        .transpose()
+    engine_info(
+        ce,
+        &["info", "-f", "{{ .Client.Os }},,,{{ .Client.Arch }}"],
+        ",,,",
+        msg_info,
+    )
 }
 
 pub fn get_container_engine() -> Result<PathBuf, which::Error> {
diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs
index 220dcc0..034ccde 100644
--- a/xtask/src/build_docker_image.rs
+++ b/xtask/src/build_docker_image.rs
@@ -183,7 +183,7 @@ pub fn build_docker_image(
 
         if push {
             docker_build.arg("--push");
-        } else if no_output {
+        } else if engine.kind == docker::EngineType::Docker && no_output {
             docker_build.args(&["--output", "type=tar,dest=/dev/null"]);
         } else {
             docker_build.arg("--load");

```
Emilgardis: @Alexhuszagh I've applied the patch but with more error handling. Hopefully I didn't break any logic that was implied by the patch.

now returns on error (with artificial flag `--foo` added to make it fail)

```
+ /Users/emil/workspace/cross/target/debug/cross run -v
+ cargo metadata --format-version 1
+ rustc --print sysroot
+ /usr/local/bin/docker
+ /usr/local/bin/docker version -f '{{ .Server.Os }},,,{{ .Server.Arch }}' --foo
+ /usr/local/bin/docker info -f '{{ .Client.Os }},,,{{ .Client.Arch }}' --foo
Error: 
   0: could not get os and arch
   1: `/usr/local/bin/docker version -f '{{ .Server.Os }},,,{{ .Server.Arch }}' --foo` failed with exit status: 16

Error:
   0: could not get os and arch
   1: `/usr/local/bin/docker info -f '{{ .Client.Os }},,,{{ .Client.Arch }}' --foo` failed with exit status: 125


Stderr:
   unknown flag: --foo
```
Alexhuszagh: lgtm, going to test briefly and then merge
Alexhuszagh: bors r+
bors[bot]: Build failed:
  * [conclusion](https://github.com/cross-rs/cross/runs/7365628878?check_suite_focus=true)

Patch:
diff --git a/.changes/817.json b/.changes/817.json
new file mode 100644
index 0000000..aa18270
--- /dev/null
+++ b/.changes/817.json
@@ -0,0 +1,16 @@
+[
+  {
+    "description": "Images can now specify a certain toolchain via `target.{target}.image.toolchain`",
+    "breaking": true,
+    "type": "added"
+  },
+  {
+    "description": "made `cross +channel` parsing more compliant to parsing a toolchain",
+    "type": "fixed"
+  },
+  {
+    "description": "`pre-build` and `dockerfile` now uses buildx/buildkit",
+    "breaking": true,
+    "type": "changed"
+  }
+]
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index fe25ba9..e9e469d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -333,7 +333,7 @@ jobs:
 
   # we should always have an artifact from a previous build.
   remote:
-    needs: [shellcheck, test, check]
+    needs: [test, check]
     runs-on: ubuntu-latest
     if: github.actor == 'bors[bot]'
     steps:
@@ -341,7 +341,6 @@ jobs:
       - uses: ./.github/actions/setup-rust
 
       - name: LLVM instrument coverage
-        id: remote-cov
         uses: ./.github/actions/cargo-llvm-cov
         with:
           name: integration-remote
@@ -353,7 +352,7 @@ jobs:
         shell: bash
 
   bisect:
-    needs: [shellcheck, test, check]
+    needs: [test, check]
     runs-on: ubuntu-latest
     if: github.actor == 'bors[bot]'
     steps:
@@ -361,7 +360,6 @@ jobs:
       - uses: ./.github/actions/setup-rust
 
       - name: LLVM instrument coverage
-        id: bisect-cov
         uses: ./.github/actions/cargo-llvm-cov
         with:
           name: integration-bisect
@@ -372,8 +370,33 @@ jobs:
         run: ./ci/test-bisect.sh
         shell: bash
 
+  foreign:
+    needs: [test, check]
+    runs-on: ubuntu-latest
+    if: github.actor == 'bors[bot]'
+    steps:
+      - uses: actions/checkout@v3
+      - uses: ./.github/actions/setup-rust
+
+      - name: LLVM instrument coverage
+        uses: ./.github/actions/cargo-llvm-cov
+        with:
+          name: integration-bisect
+      - name: Set up QEMU
+        uses: docker/setup-qemu-action@v2
+        with:
+          platforms: arm64
+      - name: Set up docker buildx
+        uses: docker/setup-buildx-action@v2
+        id: buildx
+        with:
+          install: true
+      - name: Run Foreign toolchain test
+        run: ./ci/test-foreign-toolchain.sh
+        shell: bash
+
   docker-in-docker:
-    needs: [shellcheck, test, check]
+    needs: [test, check]
     runs-on: ubuntu-latest
     if: github.actor == 'bors[bot]'
     steps:
@@ -381,7 +404,6 @@ jobs:
       - uses: ./.github/actions/setup-rust
 
       - name: LLVM instrument coverage
-        id: docker-in-docker-cov
         uses: ./.github/actions/cargo-llvm-cov
         with:
           name: integration-docker-in-docker
@@ -405,7 +427,7 @@ jobs:
           github-token: ${{ secrets.GITHUB_TOKEN }}
 
   conclusion:
-    needs: [shellcheck, fmt, clippy, test, generate-matrix, build, publish, check, remote, bisect, docker-in-docker]
+    needs: [shellcheck, fmt, clippy, test, generate-matrix, build, publish, check, remote, bisect, docker-in-docker, foreign]
     if: always()
     runs-on: ubuntu-latest
     steps:
diff --git a/Cargo.toml b/Cargo.toml
index a743533..d26b50f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -10,8 +10,6 @@ version = "0.2.4"
 edition = "2021"
 include = [
   "src/**/*",
-  "docker/Dockerfile.*",
-  "docker/*.sh",
   "docs/*.md",
   "Cargo.toml",
   "Cargo.lock",
diff --git a/README.md b/README.md
index b84717f..848aeac 100644
--- a/README.md
+++ b/README.md
@@ -46,11 +46,11 @@ One of these container engines is required. If both are installed, `cross` will
 default to `docker`.
 
 - [Docker]. Note that on Linux non-sudo users need to be in the `docker` group.
-  Read the official [post-installation steps][post]. Requires version 1.24 or later.
+  Read the official [post-installation steps][post]. Requires version 20.10 (API 1.40) or later.
 
 [post]: https://docs.docker.com/install/linux/linux-postinstall/
 
-- [Podman]. Requires version 1.6.3 or later.
+- [Podman]. Requires version 3.4.0 or later.
 
 ## Installation
 
diff --git a/ci/shared.sh b/ci/shared.sh
index 457861d..c6c736c 100755
--- a/ci/shared.sh
+++ b/ci/shared.sh
@@ -1,5 +1,21 @@
 #!/usr/bin/env bash
 
+ci_dir=$(dirname "${BASH_SOURCE[0]}")
+ci_dir=$(realpath "${ci_dir}")
+PROJECT_HOME=$(dirname "${ci_dir}")
+export PROJECT_HOME
+CARGO_TMP_DIR="${PROJECT_HOME}/target/tmp"
+export CARGO_TMP_DIR
+
+if [[ -n "${CROSS_CONTAINER_ENGINE}" ]]; then
+  CROSS_ENGINE="${CROSS_CONTAINER_ENGINE}"
+elif command -v docker >/dev/null 2>&1; then
+  CROSS_ENGINE=docker
+else
+  CROSS_ENGINE=podman
+fi
+export CROSS_ENGINE
+
 function retry {
   local tries="${TRIES-5}"
   local timeout="${TIMEOUT-1}"
@@ -21,3 +37,14 @@ function retry {
 
   return ${exit_code}
 }
+
+function mkcargotemp {
+  local td=
+  td="$CARGO_TMP_DIR"/$(mktemp -u "${@}" | xargs basename)
+  mkdir -p "$td"
+  echo '# Cargo.toml
+  [workspace]
+  members = ["'"$(basename "$td")"'"]
+   ' > "$CARGO_TMP_DIR"/Cargo.toml
+  echo "$td"
+}
diff --git a/docs/cross_toml.md b/docs/cross_toml.md
index 91e3f36..87020a7 100644
--- a/docs/cross_toml.md
+++ b/docs/cross_toml.md
@@ -56,6 +56,16 @@ $ cat ./scripts/my-script.sh
 apt-get install libssl-dev -y
 ```
 
+# `target.TARGET.image`
+
+The `image` key can also take the toolchains/platforms supported by the image.
+
+```toml
+[target.aarch64-unknown-linux-gnu]
+image.name = "alpine:edge"
+image.toolchain = ["x86_64-unknown-linux-musl", "linux/arm64=aarch64-unknown-linux-musl"] # Defaults to `x86_64-unknown-linux-gnu`
+```
+
 # `target.TARGET.env`
 
 The `target` key allows you to specify environment variables that should be used for a specific compilation target.
diff --git a/src/bin/commands/containers.rs b/src/bin/commands/containers.rs
index a249d95..f602b59 100644
--- a/src/bin/commands/containers.rs
+++ b/src/bin/commands/containers.rs
@@ -2,7 +2,11 @@ use std::io;
 
 use clap::{Args, Subcommand};
 use cross::shell::{MessageInfo, Stream};
-use cross::{docker, CommandExt};
+use cross::{docker, CommandExt, TargetTriple};
+use cross::{
+    docker::ImagePlatform,
+    rustc::{QualifiedToolchain, Toolchain},
+};
 
 #[derive(Args, Debug)]
 pub struct ListVolumes {
@@ -99,13 +103,16 @@ pub struct CreateVolume {
     /// Container engine (such as docker or podman).
     #[clap(long)]
     pub engine: Option<String>,
+    /// Toolchain to create a volume for
+    #[clap(long, default_value = TargetTriple::DEFAULT.triple(), )]
+    pub toolchain: TargetTriple,
 }
 
 impl CreateVolume {
     pub fn run(
         self,
         engine: docker::Engine,
-        channel: Option<&str>,
+        channel: Option<&Toolchain>,
         msg_info: &mut MessageInfo,
     ) -> cross::Result<()> {
         create_persistent_volume(self, &engine, channel, msg_info)
@@ -132,16 +139,19 @@ pub struct RemoveVolume {
     /// Container engine (such as docker or podman).
     #[clap(long)]
     pub engine: Option<String>,
+    /// Toolchain to remove the volume for
+    #[clap(long, default_value = TargetTriple::DEFAULT.triple(), )]
+    pub toolchain: TargetTriple,
 }
 
 impl RemoveVolume {
     pub fn run(
         self,
         engine: docker::Engine,
-        channel: Option<&str>,
+        channel: Option<&Toolchain>,
         msg_info: &mut MessageInfo,
     ) -> cross::Result<()> {
-        remove_persistent_volume(&engine, channel, msg_info)
+        remove_persistent_volume(self, &engine, channel, msg_info)
     }
 }
 
@@ -153,9 +163,9 @@ pub enum Volumes {
     RemoveAll(RemoveAllVolumes),
     /// Prune volumes not used by any container.
     Prune(PruneVolumes),
-    /// Create a persistent data volume for the current toolchain.
+    /// Create a persistent data volume for a given toolchain.
     Create(CreateVolume),
-    /// Remove a persistent data volume for the current toolchain.
+    /// Remove a persistent data volume for a given toolchain.
     Remove(RemoveVolume),
 }
 
@@ -175,15 +185,15 @@ impl Volumes {
     pub fn run(
         self,
         engine: docker::Engine,
-        toolchain: Option<&str>,
+        channel: Option<&Toolchain>,
         msg_info: &mut MessageInfo,
     ) -> cross::Result<()> {
         match self {
             Volumes::List(args) => args.run(engine, msg_info),
             Volumes::RemoveAll(args) => args.run(engine, msg_info),
             Volumes::Prune(args) => args.run(engine, msg_info),
-            Volumes::Create(args) => args.run(engine, toolchain, msg_info),
-            Volumes::Remove(args) => args.run(engine, toolchain, msg_info),
+            Volumes::Create(args) => args.run(engine, channel, msg_info),
+            Volumes::Remove(args) => args.run(engine, channel, msg_info),
         }
     }
 
@@ -373,16 +383,27 @@ pub fn prune_volumes(
 }
 
 pub fn create_persistent_volume(
-    CreateVolume { copy_registry, .. }: CreateVolume,
+    CreateVolume {
+        copy_registry,
+        toolchain,
+        ..
+    }: CreateVolume,
     engine: &docker::Engine,
-    channel: Option<&str>,
+    channel: Option<&Toolchain>,
     msg_info: &mut MessageInfo,
 ) -> 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, msg_info)?;
-    let container = docker::remote::unique_container_identifier(&target, &metadata, &dirs)?;
-    let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
+    let config = cross::config::Config::new(None);
+    let toolchain_host: cross::Target = toolchain.into();
+    let mut toolchain = QualifiedToolchain::default(&config, msg_info)?;
+    toolchain.replace_host(&ImagePlatform::from_target(
+        toolchain_host.target().clone(),
+    )?);
+    if let Some(channel) = channel {
+        toolchain = toolchain.with_picked(&config, channel.clone(), msg_info)?;
+    };
+    let (metadata, dirs) = docker::get_package_info(engine, toolchain.clone(), msg_info)?;
+    let container = docker::remote::unique_container_identifier(&toolchain_host, &metadata, &dirs)?;
+    let volume = dirs.toolchain.unique_toolchain_identifier()?;
 
     if docker::remote::volume_exists(engine, &volume, msg_info)? {
         eyre::bail!("Error: volume {volume} already exists.");
@@ -431,7 +452,7 @@ pub fn create_persistent_volume(
         engine,
         &container,
         &dirs.xargo,
-        &target,
+        &toolchain_host,
         mount_prefix.as_ref(),
         msg_info,
     )?;
@@ -446,10 +467,9 @@ pub fn create_persistent_volume(
     docker::remote::copy_volume_container_rust(
         engine,
         &container,
-        &dirs.sysroot,
-        &target,
+        &toolchain,
+        None,
         mount_prefix.as_ref(),
-        true,
         msg_info,
     )?;
 
@@ -459,14 +479,20 @@ pub fn create_persistent_volume(
 }
 
 pub fn remove_persistent_volume(
+    RemoveVolume { toolchain, .. }: RemoveVolume,
     engine: &docker::Engine,
-    channel: Option<&str>,
+    channel: Option<&Toolchain>,
     msg_info: &mut MessageInfo,
 ) -> cross::Result<()> {
-    // we only need a triple that needs docker: the actual target doesn't matter.
-    let triple = cross::Host::X86_64UnknownLinuxGnu.triple();
-    let (_, _, dirs) = docker::get_package_info(engine, triple, channel, msg_info)?;
-    let volume = docker::remote::unique_toolchain_identifier(&dirs.sysroot)?;
+    let config = cross::config::Config::new(None);
+    let target_host: cross::Target = toolchain.into();
+    let mut toolchain = QualifiedToolchain::default(&config, msg_info)?;
+    toolchain.replace_host(&ImagePlatform::from_target(target_host.target().clone())?);
+    if let Some(channel) = channel {
+        toolchain = toolchain.with_picked(&config, channel.clone(), msg_info)?;
+    };
+    let (_, dirs) = docker::get_package_info(engine, toolchain.clone(), msg_info)?;
+    let volume = dirs.toolchain.unique_toolchain_identifier()?;
 
     if !docker::remote::volume_exists(engine, &volume, msg_info)? {
         eyre::bail!("Error: volume {volume} does not exist.");
diff --git a/src/bin/cross-util.rs b/src/bin/cross-util.rs
index 8468c4c..9d634bc 100644
--- a/src/bin/cross-util.rs
+++ b/src/bin/cross-util.rs
@@ -1,8 +1,8 @@
 #![deny(missing_debug_implementations, rust_2018_idioms)]
 
 use clap::{CommandFactory, Parser, Subcommand};
-use cross::docker;
 use cross::shell::MessageInfo;
+use cross::{docker, rustc::Toolchain};
 
 mod commands;
 
@@ -11,7 +11,7 @@ mod commands;
 struct Cli {
     /// Toolchain name/version to use (such as stable or 1.59.0).
     #[clap(value_parser = is_toolchain)]
-    toolchain: Option<String>,
+    toolchain: Option<Toolchain>,
     #[clap(subcommand)]
     command: Commands,
 }
@@ -88,7 +88,7 @@ pub fn main() -> cross::Result<()> {
         Commands::Volumes(args) => {
             let mut msg_info = get_msg_info!(args)?;
             let engine = get_engine!(args, args.docker_in_docker(), msg_info)?;
-            args.run(engine, cli.toolchain.as_deref(), &mut msg_info)?;
+            args.run(engine, cli.toolchain.as_ref(), &mut msg_info)?;
         }
         Commands::Containers(args) => {
             let mut msg_info = get_msg_info!(args)?;
diff --git a/src/bin/cross.rs b/src/bin/cross.rs
index f2f8294..64536d2 100644
--- a/src/bin/cross.rs
+++ b/src/bin/cross.rs
@@ -1,10 +1,48 @@
 #![deny(missing_debug_implementations, rust_2018_idioms)]
 
+use std::{
+    env,
+    io::{self, Write},
+};
+
+use cross::{
+    cargo, cli, rustc,
+    shell::{self, Verbosity},
+    OutputExt, Subcommand,
+};
+
 pub fn main() -> cross::Result<()> {
     cross::install_panic_hook()?;
     cross::install_termination_hook()?;
 
-    let status = cross::run()?;
+    let target_list = rustc::target_list(&mut Verbosity::Quiet.into())?;
+    let args = cli::parse(&target_list)?;
+    let subcommand = args.subcommand;
+    let mut msg_info = shell::MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
+    let status = match cross::run(args, target_list, &mut msg_info)? {
+        Some(status) => status,
+        None => {
+            // if we fallback to the host cargo, use the same invocation that was made to cross
+            let argv: Vec<String> = env::args().skip(1).collect();
+            msg_info.note("Falling back to `cargo` on the host.")?;
+            match subcommand {
+                Some(Subcommand::List) => {
+                    // this won't print in order if we have both stdout and stderr.
+                    let out = cargo::run_and_get_output(&argv, &mut msg_info)?;
+                    let stdout = out.stdout()?;
+                    if out.status.success() && cli::is_subcommand_list(&stdout) {
+                        cli::fmt_subcommands(&stdout, &mut msg_info)?;
+                    } else {
+                        // Not a list subcommand, which can happen with weird edge-cases.
+                        print!("{}", stdout);
+                        io::stdout().flush().expect("could not flush");
+                    }
+                    out.status
+                }
+                _ => cargo::run(&argv, &mut msg_info)?,
+            }
+        }
+    };
     let code = status
         .code()
         .ok_or_else(|| eyre::Report::msg("Cargo process terminated by signal"))?;
diff --git a/src/build.rs b/src/build.rs
index 84677cb..fa5c998 100644
--- a/src/build.rs
+++ b/src/build.rs
@@ -1,7 +1,7 @@
 use std::env;
 use std::error::Error;
 use std::fs::File;
-use std::io::{self, Write};
+use std::io::Write;
 use std::path::PathBuf;
 use std::process::Command;
 
@@ -23,11 +23,6 @@ fn main() {
         .unwrap()
         .write_all(commit_info().as_bytes())
         .unwrap();
-
-    File::create(out_dir.join("docker-images.rs"))
-        .unwrap()
-        .write_all(docker_images().as_bytes())
-        .unwrap();
 }
 
 fn commit_info() -> String {
@@ -60,26 +55,3 @@ fn commit_date() -> Result<String, Some> {
         Err(Some {})
     }
 }
-
-fn docker_images() -> String {
-    let mut images = String::from("[");
-    let mut dir = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
-    dir.push("docker");
-
-    let dir = dir.read_dir().unwrap();
-    let mut paths = dir.collect::<io::Result<Vec<_>>>().unwrap();
-    paths.sort_by_key(|e| e.path());
-
-    for entry in paths {
-        let path = entry.path();
-        let file_name = path.file_name().unwrap().to_str().unwrap();
-        if file_name.starts_with("Dockerfile.") {
-            images.push('"');
-            images.push_str(&file_name.replacen("Dockerfile.", "", 1));
-            images.push_str("\", ");
-        }
-    }
-
-    images.push(']');
-    images
-}
diff --git a/src/config.rs b/src/config.rs
index a5d5830..9cc4d13 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,4 +1,5 @@
 use crate::docker::custom::PreBuild;
+use crate::docker::{ImagePlatform, PossibleImage};
 use crate::shell::MessageInfo;
 use crate::{CrossToml, Result, Target, TargetList};
 
@@ -66,8 +67,21 @@ impl Environment {
         self.get_values_for("BUILD_STD", target, bool_from_envvar)
     }
 
-    fn image(&self, target: &Target) -> Option<String> {
+    fn image(&self, target: &Target) -> Result<Option<PossibleImage>> {
         self.get_target_var(target, "IMAGE")
+            .map(Into::into)
+            .map(|mut i: PossibleImage| {
+                if let Some(toolchain) = self.get_target_var(target, "IMAGE_TOOLCHAIN") {
+                    i.toolchain = toolchain
+                        .split(',')
+                        .map(|t| ImagePlatform::from_target(t.trim().into()))
+                        .collect::<Result<Vec<_>>>()?;
+                    Ok(i)
+                } else {
+                    Ok(i)
+                }
+            })
+            .transpose()
     }
 
     fn dockerfile(&self, target: &Target) -> (Option<String>, Option<String>) {
@@ -110,13 +124,21 @@ impl Environment {
     }
 
     fn doctests(&self) -> Option<bool> {
-        env::var("CROSS_UNSTABLE_ENABLE_DOCTESTS")
+        self.get_var("CROSS_UNSTABLE_ENABLE_DOCTESTS")
             .map(|s| bool_from_envvar(&s))
-            .ok()
     }
 
     fn custom_toolchain(&self) -> bool {
-        std::env::var("CROSS_CUSTOM_TOOLCHAIN").is_ok()
+        self.get_var("CROSS_CUSTOM_TOOLCHAIN")
+            .map_or(false, |s| bool_from_envvar(&s))
+    }
+
+    fn custom_toolchain_compat(&self) -> Option<String> {
+        self.get_var("CUSTOM_TOOLCHAIN_COMPAT")
+    }
+
+    fn build_opts(&self) -> Option<String> {
+        self.get_var("CROSS_BUILD_OPTS")
     }
 }
 
@@ -196,21 +218,6 @@ impl Config {
         None
     }
 
-    fn string_from_config(
-        &self,
-        target: &Target,
-        env: impl Fn(&Environment, &Target) -> Option<String>,
-        config: impl Fn(&CrossToml, &Target) -> Option<String>,
-    ) -> Result<Option<String>> {
-        let env_value = env(&self.env, target);
-        if let Some(env_value) = env_value {
-            return Ok(Some(env_value));
-        }
-        self.toml
-            .as_ref()
-            .map_or(Ok(None), |t| Ok(config(t, target)))
-    }
-
     fn vec_from_config(
         &self,
         target: &Target,
@@ -275,12 +282,21 @@ impl Config {
         self.bool_from_config(target, Environment::build_std, CrossToml::build_std)
     }
 
-    pub fn image(&self, target: &Target) -> Result<Option<String>> {
-        self.string_from_config(target, Environment::image, CrossToml::image)
+    pub fn image(&self, target: &Target) -> Result<Option<PossibleImage>> {
+        let env = self.env.image(target)?;
+        self.get_from_ref(
+            target,
+            move |_, _| (None, env.clone()),
+            |toml, target| (None, toml.image(target)),
+        )
     }
 
     pub fn runner(&self, target: &Target) -> Result<Option<String>> {
-        self.string_from_config(target, Environment::runner, CrossToml::runner)
+        self.get_from_ref(
+            target,
+            |env, target| (None, env.runner(target)),
+            |toml, target| (None, toml.runner(target)),
+        )
     }
 
     pub fn doctests(&self) -> Option<bool> {
@@ -291,6 +307,14 @@ impl Config {
         self.env.custom_toolchain()
     }
 
+    pub fn custom_toolchain_compat(&self) -> Option<String> {
+        self.env.custom_toolchain_compat()
+    }
+
+    pub fn build_opts(&self) -> Option<String> {
+        self.env.build_opts()
+    }
+
     pub fn env_passthrough(&self, target: &Target) -> Result<Option<Vec<String>>> {
         self.vec_from_config(
             target,
diff --git a/src/cross_toml.rs b/src/cross_toml.rs
index 819143f..858faf8 100644
--- a/src/cross_toml.rs
+++ b/src/cross_toml.rs
@@ -1,6 +1,7 @@
 #![doc = include_str!("../docs/cross_toml.md")]
 
 use crate::docker::custom::PreBuild;
+use crate::docker::PossibleImage;
 use crate::shell::MessageInfo;
 use crate::{config, errors::*};
 use crate::{Target, TargetList};
@@ -37,7 +38,8 @@ pub struct CrossBuildConfig {
 pub struct CrossTargetConfig {
     xargo: Option<bool>,
     build_std: Option<bool>,
-    image: Option<String>,
+    #[serde(default, deserialize_with = "opt_string_or_struct")]
+    image: Option<PossibleImage>,
     #[serde(default, deserialize_with = "opt_string_or_struct")]
     dockerfile: Option<CrossTargetDockerfileConfig>,
     #[serde(default, deserialize_with = "opt_string_or_string_vec")]
@@ -215,8 +217,8 @@ impl CrossToml {
     }
 
     /// 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())
+    pub fn image(&self, target: &Target) -> Option<&PossibleImage> {
+        self.get_target(target).and_then(|t| t.image.as_ref())
     }
 
     /// Returns the `{}.dockerfile` or `{}.dockerfile.file` part of `Cross.toml`
@@ -259,8 +261,8 @@ impl CrossToml {
     }
 
     /// Returns the `target.{}.runner` part of `Cross.toml`
-    pub fn runner(&self, target: &Target) -> Option<String> {
-        self.get_string(target, |_| None, |t| t.runner.as_ref())
+    pub fn runner(&self, target: &Target) -> Option<&String> {
+        self.get_target(target).and_then(|t| t.runner.as_ref())
     }
 
     /// Returns the `build.xargo` or the `target.{}.xargo` part of `Cross.toml`
@@ -304,18 +306,6 @@ impl CrossToml {
         self.targets.get(target)
     }
 
-    fn get_string<'a>(
-        &'a self,
-        target: &Target,
-        get_build: impl Fn(&'a CrossBuildConfig) -> Option<&'a String>,
-        get_target: impl Fn(&'a CrossTargetConfig) -> Option<&'a String>,
-    ) -> Option<String> {
-        self.get_target(target)
-            .and_then(get_target)
-            .or_else(|| get_build(&self.build))
-            .map(ToOwned::to_owned)
-    }
-
     fn get_value<T>(
         &self,
         target_triple: &Target,
@@ -454,6 +444,8 @@ where
 
 #[cfg(test)]
 mod tests {
+    use crate::docker::ImagePlatform;
+
     use super::*;
     use crate::shell;
 
@@ -463,9 +455,9 @@ mod tests {
         };
     }
 
-    macro_rules! s {
+    macro_rules! p {
         ($x:literal) => {
-            $x.to_owned()
+            $x.parse()?
         };
     }
 
@@ -489,13 +481,13 @@ mod tests {
             targets: HashMap::new(),
             build: CrossBuildConfig {
                 env: CrossEnvConfig {
-                    volumes: Some(vec![s!("VOL1_ARG"), s!("VOL2_ARG")]),
-                    passthrough: Some(vec![s!("VAR1"), s!("VAR2")]),
+                    volumes: Some(vec![p!("VOL1_ARG"), p!("VOL2_ARG")]),
+                    passthrough: Some(vec![p!("VAR1"), p!("VAR2")]),
                 },
                 xargo: Some(true),
                 build_std: None,
                 default_target: None,
-                pre_build: Some(PreBuild::Lines(vec![s!("echo 'Hello World!'")])),
+                pre_build: Some(PreBuild::Lines(vec![p!("echo 'Hello World!'")])),
                 dockerfile: None,
             },
         };
@@ -522,16 +514,16 @@ mod tests {
         let mut target_map = HashMap::new();
         target_map.insert(
             Target::BuiltIn {
-                triple: s!("aarch64-unknown-linux-gnu"),
+                triple: "aarch64-unknown-linux-gnu".into(),
             },
             CrossTargetConfig {
                 env: CrossEnvConfig {
-                    passthrough: Some(vec![s!("VAR1"), s!("VAR2")]),
-                    volumes: Some(vec![s!("VOL1_ARG"), s!("VOL2_ARG")]),
+                    passthrough: Some(vec![p!("VAR1"), p!("VAR2")]),
+                    volumes: Some(vec![p!("VOL1_ARG"), p!("VOL2_ARG")]),
                 },
                 xargo: Some(false),
                 build_std: Some(true),
-                image: Some(s!("test-image")),
+                image: Some("test-image".into()),
                 runner: None,
                 dockerfile: None,
                 pre_build: Some(PreBuild::Lines(vec![])),
@@ -566,22 +558,27 @@ mod tests {
         let mut target_map = HashMap::new();
         target_map.insert(
             Target::BuiltIn {
-                triple: s!("aarch64-unknown-linux-gnu"),
+                triple: "aarch64-unknown-linux-gnu".into(),
             },
             CrossTargetConfig {
                 xargo: Some(false),
                 build_std: None,
-                image: None,
+                image: Some(PossibleImage {
+                    name: "test-image".to_owned(),
+                    toolchain: vec![ImagePlatform::from_target(
+                        "aarch64-unknown-linux-musl".into(),
+                    )?],
+                }),
                 dockerfile: Some(CrossTargetDockerfileConfig {
-                    file: s!("Dockerfile.test"),
+                    file: p!("Dockerfile.test"),
                     context: None,
                     build_args: None,
                 }),
-                pre_build: Some(PreBuild::Lines(vec![s!("echo 'Hello'")])),
+                pre_build: Some(PreBuild::Lines(vec![p!("echo 'Hello'")])),
                 runner: None,
                 env: CrossEnvConfig {
                     passthrough: None,
-                    volumes: Some(vec![s!("VOL")]),
+                    volumes: Some(vec![p!("VOL")]),
                 },
             },
         );
@@ -613,6 +610,8 @@ mod tests {
             xargo = false
             dockerfile = "Dockerfile.test"
             pre-build = ["echo 'Hello'"]
+            image.name = "test-image"
+            image.toolchain = ["aarch64-unknown-linux-musl"]
 
             [target.aarch64-unknown-linux-gnu.env]
             volumes = ["VOL"]
@@ -792,39 +791,39 @@ mod tests {
         let build = &cfg_expected.build;
         assert_eq!(build.build_std, Some(true));
         assert_eq!(build.xargo, Some(false));
-        assert_eq!(build.default_target, Some(s!("aarch64-unknown-linux-gnu")));
+        assert_eq!(build.default_target, Some(p!("aarch64-unknown-linux-gnu")));
         assert_eq!(build.pre_build, None);
         assert_eq!(build.dockerfile, None);
-        assert_eq!(build.env.passthrough, Some(vec![s!("VAR3"), s!("VAR4")]));
+        assert_eq!(build.env.passthrough, Some(vec![p!("VAR3"), p!("VAR4")]));
         assert_eq!(build.env.volumes, Some(vec![]));
 
         let targets = &cfg_expected.targets;
         let aarch64 = &targets[&Target::new_built_in("aarch64-unknown-linux-gnu")];
         assert_eq!(aarch64.build_std, Some(true));
         assert_eq!(aarch64.xargo, Some(false));
-        assert_eq!(aarch64.image, Some(s!("test-image1")));
+        assert_eq!(aarch64.image, Some(p!("test-image1")));
         assert_eq!(aarch64.pre_build, None);
         assert_eq!(aarch64.dockerfile, None);
-        assert_eq!(aarch64.env.passthrough, Some(vec![s!("VAR1")]));
-        assert_eq!(aarch64.env.volumes, Some(vec![s!("VOL1_ARG")]));
+        assert_eq!(aarch64.env.passthrough, Some(vec![p!("VAR1")]));
+        assert_eq!(aarch64.env.volumes, Some(vec![p!("VOL1_ARG")]));
 
         let target2 = &targets[&Target::new_custom("target2")];
         assert_eq!(target2.build_std, Some(false));
         assert_eq!(target2.xargo, Some(false));
-        assert_eq!(target2.image, Some(s!("test-image2-precedence")));
+        assert_eq!(target2.image, Some(p!("test-image2-precedence")));
         assert_eq!(target2.pre_build, None);
         assert_eq!(target2.dockerfile, None);
-        assert_eq!(target2.env.passthrough, Some(vec![s!("VAR2_PRECEDENCE")]));
-        assert_eq!(target2.env.volumes, Some(vec![s!("VOL2_ARG_PRECEDENCE")]));
+        assert_eq!(target2.env.passthrough, Some(vec![p!("VAR2_PRECEDENCE")]));
+        assert_eq!(target2.env.volumes, Some(vec![p!("VOL2_ARG_PRECEDENCE")]));
 
         let target3 = &targets[&Target::new_custom("target3")];
         assert_eq!(target3.build_std, Some(true));
         assert_eq!(target3.xargo, Some(false));
-        assert_eq!(target3.image, Some(s!("test-image3")));
+        assert_eq!(target3.image, Some(p!("test-image3")));
         assert_eq!(target3.pre_build, None);
         assert_eq!(target3.dockerfile, None);
-        assert_eq!(target3.env.passthrough, Some(vec![s!("VAR3")]));
-        assert_eq!(target3.env.volumes, Some(vec![s!("VOL3_ARG")]));
+        assert_eq!(target3.env.passthrough, Some(vec![p!("VAR3")]));
+        assert_eq!(target3.env.volumes, Some(vec![p!("VOL3_ARG")]));
 
         Ok(())
     }
diff --git a/src/docker/custom.rs b/src/docker/custom.rs
index cc6a0b2..847f3f0 100644
--- a/src/docker/custom.rs
+++ b/src/docker/custom.rs
@@ -2,12 +2,12 @@ use std::io::Write;
 use std::path::PathBuf;
 use std::str::FromStr;
 
-use crate::docker::{DockerOptions, DockerPaths};
+use crate::docker::{self, DockerOptions, DockerPaths};
 use crate::shell::MessageInfo;
-use crate::{docker, CargoMetadata, Target};
 use crate::{errors::*, file, CommandExt, ToUtf8};
+use crate::{CargoMetadata, TargetTriple};
 
-use super::{image_name, parse_docker_opts, path_hash};
+use super::{get_image_name, parse_docker_opts, path_hash, ImagePlatform};
 
 pub const CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX: &str = "localhost/cross-rs/cross-custom-";
 
@@ -17,9 +17,11 @@ pub enum Dockerfile<'a> {
         path: &'a str,
         context: Option<&'a str>,
         name: Option<&'a str>,
+        runs_with: &'a ImagePlatform,
     },
     Custom {
         content: String,
+        runs_with: &'a ImagePlatform,
     },
 }
 
@@ -68,9 +70,12 @@ impl<'a> Dockerfile<'a> {
         build_args: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>,
         msg_info: &mut MessageInfo,
     ) -> Result<String> {
-        let mut docker_build = docker::subcommand(&options.engine, "build");
-        docker_build.current_dir(paths.host_root());
+        let mut docker_build = docker::subcommand(&options.engine, "buildx");
+        docker_build.arg("build");
         docker_build.env("DOCKER_SCAN_SUGGEST", "false");
+        self.runs_with()
+            .specify_platform(&options.engine, &mut docker_build);
+
         docker_build.args([
             "--label",
             &format!(
@@ -79,6 +84,14 @@ impl<'a> Dockerfile<'a> {
                 options.target,
             ),
         ]);
+        docker_build.args([
+            "--label",
+            &format!(
+                "{}.runs-with={}",
+                crate::CROSS_LABEL_DOMAIN,
+                self.runs_with().target
+            ),
+        ]);
 
         docker_build.args([
             "--label",
@@ -89,20 +102,20 @@ impl<'a> Dockerfile<'a> {
             ),
         ]);
 
-        let image_name = self.image_name(&options.target, &paths.metadata)?;
+        let image_name = self.image_name(options.target.target(), &paths.metadata)?;
         docker_build.args(["--tag", &image_name]);
 
         for (key, arg) in build_args {
             docker_build.args(["--build-arg", &format!("{}={}", key.as_ref(), arg.as_ref())]);
         }
 
-        if let Some(arch) = options.target.deb_arch() {
+        if let Some(arch) = options.target.target().deb_arch() {
             docker_build.args(["--build-arg", &format!("CROSS_DEB_ARCH={arch}")]);
         }
 
         let path = match self {
             Dockerfile::File { path, .. } => PathBuf::from(path),
-            Dockerfile::Custom { content } => {
+            Dockerfile::Custom { content, .. } => {
                 let path = paths
                     .metadata
                     .target_directory
@@ -117,7 +130,7 @@ impl<'a> Dockerfile<'a> {
         };
 
         if matches!(self, Dockerfile::File { .. }) {
-            if let Ok(cross_base_image) = self::image_name(&options.config, &options.target) {
+            if let Ok(cross_base_image) = self::get_image_name(&options.config, &options.target) {
                 docker_build.args([
                     "--build-arg",
                     &format!("CROSS_BASE_IMAGE={cross_base_image}"),
@@ -127,10 +140,17 @@ impl<'a> Dockerfile<'a> {
 
         docker_build.args(["--file".into(), path]);
 
-        if let Ok(build_opts) = std::env::var("CROSS_BUILD_OPTS") {
-            // FIXME: Use shellwords
+        if let Some(build_opts) = options.config.build_opts() {
             docker_build.args(parse_docker_opts(&build_opts)?);
         }
+
+        let has_output = options.config.build_opts().map_or(false, |opts| {
+            opts.contains("--load") || opts.contains("--output")
+        });
+        if options.engine.kind.is_docker() && !has_output {
+            docker_build.args(&["--output", "type=docker"]);
+        };
+
         if let Some(context) = self.context() {
             docker_build.arg(&context);
         } else {
@@ -141,7 +161,11 @@ 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: &TargetTriple,
+        metadata: &CargoMetadata,
+    ) -> Result<String> {
         match self {
             Dockerfile::File {
                 name: Some(name), ..
@@ -169,6 +193,12 @@ impl<'a> Dockerfile<'a> {
             _ => None,
         }
     }
+    fn runs_with(&self) -> &ImagePlatform {
+        match self {
+            Dockerfile::File { runs_with, .. } => runs_with,
+            Dockerfile::Custom { runs_with, .. } => runs_with,
+        }
+    }
 }
 
 fn docker_package_name(metadata: &CargoMetadata) -> String {
diff --git a/src/docker/engine.rs b/src/docker/engine.rs
index 728f412..cd90e32 100644
--- a/src/docker/engine.rs
+++ b/src/docker/engine.rs
@@ -3,9 +3,11 @@ use std::path::{Path, PathBuf};
 use std::process::Command;
 
 use crate::config::bool_from_envvar;
-use crate::errors::*;
 use crate::extensions::CommandExt;
 use crate::shell::MessageInfo;
+use crate::{errors::*, OutputExt};
+
+use super::{Architecture, ContainerOs};
 
 pub const DOCKER: &str = "docker";
 pub const PODMAN: &str = "podman";
@@ -18,11 +20,27 @@ pub enum EngineType {
     Other,
 }
 
+impl EngineType {
+    /// Returns `true` if the engine type is [`Podman`](Self::Podman) or [`PodmanRemote`](Self::PodmanRemote).
+    #[must_use]
+    pub fn is_podman(&self) -> bool {
+        matches!(self, Self::Podman | Self::PodmanRemote)
+    }
+
+    /// Returns `true` if the engine type is [`Docker`](EngineType::Docker).
+    #[must_use]
+    pub fn is_docker(&self) -> bool {
+        matches!(self, Self::Docker)
+    }
+}
+
 #[derive(Clone, Debug, PartialEq, Eq)]
 pub struct Engine {
     pub kind: EngineType,
     pub path: PathBuf,
     pub in_docker: bool,
+    pub arch: Option<Architecture>,
+    pub os: Option<ContainerOs>,
     pub is_remote: bool,
 }
 
@@ -45,16 +63,18 @@ impl Engine {
         is_remote: Option<bool>,
         msg_info: &mut MessageInfo,
     ) -> Result<Engine> {
-        let kind = get_engine_type(&path, msg_info)?;
         let in_docker = match in_docker {
             Some(v) => v,
             None => Self::in_docker(msg_info)?,
         };
+        let (kind, arch, os) = get_engine_info(&path, msg_info)?;
         let is_remote = is_remote.unwrap_or_else(Self::is_remote);
         Ok(Engine {
             path,
             kind,
             in_docker,
+            arch,
+            os,
             is_remote,
         })
     }
@@ -92,23 +112,118 @@ impl Engine {
 
 // determine if the container engine is docker. this fixes issues with
 // any aliases (#530), and doesn't fail if an executable suffix exists.
-fn get_engine_type(ce: &Path, msg_info: &mut MessageInfo) -> Result<EngineType> {
-    let stdout = Command::new(ce)
+fn get_engine_info(
+    ce: &Path,
+    msg_info: &mut MessageInfo,
+) -> Result<(EngineType, Option<Architecture>, Option<ContainerOs>)> {
+    let stdout_help = Command::new(ce)
         .arg("--help")
         .run_and_get_stdout(msg_info)?
         .to_lowercase();
 
-    if stdout.contains("podman-remote") {
-        Ok(EngineType::PodmanRemote)
-    } else if stdout.contains("podman") {
-        Ok(EngineType::Podman)
-    } else if stdout.contains("docker") && !stdout.contains("emulate") {
-        Ok(EngineType::Docker)
+    let kind = if stdout_help.contains("podman-remote") {
+        EngineType::PodmanRemote
+    } else if stdout_help.contains("podman") {
+        EngineType::Podman
+    } else if stdout_help.contains("docker") && !stdout_help.contains("emulate") {
+        EngineType::Docker
     } else {
-        Ok(EngineType::Other)
+        EngineType::Other
+    };
+
+    // this can fail: podman can give partial output
+    //   linux,,,Error: template: version:1:15: executing "version" at <.Arch>:
+    //   can't evaluate field Arch in type *define.Version
+    let os_arch_server = engine_info(
+        ce,
+        &["version", "-f", "{{ .Server.Os }},,,{{ .Server.Arch }}"],
+        ",,,",
+        msg_info,
+    );
+
+    let (os_arch_other, os_arch_server_result) = match os_arch_server {
+        Ok(Some(os_arch)) => (Ok(Some(os_arch)), None),
+        result => {
+            if kind.is_podman() {
+                (get_podman_info(ce, msg_info), result.err())
+            } else {
+                (get_custom_info(ce, msg_info), result.err())
+            }
+        }
+    };
+
+    let os_arch = match (os_arch_other, os_arch_server_result) {
+        (Ok(os_arch), _) => os_arch,
+        (Err(e), Some(server_err)) => return Err(server_err.to_section_report().with_error(|| e)),
+        (Err(e), None) => return Err(e.to_section_report()),
+    };
+
+    let (os, arch) = os_arch.map_or(<_>::default(), |(os, arch)| (Some(os), Some(arch)));
+    Ok((kind, arch, os))
+}
+
+#[derive(Debug, thiserror::Error)]
+pub enum EngineInfoError {
+    #[error(transparent)]
+    Eyre(eyre::Report),
+    #[error("could not get os and arch")]
+    CommandError(#[from] CommandError),
+}
+
+impl EngineInfoError {
+    pub fn to_section_report(self) -> eyre::Report {
+        match self {
+            EngineInfoError::Eyre(e) => e,
+            EngineInfoError::CommandError(e) => {
+                e.to_section_report().wrap_err("could not get os and arch")
+            }
+        }
     }
 }
 
+/// Get engine info
+fn engine_info(
+    ce: &Path,
+    args: &[&str],
+    sep: &str,
+    msg_info: &mut MessageInfo,
+) -> Result<Option<(ContainerOs, Architecture)>, EngineInfoError> {
+    let mut cmd = Command::new(ce);
+    cmd.args(args);
+    let out = cmd
+        .run_and_get_output(msg_info)
+        .map_err(EngineInfoError::Eyre)?;
+
+    cmd.status_result(msg_info, out.status, Some(&out))?;
+
+    out.stdout()?
+        .to_lowercase()
+        .trim()
+        .split_once(sep)
+        .map(|(os, arch)| -> Result<_> { Ok((ContainerOs::new(os)?, Architecture::new(arch)?)) })
+        .transpose()
+        .map_err(EngineInfoError::Eyre)
+}
+
+fn get_podman_info(
+    ce: &Path,
+    msg_info: &mut MessageInfo,
+) -> Result<Option<(ContainerOs, Architecture)>, EngineInfoError> {
+    engine_info(ce, &["info", "-f", "{{ .Version.OsArch }}"], "/", msg_info)
+}
+
+fn get_custom_info(
+    ce: &Path,
+    msg_info: &mut MessageInfo,
+) -> Result<Option<(ContainerOs, Architecture)>, EngineInfoError> {
+    engine_info(
+        ce,
+        &["version", "-f", "{{ .Client.Os }},,,{{ .Client.Arch }}"],
+        ",,,",
+        msg_info,
+    )
+}
+
 pub fn get_container_engine() -> Result<PathBuf, which::Error> {
     if let Ok(ce) = env::var("CROSS_CONTAINER_ENGINE") {
         which::which(ce)
diff --git a/src/docker/image.rs b/src/docker/image.rs
new file mode 100644
index 0000000..7bfd5b9
--- /dev/null
+++ b/src/docker/image.rs
@@ -0,0 +1,422 @@
+use std::str::FromStr;
+
+use serde::{Deserialize, Serialize};
+
+use crate::{errors::*, shell::MessageInfo, TargetTriple};
+
+use super::Engine;
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct Image {
+    pub name: String,
+    // The toolchain triple the image is built for
+    pub platform: ImagePlatform,
+}
+
+impl std::fmt::Display for Image {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&self.name)
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
+pub struct PossibleImage {
+    pub name: String,
+    // The toolchain triple the image is built for
+    pub toolchain: Vec<ImagePlatform>,
+}
+
+impl PossibleImage {
+    pub(crate) fn to_definite_with(&self, engine: &Engine, msg_info: &mut MessageInfo) -> Image {
+        if self.toolchain.is_empty() {
+            Image {
+                name: self.name.clone(),
+                platform: ImagePlatform::DEFAULT,
+            }
+        } else {
+            let platform = if self.toolchain.len() == 1 {
+                self.toolchain.get(0).expect("should contain at least one")
+            } else {
+                let same_arch = self
+                    .toolchain
+                    .iter()
+                    .filter(|platform| {
+                        &platform.architecture
+                            == engine.arch.as_ref().unwrap_or(&Architecture::Amd64)
+                    })
+                    .collect::<Vec<_>>();
+
+                if same_arch.len() == 1 {
+                    // pick the platform with the same architecture
+                    same_arch.get(0).expect("should contain one element")
+                } else if let Some(platform) = same_arch
+                    .iter()
+                    .find(|platform| &platform.os == engine.os.as_ref().unwrap_or(&Os::Linux))
+                {
+                    *platform
+                } else if let Some(platform) =
+                    same_arch.iter().find(|platform| platform.os == Os::Linux)
+                {
+                    // container engine should be fine with linux
+                    platform
+                } else {
+                    let platform = self
+                        .toolchain
+                        .get(0)
+                        .expect("should be at least one platform");
+                    // FIXME: Don't throw away
+                    msg_info.warn(
+                        format_args!("could not determine what toolchain to use for image, defaulting to `{}`", platform.target),
+                    ).ok();
+                    platform
+                }
+            };
+            Image {
+                platform: platform.clone(),
+                name: self.name.clone(),
+            }
+        }
+    }
+}
+
+impl<T: AsRef<str>> From<T> for PossibleImage {
+    fn from(s: T) -> Self {
+        PossibleImage {
+            name: s.as_ref().to_owned(),
+            toolchain: vec![],
+        }
+    }
+}
+
+impl FromStr for PossibleImage {
+    type Err = std::convert::Infallible;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(s.into())
+    }
+}
+
+impl std::fmt::Display for PossibleImage {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&self.name)
+    }
+}
+/// The architecture/platform to use in the image
+///
+/// https://github.com/containerd/containerd/blob/release/1.6/platforms/platforms.go#L63
+#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
+#[serde(try_from = "&str")]
+pub struct ImagePlatform {
+    /// CPU architecture, x86_64, aarch64 etc
+    pub architecture: Architecture,
+    /// The OS, i.e linux, windows, darwin
+    pub os: Os,
+    /// The platform variant, i.e v8, v7, v6 etc
+    pub variant: Option<String>,
+    pub target: TargetTriple,
+}
+
+impl ImagePlatform {
+    pub const DEFAULT: Self = ImagePlatform::from_const_target(TargetTriple::DEFAULT);
+    pub const X86_64_UNKNOWN_LINUX_GNU: Self =
+        ImagePlatform::from_const_target(TargetTriple::X86_64UnknownLinuxGnu);
+    pub const AARCH64_UNKNOWN_LINUX_GNU: Self =
+        ImagePlatform::from_const_target(TargetTriple::Aarch64UnknownLinuxGnu);
+
+    /// Get a representative version of this platform specifier for usage in `--platform`
+    ///
+    /// Prefer using [`ImagePlatform::specify_platform`] which will supply the flag if needed
+    pub fn docker_platform(&self) -> String {
+        if let Some(variant) = &self.variant {
+            format!("{}/{}/{variant}", self.os, self.architecture)
+        } else {
+            format!("{}/{}", self.os, self.architecture)
+        }
+    }
+}
+
+impl TryFrom<&str> for ImagePlatform {
+    type Error = <Self as std::str::FromStr>::Err;
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        value.parse()
+    }
+}
+
+impl std::str::FromStr for ImagePlatform {
+    type Err = eyre::Report;
+    // [os/arch[/variant]=]toolchain
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        use serde::de::{
+            value::{Error as SerdeError, StrDeserializer},
+            IntoDeserializer,
+        };
+        if let Some((platform, toolchain)) = s.split_once('=') {
+            let image_toolchain = toolchain.into();
+            let (os, arch, variant) = if let Some((os, rest)) = platform.split_once('/') {
+                let os: StrDeserializer<'_, SerdeError> = os.into_deserializer();
+                let (arch, variant) = if let Some((arch, variant)) = rest.split_once('/') {
+                    let arch: StrDeserializer<'_, SerdeError> = arch.into_deserializer();
+                    (arch, Some(variant))
+                } else {
+                    let arch: StrDeserializer<'_, SerdeError> = rest.into_deserializer();
+                    (arch, None)
+                };
+                (os, arch, variant)
+            } else {
+                eyre::bail!("invalid platform specified")
+            };
+            Ok(ImagePlatform {
+                architecture: Architecture::deserialize(arch)?,
+                os: Os::deserialize(os)?,
+                variant: variant.map(ToOwned::to_owned),
+                target: image_toolchain,
+            })
+        } else {
+            Ok(ImagePlatform::from_target(s.into())
+                .wrap_err_with(|| format!("could not map `{s}` to a platform"))?)
+        }
+    }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Architecture {
+    I386,
+    #[serde(alias = "x86_64")]
+    Amd64,
+    #[serde(alias = "armv7")]
+    Arm,
+    #[serde(alias = "aarch64")]
+    Arm64,
+    Mips,
+    Mips64,
+    Mips64Le,
+    MipsLe,
+    Ppc64,
+    Ppc64Le,
+    #[serde(alias = "riscv64gc")]
+    Riscv64,
+    S390x,
+    Wasm,
+}
+
+impl Architecture {
+    pub fn from_target(target: &TargetTriple) -> Result<Self> {
+        let arch = target
+            .triple()
+            .split_once('-')
+            .ok_or_else(|| eyre::eyre!("malformed target"))?
+            .0;
+        Self::new(arch)
+    }
+
+    pub fn new(s: &str) -> Result<Self> {
+        use serde::de::IntoDeserializer;
+
+        Self::deserialize(<&str as IntoDeserializer>::into_deserializer(s))
+            .wrap_err_with(|| format!("architecture {s} is not supported"))
+    }
+}
+
+impl std::fmt::Display for Architecture {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.serialize(f)
+    }
+}
+
+// Supported Oses are on
+// https://rust-lang.github.io/rustup-components-history/aarch64-unknown-linux-gnu.html
+// where rust, rustc and cargo is available (e.g rustup toolchain add works)
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, serde::Serialize, serde::Deserialize)]
+#[serde(rename_all = "lowercase")]
+pub enum Os {
+    Android,
+    #[serde(alias = "macos")]
+    Darwin,
+    Freebsd,
+    Illumos,
+    Linux,
+    Netbsd,
+    Solaris,
+    Windows,
+    // Aix
+    // Dragonfly
+    // Ios
+    // Js
+    // Openbsd
+    // Plan9
+}
+
+impl Os {
+    pub fn from_target(target: &TargetTriple) -> Result<Self> {
+        let mut iter = target.triple().rsplit('-');
+        Ok(
+            match (
+                iter.next().ok_or_else(|| eyre::eyre!("malformed target"))?,
+                iter.next().ok_or_else(|| eyre::eyre!("malformed target"))?,
+            ) {
+                ("darwin", _) => Os::Darwin,
+                ("freebsd", _) => Os::Freebsd,
+                ("netbsd", _) => Os::Netbsd,
+                ("illumos", _) => Os::Illumos,
+                ("solaris", _) => Os::Solaris,
+                // android targets also set linux, so must occur first
+                ("android", _) => Os::Android,
+                (_, "linux") => Os::Linux,
+                (_, "windows") => Os::Windows,
+                (abi, system) => {
+                    eyre::bail!("unsupported os in target, abi: {abi:?}, system: {system:?} ")
+                }
+            },
+        )
+    }
+
+    pub fn new(s: &str) -> Result<Self> {
+        use serde::de::IntoDeserializer;
+
+        Self::deserialize(<&str as IntoDeserializer>::into_deserializer(s))
+            .wrap_err_with(|| format!("architecture {s} is not supported"))
+    }
+}
+
+impl std::fmt::Display for Os {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.serialize(f)
+    }
+}
+
+impl ImagePlatform {
+    pub fn from_target(target: TargetTriple) -> Result<Self> {
+        match target {
+            target @ TargetTriple::Other(_) => {
+                let os = Os::from_target(&target)
+                    .wrap_err("could not determine os in target triplet")?;
+                let architecture = Architecture::from_target(&target)
+                    .wrap_err("could not determine architecture in target triplet")?;
+                let variant = match target.triple() {
+                    // v7 is default for arm architecture, we still specify it for clarity
+                    armv7 if armv7.starts_with("armv7-") => Some("v7".to_owned()),
+                    arm if arm.starts_with("arm-") => Some("v6".to_owned()),
+                    _ => None,
+                };
+                Ok(ImagePlatform {
+                    architecture,
+                    os,
+                    variant,
+                    target,
+                })
+            }
+            target => Ok(Self::from_const_target(target)),
+        }
+    }
+    #[track_caller]
+    pub const fn from_const_target(target: TargetTriple) -> Self {
+        match target {
+            TargetTriple::Other(_) => {
+                unimplemented!()
+            }
+            TargetTriple::X86_64AppleDarwin => ImagePlatform {
+                architecture: Architecture::Amd64,
+                os: Os::Darwin,
+                variant: None,
+                target,
+            },
+            TargetTriple::Aarch64AppleDarwin => ImagePlatform {
+                architecture: Architecture::Arm64,
+                os: Os::Linux,
+                variant: None,
+                target,
+            },
+            TargetTriple::X86_64UnknownLinuxGnu => ImagePlatform {
+                architecture: Architecture::Amd64,
+                os: Os::Linux,
+                variant: None,
+                target,
+            },
+            TargetTriple::Aarch64UnknownLinuxGnu => ImagePlatform {
+                architecture: Architecture::Arm64,
+                os: Os::Linux,
+                variant: None,
+                target,
+            },
+            TargetTriple::X86_64UnknownLinuxMusl => ImagePlatform {
+                architecture: Architecture::Amd64,
+                os: Os::Linux,
+                variant: None,
+                target,
+            },
+            TargetTriple::Aarch64UnknownLinuxMusl => ImagePlatform {
+                architecture: Architecture::Arm,
+                os: Os::Linux,
+                variant: None,
+                target,
+            },
+            TargetTriple::X86_64PcWindowsMsvc => ImagePlatform {
+                architecture: Architecture::Amd64,
+                os: Os::Windows,
+                variant: None,
+                target,
+            },
+        }
+    }
+
+    pub fn specify_platform(&self, engine: &Engine, cmd: &mut std::process::Command) {
+        if self.variant.is_none()
+            && Some(&self.architecture) == engine.arch.as_ref()
+            && Some(&self.os) == engine.os.as_ref()
+        {
+        } else {
+            cmd.args(&["--platform", &self.docker_platform()]);
+        }
+    }
+}
+
+#[cfg(test)]
+pub mod tests {
+    use super::*;
+
+    macro_rules! t {
+        ($t:literal) => {
+            TargetTriple::from($t)
+        };
+    }
+
+    macro_rules! arch {
+        ($t:literal) => {
+            Architecture::from_target(&TargetTriple::from($t))
+        };
+    }
+
+    #[test]
+    fn architecture_from_target() -> Result<()> {
+        assert_eq!(arch!("x86_64-apple-darwin")?, Architecture::Amd64);
+        assert_eq!(arch!("arm-unknown-linux-gnueabihf")?, Architecture::Arm);
+        assert_eq!(arch!("armv7-unknown-linux-gnueabihf")?, Architecture::Arm);
+        assert_eq!(arch!("aarch64-unknown-linux-gnu")?, Architecture::Arm64);
+        assert_eq!(arch!("mips-unknown-linux-gnu")?, Architecture::Mips);
+        assert_eq!(
+            arch!("mips64-unknown-linux-gnuabi64")?,
+            Architecture::Mips64
+        );
+        assert_eq!(
+            arch!("mips64le-unknown-linux-gnuabi64")?,
+            Architecture::Mips64Le
+        );
+        assert_eq!(arch!("mipsle-unknown-linux-gnu")?, Architecture::MipsLe);
+        Ok(())
+    }
+
+    #[test]
+    fn os_from_target() -> Result<()> {
+        assert_eq!(Os::from_target(&t!("x86_64-apple-darwin"))?, Os::Darwin);
+        assert_eq!(Os::from_target(&t!("x86_64-unknown-freebsd"))?, Os::Freebsd);
+        assert_eq!(Os::from_target(&t!("x86_64-unknown-netbsd"))?, Os::Netbsd);
+        assert_eq!(Os::from_target(&t!("sparcv9-sun-solaris"))?, Os::Solaris);
+        assert_eq!(Os::from_target(&t!("sparcv9-sun-illumos"))?, Os::Illumos);
+        assert_eq!(Os::from_target(&t!("aarch64-linux-android"))?, Os::Android);
+        assert_eq!(Os::from_target(&t!("x86_64-unknown-linux-gnu"))?, Os::Linux);
+        assert_eq!(Os::from_target(&t!("x86_64-pc-windows-msvc"))?, Os::Windows);
+        Ok(())
+    }
+}
diff --git a/src/docker/local.rs b/src/docker/local.rs
index 72ecd9c..e11b2bb 100644
--- a/src/docker/local.rs
+++ b/src/docker/local.rs
@@ -22,6 +22,11 @@ pub(crate) fn run(
 
     let mut docker = subcommand(engine, "run");
     docker_userns(&mut docker);
+
+    options
+        .image
+        .platform
+        .specify_platform(&options.engine, &mut docker);
     docker_envvars(&mut docker, &options.config, &options.target, msg_info)?;
 
     docker_mount(
@@ -48,7 +53,10 @@ pub(crate) fn run(
         &format!("{}:{}:Z", dirs.host_root.to_utf8()?, dirs.mount_root),
     ]);
     docker
-        .args(&["-v", &format!("{}:/rust:Z,ro", dirs.sysroot.to_utf8()?)])
+        .args(&[
+            "-v",
+            &format!("{}:/rust:Z,ro", paths.get_sysroot().to_utf8()?),
+        ])
         .args(&["-v", &format!("{}:/target:Z", dirs.target.to_utf8()?)]);
     docker_cwd(&mut docker, &paths)?;
 
@@ -67,15 +75,15 @@ pub(crate) fn run(
             docker.arg("-t");
         }
     }
-    let mut image = options.image_name()?;
+    let mut image_name = options.image.name.clone();
     if options.needs_custom_image() {
-        image = options
+        image_name = options
             .custom_image_build(&paths, msg_info)
             .wrap_err("when building custom image")?;
     }
 
     docker
-        .arg(&image)
+        .arg(&image_name)
         .args(&["sh", "-c", &format!("PATH=$PATH:/rust/bin {:?}", cmd)])
         .run_and_get_status(msg_info, false)
         .map_err(Into::into)
diff --git a/src/docker/mod.rs b/src/docker/mod.rs
index aececd6..cd80b20 100644
--- a/src/docker/mod.rs
+++ b/src/docker/mod.rs
@@ -1,17 +1,44 @@
 pub mod custom;
 mod engine;
+mod image;
 mod local;
+mod provided_images;
 pub mod remote;
 mod shared;
 
 pub use self::engine::*;
+pub use self::provided_images::PROVIDED_IMAGES;
 pub use self::shared::*;
 
+pub use image::{Architecture, Image, ImagePlatform, Os as ContainerOs, PossibleImage};
+
 use std::process::ExitStatus;
 
 use crate::errors::*;
 use crate::shell::MessageInfo;
 
+#[derive(Debug)]
+pub struct ProvidedImage {
+    /// The `name` of the image, usually the target triplet
+    pub name: &'static str,
+    pub platforms: &'static [ImagePlatform],
+    pub sub: Option<&'static str>,
+}
+
+impl ProvidedImage {
+    pub fn image_name(&self, repository: &str, tag: &str) -> String {
+        image_name(self.name, self.sub, repository, tag)
+    }
+}
+
+pub fn image_name(target: &str, sub: Option<&str>, repository: &str, tag: &str) -> String {
+    if let Some(sub) = sub {
+        format!("{repository}/{target}:{tag}-{sub}")
+    } else {
+        format!("{repository}/{target}:{tag}")
+    }
+}
+
 pub fn run(
     options: DockerOptions,
     paths: DockerPaths,
diff --git a/src/docker/provided_images.rs b/src/docker/provided_images.rs
new file mode 100644
index 0000000..7c468a0
--- /dev/null
+++ b/src/docker/provided_images.rs
@@ -0,0 +1,276 @@
+#![doc = "*** AUTO-GENERATED, do not touch. Run `cargo xtask codegen` to update ***"]
+use super::{ImagePlatform, ProvidedImage};
+
+#[rustfmt::skip]
+pub static PROVIDED_IMAGES: &[ProvidedImage] = &[
+        ProvidedImage {
+            name: "x86_64-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-unknown-linux-musl",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: Some("centos")
+        },
+        ProvidedImage {
+            name: "aarch64-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "arm-unknown-linux-gnueabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "arm-unknown-linux-gnueabihf",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv7-unknown-linux-gnueabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv7-unknown-linux-gnueabihf",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "thumbv7neon-unknown-linux-gnueabihf",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i586-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i686-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mips-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mipsel-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mips64-unknown-linux-gnuabi64",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mips64el-unknown-linux-gnuabi64",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mips64-unknown-linux-muslabi64",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mips64el-unknown-linux-muslabi64",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "powerpc-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "powerpc64-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "powerpc64le-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "riscv64gc-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "s390x-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "sparc64-unknown-linux-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "aarch64-unknown-linux-musl",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "arm-unknown-linux-musleabihf",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "arm-unknown-linux-musleabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv5te-unknown-linux-gnueabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv5te-unknown-linux-musleabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv7-unknown-linux-musleabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv7-unknown-linux-musleabihf",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i586-unknown-linux-musl",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i686-unknown-linux-musl",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mips-unknown-linux-musl",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "mipsel-unknown-linux-musl",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "aarch64-linux-android",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "arm-linux-androideabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "armv7-linux-androideabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "thumbv7neon-linux-androideabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i686-linux-android",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-linux-android",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-pc-windows-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i686-pc-windows-gnu",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "wasm32-unknown-emscripten",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-unknown-dragonfly",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "i686-unknown-freebsd",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-unknown-freebsd",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-unknown-netbsd",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "sparcv9-sun-solaris",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-sun-solaris",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "x86_64-unknown-illumos",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "thumbv6m-none-eabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "thumbv7em-none-eabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "thumbv7em-none-eabihf",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+        ProvidedImage {
+            name: "thumbv7m-none-eabi",
+            platforms: &[ImagePlatform::DEFAULT],
+            sub: None
+        },
+];
diff --git a/src/docker/remote.rs b/src/docker/remote.rs
index 88f5c8a..5a97a80 100644
--- a/src/docker/remote.rs
+++ b/src/docker/remote.rs
@@ -14,11 +14,10 @@ use crate::config::bool_from_envvar;
 use crate::errors::Result;
 use crate::extensions::CommandExt;
 use crate::file::{self, PathExt, ToUtf8};
-use crate::rustc::{self, VersionMetaExt};
-use crate::rustup;
+use crate::rustc::{self, QualifiedToolchain, VersionMetaExt};
 use crate::shell::{ColorChoice, MessageInfo, Stream, Verbosity};
 use crate::temp;
-use crate::{Host, Target};
+use crate::{Target, TargetTriple};
 
 // the mount directory for the data volume.
 pub const MOUNT_PREFIX: &str = "/cross";
@@ -448,20 +447,21 @@ fn copy_volume_container_rust_manifest(
 pub fn copy_volume_container_rust_triple(
     engine: &Engine,
     container: &str,
-    sysroot: &Path,
-    triple: &str,
+    toolchain: &QualifiedToolchain,
+    target_triple: &TargetTriple,
     mount_prefix: &Path,
     skip_exists: bool,
     msg_info: &mut MessageInfo,
 ) -> Result<()> {
+    let sysroot = toolchain.get_sysroot();
     // 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);
+    let src_toolchain = sysroot.join(&rustlib).join(target_triple.triple());
+    let dst_toolchain = dst_rustlib.join(target_triple.triple());
 
-    // skip if the toolchain already exists. for the host toolchain
+    // skip if the toolchain target component 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 {
@@ -482,36 +482,47 @@ pub fn copy_volume_container_rust_triple(
 pub fn copy_volume_container_rust(
     engine: &Engine,
     container: &str,
-    sysroot: &Path,
-    target: &Target,
+    toolchain: &QualifiedToolchain,
+    target_triple: Option<&TargetTriple>,
     mount_prefix: &Path,
-    skip_target: bool,
     msg_info: &mut MessageInfo,
 ) -> Result<()> {
-    let target_triple = target.triple();
-    let image_triple = Host::X86_64UnknownLinuxGnu.triple();
-
-    copy_volume_container_rust_base(engine, container, sysroot, mount_prefix, msg_info)?;
-    copy_volume_container_rust_manifest(engine, container, sysroot, mount_prefix, msg_info)?;
+    copy_volume_container_rust_base(
+        engine,
+        container,
+        toolchain.get_sysroot(),
+        mount_prefix,
+        msg_info,
+    )?;
+    copy_volume_container_rust_manifest(
+        engine,
+        container,
+        toolchain.get_sysroot(),
+        mount_prefix,
+        msg_info,
+    )?;
     copy_volume_container_rust_triple(
         engine,
         container,
-        sysroot,
-        image_triple,
+        toolchain,
+        &toolchain.host().target,
         mount_prefix,
         false,
         msg_info,
     )?;
-    if !skip_target && target_triple != image_triple {
-        copy_volume_container_rust_triple(
-            engine,
-            container,
-            sysroot,
-            target_triple,
-            mount_prefix,
-            false,
-            msg_info,
-        )?;
+    // TODO: impl Eq
+    if let Some(target_triple) = target_triple {
+        if target_triple.triple() != toolchain.host().target.triple() {
+            copy_volume_container_rust_triple(
+                engine,
+                container,
+                toolchain,
+                target_triple,
+                mount_prefix,
+                false,
+                msg_info,
+            )?;
+        }
     }
 
     Ok(())
@@ -798,23 +809,26 @@ pub fn container_state(
     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()
-        .expect("should be able to get toolchain name")
-        .to_utf8()?;
-    let toolchain_hash = path_hash(sysroot)?;
-    Ok(format!(
-        "cross-{toolchain_name}-{toolchain_hash}-{commit_hash}"
-    ))
+impl QualifiedToolchain {
+    pub fn unique_toolchain_identifier(&self) -> 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) = self.rustc_version_string()? {
+            rustc::hash_from_version_string(&version, 1)
+        } else {
+            rustc::version_meta()?.commit_hash()
+        };
+
+        let toolchain_name = self
+            .get_sysroot()
+            .file_name()
+            .expect("should be able to get toolchain name")
+            .to_utf8()?;
+        let toolchain_hash = path_hash(self.get_sysroot())?;
+        Ok(format!(
+            "cross-{toolchain_name}-{toolchain_hash}-{commit_hash}"
+        ))
+    }
 }
 
 // unique identifier for a given project
@@ -842,7 +856,7 @@ pub fn unique_container_identifier(
 
     let name = &package.name;
     let triple = target.triple();
-    let toolchain_id = unique_toolchain_identifier(&dirs.sysroot)?;
+    let toolchain_id = dirs.toolchain.unique_toolchain_identifier()?;
     let project_hash = path_hash(&package.manifest_path)?;
     Ok(format!("{toolchain_id}-{triple}-{name}-{project_hash}"))
 }
@@ -886,7 +900,7 @@ pub(crate) fn run(
     // this can happen if we didn't gracefully exit before
     // note that since we use `docker run --rm`, it's very
     // unlikely the container state existed before.
-    let toolchain_id = unique_toolchain_identifier(&dirs.sysroot)?;
+    let toolchain_id = dirs.toolchain.unique_toolchain_identifier()?;
     let container = unique_container_identifier(target, &paths.metadata, dirs)?;
     let volume = VolumeId::create(engine, &toolchain_id, msg_info)?;
     let state = container_state(engine, &container, msg_info)?;
@@ -906,6 +920,10 @@ pub(crate) fn run(
     // 3. create our start container command here
     let mut docker = subcommand(engine, "run");
     docker_userns(&mut docker);
+    options
+        .image
+        .platform
+        .specify_platform(&options.engine, &mut docker);
     docker.args(&["--name", &container]);
     docker.arg("--rm");
     let volume_mount = match volume {
@@ -943,14 +961,16 @@ pub(crate) fn run(
         docker.arg("-t");
     }
 
-    let mut image = options.image_name()?;
+    let mut image_name = options.image.name.clone();
+
     if options.needs_custom_image() {
-        image = options
+        image_name = options
             .custom_image_build(&paths, msg_info)
             .wrap_err("when building custom image")?;
     }
 
-    docker.arg(&image);
+    docker.arg(&image_name);
+
     if !is_tty {
         // ensure the process never exits until we stop it
         // we only need this infinite loop if we don't allocate
@@ -998,10 +1018,9 @@ pub(crate) fn run(
         copy_volume_container_rust(
             engine,
             &container,
-            &dirs.sysroot,
-            target,
+            &dirs.toolchain,
+            Some(target.target()),
             mount_prefix_path,
-            false,
             msg_info,
         )
         .wrap_err("when copying rust")?;
@@ -1010,8 +1029,8 @@ pub(crate) fn run(
         copy_volume_container_rust_triple(
             engine,
             &container,
-            &dirs.sysroot,
-            target.triple(),
+            &dirs.toolchain,
+            target.target(),
             mount_prefix_path,
             true,
             msg_info,
@@ -1045,11 +1064,11 @@ pub(crate) fn run(
         msg_info,
     )
     .wrap_err("when copying project")?;
-
+    let sysroot = dirs.get_sysroot().to_owned();
     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")),
+        (&sysroot, mount_prefix_path.join("rust")),
         (&dirs.host_root, mount_root.clone()),
     ];
     let mut to_symlink = vec![];
diff --git a/src/docker/shared.rs b/src/docker/shared.rs
index 573ba24..7e1459b 100644
--- a/src/docker/shared.rs
+++ b/src/docker/shared.rs
@@ -5,13 +5,16 @@ use std::{env, fs};
 
 use super::custom::{Dockerfile, PreBuild};
 use super::engine::*;
+use super::image::PossibleImage;
+use super::Image;
+use super::PROVIDED_IMAGES;
 use crate::cargo::{cargo_metadata_with_args, CargoMetadata};
 use crate::config::{bool_from_envvar, Config};
 use crate::errors::*;
 use crate::extensions::{CommandExt, SafeCommand};
 use crate::file::{self, write_file, ToUtf8};
 use crate::id;
-use crate::rustc::{self, VersionMetaExt};
+use crate::rustc::QualifiedToolchain;
 use crate::shell::{MessageInfo, Verbosity};
 use crate::Target;
 
@@ -23,7 +26,6 @@ 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:
 // https://docs.docker.com/engine/security/seccomp/#significant-syscalls-blocked-by-the-default-profile
@@ -36,15 +38,23 @@ pub struct DockerOptions {
     pub engine: Engine,
     pub target: Target,
     pub config: Config,
+    pub image: Image,
     pub uses_xargo: bool,
 }
 
 impl DockerOptions {
-    pub fn new(engine: Engine, target: Target, config: Config, uses_xargo: bool) -> DockerOptions {
+    pub fn new(
+        engine: Engine,
+        target: Target,
+        config: Config,
+        image: Image,
+        uses_xargo: bool,
+    ) -> DockerOptions {
         DockerOptions {
             engine,
             target,
             config,
+            image,
             uses_xargo,
         }
     }
@@ -77,19 +87,25 @@ impl DockerOptions {
         paths: &DockerPaths,
         msg_info: &mut MessageInfo,
     ) -> Result<String> {
-        let mut image = self.image_name()?;
+        let mut image = self.image.clone();
 
         if let Some(path) = self.config.dockerfile(&self.target)? {
             let context = self.config.dockerfile_context(&self.target)?;
-            let name = self.config.image(&self.target)?;
+
+            let is_custom_image = self.config.image(&self.target)?.is_some();
 
             let build = Dockerfile::File {
                 path: &path,
                 context: context.as_deref(),
-                name: name.as_deref(),
+                name: if is_custom_image {
+                    Some(&image.name)
+                } else {
+                    None
+                },
+                runs_with: &image.platform,
             };
 
-            image = build
+            image.name = build
                 .build(
                     self,
                     paths,
@@ -122,9 +138,10 @@ impl DockerOptions {
                 RUN chmod +x /pre-build-script
                 RUN ./pre-build-script $CROSS_TARGET"#
                         ),
+                        runs_with: &image.platform,
                     };
 
-                    image = custom
+                    image.name = custom
                         .build(
                             self,
                             paths,
@@ -152,8 +169,9 @@ impl DockerOptions {
                 ARG CROSS_CMD
                 RUN eval "${{CROSS_CMD}}""#
                             ),
+                            runs_with: &image.platform,
                         };
-                        image = custom
+                        image.name = custom
                             .build(
                                 self,
                                 paths,
@@ -166,11 +184,7 @@ impl DockerOptions {
                 }
             }
         }
-        Ok(image)
-    }
-
-    pub(crate) fn image_name(&self) -> Result<String> {
-        image_name(&self.config, &self.target)
+        Ok(image.name.clone())
     }
 }
 
@@ -179,7 +193,6 @@ pub struct DockerPaths {
     pub mount_finder: MountFinder,
     pub metadata: CargoMetadata,
     pub cwd: PathBuf,
-    pub sysroot: PathBuf,
     pub directories: Directories,
 }
 
@@ -188,19 +201,22 @@ impl DockerPaths {
         engine: &Engine,
         metadata: CargoMetadata,
         cwd: PathBuf,
-        sysroot: PathBuf,
+        toolchain: QualifiedToolchain,
     ) -> Result<Self> {
         let mount_finder = MountFinder::create(engine)?;
-        let directories = Directories::create(&mount_finder, &metadata, &cwd, &sysroot)?;
+        let directories = Directories::create(&mount_finder, &metadata, &cwd, toolchain)?;
         Ok(Self {
             mount_finder,
             metadata,
             cwd,
-            sysroot,
             directories,
         })
     }
 
+    pub fn get_sysroot(&self) -> &Path {
+        self.directories.get_sysroot()
+    }
+
     pub fn workspace_root(&self) -> &Path {
         &self.metadata.workspace_root
     }
@@ -239,7 +255,7 @@ pub struct Directories {
     // both mount fields are WSL paths on windows: they already are POSIX paths
     pub mount_root: String,
     pub mount_cwd: String,
-    pub sysroot: PathBuf,
+    pub toolchain: QualifiedToolchain,
 }
 
 impl Directories {
@@ -247,7 +263,7 @@ impl Directories {
         mount_finder: &MountFinder,
         metadata: &CargoMetadata,
         cwd: &Path,
-        sysroot: &Path,
+        mut toolchain: QualifiedToolchain,
     ) -> Result<Self> {
         let home_dir =
             home::home_dir().ok_or_else(|| eyre::eyre!("could not find home directory"))?;
@@ -289,7 +305,8 @@ impl Directories {
             mount_root = host_root.to_utf8()?.to_owned();
         }
         let mount_cwd = mount_finder.find_path(cwd, false)?;
-        let sysroot = mount_finder.find_mount_path(sysroot);
+
+        toolchain.set_sysroot(|p| mount_finder.find_mount_path(p));
 
         Ok(Directories {
             cargo,
@@ -299,9 +316,13 @@ impl Directories {
             host_root,
             mount_root,
             mount_cwd,
-            sysroot,
+            toolchain,
         })
     }
+
+    pub fn get_sysroot(&self) -> &Path {
+        self.toolchain.get_sysroot()
+    }
 }
 
 const CACHEDIR_TAG: &str = "Signature: 8a477f597d28d172789f06886806bc55
@@ -339,23 +360,16 @@ pub fn subcommand(engine: &Engine, cmd: &str) -> Command {
 
 pub fn get_package_info(
     engine: &Engine,
-    target: &str,
-    channel: Option<&str>,
+    toolchain: QualifiedToolchain,
     msg_info: &mut MessageInfo,
-) -> Result<(Target, CargoMetadata, Directories)> {
-    let target_list = msg_info.as_quiet(rustc::target_list)?;
-    let target = Target::from(target, &target_list);
+) -> Result<(CargoMetadata, Directories)> {
     let metadata = cargo_metadata_with_args(None, None, msg_info)?
         .ok_or(eyre::eyre!("unable to get project metadata"))?;
-    let cwd = std::env::current_dir()?;
-    let host_meta = rustc::version_meta()?;
-    let host = host_meta.host();
-
-    let sysroot = rustc::get_sysroot(&host, &target, channel, msg_info)?.1;
     let mount_finder = MountFinder::create(engine)?;
-    let dirs = Directories::create(&mount_finder, &metadata, &cwd, &sysroot)?;
+    let cwd = std::env::current_dir()?;
+    let dirs = Directories::create(&mount_finder, &metadata, &cwd, toolchain)?;
 
-    Ok((target, metadata, dirs))
+    Ok((metadata, dirs))
 }
 
 /// Register binfmt interpreters
@@ -598,7 +612,7 @@ pub(crate) fn docker_seccomp(
 ) -> Result<()> {
     // docker uses seccomp now on all installations
     if target.needs_docker_seccomp() {
-        let seccomp = if engine_type == EngineType::Docker && cfg!(target_os = "windows") {
+        let seccomp = if engine_type.is_docker() && cfg!(target_os = "windows") {
             // docker on windows fails due to a bug in reading the profile
             // https://github.com/docker/for-win/issues/12760
             "unconfined".to_owned()
@@ -626,12 +640,47 @@ pub(crate) fn docker_seccomp(
     Ok(())
 }
 
-pub(crate) fn image_name(config: &Config, target: &Target) -> Result<String> {
+/// Simpler version of [get_image]
+pub fn get_image_name(config: &Config, target: &Target) -> Result<String> {
+    if let Some(image) = config.image(target)? {
+        return Ok(image.name);
+    }
+
+    let compatible = PROVIDED_IMAGES
+        .iter()
+        .filter(|p| p.name == target.triple())
+        .collect::<Vec<_>>();
+
+    if compatible.is_empty() {
+        eyre::bail!(
+            "`cross` does not provide a Docker image for target {target}, \
+                   specify a custom image in `Cross.toml`."
+        );
+    }
+
+    let version = if include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt")).is_empty() {
+        env!("CARGO_PKG_VERSION")
+    } else {
+        "main"
+    };
+
+    Ok(compatible
+        .get(0)
+        .expect("should not be empty")
+        .image_name(CROSS_IMAGE, version))
+}
+
+pub(crate) fn get_image(config: &Config, target: &Target) -> Result<PossibleImage> {
     if let Some(image) = config.image(target)? {
         return Ok(image);
     }
 
-    if !DOCKER_IMAGES.contains(&target.triple()) {
+    let compatible = PROVIDED_IMAGES
+        .iter()
+        .filter(|p| p.name == target.triple())
+        .collect::<Vec<_>>();
+
+    if compatible.is_empty() {
         eyre::bail!(
             "`cross` does not provide a Docker image for target {target}, \
                specify a custom image in `Cross.toml`."
@@ -644,7 +693,47 @@ pub(crate) fn image_name(config: &Config, target: &Target) -> Result<String> {
         "main"
     };
 
-    Ok(format!("{CROSS_IMAGE}/{target}:{version}"))
+    let pick = if compatible.len() == 1 {
+        // If only one match, use that
+        compatible.get(0).expect("should not be empty")
+    } else if compatible
+        .iter()
+        .filter(|provided| provided.sub.is_none())
+        .count()
+        == 1
+    {
+        // if multiple matches, but only one is not a sub-target, pick that one
+        compatible
+            .iter()
+            .find(|provided| provided.sub.is_none())
+            .expect("should exists at least one non-sub image in list")
+    } else {
+        // if there's multiple targets and no option can be chosen, bail
+        return Err(eyre::eyre!(
+            "`cross` provides multiple images for target {target}, \
+               specify toolchain in `Cross.toml`."
+        )
+        .with_note(|| {
+            format!(
+                "candidates: {}",
+                compatible
+                    .iter()
+                    .map(|provided| format!("\"{}\"", provided.image_name(CROSS_IMAGE, version)))
+                    .collect::<Vec<_>>()
+                    .join(", ")
+            )
+        }));
+    };
+
+    let mut image: PossibleImage = pick.image_name(CROSS_IMAGE, version).into();
+
+    eyre::ensure!(
+        !pick.platforms.is_empty(),
+        "platforms for provided image `{image}` are not specified, this is a bug in cross"
+    );
+
+    image.toolchain = pick.platforms.to_vec();
+    Ok(image)
 }
 
 fn docker_read_mount_paths(engine: &Engine) -> Result<Vec<MountDetail>> {
@@ -930,11 +1019,17 @@ mod tests {
             Ok(path)
         }
 
-        fn get_sysroot() -> Result<PathBuf> {
-            Ok(home()?
+        fn get_toolchain() -> Result<QualifiedToolchain> {
+            let sysroot = home()?
                 .join(".rustup")
                 .join("toolchains")
-                .join("stable-x86_64-unknown-linux-gnu"))
+                .join("stable-x86_64-unknown-linux-gnu");
+            Ok(QualifiedToolchain::new(
+                "stable",
+                &None,
+                &crate::docker::ImagePlatform::X86_64_UNKNOWN_LINUX_GNU,
+                &sysroot,
+            ))
         }
 
         fn get_directories(
@@ -942,8 +1037,8 @@ mod tests {
             mount_finder: &MountFinder,
         ) -> Result<Directories> {
             let cwd = get_cwd()?;
-            let sysroot = get_sysroot()?;
-            Directories::create(mount_finder, metadata, &cwd, &sysroot)
+            let toolchain = get_toolchain()?;
+            Directories::create(mount_finder, metadata, &cwd, toolchain)
         }
 
         fn path_to_posix(path: &Path) -> Result<String> {
diff --git a/src/lib.rs b/src/lib.rs
index 31f6d8f..58dd6dd 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -30,27 +30,30 @@
 #[cfg(test)]
 mod tests;
 
-mod cargo;
-mod cli;
-mod config;
+pub mod cargo;
+pub mod cli;
+pub mod config;
 mod cross_toml;
 pub mod docker;
 pub mod errors;
 mod extensions;
-mod file;
+pub mod file;
 mod id;
 mod interpreter;
 pub mod rustc;
-mod rustup;
+pub mod rustup;
 pub mod shell;
 pub mod temp;
 
 use std::env;
-use std::io::{self, Write};
 use std::path::PathBuf;
 use std::process::ExitStatus;
 
+use cli::Args;
+use color_eyre::owo_colors::OwoColorize;
+use color_eyre::{Help, SectionExt};
 use config::Config;
+use rustc::{QualifiedToolchain, Toolchain};
 use rustc_version::Channel;
 use serde::{Deserialize, Serialize, Serializer};
 
@@ -67,8 +70,10 @@ pub use self::rustc::{TargetList, VersionMetaExt};
 pub const CROSS_LABEL_DOMAIN: &str = "org.cross-rs";
 
 #[allow(non_camel_case_types)]
-#[derive(Debug, Clone, PartialEq, Eq)]
-pub enum Host {
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Hash, Serialize)]
+#[serde(from = "&str")]
+#[serde(rename_all = "snake_case")]
+pub enum TargetTriple {
     Other(String),
 
     // OSX
@@ -92,7 +97,75 @@ pub enum Host {
     X86_64PcWindowsMsvc,
 }
 
-impl Host {
+impl TargetTriple {
+    pub const DEFAULT: Self = Self::X86_64UnknownLinuxGnu;
+    /// Returns the architecture name according to `dpkg` naming convention
+    ///
+    /// # Notes
+    ///
+    /// Some of these make no sense to use in our standard images
+    pub fn deb_arch(&self) -> Option<&'static str> {
+        match self.triple() {
+            "aarch64-unknown-linux-gnu" => Some("arm64"),
+            "aarch64-unknown-linux-musl" => Some("musl-linux-arm64"),
+            "aarch64-linux-android" => None,
+            "x86_64-unknown-linux-gnu" => Some("amd64"),
+            "x86_64-apple-darwin" => Some("darwin-amd64"),
+            "x86_64-unknown-linux-musl" => Some("musl-linux-amd64"),
+
+            "x86_64-pc-windows-msvc" => None,
+            "arm-unknown-linux-gnueabi" => Some("armel"),
+            "arm-unknown-linux-gnueabihf" => Some("armhf"),
+            "armv7-unknown-linux-gnueabi" => Some("armel"),
+            "armv7-unknown-linux-gnueabihf" => Some("armhf"),
+            "thumbv7neon-unknown-linux-gnueabihf" => Some("armhf"),
+            "i586-unknown-linux-gnu" => Some("i386"),
+            "i686-unknown-linux-gnu" => Some("i386"),
+            "mips-unknown-linux-gnu" => Some("mips"),
+            "mipsel-unknown-linux-gnu" => Some("mipsel"),
+            "mips64-unknown-linux-gnuabi64" => Some("mips64"),
+            "mips64el-unknown-linux-gnuabi64" => Some("mips64el"),
+            "mips64-unknown-linux-muslabi64" => Some("musl-linux-mips64"),
+            "mips64el-unknown-linux-muslabi64" => Some("musl-linux-mips64el"),
+            "powerpc-unknown-linux-gnu" => Some("powerpc"),
+            "powerpc64-unknown-linux-gnu" => Some("ppc64"),
+            "powerpc64le-unknown-linux-gnu" => Some("ppc64el"),
+            "riscv64gc-unknown-linux-gnu" => Some("riscv64"),
+            "s390x-unknown-linux-gnu" => Some("s390x"),
+            "sparc64-unknown-linux-gnu" => Some("sparc64"),
+            "arm-unknown-linux-musleabihf" => Some("musl-linux-armhf"),
+            "arm-unknown-linux-musleabi" => Some("musl-linux-arm"),
+            "armv5te-unknown-linux-gnueabi" => None,
+            "armv5te-unknown-linux-musleabi" => None,
+            "armv7-unknown-linux-musleabi" => Some("musl-linux-arm"),
+            "armv7-unknown-linux-musleabihf" => Some("musl-linux-armhf"),
+            "i586-unknown-linux-musl" => Some("musl-linux-i386"),
+            "i686-unknown-linux-musl" => Some("musl-linux-i386"),
+            "mips-unknown-linux-musl" => Some("musl-linux-mips"),
+            "mipsel-unknown-linux-musl" => Some("musl-linux-mipsel"),
+            "arm-linux-androideabi" => None,
+            "armv7-linux-androideabi" => None,
+            "thumbv7neon-linux-androideabi" => None,
+            "i686-linux-android" => None,
+            "x86_64-linux-android" => None,
+            "x86_64-pc-windows-gnu" => None,
+            "i686-pc-windows-gnu" => None,
+            "asmjs-unknown-emscripten" => None,
+            "wasm32-unknown-emscripten" => None,
+            "x86_64-unknown-dragonfly" => Some("dragonflybsd-amd64"),
+            "i686-unknown-freebsd" => Some("freebsd-i386"),
+            "x86_64-unknown-freebsd" => Some("freebsd-amd64"),
+            "x86_64-unknown-netbsd" => Some("netbsd-amd64"),
+            "sparcv9-sun-solaris" => Some("solaris-sparc"),
+            "x86_64-sun-solaris" => Some("solaris-amd64"),
+            "thumbv6m-none-eabi" => Some("arm"),
+            "thumbv7em-none-eabi" => Some("arm"),
+            "thumbv7em-none-eabihf" => Some("armhf"),
+            "thumbv7m-none-eabi" => Some("arm"),
+            _ => None,
+        }
+    }
+
     /// Checks if this `(host, target)` pair is supported by `cross`
     ///
     /// `target == None` means `target == host`
@@ -104,17 +177,19 @@ impl Host {
             // Old behavior (up to cross version 0.2.1) can be activated on demand using environment
             // variable `CROSS_COMPATIBILITY_VERSION`.
             Ok("0.2.1") => match self {
-                Host::X86_64AppleDarwin | Host::Aarch64AppleDarwin => {
+                TargetTriple::X86_64AppleDarwin | TargetTriple::Aarch64AppleDarwin => {
                     target.map_or(false, |t| t.needs_docker())
                 }
-                Host::X86_64UnknownLinuxGnu
-                | Host::Aarch64UnknownLinuxGnu
-                | Host::X86_64UnknownLinuxMusl
-                | Host::Aarch64UnknownLinuxMusl => target.map_or(true, |t| t.needs_docker()),
-                Host::X86_64PcWindowsMsvc => target.map_or(false, |t| {
-                    t.triple() != Host::X86_64PcWindowsMsvc.triple() && t.needs_docker()
+                TargetTriple::X86_64UnknownLinuxGnu
+                | TargetTriple::Aarch64UnknownLinuxGnu
+                | TargetTriple::X86_64UnknownLinuxMusl
+                | TargetTriple::Aarch64UnknownLinuxMusl => {
+                    target.map_or(true, |t| t.needs_docker())
+                }
+                TargetTriple::X86_64PcWindowsMsvc => target.map_or(false, |t| {
+                    t.triple() != TargetTriple::X86_64PcWindowsMsvc.triple() && t.needs_docker()
                 }),
-                Host::Other(_) => false,
+                TargetTriple::Other(_) => false,
             },
             // New behaviour, if a target is provided (--target ...) then always run with docker
             // image unless the target explicitly opts-out (i.e. unless needs_docker() returns false).
@@ -133,54 +208,85 @@ impl Host {
     /// Returns the [`Target`] as target triple string
     pub fn triple(&self) -> &str {
         match self {
-            Host::X86_64AppleDarwin => "x86_64-apple-darwin",
-            Host::Aarch64AppleDarwin => "aarch64-apple-darwin",
-            Host::X86_64UnknownLinuxGnu => "x86_64-unknown-linux-gnu",
-            Host::Aarch64UnknownLinuxGnu => "aarch64-unknown-linux-gnu",
-            Host::X86_64UnknownLinuxMusl => "x86_64-unknown-linux-musl",
-            Host::Aarch64UnknownLinuxMusl => "aarch64-unknown-linux-musl",
-            Host::X86_64PcWindowsMsvc => "x86_64-pc-windows-msvc",
-            Host::Other(s) => s.as_str(),
+            TargetTriple::X86_64AppleDarwin => "x86_64-apple-darwin",
+            TargetTriple::Aarch64AppleDarwin => "aarch64-apple-darwin",
+            TargetTriple::X86_64UnknownLinuxGnu => "x86_64-unknown-linux-gnu",
+            TargetTriple::Aarch64UnknownLinuxGnu => "aarch64-unknown-linux-gnu",
+            TargetTriple::X86_64UnknownLinuxMusl => "x86_64-unknown-linux-musl",
+            TargetTriple::Aarch64UnknownLinuxMusl => "aarch64-unknown-linux-musl",
+            TargetTriple::X86_64PcWindowsMsvc => "x86_64-pc-windows-msvc",
+            TargetTriple::Other(s) => s.as_str(),
         }
     }
 }
 
-impl<'a> From<&'a str> for Host {
-    fn from(s: &str) -> Host {
+impl<'a> From<&'a str> for TargetTriple {
+    fn from(s: &str) -> TargetTriple {
         match s {
-            "x86_64-apple-darwin" => Host::X86_64AppleDarwin,
-            "x86_64-unknown-linux-gnu" => Host::X86_64UnknownLinuxGnu,
-            "x86_64-unknown-linux-musl" => Host::X86_64UnknownLinuxMusl,
-            "x86_64-pc-windows-msvc" => Host::X86_64PcWindowsMsvc,
-            "aarch64-apple-darwin" => Host::Aarch64AppleDarwin,
-            "aarch64-unknown-linux-gnu" => Host::Aarch64UnknownLinuxGnu,
-            "aarch64-unknown-linux-musl" => Host::Aarch64UnknownLinuxMusl,
-            s => Host::Other(s.to_owned()),
+            "x86_64-apple-darwin" => TargetTriple::X86_64AppleDarwin,
+            "x86_64-unknown-linux-gnu" => TargetTriple::X86_64UnknownLinuxGnu,
+            "x86_64-unknown-linux-musl" => TargetTriple::X86_64UnknownLinuxMusl,
+            "x86_64-pc-windows-msvc" => TargetTriple::X86_64PcWindowsMsvc,
+            "aarch64-apple-darwin" => TargetTriple::Aarch64AppleDarwin,
+            "aarch64-unknown-linux-gnu" => TargetTriple::Aarch64UnknownLinuxGnu,
+            "aarch64-unknown-linux-musl" => TargetTriple::Aarch64UnknownLinuxMusl,
+            s => TargetTriple::Other(s.to_owned()),
         }
     }
 }
 
+impl std::str::FromStr for TargetTriple {
+    type Err = std::convert::Infallible;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        Ok(s.into())
+    }
+}
+
+impl std::fmt::Display for TargetTriple {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(self.triple())
+    }
+}
+
+impl From<String> for TargetTriple {
+    fn from(s: String) -> TargetTriple {
+        s.as_str().into()
+    }
+}
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize)]
 #[serde(from = "String")]
 pub enum Target {
-    BuiltIn { triple: String },
-    Custom { triple: String },
+    BuiltIn { triple: TargetTriple },
+    Custom { triple: TargetTriple },
 }
 
 impl Target {
+    pub const DEFAULT: Self = Self::BuiltIn {
+        triple: TargetTriple::DEFAULT,
+    };
+
     fn new_built_in(triple: &str) -> Self {
         Target::BuiltIn {
-            triple: triple.to_owned(),
+            triple: triple.into(),
         }
     }
 
     fn new_custom(triple: &str) -> Self {
         Target::Custom {
-            triple: triple.to_owned(),
+            triple: triple.into(),
         }
     }
 
-    fn triple(&self) -> &str {
+    pub fn triple(&self) -> &str {
+        match *self {
+            Target::BuiltIn { ref triple } => triple.triple(),
+            Target::Custom { ref triple } => triple.triple(),
+        }
+    }
+
+    pub fn target(&self) -> &TargetTriple {
         match *self {
             Target::BuiltIn { ref triple } => triple,
             Target::Custom { ref triple } => triple,
@@ -259,73 +365,6 @@ impl Target {
 
         arch_32bit && self.is_android()
     }
-
-    /// Returns the architecture name according to `dpkg` naming convention
-    ///
-    /// # Notes
-    ///
-    /// Some of these make no sense to use in our standard images
-    pub fn deb_arch(&self) -> Option<&'static str> {
-        match self.triple() {
-            "aarch64-unknown-linux-gnu" => Some("arm64"),
-            "aarch64-unknown-linux-musl" => Some("musl-linux-arm64"),
-            "aarch64-linux-android" => None,
-            "x86_64-unknown-linux-gnu" => Some("amd64"),
-            "x86_64-apple-darwin" => Some("darwin-amd64"),
-            "x86_64-unknown-linux-musl" => Some("musl-linux-amd64"),
-
-            "x86_64-pc-windows-msvc" => None,
-            "arm-unknown-linux-gnueabi" => Some("armel"),
-            "arm-unknown-linux-gnueabihf" => Some("armhf"),
-            "armv7-unknown-linux-gnueabi" => Some("armel"),
-            "armv7-unknown-linux-gnueabihf" => Some("armhf"),
-            "thumbv7neon-unknown-linux-gnueabihf" => Some("armhf"),
-            "i586-unknown-linux-gnu" => Some("i386"),
-            "i686-unknown-linux-gnu" => Some("i386"),
-            "mips-unknown-linux-gnu" => Some("mips"),
-            "mipsel-unknown-linux-gnu" => Some("mipsel"),
-            "mips64-unknown-linux-gnuabi64" => Some("mips64"),
-            "mips64el-unknown-linux-gnuabi64" => Some("mips64el"),
-            "mips64-unknown-linux-muslabi64" => Some("musl-linux-mips64"),
-            "mips64el-unknown-linux-muslabi64" => Some("musl-linux-mips64el"),
-            "powerpc-unknown-linux-gnu" => Some("powerpc"),
-            "powerpc64-unknown-linux-gnu" => Some("ppc64"),
-            "powerpc64le-unknown-linux-gnu" => Some("ppc64el"),
-            "riscv64gc-unknown-linux-gnu" => Some("riscv64"),
-            "s390x-unknown-linux-gnu" => Some("s390x"),
-            "sparc64-unknown-linux-gnu" => Some("sparc64"),
-            "arm-unknown-linux-musleabihf" => Some("musl-linux-armhf"),
-            "arm-unknown-linux-musleabi" => Some("musl-linux-arm"),
-            "armv5te-unknown-linux-gnueabi" => None,
-            "armv5te-unknown-linux-musleabi" => None,
-            "armv7-unknown-linux-musleabi" => Some("musl-linux-arm"),
-            "armv7-unknown-linux-musleabihf" => Some("musl-linux-armhf"),
-            "i586-unknown-linux-musl" => Some("musl-linux-i386"),
-            "i686-unknown-linux-musl" => Some("musl-linux-i386"),
-            "mips-unknown-linux-musl" => Some("musl-linux-mips"),
-            "mipsel-unknown-linux-musl" => Some("musl-linux-mipsel"),
-            "arm-linux-androideabi" => None,
-            "armv7-linux-androideabi" => None,
-            "thumbv7neon-linux-androideabi" => None,
-            "i686-linux-android" => None,
-            "x86_64-linux-android" => None,
-            "x86_64-pc-windows-gnu" => None,
-            "i686-pc-windows-gnu" => None,
-            "asmjs-unknown-emscripten" => None,
-            "wasm32-unknown-emscripten" => None,
-            "x86_64-unknown-dragonfly" => Some("dragonflybsd-amd64"),
-            "i686-unknown-freebsd" => Some("freebsd-i386"),
-            "x86_64-unknown-freebsd" => Some("freebsd-amd64"),
-            "x86_64-unknown-netbsd" => Some("netbsd-amd64"),
-            "sparcv9-sun-solaris" => Some("solaris-sparc"),
-            "x86_64-sun-solaris" => Some("solaris-amd64"),
-            "thumbv6m-none-eabi" => Some("arm"),
-            "thumbv7em-none-eabi" => Some("arm"),
-            "thumbv7em-none-eabihf" => Some("armhf"),
-            "thumbv7m-none-eabi" => Some("arm"),
-            _ => None,
-        }
-    }
 }
 
 impl std::fmt::Display for Target {
@@ -344,17 +383,23 @@ impl Target {
     }
 }
 
-impl From<Host> for Target {
-    fn from(host: Host) -> Target {
+impl From<TargetTriple> for Target {
+    fn from(host: TargetTriple) -> Target {
         match host {
-            Host::X86_64UnknownLinuxGnu => Target::new_built_in("x86_64-unknown-linux-gnu"),
-            Host::X86_64UnknownLinuxMusl => Target::new_built_in("x86_64-unknown-linux-musl"),
-            Host::X86_64AppleDarwin => Target::new_built_in("x86_64-apple-darwin"),
-            Host::X86_64PcWindowsMsvc => Target::new_built_in("x86_64-pc-windows-msvc"),
-            Host::Aarch64AppleDarwin => Target::new_built_in("aarch64-apple-darwin"),
-            Host::Aarch64UnknownLinuxGnu => Target::new_built_in("aarch64-unknown-linux-gnu"),
-            Host::Aarch64UnknownLinuxMusl => Target::new_built_in("aarch64-unknown-linux-musl"),
-            Host::Other(s) => Target::from(
+            TargetTriple::X86_64UnknownLinuxGnu => Target::new_built_in("x86_64-unknown-linux-gnu"),
+            TargetTriple::X86_64UnknownLinuxMusl => {
+                Target::new_built_in("x86_64-unknown-linux-musl")
+            }
+            TargetTriple::X86_64AppleDarwin => Target::new_built_in("x86_64-apple-darwin"),
+            TargetTriple::X86_64PcWindowsMsvc => Target::new_built_in("x86_64-pc-windows-msvc"),
+            TargetTriple::Aarch64AppleDarwin => Target::new_built_in("aarch64-apple-darwin"),
+            TargetTriple::Aarch64UnknownLinuxGnu => {
+                Target::new_built_in("aarch64-unknown-linux-gnu")
+            }
+            TargetTriple::Aarch64UnknownLinuxMusl => {
+                Target::new_built_in("aarch64-unknown-linux-musl")
+            }
+            TargetTriple::Other(s) => Target::from(
                 s.as_str(),
                 &rustc::target_list(&mut Verbosity::Quiet.into())
                     .expect("should be able to query rustc"),
@@ -365,21 +410,22 @@ 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();
+        let target_host: TargetTriple = target_str.as_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),
-        }
+        serializer.serialize_str(self.triple())
     }
 }
 
-fn warn_on_failure(target: &Target, toolchain: &str, msg_info: &mut MessageInfo) -> Result<()> {
+fn warn_on_failure(
+    target: &Target,
+    toolchain: &QualifiedToolchain,
+    msg_info: &mut MessageInfo,
+) -> Result<()> {
     let rust_std = format!("rust-std-{target}");
     if target.is_builtin() {
         let component = rustup::check_component(&rust_std, toolchain, msg_info)?;
@@ -395,12 +441,11 @@ fn warn_on_failure(target: &Target, toolchain: &str, msg_info: &mut MessageInfo)
     }
     Ok(())
 }
-
-pub fn run() -> Result<ExitStatus> {
-    let target_list = rustc::target_list(&mut Verbosity::Quiet.into())?;
-    let args = cli::parse(&target_list)?;
-    let mut msg_info = shell::MessageInfo::create(args.verbose, args.quiet, args.color.as_deref())?;
-
+pub fn run(
+    args: Args,
+    target_list: TargetList,
+    msg_info: &mut MessageInfo,
+) -> Result<Option<ExitStatus>> {
     if args.version && args.subcommand.is_none() {
         let commit_info = include_str!(concat!(env!("OUT_DIR"), "/commit-info.txt"));
         msg_info.print(format!(
@@ -410,54 +455,110 @@ pub fn run() -> Result<ExitStatus> {
     }
 
     let host_version_meta = rustc::version_meta()?;
+
     let cwd = std::env::current_dir()?;
-    if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), &mut msg_info)? {
+    if let Some(metadata) = cargo_metadata_with_args(None, Some(&args), msg_info)? {
         let host = host_version_meta.host();
-        let toml = toml(&metadata, &mut msg_info)?;
+        let toml = toml(&metadata, msg_info)?;
         let config = Config::new(toml);
         let target = args
             .target
             .or_else(|| config.target(&target_list))
             .unwrap_or_else(|| Target::from(host.triple(), &target_list));
-        config.confusable_target(&target, &mut msg_info)?;
+        config.confusable_target(&target, msg_info)?;
 
-        let image_exists = match docker::image_name(&config, &target) {
-            Ok(_) => true,
+        // Get the image we're supposed to base all our next actions on.
+        // The image we actually run in might get changed with
+        // `target.{{TARGET}}.dockerfile` or `target.{{TARGET}}.pre-build`
+        let image = match docker::get_image(&config, &target) {
+            Ok(i) => i,
             Err(err) => {
                 msg_info.warn(err)?;
-                false
+
+                return Ok(None);
+            }
+        };
+
+        // Grab the current toolchain, this might be the one we mount in the image later
+        let default_toolchain = QualifiedToolchain::default(&config, msg_info)?;
+
+        // `cross +channel`, where channel can be `+channel[-YYYY-MM-DD]`
+        let mut toolchain = if let Some(channel) = args.channel {
+            let picked_toolchain: Toolchain = channel.parse()?;
+
+            if let Some(picked_host) = &picked_toolchain.host {
+                return Err(eyre::eyre!("the specified toolchain `{picked_toolchain}` can't be used"))
+                    .with_suggestion(|| {
+                        format!(
+                            "try `cross +{}` instead",
+                            Toolchain {
+                                host: None,
+                                ..picked_toolchain
+                            }
+                        )
+                    }).with_section(|| format!(
+r#"Overriding the toolchain in cross is only possible in CLI by specifying a channel and optional date: `+channel[-YYYY-MM-DD]`.
+To override the toolchain mounted in the image, set `target.{}.image.toolchain = "{picked_host}"`"#, target).header("Note:".bright_cyan()));
             }
+
+            default_toolchain.with_picked(&config, picked_toolchain, msg_info)?
+        } else {
+            default_toolchain
         };
 
-        if image_exists && host.is_supported(Some(&target)) {
-            let (toolchain, sysroot) =
-                rustc::get_sysroot(&host, &target, args.channel.as_deref(), &mut msg_info)?;
-            let mut is_nightly = toolchain.contains("nightly");
+        let is_remote = docker::Engine::is_remote();
+        let engine = docker::Engine::new(None, Some(is_remote), msg_info)?;
+
+        let image = image.to_definite_with(&engine, msg_info);
 
-            let installed_toolchains = rustup::installed_toolchains(&mut msg_info)?;
+        toolchain.replace_host(&image.platform);
 
-            if !installed_toolchains.into_iter().any(|t| t == toolchain) {
-                rustup::install_toolchain(&toolchain, &mut msg_info)?;
+        if image.platform.target.is_supported(Some(&target)) {
+            if image.platform.architecture != toolchain.host().architecture {
+                msg_info.warn(format_args!(
+                    "toolchain `{toolchain}` may not run on image `{image}`"
+                ))?;
             }
-            // TODO: Provide a way to pick/match the toolchain version as a consumer of `cross`.
-            if let Some((rustc_version, channel, rustc_commit)) = rustup::rustc_version(&sysroot)? {
-                warn_host_version_mismatch(
-                    &host_version_meta,
-                    &toolchain,
-                    &rustc_version,
-                    &rustc_commit,
-                    &mut msg_info,
-                )?;
+            // set the sysroot explicitly to the toolchain
+            let mut is_nightly = toolchain.channel.contains("nightly");
+
+            let installed_toolchains = rustup::installed_toolchains(msg_info)?;
+
+            if !installed_toolchains
+                .into_iter()
+                .any(|t| t == toolchain.to_string())
+            {
+                rustup::install_toolchain(&toolchain, msg_info)?;
+            }
+            let available_targets = if !toolchain.is_custom {
+                rustup::available_targets(&toolchain.full, msg_info)?
+            } else {
+                rustup::AvailableTargets {
+                    default: String::new(),
+                    installed: vec![],
+                    not_installed: vec![],
+                }
+            };
+
+            if let Some((rustc_version, channel, rustc_commit)) = toolchain.rustc_version()? {
+                if toolchain.date.is_none() {
+                    warn_host_version_mismatch(
+                        &host_version_meta,
+                        &toolchain,
+                        &rustc_version,
+                        &rustc_commit,
+                        msg_info,
+                    )?;
+                }
                 is_nightly = channel == Channel::Nightly;
             }
 
             let uses_build_std = config.build_std(&target).unwrap_or(false);
             let uses_xargo =
                 !uses_build_std && config.xargo(&target).unwrap_or(!target.is_builtin());
-            if !config.custom_toolchain() {
+            if !toolchain.is_custom {
                 // build-std overrides xargo, but only use it if it's a built-in
                 // tool but not an available target or doesn't have rust-std.
-                let available_targets = rustup::available_targets(&toolchain, &mut msg_info)?;
 
                 if !is_nightly && uses_build_std {
                     eyre::bail!(
@@ -470,14 +571,14 @@ pub fn run() -> Result<ExitStatus> {
                     && !available_targets.is_installed(&target)
                     && available_targets.contains(&target)
                 {
-                    rustup::install(&target, &toolchain, &mut msg_info)?;
-                } else if !rustup::component_is_installed("rust-src", &toolchain, &mut msg_info)? {
-                    rustup::install_component("rust-src", &toolchain, &mut msg_info)?;
+                    rustup::install(&target, &toolchain, msg_info)?;
+                } else if !rustup::component_is_installed("rust-src", &toolchain, msg_info)? {
+                    rustup::install_component("rust-src", &toolchain, msg_info)?;
                 }
                 if args.subcommand.map_or(false, |sc| sc == Subcommand::Clippy)
-                    && !rustup::component_is_installed("clippy", &toolchain, &mut msg_info)?
+                    && !rustup::component_is_installed("clippy", &toolchain, msg_info)?
                 {
-                    rustup::install_component("clippy", &toolchain, &mut msg_info)?;
+                    rustup::install_component("clippy", &toolchain, msg_info)?;
                 }
             }
 
@@ -517,55 +618,34 @@ pub fn run() -> Result<ExitStatus> {
                 filtered_args.push("-Zbuild-std".to_owned());
             }
 
-            let is_remote = docker::Engine::is_remote();
             let needs_docker = args
                 .subcommand
                 .map_or(false, |sc| sc.needs_docker(is_remote));
             if target.needs_docker() && needs_docker {
-                let engine = docker::Engine::new(None, Some(is_remote), &mut msg_info)?;
                 if host_version_meta.needs_interpreter()
                     && needs_interpreter
                     && target.needs_interpreter()
                     && !interpreter::is_registered(&target)?
                 {
-                    docker::register(&engine, &target, &mut msg_info)?;
+                    docker::register(&engine, &target, msg_info)?;
                 }
 
-                let paths = docker::DockerPaths::create(&engine, metadata, cwd, sysroot)?;
+                let paths = docker::DockerPaths::create(&engine, metadata, cwd, toolchain.clone())?;
                 let options =
-                    docker::DockerOptions::new(engine, target.clone(), config, uses_xargo);
-                let status = docker::run(options, paths, &filtered_args, &mut msg_info)
+                    docker::DockerOptions::new(engine, target.clone(), config, image, uses_xargo);
+                let status = docker::run(options, paths, &filtered_args, msg_info)
                     .wrap_err("could not run container")?;
                 let needs_host = args.subcommand.map_or(false, |sc| sc.needs_host(is_remote));
                 if !status.success() {
-                    warn_on_failure(&target, &toolchain, &mut msg_info)?;
+                    warn_on_failure(&target, &toolchain, msg_info)?;
                 }
                 if !(status.success() && needs_host) {
-                    return Ok(status);
+                    return Ok(Some(status));
                 }
             }
         }
     }
-
-    // if we fallback to the host cargo, use the same invocation that was made to cross
-    let argv: Vec<String> = env::args().skip(1).collect();
-    msg_info.note("Falling back to `cargo` on the host.")?;
-    match args.subcommand {
-        Some(Subcommand::List) => {
-            // this won't print in order if we have both stdout and stderr.
-            let out = cargo::run_and_get_output(&argv, &mut msg_info)?;
-            let stdout = out.stdout()?;
-            if out.status.success() && cli::is_subcommand_list(&stdout) {
-                cli::fmt_subcommands(&stdout, &mut msg_info)?;
-            } else {
-                // Not a list subcommand, which can happen with weird edge-cases.
-                print!("{}", stdout);
-                io::stdout().flush().expect("could not flush");
-            }
-            Ok(out.status)
-        }
-        _ => cargo::run(&argv, &mut msg_info).map_err(Into::into),
-    }
+    Ok(None)
 }
 
 #[derive(PartialEq, Eq, Debug)]
@@ -578,7 +658,7 @@ pub(crate) enum VersionMatch {
 
 pub(crate) fn warn_host_version_mismatch(
     host_version_meta: &rustc_version::VersionMeta,
-    toolchain: &str,
+    toolchain: &QualifiedToolchain,
     rustc_version: &rustc_version::Version,
     rustc_commit: &str,
     msg_info: &mut MessageInfo,
@@ -588,7 +668,6 @@ pub(crate) fn warn_host_version_mismatch(
         .split_once(' ')
         .and_then(|x| x.1.strip_suffix(')'));
 
-    // This should only hit on non Host::X86_64UnknownLinuxGnu hosts
     if rustc_version != &host_version_meta.semver || (Some(rustc_commit) != host_commit) {
         let versions = rustc_version.cmp(&host_version_meta.semver);
         let dates = rustc_commit_date.cmp(&host_version_meta.commit_date.as_deref());
diff --git a/src/rustc.rs b/src/rustc.rs
index 5b9a424..e36c947 100644
--- a/src/rustc.rs
+++ b/src/rustc.rs
@@ -1,12 +1,14 @@
-use std::path::PathBuf;
+use std::path::{Path, PathBuf};
 use std::process::Command;
 
 use rustc_version::{Version, VersionMeta};
+use serde::Deserialize;
 
+use crate::docker::ImagePlatform;
 use crate::errors::*;
 use crate::extensions::{env_program, CommandExt};
 use crate::shell::MessageInfo;
-use crate::{Host, Target};
+use crate::TargetTriple;
 
 #[derive(Debug)]
 pub struct TargetList {
@@ -21,14 +23,14 @@ impl TargetList {
 }
 
 pub trait VersionMetaExt {
-    fn host(&self) -> Host;
+    fn host(&self) -> TargetTriple;
     fn needs_interpreter(&self) -> bool;
     fn commit_hash(&self) -> String;
 }
 
 impl VersionMetaExt for VersionMeta {
-    fn host(&self) -> Host {
-        Host::from(&*self.host)
+    fn host(&self) -> TargetTriple {
+        TargetTriple::from(&*self.host)
     }
 
     fn needs_interpreter(&self) -> bool {
@@ -79,6 +81,255 @@ pub fn hash_from_version_string(version: &str, index: usize) -> String {
     short_commit_hash(&const_sha1::sha1(&buffer).to_string())
 }
 
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct QualifiedToolchain {
+    pub channel: String,
+    pub date: Option<String>,
+    pub(self) host: ImagePlatform,
+    pub is_custom: bool,
+    pub full: String,
+    pub(self) sysroot: PathBuf,
+}
+
+impl QualifiedToolchain {
+    pub fn new(channel: &str, date: &Option<String>, host: &ImagePlatform, sysroot: &Path) -> Self {
+        let mut this = Self {
+            channel: channel.to_owned(),
+            date: date.clone(),
+            host: host.clone(),
+            is_custom: false,
+            full: if let Some(date) = date {
+                format!("{}-{}-{}", channel, date, host.target)
+            } else {
+                format!("{}-{}", channel, host.target)
+            },
+            sysroot: sysroot.to_owned(),
+        };
+        this.sysroot.set_file_name(&this.full);
+        this
+    }
+
+    /// Replace the host, does nothing if ran on a custom toolchain
+    pub fn replace_host(&mut self, host: &ImagePlatform) -> &mut Self {
+        if !self.is_custom {
+            *self = Self::new(&self.channel, &self.date, host, &self.sysroot);
+            self.sysroot.set_file_name(&self.full);
+        }
+        self
+    }
+
+    /// Makes a good guess as to what the toolchain is compiled to run on.
+    pub(crate) fn custom(
+        name: &str,
+        sysroot: &Path,
+        config: &crate::config::Config,
+        msg_info: &mut MessageInfo,
+    ) -> Result<QualifiedToolchain> {
+        if let Some(compat) = config.custom_toolchain_compat() {
+            let mut toolchain: QualifiedToolchain = QualifiedToolchain::parse(
+                sysroot.to_owned(),
+                &compat,
+                config,
+                msg_info,
+            )
+            .wrap_err(
+                "could not parse CROSS_CUSTOM_TOOLCHAIN_COMPAT as a fully qualified toolchain name",
+            )?;
+            toolchain.is_custom = true;
+            toolchain.full = name.to_owned();
+            return Ok(toolchain);
+        }
+        // a toolchain installed by https://github.com/rust-lang/cargo-bisect-rustc
+        if name.starts_with("bisector-nightly") {
+            let (_, toolchain) = name.split_once('-').expect("should include -");
+            let mut toolchain =
+                QualifiedToolchain::parse(sysroot.to_owned(), toolchain, config, msg_info)
+                    .wrap_err("could not parse bisector toolchain")?;
+            toolchain.is_custom = true;
+            toolchain.full = name.to_owned();
+            return Ok(toolchain);
+        } else if let Ok(stdout) = Command::new(sysroot.join("bin/rustc"))
+            .arg("-Vv")
+            .run_and_get_stdout(msg_info)
+        {
+            let rustc_version::VersionMeta {
+                build_date,
+                channel,
+                host,
+                ..
+            } = rustc_version::version_meta_for(&stdout)?;
+            let mut toolchain = QualifiedToolchain::new(
+                match channel {
+                    rustc_version::Channel::Dev => "dev",
+                    rustc_version::Channel::Nightly => "nightly",
+                    rustc_version::Channel::Beta => "beta",
+                    rustc_version::Channel::Stable => "stable",
+                },
+                &build_date,
+                &ImagePlatform::from_target(host.into())?,
+                sysroot,
+            );
+            toolchain.is_custom = true;
+            toolchain.full = name.to_owned();
+            return Ok(toolchain);
+        }
+        Err(eyre::eyre!(
+            "cross can not figure out what your custom toolchain is"
+        ))
+        .suggestion("set `CROSS_CUSTOM_TOOLCHAIN_COMPAT` to a fully qualified toolchain name: i.e `nightly-aarch64-unknown-linux-musl`")
+    }
+
+    pub fn host(&self) -> &ImagePlatform {
+        &self.host
+    }
+
+    pub fn get_sysroot(&self) -> &Path {
+        &self.sysroot
+    }
+
+    /// Grab the current default toolchain
+    pub fn default(config: &crate::config::Config, msg_info: &mut MessageInfo) -> Result<Self> {
+        let sysroot = sysroot(msg_info)?;
+
+        let default_toolchain_name = sysroot
+            .file_name()
+            .ok_or_else(|| eyre::eyre!("couldn't get name of active toolchain"))?
+            .to_str()
+            .ok_or_else(|| eyre::eyre!("toolchain was not utf-8"))?;
+
+        if !config.custom_toolchain() {
+            QualifiedToolchain::parse(sysroot.clone(), default_toolchain_name, config, msg_info)
+        } else {
+            QualifiedToolchain::custom(default_toolchain_name, &sysroot, config, msg_info)
+        }
+    }
+
+    /// Merge a "picked" toolchain, overriding set fields.
+    pub fn with_picked(
+        self,
+        config: &crate::config::Config,
+        picked: Toolchain,
+        msg_info: &mut MessageInfo,
+    ) -> Result<Self> {
+        let toolchain = Self::default(config, msg_info)?;
+        let date = picked.date.or(self.date);
+        let host = picked
+            .host
+            .map_or(Ok(self.host), ImagePlatform::from_target)?;
+        let channel = picked.channel;
+
+        Ok(QualifiedToolchain::new(
+            &channel,
+            &date,
+            &host,
+            &toolchain.sysroot,
+        ))
+    }
+
+    pub fn set_sysroot(&mut self, convert: impl Fn(&Path) -> PathBuf) {
+        self.sysroot = convert(&self.sysroot);
+    }
+}
+
+impl std::fmt::Display for QualifiedToolchain {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&self.full)
+    }
+}
+
+impl QualifiedToolchain {
+    fn parse(
+        sysroot: PathBuf,
+        toolchain: &str,
+        config: &crate::config::Config,
+        msg_info: &mut MessageInfo,
+    ) -> Result<Self> {
+        match toolchain.parse::<Toolchain>() {
+            Ok(Toolchain {
+                channel,
+                date,
+                host: Some(host),
+                is_custom,
+                full,
+            }) => Ok(QualifiedToolchain {
+                channel,
+                date,
+                host: ImagePlatform::from_target(host)?,
+                is_custom,
+                full,
+                sysroot,
+            }),
+            Ok(_) | Err(_) if config.custom_toolchain() => {
+                QualifiedToolchain::custom(toolchain, &sysroot, config, msg_info)
+            }
+            Ok(_) => eyre::bail!("toolchain is not fully qualified"),
+            Err(e) => Err(e),
+        }
+    }
+}
+
+#[derive(Debug, Clone, Deserialize, PartialEq, Eq)]
+pub struct Toolchain {
+    pub channel: String,
+    pub date: Option<String>,
+    pub host: Option<TargetTriple>,
+    pub is_custom: bool,
+    pub full: String,
+}
+
+impl std::fmt::Display for Toolchain {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(&self.full)
+    }
+}
+
+impl std::str::FromStr for Toolchain {
+    type Err = eyre::Report;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        fn dig(s: &str) -> bool {
+            s.chars().all(|c: char| c.is_ascii_digit())
+        }
+        if let Some((channel, parts)) = s.split_once('-') {
+            if parts.starts_with(|c: char| c.is_ascii_digit()) {
+                // a date, YYYY-MM-DD
+                let mut split = parts.splitn(4, '-');
+                let ymd = [split.next(), split.next(), split.next()];
+                let ymd = match ymd {
+                    [Some(y), Some(m), Some(d)] if dig(y) && dig(m) && dig(d) => {
+                        format!("{y}-{m}-{d}")
+                    }
+                    _ => eyre::bail!("invalid toolchain `{s}`"),
+                };
+                Ok(Toolchain {
+                    channel: channel.to_owned(),
+                    date: Some(ymd),
+                    host: split.next().map(|s| s.into()),
+                    is_custom: false,
+                    full: s.to_owned(),
+                })
+            } else {
+                // channel-host
+                Ok(Toolchain {
+                    channel: channel.to_owned(),
+                    date: None,
+                    host: Some(parts.into()),
+                    is_custom: false,
+                    full: s.to_owned(),
+                })
+            }
+        } else {
+            Ok(Toolchain {
+                channel: s.to_owned(),
+                date: None,
+                host: None,
+                is_custom: false,
+                full: s.to_owned(),
+            })
+        }
+    }
+}
+
 #[must_use]
 pub fn rustc_command() -> Command {
     Command::new(env_program("RUSTC", "rustc"))
@@ -93,47 +344,15 @@ pub fn target_list(msg_info: &mut MessageInfo) -> Result<TargetList> {
         })
 }
 
-pub fn sysroot(host: &Host, target: &Target, msg_info: &mut MessageInfo) -> Result<PathBuf> {
-    let mut stdout = rustc_command()
+pub fn sysroot(msg_info: &mut MessageInfo) -> Result<PathBuf> {
+    let stdout = rustc_command()
         .args(&["--print", "sysroot"])
         .run_and_get_stdout(msg_info)?
         .trim()
         .to_owned();
-
-    // On hosts other than Linux, specify the correct toolchain path.
-    if host != &Host::X86_64UnknownLinuxGnu && target.needs_docker() {
-        stdout = stdout.replacen(host.triple(), Host::X86_64UnknownLinuxGnu.triple(), 1);
-    }
-
     Ok(PathBuf::from(stdout))
 }
 
-pub fn get_sysroot(
-    host: &Host,
-    target: &Target,
-    channel: Option<&str>,
-    msg_info: &mut MessageInfo,
-) -> Result<(String, PathBuf)> {
-    let mut sysroot = sysroot(host, target, msg_info)?;
-    let default_toolchain = sysroot
-        .file_name()
-        .and_then(|file_name| file_name.to_str())
-        .ok_or_else(|| eyre::eyre!("couldn't get toolchain name"))?;
-    let toolchain = if let Some(channel) = channel {
-        [channel]
-            .iter()
-            .cloned()
-            .chain(default_toolchain.splitn(2, '-').skip(1))
-            .collect::<Vec<_>>()
-            .join("-")
-    } else {
-        default_toolchain.to_owned()
-    };
-    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")
 }
@@ -142,6 +361,17 @@ pub fn version_meta() -> Result<rustc_version::VersionMeta> {
 mod tests {
     use super::*;
 
+    #[test]
+    fn bisect() {
+        QualifiedToolchain::custom(
+            "bisector-nightly-2022-04-26-x86_64-unknown-linux-gnu",
+            "/tmp/cross/sysroot".as_ref(),
+            &crate::config::Config::new(None),
+            &mut MessageInfo::create(true, false, None).unwrap(),
+        )
+        .unwrap();
+    }
+
     #[test]
     fn hash_from_rustc() {
         assert_eq!(
diff --git a/src/rustup.rs b/src/rustup.rs
index a13b1da..a467ae6 100644
--- a/src/rustup.rs
+++ b/src/rustup.rs
@@ -1,18 +1,21 @@
-use std::path::{Path, PathBuf};
+use std::path::PathBuf;
 use std::process::Command;
 
+use color_eyre::owo_colors::OwoColorize;
+use color_eyre::SectionExt;
 use rustc_version::{Channel, Version};
 
 use crate::errors::*;
 pub use crate::extensions::{CommandExt, OutputExt};
+use crate::rustc::QualifiedToolchain;
 use crate::shell::{MessageInfo, Verbosity};
 use crate::Target;
 
 #[derive(Debug)]
 pub struct AvailableTargets {
-    default: String,
-    installed: Vec<String>,
-    not_installed: Vec<String>,
+    pub default: String,
+    pub installed: Vec<String>,
+    pub not_installed: Vec<String>,
 }
 
 impl AvailableTargets {
@@ -44,6 +47,19 @@ fn rustup_command(msg_info: &mut MessageInfo, no_flags: bool) -> Command {
     cmd
 }
 
+pub fn active_toolchain(msg_info: &mut MessageInfo) -> Result<String> {
+    let out = rustup_command(msg_info, true)
+        .args(&["show", "active-toolchain"])
+        .run_and_get_output(msg_info)?;
+
+    Ok(out
+        .stdout()?
+        .split_once(' ')
+        .ok_or_else(|| eyre::eyre!("rustup returned invalid data"))?
+        .0
+        .to_owned())
+}
+
 pub fn installed_toolchains(msg_info: &mut MessageInfo) -> Result<Vec<String>> {
     let out = rustup_command(msg_info, true)
         .args(&["toolchain", "list"])
@@ -60,8 +76,17 @@ pub fn installed_toolchains(msg_info: &mut MessageInfo) -> Result<Vec<String>> {
         .collect())
 }
 
-pub fn available_targets(toolchain: &str, msg_info: &mut MessageInfo) -> Result<AvailableTargets> {
+pub fn available_targets(
+    // this is explicitly a string and not `QualifiedToolchain`,
+    // this is because we use this as a way to ensure that
+    // the toolchain is an official toolchain, if this errors on
+    // `is a custom toolchain`, we tell the user to set CROSS_CUSTOM_TOOLCHAIN
+    // to handle the logic needed.
+    toolchain: &str,
+    msg_info: &mut MessageInfo,
+) -> Result<AvailableTargets> {
     let mut cmd = rustup_command(msg_info, true);
+
     cmd.args(&["target", "list", "--toolchain", toolchain]);
     let output = cmd
         .run_and_get_output(msg_info)
@@ -69,7 +94,8 @@ pub fn available_targets(toolchain: &str, msg_info: &mut MessageInfo) -> Result<
 
     if !output.status.success() {
         if String::from_utf8_lossy(&output.stderr).contains("is a custom toolchain") {
-            eyre::bail!("{toolchain} is a custom toolchain. To use it, you'll need to set the environment variable `CROSS_CUSTOM_TOOLCHAIN=1`")
+            return Err(eyre::eyre!("`{toolchain}` is a custom toolchain.").with_section(|| r#"To use this toolchain with cross, you'll need to set the environment variable `CROSS_CUSTOM_TOOLCHAIN=1`
+cross will not attempt to configure the toolchain further so that it can run your binary."#.header("Suggestion".bright_cyan())));
         }
         return Err(cmd
             .status_result(msg_info, output.status, Some(&output))
@@ -104,33 +130,40 @@ pub fn available_targets(toolchain: &str, msg_info: &mut MessageInfo) -> Result<
     })
 }
 
-pub fn install_toolchain(toolchain: &str, msg_info: &mut MessageInfo) -> Result<()> {
+pub fn install_toolchain(toolchain: &QualifiedToolchain, msg_info: &mut MessageInfo) -> Result<()> {
+    let toolchain = toolchain.to_string();
     rustup_command(msg_info, false)
-        .args(&["toolchain", "add", toolchain, "--profile", "minimal"])
+        .args(&["toolchain", "add", &toolchain, "--profile", "minimal"])
         .run(msg_info, false)
         .wrap_err_with(|| format!("couldn't install toolchain `{toolchain}`"))
 }
 
-pub fn install(target: &Target, toolchain: &str, msg_info: &mut MessageInfo) -> Result<()> {
+pub fn install(
+    target: &Target,
+    toolchain: &QualifiedToolchain,
+    msg_info: &mut MessageInfo,
+) -> Result<()> {
     let target = target.triple();
-
+    let toolchain = toolchain.to_string();
     rustup_command(msg_info, false)
-        .args(&["target", "add", target, "--toolchain", toolchain])
+        .args(&["target", "add", target, "--toolchain", &toolchain])
         .run(msg_info, false)
         .wrap_err_with(|| format!("couldn't install `std` for {target}"))
 }
 
 pub fn install_component(
     component: &str,
-    toolchain: &str,
+    toolchain: &QualifiedToolchain,
     msg_info: &mut MessageInfo,
 ) -> Result<()> {
+    let toolchain = toolchain.to_string();
     rustup_command(msg_info, false)
-        .args(&["component", "add", component, "--toolchain", toolchain])
+        .args(&["component", "add", component, "--toolchain", &toolchain])
         .run(msg_info, false)
         .wrap_err_with(|| format!("couldn't install the `{component}` component"))
 }
 
+#[derive(Debug)]
 pub enum Component<'a> {
     Installed(&'a str),
     Available(&'a str),
@@ -149,11 +182,11 @@ impl<'a> Component<'a> {
 
 pub fn check_component<'a>(
     component: &'a str,
-    toolchain: &str,
+    toolchain: &QualifiedToolchain,
     msg_info: &mut MessageInfo,
 ) -> Result<Component<'a>> {
     Ok(Command::new("rustup")
-        .args(&["component", "list", "--toolchain", toolchain])
+        .args(&["component", "list", "--toolchain", &toolchain.to_string()])
         .run_and_get_stdout(msg_info)?
         .lines()
         .find_map(|line| {
@@ -175,7 +208,7 @@ pub fn check_component<'a>(
 
 pub fn component_is_installed(
     component: &str,
-    toolchain: &str,
+    toolchain: &QualifiedToolchain,
     msg_info: &mut MessageInfo,
 ) -> Result<bool> {
     Ok(check_component(component, toolchain, msg_info)?.is_installed())
@@ -196,36 +229,39 @@ 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")
-}
+impl QualifiedToolchain {
+    fn multirust_channel_manifest_path(&self) -> PathBuf {
+        self.get_sysroot()
+            .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);
-    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
-            .get("pkg")
-            .and_then(|pkg| pkg.get("rust"))
-            .and_then(|rust| rust.get("version"))
-            .and_then(|version| version.as_str())
-            .map(|version| version.to_owned()));
+    pub fn rustc_version_string(&self) -> Result<Option<String>> {
+        let path = self.multirust_channel_manifest_path();
+        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
+                .get("pkg")
+                .and_then(|pkg| pkg.get("rust"))
+                .and_then(|rust| rust.get("version"))
+                .and_then(|version| version.as_str())
+                .map(|version| version.to_owned()));
+        }
+        Ok(None)
     }
-    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())));
+    pub fn rustc_version(&self) -> Result<Option<(Version, Channel, String)>> {
+        let path = self.multirust_channel_manifest_path();
+        if let Some(rust_version) = self.rustc_version_string()? {
+            // 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)
     }
-    Ok(None)
 }
diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs
index fc00310..70f120f 100644
--- a/xtask/src/build_docker_image.rs
+++ b/xtask/src/build_docker_image.rs
@@ -3,6 +3,7 @@ use std::path::Path;
 
 use crate::util::{cargo_metadata, gha_error, gha_output, gha_print};
 use clap::Args;
+use cross::docker::ImagePlatform;
 use cross::shell::MessageInfo;
 use cross::{docker, CommandExt, ToUtf8};
 
@@ -66,6 +67,9 @@ pub struct BuildDockerImage {
     /// Additional build arguments to pass to Docker.
     #[clap(long)]
     pub build_arg: Vec<String>,
+    // [os/arch[/variant]=]toolchain
+    #[clap(long, short = 'a', action = clap::builder::ArgAction::Append)]
+    pub platform: Vec<ImagePlatform>,
     /// Targets to build for
     #[clap()]
     pub targets: Vec<crate::ImageTarget>,
@@ -106,6 +110,7 @@ pub fn build_docker_image(
         no_fastfail,
         from_ci,
         build_arg,
+        platform,
         mut targets,
         ..
     }: BuildDockerImage,
@@ -154,18 +159,31 @@ pub fn build_docker_image(
         .map(|t| locate_dockerfile(t, &docker_root, &cross_toolchains_root))
         .collect::<cross::Result<Vec<_>>>()?;
 
+    let platforms = if platform.is_empty() {
+        vec![ImagePlatform::DEFAULT]
+    } else {
+        platform
+    };
+
     let mut results = vec![];
-    for (target, dockerfile) in &targets {
+    for (platform, (target, dockerfile)) in targets
+        .iter()
+        .flat_map(|t| platforms.iter().map(move |p| (p, t)))
+    {
         if gha && targets.len() > 1 {
             gha_print("::group::Build {target}");
+        } else {
+            msg_info.note(format_args!("Build {target} for {}", platform.target))?;
         }
         let mut docker_build = docker::command(engine);
         docker_build.args(&["buildx", "build"]);
         docker_build.current_dir(&docker_root);
 
+        docker_build.args(&["--platform", &platform.docker_platform()]);
+
         if push {
             docker_build.arg("--push");
-        } else if no_output {
+        } else if engine.kind.is_docker() && no_output {
             docker_build.args(&["--output", "type=tar,dest=/dev/null"]);
         } else {
             docker_build.arg("--load");
@@ -226,7 +244,19 @@ pub fn build_docker_image(
 
         docker_build.args([
             "--label",
-            &format!("{}.for-cross-target={target}", cross::CROSS_LABEL_DOMAIN),
+            &format!(
+                "{}.for-cross-target={}",
+                cross::CROSS_LABEL_DOMAIN,
+                target.name
+            ),
+        ]);
+        docker_build.args([
+            "--label",
+            &format!(
+                "{}.runs-with={}",
+                cross::CROSS_LABEL_DOMAIN,
+                platform.target
+            ),
         ]);
 
         docker_build.args(&["-f", dockerfile]);
diff --git a/xtask/src/ci.rs b/xtask/src/ci.rs
index 8582dd5..7a9d675 100644
--- a/xtask/src/ci.rs
+++ b/xtask/src/ci.rs
@@ -40,15 +40,20 @@ pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> {
             // Set labels
             let mut labels = vec![];
 
-            let image_title = match target.triplet.as_ref() {
-                "cross" => target.triplet.to_string(),
-                _ => format!("cross (for {})", target.triplet),
+            let image_title = match target.name.as_ref() {
+                "cross" => target.name.to_string(),
+                // TODO: Mention platform?
+                _ => format!("cross (for {})", target.name),
             };
             labels.push(format!("org.opencontainers.image.title={image_title}"));
             labels.push(format!(
                 "org.opencontainers.image.licenses={}",
                 cross_meta.license.as_deref().unwrap_or_default()
             ));
+            labels.push(format!(
+                "org.opencontainers.image.created={}",
+                chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
+            ));
 
             gha_output("labels", &serde_json::to_string(&labels.join("\n"))?);
 
@@ -70,10 +75,10 @@ pub fn ci(args: CiJob, metadata: CargoMetadata) -> cross::Result<()> {
             if target.has_ci_image() {
                 gha_output("has-image", "true")
             }
-            if target.is_default_test_image() {
+            if target.is_standard_target_image() {
                 gha_output("test-variant", "default")
             } else {
-                gha_output("test-variant", &target.triplet)
+                gha_output("test-variant", &target.name)
             }
         }
         CiJob::Check { ref_type, ref_name } => {
diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs
new file mode 100644
index 0000000..d686aca
--- /dev/null
+++ b/xtask/src/codegen.rs
@@ -0,0 +1,77 @@
+use clap::Args;
+use eyre::Context;
+use std::fmt::Write;
+
+use crate::util::{get_cargo_workspace, get_matrix};
+
+#[derive(Args, Debug)]
+pub struct Codegen {
+    /// Provide verbose diagnostic output.
+    #[clap(short, long)]
+    verbose: bool,
+}
+
+pub fn codegen(Codegen { .. }: Codegen) -> cross::Result<()> {
+    let path = get_cargo_workspace().join("src/docker/provided_images.rs");
+    std::fs::write(path, docker_images()).wrap_err("when writing src/docker/provided_images.rs")?;
+    Ok(())
+}
+
+pub fn docker_images() -> String {
+    let mut images = String::from(
+        r##"#![doc = "*** AUTO-GENERATED, do not touch. Run `cargo xtask codegen` to update ***"]
+use super::{ImagePlatform, ProvidedImage};
+
+#[rustfmt::skip]
+pub static PROVIDED_IMAGES: &[ProvidedImage] = &["##,
+    );
+
+    for image_target in get_matrix()
+        .iter()
+        .filter(|i| i.to_image_target().is_standard_target_image())
+    {
+        write!(
+            &mut images,
+            r#"
+        ProvidedImage {{
+            name: "{}",
+            platforms: &[{}],
+            sub: {}
+        }},"#,
+            image_target.target.clone(),
+            if let Some(platforms) = &image_target.platforms {
+                platforms
+                    .iter()
+                    .map(|p| {
+                        format!(
+                            "ImagePlatform::{}",
+                            p.replace('-', "_").to_ascii_uppercase()
+                        )
+                    })
+                    .collect::<Vec<_>>()
+                    .as_slice()
+                    .join(", ")
+            } else {
+                "ImagePlatform::DEFAULT".to_string()
+            },
+            if let Some(sub) = &image_target.sub {
+                format!(r#"Some("{}")"#, sub)
+            } else {
+                "None".to_string()
+            }
+        )
+        .expect("writing to string should not fail")
+    }
+
+    images.push_str("\n];\n");
+    images
+}
+
+#[cfg(test)]
+#[test]
+pub fn ensure_correct_codegen() -> cross::Result<()> {
+    let provided_images = crate::util::get_cargo_workspace().join("src/docker/provided_images.rs");
+    let content = cross::file::read(provided_images)?;
+    assert_eq!(content.replace("\r\n", "\n"), docker_images());
+    Ok(())
+}
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
index 9da2df9..106d437 100644
--- a/xtask/src/main.rs
+++ b/xtask/src/main.rs
@@ -3,6 +3,7 @@
 pub mod build_docker_image;
 pub mod changelog;
 pub mod ci;
+pub mod codegen;
 pub mod crosstool;
 pub mod hooks;
 pub mod install_git_hooks;
@@ -11,6 +12,7 @@ pub mod util;
 
 use ci::CiJob;
 use clap::{CommandFactory, Parser, Subcommand};
+use codegen::Codegen;
 use cross::docker;
 use cross::shell::{MessageInfo, Verbosity};
 use util::{cargo_metadata, ImageTarget};
@@ -61,6 +63,8 @@ enum Commands {
     /// Validate changelog entries.
     #[clap(hide = true)]
     ValidateChangelog(ValidateChangelog),
+    /// Code generation
+    Codegen(Codegen),
 }
 
 fn is_toolchain(toolchain: &str) -> cross::Result<String> {
@@ -126,6 +130,7 @@ pub fn main() -> cross::Result<()> {
             let mut msg_info = get_msg_info!(args, args.verbose)?;
             changelog::validate_changelog(args, &mut msg_info)?;
         }
+        Commands::Codegen(args) => codegen::codegen(args)?,
     }
 
     Ok(())
diff --git a/xtask/src/target_info.rs b/xtask/src/target_info.rs
index 08bd8c5..ef5093f 100644
--- a/xtask/src/target_info.rs
+++ b/xtask/src/target_info.rs
@@ -51,7 +51,7 @@ fn image_info(
     let mut command = docker::command(engine);
     command.arg("run");
     command.arg("--rm");
-    command.args(&["-e", &format!("TARGET={}", target.triplet)]);
+    command.args(&["-e", &format!("TARGET={}", target.name)]);
     if msg_info.is_verbose() {
         command.args(&["-e", "VERBOSE=1"]);
     }
diff --git a/xtask/src/util.rs b/xtask/src/util.rs
index b588466..8c06295 100644
--- a/xtask/src/util.rs
+++ b/xtask/src/util.rs
@@ -5,11 +5,28 @@ use std::process::Command;
 
 use cross::shell::MessageInfo;
 use cross::{docker, CommandExt, ToUtf8};
+
 use once_cell::sync::OnceCell;
 use serde::Deserialize;
 
 const WORKFLOW: &str = include_str!("../../.github/workflows/ci.yml");
 
+static WORKSPACE: OnceCell<PathBuf> = OnceCell::new();
+
+/// Returns the cargo workspace for the manifest
+pub fn get_cargo_workspace() -> &'static Path {
+    let manifest_dir = env!("CARGO_MANIFEST_DIR");
+    WORKSPACE.get_or_init(|| {
+        cross::cargo_metadata_with_args(
+            Some(manifest_dir.as_ref()),
+            None,
+            &mut MessageInfo::create(true, false, None).expect("should not fail"),
+        )
+        .unwrap()
+        .unwrap()
+        .workspace_root
+    })
+}
 #[derive(Debug, PartialEq, Eq, Deserialize)]
 struct Workflow {
     jobs: Jobs,
@@ -43,6 +60,7 @@ pub struct Matrix {
     #[serde(default)]
     pub run: i64,
     pub os: String,
+    pub platforms: Option<Vec<String>>,
 }
 
 impl Matrix {
@@ -53,7 +71,7 @@ impl Matrix {
 
     pub fn to_image_target(&self) -> crate::ImageTarget {
         crate::ImageTarget {
-            triplet: self.target.clone(),
+            name: self.target.clone(),
             sub: self.sub.clone(),
         }
     }
@@ -101,24 +119,20 @@ pub fn pull_image(
 
 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
 pub struct ImageTarget {
-    pub triplet: String,
+    pub name: String,
     pub sub: Option<String>,
 }
 
 impl ImageTarget {
     pub fn image_name(&self, repository: &str, tag: &str) -> String {
-        if let Some(sub) = &self.sub {
-            format!("{repository}/{}:{tag}-{sub}", self.triplet)
-        } else {
-            format!("{repository}/{}:{tag}", self.triplet)
-        }
+        cross::docker::image_name(&self.name, self.sub.as_deref(), repository, tag)
     }
 
     pub fn alt(&self) -> String {
         if let Some(sub) = &self.sub {
-            format!("{}:{sub}", self.triplet,)
+            format!("{}:{sub}", self.name)
         } else {
-            self.triplet.to_string()
+            self.name.to_string()
         }
     }
 
@@ -127,17 +141,17 @@ impl ImageTarget {
         let matrix = get_matrix();
         matrix
             .iter()
-            .any(|m| m.builds_image() && m.target == self.triplet && m.sub == self.sub)
+            .any(|m| m.builds_image() && m.target == self.name && m.sub == self.sub)
     }
 
-    /// Determine if this target uses the default test script
-    pub fn is_default_test_image(&self) -> bool {
-        self.triplet != "cross"
+    /// Determine if this target is a "normal" target for a triplet
+    pub fn is_standard_target_image(&self) -> bool {
+        self.name != "cross" && self.has_ci_image()
     }
 
     /// Determine if this target needs to interact with the project root.
     pub fn needs_workspace_root_context(&self) -> bool {
-        self.triplet == "cross"
+        self.name == "cross"
     }
 }
 
@@ -147,12 +161,12 @@ impl std::str::FromStr for ImageTarget {
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         if let Some((target, sub)) = s.split_once('.') {
             Ok(ImageTarget {
-                triplet: target.to_string(),
+                name: target.to_string(),
                 sub: Some(sub.to_string()),
             })
         } else {
             Ok(ImageTarget {
-                triplet: s.to_string(),
+                name: s.to_string(),
                 sub: None,
             })
         }
@@ -162,9 +176,9 @@ impl std::str::FromStr for ImageTarget {
 impl std::fmt::Display for ImageTarget {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
         if let Some(sub) = &self.sub {
-            write!(f, "{}.{sub}", self.triplet,)
+            write!(f, "{}.{sub}", self.name)
         } else {
-            write!(f, "{}", self.triplet)
+            write!(f, "{}", self.name)
         }
     }
 }


Test patch:
diff --git a/ci/test-bisect.sh b/ci/test-bisect.sh
index e61ac67..0acd06b 100755
--- a/ci/test-bisect.sh
+++ b/ci/test-bisect.sh
@@ -13,7 +13,7 @@ fi
 ci_dir=$(dirname "${BASH_SOURCE[0]}")
 ci_dir=$(realpath "${ci_dir}")
 . "${ci_dir}"/shared.sh
-project_home=$(dirname "${ci_dir}")
+
 
 main() {
     local td=
@@ -22,7 +22,7 @@ main() {
     retry cargo fetch
     cargo build
     cargo install cargo-bisect-rustc --debug
-    export CROSS="${project_home}/target/debug/cross"
+    export CROSS="${PROJECT_HOME}/target/debug/cross"
 
     td="$(mktemp -d)"
     git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}"
diff --git a/ci/test-cross-image.sh b/ci/test-cross-image.sh
index 9fb57f5..7dcded6 100755
--- a/ci/test-cross-image.sh
+++ b/ci/test-cross-image.sh
@@ -1,5 +1,5 @@
 #!/usr/bin/env bash
-# shellcheck disable=SC2086
+# shellcheck disable=SC2086,SC1091,SC1090
 
 set -x
 set -eo pipefail
@@ -20,6 +20,9 @@ if [[ -z "${CROSS_TARGET_CROSS_IMAGE}" ]]; then
     CROSS_TARGET_CROSS_IMAGE="ghcr.io/cross-rs/cross:main"
 fi
 
+ci_dir=$(dirname "${BASH_SOURCE[0]}")
+ci_dir=$(realpath "${ci_dir}")
+. "${ci_dir}"/shared.sh
 
 main() {
 
diff --git a/ci/test-docker-in-docker.sh b/ci/test-docker-in-docker.sh
index f7705c0..d723b50 100755
--- a/ci/test-docker-in-docker.sh
+++ b/ci/test-docker-in-docker.sh
@@ -1,5 +1,5 @@
 #!/usr/bin/env bash
-# shellcheck disable=SC1004
+# shellcheck disable=SC1004,SC1091,SC1090
 
 # test to see that running docker-in-docker works
 
@@ -18,17 +18,17 @@ if [[ "${IMAGE}" ]]; then
     export "CROSS_TARGET_${TARGET_UPPER//-/_}_IMAGE"="${IMAGE}"
 fi
 
-source=$(dirname "${BASH_SOURCE[0]}")
-source=$(realpath "${source}")
-home=$(dirname "${source}")
+ci_dir=$(dirname "${BASH_SOURCE[0]}")
+ci_dir=$(realpath "${ci_dir}")
+. "${ci_dir}"/shared.sh
 
 main() {
-    docker run -v "${home}":"${home}" -w "${home}" \
+    docker run -v "${PROJECT_HOME}":"${PROJECT_HOME}" -w "${PROJECT_HOME}" \
         --rm -e TARGET -e RUSTFLAGS -e RUST_TEST_THREADS \
         -e LLVM_PROFILE_FILE -e CARGO_INCREMENTAL \
         -e "CROSS_TARGET_${TARGET_UPPER//-/_}_IMAGE" \
         -v /var/run/docker.sock:/var/run/docker.sock \
-        docker:18.09-dind sh -c '
+        docker:20.10-dind sh -c '
 #!/usr/bin/env sh
 set -x
 set -euo pipefail
diff --git a/ci/test-foreign-toolchain.sh b/ci/test-foreign-toolchain.sh
new file mode 100755
index 0000000..2f92fdf
--- /dev/null
+++ b/ci/test-foreign-toolchain.sh
@@ -0,0 +1,71 @@
+#!/usr/bin/env bash
+# shellcheck disable=SC1091,SC1090
+
+# test to see that foreign toolchains work
+
+set -x
+set -eo pipefail
+
+ci_dir=$(dirname "${BASH_SOURCE[0]}")
+ci_dir=$(realpath "${ci_dir}")
+. "${ci_dir}"/shared.sh
+
+main() {
+    local td=
+
+    retry cargo fetch
+    cargo build
+    export CROSS="${PROJECT_HOME}/target/debug/cross"
+
+    td="$(mkcargotemp -d)"
+
+    pushd "${td}"
+    cargo init --bin --name foreign_toolchain
+    # shellcheck disable=SC2016
+    echo '# Cross.toml
+[build]
+default-target = "x86_64-unknown-linux-musl"
+
+[target."x86_64-unknown-linux-musl"]
+image.name = "alpine:edge"
+image.toolchain = ["x86_64-unknown-linux-musl"]
+pre-build = ["apk add --no-cache gcc musl-dev"]' >"${CARGO_TMP_DIR}"/Cross.toml
+
+    "$CROSS" run -v
+
+    local tmp_basename
+    tmp_basename=$(basename "${CARGO_TMP_DIR}")
+    "${CROSS_ENGINE}" images --format '{{.Repository}}:{{.Tag}}' --filter 'label=org.cross-rs.for-cross-target' | grep "cross-custom-${tmp_basename}" | xargs -t "${CROSS_ENGINE}" rmi
+
+    echo '# Cross.toml
+[build]
+default-target = "x86_64-unknown-linux-gnu"
+
+[target.x86_64-unknown-linux-gnu]
+pre-build = [
+    "apt-get update && apt-get install -y libc6 g++-x86-64-linux-gnu libc6-dev-amd64-cross",
+]
+
+[target.x86_64-unknown-linux-gnu.env]
+passthrough = [
+    "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER=x86_64-linux-gnu-gcc",
+    "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUNNER=/qemu-runner x86_64",
+    "CC_x86_64_unknown_linux_gnu=x86_64-linux-gnu-gcc",
+    "CXX_x86_64_unknown_linux_gnu=x86_64-linux-gnu-g++",
+]
+
+[target.x86_64-unknown-linux-gnu.image]
+name = "ubuntu:20.04"
+toolchain = ["aarch64-unknown-linux-gnu"]
+    ' >"${CARGO_TMP_DIR}"/Cross.toml
+
+    "$CROSS" build -v
+
+    "${CROSS_ENGINE}" images --format '{{.Repository}}:{{.Tag}}' --filter 'label=org.cross-rs.for-cross-target' | grep "cross-custom-${tmp_basename}" | xargs "${CROSS_ENGINE}" rmi
+
+    popd
+
+    rm -rf "${td}"
+}
+
+main
diff --git a/ci/test-remote.sh b/ci/test-remote.sh
index 8fb3583..8b676f3 100755
--- a/ci/test-remote.sh
+++ b/ci/test-remote.sh
@@ -14,15 +14,14 @@ fi
 ci_dir=$(dirname "${BASH_SOURCE[0]}")
 ci_dir=$(realpath "${ci_dir}")
 . "${ci_dir}"/shared.sh
-project_home=$(dirname "${ci_dir}")
 
 main() {
     local err=
 
     retry cargo fetch
     cargo build
-    export CROSS="${project_home}/target/debug/cross"
-    export CROSS_UTIL="${project_home}/target/debug/cross-util"
+    export CROSS="${PROJECT_HOME}/target/debug/cross"
+    export CROSS_UTIL="${PROJECT_HOME}/target/debug/cross-util"
 
     # if the create volume fails, ensure it exists.
     if ! err=$("${CROSS_UTIL}" volumes create 2>&1 >/dev/null); then
@@ -40,7 +39,7 @@ main() {
 
 cross_test_cpp() {
     local td=
-    td="$(mktemp -d)"
+    td="$(mkcargotemp -d)"
 
     git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}"
 
diff --git a/ci/test.sh b/ci/test.sh
index fbfadda..61f764f 100755
--- a/ci/test.sh
+++ b/ci/test.sh
@@ -2,7 +2,7 @@
 # shellcheck disable=SC2086,SC1091,SC1090
 
 set -x
-set -euo pipefail
+set -eo pipefail
 
 # NOTE: "${@}" is an unbound variable for bash 3.2, which is the
 # installed version on macOS. likewise, "${var[@]}" is an unbound
@@ -11,7 +11,6 @@ set -euo pipefail
 ci_dir=$(dirname "${BASH_SOURCE[0]}")
 ci_dir=$(realpath "${ci_dir}")
 . "${ci_dir}"/shared.sh
-project_home=$(dirname "${ci_dir}")
 
 workspace_test() {
   "${CROSS[@]}" build --target "${TARGET}" --workspace "$@" ${CROSS_FLAGS}
@@ -32,8 +31,8 @@ main() {
     export QEMU_STRACE=1
 
     # ensure we have the proper toolchain and optional rust flags
-    export CROSS=("${project_home}/target/debug/cross")
-    export CROSS_FLAGS=""
+    export CROSS=("${PROJECT_HOME}/target/debug/cross")
+    export CROSS_FLAGS="-v"
     if (( ${BUILD_STD:-0} )); then
         # use build-std instead of xargo, due to xargo being
         # maintenance-only. build-std requires a nightly compiler
@@ -48,7 +47,7 @@ main() {
 
     if (( ${STD:-0} )); then
         # test `cross check`
-        td=$(mktemp -d)
+        td=$(mkcargotemp -d)
         cargo init --lib --name foo "${td}"
         pushd "${td}"
         echo '#![no_std]' > src/lib.rs
@@ -57,7 +56,7 @@ main() {
         rm -rf "${td}"
     else
         # `cross build` test for targets where `std` is not available
-        td=$(mktemp -d)
+        td=$(mkcargotemp -d)
 
         git clone \
             --depth 1 \
@@ -78,7 +77,7 @@ main() {
 
     # `cross build` test for the other targets
     if [[ "${TARGET}" == *-unknown-emscripten ]]; then
-        td=$(mktemp -d)
+        td=$(mkcargotemp -d)
 
         pushd "${td}"
         cargo init --lib --name foo .
@@ -88,7 +87,7 @@ main() {
 
         rm -rf "${td}"
     elif [[ "${TARGET}" != thumb* ]]; then
-        td=$(mktemp -d)
+        td=$(mkcargotemp -d)
 
         pushd "${td}"
         # test that linking works
@@ -103,7 +102,7 @@ main() {
     if (( ${RUN:-0} )); then
         # `cross test` test
         if (( ${DYLIB:-0} )); then
-            td=$(mktemp -d)
+            td=$(mkcargotemp -d)
 
             pushd "${td}"
             cargo init --lib --name foo .
@@ -117,7 +116,7 @@ main() {
         # `cross run` test
         case "${TARGET}" in
             thumb*-none-eabi*)
-                td=$(mktemp -d)
+                td=$(mkcargotemp -d)
 
                 git clone \
                     --depth 1 \
@@ -131,7 +130,7 @@ main() {
                 rm -rf "${td}"
             ;;
             *)
-                td=$(mktemp -d)
+                td=$(mkcargotemp -d)
 
                 cargo init --bin --name hello "${td}"
 
@@ -146,7 +145,7 @@ main() {
                 popd
 
                 rm -rf "${td}"
-                td=$(mktemp -d)
+                td=$(mkcargotemp -d)
                 git clone \
                     --depth 1 \
                     --recursive \
@@ -168,7 +167,7 @@ main() {
 
     # Test C++ support
     if (( ${CPP:-0} )); then
-        td="$(mktemp -d)"
+        td="$(mkcargotemp -d)"
 
         git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}"
 
@@ -190,7 +189,7 @@ cross_run() {
         "${CROSS[@]}" run "$@" ${CROSS_FLAGS}
     else
         for runner in ${RUNNERS}; do
-            echo -e "[target.${TARGET}]\nrunner = \"${runner}\"" > Cross.toml
+            echo -e "[target.${TARGET}]\nrunner = \"${runner}\"" > "${CARGO_TMP_DIR}"/Cross.toml
             "${CROSS[@]}" run "$@" ${CROSS_FLAGS}
         done
     fi
@@ -201,7 +200,7 @@ cross_test() {
         "${CROSS[@]}" test "$@" ${CROSS_FLAGS}
     else
         for runner in ${RUNNERS}; do
-            echo -e "[target.${TARGET}]\nrunner = \"${runner}\"" > Cross.toml
+            echo -e "[target.${TARGET}]\nrunner = \"${runner}\"" > "${CARGO_TMP_DIR}"/Cross.toml
             "${CROSS[@]}" test "$@" ${CROSS_FLAGS}
         done
     fi
@@ -212,7 +211,7 @@ cross_bench() {
         "${CROSS[@]}" bench "$@" ${CROSS_FLAGS}
     else
         for runner in ${RUNNERS}; do
-            echo -e "[target.${TARGET}]\nrunner = \"${runner}\"" > Cross.toml
+            echo -e "[target.${TARGET}]\nrunner = \"${runner}\"" > "${CARGO_TMP_DIR}"/Cross.toml
             "${CROSS[@]}" bench "$@" ${CROSS_FLAGS}
         done
     fi
diff --git a/src/tests.rs b/src/tests.rs
index b037457..2429a7b 100644
--- a/src/tests.rs
+++ b/src/tests.rs
@@ -8,7 +8,7 @@ use std::{
 use once_cell::sync::OnceCell;
 use rustc_version::VersionMeta;
 
-use crate::ToUtf8;
+use crate::{docker::ImagePlatform, rustc::QualifiedToolchain, TargetTriple, ToUtf8};
 
 static WORKSPACE: OnceCell<PathBuf> = OnceCell::new();
 
@@ -81,7 +81,12 @@ release: {version}
             expected,
             warn_host_version_mismatch(
                 &host_meta,
-                "xxxx",
+                &QualifiedToolchain::new(
+                    "xxxx",
+                    &None,
+                    &ImagePlatform::from_const_target(TargetTriple::X86_64UnknownLinuxGnu),
+                    Path::new("/toolchains/xxxx-x86_64-unknown-linux-gnu"),
+                ),
                 &target_meta.0,
                 &target_meta.1,
                 &mut msg_info,