commit fa5a911c2570001784f9b1f93a8634e9aac3c819
parent 50708eb133dc8d17ada2e20187fe30979b0c67b3
Author: Zack Newman <zack@philomathiclife.com>
Date: Sat, 31 Jan 2026 19:21:15 -0700
lock files
Diffstat:
| M | Cargo.toml | | | 247 | ++++++++++++++++++++++++++++++++----------------------------------------------- |
| M | src/cargo.rs | | | 2 | +- |
| M | src/main.rs | | | 170 | +++++++++++++++++++++++++++++++++++++++++++++---------------------------------- |
| M | src/manifest.rs | | | 144 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------ |
4 files changed, 322 insertions(+), 241 deletions(-)
diff --git a/Cargo.toml b/Cargo.toml
@@ -9,147 +9,110 @@ license = "MIT OR Apache-2.0"
name = "ci-cargo"
readme = "README.md"
repository = "https://git.philomathiclife.com/repos/ci-cargo/"
-rust-version = "1.91.1"
-version = "0.1.0"
+rust-version = "1.93.0"
+version = "0.2.0"
[lints.rust]
-ambiguous_negative_literals = { level = "deny", priority = -1 }
-closure_returning_async_block = { level = "deny", priority = -1 }
-deprecated_safe = { level = "deny", priority = -1 }
-deref_into_dyn_supertrait = { level = "deny", priority = -1 }
-ffi_unwind_calls = { level = "deny", priority = -1 }
-future_incompatible = { level = "deny", priority = -1 }
-#fuzzy_provenance_casts = { level = "deny", priority = -1 }
-impl_trait_redundant_captures = { level = "deny", priority = -1 }
-keyword_idents = { level = "deny", priority = -1 }
-let_underscore = { level = "deny", priority = -1 }
-linker_messages = { level = "deny", priority = -1 }
-#lossy_provenance_casts = { level = "deny", priority = -1 }
-macro_use_extern_crate = { level = "deny", priority = -1 }
-meta_variable_misuse = { level = "deny", priority = -1 }
-missing_copy_implementations = { level = "deny", priority = -1 }
-missing_debug_implementations = { level = "deny", priority = -1 }
-missing_docs = { level = "deny", priority = -1 }
-#multiple_supertrait_upcastable = { level = "deny", priority = -1 }
-#must_not_suspend = { level = "deny", priority = -1 }
-non_ascii_idents = { level = "deny", priority = -1 }
-#non_exhaustive_omitted_patterns = { level = "deny", priority = -1 }
-nonstandard_style = { level = "deny", priority = -1 }
-redundant_imports = { level = "deny", priority = -1 }
-redundant_lifetimes = { level = "deny", priority = -1 }
-refining_impl_trait = { level = "deny", priority = -1 }
-rust_2018_compatibility = { level = "deny", priority = -1 }
-rust_2018_idioms = { level = "deny", priority = -1 }
-rust_2021_compatibility = { level = "deny", priority = -1 }
-rust_2024_compatibility = { level = "deny", priority = -1 }
-single_use_lifetimes = { level = "deny", priority = -1 }
-#supertrait_item_shadowing_definition = { level = "deny", priority = -1 }
-#supertrait_item_shadowing_usage = { level = "deny", priority = -1 }
-trivial_casts = { level = "deny", priority = -1 }
-trivial_numeric_casts = { level = "deny", priority = -1 }
-unit_bindings = { level = "deny", priority = -1 }
-unknown_or_malformed_diagnostic_attributes = { level = "deny", priority = -1 }
-unnameable_types = { level = "deny", priority = -1 }
-#unqualified_local_imports = { level = "deny", priority = -1 }
-unreachable_pub = { level = "deny", priority = -1 }
-unsafe_code = { level = "deny", priority = -1 }
-unstable_features = { level = "deny", priority = -1 }
+deprecated-safe = { level = "deny", priority = -1 }
+future-incompatible = { level = "deny", priority = -1 }
+keyword-idents = { level = "deny", priority = -1 }
+let-underscore = { level = "deny", priority = -1 }
+nonstandard-style = { level = "deny", priority = -1 }
+refining-impl-trait = { level = "deny", priority = -1 }
+rust-2018-compatibility = { level = "deny", priority = -1 }
+rust-2018-idioms = { level = "deny", priority = -1 }
+rust-2021-compatibility = { level = "deny", priority = -1 }
+rust-2024-compatibility = { level = "deny", priority = -1 }
+unknown-or-malformed-diagnostic-attributes = { level = "deny", priority = -1 }
unused = { level = "deny", priority = -1 }
-unused_crate_dependencies = { level = "deny", priority = -1 }
-unused_import_braces = { level = "deny", priority = -1 }
-unused_lifetimes = { level = "deny", priority = -1 }
-unused_qualifications = { level = "deny", priority = -1 }
-unused_results = { level = "deny", priority = -1 }
-variant_size_differences = { level = "deny", priority = -1 }
warnings = { level = "deny", priority = -1 }
-#ambiguous_negative_literals = "allow"
-#closure_returning_async_block = "allow"
-#deprecated_safe = "allow"
-#deref_into_dyn_supertrait = "allow"
-#ffi_unwind_calls = "allow"
-#future_incompatible = "allow"
-##fuzzy_provenance_casts = "allow"
-#impl_trait_redundant_captures = "allow"
-#keyword_idents = "allow"
-#let_underscore = "allow"
-#linker_messages = "allow"
-##lossy_provenance_casts = "allow"
-#macro_use_extern_crate = "allow"
-#meta_variable_misuse = "allow"
-#missing_copy_implementations = "allow"
-#missing_debug_implementations = "allow"
-#missing_docs = "allow"
-##multiple_supertrait_upcastable = "allow"
-##must_not_suspend = "allow"
-#non_ascii_idents = "allow"
-##non_exhaustive_omitted_patterns = "allow"
-#nonstandard_style = "allow"
-#redundant_imports = "allow"
-#redundant_lifetimes = "allow"
-#refining_impl_trait = "allow"
-#rust_2018_compatibility = "allow"
-#rust_2018_idioms = "allow"
-#rust_2021_compatibility = "allow"
-#rust_2024_compatibility = "allow"
-#single_use_lifetimes = "allow"
-##supertrait_item_shadowing_definition = "allow"
-##supertrait_item_shadowing_usage = "allow"
-#trivial_casts = "allow"
-#trivial_numeric_casts = "allow"
-#unit_bindings = "allow"
-#unknown_or_malformed_diagnostic_attributes = "allow"
-#unnameable_types = "allow"
-##unqualified_local_imports = "allow"
-#unreachable_pub = "allow"
-#unsafe_code = "allow"
-#unstable_features = "allow"
-#unused = "allow"
-#unused_crate_dependencies = "allow"
-#unused_import_braces = "allow"
-#unused_lifetimes = "allow"
-#unused_qualifications = "allow"
-#unused_results = "allow"
-#variant_size_differences = "allow"
-#warnings = "allow"
-#ambiguous_associated_items = "allow"
-#ambiguous_glob_imports = "allow"
-#arithmetic_overflow = "allow"
-#binary_asm_labels = "allow"
-#bindings_with_variant_name = "allow"
-#conflicting_repr_hints = "allow"
-#dangerous_implicit_autorefs = "allow"
-##default_overrides_default_fields = "allow"
-#elided_lifetimes_in_associated_constant = "allow"
-#enum_intrinsics_non_enums = "allow"
-#explicit_builtin_cfgs_in_flags = "allow"
-#ill_formed_attribute_input = "allow"
-#incomplete_include = "allow"
-#ineffective_unstable_trait_impl = "allow"
-#invalid_atomic_ordering = "allow"
-#invalid_doc_attributes = "allow"
-#invalid_from_utf8_unchecked = "allow"
-#invalid_null_arguments = "allow"
-#invalid_reference_casting = "allow"
-#invalid_type_param_default = "allow"
-#let_underscore_lock = "allow"
-#long_running_const_eval = "allow"
-#macro_expanded_macro_exports_accessed_by_absolute_paths = "allow"
-#mutable_transmutes = "allow"
-#named_asm_labels = "allow"
-#no_mangle_const_items = "allow"
-#overflowing_literals = "allow"
-#patterns_in_fns_without_body = "allow"
-#proc_macro_derive_resolution_fallback = "allow"
-#pub_use_of_private_extern_crate = "allow"
-#soft_unstable = "allow"
-##test_unstable_lint = "allow"
-#text_direction_codepoint_in_comment = "allow"
-#text_direction_codepoint_in_literal = "allow"
-#unconditional_panic = "allow"
-#undropped_manually_drops = "allow"
-#unknown_crate_types = "allow"
-#useless_deprecated = "allow"
+ambiguous-negative-literals = { level = "deny", priority = -1 }
+closure-returning-async-block = { level = "deny", priority = -1 }
+deprecated-in-future = { level = "deny", priority = -1 }
+deref-into-dyn-supertrait = { level = "deny", priority = -1 }
+ffi-unwind-calls = { level = "deny", priority = -1 }
+#fuzzy-provenance-casts = { level = "deny", priority = -1 }
+impl-trait-redundant-captures = { level = "deny", priority = -1 }
+linker-messages = { level = "deny", priority = -1 }
+#lossy-provenance-casts = { level = "deny", priority = -1 }
+macro-use-extern-crate = { level = "deny", priority = -1 }
+meta-variable-misuse = { level = "deny", priority = -1 }
+missing-copy-implementations = { level = "deny", priority = -1 }
+missing-debug-implementations = { level = "deny", priority = -1 }
+missing-docs = { level = "deny", priority = -1 }
+#multiple-supertrait-upcastable = { level = "deny", priority = -1 }
+#must-not-suspend = { level = "deny", priority = -1 }
+non-ascii-idents = { level = "deny", priority = -1 }
+#non-exhaustive-omitted-patterns = { level = "deny", priority = -1 }
+redundant-imports = { level = "deny", priority = -1 }
+redundant-lifetimes = { level = "deny", priority = -1 }
+#resolving-to-items-shadowing-supertrait-items = { level = "deny", priority = -1 }
+#shadowing-supertrait-items = { level = "deny", priority = -1 }
+single-use-lifetimes = { level = "deny", priority = -1 }
+trivial-casts = { level = "deny", priority = -1 }
+trivial-numeric-casts = { level = "deny", priority = -1 }
+unit-bindings = { level = "deny", priority = -1 }
+unnameable-types = { level = "deny", priority = -1 }
+#unqualified-local-imports = { level = "deny", priority = -1 }
+unreachable-pub = { level = "deny", priority = -1 }
+unsafe-code = { level = "deny", priority = -1 }
+unstable-features = { level = "deny", priority = -1 }
+unused-crate-dependencies = { level = "deny", priority = -1 }
+unused-import-braces = { level = "deny", priority = -1 }
+unused-lifetimes = { level = "deny", priority = -1 }
+unused-qualifications = { level = "deny", priority = -1 }
+unused-results = { level = "deny", priority = -1 }
+variant-size-differences = { level = "deny", priority = -1 }
+# Before publishing to crates.io, comment above and uncomment below.
+#warnings = { level = "allow", priority = -1 }
+#ambiguous-associated-items = { level = "allow", priority = -1 }
+#ambiguous-glob-imports = { level = "allow", priority = -1 }
+#arithmetic-overflow = { level = "allow", priority = -1 }
+#binary-asm-labels = { level = "allow", priority = -1 }
+#bindings-with-variant-name = { level = "allow", priority = -1 }
+#conflicting-repr-hints = { level = "allow", priority = -1 }
+#dangerous-implicit-autorefs = { level = "allow", priority = -1 }
+#default-overrides-default-fields = { level = "allow", priority = -1 }
+#dependency-on-unit-never-type-fallback = { level = "allow", priority = -1 }
+#deref-nullptr = { level = "allow", priority = -1 }
+#elided-lifetimes-in-associated-constant = { level = "allow", priority = -1 }
+#enum-intrinsics-non-enums = { level = "allow", priority = -1 }
+#explicit-builtin-cfgs-in-flags = { level = "allow", priority = -1 }
+#ill-formed-attribute-input = { level = "allow", priority = -1 }
+#incomplete-include = { level = "allow", priority = -1 }
+#ineffective-unstable-trait-impl = { level = "allow", priority = -1 }
+#invalid-atomic-ordering = { level = "allow", priority = -1 }
+#invalid-doc-attributes = { level = "allow", priority = -1 }
+#invalid-from-utf8-unchecked = { level = "allow", priority = -1 }
+#invalid-macro-export-arguments = { level = "allow", priority = -1 }
+#invalid-null-arguments = { level = "allow", priority = -1 }
+#invalid-reference-casting = { level = "allow", priority = -1 }
+#invalid-type-param-default = { level = "allow", priority = -1 }
+#legacy-derive-helpers = { level = "allow", priority = -1 }
+#let-underscore-lock = { level = "allow", priority = -1 }
+#long-running-const-eval = { level = "allow", priority = -1 }
+#macro-expanded-macro-exports-accessed-by-absolute-paths = { level = "allow", priority = -1 }
+#mutable-transmutes = { level = "allow", priority = -1 }
+#named-asm-labels = { level = "allow", priority = -1 }
+#never-type-fallback-flowing-into-unsafe = { level = "allow", priority = -1 }
+#no-mangle-const-items = { level = "allow", priority = -1 }
+#out-of-scope-macro-calls = { level = "allow", priority = -1 }
+#overflowing-literals = { level = "allow", priority = -1 }
+#patterns-in-fns-without-body = { level = "allow", priority = -1 }
+#proc-macro-derive-resolution-fallback = { level = "allow", priority = -1 }
+#pub-use-of-private-extern-crate = { level = "allow", priority = -1 }
+#repr-transparent-non-zst-fields = { level = "allow", priority = -1 }
+#semicolon-in-expressions-from-macros = { level = "allow", priority = -1 }
+#soft-unstable = { level = "allow", priority = -1 }
+#test-unstable-lint = { level = "allow", priority = -1 }
+#text-direction-codepoint-in-comment = { level = "allow", priority = -1 }
+#text-direction-codepoint-in-literal = { level = "allow", priority = -1 }
+#unconditional-panic = { level = "allow", priority = -1 }
+#undropped-manually-drops = { level = "allow", priority = -1 }
+#unknown-crate-types = { level = "allow", priority = -1 }
+#useless-deprecated = { level = "allow", priority = -1 }
+# Before publishing to crates.io, comment below.
[lints.clippy]
cargo = { level = "deny", priority = -1 }
complexity = { level = "deny", priority = -1 }
@@ -175,16 +138,6 @@ return_and_then = "allow"
single_call_fn = "allow"
single_char_lifetime_names = "allow"
unseparated_literal_suffix = "allow"
-#cargo = "allow"
-#complexity = "allow"
-#correctness = "allow"
-#deprecated = "allow"
-#nursery = "allow"
-#pedantic = "allow"
-#perf = "allow"
-#restriction = "allow"
-#style = "allow"
-#suspicious = "allow"
[package.metadata.docs.rs]
default-target = "x86_64-unknown-linux-gnu"
@@ -205,7 +158,7 @@ targets = [
toml = { version = "0.9.11", default-features = false, features = ["parse"] }
[target.'cfg(target_os = "openbsd")'.dependencies]
-priv_sep = { version = "3.0.0-alpha.4.0", default-features = false, features = ["std"] }
+priv_sep = { version = "3.0.0-alpha.4.1", default-features = false, features = ["std"] }
[profile.release]
codegen-units = 1
diff --git a/src/cargo.rs b/src/cargo.rs
@@ -243,7 +243,7 @@ pub(crate) enum Toolchain<'a> {
Msrv(&'a str),
}
impl Toolchain<'_> {
- /// Extracts the compiler version from `stdout`
+ /// Extracts the compiler version from `stdout`.
///
/// This must only be called by [`Self::get_version`].
#[expect(unsafe_code, reason = "comments justify correctness")]
diff --git a/src/main.rs b/src/main.rs
@@ -17,8 +17,9 @@ use manifest::{Manifest, ManifestErr};
use priv_sep::{Errno, Permissions, Promise, Promises};
use std::{
collections::HashSet,
- env, fs,
- io::{self, BufWriter, Error, Write as _},
+ env,
+ fs::{self, File, TryLockError},
+ io::{self, BufWriter, Error, Read as _, Write as _},
path::{Path, PathBuf},
process::ExitCode,
};
@@ -40,6 +41,10 @@ enum E {
SetDir(Error, PathBuf),
/// Error reading `Cargo.toml`.
CargoTomlRead(Error, PathBuf),
+ /// Error acquiring shared lock on `Cargo.toml`.
+ CargoTomlLock(TryLockError, PathBuf),
+ /// Error when `Cargo.toml` length does not match the length we read.
+ CargoTomlLenMismatch(PathBuf),
/// Error related to extracting the necessary data from `Cargo.toml`.
Manifest(Box<ManifestErr>),
/// Error looking for `rust-toolchain.toml`.
@@ -101,6 +106,10 @@ impl E {
Self::CargoTomlRead(err, p) => {
writeln!(stderr, "There was an error reading {}: {err}.", p.display())
}
+ Self::CargoTomlLock(err, p) => {
+ writeln!(stderr, "There was an error acquiring a shared lock on {}: {err}.", p.display())
+ }
+ Self::CargoTomlLenMismatch(p) => writeln!(stderr, "The number of bytes read from {} does not match the length reported from the file system.", p.display()),
Self::Manifest(e) => e.write(stderr),
Self::RustToolchainTomlIo(err, p) => {
writeln!(
@@ -139,22 +148,21 @@ impl E {
const fn priv_init<Never>() -> Result<(), Never> {
Ok(())
}
-/// Returns the inital set of `Promises` we pledged in addition to allow read permissions to the entire file system.
+/// `pledge(2)`s `exec flock proc rpath stdio unveil` in addition to `unveil(2)`ing the file system
+/// for reads.
#[cfg(target_os = "openbsd")]
-fn priv_init() -> Result<Promises, E> {
- let proms = Promises::new([
+fn priv_init() -> Result<(), E> {
+ Promises::new([
Promise::Exec,
+ Promise::Flock,
Promise::Proc,
Promise::Rpath,
Promise::Stdio,
Promise::Unveil,
- ]);
- proms.pledge().map_err(E::Pledge).and_then(|()| {
- Permissions::READ
- .unveil(c"/")
- .map_err(E::Unveil)
- .map(|()| proms)
- })
+ ])
+ .pledge()
+ .map_err(E::Pledge)
+ .and_then(|()| Permissions::READ.unveil(c"/").map_err(E::Unveil))
}
/// `c"/"`.
#[cfg(target_os = "openbsd")]
@@ -170,27 +178,24 @@ fn rust_toolchain_toml() -> &'static Path {
/// No-op.
#[cfg(not(target_os = "openbsd"))]
#[expect(clippy::unnecessary_wraps, reason = "unify OpenBSD with non-OpenBSD")]
-const fn priv_sep_final<Never>(_: &mut (), _: &Path) -> Result<(), Never> {
+const fn priv_sep_final<Never>(_: &Path) -> Result<(), Never> {
Ok(())
}
-/// Remove read permissions to the entire file system before allowing execute permissions to `cargo_path` or `ROOT`.
-/// Last remove read and unveil permissions.
+/// Removes read permissions to entire file system before allowing execute permissions to `cargo_path` or `ROOT`.
+/// Last remove `flock rpath unveil` from `pledge(2)`.
#[cfg(target_os = "openbsd")]
-fn priv_sep_final(proms: &mut Promises, cargo_path: &Path) -> Result<(), E> {
+fn priv_sep_final(cargo_path: &Path) -> Result<(), E> {
Permissions::NONE
.unveil(ROOT)
.map_err(E::Unveil)
.and_then(|()| {
if cargo_path.is_absolute() {
- Permissions::EXECUTE.unveil(cargo_path).map_err(E::Unveil)
+ Permissions::EXECUTE.unveil(cargo_path)
} else {
- Permissions::EXECUTE.unveil(ROOT).map_err(E::Unveil)
+ Permissions::EXECUTE.unveil(ROOT)
}
- .and_then(|()| {
- proms
- .remove_promises_then_pledge([Promise::Rpath, Promise::Unveil])
- .map_err(E::Pledge)
- })
+ .map_err(E::Unveil)
+ .and_then(|()| Promises::pledge_raw(c"exec proc stdio").map_err(E::Pledge))
})
}
/// Finds `file` in `cur_dir` or its ancestor directories returning `true` iff `file` exists. Searching is
@@ -218,8 +223,12 @@ const VERSION: &str = concat!("ci-cargo ", env!("CARGO_PKG_VERSION"));
clippy::arithmetic_side_effects,
reason = "comment justifies correctness"
)]
+#[expect(
+ clippy::verbose_file_reads,
+ reason = "false positive since we want to lock the file"
+)]
fn main() -> ExitCode {
- priv_init().and_then(|mut proms| MetaCmd::from_args(env::args_os()).map_err(E::Args).and_then(|meta_cmd| {
+ priv_init().and_then(|()| MetaCmd::from_args(env::args_os()).map_err(E::Args).and_then(|meta_cmd| {
match meta_cmd {
MetaCmd::Help => io::stdout().lock().write_all(HELP_MSG.as_bytes()).map_err(E::Help),
MetaCmd::Version => writeln!(io::stdout().lock(), "{VERSION}").map_err(E::Version),
@@ -249,58 +258,75 @@ fn main() -> ExitCode {
skip_no_feats = true;
drop(opts.ignore_features.swap_remove(ig_idx));
}
- fs::read_to_string(&cur_dir).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|toml| Manifest::from_toml(toml, opts.allow_implied_features, &cur_dir, &opts.ignore_features).map_err(E::Manifest).and_then(|man| {
- if opts.default_toolchain || (!rustup::SUPPORTED && opts.rustup_home.is_none()) {
- Ok(Toolchain::Default(opts.ignore_msrv))
- } else {
- let mut cargo_toml_path = cur_dir.clone();
- _ = cargo_toml_path.pop();
- get_path_of_file(&mut cargo_toml_path, rust_toolchain_toml()).map_err(|e| E::RustToolchainTomlIo(e, cargo_toml_path)).and_then(|rust_toolchain_exists| if rust_toolchain_exists { Ok(Toolchain::Default(opts.ignore_msrv)) } else if opts.ignore_msrv { Err(E::IgnoreMsrvStable) } else { Ok(Toolchain::Stable) })
- }.and_then(|toolchain| priv_sep_final(&mut proms, &opts.cargo_path).and_then(|()| man.package().msrv().map_or(Ok(None), |msrv| if !opts.skip_msrv && (rustup::SUPPORTED || opts.rustup_home.is_some()) {
- msrv.compare_to_other(matches!(toolchain, Toolchain::Default(_)), opts.rustup_home.as_deref(), &opts.cargo_path, opts.cargo_home.as_deref()).map_err(E::Toolchain)
- } else {
- Ok(None)
- }).and_then(|msrv_string| {
- let default_feature_does_not_exist = !man.features().contains_default();
- man.features().power_set(skip_no_feats).map_err(|_e| E::TooManyFeatures(cur_dir)).and_then(|power_set_opt| power_set_opt.map_or_else(|| Ok(()), |mut power_set| {
- let mut non_term_errs = HashSet::new();
- cmd.run(Options { toolchain, rustup_home: opts.rustup_home, cargo_path: opts.cargo_path, cargo_home: opts.cargo_home, package_name: man.package().name(), color: opts.color, ignore_compile_errors: opts.ignore_compile_errors, default_feature_does_not_exist, non_terminating_errors: &mut non_term_errs, }, msrv_string.as_deref(), &mut power_set, opts.progress).map_err(E::Cargo).and_then(|()| {
- if non_term_errs.is_empty() {
- Ok(())
- } else {
- // `StderrLock` is not buffered.
- let mut stderr = BufWriter::new(io::stderr().lock());
- non_term_errs.into_iter().try_fold((), |(), msg| stderr.write_all(msg.as_bytes())).and_then(|()| stderr.flush()).map_err(|_e| E::StdErr)
- }
- }).and_then(|()| {
- if opts.summary {
- let mut stdout = io::stdout().lock();
- if matches!(toolchain, Toolchain::Stable) {
- if let Some(ref msrv_val) = msrv_string {
- writeln!(stdout, "Toolchains used: cargo +stable and cargo {msrv_val}")
- } else {
- writeln!(stdout, "Toolchain used: cargo +stable")
- }
- } else if let Some(ref msrv_val) = msrv_string {
- writeln!(stdout, "Toolchains used: cargo and cargo {msrv_val}")
- } else {
- writeln!(stdout, "Toolchain used: cargo")
- }.and_then(|()| {
- writeln!(stdout, "Features used:").and_then(|()| {
- power_set.reset();
- while let Some(features) = power_set.next_set() {
- writeln!(stdout, "{}", if features.is_empty() { "<none>" } else { features })?;
- }
- Ok(())
+ File::options().read(true).open(&cur_dir).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|mut toml_file| {
+ toml_file.try_lock_shared().map_err(|e| E::CargoTomlLock(e, cur_dir.clone())).and_then(|()| {
+ toml_file.metadata().map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|meta| {
+ let meta_len = usize::try_from(meta.len()).unwrap_or(usize::MAX);
+ let mut toml_utf8 = Vec::with_capacity(meta_len);
+ toml_file.read_to_end(&mut toml_utf8).map_err(|e| E::CargoTomlRead(e, cur_dir.clone())).and_then(|len| {
+ drop(toml_file);
+ if meta_len == len {
+ String::from_utf8(toml_utf8).map_err(|e| E::CargoTomlRead(Error::other(e), cur_dir.clone())).and_then(|toml| {
+ Manifest::from_toml(toml, opts.allow_implied_features, &cur_dir, &opts.ignore_features).map_err(E::Manifest).and_then(|man| {
+ if opts.default_toolchain || (!rustup::SUPPORTED && opts.rustup_home.is_none()) {
+ Ok(Toolchain::Default(opts.ignore_msrv))
+ } else {
+ let mut cargo_toml_path = cur_dir.clone();
+ _ = cargo_toml_path.pop();
+ get_path_of_file(&mut cargo_toml_path, rust_toolchain_toml()).map_err(|e| E::RustToolchainTomlIo(e, cargo_toml_path)).and_then(|rust_toolchain_exists| if rust_toolchain_exists { Ok(Toolchain::Default(opts.ignore_msrv)) } else if opts.ignore_msrv { Err(E::IgnoreMsrvStable) } else { Ok(Toolchain::Stable) })
+ }.and_then(|toolchain| priv_sep_final(&opts.cargo_path).and_then(|()| man.package().msrv().map_or(Ok(None), |msrv| if !opts.skip_msrv && (rustup::SUPPORTED || opts.rustup_home.is_some()) {
+ msrv.compare_to_other(matches!(toolchain, Toolchain::Default(_)), opts.rustup_home.as_deref(), &opts.cargo_path, opts.cargo_home.as_deref()).map_err(E::Toolchain)
+ } else {
+ Ok(None)
+ }).and_then(|msrv_string| {
+ let default_feature_does_not_exist = !man.features().contains_default();
+ man.features().power_set(skip_no_feats).map_err(|_e| E::TooManyFeatures(cur_dir)).and_then(|power_set_opt| power_set_opt.map_or_else(|| Ok(()), |mut power_set| {
+ let mut non_term_errs = HashSet::new();
+ cmd.run(Options { toolchain, rustup_home: opts.rustup_home, cargo_path: opts.cargo_path, cargo_home: opts.cargo_home, package_name: man.package().name(), color: opts.color, ignore_compile_errors: opts.ignore_compile_errors, default_feature_does_not_exist, non_terminating_errors: &mut non_term_errs, }, msrv_string.as_deref(), &mut power_set, opts.progress).map_err(E::Cargo).and_then(|()| {
+ if non_term_errs.is_empty() {
+ Ok(())
+ } else {
+ // `StderrLock` is not buffered.
+ let mut stderr = BufWriter::new(io::stderr().lock());
+ non_term_errs.into_iter().try_fold((), |(), msg| stderr.write_all(msg.as_bytes())).and_then(|()| stderr.flush()).map_err(|_e| E::StdErr)
+ }
+ }).and_then(|()| {
+ if opts.summary {
+ let mut stdout = io::stdout().lock();
+ if matches!(toolchain, Toolchain::Stable) {
+ if let Some(ref msrv_val) = msrv_string {
+ writeln!(stdout, "Toolchains used: cargo +stable and cargo {msrv_val}")
+ } else {
+ writeln!(stdout, "Toolchain used: cargo +stable")
+ }
+ } else if let Some(ref msrv_val) = msrv_string {
+ writeln!(stdout, "Toolchains used: cargo and cargo {msrv_val}")
+ } else {
+ writeln!(stdout, "Toolchain used: cargo")
+ }.and_then(|()| {
+ writeln!(stdout, "Features used:").and_then(|()| {
+ power_set.reset();
+ while let Some(features) = power_set.next_set() {
+ writeln!(stdout, "{}", if features.is_empty() { "<none>" } else { features })?;
+ }
+ Ok(())
+ })
+ }).map_err(E::Summary)
+ } else {
+ Ok(())
+ }
+ })
+ }))
+ })))
})
- }).map_err(E::Summary)
+ })
} else {
- Ok(())
+ Err(E::CargoTomlLenMismatch(cur_dir))
}
})
- }))
- })))
- }))
+ })
+ })
+ })
}))
}
})).map_or_else(E::into_exit_code, |()| ExitCode::SUCCESS)
diff --git a/src/manifest.rs b/src/manifest.rs
@@ -5,8 +5,8 @@ use super::{
use alloc::borrow::Cow;
use core::cmp::Ordering;
use std::{
- fs,
- io::{Error, ErrorKind, StderrLock, Write as _},
+ fs::{File, TryLockError},
+ io::{Error, ErrorKind, Read as _, StderrLock, Write as _},
path::{Path, PathBuf},
};
use toml::{
@@ -106,6 +106,10 @@ pub(crate) enum PackageErr {
InvalidWorkspaceType,
/// Variant returned when searching for the workspace file errors.
WorkspaceIo(Error),
+ /// Variant when locking the workspace file errors.
+ WorkspaceLock(TryLockError),
+ /// Variant when the length of the workspace file does not match the length reported from the file system.
+ WorkspaceLenMismatch,
/// Variant returned when there is no workspace `Cargo.toml`.
///
/// This is only returned if the package's MSRV is inherited from the workspace, there is no
@@ -117,6 +121,17 @@ pub(crate) enum PackageErr {
/// This is only returned if the table `package` had a key `workspace` that was a string, or we searched
/// for the workspace and found a `Cargo.toml`.
WorkspaceRead(Error, PathBuf),
+ /// Variant returned when the file located at `package.workspace` could not be locked.
+ ///
+ /// This is only returned if the table `package` had a key `workspace` that was a string, or we searched
+ /// for the workspace and found a `Cargo.toml`.
+ WorkspaceReadLock(TryLockError, PathBuf),
+ /// Variant returned when the length of the file located at `package.workspace` does not match the length
+ /// reported by the file system.
+ ///
+ /// This is only returned if the table `package` had a key `workspace` that was a string, or we searched
+ /// for the workspace and found a `Cargo.toml`.
+ WorkspaceReadLenMismatch(PathBuf),
/// Variant returned when the file located at `package.workspace` is not valid TOML.
///
/// This is only returned if the table `package` had a key `workspace` that was a string, or we searched
@@ -168,6 +183,16 @@ impl PackageErr {
"There was an error looking for the workspace Cargo.toml in {} and its ancestor directories: {e}.",
file.parent().unwrap_or_else(|| unreachable!("there is a bug in main. manifest::Manifest::from_toml must be passed the absolute path to the package's Cargo.toml.")).display(),
),
+ Self::WorkspaceLock(e) => writeln!(
+ stderr,
+ "There was an error locking the workspace Cargo.toml in {} and its ancestor directories: {e}.",
+ file.parent().unwrap_or_else(|| unreachable!("there is a bug in main. manifest::Manifest::from_toml must be passed the absolute path to the package's Cargo.toml.")).display(),
+ ),
+ Self::WorkspaceLenMismatch=> writeln!(
+ stderr,
+ "The length of the workspace Cargo.toml in {} does not match the length reported by the file systemn.",
+ file.parent().unwrap_or_else(|| unreachable!("there is a bug in main. manifest::Manifest::from_toml must be passed the absolute path to the package's Cargo.toml.")).display(),
+ ),
Self::WorkspaceDoesNotExist => writeln!(
stderr,
"There is no workspace Cargo.toml in {} nor its ancestor directories.",
@@ -181,6 +206,12 @@ impl PackageErr {
Self::WorkspaceRead(e, p) => {
writeln!(stderr, "There was an issue reading the workspace file {}: {e}.", p.display())
}
+ Self::WorkspaceReadLock(e, p) => {
+ writeln!(stderr, "There was an issue locking the workspace file {}: {e}.", p.display())
+ }
+ Self::WorkspaceReadLenMismatch(p) => {
+ writeln!(stderr, "The length of the workspace file {} does not match the length reported by the file system.", p.display())
+ }
Self::WorkspaceToml(e, p) => write!(
stderr,
"Error parsing workspace file {} as TOML: {e}.",
@@ -608,24 +639,57 @@ impl Msrv {
/// we want a stack overflow to occur.
///
/// Note if any error occurs not related to a not found file error, then this will error.
+ #[expect(
+ clippy::verbose_file_reads,
+ reason = "false positive since we want to lock the file"
+ )]
fn get_workspace_toml(mut cur_dir: PathBuf) -> Result<Self, PackageErr> {
cur_dir.push(super::cargo_toml());
- match fs::read_to_string(&cur_dir) {
- Ok(file) => Map::parse(&file)
- .map_err(|e| PackageErr::WorkspaceToml(e, cur_dir.clone()))
- .and_then(|toml| {
- let t = toml.into_inner();
- if t.contains_key(WORKSPACE) {
- Self::extract_workspace(&t).map_err(|e| PackageErr::Workspace(e, cur_dir))
- } else {
- _ = cur_dir.pop();
- if cur_dir.pop() {
- Self::get_workspace_toml(cur_dir)
- } else {
- Err(PackageErr::WorkspaceDoesNotExist)
- }
- }
- }),
+ match File::options()
+ .read(true)
+ .open(&cur_dir)
+ {
+ Ok(mut file) => {
+ file.try_lock_shared()
+ .map_err(PackageErr::WorkspaceLock)
+ .and_then(|()| {
+ file.metadata()
+ .map_err(PackageErr::WorkspaceIo)
+ .and_then(|meta| {
+ let meta_len = usize::try_from(meta.len()).unwrap_or(usize::MAX);
+ let mut data_utf8 = Vec::with_capacity(meta_len);
+ file.read_to_end(&mut data_utf8)
+ .map_err(PackageErr::WorkspaceIo)
+ .and_then(|len| {
+ drop(file);
+ if meta_len == len {
+ String::from_utf8(data_utf8)
+ .map_err(|e| PackageErr::WorkspaceIo(Error::other(e)))
+ .and_then(|data| {
+ Map::parse(&data)
+ .map_err(|e| PackageErr::WorkspaceToml(e, cur_dir.clone()))
+ .and_then(|toml| {
+ let t = toml.into_inner();
+ if t.contains_key(WORKSPACE) {
+ Self::extract_workspace(&t)
+ .map_err(|e| PackageErr::Workspace(e, cur_dir))
+ } else {
+ _ = cur_dir.pop();
+ if cur_dir.pop() {
+ Self::get_workspace_toml(cur_dir)
+ } else {
+ Err(PackageErr::WorkspaceDoesNotExist)
+ }
+ }
+ })
+ })
+ } else {
+ Err(PackageErr::WorkspaceLenMismatch)
+ }
+ })
+ })
+ })
+ }
Err(e) => {
if matches!(e.kind(), ErrorKind::NotFound) {
_ = cur_dir.pop();
@@ -645,6 +709,10 @@ impl Msrv {
clippy::panic_in_result_fn,
reason = "want to crash when there is a bug"
)]
+ #[expect(
+ clippy::verbose_file_reads,
+ reason = "false positive since we want to lock the file"
+ )]
fn extract_from_toml(
toml: &Map<Spanned<Cow<'_, str>>, Spanned<DeValue<'_>>>,
package: &Map<Spanned<Cow<'_, str>>, Spanned<DeValue<'_>>>,
@@ -678,7 +746,24 @@ impl Msrv {
assert!(path.pop(), "there is a bug in main. manifest::Manifest::from_toml must be passed the absolute path to the package's Cargo.toml.");
path.push(workspace_path.as_ref());
path.push(super::cargo_toml());
- fs::read_to_string(&path).map_err(|e| PackageErr::WorkspaceRead(e, path.clone())).and_then(|workspace_file| Map::parse(&workspace_file).map_err(|e| PackageErr::WorkspaceToml(e, path.clone())).and_then(|workspace_toml| Self::extract_workspace(workspace_toml.get_ref()).map_err(|e| PackageErr::Workspace(e, path)).map(Some)))
+ File::options().read(true).open(&path).map_err(|e| PackageErr::WorkspaceRead(e, path.clone())).and_then(|mut workspace_file| {
+ workspace_file.try_lock_shared().map_err(|e| PackageErr::WorkspaceReadLock(e, path.clone())).and_then(|()| {
+ workspace_file.metadata().map_err(|e| PackageErr::WorkspaceRead(e, path.clone())).and_then(|meta| {
+ let meta_len = usize::try_from(meta.len()).unwrap_or(usize::MAX);
+ let mut workspace_utf8 = Vec::with_capacity(meta_len);
+ workspace_file.read_to_end(&mut workspace_utf8).map_err(|e| PackageErr::WorkspaceRead(e, path.clone())).and_then(|len| {
+ drop(workspace_file);
+ if meta_len == len {
+ String::from_utf8(workspace_utf8).map_err(|e| PackageErr::WorkspaceRead(Error::other(e), path.clone())).and_then(|workspace_data| {
+ Map::parse(&workspace_data).map_err(|e| PackageErr::WorkspaceToml(e, path.clone())).and_then(|workspace_toml| Self::extract_workspace(workspace_toml.get_ref()).map_err(|e| PackageErr::Workspace(e, path)).map(Some))
+ })
+ } else {
+ Err(PackageErr::WorkspaceReadLenMismatch(path))
+ }
+ })
+ })
+ })
+ })
} else {
Err(PackageErr::InvalidWorkspaceType)
}
@@ -1298,7 +1383,7 @@ impl Features {
///
/// Note since all dependencies that contain a `'/'` are ignored, there may be duplicates of them.
/// Also when checking for redundant features in `dependencies`, _only_ features are considered; thus
- /// something like the following is allowed: feature = \["dep:a", "a"], a = \["dep:a"]
+ /// something like the following is allowed: feature = \["dep:a", "a"], a = \["dep:a"].
///
/// This must only be called from [`Self::extract_from_toml`].
#[expect(
@@ -1704,9 +1789,10 @@ mod tests {
use super::{
DependenciesErr, FeatureDependenciesErr, Features, FeaturesErr, ImpliedFeaturesErr,
Manifest, ManifestErr, Msrv, NonZeroUsizePlus1, Package, PackageErr, Path, PathBuf,
- PowerSet, TooManyFeaturesErr, WorkspaceErr,
+ PowerSet, TooManyFeaturesErr, TryLockError, WorkspaceErr,
};
impl PartialEq for PackageErr {
+ #[expect(clippy::cognitive_complexity, reason = "long match expression")]
fn eq(&self, other: &Self) -> bool {
match *self {
Self::Missing => matches!(*other, Self::Missing),
@@ -1721,10 +1807,26 @@ mod tests {
Self::WorkspaceIo(ref e) => {
matches!(*other, Self::WorkspaceIo(ref e2) if e.kind() == e2.kind())
}
+ Self::WorkspaceLock(ref e) => {
+ matches!(*other, Self::WorkspaceLock(ref e2) if match *e {
+ TryLockError::Error(ref inner_e) => matches!(*e2, TryLockError::Error(ref inner_e2) if inner_e.kind() == inner_e2.kind()),
+ TryLockError::WouldBlock => matches!(*e2, TryLockError::WouldBlock),
+ })
+ }
+ Self::WorkspaceLenMismatch => matches!(*other, Self::WorkspaceLenMismatch),
Self::WorkspaceDoesNotExist => matches!(*other, Self::WorkspaceDoesNotExist),
Self::WorkspaceRead(ref e, ref p) => {
matches!(*other, Self::WorkspaceRead(ref e2, ref p2) if e.kind() == e2.kind() && p == p2)
}
+ Self::WorkspaceReadLock(ref e, ref p) => {
+ matches!(*other, Self::WorkspaceReadLock(ref e2, ref p2) if p == p2 && match *e {
+ TryLockError::Error(ref inner_e) => matches!(*e2, TryLockError::Error(ref inner_e2) if inner_e.kind() == inner_e2.kind()),
+ TryLockError::WouldBlock => matches!(*e2, TryLockError::WouldBlock),
+ })
+ }
+ Self::WorkspaceReadLenMismatch(ref p) => {
+ matches!(*other, Self::WorkspaceReadLenMismatch(ref p2) if p == p2)
+ }
Self::WorkspaceToml(ref e, ref p) => {
matches!(*other, Self::WorkspaceToml(ref e2, ref p2) if e == e2 && p == p2)
}