From faf9abf5b2c3e39c86e0828c92f921b15a290233 Mon Sep 17 00:00:00 2001 From: Levi Zim Date: Mon, 24 Mar 2025 13:25:32 +0100 Subject: [PATCH] Add plain cross-compilation mode This patch introduces a "plain" cross-compilation mode that cross compiles directly using cargo build and target-specific linker(gcc). It can be used for architectures where cargo-zigbuild is broken (e.g. riscv64, loongarch64) --- book/src/ci/customizing.md | 1 + cargo-dist/src/backend/ci/github.rs | 7 ++ cargo-dist/src/build/cargo.rs | 106 +++++++++++++++++++++++++++- cargo-dist/src/lib.rs | 22 ++++-- cargo-dist/src/tasks/mod.rs | 25 ++++++- 5 files changed, 151 insertions(+), 10 deletions(-) diff --git a/book/src/ci/customizing.md b/book/src/ci/customizing.md index b8b4db2fd..8a6768efc 100644 --- a/book/src/ci/customizing.md +++ b/book/src/ci/customizing.md @@ -119,6 +119,7 @@ dist will transparently use either of: * [cargo-zigbuild](https://github.com/rust-cross/cargo-zigbuild) * [cargo-xwin](https://github.com/rust-cross/cargo-xwin) + * plain `cargo build` with target-specific linkers To try and build for the target you specified, from the host you specified. diff --git a/cargo-dist/src/backend/ci/github.rs b/cargo-dist/src/backend/ci/github.rs index 3d19c1dea..9c593b906 100644 --- a/cargo-dist/src/backend/ci/github.rs +++ b/cargo-dist/src/backend/ci/github.rs @@ -19,6 +19,7 @@ use tracing::warn; use crate::{ backend::{diff_files, templates::TEMPLATE_CI_GITHUB}, + build::cargo::target_gcc_packages, build_wrapper_for_cross, config::{ v1::{ci::github::GithubCiConfig, publishers::PublisherConfig}, @@ -336,6 +337,12 @@ impl GithubCiInfo { let mut dist_args = String::from("--artifacts=local"); for target in &targets { + let target_triple = target.parse()?; + if let Some(CargoBuildWrapper::Plain) = + build_wrapper_for_cross(&real_triple, &target_triple)? + { + dependencies.append(&mut target_gcc_packages(&real_triple, &target_triple)?); + } write!(dist_args, " --target={target}").unwrap(); } let packages_install = system_deps_install_script(&runner, &targets, &dependencies)?; diff --git a/cargo-dist/src/build/cargo.rs b/cargo-dist/src/build/cargo.rs index 06f1dfde8..df8a5a0ad 100644 --- a/cargo-dist/src/build/cargo.rs +++ b/cargo-dist/src/build/cargo.rs @@ -4,12 +4,15 @@ use std::env; use axoprocess::Cmd; use axoproject::WorkspaceIdx; -use dist_schema::target_lexicon::{Architecture, Environment, Triple}; -use dist_schema::{DistManifest, TripleName}; +use dist_schema::target_lexicon::{Architecture, Environment, OperatingSystem, Triple}; +use dist_schema::{AptPackageName, DistManifest, TripleName}; use miette::{Context, IntoDiagnostic}; use tracing::warn; use crate::build::BuildExpectations; +use crate::config::{ + DependencyKind, SystemDependencies, SystemDependency, SystemDependencyComplex, +}; use crate::env::{calculate_ldflags, fetch_brew_env, parse_env, select_brew_env}; use crate::{ build_wrapper_for_cross, errors::*, BinaryIdx, BuildStep, CargoBuildWrapper, DistGraphBuilder, @@ -162,6 +165,95 @@ impl<'a> DistGraphBuilder<'a> { } } +/// Get the apt packages for target apt linker from `host` platform. +pub fn target_gcc_packages(host: &Triple, target: &Triple) -> DistResult { + if target.operating_system != OperatingSystem::Linux { + return Err(DistError::UnsupportedCrossCompile { + host: host.clone(), + target: target.clone(), + details: "cargo-build is not a supported cross-compilation method for this combination" + .to_string(), + }); + } + let mut deps = SystemDependencies::default(); + let _env = match target.environment { + Environment::Gnu => "gnu", + Environment::Musl => { + // TODO: Currently we do not install musl cross toolchain + // because of missing apt packages. The user is up to + // install musl cross toolchain themselves. + return Ok(deps); + } + _ => { + return Err(DistError::UnsupportedCrossCompile { + host: host.clone(), + target: target.clone(), + details: format!( + "cargo-build is not a supported cross-compilation method for {}", + target.environment + ), + }); + } + }; + let linux_arch = match target.architecture { + // Strip ISA exts part from riscv triple. e.g. riscv64gc -> riscv64 + Architecture::Riscv64(_) => "riscv64".into(), + Architecture::Riscv32(_) => "riscv32".into(), + arch => arch.into_str(), + }; + deps.apt.insert( + AptPackageName::new(format!("binutils-{linux_arch}-linux-gnu")), + SystemDependency(SystemDependencyComplex { + version: None, + stage: vec![DependencyKind::Build], + targets: vec![TripleName::new(target.to_string())], + }), + ); + deps.apt.insert( + AptPackageName::new(format!("gcc-{linux_arch}-linux-gnu")), + SystemDependency(SystemDependencyComplex { + version: None, + stage: vec![DependencyKind::Build], + targets: vec![TripleName::new(target.to_string())], + }), + ); + // TODO: support homebrew + Ok(deps) +} + +/// Get the `target` specific gcc compiler to use as linker from `host` platform. +pub fn target_gcc_linker(host: &Triple, target: &Triple) -> DistResult { + if target.operating_system != OperatingSystem::Linux { + return Err(DistError::UnsupportedCrossCompile { + host: host.clone(), + target: target.clone(), + details: "cargo-build is not a supported cross-compilation method for this combination" + .to_string(), + }); + } + let env = match target.environment { + Environment::Gnu => "gnu", + Environment::Musl => "musl", + _ => { + return Err(DistError::UnsupportedCrossCompile { + host: host.clone(), + target: target.clone(), + details: format!( + "cargo-build is not a supported cross-compilation method for {}", + target.environment + ), + }); + } + }; + let linux_arch = match target.architecture { + // Strip ISA exts part from riscv triple. e.g. riscv64gc -> riscv64 + Architecture::Riscv64(_) => "riscv64".into(), + Architecture::Riscv32(_) => "riscv32".into(), + arch => arch.into_str(), + }; + Ok(format!("{linux_arch}-linux-{env}-gcc")) +} + /// Generate a `cargo build` command pub fn make_build_cargo_target_command( host: &Triple, @@ -190,6 +282,16 @@ pub fn make_build_cargo_target_command( None => { command.arg("build"); } + Some(CargoBuildWrapper::Plain) => { + command.env( + format!( + "CARGO_TARGET_{}_LINKER", + target.to_string().to_uppercase().replace('-', "_") + ), + target_gcc_linker(host, &target)?, + ); + command.arg("build"); + } Some(CargoBuildWrapper::ZigBuild) => { if auditable { return Err(DistError::CannotDoCargoAuditableAndCrossCompile { diff --git a/cargo-dist/src/lib.rs b/cargo-dist/src/lib.rs index 39c825b58..80e458636 100644 --- a/cargo-dist/src/lib.rs +++ b/cargo-dist/src/lib.rs @@ -10,7 +10,7 @@ //! It's currently not terribly well-suited to being used as a pure library because it happily //! writes to stderr/stdout whenever it pleases. Suboptimal for a library. -use std::io::Write; +use std::{borrow::Cow, io::Write}; use announce::TagSettings; use axoasset::LocalAsset; @@ -79,6 +79,7 @@ pub fn do_env_test(cfg: &Config) -> DistResult<()> { let tools = dist.tools; let host = tools.host_target.parse()?; + let mut plain_cross_targets = Vec::new(); for step in dist.local_build_steps.iter() { // Can't require cross-compilation tools if we aren't compiling. @@ -92,6 +93,9 @@ pub fn do_env_test(cfg: &Config) -> DistResult<()> { let wrapper = tasks::build_wrapper_for_cross(&host, &target)?; match wrapper { + Some(CargoBuildWrapper::Plain) => { + plain_cross_targets.push(target); + } Some(CargoBuildWrapper::Xwin) => { need_xwin = true; } @@ -109,12 +113,18 @@ pub fn do_env_test(cfg: &Config) -> DistResult<()> { // // bool::then(f) returns an Option, so we start with a // Vec>>. - let all_tools: Vec>> = vec![ - need_cargo_auditable.then(|| tools.cargo_auditable()), - need_omnibor.then(|| tools.omnibor()), - need_xwin.then(|| tools.cargo_xwin()), - need_zigbuild.then(|| tools.cargo_zigbuild()), + let mut all_tools: Vec>>> = vec![ + need_cargo_auditable.then(|| tools.cargo_auditable().map(Cow::Borrowed)), + need_omnibor.then(|| tools.omnibor().map(Cow::Borrowed)), + need_xwin.then(|| tools.cargo_xwin().map(Cow::Borrowed)), + need_zigbuild.then(|| tools.cargo_zigbuild().map(Cow::Borrowed)), ]; + all_tools.extend( + plain_cross_targets + .iter() + .map(|t| Tools::gcc_cross_toolchain(&host, t).map(Cow::Owned)) + .map(Some), + ); // Drop `None`s, then extract the values from the remaining `Option`s. let needed_tools = all_tools.into_iter().flatten(); diff --git a/cargo-dist/src/tasks/mod.rs b/cargo-dist/src/tasks/mod.rs index fe3f19897..9cab2f9a5 100644 --- a/cargo-dist/src/tasks/mod.rs +++ b/cargo-dist/src/tasks/mod.rs @@ -51,6 +51,7 @@ use std::collections::BTreeMap; use crate::backend::installer::{ExecutableZipFragment, HomebrewImpl}; +use crate::build::cargo::target_gcc_linker; use crate::platform::targets::{ TARGET_ARM64_LINUX_GNU, TARGET_ARM64_MAC, TARGET_X64_LINUX_GNU, TARGET_X64_MAC, }; @@ -58,7 +59,7 @@ use axoasset::AxoClient; use axoprocess::Cmd; use axoproject::{PackageId, PackageIdx, WorkspaceGraph}; use camino::{Utf8Path, Utf8PathBuf}; -use dist_schema::target_lexicon::{OperatingSystem, Triple}; +use dist_schema::target_lexicon::{Architecture, OperatingSystem, Triple}; use dist_schema::{ ArtifactId, BuildEnvironment, DistManifest, HomebrewPackageName, SystemId, SystemInfo, TripleName, TripleNameRef, @@ -333,6 +334,16 @@ impl Tools { tool: "cargo-zigbuild".to_owned(), }) } + + /// Returns gcc cross toolchain or an error + pub fn gcc_cross_toolchain(host: &Triple, target: &Triple) -> DistResult { + let linker = target_gcc_linker(host, target)?; + if let Some(tool) = find_tool(&linker, "--version") { + Ok(tool) + } else { + Err(DistError::ToolMissing { tool: linker }) + } + } } /// Info about the cargo toolchain we're using @@ -461,6 +472,10 @@ pub struct CargoBuildStep { /// A wrapper to use instead of `cargo build`, generally used for cross-compilation #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub enum CargoBuildWrapper { + /// Run 'cargo build' to cross-compile, e.g. from `x86_64-unknown-linux-gnu` to `aarch64-unknown-linux-gnu` + /// This wrapper sets the target linker for `cargo build`. e.g. `aarch64-linux-gnu-gcc` + Plain, + /// Run 'cargo zigbuild' to cross-compile, e.g. from `x86_64-unknown-linux-gnu` to `aarch64-unknown-linux-gnu` /// cf. ZigBuild, @@ -473,6 +488,7 @@ pub enum CargoBuildWrapper { impl std::fmt::Display for CargoBuildWrapper { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.pad(match self { + CargoBuildWrapper::Plain => "cargo-build", CargoBuildWrapper::ZigBuild => "cargo-zigbuild", CargoBuildWrapper::Xwin => "cargo-xwin", }) @@ -510,7 +526,12 @@ pub fn build_wrapper_for_cross( OperatingSystem::Linux => match host.operating_system { OperatingSystem::Linux | OperatingSystem::Darwin | OperatingSystem::Windows => { // zigbuild works for e.g. x86_64-unknown-linux-gnu => aarch64-unknown-linux-gnu - Ok(Some(CargoBuildWrapper::ZigBuild)) + match target.architecture { + Architecture::Riscv64(_) | Architecture::LoongArch64 => { + Ok(Some(CargoBuildWrapper::Plain)) + }, + _ => Ok(Some(CargoBuildWrapper::ZigBuild)) + } } _ => { Err(DistError::UnsupportedCrossCompile {