| //! 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.$TRIPLE` 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::{self, File}; |
| use std::io::SeekFrom; |
| use std::io::prelude::*; |
| use std::mem; |
| 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::compiler::rustdoc::RustdocExternMap; |
| use crate::ops::RegistryCredentialConfig; |
| use crate::sources::CRATES_IO_INDEX; |
| use crate::sources::CRATES_IO_REGISTRY; |
| use crate::util::OnceExt as _; |
| 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; |
| |
| mod value; |
| pub use value::{Definition, OptValue, Value}; |
| |
| mod key; |
| pub use key::ConfigKey; |
| |
| mod config_value; |
| pub use config_value::ConfigValue; |
| use config_value::is_nonmergeable_list; |
| |
| 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.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", |
| ]; |
| |
| /// Indicates why a config value is being loaded. |
| #[derive(Clone, Copy, Debug)] |
| enum WhyLoad { |
| /// Loaded due to a request from the global cli arg `--config` |
| /// |
| /// Indirect configs loaded via [`ConfigInclude`] are also seen as from cli args, |
| /// if the initial config is being loaded from cli. |
| Cli, |
| /// Loaded due to config file discovery. |
| FileDiscovery, |
| } |
| |
| /// 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 |
| values: OnceLock<HashMap<String, ConfigValue>>, |
| /// A collection of configuration options from the credentials file |
| credential_values: OnceLock<HashMap<String, ConfigValue>>, |
| /// 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, |
| values: Default::default(), |
| credential_values: 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() |
| } |
| |
| /// Cache of already parsed registries from the `[registries]` table. |
| pub(crate) fn registry_config( |
| &self, |
| ) -> MutexGuard<'_, HashMap<SourceId, Option<RegistryConfig>>> { |
| self.registry_config.lock().unwrap() |
| } |
| |
| /// 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 values(&self) -> CargoResult<&HashMap<String, ConfigValue>> { |
| self.values.try_borrow_with(|| self.load_values()) |
| } |
| |
| /// 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 values_mut(&mut self) -> CargoResult<&mut HashMap<String, ConfigValue>> { |
| let _ = self.values()?; |
| Ok(self.values.get_mut().expect("already loaded config values")) |
| } |
| |
| // Note: this is used by RLS, not Cargo. |
| pub fn set_values(&self, values: HashMap<String, ConfigValue>) -> CargoResult<()> { |
| if self.values.get().is_some() { |
| bail!("config values already found") |
| } |
| match self.values.set(values.into()) { |
| Ok(()) => Ok(()), |
| Err(_) => bail!("could not fill 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 values = self.load_values_from(path.as_ref())?; |
| self.values.replace(values); |
| 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)) |
| } |
| |
| /// Get a configuration value by key. |
| /// |
| /// This does NOT look at environment variables. See `get_cv_with_env` for |
| /// a variant that supports environment variables. |
| fn get_cv(&self, key: &ConfigKey) -> CargoResult<Option<ConfigValue>> { |
| if let Some(vals) = self.credential_values.get() { |
| let val = self.get_cv_helper(key, vals)?; |
| if val.is_some() { |
| return Ok(val); |
| } |
| } |
| self.get_cv_helper(key, &*self.values()?) |
| } |
| |
| fn get_cv_helper( |
| &self, |
| key: &ConfigKey, |
| vals: &HashMap<String, ConfigValue>, |
| ) -> CargoResult<Option<ConfigValue>> { |
| tracing::trace!("get cv {:?}", key); |
| if key.is_root() { |
| // Returning the entire root table (for example `cargo config get` |
| // with no key). The definition here shouldn't matter. |
| return Ok(Some(CV::Table( |
| vals.clone(), |
| Definition::Path(PathBuf::new()), |
| ))); |
| } |
| let mut parts = key.parts().enumerate(); |
| let Some(mut val) = vals.get(parts.next().unwrap().1) else { |
| return Ok(None); |
| }; |
| for (i, part) in parts { |
| match val { |
| CV::Table(map, _) => { |
| val = match map.get(part) { |
| Some(val) => val, |
| None => return Ok(None), |
| } |
| } |
| CV::Integer(_, def) |
| | CV::String(_, def) |
| | CV::List(_, def) |
| | CV::Boolean(_, def) => { |
| let mut key_so_far = ConfigKey::new(); |
| for part in key.parts().take(i) { |
| key_so_far.push(part); |
| } |
| bail!( |
| "expected table for configuration key `{}`, \ |
| but found {} in {}", |
| key_so_far, |
| val.desc(), |
| def |
| ) |
| } |
| } |
| } |
| Ok(Some(val.clone())) |
| } |
| |
| /// 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.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.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: &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.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 should_embed_metadata(&self) -> bool { |
| self.cli_unstable().embed_metadata.unwrap_or(true) |
| } |
| |
| 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_values(&self) -> CargoResult<HashMap<String, ConfigValue>> { |
| self.load_values_from(&self.cwd) |
| } |
| |
| /// Like [`load_values`](GlobalContext::load_values) but without merging config values. |
| /// |
| /// This is primarily crafted for `cargo config` command. |
| pub(crate) fn load_values_unmerged(&self) -> CargoResult<Vec<ConfigValue>> { |
| 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 cv = self._load_file(path, &mut seen, false, WhyLoad::FileDiscovery)?; |
| self.load_unmerged_include(&mut cv, &mut seen, &mut result)?; |
| result.push(cv); |
| 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, |
| cv: &mut CV, |
| seen: &mut HashSet<PathBuf>, |
| output: &mut Vec<CV>, |
| ) -> CargoResult<()> { |
| let includes = self.include_paths(cv, false)?; |
| for include in includes { |
| let Some(abs_path) = include.resolve_path(self) else { |
| continue; |
| }; |
| |
| let mut cv = self |
| ._load_file(&abs_path, seen, false, WhyLoad::FileDiscovery) |
| .with_context(|| { |
| format!( |
| "failed to load config include `{}` from `{}`", |
| include.path.display(), |
| include.def |
| ) |
| })?; |
| self.load_unmerged_include(&mut cv, seen, output)?; |
| output.push(cv); |
| } |
| Ok(()) |
| } |
| |
| /// Start a config file discovery from a path and merges all config values found. |
| fn load_values_from(&self, path: &Path) -> CargoResult<HashMap<String, ConfigValue>> { |
| // The root config value container isn't from any external source, |
| // so its definition should be built-in. |
| let mut cfg = CV::Table(HashMap::default(), Definition::BuiltIn); |
| let home = self.home_path.clone().into_path_unlocked(); |
| |
| self.walk_tree(path, &home, |path| { |
| let value = self.load_file(path)?; |
| cfg.merge(value, false).with_context(|| { |
| format!("failed to merge configuration at `{}`", path.display()) |
| })?; |
| Ok(()) |
| }) |
| .context("could not load Cargo configuration")?; |
| |
| match cfg { |
| CV::Table(map, _) => Ok(map), |
| _ => unreachable!(), |
| } |
| } |
| |
| /// Loads a config value from a path. |
| /// |
| /// This is used during config file discovery. |
| fn load_file(&self, path: &Path) -> CargoResult<ConfigValue> { |
| self._load_file(path, &mut HashSet::default(), true, WhyLoad::FileDiscovery) |
| } |
| |
| /// Loads a config value from a path with options. |
| /// |
| /// This is actual implementation of loading a config value from a path. |
| /// |
| /// * `includes` determines whether to load configs from [`ConfigInclude`]. |
| /// * `seen` is used to check for cyclic includes. |
| /// * `why_load` tells why a config is being loaded. |
| fn _load_file( |
| &self, |
| path: &Path, |
| seen: &mut HashSet<PathBuf>, |
| includes: bool, |
| why_load: WhyLoad, |
| ) -> CargoResult<ConfigValue> { |
| if !seen.insert(path.to_path_buf()) { |
| bail!( |
| "config `include` cycle detected with path `{}`", |
| path.display() |
| ); |
| } |
| tracing::debug!(?path, ?why_load, includes, "load config from file"); |
| |
| let contents = fs::read_to_string(path) |
| .with_context(|| format!("failed to read configuration file `{}`", path.display()))?; |
| let toml = parse_document(&contents, path, self).with_context(|| { |
| format!("could not parse TOML configuration in `{}`", path.display()) |
| })?; |
| let def = match why_load { |
| WhyLoad::Cli => Definition::Cli(Some(path.into())), |
| WhyLoad::FileDiscovery => Definition::Path(path.into()), |
| }; |
| let value = CV::from_toml(def, toml::Value::Table(toml)).with_context(|| { |
| format!( |
| "failed to load TOML configuration from `{}`", |
| path.display() |
| ) |
| })?; |
| if includes { |
| self.load_includes(value, seen, why_load) |
| } else { |
| Ok(value) |
| } |
| } |
| |
| /// Load any `include` files listed in the given `value`. |
| /// |
| /// Returns `value` with the given include files merged into it. |
| /// |
| /// * `seen` is used to check for cyclic includes. |
| /// * `why_load` tells why a config is being loaded. |
| fn load_includes( |
| &self, |
| mut value: CV, |
| seen: &mut HashSet<PathBuf>, |
| why_load: WhyLoad, |
| ) -> CargoResult<CV> { |
| // Get the list of files to load. |
| let includes = self.include_paths(&mut value, true)?; |
| |
| // Accumulate all values here. |
| let mut root = CV::Table(HashMap::default(), value.definition().clone()); |
| for include in includes { |
| let Some(abs_path) = include.resolve_path(self) else { |
| continue; |
| }; |
| |
| self._load_file(&abs_path, seen, true, why_load) |
| .and_then(|include| root.merge(include, true)) |
| .with_context(|| { |
| format!( |
| "failed to load config include `{}` from `{}`", |
| include.path.display(), |
| include.def |
| ) |
| })?; |
| } |
| root.merge(value, true)?; |
| Ok(root) |
| } |
| |
| /// Converts the `include` config value to a list of absolute paths. |
| fn include_paths(&self, cv: &mut CV, remove: bool) -> CargoResult<Vec<ConfigInclude>> { |
| let CV::Table(table, _def) = cv else { |
| unreachable!() |
| }; |
| 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_as_table(&self) -> CargoResult<ConfigValue> { |
| let mut loaded_args = CV::Table(HashMap::default(), Definition::Cli(None)); |
| 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 tmp_table = if !arg.is_empty() && arg_as_path.exists() { |
| // --config path_to_file |
| self._load_file(&arg_as_path, &mut seen, true, WhyLoad::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 |
| ); |
| } |
| |
| if doc |
| .get("registry") |
| .and_then(|v| v.as_table()) |
| .and_then(|t| t.get("secret-key")) |
| .is_some() |
| { |
| bail!( |
| "registry.secret-key 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("secret-key").is_some())) |
| { |
| bail!( |
| "registries.{}.secret-key cannot be set through --config for security reasons", |
| k |
| ); |
| } |
| |
| CV::from_toml(Definition::Cli(None), doc) |
| .with_context(|| format!("failed to convert --config argument `{arg}`"))? |
| }; |
| let tmp_table = self |
| .load_includes(tmp_table, &mut HashSet::default(), WhyLoad::Cli) |
| .context("failed to load --config include".to_string())?; |
| loaded_args |
| .merge(tmp_table, 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 cv_from_cli = self.cli_args_as_table()?; |
| assert!(cv_from_cli.is_table(), "cv from CLI must be a table"); |
| |
| let root_cv = mem::take(self.values_mut()?); |
| // The root config value container isn't from any external source, |
| // so its definition should be built-in. |
| let mut root_cv = CV::Table(root_cv, Definition::BuiltIn); |
| root_cv.merge(cv_from_cli, true)?; |
| |
| // Put it back to gctx |
| mem::swap(self.values_mut()?, root_cv.table_mut("<root>")?.0); |
| |
| Ok(()) |
| } |
| |
| /// 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(&format!("registries.{}.index", registry))? { |
| 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) |
| } |
| |
| /// Loads credentials config from the credentials file, if present. |
| /// |
| /// The credentials are loaded into a separate field to enable them |
| /// to be lazy-loaded after the main configuration has been loaded, |
| /// without requiring `mut` access to the [`GlobalContext`]. |
| /// |
| /// If the credentials are already loaded, this function does nothing. |
| pub fn load_credentials(&self) -> CargoResult<()> { |
| if self.credential_values.filled() { |
| return Ok(()); |
| } |
| |
| let home_path = self.home_path.clone().into_path_unlocked(); |
| let Some(credentials) = self.get_file_path(&home_path, "credentials", true)? else { |
| return Ok(()); |
| }; |
| |
| let mut value = self.load_file(&credentials)?; |
| // Backwards compatibility for old `.cargo/credentials` layout. |
| { |
| let (value_map, def) = value.table_mut("<root>")?; |
| |
| 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()) |
| }); |
| } |
| } |
| |
| let mut credential_values = HashMap::default(); |
| if let CV::Table(map, _) = value { |
| let base_map = self.values()?; |
| for (k, v) in map { |
| let entry = match base_map.get(&k) { |
| Some(base_entry) => { |
| let mut entry = base_entry.clone(); |
| entry.merge(v, true)?; |
| entry |
| } |
| None => v, |
| }; |
| credential_values.insert(k, entry); |
| } |
| } |
| self.credential_values |
| .set(credential_values) |
| .expect("was not filled at beginning of the function"); |
| Ok(()) |
| } |
| |
| /// 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")?; |
| // This currently does not support toolchain paths. |
| // This also enforces UTF-8. |
| if toolchain.to_str()?.contains(&['/', '\\']) { |
| 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 = home::rustup_home() |
| .ok()? |
| .join("toolchains") |
| .join(&toolchain) |
| .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_str("paths"); |
| // paths overrides cannot be set via env config, so use get_cv here. |
| match self.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 triple. |
| pub fn host_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> { |
| target::load_host_triple(self, target) |
| } |
| |
| /// Returns the `[target]` table definition for the given target triple. |
| pub fn target_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> { |
| target::load_target_triple(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: &str) -> CargoResult<T> { |
| let d = Deserializer { |
| gctx: self, |
| key: ConfigKey::from_str(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() |
| } |
| } |
| |
| 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 = parse_document(&contents, file.path(), gctx)?; |
| |
| // 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) |
| } |
| } |
| RegistryCredentialConfig::AsymmetricKey((secret_key, key_subject)) => { |
| // login with key |
| |
| let key = "secret-key".to_string(); |
| let value = ConfigValue::String(secret_key.expose(), path_def.clone()); |
| let mut map = HashMap::from_iter([(key, value)]); |
| if let Some(key_subject) = key_subject { |
| let key = "secret-key-subject".to_string(); |
| let value = ConfigValue::String(key_subject, path_def.clone()); |
| map.insert(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"); |
| rtable.remove("secret-key"); |
| rtable.remove("secret-key-subject"); |
| } |
| } |
| } 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"); |
| reg_table.remove("secret-key"); |
| reg_table.remove("secret-key-subject"); |
| } |
| } |
| |
| 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 parse_document(toml: &str, _file: &Path, _gctx: &GlobalContext) -> CargoResult<toml::Table> { |
| // At the moment, no compatibility checks are needed. |
| toml.parse().map_err(Into::into) |
| } |
| |
| 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>(); |
| } |
| } |