diff --git a/src/cli/common.rs b/src/cli/common.rs index a6b525a0aa..77423eeae4 100644 --- a/src/cli/common.rs +++ b/src/cli/common.rs @@ -16,17 +16,15 @@ use tracing::{debug, error, info, trace, warn}; use super::self_update; use crate::cli::download_tracker::DownloadTracker; use crate::currentprocess::{terminalsource, Process}; -use crate::dist::{manifest::ComponentStatus, TargetTriple, ToolchainDesc}; +use crate::dist::{ + manifest::ComponentStatus, notifications as dist_notifications, TargetTriple, ToolchainDesc, +}; use crate::install::UpdateStatus; -use crate::toolchain::names::{LocalToolchainName, ToolchainName}; -use crate::toolchain::toolchain::Toolchain; +use crate::toolchain::{DistributableToolchain, LocalToolchainName, Toolchain, ToolchainName}; use crate::utils::notifications as util_notifications; use crate::utils::notify::NotificationLevel; use crate::utils::utils; use crate::{config::Cfg, notifications::Notification}; -use crate::{ - dist::notifications as dist_notifications, toolchain::distributable::DistributableToolchain, -}; pub(crate) const WARN_COMPLETE_PROFILE: &str = "downloading with complete profile isn't recommended unless you are a developer of the rust language"; diff --git a/src/cli/proxy_mode.rs b/src/cli/proxy_mode.rs index 8e12e906f1..17a6b38bdd 100644 --- a/src/cli/proxy_mode.rs +++ b/src/cli/proxy_mode.rs @@ -2,12 +2,11 @@ use std::{path::PathBuf, process::ExitStatus}; use anyhow::Result; -use crate::toolchain::toolchain::Toolchain; use crate::{ cli::{common::set_globals, job, self_update}, command::run_command_for_dir, currentprocess::Process, - toolchain::names::ResolvableLocalToolchainName, + toolchain::ResolvableLocalToolchainName, }; #[cfg_attr(feature = "otel", tracing::instrument)] @@ -33,16 +32,6 @@ pub async fn main(arg0: &str, current_dir: PathBuf, process: &Process) -> Result .collect(); let cfg = set_globals(current_dir, false, true, process)?; - cfg.check_metadata_version()?; - let toolchain = toolchain - .map(|t| t.resolve(&cfg.get_default_host_triple()?)) - .transpose()?; - - let toolchain = match toolchain { - None => cfg.find_or_install_active_toolchain().await?.0, - Some(tc) => Toolchain::from_local(&tc, false, &cfg).await?, - }; - - let cmd = toolchain.command(arg0)?; + let cmd = cfg.local_toolchain(toolchain).await?.command(arg0)?; run_command_for_dir(cmd, arg0, &cmd_args) } diff --git a/src/cli/rustup_mode.rs b/src/cli/rustup_mode.rs index 66c145527b..5de45f0760 100644 --- a/src/cli/rustup_mode.rs +++ b/src/cli/rustup_mode.rs @@ -31,12 +31,9 @@ use crate::{ errors::RustupError, install::{InstallMethod, UpdateStatus}, toolchain::{ - distributable::DistributableToolchain, - names::{ - CustomToolchainName, LocalToolchainName, MaybeResolvableToolchainName, - ResolvableLocalToolchainName, ResolvableToolchainName, ToolchainName, - }, - toolchain::Toolchain, + CustomToolchainName, DistributableToolchain, LocalToolchainName, + MaybeResolvableToolchainName, ResolvableLocalToolchainName, ResolvableToolchainName, + Toolchain, ToolchainName, }, utils::utils::{self, ExitCode}, }; @@ -540,25 +537,8 @@ pub async fn main(current_dir: PathBuf, process: &Process) -> Result { write!(process.stdout().lock(), "{err}")?; info!("This is the version for the rustup toolchain manager, not the rustc compiler."); - - #[cfg_attr(feature = "otel", tracing::instrument)] - async fn rustc_version( - current_dir: PathBuf, - process: &Process, - ) -> std::result::Result> { - let cfg = &mut common::set_globals(current_dir, false, true, process)?; - - if let Some(t) = process.args().find(|x| x.starts_with('+')) { - trace!("Fetching rustc version from toolchain `{}`", t); - cfg.set_toolchain_override(&ResolvableToolchainName::try_from(&t[1..])?); - } - - let toolchain = cfg.find_or_install_active_toolchain().await?.0; - - Ok(toolchain.rustc_version()) - } - - match rustc_version(current_dir, process).await { + let mut cfg = common::set_globals(current_dir, false, true, process)?; + match cfg.active_rustc_version().await { Ok(version) => info!("The currently active `rustc` version is `{}`", version), Err(err) => trace!("Wanted to tell you the current rustc version, too, but ran into this error: {}", err), } @@ -583,14 +563,13 @@ pub async fn main(current_dir: PathBuf, process: &Process) -> Result, opts: UpdateOpts) -> Result let force = opts.force; let allow_downgrade = opts.allow_downgrade; let profile = cfg.get_profile()?; - let status = match crate::toolchain::distributable::DistributableToolchain::new( - cfg, - desc.clone(), - ) { + let status = match DistributableToolchain::new(cfg, desc.clone()) { Ok(mut d) => { d.update_extra(&components, &targets, profile, force, allow_downgrade) .await? } Err(RustupError::ToolchainNotInstalled(_)) => { - crate::toolchain::distributable::DistributableToolchain::install( + DistributableToolchain::install( cfg, &desc, &components, @@ -907,7 +883,7 @@ async fn run( install: bool, ) -> Result { let toolchain = toolchain.resolve(&cfg.get_default_host_triple()?)?; - let toolchain = Toolchain::from_local(&toolchain, install, cfg).await?; + let toolchain = Toolchain::from_local(toolchain, install, cfg).await?; let cmd = toolchain.command(&command[0])?; command::run_command_for_dir(cmd, &command[0], &command[1..]) } @@ -917,13 +893,7 @@ async fn which( binary: &str, toolchain: Option, ) -> Result { - let binary_path = if let Some(toolchain) = toolchain { - let desc = toolchain.resolve(&cfg.get_default_host_triple()?)?; - Toolchain::new(cfg, desc.into())?.binary_file(binary) - } else { - let (toolchain, _) = cfg.find_or_install_active_toolchain().await?; - toolchain.binary_file(binary) - }; + let binary_path = cfg.resolve_toolchain(toolchain).await?.binary_file(binary); utils::assert_is_file(&binary_path)?; @@ -1446,7 +1416,7 @@ async fn doc( mut topic: Option<&str>, doc_page: &DocPage, ) -> Result { - let toolchain = Toolchain::from_partial(toolchain, cfg).await?; + let toolchain = cfg.toolchain_from_partial(toolchain).await?; if let Ok(distributable) = DistributableToolchain::try_from(&toolchain) { if let [_] = distributable @@ -1507,10 +1477,8 @@ async fn man( command: &str, toolchain: Option, ) -> Result { - let toolchain = Toolchain::from_partial(toolchain, cfg).await?; - let mut path = toolchain.path().to_path_buf(); - path.push("share"); - path.push("man"); + let toolchain = cfg.toolchain_from_partial(toolchain).await?; + let path = toolchain.man_path(); utils::assert_is_directory(&path)?; let mut manpaths = std::ffi::OsString::from(path); diff --git a/src/cli/self_update.rs b/src/cli/self_update.rs index 6c31fb5487..a488fda963 100644 --- a/src/cli/self_update.rs +++ b/src/cli/self_update.rs @@ -75,9 +75,8 @@ use crate::{ errors::RustupError, install::UpdateStatus, toolchain::{ - distributable::DistributableToolchain, - names::{MaybeOfficialToolchainName, ResolvableToolchainName, ToolchainName}, - toolchain::Toolchain, + DistributableToolchain, MaybeOfficialToolchainName, ResolvableToolchainName, Toolchain, + ToolchainName, }, utils::{utils, Notification}, DUP_TOOLS, TOOLS, diff --git a/src/cli/setup_mode.rs b/src/cli/setup_mode.rs index c0234052c4..a381b829aa 100644 --- a/src/cli/setup_mode.rs +++ b/src/cli/setup_mode.rs @@ -11,7 +11,7 @@ use crate::{ }, currentprocess::Process, dist::Profile, - toolchain::names::MaybeOfficialToolchainName, + toolchain::MaybeOfficialToolchainName, utils::utils, }; diff --git a/src/config.rs b/src/config.rs index 59716f9da1..96408fd297 100644 --- a/src/config.rs +++ b/src/config.rs @@ -14,19 +14,15 @@ use tracing::trace; use crate::{ cli::self_update::SelfUpdateMode, currentprocess::Process, - dist::{self, download::DownloadCfg, temp, Profile, ToolchainDesc}, + dist::{self, download::DownloadCfg, temp, PartialToolchainDesc, Profile, ToolchainDesc}, errors::RustupError, fallback_settings::FallbackSettings, install::UpdateStatus, notifications::*, settings::{MetadataVersion, Settings, SettingsFile}, toolchain::{ - distributable::DistributableToolchain, - names::{ - CustomToolchainName, LocalToolchainName, PathBasedToolchainName, - ResolvableLocalToolchainName, ResolvableToolchainName, ToolchainName, - }, - toolchain::Toolchain, + CustomToolchainName, DistributableToolchain, LocalToolchainName, PathBasedToolchainName, + ResolvableLocalToolchainName, ResolvableToolchainName, Toolchain, ToolchainName, }, utils::utils, }; @@ -259,6 +255,16 @@ impl<'a> Cfg<'a> { utils::ensure_dir_exists("home", &rustup_dir, notify_handler.as_ref())?; let settings_file = SettingsFile::new(rustup_dir.join("settings.toml")); + settings_file.with(|s| { + (notify_handler)(Notification::ReadMetadataVersion(s.version)); + if s.version == MetadataVersion::default() { + Ok(()) + } else { + Err(anyhow!( + "rustup's metadata is out of date. run `rustup self upgrade-data`" + )) + } + })?; // Centralised file for multi-user systems to provide admin/distributor set initial values. let fallback_settings = if cfg!(not(windows)) { @@ -427,6 +433,20 @@ impl<'a> Cfg<'a> { Ok(()) } + pub(crate) fn installed_paths<'b>( + &self, + desc: &ToolchainDesc, + path: &'b Path, + ) -> anyhow::Result>> { + Ok(vec![ + InstalledPath::File { + name: "update hash", + path: self.get_hash_file(desc, false)?, + }, + InstalledPath::Dir { path }, + ]) + } + pub(crate) fn get_hash_file( &self, toolchain: &ToolchainDesc, @@ -490,11 +510,27 @@ impl<'a> Cfg<'a> { .transpose()?) } + pub(crate) async fn toolchain_from_partial( + &self, + toolchain: Option, + ) -> anyhow::Result> { + match toolchain { + Some(toolchain) => { + let desc = toolchain.resolve(&self.get_default_host_triple()?)?; + Ok(Toolchain::new( + self, + LocalToolchainName::Named(ToolchainName::Official(desc)), + )?) + } + None => Ok(self.find_or_install_active_toolchain().await?.0), + } + } + pub(crate) fn find_active_toolchain( &self, ) -> Result> { Ok( - if let Some((override_config, reason)) = self.find_override_config(&self.current_dir)? { + if let Some((override_config, reason)) = self.find_override_config()? { Some((override_config.into_local_toolchain_name(), reason)) } else { self.get_default()? @@ -503,7 +539,7 @@ impl<'a> Cfg<'a> { ) } - fn find_override_config(&self, path: &Path) -> Result> { + fn find_override_config(&self) -> Result> { let override_config: Option<(OverrideCfg, ActiveReason)> = // First check +toolchain override from the command line if let Some(ref name) = self.toolchain_override { @@ -522,7 +558,7 @@ impl<'a> Cfg<'a> { // directory in the override database, or a `rust-toolchain{.toml}` file, // in that order. else if let Some((override_cfg, active_reason)) = self.settings_file.with(|s| { - self.find_override_from_dir_walk(path, s) + self.find_override_from_dir_walk(&self.current_dir, s) })? { Some((override_cfg, active_reason)) } @@ -672,28 +708,58 @@ impl<'a> Cfg<'a> { } } - pub(crate) async fn find_or_install_active_toolchain( - &'a self, - ) -> Result<(Toolchain<'a>, ActiveReason)> { - self.maybe_find_or_install_active_toolchain(&self.current_dir) + #[cfg_attr(feature = "otel", tracing::instrument)] + pub(crate) async fn active_rustc_version(&mut self) -> Result { + if let Some(t) = self.process.args().find(|x| x.starts_with('+')) { + trace!("Fetching rustc version from toolchain `{}`", t); + self.set_toolchain_override(&ResolvableToolchainName::try_from(&t[1..])?); + } + + Ok(self + .find_or_install_active_toolchain() .await? - .ok_or_else(|| no_toolchain_error(self.process)) + .0 + .rustc_version()) + } + + pub(crate) async fn resolve_toolchain( + &self, + name: Option, + ) -> Result> { + Ok(match name { + Some(name) => { + let desc = name.resolve(&self.get_default_host_triple()?)?; + Toolchain::new(self, desc.into())? + } + None => self.find_or_install_active_toolchain().await?.0, + }) + } + + pub(crate) async fn local_toolchain( + &self, + name: Option, + ) -> Result> { + let local = name + .map(|name| name.resolve(&self.get_default_host_triple()?)) + .transpose()?; + + Ok(match local { + Some(tc) => Toolchain::from_local(tc, false, self).await?, + None => self.find_or_install_active_toolchain().await?.0, + }) } #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] - pub(crate) async fn maybe_find_or_install_active_toolchain( - &'a self, - path: &Path, - ) -> Result, ActiveReason)>> { - match self.find_override_config(path)? { + async fn find_or_install_active_toolchain(&'a self) -> Result<(Toolchain<'a>, ActiveReason)> { + match self.find_override_config()? { Some((override_config, reason)) => match override_config { OverrideCfg::PathBased(path_based_name) => { let toolchain = Toolchain::with_reason(self, path_based_name.into(), &reason)?; - Ok(Some((toolchain, reason))) + Ok((toolchain, reason)) } OverrideCfg::Custom(custom_name) => { let toolchain = Toolchain::with_reason(self, custom_name.into(), &reason)?; - Ok(Some((toolchain, reason))) + Ok((toolchain, reason)) } OverrideCfg::Official { toolchain, @@ -704,22 +770,22 @@ impl<'a> Cfg<'a> { let toolchain = self .ensure_installed(toolchain, components, targets, profile) .await?; - Ok(Some((toolchain, reason))) + Ok((toolchain, reason)) } }, None => match self.get_default()? { - None => Ok(None), + None => Err(no_toolchain_error(self.process)), Some(ToolchainName::Custom(custom_name)) => { let reason = ActiveReason::Default; let toolchain = Toolchain::with_reason(self, custom_name.into(), &reason)?; - Ok(Some((toolchain, reason))) + Ok((toolchain, reason)) } Some(ToolchainName::Official(toolchain_desc)) => { let reason = ActiveReason::Default; let toolchain = self .ensure_installed(toolchain_desc, vec![], vec![], None) .await?; - Ok(Some((toolchain, reason))) + Ok((toolchain, reason)) } }, } @@ -801,7 +867,7 @@ impl<'a> Cfg<'a> { .filter_map(|n| ToolchainName::try_from(&n).ok()) .collect(); - crate::toolchain::names::toolchain_sort(&mut toolchains); + crate::toolchain::toolchain_sort(&mut toolchains); Ok(toolchains) } else { @@ -858,22 +924,6 @@ impl<'a> Cfg<'a> { Ok(channels.collect().await) } - #[cfg_attr(feature = "otel", tracing::instrument(skip_all))] - pub(crate) fn check_metadata_version(&self) -> Result<()> { - utils::assert_is_directory(&self.rustup_dir)?; - - self.settings_file.with(|s| { - (self.notify_handler)(Notification::ReadMetadataVersion(s.version)); - if s.version == MetadataVersion::default() { - Ok(()) - } else { - Err(anyhow!( - "rustup's metadata is out of date. run `rustup self upgrade-data`" - )) - } - }) - } - pub(crate) fn set_default_host_triple(&self, host_triple: String) -> Result<()> { // Ensure that the provided host_triple is capable of resolving // against the 'stable' toolchain. This provides early errors @@ -962,6 +1012,12 @@ enum ParseMode { Both, } +/// Installed paths +pub(crate) enum InstalledPath<'a> { + File { name: &'static str, path: PathBuf }, + Dir { path: &'a Path }, +} + #[cfg(test)] mod tests { use super::*; diff --git a/src/dist/manifest.rs b/src/dist/manifest.rs index 58a8753782..e3146fe09d 100644 --- a/src/dist/manifest.rs +++ b/src/dist/manifest.rs @@ -24,7 +24,7 @@ use serde::{Deserialize, Serialize}; use crate::{ dist::{config::Config, Profile, TargetTriple, ToolchainDesc}, errors::*, - toolchain::distributable::DistributableToolchain, + toolchain::DistributableToolchain, }; /// Used by the `installed_components` function diff --git a/src/dist/mod.rs b/src/dist/mod.rs index 19399eb248..1e12ffa020 100644 --- a/src/dist/mod.rs +++ b/src/dist/mod.rs @@ -11,9 +11,7 @@ use regex::Regex; use serde::{Deserialize, Serialize}; use thiserror::Error as ThisError; -use crate::{ - currentprocess::Process, errors::RustupError, toolchain::names::ToolchainName, utils::utils, -}; +use crate::{currentprocess::Process, errors::RustupError, toolchain::ToolchainName, utils::utils}; pub mod component; pub(crate) mod config; diff --git a/src/errors.rs b/src/errors.rs index 1fba62e308..b4fe9844e7 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -11,9 +11,11 @@ use thiserror::Error as ThisError; use url::Url; use crate::{ - dist::manifest::{Component, Manifest}, - dist::{TargetTriple, ToolchainDesc}, - toolchain::names::{PathBasedToolchainName, ToolchainName}, + dist::{ + manifest::{Component, Manifest}, + {TargetTriple, ToolchainDesc}, + }, + toolchain::{PathBasedToolchainName, ToolchainName}, }; /// A type erasing thunk for the retry crate to permit use with anyhow. See diff --git a/src/install.rs b/src/install.rs index ba3231b6af..7c400f1903 100644 --- a/src/install.rs +++ b/src/install.rs @@ -9,10 +9,7 @@ use crate::{ dist::{self, download::DownloadCfg, prefix::InstallPrefix, Notification}, errors::RustupError, notifications::Notification as RootNotification, - toolchain::{ - names::{CustomToolchainName, LocalToolchainName}, - toolchain::Toolchain, - }, + toolchain::{CustomToolchainName, LocalToolchainName, Toolchain}, utils::utils, }; diff --git a/src/notifications.rs b/src/notifications.rs index 15671aac13..206e1d2f66 100644 --- a/src/notifications.rs +++ b/src/notifications.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use crate::settings::MetadataVersion; use crate::{ dist::{temp, ToolchainDesc}, - toolchain::names::ToolchainName, + toolchain::ToolchainName, utils::notify::NotificationLevel, }; diff --git a/src/toolchain.rs b/src/toolchain.rs index 6bcf91a934..a44c82f561 100644 --- a/src/toolchain.rs +++ b/src/toolchain.rs @@ -1,4 +1,471 @@ -pub(crate) mod distributable; -pub(crate) mod names; -#[allow(clippy::module_inception)] -pub(crate) mod toolchain; +use std::{ + env::{self, consts::EXE_SUFFIX}, + ffi::{OsStr, OsString}, + fmt::Debug, + fs, + io::{self, BufRead, BufReader}, + path::{Path, PathBuf}, + process::{Command, Stdio}, + str::FromStr, + time::Duration, +}; + +use anyhow::{anyhow, bail}; +use fs_at::OpenOptions; +use tracing::info; +use wait_timeout::ChildExt; + +use crate::{ + config::{ActiveReason, Cfg, InstalledPath}, + dist::PartialToolchainDesc, + env_var, install, + notifications::Notification, + utils::{raw::open_dir_following_links, utils}, + RustupError, +}; + +mod distributable; +pub(crate) use distributable::DistributableToolchain; + +mod names; +pub(crate) use names::{ + toolchain_sort, CustomToolchainName, LocalToolchainName, MaybeOfficialToolchainName, + MaybeResolvableToolchainName, PathBasedToolchainName, ResolvableLocalToolchainName, + ResolvableToolchainName, ToolchainName, +}; + +/// A toolchain installed on the local disk +#[derive(Clone, Debug)] +pub(crate) struct Toolchain<'a> { + pub(super) cfg: &'a Cfg<'a>, + name: LocalToolchainName, + path: PathBuf, +} + +impl<'a> Toolchain<'a> { + pub(crate) async fn from_local( + name: LocalToolchainName, + install_if_missing: bool, + cfg: &'a Cfg<'a>, + ) -> anyhow::Result> { + match Self::new(cfg, name) { + Ok(tc) => Ok(tc), + Err(RustupError::ToolchainNotInstalled(ToolchainName::Official(desc))) + if install_if_missing => + { + Ok( + DistributableToolchain::install(cfg, &desc, &[], &[], cfg.get_profile()?, true) + .await? + .1 + .toolchain, + ) + } + Err(e) => Err(e.into()), + } + } + + /// Calls Toolchain::new(), but augments the error message with more context + /// from the ActiveReason if the toolchain isn't installed. + pub(crate) fn with_reason( + cfg: &'a Cfg<'a>, + name: LocalToolchainName, + reason: &ActiveReason, + ) -> anyhow::Result { + match Self::new(cfg, name.clone()) { + Err(RustupError::ToolchainNotInstalled(_)) => (), + result => { + return Ok(result?); + } + } + + let reason_err = match reason { + ActiveReason::Environment => { + "the RUSTUP_TOOLCHAIN environment variable specifies an uninstalled toolchain" + .to_string() + } + ActiveReason::CommandLine => { + "the +toolchain on the command line specifies an uninstalled toolchain".to_string() + } + ActiveReason::OverrideDB(ref path) => format!( + "the directory override for '{}' specifies an uninstalled toolchain", + utils::canonicalize_path(path, cfg.notify_handler.as_ref()).display(), + ), + ActiveReason::ToolchainFile(ref path) => format!( + "the toolchain file at '{}' specifies an uninstalled toolchain", + utils::canonicalize_path(path, cfg.notify_handler.as_ref()).display(), + ), + ActiveReason::Default => { + "the default toolchain does not describe an installed toolchain".to_string() + } + }; + + Err(anyhow!(reason_err).context(format!("override toolchain '{name}' is not installed"))) + } + + pub(crate) fn new(cfg: &'a Cfg<'a>, name: LocalToolchainName) -> Result { + let path = cfg.toolchain_path(&name); + if !Toolchain::exists(cfg, &name)? { + return Err(match name { + LocalToolchainName::Named(name) => RustupError::ToolchainNotInstalled(name), + LocalToolchainName::Path(name) => RustupError::PathToolchainNotInstalled(name), + }); + } + Ok(Self { cfg, name, path }) + } + + /// Ok(True) if the toolchain exists. Ok(False) if the toolchain or its + /// containing directory don't exist. Err otherwise. + pub(crate) fn exists(cfg: &Cfg<'_>, name: &LocalToolchainName) -> Result { + let path = cfg.toolchain_path(name); + // toolchain validation should have prevented a situation where there is + // no base dir, but defensive programming is defensive. + let parent = path + .parent() + .ok_or_else(|| RustupError::InvalidToolchainName(name.to_string()))?; + let base_name = path + .file_name() + .ok_or_else(|| RustupError::InvalidToolchainName(name.to_string()))?; + let parent_dir = match open_dir_following_links(parent) { + Ok(d) => d, + Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false), + e => e?, + }; + let opened = OpenOptions::default() + .read(true) + .follow(true) + .open_dir_at(&parent_dir, base_name); + Ok(opened.is_ok()) + } + + pub(crate) fn name(&self) -> &LocalToolchainName { + &self.name + } + + pub(super) fn path(&self) -> &Path { + &self.path + } + + /// The path to a binary within the toolchain, without regard for cargo-fallback logic + pub fn binary_file(&self, name: &str) -> PathBuf { + let mut path = self.path.clone(); + path.push("bin"); + path.push(name.to_owned() + env::consts::EXE_SUFFIX); + path + } + + /// Not intended to be public, but more code golf required to get it hidden. + /// pub because of create_fallback_command + pub fn set_env(&self, cmd: &mut Command) { + self.set_ldpath(cmd); + + // Older versions of Cargo used a slightly different definition of + // cargo home. Rustup does not read HOME on Windows whereas the older + // versions of Cargo did. Rustup and Cargo should be in sync now (both + // using the same `home` crate), but this is retained to ensure cargo + // and rustup agree in older versions. + if let Ok(cargo_home) = self.cfg.process.cargo_home() { + cmd.env("CARGO_HOME", &cargo_home); + } + + env_var::inc("RUST_RECURSION_COUNT", cmd, self.cfg.process); + + cmd.env("RUSTUP_TOOLCHAIN", format!("{}", self.name)); + cmd.env("RUSTUP_HOME", &self.cfg.rustup_dir); + } + + /// Apply the appropriate LD path for a command being run from a toolchain. + fn set_ldpath(&self, cmd: &mut Command) { + let mut new_path = vec![self.path.join("lib")]; + + #[cfg(not(target_os = "macos"))] + mod sysenv { + pub const LOADER_PATH: &str = "LD_LIBRARY_PATH"; + } + #[cfg(target_os = "macos")] + mod sysenv { + // When loading and linking a dynamic library or bundle, dlopen + // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and + // DYLD_FALLBACK_LIBRARY_PATH. + // In the Mach-O format, a dynamic library has an "install path." + // Clients linking against the library record this path, and the + // dynamic linker, dyld, uses it to locate the library. + // dyld searches DYLD_LIBRARY_PATH *before* the install path. + // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot + // find the library in the install path. + // Setting DYLD_LIBRARY_PATH can easily have unintended + // consequences. + pub const LOADER_PATH: &str = "DYLD_FALLBACK_LIBRARY_PATH"; + } + if cfg!(target_os = "macos") + && self + .cfg + .process + .var_os(sysenv::LOADER_PATH) + .filter(|x| x.len() > 0) + .is_none() + { + // These are the defaults when DYLD_FALLBACK_LIBRARY_PATH isn't + // set or set to an empty string. Since we are explicitly setting + // the value, make sure the defaults still work. + if let Some(home) = self.cfg.process.var_os("HOME") { + new_path.push(PathBuf::from(home).join("lib")); + } + new_path.push(PathBuf::from("/usr/local/lib")); + new_path.push(PathBuf::from("/usr/lib")); + } + + env_var::prepend_path(sysenv::LOADER_PATH, new_path, cmd, self.cfg.process); + + // Prepend CARGO_HOME/bin to the PATH variable so that we're sure to run + // cargo/rustc via the proxy bins. There is no fallback case for if the + // proxy bins don't exist. We'll just be running whatever happens to + // be on the PATH. + let mut path_entries = vec![]; + if let Ok(cargo_home) = self.cfg.process.cargo_home() { + path_entries.push(cargo_home.join("bin")); + } + + if cfg!(target_os = "windows") { + // Historically rustup included the bin directory in PATH to + // work around some bugs (see + // https://github.com/rust-lang/rustup/pull/3178 for more + // information). This shouldn't be needed anymore, and it causes + // problems because calling tools recursively (like `cargo + // +nightly metadata` from within a cargo subcommand). The + // recursive call won't work because it is not executing the + // proxy, so the `+` toolchain override doesn't work. + // + // The RUSTUP_WINDOWS_PATH_ADD_BIN env var was added to opt-in to + // testing the fix. The default is now off, but this is left here + // just in case there are problems. Consider removing in the + // future if it doesn't seem necessary. + if self + .cfg + .process + .var_os("RUSTUP_WINDOWS_PATH_ADD_BIN") + .map_or(false, |s| s == "1") + { + path_entries.push(self.path.join("bin")); + } + } + + env_var::prepend_path("PATH", path_entries, cmd, self.cfg.process); + } + + /// Infallible function that describes the version of rustc in an installed distribution + #[cfg_attr(feature = "otel", tracing::instrument)] + pub fn rustc_version(&self) -> String { + match self.create_command("rustc") { + Ok(mut cmd) => { + cmd.arg("--version"); + cmd.stdin(Stdio::null()); + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + self.set_ldpath(&mut cmd); + + // some toolchains are faulty with some combinations of platforms and + // may fail to launch but also to timely terminate. + // (known cases include Rust 1.3.0 through 1.10.0 in recent macOS Sierra.) + // we guard against such cases by enforcing a reasonable timeout to read. + let mut line1 = None; + if let Ok(mut child) = cmd.spawn() { + let timeout = Duration::new(10, 0); + match child.wait_timeout(timeout) { + Ok(Some(status)) if status.success() => { + let out = child + .stdout + .expect("Child::stdout requested but not present"); + let mut line = String::new(); + if BufReader::new(out).read_line(&mut line).is_ok() { + let lineend = line.trim_end_matches(&['\r', '\n'][..]).len(); + line.truncate(lineend); + line1 = Some(line); + } + } + Ok(None) => { + let _ = child.kill(); + return String::from("(timeout reading rustc version)"); + } + Ok(Some(_)) | Err(_) => {} + } + } + + if let Some(line1) = line1 { + line1 + } else { + String::from("(error reading rustc version)") + } + } + Err(_) => String::from("(rustc does not exist)"), + } + } + + pub(crate) fn command(&self, binary: &str) -> anyhow::Result { + // Should push the cargo fallback into a custom toolchain type? And then + // perhaps a trait that create command layers on? + if !matches!( + self.name(), + LocalToolchainName::Named(ToolchainName::Official(_)) + ) { + if let Some(cmd) = self.maybe_do_cargo_fallback(binary)? { + return Ok(cmd); + } + } + + self.create_command(binary) + } + + // Custom toolchains don't have cargo, so here we detect that situation and + // try to find a different cargo. + pub(crate) fn maybe_do_cargo_fallback(&self, binary: &str) -> anyhow::Result> { + if binary != "cargo" && binary != "cargo.exe" { + return Ok(None); + } + + let cargo_path = self.binary_file("cargo"); + + // breadcrumb in case of regression: we used to get the cargo path and + // cargo.exe path separately, not using the binary_file helper. This may + // matter if calling a binary with some personality that allows .exe and + // not .exe to coexist (e.g. wine) - but that's not something we aim to + // support : the host should always be correct. + if cargo_path.exists() { + return Ok(None); + } + + let default_host_triple = self.cfg.get_default_host_triple()?; + // XXX: This could actually consider all installed distributable + // toolchains in principle. + for fallback in ["nightly", "beta", "stable"] { + let resolved = + PartialToolchainDesc::from_str(fallback)?.resolve(&default_host_triple)?; + if let Ok(fallback) = DistributableToolchain::new(self.cfg, resolved) { + let cmd = fallback.create_fallback_command("cargo", self)?; + return Ok(Some(cmd)); + } + } + + Ok(None) + } + + #[cfg_attr(feature="otel", tracing::instrument(err,fields(binary, recursion=self.cfg.process.var("RUST_RECURSION_COUNT").ok())))] + fn create_command + Debug>(&self, binary: T) -> Result { + // Create the path to this binary within the current toolchain sysroot + let binary = if let Some(binary_str) = binary.as_ref().to_str() { + if binary_str.to_lowercase().ends_with(EXE_SUFFIX) { + binary.as_ref().to_owned() + } else { + OsString::from(format!("{binary_str}{EXE_SUFFIX}")) + } + } else { + // Very weird case. Non-unicode command. + binary.as_ref().to_owned() + }; + + let bin_path = self.path.join("bin").join(&binary); + let path = if utils::is_file(&bin_path) { + &bin_path + } else { + let recursion_count = self + .cfg + .process + .var("RUST_RECURSION_COUNT") + .ok() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + if recursion_count > env_var::RUST_RECURSION_COUNT_MAX - 1 { + let binary_lossy: String = binary.to_string_lossy().into(); + if matches!( + &self.name, + LocalToolchainName::Named(ToolchainName::Official(_)) + ) { + let distributable = DistributableToolchain::try_from(self)?; + // Design note: this is a bit of an awkward cast from + // general (toolchain) to more specialised (distributable); + // perhaps this function should something implemented on a + // trait, permitting removal of that case. + return Err(distributable.recursion_error(binary_lossy).unwrap_err()); + } else { + let t = &self.name; + return Err(anyhow!( + "'{binary_lossy}' is not installed for the custom toolchain '{t}'.\nnote: this is a custom toolchain, which cannot use `rustup component add`\n\ + help: if you built this toolchain from source, and used `rustup toolchain link`, then you may be able to build the component with `x.py`" + )); + } + } + Path::new(&binary) + }; + let mut cmd = Command::new(path); + self.set_env(&mut cmd); + Ok(cmd) + } + + #[cfg(not(windows))] + pub(crate) fn man_path(&self) -> PathBuf { + let mut buf = PathBuf::from(&self.path); + buf.extend(["share", "man"]); + buf + } + + pub fn doc_path(&self, relative: &str) -> anyhow::Result { + let parts = vec!["share", "doc", "rust", "html"]; + let mut doc_dir = self.path.clone(); + for part in parts { + doc_dir.push(part); + } + doc_dir.push(relative); + + Ok(doc_dir) + } + + pub fn open_docs(&self, relative: &str) -> anyhow::Result<()> { + utils::open_browser(&self.doc_path(relative)?) + } + + /// Remove the toolchain from disk + /// + /// + pub fn ensure_removed(cfg: &Cfg<'_>, name: LocalToolchainName) -> anyhow::Result<()> { + let path = cfg.toolchain_path(&name); + let name = match name { + LocalToolchainName::Named(t) => t, + LocalToolchainName::Path(_) => bail!("Cannot remove a path based toolchain"), + }; + let fs_modified = match Self::exists(cfg, &(&name).into())? { + true => { + (cfg.notify_handler)(Notification::UninstallingToolchain(&name)); + let installed_paths = match &name { + ToolchainName::Custom(_) => Ok(vec![InstalledPath::Dir { path: &path }]), + ToolchainName::Official(desc) => cfg.installed_paths(desc, &path), + }?; + for path in installed_paths { + match path { + InstalledPath::File { name, path } => { + utils::ensure_file_removed(name, &path)? + } + InstalledPath::Dir { path } => { + install::uninstall(path, &|n| (cfg.notify_handler)(n.into()))? + } + } + } + true + } + false => { + // Might be a dangling symlink + if path.is_symlink() { + (cfg.notify_handler)(Notification::UninstallingToolchain(&name)); + fs::remove_dir_all(&path)?; + true + } else { + info!("no toolchain installed for '{name}'"); + false + } + } + }; + + if !path.is_symlink() && !path.exists() && fs_modified { + (cfg.notify_handler)(Notification::UninstalledToolchain(&name)); + } + Ok(()) + } +} diff --git a/src/toolchain/distributable.rs b/src/toolchain/distributable.rs index 977e34577d..83d871e00c 100644 --- a/src/toolchain/distributable.rs +++ b/src/toolchain/distributable.rs @@ -21,14 +21,13 @@ use crate::{ use super::{ names::{LocalToolchainName, ToolchainName}, - toolchain::{InstalledPath, Toolchain}, + Toolchain, }; /// An official toolchain installed on the local disk #[derive(Debug)] pub(crate) struct DistributableToolchain<'a> { - toolchain: Toolchain<'a>, - cfg: &'a Cfg<'a>, + pub(super) toolchain: Toolchain<'a>, desc: ToolchainDesc, } @@ -38,16 +37,12 @@ impl<'a> DistributableToolchain<'a> { cfg: &'a Cfg<'a>, ) -> anyhow::Result { Ok(Self::try_from( - &Toolchain::from_partial(toolchain, cfg).await?, + &cfg.toolchain_from_partial(toolchain).await?, )?) } pub(crate) fn new(cfg: &'a Cfg<'a>, desc: ToolchainDesc) -> Result { - Toolchain::new(cfg, (&desc).into()).map(|toolchain| Self { - toolchain, - cfg, - desc, - }) + Toolchain::new(cfg, (&desc).into()).map(|toolchain| Self { toolchain, desc }) } pub(crate) fn desc(&self) -> &ToolchainDesc { @@ -109,8 +104,8 @@ impl<'a> DistributableToolchain<'a> { }; let notify_handler = - &|n: crate::dist::Notification<'_>| (self.cfg.notify_handler)(n.into()); - let download_cfg = self.cfg.download_cfg(¬ify_handler); + &|n: crate::dist::Notification<'_>| (self.toolchain.cfg.notify_handler)(n.into()); + let download_cfg = self.toolchain.cfg.download_cfg(¬ify_handler); manifestation .update( @@ -204,7 +199,7 @@ impl<'a> DistributableToolchain<'a> { // it to do that, because cargo's directory contains the _wrong_ rustc. See // the documentation for the lpCommandLine argument of CreateProcess. let exe_path = if cfg!(windows) { - let fallback_dir = self.cfg.rustup_dir.join("fallback"); + let fallback_dir = self.toolchain.cfg.rustup_dir.join("fallback"); fs::create_dir_all(&fallback_dir) .context("unable to create dir to hold fallback exe")?; let fallback_file = fallback_dir.join("cargo.exe"); @@ -413,17 +408,16 @@ impl<'a> DistributableToolchain<'a> { }) .ok(); - let hash_path = self.cfg.get_hash_file(&self.desc, true)?; + let cfg = self.toolchain.cfg; + let hash_path = cfg.get_hash_file(&self.desc, true)?; let update_hash = Some(&hash_path as &Path); InstallMethod::Dist { - cfg: self.cfg, + cfg, desc: &self.desc, profile, update_hash, - dl_cfg: self - .cfg - .download_cfg(&|n| (self.cfg.notify_handler)(n.into())), + dl_cfg: cfg.download_cfg(&|n| (cfg.notify_handler)(n.into())), force, allow_downgrade, exists: true, @@ -456,7 +450,7 @@ impl<'a> DistributableToolchain<'a> { "the '{binary_lossy}' binary, normally provided by the '{short_name}' component, is not applicable to the '{desc}' toolchain")) } else { // available, not installed, recommend installation - let selector = match self.cfg.get_default()? { + let selector = match self.toolchain.cfg.get_default()? { Some(ToolchainName::Official(n)) if n == self.desc => String::new(), _ => format!("--toolchain {} ", self.toolchain.name()), }; @@ -516,8 +510,8 @@ impl<'a> DistributableToolchain<'a> { }; let notify_handler = - &|n: crate::dist::Notification<'_>| (self.cfg.notify_handler)(n.into()); - let download_cfg = self.cfg.download_cfg(¬ify_handler); + &|n: crate::dist::Notification<'_>| (self.toolchain.cfg.notify_handler)(n.into()); + let download_cfg = self.toolchain.cfg.download_cfg(¬ify_handler); manifestation .update( @@ -534,10 +528,10 @@ impl<'a> DistributableToolchain<'a> { } pub async fn show_dist_version(&self) -> anyhow::Result> { - let update_hash = self.cfg.get_hash_file(&self.desc, false)?; + let update_hash = self.toolchain.cfg.get_hash_file(&self.desc, false)?; let notify_handler = - &|n: crate::dist::Notification<'_>| (self.cfg.notify_handler)(n.into()); - let download_cfg = self.cfg.download_cfg(¬ify_handler); + &|n: crate::dist::Notification<'_>| (self.toolchain.cfg.notify_handler)(n.into()); + let download_cfg = self.toolchain.cfg.download_cfg(¬ify_handler); match crate::dist::dl_v2_manifest(download_cfg, Some(&update_hash), &self.desc).await? { Some((manifest, _)) => Ok(Some(manifest.get_rust_version()?.to_string())), @@ -551,20 +545,6 @@ impl<'a> DistributableToolchain<'a> { None => Ok(None), } } - - pub(crate) fn installed_paths<'b>( - cfg: &Cfg<'_>, - desc: &ToolchainDesc, - path: &'b Path, - ) -> anyhow::Result>> { - Ok(vec![ - InstalledPath::File { - name: "update hash", - path: cfg.get_hash_file(desc, false)?, - }, - InstalledPath::Dir { path }, - ]) - } } impl<'a> TryFrom<&Toolchain<'a>> for DistributableToolchain<'a> { @@ -574,7 +554,6 @@ impl<'a> TryFrom<&Toolchain<'a>> for DistributableToolchain<'a> { match value.name() { LocalToolchainName::Named(ToolchainName::Official(desc)) => Ok(Self { toolchain: value.clone(), - cfg: value.cfg, desc: desc.clone(), }), n => Err(RustupError::ComponentsUnsupported(n.to_string())), diff --git a/src/toolchain/names.rs b/src/toolchain/names.rs index 6b203f9ef9..256924266b 100644 --- a/src/toolchain/names.rs +++ b/src/toolchain/names.rs @@ -248,7 +248,7 @@ impl Display for MaybeOfficialToolchainName { /// ToolchainName can be used in calls to Cfg that alter configuration, /// like setting overrides, or that depend on configuration, like calculating /// the toolchain directory. -#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Clone, Debug, Eq, PartialEq)] pub enum ToolchainName { Custom(CustomToolchainName), Official(ToolchainDesc), @@ -353,7 +353,7 @@ impl Display for ResolvableLocalToolchainName { /// the toolchain directory. It is not used to model the RUSTUP_TOOLCHAIN /// variable, because that can take unresolved toolchain values that are not /// invalid for referring to an installed toolchain. -#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] +#[derive(Clone, Debug, Eq, PartialEq)] pub(crate) enum LocalToolchainName { Named(ToolchainName), Path(PathBasedToolchainName), diff --git a/src/toolchain/toolchain.rs b/src/toolchain/toolchain.rs deleted file mode 100644 index f97e28572c..0000000000 --- a/src/toolchain/toolchain.rs +++ /dev/null @@ -1,495 +0,0 @@ -use std::{ - env::{self, consts::EXE_SUFFIX}, - ffi::{OsStr, OsString}, - fmt::Debug, - fs, - io::{self, BufRead, BufReader}, - path::{Path, PathBuf}, - process::{Command, Stdio}, - str::FromStr, - time::Duration, -}; - -use anyhow::{anyhow, bail}; -use fs_at::OpenOptions; -use tracing::info; -use wait_timeout::ChildExt; - -use crate::{ - config::{ActiveReason, Cfg}, - dist::PartialToolchainDesc, - env_var, install, - notifications::Notification, - utils::{raw::open_dir_following_links, utils}, - RustupError, -}; - -use super::{ - distributable::DistributableToolchain, - names::{LocalToolchainName, ResolvableToolchainName, ToolchainName}, -}; - -/// A toolchain installed on the local disk -#[derive(Clone, Debug)] -pub(crate) struct Toolchain<'a> { - pub(super) cfg: &'a Cfg<'a>, - name: LocalToolchainName, - path: PathBuf, -} - -impl<'a> Toolchain<'a> { - pub(crate) async fn from_local( - toolchain_name: &LocalToolchainName, - install_if_missing: bool, - cfg: &'a Cfg<'a>, - ) -> anyhow::Result> { - match toolchain_name { - LocalToolchainName::Named(ToolchainName::Official(desc)) => { - match DistributableToolchain::new(cfg, desc.clone()) { - Err(RustupError::ToolchainNotInstalled(_)) => { - if install_if_missing { - DistributableToolchain::install( - cfg, - desc, - &[], - &[], - cfg.get_profile()?, - true, - ) - .await?; - } - } - o => { - o?; - } - } - } - n => { - if !Self::exists(cfg, n)? { - return Err(RustupError::ToolchainNotInstallable(n.to_string()).into()); - } - } - } - - Ok(Self::new(cfg, toolchain_name.clone())?) - } - - pub(crate) async fn from_partial( - toolchain: Option, - cfg: &'a Cfg<'a>, - ) -> anyhow::Result { - match toolchain.map(|it| ResolvableToolchainName::from(&it)) { - Some(toolchain) => { - let desc = toolchain.resolve(&cfg.get_default_host_triple()?)?; - Ok(Toolchain::new(cfg, desc.into())?) - } - None => Ok(cfg.find_or_install_active_toolchain().await?.0), - } - } - - /// Calls Toolchain::new(), but augments the error message with more context - /// from the ActiveReason if the toolchain isn't installed. - pub(crate) fn with_reason( - cfg: &'a Cfg<'a>, - name: LocalToolchainName, - reason: &ActiveReason, - ) -> anyhow::Result { - match Self::new(cfg, name.clone()) { - Err(RustupError::ToolchainNotInstalled(_)) => (), - result => { - return Ok(result?); - } - } - - let reason_err = match reason { - ActiveReason::Environment => { - "the RUSTUP_TOOLCHAIN environment variable specifies an uninstalled toolchain" - .to_string() - } - ActiveReason::CommandLine => { - "the +toolchain on the command line specifies an uninstalled toolchain".to_string() - } - ActiveReason::OverrideDB(ref path) => format!( - "the directory override for '{}' specifies an uninstalled toolchain", - utils::canonicalize_path(path, cfg.notify_handler.as_ref()).display(), - ), - ActiveReason::ToolchainFile(ref path) => format!( - "the toolchain file at '{}' specifies an uninstalled toolchain", - utils::canonicalize_path(path, cfg.notify_handler.as_ref()).display(), - ), - ActiveReason::Default => { - "the default toolchain does not describe an installed toolchain".to_string() - } - }; - - Err(anyhow!(reason_err).context(format!("override toolchain '{name}' is not installed"))) - } - - pub(crate) fn new(cfg: &'a Cfg<'a>, name: LocalToolchainName) -> Result { - let path = cfg.toolchain_path(&name); - if !Toolchain::exists(cfg, &name)? { - return Err(match name { - LocalToolchainName::Named(name) => RustupError::ToolchainNotInstalled(name), - LocalToolchainName::Path(name) => RustupError::PathToolchainNotInstalled(name), - }); - } - Ok(Self { cfg, name, path }) - } - - /// Ok(True) if the toolchain exists. Ok(False) if the toolchain or its - /// containing directory don't exist. Err otherwise. - pub(crate) fn exists(cfg: &Cfg<'_>, name: &LocalToolchainName) -> Result { - let path = cfg.toolchain_path(name); - // toolchain validation should have prevented a situation where there is - // no base dir, but defensive programming is defensive. - let parent = path - .parent() - .ok_or_else(|| RustupError::InvalidToolchainName(name.to_string()))?; - let base_name = path - .file_name() - .ok_or_else(|| RustupError::InvalidToolchainName(name.to_string()))?; - let parent_dir = match open_dir_following_links(parent) { - Ok(d) => d, - Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(false), - e => e?, - }; - let opened = OpenOptions::default() - .read(true) - .follow(true) - .open_dir_at(&parent_dir, base_name); - Ok(opened.is_ok()) - } - - pub(crate) fn name(&self) -> &LocalToolchainName { - &self.name - } - - pub(crate) fn path(&self) -> &Path { - &self.path - } - - /// The path to a binary within the toolchain, without regard for cargo-fallback logic - pub fn binary_file(&self, name: &str) -> PathBuf { - let mut path = self.path.clone(); - path.push("bin"); - path.push(name.to_owned() + env::consts::EXE_SUFFIX); - path - } - - /// Not intended to be public, but more code golf required to get it hidden. - /// pub because of create_fallback_command - pub fn set_env(&self, cmd: &mut Command) { - self.set_ldpath(cmd); - - // Older versions of Cargo used a slightly different definition of - // cargo home. Rustup does not read HOME on Windows whereas the older - // versions of Cargo did. Rustup and Cargo should be in sync now (both - // using the same `home` crate), but this is retained to ensure cargo - // and rustup agree in older versions. - if let Ok(cargo_home) = self.cfg.process.cargo_home() { - cmd.env("CARGO_HOME", &cargo_home); - } - - env_var::inc("RUST_RECURSION_COUNT", cmd, self.cfg.process); - - cmd.env("RUSTUP_TOOLCHAIN", format!("{}", self.name)); - cmd.env("RUSTUP_HOME", &self.cfg.rustup_dir); - } - - /// Apply the appropriate LD path for a command being run from a toolchain. - fn set_ldpath(&self, cmd: &mut Command) { - let mut new_path = vec![self.path.join("lib")]; - - #[cfg(not(target_os = "macos"))] - mod sysenv { - pub const LOADER_PATH: &str = "LD_LIBRARY_PATH"; - } - #[cfg(target_os = "macos")] - mod sysenv { - // When loading and linking a dynamic library or bundle, dlopen - // searches in LD_LIBRARY_PATH, DYLD_LIBRARY_PATH, PWD, and - // DYLD_FALLBACK_LIBRARY_PATH. - // In the Mach-O format, a dynamic library has an "install path." - // Clients linking against the library record this path, and the - // dynamic linker, dyld, uses it to locate the library. - // dyld searches DYLD_LIBRARY_PATH *before* the install path. - // dyld searches DYLD_FALLBACK_LIBRARY_PATH only if it cannot - // find the library in the install path. - // Setting DYLD_LIBRARY_PATH can easily have unintended - // consequences. - pub const LOADER_PATH: &str = "DYLD_FALLBACK_LIBRARY_PATH"; - } - if cfg!(target_os = "macos") - && self - .cfg - .process - .var_os(sysenv::LOADER_PATH) - .filter(|x| x.len() > 0) - .is_none() - { - // These are the defaults when DYLD_FALLBACK_LIBRARY_PATH isn't - // set or set to an empty string. Since we are explicitly setting - // the value, make sure the defaults still work. - if let Some(home) = self.cfg.process.var_os("HOME") { - new_path.push(PathBuf::from(home).join("lib")); - } - new_path.push(PathBuf::from("/usr/local/lib")); - new_path.push(PathBuf::from("/usr/lib")); - } - - env_var::prepend_path(sysenv::LOADER_PATH, new_path, cmd, self.cfg.process); - - // Prepend CARGO_HOME/bin to the PATH variable so that we're sure to run - // cargo/rustc via the proxy bins. There is no fallback case for if the - // proxy bins don't exist. We'll just be running whatever happens to - // be on the PATH. - let mut path_entries = vec![]; - if let Ok(cargo_home) = self.cfg.process.cargo_home() { - path_entries.push(cargo_home.join("bin")); - } - - if cfg!(target_os = "windows") { - // Historically rustup included the bin directory in PATH to - // work around some bugs (see - // https://github.com/rust-lang/rustup/pull/3178 for more - // information). This shouldn't be needed anymore, and it causes - // problems because calling tools recursively (like `cargo - // +nightly metadata` from within a cargo subcommand). The - // recursive call won't work because it is not executing the - // proxy, so the `+` toolchain override doesn't work. - // - // The RUSTUP_WINDOWS_PATH_ADD_BIN env var was added to opt-in to - // testing the fix. The default is now off, but this is left here - // just in case there are problems. Consider removing in the - // future if it doesn't seem necessary. - if self - .cfg - .process - .var_os("RUSTUP_WINDOWS_PATH_ADD_BIN") - .map_or(false, |s| s == "1") - { - path_entries.push(self.path.join("bin")); - } - } - - env_var::prepend_path("PATH", path_entries, cmd, self.cfg.process); - } - - /// Infallible function that describes the version of rustc in an installed distribution - #[cfg_attr(feature = "otel", tracing::instrument)] - pub fn rustc_version(&self) -> String { - match self.create_command("rustc") { - Ok(mut cmd) => { - cmd.arg("--version"); - cmd.stdin(Stdio::null()); - cmd.stdout(Stdio::piped()); - cmd.stderr(Stdio::piped()); - self.set_ldpath(&mut cmd); - - // some toolchains are faulty with some combinations of platforms and - // may fail to launch but also to timely terminate. - // (known cases include Rust 1.3.0 through 1.10.0 in recent macOS Sierra.) - // we guard against such cases by enforcing a reasonable timeout to read. - let mut line1 = None; - if let Ok(mut child) = cmd.spawn() { - let timeout = Duration::new(10, 0); - match child.wait_timeout(timeout) { - Ok(Some(status)) if status.success() => { - let out = child - .stdout - .expect("Child::stdout requested but not present"); - let mut line = String::new(); - if BufReader::new(out).read_line(&mut line).is_ok() { - let lineend = line.trim_end_matches(&['\r', '\n'][..]).len(); - line.truncate(lineend); - line1 = Some(line); - } - } - Ok(None) => { - let _ = child.kill(); - return String::from("(timeout reading rustc version)"); - } - Ok(Some(_)) | Err(_) => {} - } - } - - if let Some(line1) = line1 { - line1 - } else { - String::from("(error reading rustc version)") - } - } - Err(_) => String::from("(rustc does not exist)"), - } - } - - pub(crate) fn command(&self, binary: &str) -> anyhow::Result { - // Should push the cargo fallback into a custom toolchain type? And then - // perhaps a trait that create command layers on? - if !matches!( - self.name(), - LocalToolchainName::Named(ToolchainName::Official(_)) - ) { - if let Some(cmd) = self.maybe_do_cargo_fallback(binary)? { - return Ok(cmd); - } - } - - self.create_command(binary) - } - - // Custom toolchains don't have cargo, so here we detect that situation and - // try to find a different cargo. - pub(crate) fn maybe_do_cargo_fallback(&self, binary: &str) -> anyhow::Result> { - if binary != "cargo" && binary != "cargo.exe" { - return Ok(None); - } - - let cargo_path = self.binary_file("cargo"); - - // breadcrumb in case of regression: we used to get the cargo path and - // cargo.exe path separately, not using the binary_file helper. This may - // matter if calling a binary with some personality that allows .exe and - // not .exe to coexist (e.g. wine) - but that's not something we aim to - // support : the host should always be correct. - if cargo_path.exists() { - return Ok(None); - } - - let default_host_triple = self.cfg.get_default_host_triple()?; - // XXX: This could actually consider all installed distributable - // toolchains in principle. - for fallback in ["nightly", "beta", "stable"] { - let resolved = - PartialToolchainDesc::from_str(fallback)?.resolve(&default_host_triple)?; - if let Ok(fallback) = DistributableToolchain::new(self.cfg, resolved) { - let cmd = fallback.create_fallback_command("cargo", self)?; - return Ok(Some(cmd)); - } - } - - Ok(None) - } - - #[cfg_attr(feature="otel", tracing::instrument(err,fields(binary, recursion=self.cfg.process.var("RUST_RECURSION_COUNT").ok())))] - fn create_command + Debug>(&self, binary: T) -> Result { - // Create the path to this binary within the current toolchain sysroot - let binary = if let Some(binary_str) = binary.as_ref().to_str() { - if binary_str.to_lowercase().ends_with(EXE_SUFFIX) { - binary.as_ref().to_owned() - } else { - OsString::from(format!("{binary_str}{EXE_SUFFIX}")) - } - } else { - // Very weird case. Non-unicode command. - binary.as_ref().to_owned() - }; - - let bin_path = self.path.join("bin").join(&binary); - let path = if utils::is_file(&bin_path) { - &bin_path - } else { - let recursion_count = self - .cfg - .process - .var("RUST_RECURSION_COUNT") - .ok() - .and_then(|s| s.parse().ok()) - .unwrap_or(0); - if recursion_count > env_var::RUST_RECURSION_COUNT_MAX - 1 { - let binary_lossy: String = binary.to_string_lossy().into(); - if matches!( - &self.name, - LocalToolchainName::Named(ToolchainName::Official(_)) - ) { - let distributable = DistributableToolchain::try_from(self)?; - // Design note: this is a bit of an awkward cast from - // general (toolchain) to more specialised (distributable); - // perhaps this function should something implemented on a - // trait, permitting removal of that case. - return Err(distributable.recursion_error(binary_lossy).unwrap_err()); - } else { - let t = &self.name; - return Err(anyhow!( - "'{binary_lossy}' is not installed for the custom toolchain '{t}'.\nnote: this is a custom toolchain, which cannot use `rustup component add`\n\ - help: if you built this toolchain from source, and used `rustup toolchain link`, then you may be able to build the component with `x.py`" - )); - } - } - Path::new(&binary) - }; - let mut cmd = Command::new(path); - self.set_env(&mut cmd); - Ok(cmd) - } - - pub fn doc_path(&self, relative: &str) -> anyhow::Result { - let parts = vec!["share", "doc", "rust", "html"]; - let mut doc_dir = self.path.clone(); - for part in parts { - doc_dir.push(part); - } - doc_dir.push(relative); - - Ok(doc_dir) - } - - pub fn open_docs(&self, relative: &str) -> anyhow::Result<()> { - utils::open_browser(&self.doc_path(relative)?) - } - - /// Remove the toolchain from disk - /// - /// - pub fn ensure_removed(cfg: &Cfg<'_>, name: LocalToolchainName) -> anyhow::Result<()> { - let path = cfg.toolchain_path(&name); - let name = match name { - LocalToolchainName::Named(t) => t, - LocalToolchainName::Path(_) => bail!("Cannot remove a path based toolchain"), - }; - let fs_modified = match Self::exists(cfg, &(&name).into())? { - true => { - (cfg.notify_handler)(Notification::UninstallingToolchain(&name)); - let installed_paths = match &name { - ToolchainName::Custom(_) => Ok(vec![InstalledPath::Dir { path: &path }]), - ToolchainName::Official(desc) => { - DistributableToolchain::installed_paths(cfg, desc, &path) - } - }?; - for path in installed_paths { - match path { - InstalledPath::File { name, path } => { - utils::ensure_file_removed(name, &path)? - } - InstalledPath::Dir { path } => { - install::uninstall(path, &|n| (cfg.notify_handler)(n.into()))? - } - } - } - true - } - false => { - // Might be a dangling symlink - if path.is_symlink() { - (cfg.notify_handler)(Notification::UninstallingToolchain(&name)); - fs::remove_dir_all(&path)?; - true - } else { - info!("no toolchain installed for '{name}'"); - false - } - } - }; - - if !path.is_symlink() && !path.exists() && fs_modified { - (cfg.notify_handler)(Notification::UninstalledToolchain(&name)); - } - Ok(()) - } -} - -/// Installed paths -pub(crate) enum InstalledPath<'a> { - File { name: &'static str, path: PathBuf }, - Dir { path: &'a Path }, -} diff --git a/tests/suite/cli_exact.rs b/tests/suite/cli_exact.rs index 188ba9f183..7b2302d258 100644 --- a/tests/suite/cli_exact.rs +++ b/tests/suite/cli_exact.rs @@ -685,7 +685,7 @@ fn undefined_linked_toolchain() { config.expect_err_ex( &["cargo", "+bogus", "test"], r"", - "error: toolchain 'bogus' is not installable\n", + "error: toolchain 'bogus' is not installed\n", ); }) }); diff --git a/tests/suite/cli_rustup.rs b/tests/suite/cli_rustup.rs index 5eb5486d2a..c1018a2073 100644 --- a/tests/suite/cli_rustup.rs +++ b/tests/suite/cli_rustup.rs @@ -2450,7 +2450,7 @@ fn non_utf8_toolchain() { [OsStr::from_bytes(b"+\xc3\x28")], &[("RUST_BACKTRACE", "1")], ); - assert!(out.stderr.contains("toolchain '�(' is not installable")); + assert!(out.stderr.contains("toolchain '�(' is not installed")); }) }); } @@ -2469,7 +2469,7 @@ fn non_utf8_toolchain() { [OsString::from_wide(&[u16::from(b'+'), 0xd801, 0xd801])], &[("RUST_BACKTRACE", "1")], ); - assert!(out.stderr.contains("toolchain '��' is not installable")); + assert!(out.stderr.contains("toolchain '��' is not installed")); }) }); }