mirror of https://github.com/rust-lang/cargo
2968 lines
108 KiB
Rust
2968 lines
108 KiB
Rust
//! 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.
|
|
//!
|
|
//! ## 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.)
|
|
//!
|
|
//! ## Internal API
|
|
//!
|
|
//! Internally config values are stored with the `ConfigValue` type after they
|
|
//! have been loaded from disk. This is similar to the `toml::Value` type, but
|
|
//! includes the definition location. The `get()` method uses serde to
|
|
//! translate from `ConfigValue` and environment variables to the caller's
|
|
//! desired type.
|
|
|
|
use crate::util::cache_lock::{CacheLock, CacheLockMode, CacheLocker};
|
|
use std::borrow::Cow;
|
|
use std::cell::{RefCell, RefMut};
|
|
use std::collections::hash_map::Entry::{Occupied, Vacant};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::env;
|
|
use std::ffi::{OsStr, OsString};
|
|
use std::fmt;
|
|
use std::fs::{self, File};
|
|
use std::io::prelude::*;
|
|
use std::io::SeekFrom;
|
|
use std::mem;
|
|
use std::path::{Path, PathBuf};
|
|
use std::str::FromStr;
|
|
use std::sync::Once;
|
|
use std::time::Instant;
|
|
|
|
use self::ConfigValue as CV;
|
|
use crate::core::compiler::rustdoc::RustdocExternMap;
|
|
use crate::core::global_cache_tracker::{DeferredGlobalLastUse, GlobalCacheTracker};
|
|
use crate::core::shell::Verbosity;
|
|
use crate::core::{features, CliUnstable, Shell, SourceId, Workspace, WorkspaceRootConfig};
|
|
use crate::ops::RegistryCredentialConfig;
|
|
use crate::sources::CRATES_IO_INDEX;
|
|
use crate::sources::CRATES_IO_REGISTRY;
|
|
use crate::util::errors::CargoResult;
|
|
use crate::util::network::http::configure_http_handle;
|
|
use crate::util::network::http::http_handle;
|
|
use crate::util::try_canonicalize;
|
|
use crate::util::{internal, CanonicalUrl};
|
|
use crate::util::{Filesystem, IntoUrl, IntoUrlWithBase, Rustc};
|
|
use anyhow::{anyhow, bail, format_err, Context as _};
|
|
use cargo_credential::Secret;
|
|
use cargo_util::paths;
|
|
use cargo_util_schemas::manifest::RegistryName;
|
|
use curl::easy::Easy;
|
|
use lazycell::LazyCell;
|
|
use serde::de::IntoDeserializer as _;
|
|
use serde::Deserialize;
|
|
use serde_untagged::UntaggedEnumVisitor;
|
|
use time::OffsetDateTime;
|
|
use toml_edit::Item;
|
|
use url::Url;
|
|
|
|
mod de;
|
|
use de::Deserializer;
|
|
|
|
mod value;
|
|
pub use value::{Definition, OptValue, Value};
|
|
|
|
mod key;
|
|
pub use key::ConfigKey;
|
|
|
|
mod path;
|
|
pub use path::{ConfigRelativePath, PathAndArgs};
|
|
|
|
mod target;
|
|
pub use target::{TargetCfgConfig, TargetConfig};
|
|
|
|
mod environment;
|
|
use environment::Env;
|
|
|
|
use super::auth::RegistryConfig;
|
|
|
|
// 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),
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
/// 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 [`config-include`] are also seen as from cli args,
|
|
/// if the initial config is being loaded from cli.
|
|
///
|
|
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
|
|
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: RefCell<Shell>,
|
|
/// A collection of configuration options
|
|
values: LazyCell<HashMap<String, ConfigValue>>,
|
|
/// A collection of configuration options from the credentials file
|
|
credential_values: LazyCell<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: LazyCell<PathBuf>,
|
|
/// The location of the rustdoc executable
|
|
rustdoc: LazyCell<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<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: LazyCell<RefCell<Easy>>,
|
|
/// Cache of the `SourceId` for crates.io
|
|
crates_io_source_id: LazyCell<SourceId>,
|
|
/// If false, don't cache `rustc --version --verbose` invocations
|
|
cache_rustc_info: bool,
|
|
/// Creation time of this config, used to output the total build time
|
|
creation_time: Instant,
|
|
/// 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: LazyCell<RefCell<HashSet<SourceId>>>,
|
|
/// Cache of credentials from configuration or credential providers.
|
|
/// Maps from url to credential value.
|
|
credential_cache: LazyCell<RefCell<HashMap<CanonicalUrl, CredentialCacheValue>>>,
|
|
/// Cache of registry config from the `[registries]` table.
|
|
registry_config: LazyCell<RefCell<HashMap<SourceId, Option<RegistryConfig>>>>,
|
|
/// Locks on the package and index caches.
|
|
package_cache_lock: CacheLocker,
|
|
/// Cached configuration parsed by Cargo
|
|
http_config: LazyCell<CargoHttpConfig>,
|
|
future_incompat_config: LazyCell<CargoFutureIncompatConfig>,
|
|
net_config: LazyCell<CargoNetConfig>,
|
|
build_config: LazyCell<CargoBuildConfig>,
|
|
target_cfgs: LazyCell<Vec<(String, TargetCfgConfig)>>,
|
|
doc_extern_map: LazyCell<RustdocExternMap>,
|
|
progress_config: ProgressConfig,
|
|
env_config: LazyCell<EnvConfig>,
|
|
/// 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
|
|
pub ws_roots: RefCell<HashMap<PathBuf, WorkspaceRootConfig>>,
|
|
/// The global cache tracker is a database used to track disk cache usage.
|
|
global_cache_tracker: LazyCell<RefCell<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: LazyCell<RefCell<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(shell: Shell, cwd: PathBuf, homedir: PathBuf) -> GlobalContext {
|
|
static mut GLOBAL_JOBSERVER: *mut jobserver::Client = 0 as *mut _;
|
|
static INIT: Once = Once::new();
|
|
|
|
// This should be called early on in the process, so in theory the
|
|
// unsafety is ok here. (taken ownership of random fds)
|
|
INIT.call_once(|| unsafe {
|
|
if let Some(client) = jobserver::Client::from_env() {
|
|
GLOBAL_JOBSERVER = Box::into_raw(Box::new(client));
|
|
}
|
|
});
|
|
|
|
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,
|
|
};
|
|
|
|
GlobalContext {
|
|
home_path: Filesystem::new(homedir),
|
|
shell: RefCell::new(shell),
|
|
cwd,
|
|
search_stop_path: None,
|
|
values: LazyCell::new(),
|
|
credential_values: LazyCell::new(),
|
|
cli_config: None,
|
|
cargo_exe: LazyCell::new(),
|
|
rustdoc: LazyCell::new(),
|
|
extra_verbose: false,
|
|
frozen: false,
|
|
locked: false,
|
|
offline: false,
|
|
jobserver: unsafe {
|
|
if GLOBAL_JOBSERVER.is_null() {
|
|
None
|
|
} else {
|
|
Some((*GLOBAL_JOBSERVER).clone())
|
|
}
|
|
},
|
|
unstable_flags: CliUnstable::default(),
|
|
unstable_flags_cli: None,
|
|
easy: LazyCell::new(),
|
|
crates_io_source_id: LazyCell::new(),
|
|
cache_rustc_info,
|
|
creation_time: Instant::now(),
|
|
target_dir: None,
|
|
env,
|
|
updated_sources: LazyCell::new(),
|
|
credential_cache: LazyCell::new(),
|
|
registry_config: LazyCell::new(),
|
|
package_cache_lock: CacheLocker::new(),
|
|
http_config: LazyCell::new(),
|
|
future_incompat_config: LazyCell::new(),
|
|
net_config: LazyCell::new(),
|
|
build_config: LazyCell::new(),
|
|
target_cfgs: LazyCell::new(),
|
|
doc_extern_map: LazyCell::new(),
|
|
progress_config: ProgressConfig::default(),
|
|
env_config: LazyCell::new(),
|
|
nightly_features_allowed: matches!(&*features::channel(), "nightly" | "dev"),
|
|
ws_roots: RefCell::new(HashMap::new()),
|
|
global_cache_tracker: LazyCell::new(),
|
|
deferred_global_last_use: LazyCell::new(),
|
|
}
|
|
}
|
|
|
|
/// 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()
|
|
.with_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) -> RefMut<'_, Shell> {
|
|
self.shell.borrow_mut()
|
|
}
|
|
|
|
/// 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.target_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 = try_canonicalize(
|
|
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 = try_canonicalize(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`).
|
|
// In either case, `Path::canonicalize` will return the full absolute path
|
|
// to the target if it exists.
|
|
let argv0 = env::args_os()
|
|
.map(PathBuf::from)
|
|
.next()
|
|
.ok_or_else(|| anyhow!("no argv[0]"))?;
|
|
paths::resolve_executable(&argv0)
|
|
}
|
|
|
|
let exe = from_env()
|
|
.or_else(|_| from_current_exe())
|
|
.or_else(|_| from_argv())
|
|
.with_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) -> RefMut<'_, HashSet<SourceId>> {
|
|
self.updated_sources
|
|
.borrow_with(|| RefCell::new(HashSet::new()))
|
|
.borrow_mut()
|
|
}
|
|
|
|
/// Cached credentials from credential providers or configuration.
|
|
pub fn credential_cache(&self) -> RefMut<'_, HashMap<CanonicalUrl, CredentialCacheValue>> {
|
|
self.credential_cache
|
|
.borrow_with(|| RefCell::new(HashMap::new()))
|
|
.borrow_mut()
|
|
}
|
|
|
|
/// Cache of already parsed registries from the `[registries]` table.
|
|
pub(crate) fn registry_config(&self) -> RefMut<'_, HashMap<SourceId, Option<RegistryConfig>>> {
|
|
self.registry_config
|
|
.borrow_with(|| RefCell::new(HashMap::new()))
|
|
.borrow_mut()
|
|
}
|
|
|
|
/// 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
|
|
.borrow_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.borrow().is_some() {
|
|
bail!("config values already found")
|
|
}
|
|
match self.values.fill(values) {
|
|
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()
|
|
.with_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)
|
|
}
|
|
}
|
|
|
|
/// 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.borrow() {
|
|
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 or UnmergedStringList.
|
|
// 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<String> {
|
|
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<OsString> {
|
|
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)
|
|
}
|
|
|
|
/// Get a config value that is expected to be a path.
|
|
///
|
|
/// This returns a relative path if the value does not contain any
|
|
/// directory separators. See `ConfigRelativePath::resolve_program` for
|
|
/// more details.
|
|
pub fn get_path(&self, key: &str) -> CargoResult<OptValue<PathBuf>> {
|
|
self.get::<OptValue<ConfigRelativePath>>(key).map(|v| {
|
|
v.map(|v| Value {
|
|
val: v.val.resolve_program(self),
|
|
definition: v.definition,
|
|
})
|
|
})
|
|
}
|
|
|
|
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).join(value)
|
|
} else {
|
|
// A pathless name.
|
|
PathBuf::from(value)
|
|
}
|
|
}
|
|
|
|
/// Get a list of strings.
|
|
///
|
|
/// DO NOT USE outside of the config module. `pub` will be removed in the
|
|
/// future.
|
|
///
|
|
/// NOTE: this does **not** support environment variables. Use `get` instead
|
|
/// if you want that.
|
|
pub fn get_list(&self, key: &str) -> CargoResult<OptValue<Vec<(String, Definition)>>> {
|
|
let key = ConfigKey::from_str(key);
|
|
self._get_list(&key)
|
|
}
|
|
|
|
fn _get_list(&self, key: &ConfigKey) -> CargoResult<OptValue<Vec<(String, Definition)>>> {
|
|
match self.get_cv(key)? {
|
|
Some(CV::List(val, definition)) => Ok(Some(Value { val, definition })),
|
|
Some(val) => self.expected("list", key, &val),
|
|
None => Ok(None),
|
|
}
|
|
}
|
|
|
|
/// Helper for StringList type to get something that is a string or list.
|
|
fn get_list_or_string(
|
|
&self,
|
|
key: &ConfigKey,
|
|
merge: bool,
|
|
) -> CargoResult<Vec<(String, Definition)>> {
|
|
let mut res = Vec::new();
|
|
|
|
if !merge {
|
|
self.get_env_list(key, &mut res)?;
|
|
|
|
if !res.is_empty() {
|
|
return Ok(res);
|
|
}
|
|
}
|
|
|
|
match self.get_cv(key)? {
|
|
Some(CV::List(val, _def)) => res.extend(val),
|
|
Some(CV::String(val, def)) => {
|
|
let split_vs = val.split_whitespace().map(|s| (s.to_string(), def.clone()));
|
|
res.extend(split_vs);
|
|
}
|
|
Some(val) => {
|
|
return self.expected("string or array of strings", key, &val);
|
|
}
|
|
None => {}
|
|
}
|
|
|
|
self.get_env_list(key, &mut res)?;
|
|
|
|
Ok(res)
|
|
}
|
|
|
|
/// Internal method for getting an environment variable as a list.
|
|
fn get_env_list(
|
|
&self,
|
|
key: &ConfigKey,
|
|
output: &mut Vec<(String, Definition)>,
|
|
) -> CargoResult<()> {
|
|
let Some(env_val) = self.env.get_str(key.as_env_key()) else {
|
|
self.check_environment_key_case_mismatch(key);
|
|
return Ok(());
|
|
};
|
|
|
|
let def = Definition::Environment(key.as_env_key().to_string());
|
|
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 = toml::Value::deserialize(toml::de::ValueDeserializer::new(&env_val))
|
|
.map_err(|e| {
|
|
ConfigError::new(format!("could not parse TOML list: {}", e), def.clone())
|
|
})?;
|
|
let values = toml_v.as_array().expect("env var was not array");
|
|
for value in values {
|
|
// TODO: support other types.
|
|
let s = value.as_str().ok_or_else(|| {
|
|
ConfigError::new(
|
|
format!("expected string, found {}", value.type_str()),
|
|
def.clone(),
|
|
)
|
|
})?;
|
|
output.push((s.to_string(), def.clone()));
|
|
}
|
|
} else {
|
|
output.extend(
|
|
env_val
|
|
.split_whitespace()
|
|
.map(|s| (s.to_string(), def.clone())),
|
|
);
|
|
}
|
|
output.sort_by(|a, b| a.1.cmp(&b.1));
|
|
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<()> {
|
|
// 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;
|
|
|
|
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()?;
|
|
}
|
|
if self.unstable_flags.config_include {
|
|
// If the config was already loaded (like when fetching the
|
|
// `[alias]` table), it was loaded with includes disabled because
|
|
// the `unstable_flags` hadn't been set up, yet. Any values
|
|
// fetched before this step will not process includes, but that
|
|
// should be fine (`[alias]` is one of the only things loaded
|
|
// before configure). This can be removed when stabilized.
|
|
self.reload_rooted_at(self.cwd.clone())?;
|
|
}
|
|
|
|
self.load_unstable_flags_from_config()?;
|
|
|
|
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.frozen() && !self.offline()
|
|
}
|
|
|
|
pub fn offline(&self) -> bool {
|
|
self.offline
|
|
}
|
|
|
|
pub fn frozen(&self) -> bool {
|
|
self.frozen
|
|
}
|
|
|
|
pub fn locked(&self) -> bool {
|
|
self.locked
|
|
}
|
|
|
|
pub fn lock_update_allowed(&self) -> bool {
|
|
!self.frozen && !self.locked
|
|
}
|
|
|
|
/// 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::new();
|
|
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)?;
|
|
if self.cli_unstable().config_include {
|
|
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
|
|
}
|
|
result.push(cv);
|
|
Ok(())
|
|
})
|
|
.with_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 (path, abs_path, def) in includes {
|
|
let mut cv = self
|
|
._load_file(&abs_path, seen, false, WhyLoad::FileDiscovery)
|
|
.with_context(|| {
|
|
format!("failed to load config include `{}` from `{}`", path, 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>> {
|
|
// This definition path is ignored, this is just a temporary container
|
|
// representing the entire file.
|
|
let mut cfg = CV::Table(HashMap::new(), Definition::Path(PathBuf::from(".")));
|
|
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(())
|
|
})
|
|
.with_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::new(), 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 [`config-include`].
|
|
/// * `seen` is used to check for cyclic includes.
|
|
/// * `why_load` tells why a config is being loaded.
|
|
///
|
|
/// [`config-include`]: https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#config-include
|
|
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)?;
|
|
// Check unstable.
|
|
if !self.cli_unstable().config_include {
|
|
return Ok(value);
|
|
}
|
|
// Accumulate all values here.
|
|
let mut root = CV::Table(HashMap::new(), value.definition().clone());
|
|
for (path, abs_path, def) in includes {
|
|
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 `{}`", path, 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<(String, PathBuf, Definition)>> {
|
|
let abs = |path: &str, def: &Definition| -> (String, PathBuf, Definition) {
|
|
let abs_path = match def {
|
|
Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap().join(&path),
|
|
Definition::Environment(_) | Definition::Cli(None) => self.cwd().join(&path),
|
|
};
|
|
(path.to_string(), abs_path, def.clone())
|
|
};
|
|
let CV::Table(table, _def) = cv else {
|
|
unreachable!()
|
|
};
|
|
let owned;
|
|
let include = if remove {
|
|
owned = table.remove("include");
|
|
owned.as_ref()
|
|
} else {
|
|
table.get("include")
|
|
};
|
|
let includes = match include {
|
|
Some(CV::String(s, def)) => {
|
|
vec![abs(s, def)]
|
|
}
|
|
Some(CV::List(list, _def)) => list.iter().map(|(s, def)| abs(s, def)).collect(),
|
|
Some(other) => bail!(
|
|
"`include` expected a string or list, but found {} in `{}`",
|
|
other.desc(),
|
|
other.definition()
|
|
),
|
|
None => {
|
|
return Ok(Vec::new());
|
|
}
|
|
};
|
|
|
|
for (path, abs_path, def) in &includes {
|
|
if abs_path.extension() != Some(OsStr::new("toml")) {
|
|
bail!(
|
|
"expected a config include path ending with `.toml`, \
|
|
but found `{path}` from `{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::new(), Definition::Cli(None));
|
|
let Some(cli_args) = &self.cli_config else {
|
|
return Ok(loaded_args);
|
|
};
|
|
let mut seen = HashSet::new();
|
|
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
|
|
let str_path = arg_as_path
|
|
.to_str()
|
|
.ok_or_else(|| {
|
|
anyhow::format_err!("config path {:?} is not utf-8", arg_as_path)
|
|
})?
|
|
.to_string();
|
|
self._load_file(&self.cwd().join(&str_path), &mut seen, true, WhyLoad::Cli)
|
|
.with_context(|| format!("failed to load config from `{}`", str_path))?
|
|
} else {
|
|
// 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`)"
|
|
);
|
|
}
|
|
|
|
let toml_v: toml::Value = toml::Value::deserialize(doc.into_deserializer())
|
|
.with_context(|| {
|
|
format!("failed to parse value from --config argument `{arg}`")
|
|
})?;
|
|
|
|
if toml_v
|
|
.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, _)) = toml_v
|
|
.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 toml_v
|
|
.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, _)) = toml_v
|
|
.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), toml_v)
|
|
.with_context(|| format!("failed to convert --config argument `{arg}`"))?
|
|
};
|
|
let tmp_table = self
|
|
.load_includes(tmp_table, &mut HashSet::new(), WhyLoad::Cli)
|
|
.with_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::Table(loaded_map, _def) = self.cli_args_as_table()? else {
|
|
unreachable!()
|
|
};
|
|
let values = self.values_mut()?;
|
|
for (key, value) in loaded_map.into_iter() {
|
|
match values.entry(key) {
|
|
Vacant(entry) => {
|
|
entry.insert(value);
|
|
}
|
|
Occupied(mut entry) => entry.get_mut().merge(value, true).with_context(|| {
|
|
format!(
|
|
"failed to merge --config key `{}` into `{}`",
|
|
entry.key(),
|
|
entry.get().definition(),
|
|
)
|
|
})?,
|
|
};
|
|
}
|
|
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 possible.exists() {
|
|
if warn {
|
|
// 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.
|
|
let skip_warning = if let Ok(target_path) = fs::read_link(&possible) {
|
|
target_path == possible_with_extension
|
|
} else {
|
|
false
|
|
};
|
|
|
|
if !skip_warning {
|
|
if possible_with_extension.exists() {
|
|
self.shell().warn(format!(
|
|
"both `{}` and `{}` exist. Using `{}`",
|
|
possible.display(),
|
|
possible_with_extension.display(),
|
|
possible.display()
|
|
))?;
|
|
} else {
|
|
self.shell().warn(format!(
|
|
"`{}` is deprecated in favor of `{filename_without_extension}.toml`",
|
|
possible.display(),
|
|
))?;
|
|
self.shell().note(
|
|
format!("if you need to support cargo 1.38 or earlier, you can symlink `{filename_without_extension}` to `{filename_without_extension}.toml`"),
|
|
)?;
|
|
}
|
|
}
|
|
}
|
|
|
|
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 stash: HashSet<PathBuf> = HashSet::new();
|
|
|
|
for current in paths::ancestors(pwd, self.search_stop_path.as_deref()) {
|
|
if let Some(path) = self.get_file_path(¤t.join(".cargo"), "config", true)? {
|
|
walk(&path)?;
|
|
stash.insert(path);
|
|
}
|
|
}
|
|
|
|
// 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 let Some(path) = self.get_file_path(home, "config", true)? {
|
|
if !stash.contains(&path) {
|
|
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)
|
|
.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 CV::Table(ref mut value_map, ref def) = value else {
|
|
unreachable!();
|
|
};
|
|
|
|
if let Some(token) = value_map.remove("token") {
|
|
if let Vacant(entry) = value_map.entry("registry".into()) {
|
|
let map = HashMap::from([("token".into(), token)]);
|
|
let table = CV::Table(map, def.clone());
|
|
entry.insert(table);
|
|
}
|
|
}
|
|
}
|
|
|
|
let mut credential_values = HashMap::new();
|
|
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
|
|
.fill(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))
|
|
}
|
|
|
|
pub fn jobserver_from_env(&self) -> Option<&jobserver::Client> {
|
|
self.jobserver.as_ref()
|
|
}
|
|
|
|
pub fn http(&self) -> CargoResult<&RefCell<Easy>> {
|
|
let http = self
|
|
.easy
|
|
.try_borrow_with(|| http_handle(self).map(RefCell::new))?;
|
|
{
|
|
let mut http = http.borrow_mut();
|
|
http.reset();
|
|
let timeout = configure_http_handle(self, &mut http)?;
|
|
timeout.configure(&mut http)?;
|
|
}
|
|
Ok(http)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
pub fn env_config(&self) -> CargoResult<&EnvConfig> {
|
|
let env_config = self
|
|
.env_config
|
|
.try_borrow_with(|| 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"
|
|
);
|
|
}
|
|
}
|
|
|
|
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 creation_time(&self) -> Instant {
|
|
self.creation_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<RefMut<'_, GlobalCacheTracker>> {
|
|
let tracker = self.global_cache_tracker.try_borrow_with(|| {
|
|
Ok::<_, anyhow::Error>(RefCell::new(GlobalCacheTracker::new(self)?))
|
|
})?;
|
|
Ok(tracker.borrow_mut())
|
|
}
|
|
|
|
/// Returns a reference to the shared [`DeferredGlobalLastUse`].
|
|
pub fn deferred_global_last_use(&self) -> CargoResult<RefMut<'_, DeferredGlobalLastUse>> {
|
|
let deferred = self.deferred_global_last_use.try_borrow_with(|| {
|
|
Ok::<_, anyhow::Error>(RefCell::new(DeferredGlobalLastUse::new()))
|
|
})?;
|
|
Ok(deferred.borrow_mut())
|
|
}
|
|
}
|
|
|
|
/// Internal error for serde errors.
|
|
#[derive(Debug)]
|
|
pub struct ConfigError {
|
|
error: anyhow::Error,
|
|
definition: Option<Definition>,
|
|
}
|
|
|
|
impl ConfigError {
|
|
fn new(message: String, definition: Definition) -> ConfigError {
|
|
ConfigError {
|
|
error: anyhow::Error::msg(message),
|
|
definition: Some(definition),
|
|
}
|
|
}
|
|
|
|
fn expected(key: &ConfigKey, expected: &str, found: &ConfigValue) -> ConfigError {
|
|
ConfigError {
|
|
error: anyhow!(
|
|
"`{}` expected {}, but found a {}",
|
|
key,
|
|
expected,
|
|
found.desc()
|
|
),
|
|
definition: Some(found.definition().clone()),
|
|
}
|
|
}
|
|
|
|
fn missing(key: &ConfigKey) -> ConfigError {
|
|
ConfigError {
|
|
error: anyhow!("missing config key `{}`", key),
|
|
definition: None,
|
|
}
|
|
}
|
|
|
|
fn with_key_context(self, key: &ConfigKey, definition: Definition) -> ConfigError {
|
|
ConfigError {
|
|
error: anyhow::Error::from(self)
|
|
.context(format!("could not load config key `{}`", key)),
|
|
definition: Some(definition),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl std::error::Error for ConfigError {
|
|
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
|
self.error.source()
|
|
}
|
|
}
|
|
|
|
impl fmt::Display for ConfigError {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
if let Some(definition) = &self.definition {
|
|
write!(f, "error in {}: {}", definition, self.error)
|
|
} else {
|
|
self.error.fmt(f)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl serde::de::Error for ConfigError {
|
|
fn custom<T: fmt::Display>(msg: T) -> Self {
|
|
ConfigError {
|
|
error: anyhow::Error::msg(msg.to_string()),
|
|
definition: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<anyhow::Error> for ConfigError {
|
|
fn from(error: anyhow::Error) -> Self {
|
|
ConfigError {
|
|
error,
|
|
definition: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Eq, PartialEq, Clone)]
|
|
pub enum ConfigValue {
|
|
Integer(i64, Definition),
|
|
String(String, Definition),
|
|
List(Vec<(String, Definition)>, Definition),
|
|
Table(HashMap<String, ConfigValue>, Definition),
|
|
Boolean(bool, Definition),
|
|
}
|
|
|
|
impl fmt::Debug for ConfigValue {
|
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
match self {
|
|
CV::Integer(i, def) => write!(f, "{} (from {})", i, def),
|
|
CV::Boolean(b, def) => write!(f, "{} (from {})", b, def),
|
|
CV::String(s, def) => write!(f, "{} (from {})", s, def),
|
|
CV::List(list, def) => {
|
|
write!(f, "[")?;
|
|
for (i, (s, def)) in list.iter().enumerate() {
|
|
if i > 0 {
|
|
write!(f, ", ")?;
|
|
}
|
|
write!(f, "{} (from {})", s, def)?;
|
|
}
|
|
write!(f, "] (from {})", def)
|
|
}
|
|
CV::Table(table, _) => write!(f, "{:?}", table),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ConfigValue {
|
|
fn from_toml(def: Definition, toml: toml::Value) -> CargoResult<ConfigValue> {
|
|
match toml {
|
|
toml::Value::String(val) => Ok(CV::String(val, def)),
|
|
toml::Value::Boolean(b) => Ok(CV::Boolean(b, def)),
|
|
toml::Value::Integer(i) => Ok(CV::Integer(i, def)),
|
|
toml::Value::Array(val) => Ok(CV::List(
|
|
val.into_iter()
|
|
.map(|toml| match toml {
|
|
toml::Value::String(val) => Ok((val, def.clone())),
|
|
v => bail!("expected string but found {} in list", v.type_str()),
|
|
})
|
|
.collect::<CargoResult<_>>()?,
|
|
def,
|
|
)),
|
|
toml::Value::Table(val) => Ok(CV::Table(
|
|
val.into_iter()
|
|
.map(|(key, value)| {
|
|
let value = CV::from_toml(def.clone(), value)
|
|
.with_context(|| format!("failed to parse key `{}`", key))?;
|
|
Ok((key, value))
|
|
})
|
|
.collect::<CargoResult<_>>()?,
|
|
def,
|
|
)),
|
|
v => bail!(
|
|
"found TOML configuration value of unknown type `{}`",
|
|
v.type_str()
|
|
),
|
|
}
|
|
}
|
|
|
|
fn into_toml(self) -> toml::Value {
|
|
match self {
|
|
CV::Boolean(s, _) => toml::Value::Boolean(s),
|
|
CV::String(s, _) => toml::Value::String(s),
|
|
CV::Integer(i, _) => toml::Value::Integer(i),
|
|
CV::List(l, _) => {
|
|
toml::Value::Array(l.into_iter().map(|(s, _)| toml::Value::String(s)).collect())
|
|
}
|
|
CV::Table(l, _) => {
|
|
toml::Value::Table(l.into_iter().map(|(k, v)| (k, v.into_toml())).collect())
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Merge the given value into self.
|
|
///
|
|
/// If `force` is true, primitive (non-container) types will override existing values
|
|
/// of equal priority. For arrays, incoming values of equal priority will be placed later.
|
|
///
|
|
/// Container types (tables and arrays) are merged with existing values.
|
|
///
|
|
/// Container and non-container types cannot be mixed.
|
|
fn merge(&mut self, from: ConfigValue, force: bool) -> CargoResult<()> {
|
|
match (self, from) {
|
|
(&mut CV::List(ref mut old, _), CV::List(ref mut new, _)) => {
|
|
if force {
|
|
old.append(new);
|
|
} else {
|
|
new.append(old);
|
|
mem::swap(new, old);
|
|
}
|
|
old.sort_by(|a, b| a.1.cmp(&b.1));
|
|
}
|
|
(&mut CV::Table(ref mut old, _), CV::Table(ref mut new, _)) => {
|
|
for (key, value) in mem::take(new) {
|
|
match old.entry(key.clone()) {
|
|
Occupied(mut entry) => {
|
|
let new_def = value.definition().clone();
|
|
let entry = entry.get_mut();
|
|
entry.merge(value, force).with_context(|| {
|
|
format!(
|
|
"failed to merge key `{}` between \
|
|
{} and {}",
|
|
key,
|
|
entry.definition(),
|
|
new_def,
|
|
)
|
|
})?;
|
|
}
|
|
Vacant(entry) => {
|
|
entry.insert(value);
|
|
}
|
|
};
|
|
}
|
|
}
|
|
// Allow switching types except for tables or arrays.
|
|
(expected @ &mut CV::List(_, _), found)
|
|
| (expected @ &mut CV::Table(_, _), found)
|
|
| (expected, found @ CV::List(_, _))
|
|
| (expected, found @ CV::Table(_, _)) => {
|
|
return Err(anyhow!(
|
|
"failed to merge config value from `{}` into `{}`: expected {}, but found {}",
|
|
found.definition(),
|
|
expected.definition(),
|
|
expected.desc(),
|
|
found.desc()
|
|
));
|
|
}
|
|
(old, mut new) => {
|
|
if force || new.definition().is_higher_priority(old.definition()) {
|
|
mem::swap(old, &mut new);
|
|
}
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn i64(&self, key: &str) -> CargoResult<(i64, &Definition)> {
|
|
match self {
|
|
CV::Integer(i, def) => Ok((*i, def)),
|
|
_ => self.expected("integer", key),
|
|
}
|
|
}
|
|
|
|
pub fn string(&self, key: &str) -> CargoResult<(&str, &Definition)> {
|
|
match self {
|
|
CV::String(s, def) => Ok((s, def)),
|
|
_ => self.expected("string", key),
|
|
}
|
|
}
|
|
|
|
pub fn table(&self, key: &str) -> CargoResult<(&HashMap<String, ConfigValue>, &Definition)> {
|
|
match self {
|
|
CV::Table(table, def) => Ok((table, def)),
|
|
_ => self.expected("table", key),
|
|
}
|
|
}
|
|
|
|
pub fn list(&self, key: &str) -> CargoResult<&[(String, Definition)]> {
|
|
match self {
|
|
CV::List(list, _) => Ok(list),
|
|
_ => self.expected("list", key),
|
|
}
|
|
}
|
|
|
|
pub fn boolean(&self, key: &str) -> CargoResult<(bool, &Definition)> {
|
|
match self {
|
|
CV::Boolean(b, def) => Ok((*b, def)),
|
|
_ => self.expected("bool", key),
|
|
}
|
|
}
|
|
|
|
pub fn desc(&self) -> &'static str {
|
|
match *self {
|
|
CV::Table(..) => "table",
|
|
CV::List(..) => "array",
|
|
CV::String(..) => "string",
|
|
CV::Boolean(..) => "boolean",
|
|
CV::Integer(..) => "integer",
|
|
}
|
|
}
|
|
|
|
pub fn definition(&self) -> &Definition {
|
|
match self {
|
|
CV::Boolean(_, def)
|
|
| CV::Integer(_, def)
|
|
| CV::String(_, def)
|
|
| CV::List(_, def)
|
|
| CV::Table(_, def) => def,
|
|
}
|
|
}
|
|
|
|
fn expected<T>(&self, wanted: &str, key: &str) -> CargoResult<T> {
|
|
bail!(
|
|
"expected a {}, but found a {} for `{}` in {}",
|
|
wanted,
|
|
self.desc(),
|
|
key,
|
|
self.definition()
|
|
)
|
|
}
|
|
}
|
|
|
|
pub fn homedir(cwd: &Path) -> Option<PathBuf> {
|
|
::home::cargo_home_with_cwd(cwd).ok()
|
|
}
|
|
|
|
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") {
|
|
let map = 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([(key, value)]);
|
|
let table = CV::Table(map, path_def.clone());
|
|
|
|
if let Some(registry) = registry {
|
|
let map = HashMap::from([(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([(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([(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))]
|
|
#[allow(unused)]
|
|
fn set_permissions(file: &File, mode: u32) -> CargoResult<()> {
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct CargoHttpConfig {
|
|
pub proxy: Option<String>,
|
|
pub low_speed_limit: Option<u32>,
|
|
pub timeout: Option<u64>,
|
|
pub cainfo: Option<ConfigRelativePath>,
|
|
pub check_revoke: Option<bool>,
|
|
pub user_agent: Option<String>,
|
|
pub debug: Option<bool>,
|
|
pub multiplexing: Option<bool>,
|
|
pub ssl_version: Option<SslVersionConfig>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct CargoFutureIncompatConfig {
|
|
frequency: Option<CargoFutureIncompatFrequencyConfig>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum CargoFutureIncompatFrequencyConfig {
|
|
#[default]
|
|
Always,
|
|
Never,
|
|
}
|
|
|
|
impl CargoFutureIncompatConfig {
|
|
pub fn should_display_message(&self) -> bool {
|
|
use CargoFutureIncompatFrequencyConfig::*;
|
|
|
|
let frequency = self.frequency.as_ref().unwrap_or(&Always);
|
|
match frequency {
|
|
Always => true,
|
|
Never => false,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Configuration for `ssl-version` in `http` section
|
|
/// There are two ways to configure:
|
|
///
|
|
/// ```text
|
|
/// [http]
|
|
/// ssl-version = "tlsv1.3"
|
|
/// ```
|
|
///
|
|
/// ```text
|
|
/// [http]
|
|
/// ssl-version.min = "tlsv1.2"
|
|
/// ssl-version.max = "tlsv1.3"
|
|
/// ```
|
|
#[derive(Clone, Debug, PartialEq)]
|
|
pub enum SslVersionConfig {
|
|
Single(String),
|
|
Range(SslVersionConfigRange),
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for SslVersionConfig {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
UntaggedEnumVisitor::new()
|
|
.string(|single| Ok(SslVersionConfig::Single(single.to_owned())))
|
|
.map(|map| map.deserialize().map(SslVersionConfig::Range))
|
|
.deserialize(deserializer)
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct SslVersionConfigRange {
|
|
pub min: Option<String>,
|
|
pub max: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct CargoNetConfig {
|
|
pub retry: Option<u32>,
|
|
pub offline: Option<bool>,
|
|
pub git_fetch_with_cli: Option<bool>,
|
|
pub ssh: Option<CargoSshConfig>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct CargoSshConfig {
|
|
pub known_hosts: Option<Vec<Value<String>>>,
|
|
}
|
|
|
|
/// Configuration for `jobs` in `build` section. There are two
|
|
/// ways to configure: An integer or a simple string expression.
|
|
///
|
|
/// ```toml
|
|
/// [build]
|
|
/// jobs = 1
|
|
/// ```
|
|
///
|
|
/// ```toml
|
|
/// [build]
|
|
/// jobs = "default" # Currently only support "default".
|
|
/// ```
|
|
#[derive(Debug, Clone)]
|
|
pub enum JobsConfig {
|
|
Integer(i32),
|
|
String(String),
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for JobsConfig {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
UntaggedEnumVisitor::new()
|
|
.i32(|int| Ok(JobsConfig::Integer(int)))
|
|
.string(|string| Ok(JobsConfig::String(string.to_owned())))
|
|
.deserialize(deserializer)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct CargoBuildConfig {
|
|
// deprecated, but preserved for compatibility
|
|
pub pipelining: Option<bool>,
|
|
pub dep_info_basedir: Option<ConfigRelativePath>,
|
|
pub target_dir: Option<ConfigRelativePath>,
|
|
pub incremental: Option<bool>,
|
|
pub target: Option<BuildTargetConfig>,
|
|
pub jobs: Option<JobsConfig>,
|
|
pub rustflags: Option<StringList>,
|
|
pub rustdocflags: Option<StringList>,
|
|
pub rustc_wrapper: Option<ConfigRelativePath>,
|
|
pub rustc_workspace_wrapper: Option<ConfigRelativePath>,
|
|
pub rustc: Option<ConfigRelativePath>,
|
|
pub rustdoc: Option<ConfigRelativePath>,
|
|
pub out_dir: Option<ConfigRelativePath>,
|
|
}
|
|
|
|
/// Configuration for `build.target`.
|
|
///
|
|
/// Accepts in the following forms:
|
|
///
|
|
/// ```toml
|
|
/// target = "a"
|
|
/// target = ["a"]
|
|
/// target = ["a", "b"]
|
|
/// ```
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct BuildTargetConfig {
|
|
inner: Value<BuildTargetConfigInner>,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum BuildTargetConfigInner {
|
|
One(String),
|
|
Many(Vec<String>),
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for BuildTargetConfigInner {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
UntaggedEnumVisitor::new()
|
|
.string(|one| Ok(BuildTargetConfigInner::One(one.to_owned())))
|
|
.seq(|many| many.deserialize().map(BuildTargetConfigInner::Many))
|
|
.deserialize(deserializer)
|
|
}
|
|
}
|
|
|
|
impl BuildTargetConfig {
|
|
/// Gets values of `build.target` as a list of strings.
|
|
pub fn values(&self, gctx: &GlobalContext) -> CargoResult<Vec<String>> {
|
|
let map = |s: &String| {
|
|
if s.ends_with(".json") {
|
|
// Path to a target specification file (in JSON).
|
|
// <https://doc.rust-lang.org/rustc/targets/custom.html>
|
|
self.inner
|
|
.definition
|
|
.root(gctx)
|
|
.join(s)
|
|
.to_str()
|
|
.expect("must be utf-8 in toml")
|
|
.to_string()
|
|
} else {
|
|
// A string. Probably a target triple.
|
|
s.to_string()
|
|
}
|
|
};
|
|
let values = match &self.inner.val {
|
|
BuildTargetConfigInner::One(s) => vec![map(s)],
|
|
BuildTargetConfigInner::Many(v) => v.iter().map(map).collect(),
|
|
};
|
|
Ok(values)
|
|
}
|
|
}
|
|
|
|
#[derive(Deserialize, Default)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct TermConfig {
|
|
pub verbose: Option<bool>,
|
|
pub quiet: Option<bool>,
|
|
pub color: Option<String>,
|
|
pub hyperlinks: Option<bool>,
|
|
pub unicode: Option<bool>,
|
|
#[serde(default)]
|
|
#[serde(deserialize_with = "progress_or_string")]
|
|
pub progress: Option<ProgressConfig>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub struct ProgressConfig {
|
|
pub when: ProgressWhen,
|
|
pub width: Option<usize>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
#[serde(rename_all = "kebab-case")]
|
|
pub enum ProgressWhen {
|
|
#[default]
|
|
Auto,
|
|
Never,
|
|
Always,
|
|
}
|
|
|
|
fn progress_or_string<'de, D>(deserializer: D) -> Result<Option<ProgressConfig>, D::Error>
|
|
where
|
|
D: serde::de::Deserializer<'de>,
|
|
{
|
|
struct ProgressVisitor;
|
|
|
|
impl<'de> serde::de::Visitor<'de> for ProgressVisitor {
|
|
type Value = Option<ProgressConfig>;
|
|
|
|
fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
formatter.write_str("a string (\"auto\" or \"never\") or a table")
|
|
}
|
|
|
|
fn visit_str<E>(self, s: &str) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
match s {
|
|
"auto" => Ok(Some(ProgressConfig {
|
|
when: ProgressWhen::Auto,
|
|
width: None,
|
|
})),
|
|
"never" => Ok(Some(ProgressConfig {
|
|
when: ProgressWhen::Never,
|
|
width: None,
|
|
})),
|
|
"always" => Err(E::custom("\"always\" progress requires a `width` key")),
|
|
_ => Err(E::unknown_variant(s, &["auto", "never"])),
|
|
}
|
|
}
|
|
|
|
fn visit_none<E>(self) -> Result<Self::Value, E>
|
|
where
|
|
E: serde::de::Error,
|
|
{
|
|
Ok(None)
|
|
}
|
|
|
|
fn visit_some<D>(self, deserializer: D) -> Result<Self::Value, D::Error>
|
|
where
|
|
D: serde::de::Deserializer<'de>,
|
|
{
|
|
let pc = ProgressConfig::deserialize(deserializer)?;
|
|
if let ProgressConfig {
|
|
when: ProgressWhen::Always,
|
|
width: None,
|
|
} = pc
|
|
{
|
|
return Err(serde::de::Error::custom(
|
|
"\"always\" progress requires a `width` key",
|
|
));
|
|
}
|
|
Ok(Some(pc))
|
|
}
|
|
}
|
|
|
|
deserializer.deserialize_option(ProgressVisitor)
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
enum EnvConfigValueInner {
|
|
Simple(String),
|
|
WithOptions {
|
|
value: String,
|
|
force: bool,
|
|
relative: bool,
|
|
},
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for EnvConfigValueInner {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
#[derive(Deserialize)]
|
|
struct WithOptions {
|
|
value: String,
|
|
#[serde(default)]
|
|
force: bool,
|
|
#[serde(default)]
|
|
relative: bool,
|
|
}
|
|
|
|
UntaggedEnumVisitor::new()
|
|
.string(|simple| Ok(EnvConfigValueInner::Simple(simple.to_owned())))
|
|
.map(|map| {
|
|
let with_options: WithOptions = map.deserialize()?;
|
|
Ok(EnvConfigValueInner::WithOptions {
|
|
value: with_options.value,
|
|
force: with_options.force,
|
|
relative: with_options.relative,
|
|
})
|
|
})
|
|
.deserialize(deserializer)
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
#[serde(transparent)]
|
|
pub struct EnvConfigValue {
|
|
inner: Value<EnvConfigValueInner>,
|
|
}
|
|
|
|
impl EnvConfigValue {
|
|
pub fn is_force(&self) -> bool {
|
|
match self.inner.val {
|
|
EnvConfigValueInner::Simple(_) => false,
|
|
EnvConfigValueInner::WithOptions { force, .. } => force,
|
|
}
|
|
}
|
|
|
|
pub fn resolve<'a>(&'a self, gctx: &GlobalContext) -> Cow<'a, OsStr> {
|
|
match self.inner.val {
|
|
EnvConfigValueInner::Simple(ref s) => Cow::Borrowed(OsStr::new(s.as_str())),
|
|
EnvConfigValueInner::WithOptions {
|
|
ref value,
|
|
relative,
|
|
..
|
|
} => {
|
|
if relative {
|
|
let p = self.inner.definition.root(gctx).join(&value);
|
|
Cow::Owned(p.into_os_string())
|
|
} else {
|
|
Cow::Borrowed(OsStr::new(value.as_str()))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
pub type EnvConfig = HashMap<String, EnvConfigValue>;
|
|
|
|
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)
|
|
}
|
|
|
|
/// 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
|
|
}
|
|
}
|
|
|
|
/// StringList automatically merges config values with environment values,
|
|
/// this instead follows the precedence rules, so that eg. a string list found
|
|
/// in the environment will be used instead of one in a config file.
|
|
///
|
|
/// This is currently only used by `PathAndArgs`
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct UnmergedStringList(Vec<String>);
|
|
|
|
#[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::disables_multiplexing_for_bad_curl;
|
|
use super::CargoHttpConfig;
|
|
use super::GlobalContext;
|
|
use super::Shell;
|
|
|
|
#[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);
|
|
}
|
|
}
|
|
}
|