diff --git a/src/commands/package.rs b/src/commands/package.rs index e25c208..a91a2c0 100644 --- a/src/commands/package.rs +++ b/src/commands/package.rs @@ -206,19 +206,27 @@ fn run_for_crate( } } - if !skip_toolchains_check { - let missing_toolchains = check_installed_toolchains(&targets); - let nightly_toolchains = check_nightly_installed(&targets); + let toolchain_targets = ToolchainTargets::query(&targets); - let installation_required = - &[missing_toolchains.as_slice(), nightly_toolchains.as_slice()].concat(); + if !skip_toolchains_check { + let missing_stable = check_stable_missing_targets(&targets, &toolchain_targets); + let missing_nightly_targets = check_nightly_missing_targets(&targets, &toolchain_targets); + let missing_nightly_src = check_nightly_src_installed(&targets, &toolchain_targets); + + let installation_required = &[ + missing_stable.as_slice(), + missing_nightly_targets.as_slice(), + missing_nightly_src.as_slice(), + ] + .concat(); if !installation_required.is_empty() { if config.accept_all || prompt_toolchain_installation(installation_required) { - install_toolchains(&missing_toolchains, config.silent)?; - if !nightly_toolchains.is_empty() { + install_toolchains(&missing_stable, config.silent)?; + if !missing_nightly_targets.is_empty() || !missing_nightly_src.is_empty() { install_nightly_src(config.silent)?; } + install_nightly_targets(&missing_nightly_targets, config.silent)?; } else { Err("Toolchains for some target platforms were missing!")?; } @@ -227,7 +235,15 @@ fn run_for_crate( let crate_name = lib.name.replace('-', "_"); for target in &targets { - build_with_output(target, &crate_name, mode, lib_type, config, &features)?; + build_with_output( + target, + &crate_name, + mode, + lib_type, + config, + &features, + &toolchain_targets, + )?; } let ffi_module_name = @@ -419,41 +435,46 @@ fn prompt_platforms(accept_all: bool) -> Vec { .collect() } -/// Checks if toolchains for all target architectures are installed and returns a -/// list containing the names of all missing toolchains -fn check_installed_toolchains(targets: &[Target]) -> Vec<&'static str> { - let mut rustup = command!("rustup target list"); - rustup.stdout(Stdio::piped()); - let output = rustup - .execute_output() - .expect("Failed to check installed toolchains. Is rustup installed on your system?"); - let output = String::from_utf8_lossy(&output.stdout); - - let installed: Vec<_> = output - .split('\n') - .filter(|s| s.contains("installed")) - .map(|s| s.replace("(installed)", "").trim().to_owned()) - .collect(); +/// Checks if toolchains for all tier 1/2 target architectures are installed on the +/// default (stable) toolchain and returns a list of missing ones. +fn check_stable_missing_targets( + targets: &[Target], + toolchain_targets: &ToolchainTargets, +) -> Vec<&'static str> { + targets + .iter() + .flat_map(|t| t.architectures()) + .filter(|arch| toolchain_targets.is_stable_missing(arch)) + .collect() +} +/// Checks if targets that are only available on nightly (tier 2 on nightly, tier 3 on stable) +/// are installed on the nightly toolchain. +fn check_nightly_missing_targets( + targets: &[Target], + toolchain_targets: &ToolchainTargets, +) -> Vec<&'static str> { targets .iter() - .filter(|t| !t.platform().is_tier_3()) .flat_map(|t| t.architectures()) - .filter(|arch| { - !installed - .iter() - .any(|toolchain| toolchain.eq_ignore_ascii_case(arch)) - }) + .filter(|arch| toolchain_targets.is_nightly_missing(arch)) .collect() } -/// Checks if rust-src component for tier 3 targets are installed -fn check_nightly_installed(targets: &[Target]) -> Vec<&'static str> { - if !targets.iter().any(|t| t.platform().is_tier_3()) { +/// Checks if rust-src component for tier 3 targets (needing -Z build-std) is installed +fn check_nightly_src_installed( + targets: &[Target], + toolchain_targets: &ToolchainTargets, +) -> Vec<&'static str> { + let has_build_std = targets + .iter() + .flat_map(|t| t.architectures()) + .any(|arch| toolchain_targets.needs_build_std(arch)); + + if !has_build_std { return vec![]; } - // TODO: Check if the correct nightly toolchain itself is installed let mut rustup = command("rustup component list --toolchain nightly"); rustup.stdout(Stdio::piped()); // HACK: Silence error that toolchain is not installed @@ -529,6 +550,38 @@ fn install_toolchains(toolchains: &[&str], silent: bool) -> Result<()> { Ok(()) } +/// Attempts to install the given targets on the nightly toolchain +fn install_nightly_targets(toolchains: &[&str], silent: bool) -> Result<()> { + if toolchains.is_empty() { + return Ok(()); + }; + + let multi = silent.not().then(MultiProgress::new); + let spinner = silent + .not() + .then(|| MainSpinner::with_message("Installing Nightly Targets...".to_owned())); + multi.add(&spinner); + spinner.start(); + for toolchain in toolchains { + let mut install = Command::new("rustup"); + install.args(["target", "install", toolchain, "--toolchain", "nightly"]); + install.stdin(Stdio::null()); + + let step = silent.not().then(|| CommandSpinner::with_command(&install)); + multi.add(&step); + step.start(); + + install + .execute() + .map_err(|e| format!("Error while installing nightly target {toolchain}: \n\t{e}"))?; + + step.finish(); + } + spinner.finish(); + + Ok(()) +} + /// Attempts to install the "rust-src" component on nightly fn install_nightly_src(silent: bool) -> Result<()> { let multi = silent.not().then(MultiProgress::new); @@ -638,8 +691,9 @@ fn build_with_output( lib_type: LibType, config: &Config, features: &FeatureOptions, + toolchain_targets: &ToolchainTargets, ) -> Result<()> { - let mut commands = target.commands(lib_name, mode, lib_type, features); + let mut commands = target.commands(lib_name, mode, lib_type, features, toolchain_targets); for command in &mut commands { command.env("CARGO_TERM_COLOR", "always"); } diff --git a/src/targets.rs b/src/targets.rs index ea12ea7..9168c23 100644 --- a/src/targets.rs +++ b/src/targets.rs @@ -1,18 +1,105 @@ +use std::collections::HashSet; +use std::process::Stdio; use std::{fmt::Display, process::Command}; use execute::command; +use execute::Execute; use nonempty::{nonempty, NonEmpty}; use crate::lib_type::LibType; use crate::metadata::{metadata, MetadataExt}; use crate::package::FeatureOptions; -pub trait TargetInfo { - fn target(&self) -> Target; - /// Marks whether a pre-built std-lib is provided for this target (Tier 1 and Tier 2) via rustup or target needs to - /// be build (Tier 3) - /// See: https://doc.rust-lang.org/nightly/rustc/platform-support.html - fn is_tier_3(&self) -> bool; +/// Queries `rustup target list` for both the default and nightly toolchains, +/// caching which targets are available and installed on each. When any +/// architecture requires nightly, all builds use nightly for consistency +/// (same Rust compiler version across the entire build). +pub struct ToolchainTargets { + stable_available: HashSet, + stable_installed: HashSet, + nightly_available: HashSet, + nightly_installed: HashSet, + /// When true, all targets use `cargo +nightly` for compiler version consistency. + use_nightly: bool, +} + +impl ToolchainTargets { + /// Run `rustup target list` for both default and nightly toolchains, then + /// determine whether nightly is needed for any of the given target architectures. + /// If nightly is not installed, its sets will be empty (conservative fallback). + pub fn query(targets: &[Target]) -> Self { + let (stable_available, stable_installed) = Self::parse_target_list(&["target", "list"]); + let (nightly_available, nightly_installed) = + Self::parse_target_list(&["target", "list", "--toolchain", "nightly"]); + + let use_nightly = targets + .iter() + .flat_map(|t| t.architectures()) + .any(|arch| !stable_available.contains(arch)); + + Self { + stable_available, + stable_installed, + nightly_available, + nightly_installed, + use_nightly, + } + } + + fn parse_target_list(args: &[&str]) -> (HashSet, HashSet) { + let mut rustup = Command::new("rustup"); + rustup.args(args); + rustup.stdout(Stdio::piped()); + rustup.stderr(Stdio::null()); + + let output = match rustup.execute_output() { + Ok(output) if output.status.success() => output, + _ => return (HashSet::new(), HashSet::new()), + }; + let output = String::from_utf8_lossy(&output.stdout); + + let mut available = HashSet::new(); + let mut installed = HashSet::new(); + + for line in output.lines() { + let trimmed = line.trim(); + if trimmed.is_empty() { + continue; + } + if let Some(name) = trimmed.strip_suffix("(installed)") { + let name = name.trim().to_owned(); + installed.insert(name.clone()); + available.insert(name); + } else { + available.insert(trimmed.to_owned()); + } + } + + (available, installed) + } + + /// Returns true if the arch requires `-Z build-std` + /// (not available on either stable or nightly). + pub fn needs_build_std(&self, arch: &str) -> bool { + !self.stable_available.contains(arch) && !self.nightly_available.contains(arch) + } + + /// Returns true if any architecture in the build requires nightly. + pub fn use_nightly(&self) -> bool { + self.use_nightly + } + + /// Returns true if the target is available on stable but not yet installed. + pub fn is_stable_missing(&self, arch: &str) -> bool { + self.stable_available.contains(arch) && !self.stable_installed.contains(arch) + } + + /// Returns true if the target is only available on nightly and not yet installed there. + pub fn is_nightly_missing(&self, arch: &str) -> bool { + !self.stable_available.contains(arch) + && self.nightly_available.contains(arch) + && !self.nightly_installed.contains(arch) + } } #[derive(Debug, Clone)] @@ -46,13 +133,19 @@ impl Display for Mode { } impl Target { - fn cargo_build_commands(&self, mode: Mode, features: &FeatureOptions) -> Vec { + fn cargo_build_commands( + &self, + mode: Mode, + features: &FeatureOptions, + toolchain_targets: &ToolchainTargets, + ) -> Vec { self.architectures() .into_iter() .map(|arch| { - // FIXME: Remove nightly for Tier 3 targets here once build-std is stabilized - let mut cmd = if self.platform().is_tier_3() { + let mut cmd = if toolchain_targets.needs_build_std(arch) { command("cargo +nightly build -Z build-std") + } else if toolchain_targets.use_nightly() { + command("cargo +nightly build") } else { command("cargo build") }; @@ -129,8 +222,9 @@ impl Target { mode: Mode, lib_type: LibType, features: &FeatureOptions, + toolchain_targets: &ToolchainTargets, ) -> Vec { - self.cargo_build_commands(mode, features) + self.cargo_build_commands(mode, features, toolchain_targets) .into_iter() .chain(self.lipo_commands(lib_name, mode, lib_type)) .chain(self.rpath_install_id_commands(lib_name, mode, lib_type)) @@ -203,8 +297,8 @@ pub enum ApplePlatform { VisionOSSimulator, } -impl TargetInfo for ApplePlatform { - fn target(&self) -> Target { +impl ApplePlatform { + pub fn target(&self) -> Target { use ApplePlatform::*; match self { IOS => Target::Single { @@ -269,15 +363,4 @@ impl TargetInfo for ApplePlatform { }, } } - - fn is_tier_3(&self) -> bool { - match self { - ApplePlatform::IOS | ApplePlatform::IOSSimulator => false, - ApplePlatform::MacOS => false, - ApplePlatform::MacCatalyst => false, - ApplePlatform::TvOS | ApplePlatform::TvOSSimulator => true, - ApplePlatform::WatchOS | ApplePlatform::WatchOSSimulator => true, - ApplePlatform::VisionOS | ApplePlatform::VisionOSSimulator => true, - } - } }