| //! Cargo's config system. | |
| //! | |
| //! The [`GlobalContext`] object contains general information about the environment, | |
| //! and provides access to Cargo's configuration files. | |
| //! | |
| //! ## Config value API | |
| //! | |
| //! The primary API for fetching user-defined config values is the | |
| //! [`GlobalContext::get`] method. It uses `serde` to translate config values to a | |
| //! target type. | |
| //! | |
| //! There are a variety of helper types for deserializing some common formats: | |
| //! | |
| //! - [`value::Value`]: This type provides access to the location where the | |
| //! config value was defined. | |
| //! - [`ConfigRelativePath`]: For a path that is relative to where it is | |
| //! defined. | |
| //! - [`PathAndArgs`]: Similar to [`ConfigRelativePath`], | |
| //! but also supports a list of arguments, useful for programs to execute. | |
| //! - [`StringList`]: Get a value that is either a list or a whitespace split | |
| //! string. | |
| //! | |
| //! # Config schemas | |
| //! | |
| //! Configuration schemas are defined in the [`schema`] module. | |
| //! | |
| //! ## Config deserialization | |
| //! | |
| //! Cargo uses a two-layer deserialization approach: | |
| //! | |
| //! 1. **External sources → `ConfigValue`** --- | |
| //! Configuration files, environment variables, and CLI `--config` arguments | |
| //! are parsed into [`ConfigValue`] instances via [`ConfigValue::from_toml`]. | |
| //! These parsed results are stored in [`GlobalContext`]. | |
| //! | |
| //! 2. **`ConfigValue` → Target types** --- | |
| //! The [`GlobalContext::get`] method uses a [custom serde deserializer](Deserializer) | |
| //! to convert [`ConfigValue`] instances to the caller's desired type. | |
| //! Precedence between [`ConfigValue`] sources is resolved during retrieval | |
| //! based on [`Definition`] priority. | |
| //! See the top-level documentation of the [`de`] module for more. | |
| //! | |
| //! ## Map key recommendations | |
| //! | |
| //! Handling tables that have arbitrary keys can be tricky, particularly if it | |
| //! should support environment variables. In general, if possible, the caller | |
| //! should pass the full key path into the `get()` method so that the config | |
| //! deserializer can properly handle environment variables (which need to be | |
| //! uppercased, and dashes converted to underscores). | |
| //! | |
| //! A good example is the `[target]` table. The code will request | |
| //! `target.$TUPLE` and the config system can then appropriately fetch | |
| //! environment variables like `CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER`. | |
| //! Conversely, it is not possible do the same thing for the `cfg()` target | |
| //! tables (because Cargo must fetch all of them), so those do not support | |
| //! environment variables. | |
| //! | |
| //! Try to avoid keys that are a prefix of another with a dash/underscore. For | |
| //! example `build.target` and `build.target-dir`. This is OK if these are not | |
| //! structs/maps, but if it is a struct or map, then it will not be able to | |
| //! read the environment variable due to ambiguity. (See `ConfigMapAccess` for | |
| //! more details.) | |
| use crate::util::data_structures::{HashMap, HashSet}; | |
| use std::borrow::Cow; | |
| use std::env; | |
| use std::ffi::{OsStr, OsString}; | |
| use std::fmt; | |
| use std::fs::File; | |
| use std::io::SeekFrom; | |
| use std::io::prelude::*; | |
| use std::path::{Path, PathBuf}; | |
| use std::str::FromStr; | |
| use std::sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock}; | |
| use std::time::Instant; | |
| use self::ConfigValue as CV; | |
| use crate::ops::RegistryCredentialConfig; | |
| use crate::sources::CRATES_IO_INDEX; | |
| use crate::sources::CRATES_IO_REGISTRY; | |
| use crate::util::OnceExt as _; | |
| use crate::util::VersionReqMatchMode; | |
| use crate::util::cache_lock::{CacheLock, CacheLockMode, CacheLocker}; | |
| use crate::util::errors::CargoResult; | |
| use crate::util::network::http::{HandleConfiguration, configure_http_handle, http_handle}; | |
| use crate::util::network::http_async; | |
| use crate::util::restricted_names::is_glob_pattern; | |
| use crate::util::{CanonicalUrl, closest_msg, internal}; | |
| use crate::util::{Filesystem, IntoUrl, IntoUrlWithBase, Rustc}; | |
| use crate::workspace::global_cache_tracker::{DeferredGlobalLastUse, GlobalCacheTracker}; | |
| use crate::workspace::{CliUnstable, SourceId, Workspace, WorkspaceRootConfig, features}; | |
| use anyhow::{Context as _, anyhow, bail, format_err}; | |
| use cargo_credential::Secret; | |
| use cargo_util::paths; | |
| use cargo_util_schemas::manifest::RegistryName; | |
| use cargo_util_terminal::report::Level; | |
| use cargo_util_terminal::{Shell, Verbosity}; | |
| use curl::easy::Easy; | |
| use itertools::Itertools; | |
| use serde::Deserialize; | |
| use serde::de::IntoDeserializer as _; | |
| use time::OffsetDateTime; | |
| use toml_edit::Item; | |
| use url::Url; | |
| mod de; | |
| use de::Deserializer; | |
| mod error; | |
| pub use error::ConfigError; | |
| use crate::config::is_nonmergeable_list; | |
| pub use crate::config::{ConfigKey, ConfigStore, ConfigValue, Definition, DefinitionKind}; | |
| mod value; | |
| pub use value::{OptValue, Value}; | |
| mod path; | |
| pub use path::BracketType; | |
| pub use path::ConfigRelativePath; | |
| pub use path::PathAndArgs; | |
| pub use path::ResolveTemplateError; | |
| mod target; | |
| pub use target::{TargetCfgConfig, TargetConfig}; | |
| mod environment; | |
| use environment::Env; | |
| mod schema; | |
| pub use schema::*; | |
| /// Helper macro for creating typed access methods. | |
| macro_rules! get_value_typed { | |
| ($name:ident, $ty:ty, $variant:ident, $expected:expr) => { | |
| /// Low-level private method for getting a config value as an [`OptValue`]. | |
| fn $name(&self, key: &ConfigKey) -> Result<OptValue<$ty>, ConfigError> { | |
| let cv = self.config()?.get_cv(key)?; | |
| let env = self.get_config_env::<$ty>(key)?; | |
| match (cv, env) { | |
| (Some(CV::$variant(val, definition)), Some(env)) => { | |
| if definition.is_higher_priority(&env.definition) { | |
| Ok(Some(Value { val, definition })) | |
| } else { | |
| Ok(Some(env)) | |
| } | |
| } | |
| (Some(CV::$variant(val, definition)), None) => Ok(Some(Value { val, definition })), | |
| (Some(cv), _) => Err(ConfigError::expected(key, $expected, &cv)), | |
| (None, Some(env)) => Ok(Some(env)), | |
| (None, None) => Ok(None), | |
| } | |
| } | |
| }; | |
| } | |
| pub const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[ | |
| "paths", | |
| "alias", | |
| "build", | |
| "credential-alias", | |
| "doc", | |
| "env", | |
| "future-incompat-report", | |
| "cache", | |
| "cargo-new", | |
| "http", | |
| "install", | |
| "net", | |
| "patch", | |
| "profile", | |
| "resolver", | |
| "registries", | |
| "registry", | |
| "source", | |
| "target", | |
| "term", | |
| ]; | |
| /// A previously generated authentication token and the data needed to determine if it can be reused. | |
| #[derive(Debug)] | |
| pub struct CredentialCacheValue { | |
| pub token_value: Secret<String>, | |
| pub expiration: Option<OffsetDateTime>, | |
| pub operation_independent: bool, | |
| } | |
| /// Configuration information for cargo. This is not specific to a build, it is information | |
| /// relating to cargo itself. | |
| #[derive(Debug)] | |
| pub struct GlobalContext { | |
| /// The location of the user's Cargo home directory. OS-dependent. | |
| home_path: Filesystem, | |
| /// Information about how to write messages to the shell | |
| shell: Mutex<Shell>, | |
| /// A collection of configuration options | |
| config: OnceLock<ConfigStore>, | |
| /// A collection of configuration options from the credentials file | |
| credentials_config: OnceLock<ConfigStore>, | |
| /// CLI config values, passed in via `configure`. | |
| cli_config: Option<Vec<String>>, | |
| /// The current working directory of cargo | |
| cwd: PathBuf, | |
| /// Directory where config file searching should stop (inclusive). | |
| search_stop_path: Option<PathBuf>, | |
| /// The location of the cargo executable (path to current process) | |
| cargo_exe: OnceLock<PathBuf>, | |
| /// The location of the rustdoc executable | |
| rustdoc: OnceLock<PathBuf>, | |
| /// Whether we are printing extra verbose messages | |
| extra_verbose: bool, | |
| /// `frozen` is the same as `locked`, but additionally will not access the | |
| /// network to determine if the lock file is out-of-date. | |
| frozen: bool, | |
| /// `locked` is set if we should not update lock files. If the lock file | |
| /// is missing, or needs to be updated, an error is produced. | |
| locked: bool, | |
| /// `offline` is set if we should never access the network, but otherwise | |
| /// continue operating if possible. | |
| offline: bool, | |
| /// A global static IPC control mechanism (used for managing parallel builds) | |
| jobserver: Option<&'static jobserver::Client>, | |
| /// Cli flags of the form "-Z something" merged with config file values | |
| unstable_flags: CliUnstable, | |
| /// Cli flags of the form "-Z something" | |
| unstable_flags_cli: Option<Vec<String>>, | |
| /// A handle on curl easy mode for http calls | |
| easy: OnceLock<Mutex<Easy>>, | |
| /// Cache of the `SourceId` for crates.io | |
| crates_io_source_id: OnceLock<SourceId>, | |
| /// If false, don't cache `rustc --version --verbose` invocations | |
| cache_rustc_info: bool, | |
| /// Monotonic start of this cargo invocation for reporting time elapsed. | |
| invocation_instant: Instant, | |
| /// Wall-clock time of this cargo invocation. | |
| /// | |
| /// Currently used as the reference time for `min-publish-age` and `-Zbuild-analysis`. | |
| invocation_time: jiff::Timestamp, | |
| /// Target Directory via resolved Cli parameter | |
| target_dir: Option<Filesystem>, | |
| /// Environment variable snapshot. | |
| env: Env, | |
| /// Tracks which sources have been updated to avoid multiple updates. | |
| updated_sources: Mutex<HashSet<SourceId>>, | |
| /// Cache of credentials from configuration or credential providers. | |
| /// Maps from url to credential value. | |
| credential_cache: Mutex<HashMap<CanonicalUrl, CredentialCacheValue>>, | |
| /// Cache of registry config from the `[registries]` table. | |
| registry_config: Mutex<HashMap<SourceId, Option<RegistryConfig>>>, | |
| /// Locks on the package and index caches. | |
| package_cache_lock: CacheLocker, | |
| /// Cached configuration parsed by Cargo | |
| http_config: OnceLock<CargoHttpConfig>, | |
| http_async: OnceLock<http_async::Client>, | |
| future_incompat_config: OnceLock<CargoFutureIncompatConfig>, | |
| net_config: OnceLock<CargoNetConfig>, | |
| build_config: OnceLock<CargoBuildConfig>, | |
| target_cfgs: OnceLock<Vec<(String, TargetCfgConfig)>>, | |
| doc_extern_map: OnceLock<RustdocExternMap>, | |
| progress_config: ProgressConfig, | |
| env_config: OnceLock<Arc<HashMap<String, OsString>>>, | |
| /// This should be false if: | |
| /// - this is an artifact of the rustc distribution process for "stable" or for "beta" | |
| /// - this is an `#[test]` that does not opt in with `enable_nightly_features` | |
| /// - this is an integration test that uses `ProcessBuilder` | |
| /// that does not opt in with `masquerade_as_nightly_cargo` | |
| /// This should be true if: | |
| /// - this is an artifact of the rustc distribution process for "nightly" | |
| /// - this is being used in the rustc distribution process internally | |
| /// - this is a cargo executable that was built from source | |
| /// - this is an `#[test]` that called `enable_nightly_features` | |
| /// - this is an integration test that uses `ProcessBuilder` | |
| /// that called `masquerade_as_nightly_cargo` | |
| /// It's public to allow tests use nightly features. | |
| /// NOTE: this should be set before `configure()`. If calling this from an integration test, | |
| /// consider using `ConfigBuilder::enable_nightly_features` instead. | |
| pub nightly_features_allowed: bool, | |
| /// `WorkspaceRootConfigs` that have been found | |
| ws_roots: Mutex<HashMap<PathBuf, WorkspaceRootConfig>>, | |
| /// The global cache tracker is a database used to track disk cache usage. | |
| global_cache_tracker: OnceLock<Mutex<GlobalCacheTracker>>, | |
| /// A cache of modifications to make to [`GlobalContext::global_cache_tracker`], | |
| /// saved to disk in a batch to improve performance. | |
| deferred_global_last_use: OnceLock<Mutex<DeferredGlobalLastUse>>, | |
| } | |
| impl GlobalContext { | |
| /// Creates a new config instance. | |
| /// | |
| /// This is typically used for tests or other special cases. `default` is | |
| /// preferred otherwise. | |
| /// | |
| /// This does only minimal initialization. In particular, it does not load | |
| /// any config files from disk. Those will be loaded lazily as-needed. | |
| pub fn new(mut shell: Shell, cwd: PathBuf, homedir: PathBuf) -> GlobalContext { | |
| static GLOBAL_JOBSERVER: LazyLock<CargoResult<Option<jobserver::Client>>> = LazyLock::new( | |
| || { | |
| use jobserver::FromEnvErrorKind; | |
| // Note that this is unsafe because it may misinterpret file descriptors | |
| // on Unix as jobserver file descriptors. We hopefully execute this near | |
| // the beginning of the process though to ensure we don't get false | |
| // positives, or in other words we try to execute this before we open | |
| // any file descriptors ourselves. | |
| let jobserver::FromEnv { client, var } = | |
| unsafe { jobserver::Client::from_env_ext(true) }; | |
| match client { | |
| Ok(client) => return Ok(Some(client)), | |
| Err(e) | |
| if matches!( | |
| e.kind(), | |
| FromEnvErrorKind::NoEnvVar | |
| | FromEnvErrorKind::NoJobserver | |
| | FromEnvErrorKind::NegativeFd | |
| | FromEnvErrorKind::Unsupported | |
| ) => | |
| { | |
| Ok(None) | |
| } | |
| Err(e) => { | |
| let (name, value) = var.unwrap(); | |
| Err(anyhow::anyhow!( | |
| "failed to connect to jobserver from environment variable `{name}={value:?}`: {e}" | |
| )) | |
| } | |
| } | |
| }, | |
| ); | |
| let jobserver = match &*GLOBAL_JOBSERVER { | |
| Ok(jobserver) => jobserver.as_ref(), | |
| Err(e) => { | |
| let _ = shell.warn(e); | |
| None | |
| } | |
| }; | |
| let env = Env::new(); | |
| let cache_key = "CARGO_CACHE_RUSTC_INFO"; | |
| let cache_rustc_info = match env.get_env_os(cache_key) { | |
| Some(cache) => cache != "0", | |
| _ => true, | |
| }; | |
| #[expect( | |
| clippy::disallowed_methods, | |
| reason = "testing only, no reason for config support" | |
| )] | |
| let invocation_time = match env::var("__CARGO_TEST_INVOCATION_TIME") { | |
| Ok(now) => now.parse().unwrap(), | |
| Err(_) => jiff::Timestamp::now(), | |
| }; | |
| GlobalContext { | |
| home_path: Filesystem::new(homedir), | |
| shell: Mutex::new(shell), | |
| cwd, | |
| search_stop_path: None, | |
| config: Default::default(), | |
| credentials_config: Default::default(), | |
| cli_config: None, | |
| cargo_exe: Default::default(), | |
| rustdoc: Default::default(), | |
| extra_verbose: false, | |
| frozen: false, | |
| locked: false, | |
| offline: false, | |
| jobserver, | |
| unstable_flags: CliUnstable::default(), | |
| unstable_flags_cli: None, | |
| easy: Default::default(), | |
| crates_io_source_id: Default::default(), | |
| cache_rustc_info, | |
| invocation_instant: Instant::now(), | |
| invocation_time, | |
| target_dir: None, | |
| env, | |
| updated_sources: Default::default(), | |
| credential_cache: Default::default(), | |
| registry_config: Default::default(), | |
| package_cache_lock: CacheLocker::new(), | |
| http_config: Default::default(), | |
| http_async: Default::default(), | |
| future_incompat_config: Default::default(), | |
| net_config: Default::default(), | |
| build_config: Default::default(), | |
| target_cfgs: Default::default(), | |
| doc_extern_map: Default::default(), | |
| progress_config: ProgressConfig::default(), | |
| env_config: Default::default(), | |
| nightly_features_allowed: matches!(&*features::channel(), "nightly" | "dev"), | |
| ws_roots: Default::default(), | |
| global_cache_tracker: Default::default(), | |
| deferred_global_last_use: Default::default(), | |
| } | |
| } | |
| /// Creates a new instance, with all default settings. | |
| /// | |
| /// This does only minimal initialization. In particular, it does not load | |
| /// any config files from disk. Those will be loaded lazily as-needed. | |
| pub fn default() -> CargoResult<GlobalContext> { | |
| let shell = Shell::new(); | |
| let cwd = | |
| env::current_dir().context("couldn't get the current directory of the process")?; | |
| let homedir = homedir(&cwd).ok_or_else(|| { | |
| anyhow!( | |
| "Cargo couldn't find your home directory. \ | |
| This probably means that $HOME was not set." | |
| ) | |
| })?; | |
| Ok(GlobalContext::new(shell, cwd, homedir)) | |
| } | |
| /// Gets the user's Cargo home directory (OS-dependent). | |
| pub fn home(&self) -> &Filesystem { | |
| &self.home_path | |
| } | |
| /// Returns a path to display to the user with the location of their home | |
| /// config file (to only be used for displaying a diagnostics suggestion, | |
| /// such as recommending where to add a config value). | |
| pub fn diagnostic_home_config(&self) -> String { | |
| let home = self.home_path.as_path_unlocked(); | |
| let path = match self.get_file_path(home, "config", false) { | |
| Ok(Some(existing_path)) => existing_path, | |
| _ => home.join("config.toml"), | |
| }; | |
| path.to_string_lossy().to_string() | |
| } | |
| /// Gets the Cargo Git directory (`<cargo_home>/git`). | |
| pub fn git_path(&self) -> Filesystem { | |
| self.home_path.join("git") | |
| } | |
| /// Gets the directory of code sources Cargo checkouts from Git bare repos | |
| /// (`<cargo_home>/git/checkouts`). | |
| pub fn git_checkouts_path(&self) -> Filesystem { | |
| self.git_path().join("checkouts") | |
| } | |
| /// Gets the directory for all Git bare repos Cargo clones | |
| /// (`<cargo_home>/git/db`). | |
| pub fn git_db_path(&self) -> Filesystem { | |
| self.git_path().join("db") | |
| } | |
| /// Gets the Cargo base directory for all registry information (`<cargo_home>/registry`). | |
| pub fn registry_base_path(&self) -> Filesystem { | |
| self.home_path.join("registry") | |
| } | |
| /// Gets the Cargo registry index directory (`<cargo_home>/registry/index`). | |
| pub fn registry_index_path(&self) -> Filesystem { | |
| self.registry_base_path().join("index") | |
| } | |
| /// Gets the Cargo registry cache directory (`<cargo_home>/registry/cache`). | |
| pub fn registry_cache_path(&self) -> Filesystem { | |
| self.registry_base_path().join("cache") | |
| } | |
| /// Gets the Cargo registry source directory (`<cargo_home>/registry/src`). | |
| pub fn registry_source_path(&self) -> Filesystem { | |
| self.registry_base_path().join("src") | |
| } | |
| /// Gets the default Cargo registry. | |
| pub fn default_registry(&self) -> CargoResult<Option<String>> { | |
| Ok(self | |
| .get_string(["registry", "default"])? | |
| .map(|registry| registry.val)) | |
| } | |
| /// Gets a reference to the shell, e.g., for writing error messages. | |
| pub fn shell(&self) -> MutexGuard<'_, Shell> { | |
| self.shell.lock().unwrap() | |
| } | |
| /// Assert [`Self::shell`] is not in use | |
| /// | |
| /// Testing might not identify bugs with two accesses to `shell` at once | |
| /// due to conditional logic, | |
| /// so place this outside of the conditions to catch these bugs in more situations. | |
| pub fn debug_assert_shell_not_borrowed(&self) { | |
| if cfg!(debug_assertions) { | |
| match self.shell.try_lock() { | |
| Ok(_) | Err(std::sync::TryLockError::Poisoned(_)) => (), | |
| Err(std::sync::TryLockError::WouldBlock) => panic!("shell is borrowed!"), | |
| } | |
| } | |
| } | |
| /// Gets the path to the `rustdoc` executable. | |
| pub fn rustdoc(&self) -> CargoResult<&Path> { | |
| self.rustdoc | |
| .try_borrow_with(|| Ok(self.get_tool(Tool::Rustdoc, &self.build_config()?.rustdoc))) | |
| .map(AsRef::as_ref) | |
| } | |
| /// Gets the path to the `rustc` executable. | |
| pub fn load_global_rustc(&self, ws: Option<&Workspace<'_>>) -> CargoResult<Rustc> { | |
| let cache_location = | |
| ws.map(|ws| ws.build_dir().join(".rustc_info.json").into_path_unlocked()); | |
| let wrapper = self.maybe_get_tool("rustc_wrapper", &self.build_config()?.rustc_wrapper); | |
| let rustc_workspace_wrapper = self.maybe_get_tool( | |
| "rustc_workspace_wrapper", | |
| &self.build_config()?.rustc_workspace_wrapper, | |
| ); | |
| Rustc::new( | |
| self.get_tool(Tool::Rustc, &self.build_config()?.rustc), | |
| wrapper, | |
| rustc_workspace_wrapper, | |
| &self | |
| .home() | |
| .join("bin") | |
| .join("rustc") | |
| .into_path_unlocked() | |
| .with_extension(env::consts::EXE_EXTENSION), | |
| if self.cache_rustc_info { | |
| cache_location | |
| } else { | |
| None | |
| }, | |
| self, | |
| ) | |
| } | |
| /// Gets the path to the `cargo` executable. | |
| pub fn cargo_exe(&self) -> CargoResult<&Path> { | |
| self.cargo_exe | |
| .try_borrow_with(|| { | |
| let from_env = || -> CargoResult<PathBuf> { | |
| // Try re-using the `cargo` set in the environment already. This allows | |
| // commands that use Cargo as a library to inherit (via `cargo <subcommand>`) | |
| // or set (by setting `$CARGO`) a correct path to `cargo` when the current exe | |
| // is not actually cargo (e.g., `cargo-*` binaries, Valgrind, `ld.so`, etc.). | |
| let exe = self | |
| .get_env_os(crate::CARGO_ENV) | |
| .map(PathBuf::from) | |
| .ok_or_else(|| anyhow!("$CARGO not set"))?; | |
| Ok(exe) | |
| }; | |
| fn from_current_exe() -> CargoResult<PathBuf> { | |
| // Try fetching the path to `cargo` using `env::current_exe()`. | |
| // The method varies per operating system and might fail; in particular, | |
| // it depends on `/proc` being mounted on Linux, and some environments | |
| // (like containers or chroots) may not have that available. | |
| let exe = env::current_exe()?; | |
| Ok(exe) | |
| } | |
| fn from_argv() -> CargoResult<PathBuf> { | |
| // Grab `argv[0]` and attempt to resolve it to an absolute path. | |
| // If `argv[0]` has one component, it must have come from a `PATH` lookup, | |
| // so probe `PATH` in that case. | |
| // Otherwise, it has multiple components and is either: | |
| // - a relative path (e.g., `./cargo`, `target/debug/cargo`), or | |
| // - an absolute path (e.g., `/usr/local/bin/cargo`). | |
| let argv0 = env::args_os() | |
| .map(PathBuf::from) | |
| .next() | |
| .ok_or_else(|| anyhow!("no argv[0]"))?; | |
| paths::resolve_executable(&argv0) | |
| } | |
| // Determines whether `path` is a cargo binary. | |
| // See: https://github.com/rust-lang/cargo/issues/15099#issuecomment-2666737150 | |
| fn is_cargo(path: &Path) -> bool { | |
| path.file_stem() == Some(OsStr::new("cargo")) | |
| } | |
| let from_current_exe = from_current_exe(); | |
| if from_current_exe.as_deref().is_ok_and(is_cargo) { | |
| return from_current_exe; | |
| } | |
| let from_argv = from_argv(); | |
| if from_argv.as_deref().is_ok_and(is_cargo) { | |
| return from_argv; | |
| } | |
| let exe = from_env() | |
| .or(from_current_exe) | |
| .or(from_argv) | |
| .context("couldn't get the path to cargo executable")?; | |
| Ok(exe) | |
| }) | |
| .map(AsRef::as_ref) | |
| } | |
| /// Which package sources have been updated, used to ensure it is only done once. | |
| pub fn updated_sources(&self) -> MutexGuard<'_, HashSet<SourceId>> { | |
| self.updated_sources.lock().unwrap() | |
| } | |
| /// Cached credentials from credential providers or configuration. | |
| pub fn credential_cache(&self) -> MutexGuard<'_, HashMap<CanonicalUrl, CredentialCacheValue>> { | |
| self.credential_cache.lock().unwrap() | |
| } | |
| /// Gets the configuration for a remote registry. | |
| /// | |
| /// The registry name is resolved from its index URL. This may emit a note if | |
| /// the resolved name differs from the source's configured name. Returns | |
| /// `None` if no configuration is found. | |
| /// | |
| /// **Note:** this does not load tokens from `credentials.toml`, see instead | |
| /// [`Self::registry_credentials`]. | |
| pub fn registry_config(&self, sid: &SourceId) -> CargoResult<Option<RegistryConfig>> { | |
| let mut cache = self.registry_config.lock().unwrap(); | |
| if let Some(cfg) = cache.get(sid) { | |
| return Ok(cfg.clone()); | |
| } | |
| let cfg = registry_config_uncached(self, sid)?; | |
| cache.insert(*sid, cfg.clone()); | |
| Ok(cfg) | |
| } | |
| /// Gets the credentials configured for a remote registry. | |
| /// | |
| /// This loads credentials if needed and resolves the registry name in the | |
| /// same way as [`GlobalContext::registry_config`]. | |
| pub fn registry_credentials(&self, sid: &SourceId) -> CargoResult<Option<RegistryCredential>> { | |
| let credentials = self.credentials_config()?; | |
| let mut key = if sid.is_crates_io() { | |
| ConfigKey::from_parts(["registry"]) | |
| } else if let Some(name) = registry_name(self, sid)? { | |
| ConfigKey::from_parts(["registries", &name]) | |
| } else { | |
| return Ok(None); | |
| }; | |
| let has_credentials = credentials.get_cv(&key)?.is_some(); | |
| key.push("token"); | |
| let credentials_token = match credentials.get_cv(&key)? { | |
| Some(CV::String(val, definition)) => Some(Value { | |
| val: Secret::from(val), | |
| definition, | |
| }), | |
| Some(cv) => return Err(ConfigError::expected(&key, "a string", &cv).into()), | |
| None => None, | |
| }; | |
| let config = self.registry_config(sid)?; | |
| let has_config = config.is_some(); | |
| let config_token = config.and_then(|config| { | |
| #[expect(deprecated)] | |
| config.token | |
| }); | |
| let token = match (config_token, credentials_token) { | |
| (Some(config_token), Some(credentials_token)) | |
| if config_token | |
| .definition | |
| .is_higher_priority(&credentials_token.definition) => | |
| { | |
| Some(config_token) | |
| } | |
| (_, Some(credentials_token)) => Some(credentials_token), | |
| (config_token, None) => config_token, | |
| }; | |
| Ok((has_config || has_credentials).then_some(RegistryCredential { token })) | |
| } | |
| /// Gets all config values from disk. | |
| /// | |
| /// This will lazy-load the values as necessary. Callers are responsible | |
| /// for checking environment variables. Callers outside of the `config` | |
| /// module should avoid using this. | |
| pub fn config(&self) -> CargoResult<&ConfigStore> { | |
| self.config.try_borrow_with(|| self.load_config()) | |
| } | |
| /// Gets a mutable copy of the on-disk config values. | |
| /// | |
| /// This requires the config values to already have been loaded. This | |
| /// currently only exists for `cargo vendor` to remove the `source` | |
| /// entries. This doesn't respect environment variables. You should avoid | |
| /// using this if possible. | |
| pub fn config_mut(&mut self) -> CargoResult<&mut ConfigStore> { | |
| let _ = self.config()?; | |
| Ok(self.config.get_mut().expect("already loaded config values")) | |
| } | |
| /// Sets the path where ancestor config file searching will stop. The | |
| /// given path is included, but its ancestors are not. | |
| pub fn set_search_stop_path<P: Into<PathBuf>>(&mut self, path: P) { | |
| let path = path.into(); | |
| debug_assert!(self.cwd.starts_with(&path)); | |
| self.search_stop_path = Some(path); | |
| } | |
| /// Switches the working directory to [`std::env::current_dir`] | |
| /// | |
| /// There is not a need to also call [`Self::reload_rooted_at`]. | |
| pub fn reload_cwd(&mut self) -> CargoResult<()> { | |
| let cwd = | |
| env::current_dir().context("couldn't get the current directory of the process")?; | |
| let homedir = homedir(&cwd).ok_or_else(|| { | |
| anyhow!( | |
| "Cargo couldn't find your home directory. \ | |
| This probably means that $HOME was not set." | |
| ) | |
| })?; | |
| self.cwd = cwd; | |
| self.home_path = Filesystem::new(homedir); | |
| self.reload_rooted_at(self.cwd.clone())?; | |
| Ok(()) | |
| } | |
| /// Reloads on-disk configuration values, starting at the given path and | |
| /// walking up its ancestors. | |
| pub fn reload_rooted_at<P: AsRef<Path>>(&mut self, path: P) -> CargoResult<()> { | |
| let config = self.load_config_from(path.as_ref())?; | |
| self.config.replace(config); | |
| self.merge_cli_args()?; | |
| self.load_unstable_flags_from_config()?; | |
| Ok(()) | |
| } | |
| /// The current working directory. | |
| pub fn cwd(&self) -> &Path { | |
| &self.cwd | |
| } | |
| /// The `target` output directory to use. | |
| /// | |
| /// Returns `None` if the user has not chosen an explicit directory. | |
| /// | |
| /// Callers should prefer [`Workspace::target_dir`] instead. | |
| pub fn target_dir(&self) -> CargoResult<Option<Filesystem>> { | |
| if let Some(dir) = &self.target_dir { | |
| Ok(Some(dir.clone())) | |
| } else if let Some(dir) = self.get_env_os("CARGO_TARGET_DIR") { | |
| // Check if the CARGO_TARGET_DIR environment variable is set to an empty string. | |
| if dir.is_empty() { | |
| bail!( | |
| "the target directory is set to an empty string in the \ | |
| `CARGO_TARGET_DIR` environment variable" | |
| ) | |
| } | |
| Ok(Some(Filesystem::new(self.cwd.join(dir)))) | |
| } else if let Some(val) = &self.build_config()?.target_dir { | |
| let path = val.resolve_path(self); | |
| // Check if the target directory is set to an empty string in the config.toml file. | |
| if val.raw_value().is_empty() { | |
| bail!( | |
| "the target directory is set to an empty string in {}", | |
| val.value().definition | |
| ) | |
| } | |
| Ok(Some(Filesystem::new(path))) | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| /// The directory to use for intermediate build artifacts. | |
| /// | |
| /// Callers should prefer [`Workspace::build_dir`] instead. | |
| pub fn build_dir(&self, workspace_manifest_path: &Path) -> CargoResult<Option<Filesystem>> { | |
| let Some(val) = &self.build_config()?.build_dir else { | |
| return Ok(None); | |
| }; | |
| self.custom_build_dir(val, workspace_manifest_path) | |
| .map(Some) | |
| } | |
| /// The directory to use for intermediate build artifacts. | |
| /// | |
| /// Callers should prefer [`Workspace::build_dir`] instead. | |
| pub fn custom_build_dir( | |
| &self, | |
| val: &ConfigRelativePath, | |
| workspace_manifest_path: &Path, | |
| ) -> CargoResult<Filesystem> { | |
| let replacements = [ | |
| ( | |
| "{workspace-root}", | |
| workspace_manifest_path | |
| .parent() | |
| .unwrap() | |
| .to_str() | |
| .context("workspace root was not valid utf-8")? | |
| .to_string(), | |
| ), | |
| ( | |
| "{cargo-cache-home}", | |
| self.home() | |
| .as_path_unlocked() | |
| .to_str() | |
| .context("cargo home was not valid utf-8")? | |
| .to_string(), | |
| ), | |
| ("{workspace-path-hash}", { | |
| let real_path = std::fs::canonicalize(workspace_manifest_path) | |
| .unwrap_or_else(|_err| workspace_manifest_path.to_owned()); | |
| let hash = crate::util::hex::short_hash(&real_path); | |
| format!("{}{}{}", &hash[0..2], std::path::MAIN_SEPARATOR, &hash[2..]) | |
| }), | |
| ]; | |
| let template_variables = replacements | |
| .iter() | |
| .map(|(key, _)| key[1..key.len() - 1].to_string()) | |
| .collect_vec(); | |
| let path = val | |
| .resolve_templated_path(self, replacements) | |
| .map_err(|e| match e { | |
| path::ResolveTemplateError::UnexpectedVariable { | |
| variable, | |
| raw_template, | |
| } => { | |
| let mut suggestion = closest_msg(&variable, template_variables.iter(), |key| key, "template variable"); | |
| if suggestion == "" { | |
| let variables = template_variables.iter().map(|v| format!("`{{{v}}}`")).join(", "); | |
| suggestion = format!("\n\nhelp: available template variables are {variables}"); | |
| } | |
| anyhow!( | |
| "unexpected variable `{variable}` in build.build-dir path `{raw_template}`{suggestion}" | |
| ) | |
| } | |
| path::ResolveTemplateError::UnexpectedBracket { bracket_type, raw_template } => { | |
| let (btype, literal) = match bracket_type { | |
| path::BracketType::Opening => ("opening", "{"), | |
| path::BracketType::Closing => ("closing", "}"), | |
| }; | |
| anyhow!( | |
| "unexpected {btype} bracket `{literal}` in build.build-dir path `{raw_template}`" | |
| ) | |
| } | |
| })?; | |
| // Check if the target directory is set to an empty string in the config.toml file. | |
| if val.raw_value().is_empty() { | |
| bail!( | |
| "the build directory is set to an empty string in {}", | |
| val.value().definition | |
| ) | |
| } | |
| Ok(Filesystem::new(path)) | |
| } | |
| /// This is a helper for getting a CV from a file or env var. | |
| pub(crate) fn get_cv_with_env(&self, key: &ConfigKey) -> CargoResult<Option<CV>> { | |
| // Determine if value comes from env, cli, or file, and merge env if | |
| // possible. | |
| let cv = self.config()?.get_cv(key)?; | |
| if key.is_root() { | |
| // Root table can't have env value. | |
| return Ok(cv); | |
| } | |
| let env = self.env.get_str(key.as_env_key()); | |
| let env_def = Definition::Environment(key.as_env_key().to_string()); | |
| let use_env = match (&cv, env) { | |
| // Lists are always merged. | |
| (Some(CV::List(..)), Some(_)) => true, | |
| (Some(cv), Some(_)) => env_def.is_higher_priority(cv.definition()), | |
| (None, Some(_)) => true, | |
| _ => false, | |
| }; | |
| if !use_env { | |
| return Ok(cv); | |
| } | |
| // Future note: If you ever need to deserialize a non-self describing | |
| // map type, this should implement a starts_with check (similar to how | |
| // ConfigMapAccess does). | |
| let env = env.unwrap(); | |
| if env == "true" { | |
| Ok(Some(CV::Boolean(true, env_def))) | |
| } else if env == "false" { | |
| Ok(Some(CV::Boolean(false, env_def))) | |
| } else if let Ok(i) = env.parse::<i64>() { | |
| Ok(Some(CV::Integer(i, env_def))) | |
| } else if self.cli_unstable().advanced_env && env.starts_with('[') && env.ends_with(']') { | |
| match cv { | |
| Some(CV::List(mut cv_list, cv_def)) => { | |
| // Merge with config file. | |
| self.get_env_list(key, &mut cv_list)?; | |
| Ok(Some(CV::List(cv_list, cv_def))) | |
| } | |
| Some(cv) => { | |
| // This can't assume StringList. | |
| // Return an error, which is the behavior of merging | |
| // multiple config.toml files with the same scenario. | |
| bail!( | |
| "unable to merge array env for config `{}`\n\ | |
| file: {:?}\n\ | |
| env: {}", | |
| key, | |
| cv, | |
| env | |
| ); | |
| } | |
| None => { | |
| let mut cv_list = Vec::new(); | |
| self.get_env_list(key, &mut cv_list)?; | |
| Ok(Some(CV::List(cv_list, env_def))) | |
| } | |
| } | |
| } else { | |
| // Try to merge if possible. | |
| match cv { | |
| Some(CV::List(mut cv_list, cv_def)) => { | |
| // Merge with config file. | |
| self.get_env_list(key, &mut cv_list)?; | |
| Ok(Some(CV::List(cv_list, cv_def))) | |
| } | |
| _ => { | |
| // Note: CV::Table merging is not implemented, as env | |
| // vars do not support table values. In the future, we | |
| // could check for `{}`, and interpret it as TOML if | |
| // that seems useful. | |
| Ok(Some(CV::String(env.to_string(), env_def))) | |
| } | |
| } | |
| } | |
| } | |
| /// Helper primarily for testing. | |
| pub fn set_env(&mut self, env: HashMap<String, String>) { | |
| self.env = Env::from_map(env); | |
| } | |
| /// Returns all environment variables as an iterator, | |
| /// keeping only entries where both the key and value are valid UTF-8. | |
| pub(crate) fn env(&self) -> impl Iterator<Item = (&str, &str)> { | |
| self.env.iter_str() | |
| } | |
| /// Returns all environment variable keys, filtering out keys that are not valid UTF-8. | |
| fn env_keys(&self) -> impl Iterator<Item = &str> { | |
| self.env.keys_str() | |
| } | |
| fn get_config_env<T>(&self, key: &ConfigKey) -> Result<OptValue<T>, ConfigError> | |
| where | |
| T: FromStr, | |
| <T as FromStr>::Err: fmt::Display, | |
| { | |
| match self.env.get_str(key.as_env_key()) { | |
| Some(value) => { | |
| let definition = Definition::Environment(key.as_env_key().to_string()); | |
| Ok(Some(Value { | |
| val: value | |
| .parse() | |
| .map_err(|e| ConfigError::new(format!("{}", e), definition.clone()))?, | |
| definition, | |
| })) | |
| } | |
| None => { | |
| self.check_environment_key_case_mismatch(key); | |
| Ok(None) | |
| } | |
| } | |
| } | |
| /// Get the value of environment variable `key` through the snapshot in | |
| /// [`GlobalContext`]. | |
| /// | |
| /// This can be used similarly to [`std::env::var`]. | |
| pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<&str> { | |
| self.env.get_env(key) | |
| } | |
| /// Get the value of environment variable `key` through the snapshot in | |
| /// [`GlobalContext`]. | |
| /// | |
| /// This can be used similarly to [`std::env::var_os`]. | |
| pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> { | |
| self.env.get_env_os(key) | |
| } | |
| /// Check if the [`GlobalContext`] contains a given [`ConfigKey`]. | |
| /// | |
| /// See `ConfigMapAccess` for a description of `env_prefix_ok`. | |
| fn has_key(&self, key: &ConfigKey, env_prefix_ok: bool) -> CargoResult<bool> { | |
| if self.env.contains_key(key.as_env_key()) { | |
| return Ok(true); | |
| } | |
| if env_prefix_ok { | |
| let env_prefix = format!("{}_", key.as_env_key()); | |
| if self.env_keys().any(|k| k.starts_with(&env_prefix)) { | |
| return Ok(true); | |
| } | |
| } | |
| if self.config()?.get_cv(key)?.is_some() { | |
| return Ok(true); | |
| } | |
| self.check_environment_key_case_mismatch(key); | |
| Ok(false) | |
| } | |
| fn check_environment_key_case_mismatch(&self, key: &ConfigKey) { | |
| if let Some(env_key) = self.env.get_normalized(key.as_env_key()) { | |
| let _ = self.shell().warn(format!( | |
| "environment variables are expected to use uppercase letters and underscores, \ | |
| the variable `{}` will be ignored and have no effect", | |
| env_key | |
| )); | |
| } | |
| } | |
| /// Get a string config value. | |
| /// | |
| /// See `get` for more details. | |
| pub fn get_string( | |
| &self, | |
| key: impl IntoIterator<Item = impl AsRef<str>>, | |
| ) -> CargoResult<OptValue<String>> { | |
| self.get::<OptValue<String>>(key) | |
| } | |
| fn string_to_path(&self, value: &str, definition: &Definition) -> PathBuf { | |
| let is_path = value.contains('/') || (cfg!(windows) && value.contains('\\')); | |
| if is_path { | |
| definition.root(self.cwd()).join(value) | |
| } else { | |
| // A pathless name. | |
| PathBuf::from(value) | |
| } | |
| } | |
| /// Internal method for getting an environment variable as a list. | |
| /// If the key is a non-mergeable list and a value is found in the environment, existing values are cleared. | |
| fn get_env_list(&self, key: &ConfigKey, output: &mut Vec<ConfigValue>) -> CargoResult<()> { | |
| let Some(env_val) = self.env.get_str(key.as_env_key()) else { | |
| self.check_environment_key_case_mismatch(key); | |
| return Ok(()); | |
| }; | |
| let env_def = Definition::Environment(key.as_env_key().to_string()); | |
| if is_nonmergeable_list(&key) { | |
| assert!( | |
| output | |
| .windows(2) | |
| .all(|cvs| cvs[0].definition() == cvs[1].definition()), | |
| "non-mergeable list must have only one definition: {output:?}", | |
| ); | |
| // Keep existing config if higher priority than env (e.g., --config CLI), | |
| // otherwise clear for env | |
| if output | |
| .first() | |
| .map(|o| o.definition() > &env_def) | |
| .unwrap_or_default() | |
| { | |
| return Ok(()); | |
| } else { | |
| output.clear(); | |
| } | |
| } | |
| if self.cli_unstable().advanced_env && env_val.starts_with('[') && env_val.ends_with(']') { | |
| // Parse an environment string as a TOML array. | |
| let toml_v = env_val.parse::<toml::Value>().map_err(|e| { | |
| ConfigError::new(format!("could not parse TOML list: {}", e), env_def.clone()) | |
| })?; | |
| let values = toml_v.as_array().expect("env var was not array"); | |
| for value in values { | |
| // Until we figure out how to deal with it through `-Zadvanced-env`, | |
| // complex array types are unsupported. | |
| let s = value.as_str().ok_or_else(|| { | |
| ConfigError::new( | |
| format!("expected string, found {}", value.type_str()), | |
| env_def.clone(), | |
| ) | |
| })?; | |
| output.push(CV::String(s.to_string(), env_def.clone())) | |
| } | |
| } else { | |
| output.extend( | |
| env_val | |
| .split_whitespace() | |
| .map(|s| CV::String(s.to_string(), env_def.clone())), | |
| ); | |
| } | |
| output.sort_by(|a, b| a.definition().cmp(b.definition())); | |
| Ok(()) | |
| } | |
| /// Low-level method for getting a config value as an `OptValue<HashMap<String, CV>>`. | |
| /// | |
| /// NOTE: This does not read from env. The caller is responsible for that. | |
| fn get_table(&self, key: &ConfigKey) -> CargoResult<OptValue<HashMap<String, CV>>> { | |
| match self.config()?.get_cv(key)? { | |
| Some(CV::Table(val, definition)) => Ok(Some(Value { val, definition })), | |
| Some(val) => self.expected("table", key, &val), | |
| None => Ok(None), | |
| } | |
| } | |
| get_value_typed! {get_integer, i64, Integer, "an integer"} | |
| get_value_typed! {get_bool, bool, Boolean, "true/false"} | |
| get_value_typed! {get_string_priv, String, String, "a string"} | |
| /// Generate an error when the given value is the wrong type. | |
| fn expected<T>(&self, ty: &str, key: &ConfigKey, val: &CV) -> CargoResult<T> { | |
| val.expected(ty, &key.to_string()) | |
| .map_err(|e| anyhow!("invalid configuration for key `{}`\n{}", key, e)) | |
| } | |
| /// Update the instance based on settings typically passed in on | |
| /// the command-line. | |
| /// | |
| /// This may also load the config from disk if it hasn't already been | |
| /// loaded. | |
| pub fn configure( | |
| &mut self, | |
| verbose: u32, | |
| quiet: bool, | |
| color: Option<&str>, | |
| frozen: bool, | |
| locked: bool, | |
| offline: bool, | |
| target_dir: &Option<PathBuf>, | |
| unstable_flags: &[String], | |
| cli_config: &[String], | |
| ) -> CargoResult<()> { | |
| for warning in self | |
| .unstable_flags | |
| .parse(unstable_flags, self.nightly_features_allowed)? | |
| { | |
| self.shell().warn(warning)?; | |
| } | |
| if !unstable_flags.is_empty() { | |
| // store a copy of the cli flags separately for `load_unstable_flags_from_config` | |
| // (we might also need it again for `reload_rooted_at`) | |
| self.unstable_flags_cli = Some(unstable_flags.to_vec()); | |
| } | |
| if !cli_config.is_empty() { | |
| self.cli_config = Some(cli_config.iter().map(|s| s.to_string()).collect()); | |
| self.merge_cli_args()?; | |
| } | |
| self.load_unstable_flags_from_config()?; | |
| // Ignore errors in the configuration files. We don't want basic | |
| // commands like `cargo version` to error out due to config file | |
| // problems. | |
| let term = self.get::<TermConfig>(["term"]).unwrap_or_default(); | |
| // The command line takes precedence over configuration. | |
| let extra_verbose = verbose >= 2; | |
| let verbose = verbose != 0; | |
| let verbosity = match (verbose, quiet) { | |
| (true, true) => bail!("cannot set both --verbose and --quiet"), | |
| (true, false) => Verbosity::Verbose, | |
| (false, true) => Verbosity::Quiet, | |
| (false, false) => match (term.verbose, term.quiet) { | |
| (Some(true), Some(true)) => { | |
| bail!("cannot set both `term.verbose` and `term.quiet`") | |
| } | |
| (Some(true), _) => Verbosity::Verbose, | |
| (_, Some(true)) => Verbosity::Quiet, | |
| _ => Verbosity::Normal, | |
| }, | |
| }; | |
| self.shell().set_verbosity(verbosity); | |
| self.extra_verbose = extra_verbose; | |
| let color = color.or_else(|| term.color.as_deref()); | |
| self.shell().set_color_choice(color)?; | |
| if let Some(hyperlinks) = term.hyperlinks { | |
| self.shell().set_hyperlinks(hyperlinks)?; | |
| } | |
| if let Some(unicode) = term.unicode { | |
| self.shell().set_unicode(unicode)?; | |
| } | |
| self.progress_config = term.progress.unwrap_or_default(); | |
| self.frozen = frozen; | |
| self.locked = locked; | |
| self.offline = offline | |
| || self | |
| .net_config() | |
| .ok() | |
| .and_then(|n| n.offline) | |
| .unwrap_or(false); | |
| let cli_target_dir = target_dir.as_ref().map(|dir| Filesystem::new(dir.clone())); | |
| self.target_dir = cli_target_dir; | |
| self.shell() | |
| .set_unstable_flags_rustc_unicode(self.unstable_flags.rustc_unicode)?; | |
| Ok(()) | |
| } | |
| fn load_unstable_flags_from_config(&mut self) -> CargoResult<()> { | |
| // If nightly features are enabled, allow setting Z-flags from config | |
| // using the `unstable` table. Ignore that block otherwise. | |
| if self.nightly_features_allowed { | |
| self.unstable_flags = self | |
| .get::<Option<CliUnstable>>(["unstable"])? | |
| .unwrap_or_default(); | |
| if let Some(unstable_flags_cli) = &self.unstable_flags_cli { | |
| // NB. It's not ideal to parse these twice, but doing it again here | |
| // allows the CLI to override config files for both enabling | |
| // and disabling, and doing it up top allows CLI Zflags to | |
| // control config parsing behavior. | |
| self.unstable_flags.parse(unstable_flags_cli, true)?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| pub fn cli_unstable(&self) -> &CliUnstable { | |
| &self.unstable_flags | |
| } | |
| pub fn extra_verbose(&self) -> bool { | |
| self.extra_verbose | |
| } | |
| pub fn network_allowed(&self) -> bool { | |
| !self.offline_flag().is_some() | |
| } | |
| pub fn offline_flag(&self) -> Option<&'static str> { | |
| if self.frozen { | |
| Some("--frozen") | |
| } else if self.offline { | |
| Some("--offline") | |
| } else { | |
| None | |
| } | |
| } | |
| pub fn set_locked(&mut self, locked: bool) { | |
| self.locked = locked; | |
| } | |
| pub fn lock_update_allowed(&self) -> bool { | |
| !self.locked_flag().is_some() | |
| } | |
| pub fn locked_flag(&self) -> Option<&'static str> { | |
| if self.frozen { | |
| Some("--frozen") | |
| } else if self.locked { | |
| Some("--locked") | |
| } else { | |
| None | |
| } | |
| } | |
| /// Loads configuration from the filesystem. | |
| pub fn load_config(&self) -> CargoResult<ConfigStore> { | |
| self.load_config_from(&self.cwd) | |
| } | |
| /// Like [`load_config`](GlobalContext::load_config) but without merging config values. | |
| /// | |
| /// This is primarily crafted for `cargo config` command. | |
| pub(crate) fn load_config_unmerged(&self) -> CargoResult<Vec<ConfigStore>> { | |
| let mut result = Vec::new(); | |
| let mut seen = HashSet::default(); | |
| let home = self.home_path.clone().into_path_unlocked(); | |
| self.walk_tree(&self.cwd, &home, |path| { | |
| let mut config = self._load_file(path, &mut seen, false, DefinitionKind::Path)?; | |
| self.load_unmerged_include(&mut config, &mut seen, &mut result)?; | |
| result.push(config); | |
| Ok(()) | |
| }) | |
| .context("could not load Cargo configuration")?; | |
| Ok(result) | |
| } | |
| /// Like [`load_includes`](GlobalContext::load_includes) but without merging config values. | |
| /// | |
| /// This is primarily crafted for `cargo config` command. | |
| fn load_unmerged_include( | |
| &self, | |
| config: &mut ConfigStore, | |
| seen: &mut HashSet<PathBuf>, | |
| output: &mut Vec<ConfigStore>, | |
| ) -> CargoResult<()> { | |
| let includes = self.include_paths(config, false)?; | |
| for include in includes { | |
| let Some(abs_path) = include.resolve_path(self) else { | |
| continue; | |
| }; | |
| let mut config = self | |
| ._load_file(&abs_path, seen, false, DefinitionKind::Path) | |
| .with_context(|| { | |
| format!( | |
| "failed to load config include `{}` from `{}`", | |
| include.path.display(), | |
| include.def | |
| ) | |
| })?; | |
| self.load_unmerged_include(&mut config, seen, output)?; | |
| output.push(config); | |
| } | |
| Ok(()) | |
| } | |
| /// Start a config file discovery from a path and merges all config values found. | |
| fn load_config_from(&self, path: &Path) -> CargoResult<ConfigStore> { | |
| let mut cfg = ConfigStore::default(); | |
| let home = self.home_path.clone().into_path_unlocked(); | |
| self.walk_tree(path, &home, |path| { | |
| let config = self.load_file(path)?; | |
| cfg.merge(config, false).with_context(|| { | |
| format!("failed to merge configuration at `{}`", path.display()) | |
| })?; | |
| Ok(()) | |
| }) | |
| .context("could not load Cargo configuration")?; | |
| Ok(cfg) | |
| } | |
| /// Loads configuration from a path. | |
| /// | |
| /// This is used during config file discovery. | |
| fn load_file(&self, path: &Path) -> CargoResult<ConfigStore> { | |
| self._load_file(path, &mut HashSet::default(), true, DefinitionKind::Path) | |
| } | |
| /// Loads configuration from a path with options. | |
| /// | |
| /// This is the implementation of loading configuration from a path. | |
| /// | |
| /// * `includes` determines whether to load configs from [`ConfigInclude`]. | |
| /// * `seen` is used to check for cyclic includes. | |
| /// * `definition_kind` tells why a config is being loaded. | |
| fn _load_file( | |
| &self, | |
| path: &Path, | |
| seen: &mut HashSet<PathBuf>, | |
| includes: bool, | |
| definition_kind: DefinitionKind, | |
| ) -> CargoResult<ConfigStore> { | |
| if !seen.insert(path.to_path_buf()) { | |
| bail!( | |
| "config `include` cycle detected with path `{}`", | |
| path.display() | |
| ); | |
| } | |
| tracing::debug!(?path, ?definition_kind, includes, "load config from file"); | |
| let config = ConfigStore::read_layer(path, definition_kind)?; | |
| if includes { | |
| self.load_includes(config, seen, definition_kind) | |
| } else { | |
| Ok(config) | |
| } | |
| } | |
| /// Load any `include` files listed in the given `config`. | |
| /// | |
| /// Returns `config` with the given include files merged into it. | |
| /// | |
| /// * `seen` is used to check for cyclic includes. | |
| /// * `definition_kind` tells why a config is being loaded. | |
| fn load_includes( | |
| &self, | |
| mut config: ConfigStore, | |
| seen: &mut HashSet<PathBuf>, | |
| definition_kind: DefinitionKind, | |
| ) -> CargoResult<ConfigStore> { | |
| // Get the list of files to load. | |
| let includes = self.include_paths(&mut config, true)?; | |
| // Accumulate all values here. | |
| let root = CV::Table(HashMap::default(), config.definition().clone()); | |
| let mut root = ConfigStore::with_table_value(root).unwrap(); | |
| for include in includes { | |
| let Some(abs_path) = include.resolve_path(self) else { | |
| continue; | |
| }; | |
| self._load_file(&abs_path, seen, true, definition_kind) | |
| .and_then(|include| root.merge(include, true)) | |
| .with_context(|| { | |
| format!( | |
| "failed to load config include `{}` from `{}`", | |
| include.path.display(), | |
| include.def | |
| ) | |
| })?; | |
| } | |
| root.merge(config, true)?; | |
| Ok(root) | |
| } | |
| /// Converts the `include` config value to a list of absolute paths. | |
| fn include_paths( | |
| &self, | |
| config: &mut ConfigStore, | |
| remove: bool, | |
| ) -> CargoResult<Vec<ConfigInclude>> { | |
| let table = config.values_mut(); | |
| let include = if remove { | |
| table.remove("include").map(Cow::Owned) | |
| } else { | |
| table.get("include").map(Cow::Borrowed) | |
| }; | |
| let includes = match include.map(|c| c.into_owned()) { | |
| Some(CV::List(list, _def)) => list | |
| .into_iter() | |
| .enumerate() | |
| .map(|(idx, cv)| match cv { | |
| CV::String(s, def) => Ok(ConfigInclude::new(s, def)), | |
| CV::Table(mut table, def) => { | |
| // Extract `include.path` | |
| let s = match table.remove("path") { | |
| Some(CV::String(s, _)) => s, | |
| Some(other) => bail!( | |
| "expected a string, but found {} at `include[{idx}].path` in `{def}`", | |
| other.desc() | |
| ), | |
| None => bail!("missing field `path` at `include[{idx}]` in `{def}`"), | |
| }; | |
| // Extract optional `include.optional` field | |
| let optional = match table.remove("optional") { | |
| Some(CV::Boolean(b, _)) => b, | |
| Some(other) => bail!( | |
| "expected a boolean, but found {} at `include[{idx}].optional` in `{def}`", | |
| other.desc() | |
| ), | |
| None => false, | |
| }; | |
| let mut include = ConfigInclude::new(s, def); | |
| include.optional = optional; | |
| Ok(include) | |
| } | |
| other => bail!( | |
| "expected a string or table, but found {} at `include[{idx}]` in {}", | |
| other.desc(), | |
| other.definition(), | |
| ), | |
| }) | |
| .collect::<CargoResult<Vec<_>>>()?, | |
| Some(other) => bail!( | |
| "expected a list of strings or a list of tables, but found {} at `include` in `{}", | |
| other.desc(), | |
| other.definition() | |
| ), | |
| None => { | |
| return Ok(Vec::new()); | |
| } | |
| }; | |
| for include in &includes { | |
| if include.path.extension() != Some(OsStr::new("toml")) { | |
| bail!( | |
| "expected a config include path ending with `.toml`, \ | |
| but found `{}` from `{}`", | |
| include.path.display(), | |
| include.def, | |
| ) | |
| } | |
| if let Some(path) = include.path.to_str() { | |
| // Ignore non UTF-8 bytes as glob and template syntax are for textual config. | |
| if is_glob_pattern(path) { | |
| bail!( | |
| "expected a config include path without glob patterns, \ | |
| but found `{}` from `{}`", | |
| include.path.display(), | |
| include.def, | |
| ) | |
| } | |
| if path.contains(&['{', '}']) { | |
| bail!( | |
| "expected a config include path without template braces, \ | |
| but found `{}` from `{}`", | |
| include.path.display(), | |
| include.def, | |
| ) | |
| } | |
| } | |
| } | |
| Ok(includes) | |
| } | |
| /// Parses the CLI config args and returns them as a table. | |
| pub(crate) fn cli_args_to_config(&self) -> CargoResult<ConfigStore> { | |
| let mut loaded_args = | |
| ConfigStore::with_table_value(CV::Table(HashMap::default(), Definition::Cli(None))) | |
| .unwrap(); | |
| let Some(cli_args) = &self.cli_config else { | |
| return Ok(loaded_args); | |
| }; | |
| let mut seen = HashSet::default(); | |
| for arg in cli_args { | |
| let arg_as_path = self.cwd.join(arg); | |
| let arg_config = if !arg.is_empty() && arg_as_path.exists() { | |
| // --config path_to_file | |
| self._load_file(&arg_as_path, &mut seen, true, DefinitionKind::Cli) | |
| .with_context(|| { | |
| format!("failed to load config from `{}`", arg_as_path.display()) | |
| })? | |
| } else { | |
| let doc = toml_dotted_keys(arg)?; | |
| let doc: toml::Value = toml::Value::deserialize(doc.into_deserializer()) | |
| .with_context(|| { | |
| format!("failed to parse value from --config argument `{arg}`") | |
| })?; | |
| if doc | |
| .get("registry") | |
| .and_then(|v| v.as_table()) | |
| .and_then(|t| t.get("token")) | |
| .is_some() | |
| { | |
| bail!("registry.token cannot be set through --config for security reasons"); | |
| } else if let Some((k, _)) = doc | |
| .get("registries") | |
| .and_then(|v| v.as_table()) | |
| .and_then(|t| t.iter().find(|(_, v)| v.get("token").is_some())) | |
| { | |
| bail!( | |
| "registries.{}.token cannot be set through --config for security reasons", | |
| k | |
| ); | |
| } | |
| let table = CV::from_toml(Definition::Cli(None), doc) | |
| .with_context(|| format!("failed to convert --config argument `{arg}`"))?; | |
| ConfigStore::with_table_value(table).unwrap() | |
| }; | |
| let arg_config = self | |
| .load_includes(arg_config, &mut HashSet::default(), DefinitionKind::Cli) | |
| .context("failed to load --config include".to_string())?; | |
| loaded_args | |
| .merge(arg_config, true) | |
| .with_context(|| format!("failed to merge --config argument `{arg}`"))?; | |
| } | |
| Ok(loaded_args) | |
| } | |
| /// Add config arguments passed on the command line. | |
| fn merge_cli_args(&mut self) -> CargoResult<()> { | |
| let cli_config = self.cli_args_to_config()?; | |
| self.config_mut()?.merge(cli_config, true) | |
| } | |
| /// The purpose of this function is to aid in the transition to using | |
| /// .toml extensions on Cargo's config files, which were historically not used. | |
| /// Both 'config.toml' and 'credentials.toml' should be valid with or without extension. | |
| /// When both exist, we want to prefer the one without an extension for | |
| /// backwards compatibility, but warn the user appropriately. | |
| fn get_file_path( | |
| &self, | |
| dir: &Path, | |
| filename_without_extension: &str, | |
| warn: bool, | |
| ) -> CargoResult<Option<PathBuf>> { | |
| let possible = dir.join(filename_without_extension); | |
| let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension)); | |
| if let Ok(possible_handle) = same_file::Handle::from_path(&possible) { | |
| if warn { | |
| if let Ok(possible_with_extension_handle) = | |
| same_file::Handle::from_path(&possible_with_extension) | |
| { | |
| // We don't want to print a warning if the version | |
| // without the extension is just a symlink to the version | |
| // WITH an extension, which people may want to do to | |
| // support multiple Cargo versions at once and not | |
| // get a warning. | |
| if possible_handle != possible_with_extension_handle { | |
| self.shell().warn(format!( | |
| "both `{}` and `{}` exist. Using `{}`", | |
| possible.display(), | |
| possible_with_extension.display(), | |
| possible.display() | |
| ))?; | |
| } | |
| } else { | |
| self.shell().print_report(&[ | |
| Level::WARNING.secondary_title( | |
| format!( | |
| "`{}` is deprecated in favor of `{filename_without_extension}.toml`", | |
| possible.display(), | |
| )).element(Level::HELP.message( | |
| format!("if you need to support cargo 1.38 or earlier, you can symlink `{filename_without_extension}` to `{filename_without_extension}.toml`"))) | |
| ], false)?; | |
| } | |
| } | |
| Ok(Some(possible)) | |
| } else if possible_with_extension.exists() { | |
| Ok(Some(possible_with_extension)) | |
| } else { | |
| Ok(None) | |
| } | |
| } | |
| fn walk_tree<F>(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()> | |
| where | |
| F: FnMut(&Path) -> CargoResult<()>, | |
| { | |
| let mut seen_dir = HashSet::default(); | |
| for current in paths::ancestors(pwd, self.search_stop_path.as_deref()) { | |
| let config_root = current.join(".cargo"); | |
| if let Some(path) = self.get_file_path(&config_root, "config", true)? { | |
| walk(&path)?; | |
| } | |
| let canonical_root = config_root.canonicalize().unwrap_or(config_root); | |
| seen_dir.insert(canonical_root); | |
| } | |
| let canonical_home = home.canonicalize().unwrap_or(home.to_path_buf()); | |
| // Once we're done, also be sure to walk the home directory even if it's not | |
| // in our history to be sure we pick up that standard location for | |
| // information. | |
| if !seen_dir.contains(&canonical_home) && !seen_dir.contains(home) { | |
| if let Some(path) = self.get_file_path(home, "config", true)? { | |
| walk(&path)?; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| /// Gets the index for a registry. | |
| pub fn get_registry_index(&self, registry: &str) -> CargoResult<Url> { | |
| RegistryName::new(registry)?; | |
| if let Some(index) = self.get_string(["registries", registry, "index"])? { | |
| self.resolve_registry_index(&index).with_context(|| { | |
| format!( | |
| "invalid index URL for registry `{}` defined in {}", | |
| registry, index.definition | |
| ) | |
| }) | |
| } else { | |
| bail!( | |
| "registry index was not found in any configuration: `{}`", | |
| registry | |
| ); | |
| } | |
| } | |
| /// Returns an error if `registry.index` is set. | |
| pub fn check_registry_index_not_set(&self) -> CargoResult<()> { | |
| if self.get_string(["registry", "index"])?.is_some() { | |
| bail!( | |
| "the `registry.index` config value is no longer supported\n\ | |
| Use `[source]` replacement to alter the default index for crates.io." | |
| ); | |
| } | |
| Ok(()) | |
| } | |
| fn resolve_registry_index(&self, index: &Value<String>) -> CargoResult<Url> { | |
| // This handles relative file: URLs, relative to the config definition. | |
| let base = index | |
| .definition | |
| .root(self.cwd()) | |
| .join("truncated-by-url_with_base"); | |
| // Parse val to check it is a URL, not a relative path without a protocol. | |
| let _parsed = index.val.into_url()?; | |
| let url = index.val.into_url_with_base(Some(&*base))?; | |
| if url.password().is_some() { | |
| bail!("registry URLs may not contain passwords"); | |
| } | |
| Ok(url) | |
| } | |
| /// Gets configuration values from the credentials file, loading them on first use. | |
| fn credentials_config(&self) -> CargoResult<&ConfigStore> { | |
| self.credentials_config | |
| .try_borrow_with(|| self.load_credentials()) | |
| } | |
| /// Loads credentials config from the credentials file, if present. | |
| fn load_credentials(&self) -> CargoResult<ConfigStore> { | |
| let home_path = self.home_path.clone().into_path_unlocked(); | |
| let Some(credentials) = self.get_file_path(&home_path, "credentials", true)? else { | |
| return Ok(ConfigStore::default()); | |
| }; | |
| let mut config = ConfigStore::read_layer(&credentials, DefinitionKind::Path)?; | |
| // Backwards compatibility for old `.cargo/credentials` layout. | |
| { | |
| let def = config.definition().clone(); | |
| let value_map = config.values_mut(); | |
| if let Some(token) = value_map.remove("token") { | |
| value_map.entry("registry".into()).or_insert_with(|| { | |
| let map = HashMap::from_iter([("token".into(), token)]); | |
| CV::Table(map, def.clone()) | |
| }); | |
| } | |
| } | |
| Ok(config) | |
| } | |
| /// Looks for a path for `tool` in an environment variable or the given config, and returns | |
| /// `None` if it's not present. | |
| fn maybe_get_tool( | |
| &self, | |
| tool: &str, | |
| from_config: &Option<ConfigRelativePath>, | |
| ) -> Option<PathBuf> { | |
| let var = tool.to_uppercase(); | |
| match self.get_env_os(&var).as_ref().and_then(|s| s.to_str()) { | |
| Some(tool_path) => { | |
| let maybe_relative = tool_path.contains('/') || tool_path.contains('\\'); | |
| let path = if maybe_relative { | |
| self.cwd.join(tool_path) | |
| } else { | |
| PathBuf::from(tool_path) | |
| }; | |
| Some(path) | |
| } | |
| None => from_config.as_ref().map(|p| p.resolve_program(self)), | |
| } | |
| } | |
| /// Returns the path for the given tool. | |
| /// | |
| /// This will look for the tool in the following order: | |
| /// | |
| /// 1. From an environment variable matching the tool name (such as `RUSTC`). | |
| /// 2. From the given config value (which is usually something like `build.rustc`). | |
| /// 3. Finds the tool in the PATH environment variable. | |
| /// | |
| /// This is intended for tools that are rustup proxies. If you need to get | |
| /// a tool that is not a rustup proxy, use `maybe_get_tool` instead. | |
| fn get_tool(&self, tool: Tool, from_config: &Option<ConfigRelativePath>) -> PathBuf { | |
| let tool_str = tool.as_str(); | |
| self.maybe_get_tool(tool_str, from_config) | |
| .or_else(|| { | |
| // This is an optimization to circumvent the rustup proxies | |
| // which can have a significant performance hit. The goal here | |
| // is to determine if calling `rustc` from PATH would end up | |
| // calling the proxies. | |
| // | |
| // This is somewhat cautious trying to determine if it is safe | |
| // to circumvent rustup, because there are some situations | |
| // where users may do things like modify PATH, call cargo | |
| // directly, use a custom rustup toolchain link without a | |
| // cargo executable, etc. However, there is still some risk | |
| // this may make the wrong decision in unusual circumstances. | |
| // | |
| // First, we must be running under rustup in the first place. | |
| let toolchain = self.get_env_os("RUSTUP_TOOLCHAIN")?; | |
| // Rustup supports named toolchains and absolute path toolchains, | |
| // but not relative path toolchains. This also enforces UTF-8. | |
| let toolchain = toolchain.to_str()?; | |
| let toolchain_path = Path::new(toolchain); | |
| let toolchain_dir = if toolchain_path.is_absolute() { | |
| toolchain_path.to_path_buf() | |
| } else if !toolchain.contains(&['/', '\\']) { | |
| home::rustup_home().ok()?.join("toolchains").join(toolchain) | |
| } else { | |
| return None; | |
| }; | |
| // If the tool on PATH is the same as `rustup` on path, then | |
| // there is pretty good evidence that it will be a proxy. | |
| let tool_resolved = paths::resolve_executable(Path::new(tool_str)).ok()?; | |
| let rustup_resolved = paths::resolve_executable(Path::new("rustup")).ok()?; | |
| let tool_meta = tool_resolved.metadata().ok()?; | |
| let rustup_meta = rustup_resolved.metadata().ok()?; | |
| // This works on the assumption that rustup and its proxies | |
| // use hard links to a single binary. If rustup ever changes | |
| // that setup, then I think the worst consequence is that this | |
| // optimization will not work, and it will take the slow path. | |
| if tool_meta.len() != rustup_meta.len() { | |
| return None; | |
| } | |
| // Try to find the tool in rustup's toolchain directory. | |
| let tool_exe = Path::new(tool_str).with_extension(env::consts::EXE_EXTENSION); | |
| let toolchain_exe = toolchain_dir.join("bin").join(&tool_exe); | |
| toolchain_exe.exists().then_some(toolchain_exe) | |
| }) | |
| .unwrap_or_else(|| PathBuf::from(tool_str)) | |
| } | |
| /// Get the `paths` overrides config value. | |
| pub fn paths_overrides(&self) -> CargoResult<OptValue<Vec<(String, Definition)>>> { | |
| let key = ConfigKey::from_parts(["paths"]); | |
| // paths overrides cannot be set via env config, so use get_cv here. | |
| match self.config()?.get_cv(&key)? { | |
| Some(CV::List(val, definition)) => { | |
| let val = val | |
| .into_iter() | |
| .map(|cv| match cv { | |
| CV::String(s, def) => Ok((s, def)), | |
| other => self.expected("string", &key, &other), | |
| }) | |
| .collect::<CargoResult<Vec<_>>>()?; | |
| Ok(Some(Value { val, definition })) | |
| } | |
| Some(val) => self.expected("list", &key, &val), | |
| None => Ok(None), | |
| } | |
| } | |
| pub fn jobserver_from_env(&self) -> Option<&jobserver::Client> { | |
| self.jobserver | |
| } | |
| pub fn http(&self) -> CargoResult<&Mutex<Easy>> { | |
| let http = self | |
| .easy | |
| .try_borrow_with(|| http_handle(self).map(Into::into))?; | |
| { | |
| let mut http = http.lock().unwrap(); | |
| http.reset(); | |
| let timeout = configure_http_handle(self, &mut http)?; | |
| timeout.configure(&mut http)?; | |
| } | |
| Ok(http) | |
| } | |
| pub fn http_async(&self) -> CargoResult<&http_async::Client> { | |
| self.http_async.try_borrow_with(|| { | |
| let handle_config = HandleConfiguration::new(&self)?; | |
| Ok(http_async::Client::new(handle_config)) | |
| }) | |
| } | |
| pub fn http_config(&self) -> CargoResult<&CargoHttpConfig> { | |
| self.http_config.try_borrow_with(|| { | |
| let mut http = self.get::<CargoHttpConfig>(["http"])?; | |
| let curl_v = curl::Version::get(); | |
| disables_multiplexing_for_bad_curl(curl_v.version(), &mut http, self); | |
| Ok(http) | |
| }) | |
| } | |
| pub fn future_incompat_config(&self) -> CargoResult<&CargoFutureIncompatConfig> { | |
| self.future_incompat_config | |
| .try_borrow_with(|| self.get::<CargoFutureIncompatConfig>(["future-incompat-report"])) | |
| } | |
| pub fn net_config(&self) -> CargoResult<&CargoNetConfig> { | |
| self.net_config | |
| .try_borrow_with(|| self.get::<CargoNetConfig>(["net"])) | |
| } | |
| pub fn build_config(&self) -> CargoResult<&CargoBuildConfig> { | |
| self.build_config | |
| .try_borrow_with(|| self.get::<CargoBuildConfig>(["build"])) | |
| } | |
| pub fn progress_config(&self) -> &ProgressConfig { | |
| &self.progress_config | |
| } | |
| /// Get the env vars from the config `[env]` table which | |
| /// are `force = true` or don't exist in the env snapshot [`GlobalContext::get_env`]. | |
| pub fn env_config(&self) -> CargoResult<&Arc<HashMap<String, OsString>>> { | |
| let env_config = self.env_config.try_borrow_with(|| { | |
| CargoResult::Ok(Arc::new({ | |
| let env_config = self.get::<EnvConfig>(["env"])?; | |
| // Reasons for disallowing these values: | |
| // | |
| // - CARGO_HOME: The initial call to cargo does not honor this value | |
| // from the [env] table. Recursive calls to cargo would use the new | |
| // value, possibly behaving differently from the outer cargo. | |
| // | |
| // - RUSTUP_HOME and RUSTUP_TOOLCHAIN: Under normal usage with rustup, | |
| // this will have no effect because the rustup proxy sets | |
| // RUSTUP_HOME and RUSTUP_TOOLCHAIN, and that would override the | |
| // [env] table. If the outer cargo is executed directly | |
| // circumventing the rustup proxy, then this would affect calls to | |
| // rustc (assuming that is a proxy), which could potentially cause | |
| // problems with cargo and rustc being from different toolchains. We | |
| // consider this to be not a use case we would like to support, | |
| // since it will likely cause problems or lead to confusion. | |
| for disallowed in &["CARGO_HOME", "RUSTUP_HOME", "RUSTUP_TOOLCHAIN"] { | |
| if env_config.contains_key(*disallowed) { | |
| bail!( | |
| "setting the `{disallowed}` environment variable is not supported \ | |
| in the `[env]` configuration table" | |
| ); | |
| } | |
| } | |
| env_config | |
| .into_iter() | |
| .filter_map(|(k, v)| { | |
| if v.is_force() || self.get_env_os(&k).is_none() { | |
| Some((k, v.resolve(self.cwd()).to_os_string())) | |
| } else { | |
| None | |
| } | |
| }) | |
| .collect() | |
| })) | |
| })?; | |
| Ok(env_config) | |
| } | |
| /// This is used to validate the `term` table has valid syntax. | |
| /// | |
| /// This is necessary because loading the term settings happens very | |
| /// early, and in some situations (like `cargo version`) we don't want to | |
| /// fail if there are problems with the config file. | |
| pub fn validate_term_config(&self) -> CargoResult<()> { | |
| drop(self.get::<TermConfig>(["term"])?); | |
| Ok(()) | |
| } | |
| /// Returns a list of `target.'cfg()'` tables. | |
| /// | |
| /// The list is sorted by the table name. | |
| pub fn target_cfgs(&self) -> CargoResult<&Vec<(String, TargetCfgConfig)>> { | |
| self.target_cfgs | |
| .try_borrow_with(|| target::load_target_cfgs(self)) | |
| } | |
| pub fn doc_extern_map(&self) -> CargoResult<&RustdocExternMap> { | |
| // Note: This does not support environment variables. The `Unit` | |
| // fundamentally does not have access to the registry name, so there is | |
| // nothing to query. Plumbing the name into SourceId is quite challenging. | |
| self.doc_extern_map | |
| .try_borrow_with(|| self.get::<RustdocExternMap>(["doc", "extern-map"])) | |
| } | |
| /// Returns true if the `[target]` table should be applied to host targets. | |
| pub fn target_applies_to_host(&self) -> CargoResult<bool> { | |
| target::get_target_applies_to_host(self) | |
| } | |
| /// Returns the `[host]` table definition for the given target tuple. | |
| pub fn host_cfg_tuple(&self, target: &str) -> CargoResult<TargetConfig> { | |
| target::load_host_tuple(self, target) | |
| } | |
| /// Returns the `[target]` table definition for the given target tuple. | |
| pub fn target_cfg_tuple(&self, target: &str) -> CargoResult<TargetConfig> { | |
| target::load_target_tuple(self, target) | |
| } | |
| /// Returns the cached [`SourceId`] corresponding to the main repository. | |
| /// | |
| /// This is the main cargo registry by default, but it can be overridden in | |
| /// a `.cargo/config.toml`. | |
| pub fn crates_io_source_id(&self) -> CargoResult<SourceId> { | |
| let source_id = self.crates_io_source_id.try_borrow_with(|| { | |
| self.check_registry_index_not_set()?; | |
| let url = CRATES_IO_INDEX.into_url().unwrap(); | |
| SourceId::for_alt_registry(&url, CRATES_IO_REGISTRY) | |
| })?; | |
| Ok(*source_id) | |
| } | |
| pub fn invocation_instant(&self) -> Instant { | |
| self.invocation_instant | |
| } | |
| /// Returns the wall-clock time of this cargo invocation. | |
| /// | |
| /// See the [`invocation_time`] field doc for details. | |
| /// | |
| /// [`invocation_time`]: GlobalContext::invocation_time | |
| pub fn invocation_time(&self) -> jiff::Timestamp { | |
| self.invocation_time | |
| } | |
| /// Retrieves a config variable. | |
| /// | |
| /// This supports most serde `Deserialize` types. Examples: | |
| /// | |
| /// ```rust,ignore | |
| /// let v: Option<u32> = config.get(["some", "nested", "key"])?; | |
| /// let v: Option<MyStruct> = config.get(["some", "key"])?; | |
| /// let v: Option<HashMap<String, MyStruct>> = config.get(["foo"])?; | |
| /// ``` | |
| /// | |
| /// The key may be a dotted key, but this does NOT support TOML key | |
| /// quoting. Avoid key components that may have dots. For example, | |
| /// `foo.'a.b'.bar" does not work if you try to fetch `foo.'a.b'". You can | |
| /// fetch `foo` if it is a map, though. | |
| pub fn get<'de, T: serde::de::Deserialize<'de>>( | |
| &self, | |
| key: impl IntoIterator<Item = impl AsRef<str>>, | |
| ) -> CargoResult<T> { | |
| let d = Deserializer { | |
| gctx: self, | |
| key: ConfigKey::from_parts(key), | |
| env_prefix_ok: true, | |
| }; | |
| T::deserialize(d).map_err(|e| e.into()) | |
| } | |
| /// Obtain a [`Path`] from a [`Filesystem`], verifying that the | |
| /// appropriate lock is already currently held. | |
| /// | |
| /// Locks are usually acquired via [`GlobalContext::acquire_package_cache_lock`] | |
| /// or [`GlobalContext::try_acquire_package_cache_lock`]. | |
| #[track_caller] | |
| #[tracing::instrument(skip_all)] | |
| pub fn assert_package_cache_locked<'a>( | |
| &self, | |
| mode: CacheLockMode, | |
| f: &'a Filesystem, | |
| ) -> &'a Path { | |
| let ret = f.as_path_unlocked(); | |
| assert!( | |
| self.package_cache_lock.is_locked(mode), | |
| "package cache lock is not currently held, Cargo forgot to call \ | |
| `acquire_package_cache_lock` before we got to this stack frame", | |
| ); | |
| assert!(ret.starts_with(self.home_path.as_path_unlocked())); | |
| ret | |
| } | |
| /// Acquires a lock on the global "package cache", blocking if another | |
| /// cargo holds the lock. | |
| /// | |
| /// See [`crate::util::cache_lock`] for an in-depth discussion of locking | |
| /// and lock modes. | |
| #[tracing::instrument(skip_all)] | |
| pub fn acquire_package_cache_lock(&self, mode: CacheLockMode) -> CargoResult<CacheLock<'_>> { | |
| self.package_cache_lock.lock(self, mode) | |
| } | |
| /// Acquires a lock on the global "package cache", returning `None` if | |
| /// another cargo holds the lock. | |
| /// | |
| /// See [`crate::util::cache_lock`] for an in-depth discussion of locking | |
| /// and lock modes. | |
| #[tracing::instrument(skip_all)] | |
| pub fn try_acquire_package_cache_lock( | |
| &self, | |
| mode: CacheLockMode, | |
| ) -> CargoResult<Option<CacheLock<'_>>> { | |
| self.package_cache_lock.try_lock(self, mode) | |
| } | |
| /// Returns a reference to the shared [`GlobalCacheTracker`]. | |
| /// | |
| /// The package cache lock must be held to call this function (and to use | |
| /// it in general). | |
| pub fn global_cache_tracker(&self) -> CargoResult<MutexGuard<'_, GlobalCacheTracker>> { | |
| let tracker = self.global_cache_tracker.try_borrow_with(|| { | |
| Ok::<_, anyhow::Error>(Mutex::new(GlobalCacheTracker::new(self)?)) | |
| })?; | |
| Ok(tracker.lock().unwrap()) | |
| } | |
| /// Returns a reference to the shared [`DeferredGlobalLastUse`]. | |
| pub fn deferred_global_last_use(&self) -> CargoResult<MutexGuard<'_, DeferredGlobalLastUse>> { | |
| let deferred = self | |
| .deferred_global_last_use | |
| .try_borrow_with(|| Ok::<_, anyhow::Error>(Mutex::new(DeferredGlobalLastUse::new())))?; | |
| Ok(deferred.lock().unwrap()) | |
| } | |
| /// Get the global [`WarningHandling`] configuration. | |
| pub fn warning_handling(&self) -> CargoResult<WarningHandling> { | |
| Ok(self.build_config()?.warnings.unwrap_or_default()) | |
| } | |
| pub fn ws_roots(&self) -> MutexGuard<'_, HashMap<PathBuf, WorkspaceRootConfig>> { | |
| self.ws_roots.lock().unwrap() | |
| } | |
| /// Mode for matching version requirements | |
| /// against versions already selected / locked. | |
| /// | |
| /// Once stabilized, | |
| /// this always returns [`VersionReqMatchMode::Prerelease`] (or remove and inline). | |
| pub fn version_req_match_mode(&self) -> VersionReqMatchMode { | |
| if self.cli_unstable().prerelease { | |
| VersionReqMatchMode::Prerelease | |
| } else { | |
| VersionReqMatchMode::Default | |
| } | |
| } | |
| } | |
| fn registry_config_uncached( | |
| gctx: &GlobalContext, | |
| sid: &SourceId, | |
| ) -> CargoResult<Option<RegistryConfig>> { | |
| tracing::trace!("loading registry config for {}", sid); | |
| if !sid.is_remote_registry() { | |
| bail!( | |
| "{} does not support API commands.\n\ | |
| Check for a source-replacement in .cargo/config.", | |
| sid | |
| ); | |
| } | |
| // Handle crates.io specially, since it uses different configuration keys. | |
| if sid.is_crates_io() { | |
| gctx.check_registry_index_not_set()?; | |
| return Ok(gctx | |
| .get::<Option<GlobalRegistryConfig>>(["registry"])? | |
| .map(|c| c.to_registry_config())); | |
| } | |
| let name = registry_name(gctx, sid)?; | |
| // It's possible to have a registry configured in a Cargo config file, | |
| // then override it with configuration from environment variables. | |
| // If the name doesn't match, leave a note to help the user understand | |
| // the potentially confusing situation. | |
| if let Some(name) = name.as_deref() { | |
| if Some(name) != sid.alt_registry_key() { | |
| gctx.shell().note(format!( | |
| "name of alternative registry `{}` set to `{name}`", | |
| sid.url() | |
| ))? | |
| } | |
| } | |
| if let Some(name) = &name { | |
| tracing::debug!("found alternative registry name `{name}` for {sid}"); | |
| gctx.get::<Option<RegistryConfig>>(["registries", name.as_str()]) | |
| } else { | |
| tracing::debug!("no registry name found for {sid}"); | |
| Ok(None) | |
| } | |
| } | |
| fn registry_name(gctx: &GlobalContext, sid: &SourceId) -> CargoResult<Option<String>> { | |
| // Find the SourceId's name by its index URL. If environment variables | |
| // are available they will be preferred over configuration values. | |
| // | |
| // The fundamental problem is that we only know the index url of the registry | |
| // for certain. For example, an unnamed registry source can come from the `--index` | |
| // command line argument, or from a Cargo.lock file. For this reason, we always | |
| // attempt to discover the name by looking it up by the index URL. | |
| // | |
| // This also allows the authorization token for a registry to be set | |
| // without knowing the registry name by using the _INDEX and _TOKEN | |
| // environment variables. | |
| let name = { | |
| // Discover names from environment variables. | |
| let index = sid.canonical_url(); | |
| let mut names: Vec<_> = gctx | |
| .env() | |
| .filter_map(|(k, v)| { | |
| Some(( | |
| k.strip_prefix("CARGO_REGISTRIES_")? | |
| .strip_suffix("_INDEX")?, | |
| v, | |
| )) | |
| }) | |
| .filter_map(|(k, v)| Some((k, CanonicalUrl::new(&v.into_url().ok()?).ok()?))) | |
| .filter(|(_, v)| v == index) | |
| .map(|(k, _)| k.to_lowercase()) | |
| .collect(); | |
| // Discover names from the configuration only if none were found in the environment. | |
| if names.len() == 0 { | |
| if let Some(registries) = gctx.config()?.values().get("registries") { | |
| let (registries, _) = registries.table("registries")?; | |
| for (name, value) in registries { | |
| if let Some(v) = value.table(&format!("registries.{name}"))?.0.get("index") { | |
| let (v, _) = v.string(&format!("registries.{name}.index"))?; | |
| if index == &CanonicalUrl::new(&v.into_url()?)? { | |
| names.push(name.clone()); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| names.sort(); | |
| match names.len() { | |
| 0 => None, | |
| 1 => Some(std::mem::take(&mut names[0])), | |
| _ => anyhow::bail!( | |
| "multiple registries are configured with the same index url '{}': {}", | |
| &sid.as_url(), | |
| names.join(", ") | |
| ), | |
| } | |
| }; | |
| Ok(name) | |
| } | |
| pub fn homedir(cwd: &Path) -> Option<PathBuf> { | |
| ::home::cargo_home_with_cwd(cwd) | |
| .ok() | |
| // https://github.com/rust-lang/cargo/issues/15981 | |
| // This is so everything shares one spelling and | |
| // isn't incorrectly seen as distinct. | |
| .map(|home| paths::normalize_path(&home)) | |
| } | |
| pub fn save_credentials( | |
| gctx: &GlobalContext, | |
| token: Option<RegistryCredentialConfig>, | |
| registry: &SourceId, | |
| ) -> CargoResult<()> { | |
| let registry = if registry.is_crates_io() { | |
| None | |
| } else { | |
| let name = registry | |
| .alt_registry_key() | |
| .ok_or_else(|| internal("can't save credentials for anonymous registry"))?; | |
| Some(name) | |
| }; | |
| // If 'credentials' exists, write to that for backward compatibility reasons. | |
| // Otherwise write to 'credentials.toml'. There's no need to print the | |
| // warning here, because it would already be printed at load time. | |
| let home_path = gctx.home_path.clone().into_path_unlocked(); | |
| let filename = match gctx.get_file_path(&home_path, "credentials", false)? { | |
| Some(path) => match path.file_name() { | |
| Some(filename) => Path::new(filename).to_owned(), | |
| None => Path::new("credentials.toml").to_owned(), | |
| }, | |
| None => Path::new("credentials.toml").to_owned(), | |
| }; | |
| let mut file = { | |
| gctx.home_path.create_dir()?; | |
| gctx.home_path | |
| .open_rw_exclusive_create(filename, gctx, "credentials' config file")? | |
| }; | |
| let mut contents = String::new(); | |
| file.read_to_string(&mut contents).with_context(|| { | |
| format!( | |
| "failed to read configuration file `{}`", | |
| file.path().display() | |
| ) | |
| })?; | |
| let mut toml = contents.parse::<toml::Table>()?; | |
| // Move the old token location to the new one. | |
| if let Some(token) = toml.remove("token") { | |
| #[expect( | |
| clippy::disallowed_types, | |
| reason = "need stdlib's HashMap because of TOML compatibility" | |
| )] | |
| let map = std::collections::HashMap::from([("token".to_string(), token)]); | |
| toml.insert("registry".into(), map.into()); | |
| } | |
| if let Some(token) = token { | |
| // login | |
| let path_def = Definition::Path(file.path().to_path_buf()); | |
| let (key, mut value) = match token { | |
| RegistryCredentialConfig::Token(token) => { | |
| // login with token | |
| let key = "token".to_string(); | |
| let value = ConfigValue::String(token.expose(), path_def.clone()); | |
| let map = HashMap::from_iter([(key, value)]); | |
| let table = CV::Table(map, path_def.clone()); | |
| if let Some(registry) = registry { | |
| let map = HashMap::from_iter([(registry.to_string(), table)]); | |
| ("registries".into(), CV::Table(map, path_def.clone())) | |
| } else { | |
| ("registry".into(), table) | |
| } | |
| } | |
| _ => unreachable!(), | |
| }; | |
| if registry.is_some() { | |
| if let Some(table) = toml.remove("registries") { | |
| let v = CV::from_toml(path_def, table)?; | |
| value.merge(v, false)?; | |
| } | |
| } | |
| toml.insert(key, value.into_toml()); | |
| } else { | |
| // logout | |
| if let Some(registry) = registry { | |
| if let Some(registries) = toml.get_mut("registries") { | |
| if let Some(reg) = registries.get_mut(registry) { | |
| let rtable = reg.as_table_mut().ok_or_else(|| { | |
| format_err!("expected `[registries.{}]` to be a table", registry) | |
| })?; | |
| rtable.remove("token"); | |
| } | |
| } | |
| } else if let Some(registry) = toml.get_mut("registry") { | |
| let reg_table = registry | |
| .as_table_mut() | |
| .ok_or_else(|| format_err!("expected `[registry]` to be a table"))?; | |
| reg_table.remove("token"); | |
| } | |
| } | |
| let contents = toml.to_string(); | |
| file.seek(SeekFrom::Start(0))?; | |
| file.write_all(contents.as_bytes()) | |
| .with_context(|| format!("failed to write to `{}`", file.path().display()))?; | |
| file.file().set_len(contents.len() as u64)?; | |
| set_permissions(file.file(), 0o600) | |
| .with_context(|| format!("failed to set permissions of `{}`", file.path().display()))?; | |
| return Ok(()); | |
| #[cfg(unix)] | |
| fn set_permissions(file: &File, mode: u32) -> CargoResult<()> { | |
| use std::os::unix::fs::PermissionsExt; | |
| let mut perms = file.metadata()?.permissions(); | |
| perms.set_mode(mode); | |
| file.set_permissions(perms)?; | |
| Ok(()) | |
| } | |
| #[cfg(not(unix))] | |
| fn set_permissions(_file: &File, _mode: u32) -> CargoResult<()> { | |
| Ok(()) | |
| } | |
| } | |
| /// Represents a config-include value in the configuration. | |
| /// | |
| /// This intentionally doesn't derive serde deserialization | |
| /// to avoid any misuse of `GlobalContext::get::<ConfigInclude>()`, | |
| /// which might lead to wrong config loading order. | |
| struct ConfigInclude { | |
| /// Path to a config-include configuration file. | |
| /// Could be either relative or absolute. | |
| path: PathBuf, | |
| def: Definition, | |
| /// Whether this include is optional (missing files are silently ignored) | |
| optional: bool, | |
| } | |
| impl ConfigInclude { | |
| fn new(p: impl Into<PathBuf>, def: Definition) -> Self { | |
| Self { | |
| path: p.into(), | |
| def, | |
| optional: false, | |
| } | |
| } | |
| /// Resolves the absolute path for this include. | |
| /// | |
| /// For file based include, | |
| /// it is relative to parent directory of the config file includes it. | |
| /// For example, if `.cargo/config.toml has a `include = "foo.toml"`, | |
| /// Cargo will load `.cargo/foo.toml`. | |
| /// | |
| /// For CLI based include (e.g., `--config 'include = "foo.toml"'`), | |
| /// it is relative to the current working directory. | |
| /// | |
| /// Returns `None` if this is an optional include and the file doesn't exist. | |
| /// Otherwise returns `Some(PathBuf)` with the absolute path. | |
| fn resolve_path(&self, gctx: &GlobalContext) -> Option<PathBuf> { | |
| let abs_path = match &self.def { | |
| Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap(), | |
| Definition::Environment(_) | Definition::Cli(None) | Definition::BuiltIn => gctx.cwd(), | |
| } | |
| .join(&self.path); | |
| let abs_path = paths::normalize_path(&abs_path); | |
| if self.optional && !abs_path.exists() { | |
| tracing::info!( | |
| "skipping optional include `{}` in `{}`: file not found at `{}`", | |
| self.path.display(), | |
| self.def, | |
| abs_path.display(), | |
| ); | |
| None | |
| } else { | |
| Some(abs_path) | |
| } | |
| } | |
| } | |
| fn toml_dotted_keys(arg: &str) -> CargoResult<toml_edit::DocumentMut> { | |
| // We only want to allow "dotted key" (see https://toml.io/en/v1.0.0#keys) | |
| // expressions followed by a value that's not an "inline table" | |
| // (https://toml.io/en/v1.0.0#inline-table). Easiest way to check for that is to | |
| // parse the value as a toml_edit::DocumentMut, and check that the (single) | |
| // inner-most table is set via dotted keys. | |
| let doc: toml_edit::DocumentMut = arg.parse().with_context(|| { | |
| format!("failed to parse value from --config argument `{arg}` as a dotted key expression") | |
| })?; | |
| fn non_empty(d: Option<&toml_edit::RawString>) -> bool { | |
| d.map_or(false, |p| !p.as_str().unwrap_or_default().trim().is_empty()) | |
| } | |
| fn non_empty_decor(d: &toml_edit::Decor) -> bool { | |
| non_empty(d.prefix()) || non_empty(d.suffix()) | |
| } | |
| fn non_empty_key_decor(k: &toml_edit::Key) -> bool { | |
| non_empty_decor(k.leaf_decor()) || non_empty_decor(k.dotted_decor()) | |
| } | |
| let ok = { | |
| let mut got_to_value = false; | |
| let mut table = doc.as_table(); | |
| let mut is_root = true; | |
| while table.is_dotted() || is_root { | |
| is_root = false; | |
| if table.len() != 1 { | |
| break; | |
| } | |
| let (k, n) = table.iter().next().expect("len() == 1 above"); | |
| match n { | |
| Item::Table(nt) => { | |
| if table.key(k).map_or(false, non_empty_key_decor) | |
| || non_empty_decor(nt.decor()) | |
| { | |
| bail!( | |
| "--config argument `{arg}` \ | |
| includes non-whitespace decoration" | |
| ) | |
| } | |
| table = nt; | |
| } | |
| Item::Value(v) if v.is_inline_table() => { | |
| bail!( | |
| "--config argument `{arg}` \ | |
| sets a value to an inline table, which is not accepted" | |
| ); | |
| } | |
| Item::Value(v) => { | |
| if table | |
| .key(k) | |
| .map_or(false, |k| non_empty(k.leaf_decor().prefix())) | |
| || non_empty_decor(v.decor()) | |
| { | |
| bail!( | |
| "--config argument `{arg}` \ | |
| includes non-whitespace decoration" | |
| ) | |
| } | |
| got_to_value = true; | |
| break; | |
| } | |
| Item::ArrayOfTables(_) => { | |
| bail!( | |
| "--config argument `{arg}` \ | |
| sets a value to an array of tables, which is not accepted" | |
| ); | |
| } | |
| Item::None => { | |
| bail!("--config argument `{arg}` doesn't provide a value") | |
| } | |
| } | |
| } | |
| got_to_value | |
| }; | |
| if !ok { | |
| bail!( | |
| "--config argument `{arg}` was not a TOML dotted key expression (such as `build.jobs = 2`)" | |
| ); | |
| } | |
| Ok(doc) | |
| } | |
| /// A type to deserialize a list of strings from a toml file. | |
| /// | |
| /// Supports deserializing either a whitespace-separated list of arguments in a | |
| /// single string or a string list itself. For example these deserialize to | |
| /// equivalent values: | |
| /// | |
| /// ```toml | |
| /// a = 'a b c' | |
| /// b = ['a', 'b', 'c'] | |
| /// ``` | |
| #[derive(Debug, Deserialize, Clone)] | |
| pub struct StringList(Vec<String>); | |
| impl StringList { | |
| pub fn as_slice(&self) -> &[String] { | |
| &self.0 | |
| } | |
| } | |
| #[macro_export] | |
| macro_rules! __shell_print { | |
| ($config:expr, $which:ident, $newline:literal, $($arg:tt)*) => ({ | |
| let mut shell = $config.shell(); | |
| let out = shell.$which(); | |
| drop(out.write_fmt(format_args!($($arg)*))); | |
| if $newline { | |
| drop(out.write_all(b"\n")); | |
| } | |
| }); | |
| } | |
| #[macro_export] | |
| macro_rules! drop_println { | |
| ($config:expr) => ( $crate::drop_print!($config, "\n") ); | |
| ($config:expr, $($arg:tt)*) => ( | |
| $crate::__shell_print!($config, out, true, $($arg)*) | |
| ); | |
| } | |
| #[macro_export] | |
| macro_rules! drop_eprintln { | |
| ($config:expr) => ( $crate::drop_eprint!($config, "\n") ); | |
| ($config:expr, $($arg:tt)*) => ( | |
| $crate::__shell_print!($config, err, true, $($arg)*) | |
| ); | |
| } | |
| #[macro_export] | |
| macro_rules! drop_print { | |
| ($config:expr, $($arg:tt)*) => ( | |
| $crate::__shell_print!($config, out, false, $($arg)*) | |
| ); | |
| } | |
| #[macro_export] | |
| macro_rules! drop_eprint { | |
| ($config:expr, $($arg:tt)*) => ( | |
| $crate::__shell_print!($config, err, false, $($arg)*) | |
| ); | |
| } | |
| enum Tool { | |
| Rustc, | |
| Rustdoc, | |
| } | |
| impl Tool { | |
| fn as_str(&self) -> &str { | |
| match self { | |
| Tool::Rustc => "rustc", | |
| Tool::Rustdoc => "rustdoc", | |
| } | |
| } | |
| } | |
| /// Disable HTTP/2 multiplexing for some broken versions of libcurl. | |
| /// | |
| /// In certain versions of libcurl when proxy is in use with HTTP/2 | |
| /// multiplexing, connections will continue stacking up. This was | |
| /// fixed in libcurl 8.0.0 in curl/curl@821f6e2a89de8aec1c7da3c0f381b92b2b801efc | |
| /// | |
| /// However, Cargo can still link against old system libcurl if it is from a | |
| /// custom built one or on macOS. For those cases, multiplexing needs to be | |
| /// disabled when those versions are detected. | |
| fn disables_multiplexing_for_bad_curl( | |
| curl_version: &str, | |
| http: &mut CargoHttpConfig, | |
| gctx: &GlobalContext, | |
| ) { | |
| use crate::util::network; | |
| if network::proxy::http_proxy_exists(http, gctx) && http.multiplexing.is_none() { | |
| let bad_curl_versions = ["7.87.0", "7.88.0", "7.88.1"]; | |
| if bad_curl_versions | |
| .iter() | |
| .any(|v| curl_version.starts_with(v)) | |
| { | |
| tracing::info!("disabling multiplexing with proxy, curl version is {curl_version}"); | |
| http.multiplexing = Some(false); | |
| } | |
| } | |
| } | |
| #[cfg(test)] | |
| mod tests { | |
| use super::CargoHttpConfig; | |
| use super::GlobalContext; | |
| use super::Shell; | |
| use super::disables_multiplexing_for_bad_curl; | |
| #[test] | |
| fn disables_multiplexing() { | |
| let mut gctx = GlobalContext::new(Shell::new(), "".into(), "".into()); | |
| gctx.set_search_stop_path(std::path::PathBuf::new()); | |
| gctx.set_env(Default::default()); | |
| let mut http = CargoHttpConfig::default(); | |
| http.proxy = Some("127.0.0.1:3128".into()); | |
| disables_multiplexing_for_bad_curl("7.88.1", &mut http, &gctx); | |
| assert_eq!(http.multiplexing, Some(false)); | |
| let cases = [ | |
| (None, None, "7.87.0", None), | |
| (None, None, "7.88.0", None), | |
| (None, None, "7.88.1", None), | |
| (None, None, "8.0.0", None), | |
| (Some("".into()), None, "7.87.0", Some(false)), | |
| (Some("".into()), None, "7.88.0", Some(false)), | |
| (Some("".into()), None, "7.88.1", Some(false)), | |
| (Some("".into()), None, "8.0.0", None), | |
| (Some("".into()), Some(false), "7.87.0", Some(false)), | |
| (Some("".into()), Some(false), "7.88.0", Some(false)), | |
| (Some("".into()), Some(false), "7.88.1", Some(false)), | |
| (Some("".into()), Some(false), "8.0.0", Some(false)), | |
| ]; | |
| for (proxy, multiplexing, curl_v, result) in cases { | |
| let mut http = CargoHttpConfig { | |
| multiplexing, | |
| proxy, | |
| ..Default::default() | |
| }; | |
| disables_multiplexing_for_bad_curl(curl_v, &mut http, &gctx); | |
| assert_eq!(http.multiplexing, result); | |
| } | |
| } | |
| #[test] | |
| fn sync_context() { | |
| fn assert_sync<S: Sync>() {} | |
| assert_sync::<GlobalContext>(); | |
| } | |
| } |