From 95fe313c7d4b7c5f48ea8b3decf83cfa53dfc3ad Mon Sep 17 00:00:00 2001 From: Alex Huszagh Date: Sun, 17 Jul 2022 10:15:24 -0500 Subject: [PATCH] Add a `zig cc`-based image. Uses cargo-zigbuild as a backend, and adds configuration options for zig under `[build.zig]` and `[target.(...).zig]`. If enabled, and an image override is not provided, `cross` will always use the `zig` image. The feature can be enabled by providing `zig` as a table, bool, or string. It supports custom glibc versions by passing the `zig.version` key, and `zig` can be separately enabled or disabled by providing `zig.enable`. ``` [target.x86_64-unknown-linux-gnu.zig] enable = true # enable use of the zig image version = "2.17" # glibc version to use image = "ghcr.io/cross-rs/zig:local" # custom image to use ``` If provided as a bool, it will use the default glibc version: ``` [target.x86_64-unknown-linux-gnu] \# equivalent to { enable = true } zig = true ``` If provided as a string, `zig` will be automatically enabled: ``` [target.x86_64-unknown-linux-gnu] \# equivalent to { enable = true, version = "2.17" } zig = "2.17" ``` The image does not provide runners, `bindgen` Clang args, or `pkg-config` paths, since `zig cc` does not provide the dynamic library loader (`ld-linux*.so`) required, meaning none of the binaries can be run. For `bindgen`, `zig cc` has an unusual directory structure, so there is no traditional sysroot with `usr`, `lib`, and `include` subdirectories. Finally, since we don't have system packages we can work with, exporting a `pkg-config` path makes little sense. --- .changes/880.json | 5 + .github/workflows/ci.yml | 8 +- ci/test-zig-image.sh | 54 ++++++++++ docker/Dockerfile.zig | 22 ++++ docker/zig.sh | 197 ++++++++++++++++++++++++++++++++++ docs/cross_toml.md | 24 +++++ src/config.rs | 131 ++++++++++++++++++---- src/cross_toml.rs | 176 ++++++++++++++++++++++++++++++ src/docker/custom.rs | 5 +- src/docker/local.rs | 3 +- src/docker/provided_images.rs | 5 + src/docker/remote.rs | 11 +- src/docker/shared.rs | 49 ++++++--- src/lib.rs | 75 ++++++++++++- xtask/src/codegen.rs | 2 +- xtask/src/util.rs | 8 +- 16 files changed, 729 insertions(+), 46 deletions(-) create mode 100644 .changes/880.json create mode 100755 ci/test-zig-image.sh create mode 100644 docker/Dockerfile.zig create mode 100755 docker/zig.sh diff --git a/.changes/880.json b/.changes/880.json new file mode 100644 index 000000000..978cdcab1 --- /dev/null +++ b/.changes/880.json @@ -0,0 +1,5 @@ +{ + "description": "added a zig-based image, allowing multiple targets to be built from the same image, using cargo-zigbuild.", + "type": "added", + "issues": [860] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f446bd085..382ad49e3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -209,6 +209,7 @@ jobs: - { target: thumbv7em-none-eabihf, os: ubuntu-latest, std: 1 } - { target: thumbv7m-none-eabi, os: ubuntu-latest, std: 1 } - { target: cross, os: ubuntu-latest } + - { target: zig, os: ubuntu-latest } build: name: target (${{ matrix.pretty }},${{ matrix.os }}) @@ -247,7 +248,7 @@ jobs: - name: LLVM instrument coverage id: cov uses: ./.github/actions/cargo-llvm-cov - if: steps.prepare-meta.outputs.has-image + if: steps.prepare-meta.outputs.has-image && steps.prepare-meta.outputs.test-variant != 'zig' with: name: cross-${{matrix.pretty}} @@ -302,6 +303,11 @@ jobs: target: ${{ matrix.target }} image: ${{ steps.build-docker-image.outputs.image }} + - name: Test Zig Image + if: steps.prepare-meta.outputs.has-image && steps.prepare-meta.outputs.test-variant == 'zig' + run: ./ci/test-zig-image.sh + shell: bash + - name: Test Cross Image if: steps.prepare-meta.outputs.has-image && steps.prepare-meta.outputs.test-variant == 'cross' run: ./ci/test-cross-image.sh diff --git a/ci/test-zig-image.sh b/ci/test-zig-image.sh new file mode 100755 index 000000000..2468b45a4 --- /dev/null +++ b/ci/test-zig-image.sh @@ -0,0 +1,54 @@ +#!/usr/bin/env bash +# shellcheck disable=SC2086,SC1091,SC1090 + +set -x +set -eo pipefail + +# NOTE: "${@}" is an unbound variable for bash 3.2, which is the +# installed version on macOS. likewise, "${var[@]}" is an unbound +# error if var is an empty array. + +ci_dir=$(dirname "${BASH_SOURCE[0]}") +ci_dir=$(realpath "${ci_dir}") +. "${ci_dir}"/shared.sh + +# zig cc is very slow: only use a few targets. +TARGETS=( + "aarch64-unknown-linux-gnu" + "aarch64-unknown-linux-musl" + "i586-unknown-linux-gnu" + "i586-unknown-linux-musl" +) + +# on CI, it sets `CROSS_TARGET_ZIG_IMAGE` rather than `CROSS_TARGET_ZIG_IMAGE` +if [[ -n "${CROSS_TARGET_ZIG_IMAGE}" ]]; then + export CROSS_BUILD_ZIG_IMAGE="${CROSS_TARGET_ZIG_IMAGE}" + unset CROSS_TARGET_ZIG_IMAGE +fi + +main() { + export CROSS_BUILD_ZIG=1 + + local td= + local target= + + retry cargo fetch + cargo build + export CROSS="${PROJECT_HOME}/target/debug/cross" + + td="$(mktemp -d)" + git clone --depth 1 https://github.com/cross-rs/rust-cpp-hello-word "${td}" + pushd "${td}" + + for target in "${TARGETS[@]}"; do + CROSS_CONTAINER_ENGINE="${CROSS_ENGINE}" "${CROSS}" build --target "${target}" --verbose + # note: ensure #724 doesn't replicate during CI. + # https://github.com/cross-rs/cross/issues/724 + cargo clean + done + + popd + rm -rf "${td}" +} + +main "${@}" diff --git a/docker/Dockerfile.zig b/docker/Dockerfile.zig new file mode 100644 index 000000000..ebca27452 --- /dev/null +++ b/docker/Dockerfile.zig @@ -0,0 +1,22 @@ +FROM ubuntu:20.04 +ARG DEBIAN_FRONTEND=noninteractive + +COPY common.sh lib.sh / +RUN /common.sh + +COPY cmake.sh / +RUN /cmake.sh + +COPY xargo.sh / +RUN /xargo.sh + +ARG TARGETPLATFORM +COPY zig.sh / +RUN /zig.sh $TARGETPLATFORM + +# we don't export `BINDGEN_EXTRA_CLANG_ARGS`, `QEMU_LD_PREFIX`, or +# `PKG_CONFIG_PATH` since zig doesn't have a traditional sysroot structure, +# and we're not using standard, shared packages. none of the packages +# have runners either, since they do not ship with the required +# dynamic linker (`ld-linux-${arch}.so`). +ENV PATH=$PATH:/opt/zig diff --git a/docker/zig.sh b/docker/zig.sh new file mode 100755 index 000000000..d41ee3742 --- /dev/null +++ b/docker/zig.sh @@ -0,0 +1,197 @@ +#!/usr/bin/env bash + +set -x +set -eo pipefail + +# shellcheck disable=SC1091 +. lib.sh + +main() { + local platform="${1}" + install_packages ca-certificates curl xz-utils + + install_zig "${platform}" + install_zigbuild "${platform}" + + purge_packages + rm "${0}" +} + +install_zig() { + local platform="${1}" + local version="0.9.1" + local dst="/opt/zig" + local arch= + local os= + local triple= + + case "${platform}" in + 'linux/386') + arch="i386" + os="linux" + ;; + 'linux/amd64') + arch="x86_64" + os="linux" + ;; + 'linux/arm64') + arch="aarch64" + os="linux" + ;; + 'linux/riscv64') + arch="riscv64" + os="linux" + ;; + 'linux/ppc64le') + triple="powerpc64le-linux-gnu" + ;; + 'linux/s390x') + triple="s390x-linux-gnu" + ;; + 'darwin/amd64') + arch="x86_64" + os="macos" + ;; + 'darwin/arm64') + arch="aarch64" + os="macos" + ;; + # NOTE: explicitly don't support linux/arm/v6 + *) + echo "Unsupported target platform '${platform}'" 1>&2 + exit 1 + ;; + esac + + if [[ -n "${arch}" ]]; then + install_zig_tarball "${arch}" "${os}" "${version}" "${dst}" + else + install_zig_source "${triple}" "${version}" "${dst}" + fi +} + +install_zig_tarball() { + local arch="${1}" + local os="${2}" + local version="${3}" + local dst="${4}" + local filename="zig-${os}-${arch}-${version}.tar.xz" + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://ziglang.org/download/${version}/${filename}" -O + mkdir -p "${dst}" + tar --strip-components=1 -xJf "${filename}" --directory "${dst}" + + popd + + rm -rf "${td}" +} + +install_zig_source() { + local triple="${1}" + local version="${2}" + local dst="${3}" + local filename="zig-bootstrap-${version}.tar.xz" + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "https://ziglang.org/download/${version}/${filename}" -O + mkdir zig + tar --strip-components=1 -xJf "${filename}" --directory zig + + pushd zig + install_packages python3 make g++ + ./build -j5 "${triple}" native + mv "out/zig-${triple}-native" /opt/zig + + popd + popd + + rm -rf "${td}" +} + +install_zigbuild() { + local platform="${1}" + local version=0.11.0 + local dst="/usr/local" + local triple= + + # we don't know if `linux/arm/v7` is hard-float, + # and we don't know the the zigbuild `apple-darwin` + # target doesn't manually specify the architecture. + case "${platform}" in + 'linux/386') + triple="i686-unknown-linux-musl" + ;; + 'linux/amd64') + triple="x86_64-unknown-linux-musl" + ;; + 'linux/arm64') + triple="aarch64-unknown-linux-musl" + ;; + *) + ;; + esac + + if [[ -n "${triple}" ]]; then + install_zigbuild_tarball "${triple}" "${version}" "${dst}" + else + install_zigbuild_source "${version}" "${dst}" + fi +} + +install_zigbuild_tarball() { + local triple="${1}" + local version="${2}" + local dst="${3}" + local repo="https://github.com/messense/cargo-zigbuild" + local filename="cargo-zigbuild-v${version}.${triple}.tar.gz" + + local td + td="$(mktemp -d)" + + pushd "${td}" + + curl --retry 3 -sSfL "${repo}/releases/download/v${version}/${filename}" -O + mkdir -p "${dst}/bin" + tar -xzf "${filename}" --directory "${dst}/bin" + + popd + + rm -rf "${td}" +} + +install_zigbuild_source() { + local version="${1}" + local dst="${2}" + + local td + td="$(mktemp -d)" + + pushd "${td}" + + export RUSTUP_HOME="${td}/rustup" + export CARGO_HOME="${td}/cargo" + + curl --retry 3 -sSfL https://sh.rustup.rs -o rustup-init.sh + sh rustup-init.sh -y --no-modify-path --profile minimal + + PATH="${CARGO_HOME}/bin:${PATH}" \ + cargo install cargo-zigbuild \ + --version "${version}" \ + --root "${dst}" \ + --locked + + popd + + rm -rf "${td}" +} + +main "${@}" diff --git a/docs/cross_toml.md b/docs/cross_toml.md index 87020a792..98b935e0b 100644 --- a/docs/cross_toml.md +++ b/docs/cross_toml.md @@ -33,6 +33,7 @@ The `target` key allows you to specify parameters for specific compilation targe [target.aarch64-unknown-linux-gnu] xargo = false build-std = false +zig = "2.17" image = "test-image" pre-build = ["apt-get update"] # can also be the path to a file to run runner = "custom-runner" @@ -92,3 +93,26 @@ also supports [target.x86_64-unknown-linux-gnu] dockerfile = "./Dockerfile" ``` + +# `target.TARGET.zig` + +```toml +[target.x86_64-unknown-linux-gnu.zig] +enable = true # enable use of the zig image +version = "2.17" # glibc version to use +image = "zig:local" # custom zig image to use +``` + +also supports + +```toml +[target.x86_64-unknown-linux-gnu] +zig = true +``` + +or + +```toml +[target.x86_64-unknown-linux-gnu] +zig = "2.17" +``` diff --git a/src/config.rs b/src/config.rs index 9cc4d13ef..a0968f6f9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -67,29 +67,46 @@ impl Environment { self.get_values_for("BUILD_STD", target, bool_from_envvar) } + fn zig(&self, target: &Target) -> (Option, Option) { + self.get_values_for("ZIG", target, bool_from_envvar) + } + + fn zig_version(&self, target: &Target) -> (Option, Option) { + self.get_values_for("ZIG_VERSION", target, ToOwned::to_owned) + } + + fn zig_image(&self, target: &Target) -> Result<(Option, Option)> { + let get_build = |env: &Environment, var: &str| env.get_build_var(var); + let get_target = |env: &Environment, var: &str| env.get_target_var(target, var); + let env_build = get_possible_image( + self, + "ZIG_IMAGE", + "ZIG_IMAGE_TOOLCHAIN", + get_build, + get_build, + )?; + let env_target = get_possible_image( + self, + "ZIG_IMAGE", + "ZIG_IMAGE_TOOLCHAIN", + get_target, + get_target, + )?; + + Ok((env_build, env_target)) + } + 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() + let get_target = |env: &Environment, var: &str| env.get_target_var(target, var); + get_possible_image(self, "IMAGE", "IMAGE_TOOLCHAIN", get_target, get_target) } fn dockerfile(&self, target: &Target) -> (Option, Option) { - self.get_values_for("DOCKERFILE", target, |s| s.to_owned()) + self.get_values_for("DOCKERFILE", target, ToOwned::to_owned) } fn dockerfile_context(&self, target: &Target) -> (Option, Option) { - self.get_values_for("DOCKERFILE_CONTEXT", target, |s| s.to_owned()) + self.get_values_for("DOCKERFILE_CONTEXT", target, ToOwned::to_owned) } fn pre_build(&self, target: &Target) -> (Option, Option) { @@ -142,6 +159,29 @@ impl Environment { } } +fn get_possible_image( + env: &Environment, + image_var: &str, + toolchain_var: &str, + get_image: impl Fn(&Environment, &str) -> Option, + get_toolchain: impl Fn(&Environment, &str) -> Option, +) -> Result> { + get_image(env, image_var) + .map(Into::into) + .map(|mut i: PossibleImage| { + if let Some(toolchain) = get_toolchain(env, toolchain_var) { + i.toolchain = toolchain + .split(',') + .map(|t| ImagePlatform::from_target(t.trim().into())) + .collect::>>()?; + Ok(i) + } else { + Ok(i) + } + }) + .transpose() +} + fn split_to_cloned_by_ws(string: &str) -> Vec { string.split_whitespace().map(String::from).collect() } @@ -269,6 +309,36 @@ impl Config { } } + fn get_from_value( + &self, + target: &Target, + env: impl Fn(&Environment, &Target) -> (Option, Option), + config: impl Fn(&CrossToml, &Target) -> (Option, Option), + ) -> Result> { + let (env_build, env_target) = env(&self.env, target); + + if let Some(env_target) = env_target { + return Ok(Some(env_target)); + } + + let (build, target) = self + .toml + .as_ref() + .map(|t| config(t, target)) + .unwrap_or_default(); + + // FIXME: let expression + if target.is_none() && env_build.is_some() { + return Ok(env_build); + } + + if target.is_none() { + Ok(build) + } else { + Ok(target) + } + } + #[cfg(test)] fn new_with(toml: Option, env: Environment) -> Self { Config { toml, env } @@ -282,6 +352,19 @@ impl Config { self.bool_from_config(target, Environment::build_std, CrossToml::build_std) } + pub fn zig(&self, target: &Target) -> Option { + self.bool_from_config(target, Environment::zig, CrossToml::zig) + } + + pub fn zig_version(&self, target: &Target) -> Result> { + self.get_from_value(target, Environment::zig_version, CrossToml::zig_version) + } + + pub fn zig_image(&self, target: &Target) -> Result> { + let (b, t) = self.env.zig_image(target)?; + self.get_from_value(target, |_, _| (b.clone(), t.clone()), CrossToml::zig_image) + } + pub fn image(&self, target: &Target) -> Result> { let env = self.env.image(target)?; self.get_from_ref( @@ -438,24 +521,36 @@ mod tests { use super::*; #[test] - pub fn parse_error_in_env() { + pub fn parse_error_in_env() -> Result<()> { let mut map = std::collections::HashMap::new(); map.insert("CROSS_BUILD_XARGO", "tru"); map.insert("CROSS_BUILD_STD", "false"); + map.insert("CROSS_BUILD_ZIG_IMAGE", "zig:local"); let env = Environment::new(Some(map)); assert_eq!(env.xargo(&target()), (Some(true), None)); assert_eq!(env.build_std(&target()), (Some(false), None)); + assert_eq!(env.zig(&target()), (None, None)); + assert_eq!(env.zig_version(&target()), (None, None)); + assert_eq!(env.zig_image(&target())?, (Some("zig:local".into()), None)); + + Ok(()) } #[test] - pub fn build_and_target_set_returns_tuple() { + pub fn build_and_target_set_returns_tuple() -> Result<()> { let mut map = std::collections::HashMap::new(); map.insert("CROSS_BUILD_XARGO", "true"); + map.insert("CROSS_BUILD_ZIG", "true"); + map.insert("CROSS_BUILD_ZIG_VERSION", "2.17"); map.insert("CROSS_TARGET_AARCH64_UNKNOWN_LINUX_GNU_XARGO", "false"); let env = Environment::new(Some(map)); assert_eq!(env.xargo(&target()), (Some(true), Some(false))); + assert_eq!(env.zig(&target()), (Some(true), None)); + assert_eq!(env.zig_version(&target()), (Some("2.17".into()), None)); + + Ok(()) } #[test] diff --git a/src/cross_toml.rs b/src/cross_toml.rs index 858faf8c2..8068e222f 100644 --- a/src/cross_toml.rs +++ b/src/cross_toml.rs @@ -25,6 +25,8 @@ pub struct CrossBuildConfig { env: CrossEnvConfig, xargo: Option, build_std: Option, + #[serde(default, deserialize_with = "opt_string_bool_or_struct")] + zig: Option, default_target: Option, #[serde(default, deserialize_with = "opt_string_or_string_vec")] pre_build: Option, @@ -38,6 +40,8 @@ pub struct CrossBuildConfig { pub struct CrossTargetConfig { xargo: Option, build_std: Option, + #[serde(default, deserialize_with = "opt_string_bool_or_struct")] + zig: Option, #[serde(default, deserialize_with = "opt_string_or_struct")] image: Option, #[serde(default, deserialize_with = "opt_string_or_struct")] @@ -70,6 +74,44 @@ impl FromStr for CrossTargetDockerfileConfig { } } +/// Zig configuration +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "kebab-case")] +pub struct CrossZigConfig { + enable: Option, + version: Option, + #[serde(default, deserialize_with = "opt_string_or_struct")] + image: Option, +} + +impl From<&str> for CrossZigConfig { + fn from(s: &str) -> CrossZigConfig { + CrossZigConfig { + enable: Some(true), + version: Some(s.to_owned()), + image: None, + } + } +} + +impl From for CrossZigConfig { + fn from(s: bool) -> CrossZigConfig { + CrossZigConfig { + enable: Some(s), + version: None, + image: None, + } + } +} + +impl FromStr for CrossZigConfig { + type Err = std::convert::Infallible; + + fn from_str(s: &str) -> Result { + Ok(s.into()) + } +} + /// Cross configuration #[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Default)] pub struct CrossToml { @@ -275,6 +317,33 @@ impl CrossToml { self.get_value(target, |b| b.build_std, |t| t.build_std) } + /// Returns the `{}.zig` or `{}.zig.version` part of `Cross.toml` + pub fn zig(&self, target: &Target) -> (Option, Option) { + self.get_value( + target, + |b| b.zig.as_ref().and_then(|z| z.enable), + |t| t.zig.as_ref().and_then(|z| z.enable), + ) + } + + /// Returns the `{}.zig` or `{}.zig.version` part of `Cross.toml` + pub fn zig_version(&self, target: &Target) -> (Option, Option) { + self.get_value( + target, + |b| b.zig.as_ref().and_then(|c| c.version.clone()), + |t| t.zig.as_ref().and_then(|c| c.version.clone()), + ) + } + + /// Returns the `{}.zig.image` part of `Cross.toml` + pub fn zig_image(&self, target: &Target) -> (Option, Option) { + self.get_value( + target, + |b| b.zig.as_ref().and_then(|c| c.image.clone()), + |t| t.zig.as_ref().and_then(|c| c.image.clone()), + ) + } + /// Returns the list of environment variables to pass through for `build` and `target` pub fn env_passthrough(&self, target: &Target) -> (Option<&[String]>, Option<&[String]>) { self.get_ref( @@ -442,6 +511,68 @@ where deserializer.deserialize_any(StringOrStringVec(PhantomData)) } +fn opt_string_bool_or_struct<'de, T, D>(deserializer: D) -> Result, D::Error> +where + T: Deserialize<'de> + From + std::str::FromStr, + D: serde::Deserializer<'de>, +{ + use std::{fmt, marker::PhantomData}; + + use serde::de::{self, MapAccess, Visitor}; + + struct StringBoolOrStruct(PhantomData T>); + + impl<'de, T> Visitor<'de> for StringBoolOrStruct + where + T: Deserialize<'de> + From + std::str::FromStr, + { + type Value = Option; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("string, bool, or map") + } + + fn visit_bool(self, value: bool) -> Result + where + E: de::Error, + { + Ok(Some(From::from(value))) + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + Ok(FromStr::from_str(value).ok()) + } + + fn visit_map(self, map: M) -> Result + where + M: MapAccess<'de>, + { + let t: Result = + Deserialize::deserialize(de::value::MapAccessDeserializer::new(map)); + t.map(Some) + } + + fn visit_none(self) -> Result + where + E: de::Error, + { + Ok(None) + } + + fn visit_unit(self) -> Result + where + E: de::Error, + { + Ok(None) + } + } + + deserializer.deserialize_any(StringBoolOrStruct(PhantomData)) +} + #[cfg(test)] mod tests { use crate::docker::ImagePlatform; @@ -486,6 +617,7 @@ mod tests { }, xargo: Some(true), build_std: None, + zig: None, default_target: None, pre_build: Some(PreBuild::Lines(vec![p!("echo 'Hello World!'")])), dockerfile: None, @@ -523,12 +655,35 @@ mod tests { }, xargo: Some(false), build_std: Some(true), + zig: None, image: Some("test-image".into()), runner: None, dockerfile: None, pre_build: Some(PreBuild::Lines(vec![])), }, ); + target_map.insert( + Target::BuiltIn { + triple: "aarch64-unknown-linux-musl".into(), + }, + CrossTargetConfig { + env: CrossEnvConfig { + passthrough: None, + volumes: None, + }, + xargo: None, + build_std: None, + zig: Some(CrossZigConfig { + enable: Some(true), + version: Some(p!("2.17")), + image: Some("zig:local".into()), + }), + image: None, + runner: None, + dockerfile: None, + pre_build: None, + }, + ); let cfg = CrossToml { targets: target_map, @@ -544,6 +699,11 @@ mod tests { build-std = true image = "test-image" pre-build = [] + + [target.aarch64-unknown-linux-musl.zig] + enable = true + version = "2.17" + image = "zig:local" "#; let (parsed_cfg, unused) = CrossToml::parse_from_cross(test_str, &mut m!())?; @@ -563,6 +723,7 @@ mod tests { CrossTargetConfig { xargo: Some(false), build_std: None, + zig: None, image: Some(PossibleImage { name: "test-image".to_owned(), toolchain: vec![ImagePlatform::from_target( @@ -592,6 +753,16 @@ mod tests { }, xargo: Some(true), build_std: None, + zig: Some(CrossZigConfig { + enable: None, + version: None, + image: Some(PossibleImage { + name: "zig:local".to_owned(), + toolchain: vec![ImagePlatform::from_target( + "aarch64-unknown-linux-gnu".into(), + )?], + }), + }), default_target: None, pre_build: Some(PreBuild::Lines(vec![])), dockerfile: None, @@ -603,6 +774,10 @@ mod tests { xargo = true pre-build = [] + [build.zig.image] + name = "zig:local" + toolchain = ["aarch64-unknown-linux-gnu"] + [build.env] passthrough = [] @@ -652,6 +827,7 @@ mod tests { }, build_std: None, xargo: Some(true), + zig: None, default_target: None, pre_build: None, dockerfile: None, diff --git a/src/docker/custom.rs b/src/docker/custom.rs index 847f3f0b3..bfeba1f3d 100644 --- a/src/docker/custom.rs +++ b/src/docker/custom.rs @@ -70,6 +70,7 @@ impl<'a> Dockerfile<'a> { build_args: impl IntoIterator, impl AsRef)>, msg_info: &mut MessageInfo, ) -> Result { + let uses_zig = options.cargo_variant.uses_zig(); let mut docker_build = docker::subcommand(&options.engine, "buildx"); docker_build.arg("build"); docker_build.env("DOCKER_SCAN_SUGGEST", "false"); @@ -130,7 +131,9 @@ impl<'a> Dockerfile<'a> { }; if matches!(self, Dockerfile::File { .. }) { - if let Ok(cross_base_image) = self::get_image_name(&options.config, &options.target) { + if let Ok(cross_base_image) = + self::get_image_name(&options.config, &options.target, uses_zig) + { docker_build.args([ "--build-arg", &format!("CROSS_BASE_IMAGE={cross_base_image}"), diff --git a/src/docker/local.rs b/src/docker/local.rs index 05e1b3d37..493c4ef5e 100644 --- a/src/docker/local.rs +++ b/src/docker/local.rs @@ -28,7 +28,7 @@ pub(crate) fn run( let engine = &options.engine; let dirs = &paths.directories; - let mut cmd = cargo_safe_command(options.uses_xargo); + let mut cmd = cargo_safe_command(options.cargo_variant); cmd.args(args); let mut docker = subcommand(engine, "run"); @@ -43,6 +43,7 @@ pub(crate) fn run( &options.config, dirs, &options.target, + options.cargo_variant, msg_info, )?; diff --git a/src/docker/provided_images.rs b/src/docker/provided_images.rs index 7c468a0e6..9e4ee4590 100644 --- a/src/docker/provided_images.rs +++ b/src/docker/provided_images.rs @@ -273,4 +273,9 @@ pub static PROVIDED_IMAGES: &[ProvidedImage] = &[ platforms: &[ImagePlatform::DEFAULT], sub: None }, + ProvidedImage { + name: "zig", + platforms: &[ImagePlatform::DEFAULT], + sub: None + }, ]; diff --git a/src/docker/remote.rs b/src/docker/remote.rs index cfc5cf32c..23215f638 100644 --- a/src/docker/remote.rs +++ b/src/docker/remote.rs @@ -1124,7 +1124,7 @@ pub(crate) fn run( final_args.push("--target-dir".to_owned()); final_args.push(target_dir_string); } - let mut cmd = cargo_safe_command(options.uses_xargo); + let mut cmd = cargo_safe_command(options.cargo_variant); cmd.args(final_args); // 5. create symlinks for copied data @@ -1174,7 +1174,14 @@ symlink_recurse \"${{prefix}}\" // 6. execute our cargo command inside the container let mut docker = subcommand(engine, "exec"); docker_user_id(&mut docker, engine.kind); - docker_envvars(&mut docker, &options.config, dirs, target, msg_info)?; + docker_envvars( + &mut docker, + &options.config, + dirs, + target, + options.cargo_variant, + msg_info, + )?; docker_cwd(&mut docker, &paths)?; docker.arg(&container); docker.args(&["sh", "-c", &build_command(dirs, &cmd)]); diff --git a/src/docker/shared.rs b/src/docker/shared.rs index af6d13d23..5ce582d37 100644 --- a/src/docker/shared.rs +++ b/src/docker/shared.rs @@ -16,7 +16,7 @@ use crate::file::{self, write_file, PathExt, ToUtf8}; use crate::id; use crate::rustc::QualifiedToolchain; use crate::shell::{MessageInfo, Verbosity}; -use crate::Target; +use crate::{CargoVariant, Target}; pub use super::custom::CROSS_CUSTOM_DOCKERFILE_IMAGE_PREFIX; @@ -36,7 +36,7 @@ pub struct DockerOptions { pub target: Target, pub config: Config, pub image: Image, - pub uses_xargo: bool, + pub cargo_variant: CargoVariant, } impl DockerOptions { @@ -45,14 +45,14 @@ impl DockerOptions { target: Target, config: Config, image: Image, - uses_xargo: bool, + cargo_variant: CargoVariant, ) -> DockerOptions { DockerOptions { engine, target, config, image, - uses_xargo, + cargo_variant, } } @@ -458,12 +458,8 @@ pub fn parse_docker_opts(value: &str) -> Result> { shell_words::split(value).wrap_err_with(|| format!("could not parse docker opts of {}", value)) } -pub(crate) fn cargo_safe_command(uses_xargo: bool) -> SafeCommand { - if uses_xargo { - SafeCommand::new("xargo") - } else { - SafeCommand::new("cargo") - } +pub(crate) fn cargo_safe_command(cargo_variant: CargoVariant) -> SafeCommand { + SafeCommand::new(cargo_variant.to_str()) } fn add_cargo_configuration_envvars(docker: &mut Command) { @@ -505,6 +501,7 @@ pub(crate) fn docker_envvars( config: &Config, dirs: &Directories, target: &Target, + cargo_variant: CargoVariant, msg_info: &mut MessageInfo, ) -> Result<()> { for ref var in config.env_passthrough(target)?.unwrap_or_default() { @@ -523,6 +520,10 @@ pub(crate) fn docker_envvars( .args(&["-e", &format!("CARGO_HOME={}", dirs.cargo_mount_path())]) .args(&["-e", "CARGO_TARGET_DIR=/target"]) .args(&["-e", &cross_runner]); + if cargo_variant.uses_zig() { + // otherwise, zig has a permission error trying to create the cache + docker.args(&["-e", "XDG_CACHE_HOME=/target/.zig-cache"]); + } add_cargo_configuration_envvars(docker); if let Some(username) = id::username().wrap_err("could not get username")? { @@ -693,19 +694,26 @@ pub(crate) fn docker_seccomp( } /// Simpler version of [get_image] -pub fn get_image_name(config: &Config, target: &Target) -> Result { +pub fn get_image_name(config: &Config, target: &Target, uses_zig: bool) -> Result { if let Some(image) = config.image(target)? { return Ok(image.name); } + let target_name = match uses_zig { + true => match config.zig_image(target)? { + Some(image) => return Ok(image.name), + None => "zig", + }, + false => target.triple(), + }; let compatible = PROVIDED_IMAGES .iter() - .filter(|p| p.name == target.triple()) + .filter(|p| p.name == target_name) .collect::>(); if compatible.is_empty() { eyre::bail!( - "`cross` does not provide a Docker image for target {target}, \ + "`cross` does not provide a Docker image for target {target_name}, \ specify a custom image in `Cross.toml`." ); } @@ -722,19 +730,26 @@ pub fn get_image_name(config: &Config, target: &Target) -> Result { .image_name(CROSS_IMAGE, version)) } -pub(crate) fn get_image(config: &Config, target: &Target) -> Result { +pub(crate) fn get_image(config: &Config, target: &Target, uses_zig: bool) -> Result { if let Some(image) = config.image(target)? { return Ok(image); } + let target_name = match uses_zig { + true => match config.zig_image(target)? { + Some(image) => return Ok(image), + None => "zig", + }, + false => target.triple(), + }; let compatible = PROVIDED_IMAGES .iter() - .filter(|p| p.name == target.triple()) + .filter(|p| p.name == target_name) .collect::>(); if compatible.is_empty() { eyre::bail!( - "`cross` does not provide a Docker image for target {target}, \ + "`cross` does not provide a Docker image for target {target_name}, \ specify a custom image in `Cross.toml`." ); } @@ -762,7 +777,7 @@ pub(crate) fn get_image(config: &Config, target: &Target) -> Result Result { + match (uses_zig, uses_xargo) { + (true, true) => eyre::bail!("cannot use both zig and xargo"), + (true, false) => Ok(CargoVariant::Zig), + (false, true) => Ok(CargoVariant::Xargo), + (false, false) => Ok(CargoVariant::Cargo), + } + } + + pub fn to_str(self) -> &'static str { + match self { + CargoVariant::Cargo => "cargo", + CargoVariant::Xargo => "xargo", + CargoVariant::Zig => "cargo-zigbuild", + } + } + + pub fn uses_xargo(self) -> bool { + self == CargoVariant::Xargo + } + + pub fn uses_zig(self) -> bool { + self == CargoVariant::Zig + } +} + fn warn_on_failure( target: &Target, toolchain: &QualifiedToolchain, @@ -453,6 +487,14 @@ fn warn_on_failure( } Ok(()) } + +fn add_libc_version(triple: &str, zig_version: Option<&str>) -> String { + match zig_version { + Some(libc) => format!("{triple}.{libc}"), + None => triple.to_owned(), + } +} + pub fn run( args: Args, target_list: TargetList, @@ -479,10 +521,12 @@ pub fn run( .unwrap_or_else(|| Target::from(host.triple(), &target_list)); config.confusable_target(&target, msg_info)?; + let uses_zig = config.zig(&target).unwrap_or(false); + let zig_version = config.zig_version(&target)?; // 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) { + let image = match docker::get_image(&config, &target, uses_zig) { Ok(i) => i, Err(err) => { msg_info.warn(err)?; @@ -568,6 +612,7 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = 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()); + let cargo_variant = CargoVariant::create(uses_zig, uses_xargo)?; 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. @@ -596,6 +641,7 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = let needs_interpreter = args.subcommand.map_or(false, |sc| sc.needs_interpreter()); + let add_libc = |triple: &str| add_libc_version(triple, zig_version.as_deref()); let mut filtered_args = if args .subcommand .map_or(false, |s| !s.needs_target_in_command()) @@ -616,8 +662,24 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = } else if !args.all.iter().any(|a| a.starts_with("--target")) { let mut args_with_target = args.all.clone(); args_with_target.push("--target".to_owned()); - args_with_target.push(target.triple().to_owned()); + args_with_target.push(add_libc(target.triple())); args_with_target + } else if zig_version.is_some() { + let mut filtered_args = Vec::new(); + let mut args_iter = args.all.clone().into_iter(); + while let Some(arg) = args_iter.next() { + if arg == "--target" { + filtered_args.push("--target".to_owned()); + if let Some(triple) = args_iter.next() { + filtered_args.push(add_libc(&triple)); + } + } else if let Some(stripped) = arg.strip_prefix("--target=") { + filtered_args.push(format!("--target={}", add_libc(stripped))); + } else { + filtered_args.push(arg); + } + } + filtered_args } else { args.all.clone() }; @@ -643,8 +705,13 @@ To override the toolchain mounted in the image, set `target.{}.image.toolchain = } let paths = docker::DockerPaths::create(&engine, metadata, cwd, toolchain.clone())?; - let options = - docker::DockerOptions::new(engine, target.clone(), config, image, uses_xargo); + let options = docker::DockerOptions::new( + engine, + target.clone(), + config, + image, + cargo_variant, + ); 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)); diff --git a/xtask/src/codegen.rs b/xtask/src/codegen.rs index d686aca85..363ac153c 100644 --- a/xtask/src/codegen.rs +++ b/xtask/src/codegen.rs @@ -28,7 +28,7 @@ pub static PROVIDED_IMAGES: &[ProvidedImage] = &["##, for image_target in get_matrix() .iter() - .filter(|i| i.to_image_target().is_standard_target_image()) + .filter(|i| i.to_image_target().is_toolchain_image()) { write!( &mut images, diff --git a/xtask/src/util.rs b/xtask/src/util.rs index 8c0629574..c761163d4 100644 --- a/xtask/src/util.rs +++ b/xtask/src/util.rs @@ -146,7 +146,13 @@ impl ImageTarget { /// 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() + !matches!(self.name.as_ref(), "cross" | "zig") && self.has_ci_image() + } + + // this exists solely for zig, since we also want it as a provided target. + /// Determine if this target has a toolchain image + pub fn is_toolchain_image(&self) -> bool { + !matches!(self.name.as_ref(), "cross") && self.has_ci_image() } /// Determine if this target needs to interact with the project root.