feat: Add lint for global use of `hint-mostly-unused` (#15995)
This PR adds a lint that checks for global use of `hint-mostly-unused`.
This also adds a "workspace lint pass", since we only want to lint on
the workspace's `[profile]`, as all other `[profile]` are ignored. This
new pass uses `[workspace.lints.cargo]` for configuration if
`[workspace]` exists, and `[lints]` if it doesn't[^1].
Note: This lint will get emitted if `-Zprofile-hint-mostly-unused` is
set, regardless of whether `-Zcargo-lints` is set.
CC @joshtriplett
[^1]: We can change the behavior in the future if it is not correct
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index 30f301c..3845469 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -103,7 +103,7 @@
- name: Install cargo-semver-checks
run: |
mkdir installed-bins
- curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.43.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \
+ curl -Lf https://github.com/obi1kenobi/cargo-semver-checks/releases/download/v0.44.0/cargo-semver-checks-x86_64-unknown-linux-gnu.tar.gz \
| tar -xz --directory=./installed-bins
echo `pwd`/installed-bins >> $GITHUB_PATH
- run: ci/validate-version-bump.sh
diff --git a/Cargo.toml b/Cargo.toml
index d7e1cf0..132360d 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@
]
[workspace.package]
-rust-version = "1.87" # MSRV:3
+rust-version = "1.88" # MSRV:3
edition = "2024"
license = "MIT OR Apache-2.0"
homepage = "https://github.com/rust-lang/cargo"
diff --git a/crates/cargo-util-schemas/lockfile.schema.json b/crates/cargo-util-schemas/lockfile.schema.json
index 5d7149d..5c54abe 100644
--- a/crates/cargo-util-schemas/lockfile.schema.json
+++ b/crates/cargo-util-schemas/lockfile.schema.json
@@ -1,10 +1,11 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"title": "TomlLockfile",
- "description": "The `Cargo.lock` structure.",
+ "description": "Serialization of `Cargo.lock`",
"type": "object",
"properties": {
"version": {
+ "description": "The lockfile format version (`version =` field).\n\nThis field is optional for backward compatibility. Older lockfiles, i.e. V1 and V2, does\nnot have the version field serialized.",
"type": [
"integer",
"null"
@@ -13,6 +14,7 @@
"minimum": 0
},
"package": {
+ "description": "The list of `[[package]]` entries describing each resolved dependency.",
"type": [
"array",
"null"
@@ -22,7 +24,7 @@
}
},
"root": {
- "description": "`root` is optional to allow backward compatibility.",
+ "description": "The `[root]` table describing the root package.\n\nThis field is optional for backward compatibility. Older lockfiles have the root package\nseparated, whereas newer lockfiles have the root package as part of `[[package]]`.",
"anyOf": [
{
"$ref": "#/$defs/TomlLockfileDependency"
@@ -33,6 +35,7 @@
]
},
"metadata": {
+ "description": "The `[metadata]` table\n\n\nIn older lockfile versions, dependency checksums were stored here instead of alongside each\npackage entry.",
"type": [
"object",
"null"
@@ -42,32 +45,43 @@
}
},
"patch": {
+ "description": "The `[patch]` table describing unused patches.\n\nThe lockfile stores them as a list of `[[patch.unused]]` entries.",
"$ref": "#/$defs/TomlLockfilePatch"
}
},
"$defs": {
"TomlLockfileDependency": {
+ "description": "Serialization of lockfiles dependencies",
"type": "object",
"properties": {
"name": {
+ "description": "The name of the dependency.",
"type": "string"
},
"version": {
+ "description": "The version of the dependency.",
"type": "string"
},
"source": {
- "type": [
- "string",
- "null"
+ "description": "The source of the dependency.\n\nCargo does not serialize path dependencies.",
+ "anyOf": [
+ {
+ "$ref": "#/$defs/TomlLockfileSourceId"
+ },
+ {
+ "type": "null"
+ }
]
},
"checksum": {
+ "description": "The checksum of the dependency.\n\nIn older lockfiles, checksums were not stored here and instead on a separate `[metadata]`\ntable (see [`TomlLockfileMetadata`]).",
"type": [
"string",
"null"
]
},
"dependencies": {
+ "description": "The transitive dependencies used by this dependency.",
"type": [
"array",
"null"
@@ -77,6 +91,7 @@
}
},
"replace": {
+ "description": "The replace of the dependency.",
"anyOf": [
{
"$ref": "#/$defs/TomlLockfilePackageId"
@@ -92,7 +107,12 @@
"version"
]
},
+ "TomlLockfileSourceId": {
+ "description": "Serialization of dependency's source",
+ "type": "string"
+ },
"TomlLockfilePackageId": {
+ "description": "Serialization of package IDs.\n\nThe version and source are only included when necessary to disambiguate between packages:\n- If multiple packages share the same name, the version is included.\n- If multiple packages share the same name and version, the source is included.",
"type": "object",
"properties": {
"name": {
@@ -105,9 +125,13 @@
]
},
"source": {
- "type": [
- "string",
- "null"
+ "anyOf": [
+ {
+ "$ref": "#/$defs/TomlLockfileSourceId"
+ },
+ {
+ "type": "null"
+ }
]
}
},
@@ -116,9 +140,11 @@
]
},
"TomlLockfilePatch": {
+ "description": "Serialization of unused patches\n\nCargo stores patches that were declared but not used during resolution.",
"type": "object",
"properties": {
"unused": {
+ "description": "The list of unused dependency patches.",
"type": "array",
"items": {
"$ref": "#/$defs/TomlLockfileDependency"
diff --git a/crates/cargo-util-schemas/src/lockfile.rs b/crates/cargo-util-schemas/src/lockfile.rs
index 8e949d0..c953c79 100644
--- a/crates/cargo-util-schemas/src/lockfile.rs
+++ b/crates/cargo-util-schemas/src/lockfile.rs
@@ -1,3 +1,5 @@
+//! `Cargo.lock` / Lockfile schema definition
+
use std::collections::BTreeMap;
use std::fmt;
use std::{cmp::Ordering, str::FromStr};
@@ -7,44 +9,80 @@
use crate::core::{GitReference, SourceKind};
-/// The `Cargo.lock` structure.
+/// Serialization of `Cargo.lock`
#[derive(Serialize, Deserialize, Debug)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
pub struct TomlLockfile {
+ /// The lockfile format version (`version =` field).
+ ///
+ /// This field is optional for backward compatibility. Older lockfiles, i.e. V1 and V2, does
+ /// not have the version field serialized.
pub version: Option<u32>,
+ /// The list of `[[package]]` entries describing each resolved dependency.
pub package: Option<Vec<TomlLockfileDependency>>,
- /// `root` is optional to allow backward compatibility.
+ /// The `[root]` table describing the root package.
+ ///
+ /// This field is optional for backward compatibility. Older lockfiles have the root package
+ /// separated, whereas newer lockfiles have the root package as part of `[[package]]`.
pub root: Option<TomlLockfileDependency>,
+ /// The `[metadata]` table
+ ///
+ ///
+ /// In older lockfile versions, dependency checksums were stored here instead of alongside each
+ /// package entry.
pub metadata: Option<TomlLockfileMetadata>,
+ /// The `[patch]` table describing unused patches.
+ ///
+ /// The lockfile stores them as a list of `[[patch.unused]]` entries.
#[serde(default, skip_serializing_if = "TomlLockfilePatch::is_empty")]
pub patch: TomlLockfilePatch,
}
+/// Serialization of lockfiles metadata
+///
+/// Older versions of lockfiles have their dependencies' checksums on this `[metadata]` table.
+pub type TomlLockfileMetadata = BTreeMap<String, String>;
+
+/// Serialization of unused patches
+///
+/// Cargo stores patches that were declared but not used during resolution.
#[derive(Serialize, Deserialize, Debug, Default)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
pub struct TomlLockfilePatch {
+ /// The list of unused dependency patches.
pub unused: Vec<TomlLockfileDependency>,
}
-pub type TomlLockfileMetadata = BTreeMap<String, String>;
-
impl TomlLockfilePatch {
fn is_empty(&self) -> bool {
self.unused.is_empty()
}
}
+/// Serialization of lockfiles dependencies
#[derive(Serialize, Deserialize, Debug, PartialOrd, Ord, PartialEq, Eq)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
pub struct TomlLockfileDependency {
+ /// The name of the dependency.
pub name: String,
+ /// The version of the dependency.
pub version: String,
+ /// The source of the dependency.
+ ///
+ /// Cargo does not serialize path dependencies.
pub source: Option<TomlLockfileSourceId>,
+ /// The checksum of the dependency.
+ ///
+ /// In older lockfiles, checksums were not stored here and instead on a separate `[metadata]`
+ /// table (see [`TomlLockfileMetadata`]).
pub checksum: Option<String>,
+ /// The transitive dependencies used by this dependency.
pub dependencies: Option<Vec<TomlLockfilePackageId>>,
+ /// The replace of the dependency.
pub replace: Option<TomlLockfilePackageId>,
}
+/// Serialization of dependency's source
#[derive(Debug, Clone)]
#[cfg_attr(
feature = "unstable-schema",
@@ -52,11 +90,15 @@
schemars(with = "String")
)]
pub struct TomlLockfileSourceId {
- /// Full string of the source
+ /// The string representation of the source as it appears in the lockfile.
source_str: String,
- /// Used for sources ordering
+ /// The parsed source type, e.g. `git`, `registry`.
+ ///
+ /// Used for sources ordering.
kind: SourceKind,
- /// Used for sources ordering
+ /// The parsed URL of the source.
+ ///
+ /// Used for sources ordering.
url: Url,
}
@@ -157,6 +199,11 @@
}
}
+/// Serialization of package IDs.
+///
+/// The version and source are only included when necessary to disambiguate between packages:
+/// - If multiple packages share the same name, the version is included.
+/// - If multiple packages share the same name and version, the source is included.
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Hash, Clone)]
#[cfg_attr(feature = "unstable-schema", derive(schemars::JsonSchema))]
pub struct TomlLockfilePackageId {
diff --git a/src/cargo/core/registry.rs b/src/cargo/core/registry.rs
index d116c1e..9bc4d82 100644
--- a/src/cargo/core/registry.rs
+++ b/src/cargo/core/registry.rs
@@ -800,11 +800,9 @@
#[tracing::instrument(skip_all)]
fn block_until_ready(&mut self) -> CargoResult<()> {
- if cfg!(debug_assertions) {
- // Force borrow to catch invalid borrows, regardless of which source is used and how it
- // happens to behave this time
- self.gctx.shell().verbosity();
- }
+ // Ensure `shell` is not already in use,
+ // regardless of which source is used and how it happens to behave this time
+ self.gctx.debug_assert_shell_not_borrowed();
for (source_id, source) in self.sources.sources_mut() {
source
.block_until_ready()
diff --git a/src/cargo/core/shell.rs b/src/cargo/core/shell.rs
index 8ac6e6f..ed1bbf7 100644
--- a/src/cargo/core/shell.rs
+++ b/src/cargo/core/shell.rs
@@ -65,7 +65,7 @@
}
/// Creates a shell from a plain writable object, with no color, and max verbosity.
- pub fn from_write(out: Box<dyn Write>) -> Shell {
+ pub fn from_write(out: Box<dyn Write + Send + Sync>) -> Shell {
Shell {
output: ShellOut::Write(AutoStream::never(out)), // strip all formatting on write
verbosity: Verbosity::Verbose,
@@ -432,7 +432,7 @@
/// A `Write`able object, either with or without color support
enum ShellOut {
/// A plain write object without color support
- Write(AutoStream<Box<dyn Write>>),
+ Write(AutoStream<Box<dyn Write + Send + Sync>>),
/// Color-enabled stdio, with information on whether color should be used
Stream {
stdout: AutoStream<std::io::Stdout>,
diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs
index f2e8397..fc20aa2 100644
--- a/src/cargo/core/workspace.rs
+++ b/src/cargo/core/workspace.rs
@@ -2119,7 +2119,7 @@
) -> CargoResult<Option<PathBuf>> {
// Check if there are any workspace roots that have already been found that would work
{
- let roots = gctx.ws_roots.borrow();
+ let roots = gctx.ws_roots();
// Iterate through the manifests parent directories until we find a workspace
// root. Note we skip the first item since that is just the path itself
for current in manifest_path.ancestors().skip(1) {
diff --git a/src/cargo/sources/git/utils.rs b/src/cargo/sources/git/utils.rs
index e0346aa..e9224df 100644
--- a/src/cargo/sources/git/utils.rs
+++ b/src/cargo/sources/git/utils.rs
@@ -1528,7 +1528,7 @@
"https://api.github.com/repos/{}/{}/commits/{}",
username, repository, github_branch_name,
);
- let mut handle = gctx.http()?.borrow_mut();
+ let mut handle = gctx.http()?.lock().unwrap();
debug!("attempting GitHub fast path for {}", url);
handle.get(true)?;
handle.url(&url)?;
diff --git a/src/cargo/util/cache_lock.rs b/src/cargo/util/cache_lock.rs
index afd06c9..068e1bb 100644
--- a/src/cargo/util/cache_lock.rs
+++ b/src/cargo/util/cache_lock.rs
@@ -91,8 +91,8 @@
use crate::CargoResult;
use crate::GlobalContext;
use anyhow::Context as _;
-use std::cell::RefCell;
use std::io;
+use std::sync::Mutex;
/// The style of lock to acquire.
#[derive(Copy, Clone, Debug, PartialEq)]
@@ -435,7 +435,11 @@
impl Drop for CacheLock<'_> {
fn drop(&mut self) {
use CacheLockMode::*;
- let mut state = self.locker.state.borrow_mut();
+ let mut state = match self.locker.state.lock() {
+ Ok(result) => result,
+ // we should release the cache even if a thread panicked while holding a lock
+ Err(poison) => poison.into_inner(),
+ };
match self.mode {
Shared => {
state.mutate_lock.decrement();
@@ -472,24 +476,25 @@
///
/// [`CacheLocker`] uses interior mutability because it is stuffed inside
/// [`GlobalContext`], which does not allow mutation.
- state: RefCell<CacheState>,
+ state: Mutex<CacheState>,
}
impl CacheLocker {
/// Creates a new `CacheLocker`.
pub fn new() -> CacheLocker {
CacheLocker {
- state: RefCell::new(CacheState {
+ state: CacheState {
cache_lock: RecursiveLock::new(CACHE_LOCK_NAME),
mutate_lock: RecursiveLock::new(MUTATE_NAME),
- }),
+ }
+ .into(),
}
}
/// Acquires a lock with the given mode, possibly blocking if another
/// cargo is holding the lock.
pub fn lock(&self, gctx: &GlobalContext, mode: CacheLockMode) -> CargoResult<CacheLock<'_>> {
- let mut state = self.state.borrow_mut();
+ let mut state = self.state.lock().unwrap();
let _ = state.lock(gctx, mode, Blocking)?;
Ok(CacheLock { mode, locker: self })
}
@@ -501,7 +506,7 @@
gctx: &GlobalContext,
mode: CacheLockMode,
) -> CargoResult<Option<CacheLock<'_>>> {
- let mut state = self.state.borrow_mut();
+ let mut state = self.state.lock().unwrap();
if state.lock(gctx, mode, NonBlocking)? == LockAcquired {
Ok(Some(CacheLock { mode, locker: self }))
} else {
@@ -519,7 +524,7 @@
/// `DownloadExclusive` will return true if a `MutateExclusive` lock is
/// held since they overlap.
pub fn is_locked(&self, mode: CacheLockMode) -> bool {
- let state = self.state.borrow();
+ let state = self.state.lock().unwrap();
match (
mode,
state.cache_lock.count,
diff --git a/src/cargo/util/context/mod.rs b/src/cargo/util/context/mod.rs
index 62b0411..01ba990 100644
--- a/src/cargo/util/context/mod.rs
+++ b/src/cargo/util/context/mod.rs
@@ -51,7 +51,6 @@
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;
@@ -63,7 +62,7 @@
use std::mem;
use std::path::{Path, PathBuf};
use std::str::FromStr;
-use std::sync::{Arc, Once};
+use std::sync::{Arc, Mutex, MutexGuard, Once, OnceLock};
use std::time::Instant;
use self::ConfigValue as CV;
@@ -74,6 +73,7 @@
use crate::ops::RegistryCredentialConfig;
use crate::sources::CRATES_IO_INDEX;
use crate::sources::CRATES_IO_REGISTRY;
+use crate::util::OnceExt as _;
use crate::util::errors::CargoResult;
use crate::util::network::http::configure_http_handle;
use crate::util::network::http::http_handle;
@@ -85,7 +85,6 @@
use cargo_util_schemas::manifest::RegistryName;
use curl::easy::Easy;
use itertools::Itertools;
-use lazycell::LazyCell;
use serde::Deserialize;
use serde::de::IntoDeserializer as _;
use serde_untagged::UntaggedEnumVisitor;
@@ -166,11 +165,11 @@
/// 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>,
+ shell: Mutex<Shell>,
/// A collection of configuration options
- values: LazyCell<HashMap<String, ConfigValue>>,
+ values: OnceLock<HashMap<String, ConfigValue>>,
/// A collection of configuration options from the credentials file
- credential_values: LazyCell<HashMap<String, ConfigValue>>,
+ credential_values: OnceLock<HashMap<String, ConfigValue>>,
/// CLI config values, passed in via `configure`.
cli_config: Option<Vec<String>>,
/// The current working directory of cargo
@@ -178,9 +177,9 @@
/// 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>,
+ cargo_exe: OnceLock<PathBuf>,
/// The location of the rustdoc executable
- rustdoc: LazyCell<PathBuf>,
+ rustdoc: OnceLock<PathBuf>,
/// Whether we are printing extra verbose messages
extra_verbose: bool,
/// `frozen` is the same as `locked`, but additionally will not access the
@@ -199,9 +198,9 @@
/// 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>>,
+ easy: OnceLock<Mutex<Easy>>,
/// Cache of the `SourceId` for crates.io
- crates_io_source_id: LazyCell<SourceId>,
+ crates_io_source_id: OnceLock<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
@@ -211,23 +210,23 @@
/// Environment variable snapshot.
env: Env,
/// Tracks which sources have been updated to avoid multiple updates.
- updated_sources: LazyCell<RefCell<HashSet<SourceId>>>,
+ updated_sources: Mutex<HashSet<SourceId>>,
/// Cache of credentials from configuration or credential providers.
/// Maps from url to credential value.
- credential_cache: LazyCell<RefCell<HashMap<CanonicalUrl, CredentialCacheValue>>>,
+ credential_cache: Mutex<HashMap<CanonicalUrl, CredentialCacheValue>>,
/// Cache of registry config from the `[registries]` table.
- registry_config: LazyCell<RefCell<HashMap<SourceId, Option<RegistryConfig>>>>,
+ registry_config: Mutex<HashMap<SourceId, Option<RegistryConfig>>>,
/// Locks on the package and index caches.
package_cache_lock: CacheLocker,
/// Cached configuration parsed by Cargo
- http_config: 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>,
+ http_config: OnceLock<CargoHttpConfig>,
+ 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: LazyCell<Arc<HashMap<String, OsString>>>,
+ env_config: OnceLock<Arc<HashMap<String, OsString>>>,
/// This should be false if:
/// - this is an artifact of the rustc distribution process for "stable" or for "beta"
/// - this is an `#[test]` that does not opt in with `enable_nightly_features`
@@ -245,12 +244,12 @@
/// consider using `ConfigBuilder::enable_nightly_features` instead.
pub nightly_features_allowed: bool,
/// `WorkspaceRootConfigs` that have been found
- pub ws_roots: RefCell<HashMap<PathBuf, WorkspaceRootConfig>>,
+ ws_roots: Mutex<HashMap<PathBuf, WorkspaceRootConfig>>,
/// The global cache tracker is a database used to track disk cache usage.
- global_cache_tracker: LazyCell<RefCell<GlobalCacheTracker>>,
+ global_cache_tracker: OnceLock<Mutex<GlobalCacheTracker>>,
/// A cache of modifications to make to [`GlobalContext::global_cache_tracker`],
/// saved to disk in a batch to improve performance.
- deferred_global_last_use: LazyCell<RefCell<DeferredGlobalLastUse>>,
+ deferred_global_last_use: OnceLock<Mutex<DeferredGlobalLastUse>>,
}
impl GlobalContext {
@@ -283,14 +282,14 @@
GlobalContext {
home_path: Filesystem::new(homedir),
- shell: RefCell::new(shell),
+ shell: Mutex::new(shell),
cwd,
search_stop_path: None,
- values: LazyCell::new(),
- credential_values: LazyCell::new(),
+ values: Default::default(),
+ credential_values: Default::default(),
cli_config: None,
- cargo_exe: LazyCell::new(),
- rustdoc: LazyCell::new(),
+ cargo_exe: Default::default(),
+ rustdoc: Default::default(),
extra_verbose: false,
frozen: false,
locked: false,
@@ -304,28 +303,28 @@
},
unstable_flags: CliUnstable::default(),
unstable_flags_cli: None,
- easy: LazyCell::new(),
- crates_io_source_id: LazyCell::new(),
+ easy: Default::default(),
+ crates_io_source_id: Default::default(),
cache_rustc_info,
creation_time: Instant::now(),
target_dir: None,
env,
- updated_sources: LazyCell::new(),
- credential_cache: LazyCell::new(),
- registry_config: LazyCell::new(),
+ updated_sources: Default::default(),
+ credential_cache: Default::default(),
+ registry_config: Default::default(),
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(),
+ http_config: 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: LazyCell::new(),
+ env_config: Default::default(),
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(),
+ ws_roots: Default::default(),
+ global_cache_tracker: Default::default(),
+ deferred_global_last_use: Default::default(),
}
}
@@ -408,8 +407,22 @@
}
/// Gets a reference to the shell, e.g., for writing error messages.
- pub fn shell(&self) -> RefMut<'_, Shell> {
- self.shell.borrow_mut()
+ pub fn shell(&self) -> MutexGuard<'_, Shell> {
+ self.shell.lock().unwrap()
+ }
+
+ /// Assert [`Self::shell`] is not in use
+ ///
+ /// Testing might not identify bugs with two accesses to `shell` at once
+ /// due to conditional logic,
+ /// so place this outside of the conditions to catch these bugs in more situations.
+ pub fn debug_assert_shell_not_borrowed(&self) {
+ if cfg!(debug_assertions) {
+ match self.shell.try_lock() {
+ Ok(_) | Err(std::sync::TryLockError::Poisoned(_)) => (),
+ Err(std::sync::TryLockError::WouldBlock) => panic!("shell is borrowed!"),
+ }
+ }
}
/// Gets the path to the `rustdoc` executable.
@@ -513,24 +526,20 @@
}
/// 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()
+ pub fn updated_sources(&self) -> MutexGuard<'_, HashSet<SourceId>> {
+ self.updated_sources.lock().unwrap()
}
/// Cached credentials from credential providers or configuration.
- pub fn credential_cache(&self) -> RefMut<'_, HashMap<CanonicalUrl, CredentialCacheValue>> {
- self.credential_cache
- .borrow_with(|| RefCell::new(HashMap::new()))
- .borrow_mut()
+ pub fn credential_cache(&self) -> MutexGuard<'_, HashMap<CanonicalUrl, CredentialCacheValue>> {
+ self.credential_cache.lock().unwrap()
}
/// Cache of already parsed registries from the `[registries]` table.
- pub(crate) fn registry_config(&self) -> RefMut<'_, HashMap<SourceId, Option<RegistryConfig>>> {
- self.registry_config
- .borrow_with(|| RefCell::new(HashMap::new()))
- .borrow_mut()
+ pub(crate) fn registry_config(
+ &self,
+ ) -> MutexGuard<'_, HashMap<SourceId, Option<RegistryConfig>>> {
+ self.registry_config.lock().unwrap()
}
/// Gets all config values from disk.
@@ -550,18 +559,15 @@
/// 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"))
+ Ok(self.values.get_mut().expect("already loaded config values"))
}
// Note: this is used by RLS, not Cargo.
pub fn set_values(&self, values: HashMap<String, ConfigValue>) -> CargoResult<()> {
- if self.values.borrow().is_some() {
+ if self.values.get().is_some() {
bail!("config values already found")
}
- match self.values.fill(values) {
+ match self.values.set(values.into()) {
Ok(()) => Ok(()),
Err(_) => bail!("could not fill values"),
}
@@ -730,13 +736,13 @@
/// 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() {
+ 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()?)
+ self.get_cv_helper(key, &*self.values()?)
}
fn get_cv_helper(
@@ -1791,7 +1797,7 @@
}
}
self.credential_values
- .fill(credential_values)
+ .set(credential_values)
.expect("was not filled at beginning of the function");
Ok(())
}
@@ -1883,12 +1889,12 @@
self.jobserver.as_ref()
}
- pub fn http(&self) -> CargoResult<&RefCell<Easy>> {
+ pub fn http(&self) -> CargoResult<&Mutex<Easy>> {
let http = self
.easy
- .try_borrow_with(|| http_handle(self).map(RefCell::new))?;
+ .try_borrow_with(|| http_handle(self).map(Into::into))?;
{
- let mut http = http.borrow_mut();
+ let mut http = http.lock().unwrap();
http.reset();
let timeout = configure_http_handle(self, &mut http)?;
timeout.configure(&mut http)?;
@@ -2099,19 +2105,19 @@
///
/// 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>> {
+ pub fn global_cache_tracker(&self) -> CargoResult<MutexGuard<'_, GlobalCacheTracker>> {
let tracker = self.global_cache_tracker.try_borrow_with(|| {
- Ok::<_, anyhow::Error>(RefCell::new(GlobalCacheTracker::new(self)?))
+ Ok::<_, anyhow::Error>(Mutex::new(GlobalCacheTracker::new(self)?))
})?;
- Ok(tracker.borrow_mut())
+ Ok(tracker.lock().unwrap())
}
/// 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())
+ pub fn deferred_global_last_use(&self) -> CargoResult<MutexGuard<'_, DeferredGlobalLastUse>> {
+ let deferred = self
+ .deferred_global_last_use
+ .try_borrow_with(|| Ok::<_, anyhow::Error>(Mutex::new(DeferredGlobalLastUse::new())))?;
+ Ok(deferred.lock().unwrap())
}
/// Get the global [`WarningHandling`] configuration.
@@ -2122,6 +2128,10 @@
Ok(WarningHandling::default())
}
}
+
+ pub fn ws_roots(&self) -> MutexGuard<'_, HashMap<PathBuf, WorkspaceRootConfig>> {
+ self.ws_roots.lock().unwrap()
+ }
}
/// Internal error for serde errors.
@@ -3192,4 +3202,10 @@
assert_eq!(http.multiplexing, result);
}
}
+
+ #[test]
+ fn sync_context() {
+ fn assert_sync<S: Sync>() {}
+ assert_sync::<GlobalContext>();
+ }
}
diff --git a/src/cargo/util/flock.rs b/src/cargo/util/flock.rs
index 9c929de..e82465e 100644
--- a/src/cargo/util/flock.rs
+++ b/src/cargo/util/flock.rs
@@ -389,10 +389,9 @@
lock_try: &dyn Fn() -> Result<(), TryLockError>,
lock_block: &dyn Fn() -> io::Result<()>,
) -> CargoResult<()> {
- if cfg!(debug_assertions) {
- // Force borrow to catch invalid borrows outside of contention situations
- gctx.shell().verbosity();
- }
+ // Ensure `shell` is not already in use,
+ // regardless of whether we hit contention or not
+ gctx.debug_assert_shell_not_borrowed();
if try_acquire(path, lock_try)? {
return Ok(());
}
diff --git a/src/cargo/util/frontmatter.rs b/src/cargo/util/frontmatter.rs
index 7eeb6ce..6d50878 100644
--- a/src/cargo/util/frontmatter.rs
+++ b/src/cargo/util/frontmatter.rs
@@ -86,7 +86,8 @@
.push_visible_span(open_start..open_end));
};
let info = input.next_slice(info_nl.start);
- let info = info.trim_matches(is_whitespace);
+ let info = info.strip_suffix('\r').unwrap_or(info); // already excludes `\n`
+ let info = info.trim_matches(is_horizontal_whitespace);
if !info.is_empty() {
let info_start = info.offset_from(&raw);
let info_end = info_start + info.len();
@@ -147,7 +148,8 @@
)
.push_visible_span(open_start..open_end));
} else {
- let after_closing_fence = after_closing_fence.trim_matches(is_whitespace);
+ let after_closing_fence = strip_newline(after_closing_fence);
+ let after_closing_fence = after_closing_fence.trim_matches(is_horizontal_whitespace);
if !after_closing_fence.is_empty() {
// extra characters beyond the original fence pattern
let after_start = after_closing_fence.offset_from(&raw);
@@ -261,8 +263,6 @@
/// True if `c` is considered a whitespace according to Rust language definition.
/// See [Rust language reference](https://doc.rust-lang.org/reference/whitespace.html)
/// for definitions of these classes.
-///
-/// See rust-lang/rust's compiler/rustc_lexer/src/lib.rs `is_whitespace`
fn is_whitespace(c: char) -> bool {
// This is Pattern_White_Space.
//
@@ -271,27 +271,46 @@
matches!(
c,
- // Usual ASCII suspects
- '\u{0009}' // \t
- | '\u{000A}' // \n
+ // End-of-line characters
+ | '\u{000A}' // line feed (\n)
| '\u{000B}' // vertical tab
| '\u{000C}' // form feed
- | '\u{000D}' // \r
- | '\u{0020}' // space
+ | '\u{000D}' // carriage return (\r)
+ | '\u{0085}' // next line (from latin1)
+ | '\u{2028}' // LINE SEPARATOR
+ | '\u{2029}' // PARAGRAPH SEPARATOR
- // NEXT LINE from latin1
- | '\u{0085}'
-
- // Bidi markers
+ // `Default_Ignorable_Code_Point` characters
| '\u{200E}' // LEFT-TO-RIGHT MARK
| '\u{200F}' // RIGHT-TO-LEFT MARK
- // Dedicated whitespace characters from Unicode
- | '\u{2028}' // LINE SEPARATOR
- | '\u{2029}' // PARAGRAPH SEPARATOR
+ // Horizontal space characters
+ | '\u{0009}' // tab (\t)
+ | '\u{0020}' // space
)
}
+/// True if `c` is considered horizontal whitespace according to Rust language definition.
+fn is_horizontal_whitespace(c: char) -> bool {
+ // This is Pattern_White_Space.
+ //
+ // Note that this set is stable (ie, it doesn't change with different
+ // Unicode versions), so it's ok to just hard-code the values.
+
+ matches!(
+ c,
+ // Horizontal space characters
+ '\u{0009}' // tab (\t)
+ | '\u{0020}' // space
+ )
+}
+
+fn strip_newline(text: &str) -> &str {
+ text.strip_suffix("\r\n")
+ .or_else(|| text.strip_suffix('\n'))
+ .unwrap_or(text)
+}
+
#[derive(Debug)]
pub struct FrontmatterError {
message: String,
diff --git a/src/cargo/util/mod.rs b/src/cargo/util/mod.rs
index a61fb5b..62f181f 100644
--- a/src/cargo/util/mod.rs
+++ b/src/cargo/util/mod.rs
@@ -18,6 +18,7 @@
pub use self::into_url_with_base::IntoUrlWithBase;
pub(crate) use self::io::LimitErrorReader;
pub use self::lockserver::{LockServer, LockServerClient, LockServerStarted};
+pub use self::once::OnceExt;
pub use self::progress::{Progress, ProgressStyle};
pub use self::queue::Queue;
pub use self::rustc::Rustc;
@@ -56,6 +57,7 @@
mod lockserver;
pub mod machine_message;
pub mod network;
+mod once;
mod progress;
mod queue;
pub mod restricted_names;
diff --git a/src/cargo/util/once.rs b/src/cargo/util/once.rs
new file mode 100644
index 0000000..63f0647
--- /dev/null
+++ b/src/cargo/util/once.rs
@@ -0,0 +1,90 @@
+//! Extension functions for [`std::sync::OnceLock`] / [`std::cell::OnceCell`]
+//!
+//! This adds polyfills for functionality in `lazycell` that is not stable within `std`.
+
+pub trait OnceExt {
+ type T;
+
+ /// This might run `f` multiple times if different threads start initializing at once.
+ fn try_borrow_with<F, E>(&self, f: F) -> Result<&Self::T, E>
+ where
+ F: FnOnce() -> Result<Self::T, E>;
+
+ fn replace(&mut self, new_value: Self::T) -> Option<Self::T>;
+
+ fn filled(&self) -> bool;
+}
+
+impl<T> OnceExt for std::sync::OnceLock<T> {
+ type T = T;
+
+ fn try_borrow_with<F, E>(&self, f: F) -> Result<&T, E>
+ where
+ F: FnOnce() -> Result<T, E>,
+ {
+ if let Some(value) = self.get() {
+ return Ok(value);
+ }
+
+ // This is not how the unstable `OnceLock::get_or_try_init` works. That only starts `f` if
+ // no other `f` is executing and the value is not initialized. However, correctly implementing that is
+ // hard (one has properly handle panics in `f`) and not doable with the stable API of `OnceLock`.
+ let value = f()?;
+ // Another thread might have initialized `self` since we checked that `self.get()` returns `None`. If this is the case, `self.set()`
+ // returns an error. We ignore it and return the value set by the other
+ // thread.
+ let _ = self.set(value);
+ Ok(self.get().unwrap())
+ }
+
+ fn replace(&mut self, new_value: T) -> Option<T> {
+ if let Some(value) = self.get_mut() {
+ Some(std::mem::replace(value, new_value))
+ } else {
+ let result = self.set(new_value);
+ assert!(result.is_ok());
+ None
+ }
+ }
+
+ fn filled(&self) -> bool {
+ self.get().is_some()
+ }
+}
+
+impl<T> OnceExt for std::cell::OnceCell<T> {
+ type T = T;
+
+ fn try_borrow_with<F, E>(&self, f: F) -> Result<&T, E>
+ where
+ F: FnOnce() -> Result<T, E>,
+ {
+ if let Some(value) = self.get() {
+ return Ok(value);
+ }
+
+ // This is not how the unstable `OnceLock::get_or_try_init` works. That only starts `f` if
+ // no other `f` is executing and the value is not initialized. However, correctly implementing that is
+ // hard (one has properly handle panics in `f`) and not doable with the stable API of `OnceLock`.
+ let value = f()?;
+ // Another thread might have initialized `self` since we checked that `self.get()` returns `None`. If this is the case, `self.set()`
+ // returns an error. We ignore it and return the value set by the other
+ // thread.
+ let _ = self.set(value);
+ Ok(self.get().unwrap())
+ }
+
+ fn replace(&mut self, new_value: T) -> Option<T> {
+ if let Some(value) = self.get_mut() {
+ Some(std::mem::replace(value, new_value))
+ } else {
+ let result = self.set(new_value);
+ assert!(result.is_ok());
+ None
+ }
+ }
+
+ fn filled(&self) -> bool {
+ self.get().is_some()
+ }
+}
diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs
index d0a7ced..cf20471 100644
--- a/src/cargo/util/toml/mod.rs
+++ b/src/cargo/util/toml/mod.rs
@@ -81,8 +81,7 @@
to_workspace_config(&original_toml, path, is_embedded, gctx, &mut warnings)?;
if let WorkspaceConfig::Root(ws_root_config) = &workspace_config {
let package_root = path.parent().unwrap();
- gctx.ws_roots
- .borrow_mut()
+ gctx.ws_roots()
.insert(package_root.to_owned(), ws_root_config.clone());
}
let normalized_toml = normalize_toml(
@@ -996,7 +995,7 @@
// Let the borrow exit scope so that it can be picked up if there is a need to
// read a manifest
- if let Some(ws_root) = gctx.ws_roots.borrow().get(workspace_path_root) {
+ if let Some(ws_root) = gctx.ws_roots().get(workspace_path_root) {
return Ok(ws_root.inheritable().clone());
};
@@ -1004,9 +1003,7 @@
let man = read_manifest(&workspace_path, source_id, gctx)?;
match man.workspace_config() {
WorkspaceConfig::Root(root) => {
- gctx.ws_roots
- .borrow_mut()
- .insert(workspace_path, root.clone());
+ gctx.ws_roots().insert(workspace_path, root.clone());
Ok(root.inheritable().clone())
}
_ => bail!(
diff --git a/src/doc/src/guide/build-performance.md b/src/doc/src/guide/build-performance.md
index a9e46f6..c321ee8 100644
--- a/src/doc/src/guide/build-performance.md
+++ b/src/doc/src/guide/build-performance.md
@@ -63,8 +63,29 @@
Trade-offs:
- ✅ Faster code generation (`cargo build`)
-- ❌ **Requires using nightly Rust and an unstable Cargo feature**
+- ❌ **Requires using nightly Rust and an [unstable Cargo feature][codegen-backend-feature]**
- ❌ Worse runtime performance of the generated code
- Speeds up build part of `cargo test`, but might increase its test execution part
- ❌ Only available for [certain targets](https://github.com/rust-lang/rustc_codegen_cranelift?tab=readme-ov-file#platform-support)
- ❌ Might not support all Rust features (e.g. unwinding)
+
+[codegen-backend-feature]: ../reference/unstable.md#codegen-backend
+
+### Enable the experimental parallel frontend
+
+Recommendation: Add to your `.cargo/config.toml`:
+
+```toml
+[build]
+rustflags = "-Zthreads=8"
+```
+
+This [`rustflags`][build.rustflags] will enable the [parallel frontend][parallel-frontend-blog] of the Rust compiler, and tell it to use `n` threads. The value of `n` should be chosen according to the number of cores available on your system, although there are diminishing returns. We recommend using at most `8` threads.
+
+Trade-offs:
+- ✅ Faster build times
+- ❌ **Requires using nightly Rust and an [unstable Rust feature][parallel-frontend-issue]**
+
+[parallel-frontend-blog]: https://blog.rust-lang.org/2023/11/09/parallel-rustc/
+[parallel-frontend-issue]: https://github.com/rust-lang/rust/issues/113349
+[build.rustflags]: ../reference/config.md#buildrustflags