diff --git a/.changes/817.json b/.changes/817.json new file mode 100644 index 000000000..a903feb67 --- /dev/null +++ b/.changes/817.json @@ -0,0 +1,11 @@ +[ + { + "description": "Images can now specify a certain toolchain via `target.{target}.image.toolchain`", + "breaking": true, + "type": "changed" + }, + { + "description": "made `cross +channel` parsing more compliant to parsing a toolchain", + "type": "fixed" + } +] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe25ba95b..e9e469dff 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 a743533e1..d26b50f37 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 b84717f2a..848aeac1a 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 457861dd7..c6c736c49 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/ci/test-bisect.sh b/ci/test-bisect.sh index e61ac673b..0acd06b28 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 9fb57f51b..bab8a5c25 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() { @@ -27,7 +30,7 @@ main() { -v /var/run/docker.sock:/var/run/docker.sock \ "${CROSS_TARGET_CROSS_IMAGE}" sh -c ' #!/usr/bin/env sh -td="$(mktemp -d)" +td="$(mkcargotemp -d)" git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}" cd "${td}" cross run --target "${TARGET}" diff --git a/ci/test-docker-in-docker.sh b/ci/test-docker-in-docker.sh index f7705c068..d723b5051 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 000000000..2f92fdf44 --- /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 8fb3583ed..8b676f3eb 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 fbfaddaa5..61f764f0c 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/docs/cross_toml.md b/docs/cross_toml.md index 91e3f36f3..87020a792 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 a249d9558..f602b59c8 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, + /// 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, + /// 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 8468c4c31..9d634bcd6 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, + toolchain: Option, #[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 f2f8294ea..64536d296 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 = 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 84677cb38..fa5c99853 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 { 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::>>().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 a5d583016..9cc4d13ef 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 { + fn image(&self, target: &Target) -> Result> { 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::>>()?; + Ok(i) + } else { + Ok(i) + } + }) + .transpose() } fn dockerfile(&self, target: &Target) -> (Option, Option) { @@ -110,13 +124,21 @@ impl Environment { } fn doctests(&self) -> Option { - 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 { + self.get_var("CUSTOM_TOOLCHAIN_COMPAT") + } + + fn build_opts(&self) -> Option { + 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, - config: impl Fn(&CrossToml, &Target) -> Option, - ) -> Result> { - 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> { - self.string_from_config(target, Environment::image, CrossToml::image) + pub fn image(&self, target: &Target) -> Result> { + 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> { - 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 { @@ -291,6 +307,14 @@ impl Config { self.env.custom_toolchain() } + pub fn custom_toolchain_compat(&self) -> Option { + self.env.custom_toolchain_compat() + } + + pub fn build_opts(&self) -> Option { + self.env.build_opts() + } + pub fn env_passthrough(&self, target: &Target) -> Result>> { self.vec_from_config( target, diff --git a/src/cross_toml.rs b/src/cross_toml.rs index 819143f05..858faf8c2 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, build_std: Option, - image: Option, + #[serde(default, deserialize_with = "opt_string_or_struct")] + image: Option, #[serde(default, deserialize_with = "opt_string_or_struct")] dockerfile: Option, #[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 { - 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 { - 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 { - self.get_target(target) - .and_then(get_target) - .or_else(|| get_build(&self.build)) - .map(ToOwned::to_owned) - } - fn get_value( &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 cc6a0b2eb..847f3f0b3 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, impl AsRef)>, msg_info: &mut MessageInfo, ) -> Result { - 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 { + pub fn image_name( + &self, + target_triple: &TargetTriple, + metadata: &CargoMetadata, + ) -> Result { 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 728f41274..cd90e32c1 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, + pub os: Option, pub is_remote: bool, } @@ -45,16 +63,18 @@ impl Engine { is_remote: Option, msg_info: &mut MessageInfo, ) -> Result { - 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 { - let stdout = Command::new(ce) +fn get_engine_info( + ce: &Path, + msg_info: &mut MessageInfo, +) -> Result<(EngineType, Option, Option)> { + 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, 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, EngineInfoError> { + engine_info(ce, &["info", "-f", "{{ .Version.OsArch }}"], "/", msg_info) +} + +fn get_custom_info( + ce: &Path, + msg_info: &mut MessageInfo, +) -> Result, EngineInfoError> { + engine_info( + ce, + &["version", "-f", "{{ .Client.Os }},,,{{ .Client.Arch }}"], + ",,,", + msg_info, + ) +} + pub fn get_container_engine() -> Result { 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 000000000..7bfd5b9f5 --- /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, +} + +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::>(); + + 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> From 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 { + 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, + 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 = ::Err; + + fn try_from(value: &str) -> Result { + value.parse() + } +} + +impl std::str::FromStr for ImagePlatform { + type Err = eyre::Report; + // [os/arch[/variant]=]toolchain + fn from_str(s: &str) -> Result { + 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 { + let arch = target + .triple() + .split_once('-') + .ok_or_else(|| eyre::eyre!("malformed target"))? + .0; + Self::new(arch) + } + + pub fn new(s: &str) -> Result { + 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 { + 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 { + 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 { + 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 72ecd9c49..e11b2bb8c 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 aececd69b..cd80b20ce 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 000000000..7c468a0e6 --- /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 88f5c8a3a..5a97a807c 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 { - // 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 { + // 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 573ba2458..7e1459b93 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 { - 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 { - 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 { 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 { 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 { +/// Simpler version of [get_image] +pub fn get_image_name(config: &Config, target: &Target) -> Result { + if let Some(image) = config.image(target)? { + return Ok(image.name); + } + + let compatible = PROVIDED_IMAGES + .iter() + .filter(|p| p.name == target.triple()) + .collect::>(); + + 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 { 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::>(); + + 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 { "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::>() + .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> { @@ -930,11 +1019,17 @@ mod tests { Ok(path) } - fn get_sysroot() -> Result { - Ok(home()? + fn get_toolchain() -> Result { + 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 { 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 { diff --git a/src/lib.rs b/src/lib.rs index 31f6d8f1f..58dd6dd12 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 { + 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 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 for Target { - fn from(host: Host) -> Target { +impl From 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 for Target { impl From 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(&self, serializer: S) -> Result { - 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 { - 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> { 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 { } 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 { && !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 { 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 = 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 5b9a42404..e36c947c0 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, + pub(self) host: ImagePlatform, + pub is_custom: bool, + pub full: String, + pub(self) sysroot: PathBuf, +} + +impl QualifiedToolchain { + pub fn new(channel: &str, date: &Option, 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 { + 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 { + 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 { + 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 { + match toolchain.parse::() { + 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, + pub host: Option, + 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 { + 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 { }) } -pub fn sysroot(host: &Host, target: &Target, msg_info: &mut MessageInfo) -> Result { - let mut stdout = rustc_command() +pub fn sysroot(msg_info: &mut MessageInfo) -> Result { + 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::>() - .join("-") - } else { - default_toolchain.to_owned() - }; - sysroot.set_file_name(&toolchain); - - Ok((toolchain, sysroot)) -} - pub fn version_meta() -> Result { rustc_version::version_meta().wrap_err("couldn't fetch the `rustc` version") } @@ -142,6 +361,17 @@ pub fn version_meta() -> Result { 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 a13b1dad5..a467ae61a 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, - not_installed: Vec, + pub default: String, + pub installed: Vec, + pub not_installed: Vec, } 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 { + 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> { let out = rustup_command(msg_info, true) .args(&["toolchain", "list"]) @@ -60,8 +76,17 @@ pub fn installed_toolchains(msg_info: &mut MessageInfo) -> Result> { .collect()) } -pub fn available_targets(toolchain: &str, msg_info: &mut MessageInfo) -> Result { +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 { 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> { 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 { Ok(check_component(component, toolchain, msg_info)?.is_installed()) @@ -196,36 +229,39 @@ fn rustc_channel(version: &Version) -> Result { } } -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> { - 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> { + 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> { - 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> { + 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/src/tests.rs b/src/tests.rs index b03745746..2429a7bf8 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 = 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, diff --git a/xtask/src/build_docker_image.rs b/xtask/src/build_docker_image.rs index fc00310ba..70f120feb 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, + // [os/arch[/variant]=]toolchain + #[clap(long, short = 'a', action = clap::builder::ArgAction::Append)] + pub platform: Vec, /// Targets to build for #[clap()] pub targets: Vec, @@ -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::>>()?; + 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 8582dd5b1..7a9d67534 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 000000000..d686aca85 --- /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::>() + .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 9da2df92b..106d43762 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 { @@ -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 08bd8c51c..ef5093f8e 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 b5884660d..8c0629574 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 = 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>, } 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, } 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 { 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) } } }