ci-cargo

CI for Rust code.
git clone https://git.philomathiclife.com/repos/ci-cargo
Log | Files | Refs | README

commit fa5a911c2570001784f9b1f93a8634e9aac3c819
parent 50708eb133dc8d17ada2e20187fe30979b0c67b3
Author: Zack Newman <zack@philomathiclife.com>
Date:   Sat, 31 Jan 2026 19:21:15 -0700

lock files

Diffstat:
MCargo.toml | 247++++++++++++++++++++++++++++++++-----------------------------------------------
Msrc/cargo.rs | 2+-
Msrc/main.rs | 170+++++++++++++++++++++++++++++++++++++++++++++----------------------------------
Msrc/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) }