use crate::util::data_structures::{HashMap, HashSet};
use std::borrow::Cow;
use std::env;
use std::ffi::{OsStr, OsString};
use std::fmt;
use std::fs::{self, File};
use std::io::SeekFrom;
use std::io::prelude::*;
use std::mem;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock};
use std::time::Instant;
use self::ConfigValue as CV;
use crate::compiler::rustdoc::RustdocExternMap;
use crate::ops::RegistryCredentialConfig;
use crate::sources::CRATES_IO_INDEX;
use crate::sources::CRATES_IO_REGISTRY;
use crate::util::OnceExt as _;
use crate::util::cache_lock::{CacheLock, CacheLockMode, CacheLocker};
use crate::util::errors::CargoResult;
use crate::util::network::http::{HandleConfiguration, configure_http_handle, http_handle};
use crate::util::network::http_async;
use crate::util::restricted_names::is_glob_pattern;
use crate::util::{CanonicalUrl, closest_msg, internal};
use crate::util::{Filesystem, IntoUrl, IntoUrlWithBase, Rustc};
use crate::workspace::global_cache_tracker::{DeferredGlobalLastUse, GlobalCacheTracker};
use crate::workspace::{CliUnstable, SourceId, Workspace, WorkspaceRootConfig, features};
use anyhow::{Context as _, anyhow, bail, format_err};
use cargo_credential::Secret;
use cargo_util::paths;
use cargo_util_schemas::manifest::RegistryName;
use cargo_util_terminal::report::Level;
use cargo_util_terminal::{Shell, Verbosity};
use curl::easy::Easy;
use itertools::Itertools;
use serde::Deserialize;
use serde::de::IntoDeserializer as _;
use time::OffsetDateTime;
use toml_edit::Item;
use url::Url;
mod de;
use de::Deserializer;
mod error;
pub use error::ConfigError;
mod value;
pub use value::{Definition, OptValue, Value};
mod key;
pub use key::ConfigKey;
mod config_value;
pub use config_value::ConfigValue;
use config_value::is_nonmergeable_list;
mod path;
pub use path::BracketType;
pub use path::ConfigRelativePath;
pub use path::PathAndArgs;
pub use path::ResolveTemplateError;
mod target;
pub use target::{TargetCfgConfig, TargetConfig};
mod environment;
use environment::Env;
mod schema;
pub use schema::*;
macro_rules! get_value_typed {
($name:ident, $ty:ty, $variant:ident, $expected:expr) => {
fn $name(&self, key: &ConfigKey) -> Result<OptValue<$ty>, ConfigError> {
let cv = self.get_cv(key)?;
let env = self.get_config_env::<$ty>(key)?;
match (cv, env) {
(Some(CV::$variant(val, definition)), Some(env)) => {
if definition.is_higher_priority(&env.definition) {
Ok(Some(Value { val, definition }))
} else {
Ok(Some(env))
}
}
(Some(CV::$variant(val, definition)), None) => Ok(Some(Value { val, definition })),
(Some(cv), _) => Err(ConfigError::expected(key, $expected, &cv)),
(None, Some(env)) => Ok(Some(env)),
(None, None) => Ok(None),
}
}
};
}
pub const TOP_LEVEL_CONFIG_KEYS: &[&str] = &[
"paths",
"alias",
"build",
"credential-alias",
"doc",
"env",
"future-incompat-report",
"cache",
"cargo-new",
"http",
"install",
"net",
"patch",
"profile",
"resolver",
"registries",
"registry",
"source",
"target",
"term",
];
#[derive(Clone, Copy, Debug)]
enum WhyLoad {
Cli,
FileDiscovery,
}
#[derive(Debug)]
pub struct CredentialCacheValue {
pub token_value: Secret<String>,
pub expiration: Option<OffsetDateTime>,
pub operation_independent: bool,
}
#[derive(Debug)]
pub struct GlobalContext {
home_path: Filesystem,
shell: Mutex<Shell>,
values: OnceLock<HashMap<String, ConfigValue>>,
credential_values: OnceLock<HashMap<String, ConfigValue>>,
cli_config: Option<Vec<String>>,
cwd: PathBuf,
search_stop_path: Option<PathBuf>,
cargo_exe: OnceLock<PathBuf>,
rustdoc: OnceLock<PathBuf>,
extra_verbose: bool,
frozen: bool,
locked: bool,
offline: bool,
jobserver: Option<&'static jobserver::Client>,
unstable_flags: CliUnstable,
unstable_flags_cli: Option<Vec<String>>,
easy: OnceLock<Mutex<Easy>>,
crates_io_source_id: OnceLock<SourceId>,
cache_rustc_info: bool,
invocation_instant: Instant,
invocation_time: jiff::Timestamp,
target_dir: Option<Filesystem>,
env: Env,
updated_sources: Mutex<HashSet<SourceId>>,
credential_cache: Mutex<HashMap<CanonicalUrl, CredentialCacheValue>>,
registry_config: Mutex<HashMap<SourceId, Option<RegistryConfig>>>,
package_cache_lock: CacheLocker,
http_config: OnceLock<CargoHttpConfig>,
http_async: OnceLock<http_async::Client>,
future_incompat_config: OnceLock<CargoFutureIncompatConfig>,
net_config: OnceLock<CargoNetConfig>,
build_config: OnceLock<CargoBuildConfig>,
target_cfgs: OnceLock<Vec<(String, TargetCfgConfig)>>,
doc_extern_map: OnceLock<RustdocExternMap>,
progress_config: ProgressConfig,
env_config: OnceLock<Arc<HashMap<String, OsString>>>,
pub nightly_features_allowed: bool,
ws_roots: Mutex<HashMap<PathBuf, WorkspaceRootConfig>>,
global_cache_tracker: OnceLock<Mutex<GlobalCacheTracker>>,
deferred_global_last_use: OnceLock<Mutex<DeferredGlobalLastUse>>,
}
impl GlobalContext {
pub fn new(mut shell: Shell, cwd: PathBuf, homedir: PathBuf) -> GlobalContext {
static GLOBAL_JOBSERVER: LazyLock<CargoResult<Option<jobserver::Client>>> = LazyLock::new(
|| {
use jobserver::FromEnvErrorKind;
let jobserver::FromEnv { client, var } =
unsafe { jobserver::Client::from_env_ext(true) };
match client {
Ok(client) => return Ok(Some(client)),
Err(e)
if matches!(
e.kind(),
FromEnvErrorKind::NoEnvVar
| FromEnvErrorKind::NoJobserver
| FromEnvErrorKind::NegativeFd
| FromEnvErrorKind::Unsupported
) =>
{
Ok(None)
}
Err(e) => {
let (name, value) = var.unwrap();
Err(anyhow::anyhow!(
"failed to connect to jobserver from environment variable `{name}={value:?}`: {e}"
))
}
}
},
);
let jobserver = match &*GLOBAL_JOBSERVER {
Ok(jobserver) => jobserver.as_ref(),
Err(e) => {
let _ = shell.warn(e);
None
}
};
let env = Env::new();
let cache_key = "CARGO_CACHE_RUSTC_INFO";
let cache_rustc_info = match env.get_env_os(cache_key) {
Some(cache) => cache != "0",
_ => true,
};
#[expect(
clippy::disallowed_methods,
reason = "testing only, no reason for config support"
)]
let invocation_time = match env::var("__CARGO_TEST_INVOCATION_TIME") {
Ok(now) => now.parse().unwrap(),
Err(_) => jiff::Timestamp::now(),
};
GlobalContext {
home_path: Filesystem::new(homedir),
shell: Mutex::new(shell),
cwd,
search_stop_path: None,
values: Default::default(),
credential_values: Default::default(),
cli_config: None,
cargo_exe: Default::default(),
rustdoc: Default::default(),
extra_verbose: false,
frozen: false,
locked: false,
offline: false,
jobserver,
unstable_flags: CliUnstable::default(),
unstable_flags_cli: None,
easy: Default::default(),
crates_io_source_id: Default::default(),
cache_rustc_info,
invocation_instant: Instant::now(),
invocation_time,
target_dir: None,
env,
updated_sources: Default::default(),
credential_cache: Default::default(),
registry_config: Default::default(),
package_cache_lock: CacheLocker::new(),
http_config: Default::default(),
http_async: Default::default(),
future_incompat_config: Default::default(),
net_config: Default::default(),
build_config: Default::default(),
target_cfgs: Default::default(),
doc_extern_map: Default::default(),
progress_config: ProgressConfig::default(),
env_config: Default::default(),
nightly_features_allowed: matches!(&*features::channel(), "nightly" | "dev"),
ws_roots: Default::default(),
global_cache_tracker: Default::default(),
deferred_global_last_use: Default::default(),
}
}
pub fn default() -> CargoResult<GlobalContext> {
let shell = Shell::new();
let cwd =
env::current_dir().context("couldn't get the current directory of the process")?;
let homedir = homedir(&cwd).ok_or_else(|| {
anyhow!(
"Cargo couldn't find your home directory. \
This probably means that $HOME was not set."
)
})?;
Ok(GlobalContext::new(shell, cwd, homedir))
}
pub fn home(&self) -> &Filesystem {
&self.home_path
}
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()
}
pub fn git_path(&self) -> Filesystem {
self.home_path.join("git")
}
pub fn git_checkouts_path(&self) -> Filesystem {
self.git_path().join("checkouts")
}
pub fn git_db_path(&self) -> Filesystem {
self.git_path().join("db")
}
pub fn registry_base_path(&self) -> Filesystem {
self.home_path.join("registry")
}
pub fn registry_index_path(&self) -> Filesystem {
self.registry_base_path().join("index")
}
pub fn registry_cache_path(&self) -> Filesystem {
self.registry_base_path().join("cache")
}
pub fn registry_source_path(&self) -> Filesystem {
self.registry_base_path().join("src")
}
pub fn default_registry(&self) -> CargoResult<Option<String>> {
Ok(self
.get_string("registry.default")?
.map(|registry| registry.val))
}
pub fn shell(&self) -> MutexGuard<'_, Shell> {
self.shell.lock().unwrap()
}
pub fn debug_assert_shell_not_borrowed(&self) {
if cfg!(debug_assertions) {
match self.shell.try_lock() {
Ok(_) | Err(std::sync::TryLockError::Poisoned(_)) => (),
Err(std::sync::TryLockError::WouldBlock) => panic!("shell is borrowed!"),
}
}
}
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)
}
pub fn load_global_rustc(&self, ws: Option<&Workspace<'_>>) -> CargoResult<Rustc> {
let cache_location =
ws.map(|ws| ws.build_dir().join(".rustc_info.json").into_path_unlocked());
let wrapper = self.maybe_get_tool("rustc_wrapper", &self.build_config()?.rustc_wrapper);
let rustc_workspace_wrapper = self.maybe_get_tool(
"rustc_workspace_wrapper",
&self.build_config()?.rustc_workspace_wrapper,
);
Rustc::new(
self.get_tool(Tool::Rustc, &self.build_config()?.rustc),
wrapper,
rustc_workspace_wrapper,
&self
.home()
.join("bin")
.join("rustc")
.into_path_unlocked()
.with_extension(env::consts::EXE_EXTENSION),
if self.cache_rustc_info {
cache_location
} else {
None
},
self,
)
}
pub fn cargo_exe(&self) -> CargoResult<&Path> {
self.cargo_exe
.try_borrow_with(|| {
let from_env = || -> CargoResult<PathBuf> {
let exe = self
.get_env_os(crate::CARGO_ENV)
.map(PathBuf::from)
.ok_or_else(|| anyhow!("$CARGO not set"))?;
Ok(exe)
};
fn from_current_exe() -> CargoResult<PathBuf> {
let exe = env::current_exe()?;
Ok(exe)
}
fn from_argv() -> CargoResult<PathBuf> {
let argv0 = env::args_os()
.map(PathBuf::from)
.next()
.ok_or_else(|| anyhow!("no argv[0]"))?;
paths::resolve_executable(&argv0)
}
fn is_cargo(path: &Path) -> bool {
path.file_stem() == Some(OsStr::new("cargo"))
}
let from_current_exe = from_current_exe();
if from_current_exe.as_deref().is_ok_and(is_cargo) {
return from_current_exe;
}
let from_argv = from_argv();
if from_argv.as_deref().is_ok_and(is_cargo) {
return from_argv;
}
let exe = from_env()
.or(from_current_exe)
.or(from_argv)
.context("couldn't get the path to cargo executable")?;
Ok(exe)
})
.map(AsRef::as_ref)
}
pub fn updated_sources(&self) -> MutexGuard<'_, HashSet<SourceId>> {
self.updated_sources.lock().unwrap()
}
pub fn credential_cache(&self) -> MutexGuard<'_, HashMap<CanonicalUrl, CredentialCacheValue>> {
self.credential_cache.lock().unwrap()
}
pub(crate) fn registry_config(
&self,
) -> MutexGuard<'_, HashMap<SourceId, Option<RegistryConfig>>> {
self.registry_config.lock().unwrap()
}
pub fn values(&self) -> CargoResult<&HashMap<String, ConfigValue>> {
self.values.try_borrow_with(|| self.load_values())
}
pub fn values_mut(&mut self) -> CargoResult<&mut HashMap<String, ConfigValue>> {
let _ = self.values()?;
Ok(self.values.get_mut().expect("already loaded config values"))
}
pub fn set_values(&self, values: HashMap<String, ConfigValue>) -> CargoResult<()> {
if self.values.get().is_some() {
bail!("config values already found")
}
match self.values.set(values.into()) {
Ok(()) => Ok(()),
Err(_) => bail!("could not fill values"),
}
}
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);
}
pub fn reload_cwd(&mut self) -> CargoResult<()> {
let cwd =
env::current_dir().context("couldn't get the current directory of the process")?;
let homedir = homedir(&cwd).ok_or_else(|| {
anyhow!(
"Cargo couldn't find your home directory. \
This probably means that $HOME was not set."
)
})?;
self.cwd = cwd;
self.home_path = Filesystem::new(homedir);
self.reload_rooted_at(self.cwd.clone())?;
Ok(())
}
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(())
}
pub fn cwd(&self) -> &Path {
&self.cwd
}
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") {
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);
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)
}
}
pub fn build_dir(&self, workspace_manifest_path: &Path) -> CargoResult<Option<Filesystem>> {
let Some(val) = &self.build_config()?.build_dir else {
return Ok(None);
};
self.custom_build_dir(val, workspace_manifest_path)
.map(Some)
}
pub fn custom_build_dir(
&self,
val: &ConfigRelativePath,
workspace_manifest_path: &Path,
) -> CargoResult<Filesystem> {
let replacements = [
(
"{workspace-root}",
workspace_manifest_path
.parent()
.unwrap()
.to_str()
.context("workspace root was not valid utf-8")?
.to_string(),
),
(
"{cargo-cache-home}",
self.home()
.as_path_unlocked()
.to_str()
.context("cargo home was not valid utf-8")?
.to_string(),
),
("{workspace-path-hash}", {
let real_path = std::fs::canonicalize(workspace_manifest_path)
.unwrap_or_else(|_err| workspace_manifest_path.to_owned());
let hash = crate::util::hex::short_hash(&real_path);
format!("{}{}{}", &hash[0..2], std::path::MAIN_SEPARATOR, &hash[2..])
}),
];
let template_variables = replacements
.iter()
.map(|(key, _)| key[1..key.len() - 1].to_string())
.collect_vec();
let path = val
.resolve_templated_path(self, replacements)
.map_err(|e| match e {
path::ResolveTemplateError::UnexpectedVariable {
variable,
raw_template,
} => {
let mut suggestion = closest_msg(&variable, template_variables.iter(), |key| key, "template variable");
if suggestion == "" {
let variables = template_variables.iter().map(|v| format!("`{{{v}}}`")).join(", ");
suggestion = format!("\n\nhelp: available template variables are {variables}");
}
anyhow!(
"unexpected variable `{variable}` in build.build-dir path `{raw_template}`{suggestion}"
)
}
path::ResolveTemplateError::UnexpectedBracket { bracket_type, raw_template } => {
let (btype, literal) = match bracket_type {
path::BracketType::Opening => ("opening", "{"),
path::BracketType::Closing => ("closing", "}"),
};
anyhow!(
"unexpected {btype} bracket `{literal}` in build.build-dir path `{raw_template}`"
)
}
})?;
if val.raw_value().is_empty() {
bail!(
"the build directory is set to an empty string in {}",
val.value().definition
)
}
Ok(Filesystem::new(path))
}
fn get_cv(&self, key: &ConfigKey) -> CargoResult<Option<ConfigValue>> {
if let Some(vals) = self.credential_values.get() {
let val = self.get_cv_helper(key, vals)?;
if val.is_some() {
return Ok(val);
}
}
self.get_cv_helper(key, &*self.values()?)
}
fn get_cv_helper(
&self,
key: &ConfigKey,
vals: &HashMap<String, ConfigValue>,
) -> CargoResult<Option<ConfigValue>> {
tracing::trace!("get cv {:?}", key);
if key.is_root() {
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()))
}
pub(crate) fn get_cv_with_env(&self, key: &ConfigKey) -> CargoResult<Option<CV>> {
let cv = self.get_cv(key)?;
if key.is_root() {
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) {
(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);
}
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)) => {
self.get_env_list(key, &mut cv_list)?;
Ok(Some(CV::List(cv_list, cv_def)))
}
Some(cv) => {
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 {
match cv {
Some(CV::List(mut cv_list, cv_def)) => {
self.get_env_list(key, &mut cv_list)?;
Ok(Some(CV::List(cv_list, cv_def)))
}
_ => {
Ok(Some(CV::String(env.to_string(), env_def)))
}
}
}
}
pub fn set_env(&mut self, env: HashMap<String, String>) {
self.env = Env::from_map(env);
}
pub(crate) fn env(&self) -> impl Iterator<Item = (&str, &str)> {
self.env.iter_str()
}
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)
}
}
}
pub fn get_env(&self, key: impl AsRef<OsStr>) -> CargoResult<&str> {
self.env.get_env(key)
}
pub fn get_env_os(&self, key: impl AsRef<OsStr>) -> Option<&OsStr> {
self.env.get_env_os(key)
}
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
));
}
}
pub fn get_string(&self, key: &str) -> CargoResult<OptValue<String>> {
self.get::<OptValue<String>>(key)
}
fn string_to_path(&self, value: &str, definition: &Definition) -> PathBuf {
let is_path = value.contains('/') || (cfg!(windows) && value.contains('\\'));
if is_path {
definition.root(self.cwd()).join(value)
} else {
PathBuf::from(value)
}
}
fn get_env_list(&self, key: &ConfigKey, output: &mut Vec<ConfigValue>) -> CargoResult<()> {
let Some(env_val) = self.env.get_str(key.as_env_key()) else {
self.check_environment_key_case_mismatch(key);
return Ok(());
};
let env_def = Definition::Environment(key.as_env_key().to_string());
if is_nonmergeable_list(&key) {
assert!(
output
.windows(2)
.all(|cvs| cvs[0].definition() == cvs[1].definition()),
"non-mergeable list must have only one definition: {output:?}",
);
if output
.first()
.map(|o| o.definition() > &env_def)
.unwrap_or_default()
{
return Ok(());
} else {
output.clear();
}
}
if self.cli_unstable().advanced_env && env_val.starts_with('[') && env_val.ends_with(']') {
let toml_v = env_val.parse::<toml::Value>().map_err(|e| {
ConfigError::new(format!("could not parse TOML list: {}", e), env_def.clone())
})?;
let values = toml_v.as_array().expect("env var was not array");
for value in values {
let s = value.as_str().ok_or_else(|| {
ConfigError::new(
format!("expected string, found {}", value.type_str()),
env_def.clone(),
)
})?;
output.push(CV::String(s.to_string(), env_def.clone()))
}
} else {
output.extend(
env_val
.split_whitespace()
.map(|s| CV::String(s.to_string(), env_def.clone())),
);
}
output.sort_by(|a, b| a.definition().cmp(b.definition()));
Ok(())
}
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"}
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))
}
pub fn configure(
&mut self,
verbose: u32,
quiet: bool,
color: Option<&str>,
frozen: bool,
locked: bool,
offline: bool,
target_dir: &Option<PathBuf>,
unstable_flags: &[String],
cli_config: &[String],
) -> CargoResult<()> {
for warning in self
.unstable_flags
.parse(unstable_flags, self.nightly_features_allowed)?
{
self.shell().warn(warning)?;
}
if !unstable_flags.is_empty() {
self.unstable_flags_cli = Some(unstable_flags.to_vec());
}
if !cli_config.is_empty() {
self.cli_config = Some(cli_config.iter().map(|s| s.to_string()).collect());
self.merge_cli_args()?;
}
self.load_unstable_flags_from_config()?;
let term = self.get::<TermConfig>("term").unwrap_or_default();
let extra_verbose = verbose >= 2;
let verbose = verbose != 0;
let verbosity = match (verbose, quiet) {
(true, true) => bail!("cannot set both --verbose and --quiet"),
(true, false) => Verbosity::Verbose,
(false, true) => Verbosity::Quiet,
(false, false) => match (term.verbose, term.quiet) {
(Some(true), Some(true)) => {
bail!("cannot set both `term.verbose` and `term.quiet`")
}
(Some(true), _) => Verbosity::Verbose,
(_, Some(true)) => Verbosity::Quiet,
_ => Verbosity::Normal,
},
};
self.shell().set_verbosity(verbosity);
self.extra_verbose = extra_verbose;
let color = color.or_else(|| term.color.as_deref());
self.shell().set_color_choice(color)?;
if let Some(hyperlinks) = term.hyperlinks {
self.shell().set_hyperlinks(hyperlinks)?;
}
if let Some(unicode) = term.unicode {
self.shell().set_unicode(unicode)?;
}
self.progress_config = term.progress.unwrap_or_default();
self.frozen = frozen;
self.locked = locked;
self.offline = offline
|| self
.net_config()
.ok()
.and_then(|n| n.offline)
.unwrap_or(false);
let cli_target_dir = target_dir.as_ref().map(|dir| Filesystem::new(dir.clone()));
self.target_dir = cli_target_dir;
self.shell()
.set_unstable_flags_rustc_unicode(self.unstable_flags.rustc_unicode)?;
Ok(())
}
fn load_unstable_flags_from_config(&mut self) -> CargoResult<()> {
if 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 {
self.unstable_flags.parse(unstable_flags_cli, true)?;
}
}
Ok(())
}
pub fn cli_unstable(&self) -> &CliUnstable {
&self.unstable_flags
}
pub fn extra_verbose(&self) -> bool {
self.extra_verbose
}
pub fn should_embed_metadata(&self) -> bool {
self.cli_unstable().embed_metadata.unwrap_or(true)
}
pub fn network_allowed(&self) -> bool {
!self.offline_flag().is_some()
}
pub fn offline_flag(&self) -> Option<&'static str> {
if self.frozen {
Some("--frozen")
} else if self.offline {
Some("--offline")
} else {
None
}
}
pub fn set_locked(&mut self, locked: bool) {
self.locked = locked;
}
pub fn lock_update_allowed(&self) -> bool {
!self.locked_flag().is_some()
}
pub fn locked_flag(&self) -> Option<&'static str> {
if self.frozen {
Some("--frozen")
} else if self.locked {
Some("--locked")
} else {
None
}
}
pub fn load_values(&self) -> CargoResult<HashMap<String, ConfigValue>> {
self.load_values_from(&self.cwd)
}
pub(crate) fn load_values_unmerged(&self) -> CargoResult<Vec<ConfigValue>> {
let mut result = Vec::new();
let mut seen = HashSet::default();
let home = self.home_path.clone().into_path_unlocked();
self.walk_tree(&self.cwd, &home, |path| {
let mut cv = self._load_file(path, &mut seen, false, WhyLoad::FileDiscovery)?;
self.load_unmerged_include(&mut cv, &mut seen, &mut result)?;
result.push(cv);
Ok(())
})
.context("could not load Cargo configuration")?;
Ok(result)
}
fn load_unmerged_include(
&self,
cv: &mut CV,
seen: &mut HashSet<PathBuf>,
output: &mut Vec<CV>,
) -> CargoResult<()> {
let includes = self.include_paths(cv, false)?;
for include in includes {
let Some(abs_path) = include.resolve_path(self) else {
continue;
};
let mut cv = self
._load_file(&abs_path, seen, false, WhyLoad::FileDiscovery)
.with_context(|| {
format!(
"failed to load config include `{}` from `{}`",
include.path.display(),
include.def
)
})?;
self.load_unmerged_include(&mut cv, seen, output)?;
output.push(cv);
}
Ok(())
}
fn load_values_from(&self, path: &Path) -> CargoResult<HashMap<String, ConfigValue>> {
let mut cfg = CV::Table(HashMap::default(), Definition::BuiltIn);
let home = self.home_path.clone().into_path_unlocked();
self.walk_tree(path, &home, |path| {
let value = self.load_file(path)?;
cfg.merge(value, false).with_context(|| {
format!("failed to merge configuration at `{}`", path.display())
})?;
Ok(())
})
.context("could not load Cargo configuration")?;
match cfg {
CV::Table(map, _) => Ok(map),
_ => unreachable!(),
}
}
fn load_file(&self, path: &Path) -> CargoResult<ConfigValue> {
self._load_file(path, &mut HashSet::default(), true, WhyLoad::FileDiscovery)
}
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)
}
}
fn load_includes(
&self,
mut value: CV,
seen: &mut HashSet<PathBuf>,
why_load: WhyLoad,
) -> CargoResult<CV> {
let includes = self.include_paths(&mut value, true)?;
let mut root = CV::Table(HashMap::default(), value.definition().clone());
for include in includes {
let Some(abs_path) = include.resolve_path(self) else {
continue;
};
self._load_file(&abs_path, seen, true, why_load)
.and_then(|include| root.merge(include, true))
.with_context(|| {
format!(
"failed to load config include `{}` from `{}`",
include.path.display(),
include.def
)
})?;
}
root.merge(value, true)?;
Ok(root)
}
fn include_paths(&self, cv: &mut CV, remove: bool) -> CargoResult<Vec<ConfigInclude>> {
let CV::Table(table, _def) = cv else {
unreachable!()
};
let include = if remove {
table.remove("include").map(Cow::Owned)
} else {
table.get("include").map(Cow::Borrowed)
};
let includes = match include.map(|c| c.into_owned()) {
Some(CV::List(list, _def)) => list
.into_iter()
.enumerate()
.map(|(idx, cv)| match cv {
CV::String(s, def) => Ok(ConfigInclude::new(s, def)),
CV::Table(mut table, def) => {
let s = match table.remove("path") {
Some(CV::String(s, _)) => s,
Some(other) => bail!(
"expected a string, but found {} at `include[{idx}].path` in `{def}`",
other.desc()
),
None => bail!("missing field `path` at `include[{idx}]` in `{def}`"),
};
let optional = match table.remove("optional") {
Some(CV::Boolean(b, _)) => b,
Some(other) => bail!(
"expected a boolean, but found {} at `include[{idx}].optional` in `{def}`",
other.desc()
),
None => false,
};
let mut include = ConfigInclude::new(s, def);
include.optional = optional;
Ok(include)
}
other => bail!(
"expected a string or table, but found {} at `include[{idx}]` in {}",
other.desc(),
other.definition(),
),
})
.collect::<CargoResult<Vec<_>>>()?,
Some(other) => bail!(
"expected a list of strings or a list of tables, but found {} at `include` in `{}",
other.desc(),
other.definition()
),
None => {
return Ok(Vec::new());
}
};
for include in &includes {
if include.path.extension() != Some(OsStr::new("toml")) {
bail!(
"expected a config include path ending with `.toml`, \
but found `{}` from `{}`",
include.path.display(),
include.def,
)
}
if let Some(path) = include.path.to_str() {
if is_glob_pattern(path) {
bail!(
"expected a config include path without glob patterns, \
but found `{}` from `{}`",
include.path.display(),
include.def,
)
}
if path.contains(&['{', '}']) {
bail!(
"expected a config include path without template braces, \
but found `{}` from `{}`",
include.path.display(),
include.def,
)
}
}
}
Ok(includes)
}
pub(crate) fn cli_args_as_table(&self) -> CargoResult<ConfigValue> {
let mut loaded_args = CV::Table(HashMap::default(), Definition::Cli(None));
let Some(cli_args) = &self.cli_config else {
return Ok(loaded_args);
};
let mut seen = HashSet::default();
for arg in cli_args {
let arg_as_path = self.cwd.join(arg);
let tmp_table = if !arg.is_empty() && arg_as_path.exists() {
self._load_file(&arg_as_path, &mut seen, true, WhyLoad::Cli)
.with_context(|| {
format!("failed to load config from `{}`", arg_as_path.display())
})?
} else {
let doc = toml_dotted_keys(arg)?;
let doc: toml::Value = toml::Value::deserialize(doc.into_deserializer())
.with_context(|| {
format!("failed to parse value from --config argument `{arg}`")
})?;
if doc
.get("registry")
.and_then(|v| v.as_table())
.and_then(|t| t.get("token"))
.is_some()
{
bail!("registry.token cannot be set through --config for security reasons");
} else if let Some((k, _)) = doc
.get("registries")
.and_then(|v| v.as_table())
.and_then(|t| t.iter().find(|(_, v)| v.get("token").is_some()))
{
bail!(
"registries.{}.token cannot be set through --config for security reasons",
k
);
}
if doc
.get("registry")
.and_then(|v| v.as_table())
.and_then(|t| t.get("secret-key"))
.is_some()
{
bail!(
"registry.secret-key cannot be set through --config for security reasons"
);
} else if let Some((k, _)) = doc
.get("registries")
.and_then(|v| v.as_table())
.and_then(|t| t.iter().find(|(_, v)| v.get("secret-key").is_some()))
{
bail!(
"registries.{}.secret-key cannot be set through --config for security reasons",
k
);
}
CV::from_toml(Definition::Cli(None), doc)
.with_context(|| format!("failed to convert --config argument `{arg}`"))?
};
let tmp_table = self
.load_includes(tmp_table, &mut HashSet::default(), WhyLoad::Cli)
.context("failed to load --config include".to_string())?;
loaded_args
.merge(tmp_table, true)
.with_context(|| format!("failed to merge --config argument `{arg}`"))?;
}
Ok(loaded_args)
}
fn merge_cli_args(&mut self) -> CargoResult<()> {
let cv_from_cli = self.cli_args_as_table()?;
assert!(cv_from_cli.is_table(), "cv from CLI must be a table");
let root_cv = mem::take(self.values_mut()?);
let mut root_cv = CV::Table(root_cv, Definition::BuiltIn);
root_cv.merge(cv_from_cli, true)?;
mem::swap(self.values_mut()?, root_cv.table_mut("<root>")?.0);
Ok(())
}
fn get_file_path(
&self,
dir: &Path,
filename_without_extension: &str,
warn: bool,
) -> CargoResult<Option<PathBuf>> {
let possible = dir.join(filename_without_extension);
let possible_with_extension = dir.join(format!("{}.toml", filename_without_extension));
if let Ok(possible_handle) = same_file::Handle::from_path(&possible) {
if warn {
if let Ok(possible_with_extension_handle) =
same_file::Handle::from_path(&possible_with_extension)
{
if possible_handle != possible_with_extension_handle {
self.shell().warn(format!(
"both `{}` and `{}` exist. Using `{}`",
possible.display(),
possible_with_extension.display(),
possible.display()
))?;
}
} else {
self.shell().print_report(&[
Level::WARNING.secondary_title(
format!(
"`{}` is deprecated in favor of `{filename_without_extension}.toml`",
possible.display(),
)).element(Level::HELP.message(
format!("if you need to support cargo 1.38 or earlier, you can symlink `{filename_without_extension}` to `{filename_without_extension}.toml`")))
], false)?;
}
}
Ok(Some(possible))
} else if possible_with_extension.exists() {
Ok(Some(possible_with_extension))
} else {
Ok(None)
}
}
fn walk_tree<F>(&self, pwd: &Path, home: &Path, mut walk: F) -> CargoResult<()>
where
F: FnMut(&Path) -> CargoResult<()>,
{
let mut seen_dir = HashSet::default();
for current in paths::ancestors(pwd, self.search_stop_path.as_deref()) {
let config_root = current.join(".cargo");
if let Some(path) = self.get_file_path(&config_root, "config", true)? {
walk(&path)?;
}
let canonical_root = config_root.canonicalize().unwrap_or(config_root);
seen_dir.insert(canonical_root);
}
let canonical_home = home.canonicalize().unwrap_or(home.to_path_buf());
if !seen_dir.contains(&canonical_home) && !seen_dir.contains(home) {
if let Some(path) = self.get_file_path(home, "config", true)? {
walk(&path)?;
}
}
Ok(())
}
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
);
}
}
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> {
let base = index
.definition
.root(self.cwd())
.join("truncated-by-url_with_base");
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)
}
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)?;
{
let (value_map, def) = value.table_mut("<root>")?;
if let Some(token) = value_map.remove("token") {
value_map.entry("registry".into()).or_insert_with(|| {
let map = HashMap::from_iter([("token".into(), token)]);
CV::Table(map, def.clone())
});
}
}
let mut credential_values = HashMap::default();
if let CV::Table(map, _) = value {
let base_map = self.values()?;
for (k, v) in map {
let entry = match base_map.get(&k) {
Some(base_entry) => {
let mut entry = base_entry.clone();
entry.merge(v, true)?;
entry
}
None => v,
};
credential_values.insert(k, entry);
}
}
self.credential_values
.set(credential_values)
.expect("was not filled at beginning of the function");
Ok(())
}
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)),
}
}
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(|| {
let toolchain = self.get_env_os("RUSTUP_TOOLCHAIN")?;
if toolchain.to_str()?.contains(&['/', '\\']) {
return None;
}
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()?;
if tool_meta.len() != rustup_meta.len() {
return None;
}
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 paths_overrides(&self) -> CargoResult<OptValue<Vec<(String, Definition)>>> {
let key = ConfigKey::from_str("paths");
match self.get_cv(&key)? {
Some(CV::List(val, definition)) => {
let val = val
.into_iter()
.map(|cv| match cv {
CV::String(s, def) => Ok((s, def)),
other => self.expected("string", &key, &other),
})
.collect::<CargoResult<Vec<_>>>()?;
Ok(Some(Value { val, definition }))
}
Some(val) => self.expected("list", &key, &val),
None => Ok(None),
}
}
pub fn jobserver_from_env(&self) -> Option<&jobserver::Client> {
self.jobserver
}
pub fn http(&self) -> CargoResult<&Mutex<Easy>> {
let http = self
.easy
.try_borrow_with(|| http_handle(self).map(Into::into))?;
{
let mut http = http.lock().unwrap();
http.reset();
let timeout = configure_http_handle(self, &mut http)?;
timeout.configure(&mut http)?;
}
Ok(http)
}
pub fn http_async(&self) -> CargoResult<&http_async::Client> {
self.http_async.try_borrow_with(|| {
let handle_config = HandleConfiguration::new(&self)?;
Ok(http_async::Client::new(handle_config))
})
}
pub fn http_config(&self) -> CargoResult<&CargoHttpConfig> {
self.http_config.try_borrow_with(|| {
let mut http = self.get::<CargoHttpConfig>("http")?;
let curl_v = curl::Version::get();
disables_multiplexing_for_bad_curl(curl_v.version(), &mut http, self);
Ok(http)
})
}
pub fn future_incompat_config(&self) -> CargoResult<&CargoFutureIncompatConfig> {
self.future_incompat_config
.try_borrow_with(|| self.get::<CargoFutureIncompatConfig>("future-incompat-report"))
}
pub fn net_config(&self) -> CargoResult<&CargoNetConfig> {
self.net_config
.try_borrow_with(|| self.get::<CargoNetConfig>("net"))
}
pub fn build_config(&self) -> CargoResult<&CargoBuildConfig> {
self.build_config
.try_borrow_with(|| self.get::<CargoBuildConfig>("build"))
}
pub fn progress_config(&self) -> &ProgressConfig {
&self.progress_config
}
pub fn env_config(&self) -> CargoResult<&Arc<HashMap<String, OsString>>> {
let env_config = self.env_config.try_borrow_with(|| {
CargoResult::Ok(Arc::new({
let env_config = self.get::<EnvConfig>("env")?;
for disallowed in &["CARGO_HOME", "RUSTUP_HOME", "RUSTUP_TOOLCHAIN"] {
if env_config.contains_key(*disallowed) {
bail!(
"setting the `{disallowed}` environment variable is not supported \
in the `[env]` configuration table"
);
}
}
env_config
.into_iter()
.filter_map(|(k, v)| {
if v.is_force() || self.get_env_os(&k).is_none() {
Some((k, v.resolve(self.cwd()).to_os_string()))
} else {
None
}
})
.collect()
}))
})?;
Ok(env_config)
}
pub fn validate_term_config(&self) -> CargoResult<()> {
drop(self.get::<TermConfig>("term")?);
Ok(())
}
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> {
self.doc_extern_map
.try_borrow_with(|| self.get::<RustdocExternMap>("doc.extern-map"))
}
pub fn target_applies_to_host(&self) -> CargoResult<bool> {
target::get_target_applies_to_host(self)
}
pub fn host_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> {
target::load_host_triple(self, target)
}
pub fn target_cfg_triple(&self, target: &str) -> CargoResult<TargetConfig> {
target::load_target_triple(self, target)
}
pub fn crates_io_source_id(&self) -> CargoResult<SourceId> {
let source_id = self.crates_io_source_id.try_borrow_with(|| {
self.check_registry_index_not_set()?;
let url = CRATES_IO_INDEX.into_url().unwrap();
SourceId::for_alt_registry(&url, CRATES_IO_REGISTRY)
})?;
Ok(*source_id)
}
pub fn invocation_instant(&self) -> Instant {
self.invocation_instant
}
pub fn invocation_time(&self) -> jiff::Timestamp {
self.invocation_time
}
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())
}
#[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
}
#[tracing::instrument(skip_all)]
pub fn acquire_package_cache_lock(&self, mode: CacheLockMode) -> CargoResult<CacheLock<'_>> {
self.package_cache_lock.lock(self, mode)
}
#[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)
}
pub fn global_cache_tracker(&self) -> CargoResult<MutexGuard<'_, GlobalCacheTracker>> {
let tracker = self.global_cache_tracker.try_borrow_with(|| {
Ok::<_, anyhow::Error>(Mutex::new(GlobalCacheTracker::new(self)?))
})?;
Ok(tracker.lock().unwrap())
}
pub fn deferred_global_last_use(&self) -> CargoResult<MutexGuard<'_, DeferredGlobalLastUse>> {
let deferred = self
.deferred_global_last_use
.try_borrow_with(|| Ok::<_, anyhow::Error>(Mutex::new(DeferredGlobalLastUse::new())))?;
Ok(deferred.lock().unwrap())
}
pub fn warning_handling(&self) -> CargoResult<WarningHandling> {
Ok(self.build_config()?.warnings.unwrap_or_default())
}
pub fn ws_roots(&self) -> MutexGuard<'_, HashMap<PathBuf, WorkspaceRootConfig>> {
self.ws_roots.lock().unwrap()
}
}
pub fn homedir(cwd: &Path) -> Option<PathBuf> {
::home::cargo_home_with_cwd(cwd)
.ok()
.map(|home| paths::normalize_path(&home))
}
pub fn save_credentials(
gctx: &GlobalContext,
token: Option<RegistryCredentialConfig>,
registry: &SourceId,
) -> CargoResult<()> {
let registry = if registry.is_crates_io() {
None
} else {
let name = registry
.alt_registry_key()
.ok_or_else(|| internal("can't save credentials for anonymous registry"))?;
Some(name)
};
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)?;
if let Some(token) = toml.remove("token") {
#[expect(
clippy::disallowed_types,
reason = "need stdlib's HashMap because of TOML compatibility"
)]
let map = std::collections::HashMap::from([("token".to_string(), token)]);
toml.insert("registry".into(), map.into());
}
if let Some(token) = token {
let path_def = Definition::Path(file.path().to_path_buf());
let (key, mut value) = match token {
RegistryCredentialConfig::Token(token) => {
let key = "token".to_string();
let value = ConfigValue::String(token.expose(), path_def.clone());
let map = HashMap::from_iter([(key, value)]);
let table = CV::Table(map, path_def.clone());
if let Some(registry) = registry {
let map = HashMap::from_iter([(registry.to_string(), table)]);
("registries".into(), CV::Table(map, path_def.clone()))
} else {
("registry".into(), table)
}
}
RegistryCredentialConfig::AsymmetricKey((secret_key, key_subject)) => {
let key = "secret-key".to_string();
let value = ConfigValue::String(secret_key.expose(), path_def.clone());
let mut map = HashMap::from_iter([(key, value)]);
if let Some(key_subject) = key_subject {
let key = "secret-key-subject".to_string();
let value = ConfigValue::String(key_subject, path_def.clone());
map.insert(key, value);
}
let table = CV::Table(map, path_def.clone());
if let Some(registry) = registry {
let map = HashMap::from_iter([(registry.to_string(), table)]);
("registries".into(), CV::Table(map, path_def.clone()))
} else {
("registry".into(), table)
}
}
_ => unreachable!(),
};
if registry.is_some() {
if let Some(table) = toml.remove("registries") {
let v = CV::from_toml(path_def, table)?;
value.merge(v, false)?;
}
}
toml.insert(key, value.into_toml());
} else {
if let Some(registry) = registry {
if let Some(registries) = toml.get_mut("registries") {
if let Some(reg) = registries.get_mut(registry) {
let rtable = reg.as_table_mut().ok_or_else(|| {
format_err!("expected `[registries.{}]` to be a table", registry)
})?;
rtable.remove("token");
rtable.remove("secret-key");
rtable.remove("secret-key-subject");
}
}
} else if let Some(registry) = toml.get_mut("registry") {
let reg_table = registry
.as_table_mut()
.ok_or_else(|| format_err!("expected `[registry]` to be a table"))?;
reg_table.remove("token");
reg_table.remove("secret-key");
reg_table.remove("secret-key-subject");
}
}
let contents = toml.to_string();
file.seek(SeekFrom::Start(0))?;
file.write_all(contents.as_bytes())
.with_context(|| format!("failed to write to `{}`", file.path().display()))?;
file.file().set_len(contents.len() as u64)?;
set_permissions(file.file(), 0o600)
.with_context(|| format!("failed to set permissions of `{}`", file.path().display()))?;
return Ok(());
#[cfg(unix)]
fn set_permissions(file: &File, mode: u32) -> CargoResult<()> {
use std::os::unix::fs::PermissionsExt;
let mut perms = file.metadata()?.permissions();
perms.set_mode(mode);
file.set_permissions(perms)?;
Ok(())
}
#[cfg(not(unix))]
fn set_permissions(_file: &File, _mode: u32) -> CargoResult<()> {
Ok(())
}
}
struct ConfigInclude {
path: PathBuf,
def: Definition,
optional: bool,
}
impl ConfigInclude {
fn new(p: impl Into<PathBuf>, def: Definition) -> Self {
Self {
path: p.into(),
def,
optional: false,
}
}
fn resolve_path(&self, gctx: &GlobalContext) -> Option<PathBuf> {
let abs_path = match &self.def {
Definition::Path(p) | Definition::Cli(Some(p)) => p.parent().unwrap(),
Definition::Environment(_) | Definition::Cli(None) | Definition::BuiltIn => gctx.cwd(),
}
.join(&self.path);
let abs_path = paths::normalize_path(&abs_path);
if self.optional && !abs_path.exists() {
tracing::info!(
"skipping optional include `{}` in `{}`: file not found at `{}`",
self.path.display(),
self.def,
abs_path.display(),
);
None
} else {
Some(abs_path)
}
}
}
fn parse_document(toml: &str, _file: &Path, _gctx: &GlobalContext) -> CargoResult<toml::Table> {
toml.parse().map_err(Into::into)
}
fn toml_dotted_keys(arg: &str) -> CargoResult<toml_edit::DocumentMut> {
let doc: toml_edit::DocumentMut = arg.parse().with_context(|| {
format!("failed to parse value from --config argument `{arg}` as a dotted key expression")
})?;
fn non_empty(d: Option<&toml_edit::RawString>) -> bool {
d.map_or(false, |p| !p.as_str().unwrap_or_default().trim().is_empty())
}
fn non_empty_decor(d: &toml_edit::Decor) -> bool {
non_empty(d.prefix()) || non_empty(d.suffix())
}
fn non_empty_key_decor(k: &toml_edit::Key) -> bool {
non_empty_decor(k.leaf_decor()) || non_empty_decor(k.dotted_decor())
}
let ok = {
let mut got_to_value = false;
let mut table = doc.as_table();
let mut is_root = true;
while table.is_dotted() || is_root {
is_root = false;
if table.len() != 1 {
break;
}
let (k, n) = table.iter().next().expect("len() == 1 above");
match n {
Item::Table(nt) => {
if table.key(k).map_or(false, non_empty_key_decor)
|| non_empty_decor(nt.decor())
{
bail!(
"--config argument `{arg}` \
includes non-whitespace decoration"
)
}
table = nt;
}
Item::Value(v) if v.is_inline_table() => {
bail!(
"--config argument `{arg}` \
sets a value to an inline table, which is not accepted"
);
}
Item::Value(v) => {
if table
.key(k)
.map_or(false, |k| non_empty(k.leaf_decor().prefix()))
|| non_empty_decor(v.decor())
{
bail!(
"--config argument `{arg}` \
includes non-whitespace decoration"
)
}
got_to_value = true;
break;
}
Item::ArrayOfTables(_) => {
bail!(
"--config argument `{arg}` \
sets a value to an array of tables, which is not accepted"
);
}
Item::None => {
bail!("--config argument `{arg}` doesn't provide a value")
}
}
}
got_to_value
};
if !ok {
bail!(
"--config argument `{arg}` was not a TOML dotted key expression (such as `build.jobs = 2`)"
);
}
Ok(doc)
}
#[derive(Debug, Deserialize, Clone)]
pub struct StringList(Vec<String>);
impl StringList {
pub fn as_slice(&self) -> &[String] {
&self.0
}
}
#[macro_export]
macro_rules! __shell_print {
($config:expr, $which:ident, $newline:literal, $($arg:tt)*) => ({
let mut shell = $config.shell();
let out = shell.$which();
drop(out.write_fmt(format_args!($($arg)*)));
if $newline {
drop(out.write_all(b"\n"));
}
});
}
#[macro_export]
macro_rules! drop_println {
($config:expr) => ( $crate::drop_print!($config, "\n") );
($config:expr, $($arg:tt)*) => (
$crate::__shell_print!($config, out, true, $($arg)*)
);
}
#[macro_export]
macro_rules! drop_eprintln {
($config:expr) => ( $crate::drop_eprint!($config, "\n") );
($config:expr, $($arg:tt)*) => (
$crate::__shell_print!($config, err, true, $($arg)*)
);
}
#[macro_export]
macro_rules! drop_print {
($config:expr, $($arg:tt)*) => (
$crate::__shell_print!($config, out, false, $($arg)*)
);
}
#[macro_export]
macro_rules! drop_eprint {
($config:expr, $($arg:tt)*) => (
$crate::__shell_print!($config, err, false, $($arg)*)
);
}
enum Tool {
Rustc,
Rustdoc,
}
impl Tool {
fn as_str(&self) -> &str {
match self {
Tool::Rustc => "rustc",
Tool::Rustdoc => "rustdoc",
}
}
}
fn disables_multiplexing_for_bad_curl(
curl_version: &str,
http: &mut CargoHttpConfig,
gctx: &GlobalContext,
) {
use crate::util::network;
if network::proxy::http_proxy_exists(http, gctx) && http.multiplexing.is_none() {
let bad_curl_versions = ["7.87.0", "7.88.0", "7.88.1"];
if bad_curl_versions
.iter()
.any(|v| curl_version.starts_with(v))
{
tracing::info!("disabling multiplexing with proxy, curl version is {curl_version}");
http.multiplexing = Some(false);
}
}
}
#[cfg(test)]
mod tests {
use super::CargoHttpConfig;
use super::GlobalContext;
use super::Shell;
use super::disables_multiplexing_for_bad_curl;
#[test]
fn disables_multiplexing() {
let mut gctx = GlobalContext::new(Shell::new(), "".into(), "".into());
gctx.set_search_stop_path(std::path::PathBuf::new());
gctx.set_env(Default::default());
let mut http = CargoHttpConfig::default();
http.proxy = Some("127.0.0.1:3128".into());
disables_multiplexing_for_bad_curl("7.88.1", &mut http, &gctx);
assert_eq!(http.multiplexing, Some(false));
let cases = [
(None, None, "7.87.0", None),
(None, None, "7.88.0", None),
(None, None, "7.88.1", None),
(None, None, "8.0.0", None),
(Some("".into()), None, "7.87.0", Some(false)),
(Some("".into()), None, "7.88.0", Some(false)),
(Some("".into()), None, "7.88.1", Some(false)),
(Some("".into()), None, "8.0.0", None),
(Some("".into()), Some(false), "7.87.0", Some(false)),
(Some("".into()), Some(false), "7.88.0", Some(false)),
(Some("".into()), Some(false), "7.88.1", Some(false)),
(Some("".into()), Some(false), "8.0.0", Some(false)),
];
for (proxy, multiplexing, curl_v, result) in cases {
let mut http = CargoHttpConfig {
multiplexing,
proxy,
..Default::default()
};
disables_multiplexing_for_bad_curl(curl_v, &mut http, &gctx);
assert_eq!(http.multiplexing, result);
}
}
#[test]
fn sync_context() {
fn assert_sync<S: Sync>() {}
assert_sync::<GlobalContext>();
}
}