diff --git a/src/bin/cargo/commands/fix.rs b/src/bin/cargo/commands/fix.rs index 30d07cf1944..8a901de8ae5 100644 --- a/src/bin/cargo/commands/fix.rs +++ b/src/bin/cargo/commands/fix.rs @@ -1,5 +1,6 @@ use crate::command_prelude::*; +use cargo::core::Workspace; use cargo::ops; pub fn cli() -> Command { @@ -60,7 +61,6 @@ pub fn cli() -> Command { } pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { - let ws = args.workspace(gctx)?; // This is a legacy behavior that causes `cargo fix` to pass `--test`. let test = matches!( args.get_one::("profile").map(String::as_str), @@ -70,6 +70,9 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { // Unlike other commands default `cargo fix` to all targets to fix as much // code as we can. + let root_manifest = args.root_manifest(gctx)?; + let mut ws = Workspace::new(&root_manifest, gctx)?; + ws.set_honor_rust_version(args.honor_rust_version()); let mut opts = args.compile_options(gctx, mode, Some(&ws), ProfileChecking::LegacyTestOnly)?; if !opts.filter.is_specific() { @@ -78,7 +81,9 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { } ops::fix( + gctx, &ws, + &root_manifest, &mut ops::FixOptions { edition: args.flag("edition"), idioms: args.flag("edition-idioms"), diff --git a/src/cargo/core/workspace.rs b/src/cargo/core/workspace.rs index 7b92d2e3706..8a0596cb5fd 100644 --- a/src/cargo/core/workspace.rs +++ b/src/cargo/core/workspace.rs @@ -611,6 +611,10 @@ impl<'gctx> Workspace<'gctx> { self.honor_rust_version = honor_rust_version; } + pub fn honor_rust_version(&self) -> Option { + self.honor_rust_version + } + pub fn resolve_honors_rust_version(&self) -> bool { self.gctx().cli_unstable().msrv_policy && self.honor_rust_version.unwrap_or(true) } diff --git a/src/cargo/ops/fix.rs b/src/cargo/ops/fix.rs index 6c34abc37cf..aaf731b4e85 100644 --- a/src/cargo/ops/fix.rs +++ b/src/cargo/ops/fix.rs @@ -53,7 +53,8 @@ use tracing::{debug, trace, warn}; use crate::core::compiler::RustcTargetData; use crate::core::resolver::features::{DiffMap, FeatureOpts, FeatureResolver, FeaturesFor}; use crate::core::resolver::{HasDevUnits, Resolve, ResolveBehavior}; -use crate::core::{Edition, MaybePackage, PackageId, Workspace}; +use crate::core::PackageIdSpecQuery as _; +use crate::core::{Edition, MaybePackage, Package, PackageId, Workspace}; use crate::ops::resolve::WorkspaceResolve; use crate::ops::{self, CompileOptions}; use crate::util::diagnostic_server::{Message, RustfixDiagnosticServer}; @@ -87,11 +88,26 @@ pub struct FixOptions { pub broken_code: bool, } -pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> { - check_version_control(ws.gctx(), opts)?; +pub fn fix( + gctx: &GlobalContext, + original_ws: &Workspace<'_>, + root_manifest: &Path, + opts: &mut FixOptions, +) -> CargoResult<()> { + check_version_control(gctx, opts)?; + if opts.edition { - check_resolver_change(ws, opts)?; + let specs = opts.compile_opts.spec.to_package_id_specs(&original_ws)?; + let members: Vec<&Package> = original_ws + .members() + .filter(|m| specs.iter().any(|spec| spec.matches(m.package_id()))) + .collect(); + migrate_manifests(original_ws, &members)?; + + check_resolver_change(&original_ws, opts)?; } + let mut ws = Workspace::new(&root_manifest, gctx)?; + ws.set_honor_rust_version(original_ws.honor_rust_version()); // Spin up our lock server, which our subprocesses will use to synchronize fixes. let lock_server = LockServer::new()?; @@ -128,7 +144,7 @@ pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> { server.configure(&mut wrapper); } - let rustc = ws.gctx().load_global_rustc(Some(ws))?; + let rustc = ws.gctx().load_global_rustc(Some(&ws))?; wrapper.arg(&rustc.path); // This is calling rustc in cargo fix-proxy-mode, so it also need to retry. // The argfile handling are located at `FixArgs::from_args`. @@ -138,7 +154,7 @@ pub fn fix(ws: &Workspace<'_>, opts: &mut FixOptions) -> CargoResult<()> { // repeating build until there are no more changes to be applied opts.compile_opts.build_config.primary_unit_rustc = Some(wrapper); - ops::compile(ws, &opts.compile_opts)?; + ops::compile(&ws, &opts.compile_opts)?; Ok(()) } @@ -215,6 +231,62 @@ fn check_version_control(gctx: &GlobalContext, opts: &FixOptions) -> CargoResult ); } +fn migrate_manifests(ws: &Workspace<'_>, pkgs: &[&Package]) -> CargoResult<()> { + for pkg in pkgs { + let existing_edition = pkg.manifest().edition(); + let prepare_for_edition = existing_edition.saturating_next(); + if existing_edition == prepare_for_edition + || (!prepare_for_edition.is_stable() && !ws.gctx().nightly_features_allowed) + { + continue; + } + let file = pkg.manifest_path(); + let file = file.strip_prefix(ws.root()).unwrap_or(file); + let file = file.display(); + ws.gctx().shell().status( + "Migrating", + format!("{file} from {existing_edition} edition to {prepare_for_edition}"), + )?; + + if Edition::Edition2024 <= prepare_for_edition { + let mut document = pkg.manifest().document().clone().into_mut(); + let mut fixes = 0; + + let root = document.as_table_mut(); + if rename_table(root, "project", "package") { + fixes += 1; + } + + if 0 < fixes { + let verb = if fixes == 1 { "fix" } else { "fixes" }; + let msg = format!("{file} ({fixes} {verb})"); + ws.gctx().shell().status("Fixed", msg)?; + + let s = document.to_string(); + let new_contents_bytes = s.as_bytes(); + cargo_util::paths::write_atomic(pkg.manifest_path(), new_contents_bytes)?; + } + } + } + + Ok(()) +} + +fn rename_table(parent: &mut dyn toml_edit::TableLike, old: &str, new: &str) -> bool { + let Some(old_key) = parent.key(old).cloned() else { + return false; + }; + + let project = parent.remove(old).expect("returned early"); + if !parent.contains_key(new) { + parent.insert(new, project); + let mut new_key = parent.key_mut(new).expect("just inserted"); + *new_key.dotted_decor_mut() = old_key.dotted_decor().clone(); + *new_key.leaf_decor_mut() = old_key.leaf_decor().clone(); + } + true +} + fn check_resolver_change(ws: &Workspace<'_>, opts: &FixOptions) -> CargoResult<()> { let root = ws.root_maybe(); match root { diff --git a/src/cargo/util/toml/mod.rs b/src/cargo/util/toml/mod.rs index 9cf3bc9e957..020b08d2ac5 100644 --- a/src/cargo/util/toml/mod.rs +++ b/src/cargo/util/toml/mod.rs @@ -935,26 +935,9 @@ fn to_real_manifest( ); }; - let original_package = match (&original_toml.package, &original_toml.project) { - (Some(_), Some(project)) => { - warnings.push(format!( - "manifest at `{}` contains both `project` and `package`, \ - this could become a hard error in the future", - package_root.display() - )); - project.clone() - } - (Some(package), None) => package.clone(), - (None, Some(project)) => { - warnings.push(format!( - "manifest at `{}` contains `[project]` instead of `[package]`, \ - this could become a hard error in the future", - package_root.display() - )); - project.clone() - } - (None, None) => bail!("no `package` section found"), - }; + let original_package = original_toml + .package() + .ok_or_else(|| anyhow::format_err!("no `package` section found"))?; let package_name = &original_package.name; if package_name.contains(':') { @@ -1044,6 +1027,16 @@ fn to_real_manifest( ))); } + if original_toml.project.is_some() { + if Edition::Edition2024 <= edition { + anyhow::bail!( + "`[project]` is not supported as of the 2024 Edition, please use `[package]`" + ); + } else { + warnings.push(format!("`[project]` is deprecated in favor of `[package]`")); + } + } + if resolved_package.metabuild.is_some() { features.require(Feature::metabuild())?; } diff --git a/tests/testsuite/check.rs b/tests/testsuite/check.rs index de6a324acd3..da8239fd748 100644 --- a/tests/testsuite/check.rs +++ b/tests/testsuite/check.rs @@ -979,6 +979,63 @@ fn rustc_workspace_wrapper_excludes_published_deps() { .run(); } +#[cargo_test] +fn warn_manifest_with_project() { + let p = project() + .file( + "Cargo.toml", + r#" + [project] + name = "foo" + version = "0.0.1" + edition = "2015" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("check") + .with_stderr( + "\ +[WARNING] `[project]` is deprecated in favor of `[package]` +[CHECKING] foo v0.0.1 ([CWD]) +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [..] +", + ) + .run(); +} + +#[cargo_test(nightly, reason = "edition2024")] +fn error_manifest_with_project_on_2024() { + let p = project() + .file( + "Cargo.toml", + r#" + cargo-features = ["edition2024"] + + [project] + name = "foo" + version = "0.0.1" + edition = "2024" + "#, + ) + .file("src/main.rs", "fn main() {}") + .build(); + + p.cargo("check") + .masquerade_as_nightly_cargo(&["edition2024"]) + .with_status(101) + .with_stderr( + "\ +[ERROR] failed to parse manifest at `[CWD]/Cargo.toml` + +Caused by: + `[project]` is not supported as of the 2024 Edition, please use `[package]` +", + ) + .run(); +} + #[cargo_test] fn warn_manifest_package_and_project() { let p = project() @@ -1002,7 +1059,7 @@ fn warn_manifest_package_and_project() { p.cargo("check") .with_stderr( "\ -[WARNING] manifest at `[CWD]` contains both `project` and `package`, this could become a hard error in the future +[WARNING] `[project]` is deprecated in favor of `[package]` [CHECKING] foo v0.0.1 ([CWD]) [FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [..] ", @@ -1065,32 +1122,6 @@ fn git_manifest_package_and_project() { .run(); } -#[cargo_test] -fn warn_manifest_with_project() { - let p = project() - .file( - "Cargo.toml", - r#" - [project] - name = "foo" - version = "0.0.1" - edition = "2015" - "#, - ) - .file("src/main.rs", "fn main() {}") - .build(); - - p.cargo("check") - .with_stderr( - "\ -[WARNING] manifest at `[CWD]` contains `[project]` instead of `[package]`, this could become a hard error in the future -[CHECKING] foo v0.0.1 ([CWD]) -[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [..] -", - ) - .run(); -} - #[cargo_test] fn git_manifest_with_project() { let p = project(); diff --git a/tests/testsuite/fix.rs b/tests/testsuite/fix.rs index a20ded1965b..485511ab764 100644 --- a/tests/testsuite/fix.rs +++ b/tests/testsuite/fix.rs @@ -172,6 +172,7 @@ fn prepare_for_2018() { .build(); let stderr = "\ +[MIGRATING] Cargo.toml from 2015 edition to 2018 [CHECKING] foo v0.0.1 ([..]) [MIGRATING] src/lib.rs from 2015 edition to 2018 [FIXED] src/lib.rs (2 fixes) @@ -211,6 +212,7 @@ fn local_paths() { p.cargo("fix --edition --allow-no-vcs") .with_stderr( "\ +[MIGRATING] Cargo.toml from 2015 edition to 2018 [CHECKING] foo v0.0.1 ([..]) [MIGRATING] src/lib.rs from 2015 edition to 2018 [FIXED] src/lib.rs (1 fix) @@ -298,6 +300,7 @@ fn specify_rustflags() { .env("RUSTFLAGS", "-C linker=cc") .with_stderr( "\ +[MIGRATING] Cargo.toml from 2015 edition to 2018 [CHECKING] foo v0.0.1 ([..]) [MIGRATING] src/lib.rs from 2015 edition to 2018 [FIXED] src/lib.rs (1 fix) @@ -770,6 +773,7 @@ https://doc.rust-lang.org/edition-guide/editions/transitioning-an-existing-proje .masquerade_as_nightly_cargo(&["always_nightly"]) .with_stderr(&format!( "\ +[MIGRATING] Cargo.toml from {latest_stable} edition to {next} [CHECKING] foo [..] [MIGRATING] src/lib.rs from {latest_stable} edition to {next} [FINISHED] [..] @@ -804,11 +808,11 @@ fn prepare_for_latest_stable() { p.cargo("fix --edition --allow-no-vcs") .with_stderr(&format!( "\ +[MIGRATING] Cargo.toml from {previous} edition to {latest_stable} [CHECKING] foo [..] -[MIGRATING] src/lib.rs from {} edition to {} +[MIGRATING] src/lib.rs from {previous} edition to {latest_stable} [FINISHED] [..] ", - previous, latest_stable )) .run(); } @@ -911,6 +915,7 @@ fn fix_overlapping() { p.cargo("fix --allow-no-vcs --edition --lib") .with_stderr( "\ +[MIGRATING] Cargo.toml from 2015 edition to 2018 [CHECKING] foo [..] [MIGRATING] src/lib.rs from 2015 edition to 2018 [FIXED] src/lib.rs (2 fixes) @@ -1202,6 +1207,7 @@ fn only_warn_for_relevant_crates() { p.cargo("fix --allow-no-vcs --edition") .with_stderr( "\ +[MIGRATING] Cargo.toml from 2015 edition to 2018 [LOCKING] 2 packages to latest compatible versions [CHECKING] a v0.1.0 ([..]) [CHECKING] foo v0.1.0 ([..]) @@ -1398,6 +1404,7 @@ fn edition_v2_resolver_report() { p.cargo("fix --edition --allow-no-vcs") .with_stderr_unordered("\ +[MIGRATING] Cargo.toml from 2018 edition to 2021 [UPDATING] [..] [LOCKING] 4 packages to latest compatible versions [DOWNLOADING] crates ... @@ -1477,6 +1484,7 @@ fn fix_edition_2021() { p.cargo("fix --edition --allow-no-vcs") .with_stderr( "\ +[MIGRATING] Cargo.toml from 2018 edition to 2021 [CHECKING] foo v0.1.0 [..] [MIGRATING] src/lib.rs from 2018 edition to 2021 [FIXED] src/lib.rs (1 fix) @@ -1942,3 +1950,102 @@ fn fix_only_once_for_duplicates() { 1 from newly-applied unsafe blocks" ); } + +#[cargo_test] +fn migrate_project_to_package() { + let p = project() + .file( + "Cargo.toml", + r#" +cargo-features = ["edition2024"] + +# Before project +[ project ] # After project header +# After project header line +name = "foo" +edition = "2021" +# After project table +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("fix --edition --allow-no-vcs") + .masquerade_as_nightly_cargo(&["edition2024"]) + .with_stderr( + "\ +[MIGRATING] Cargo.toml from 2021 edition to 2024 +[FIXED] Cargo.toml (1 fix) +[CHECKING] foo v0.0.0 ([CWD]) +[MIGRATING] src/lib.rs from 2021 edition to 2024 +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [..]s +", + ) + .run(); + assert_eq!( + p.read_file("Cargo.toml"), + r#" +cargo-features = ["edition2024"] + +# Before project +[ package ] # After project header +# After project header line +name = "foo" +edition = "2021" +# After project table +"# + ); +} + +#[cargo_test] +fn migrate_removes_project() { + let p = project() + .file( + "Cargo.toml", + r#" +cargo-features = ["edition2024"] + +# Before package +[ package ] # After package header +# After package header line +name = "foo" +edition = "2021" +# After package table + +# Before project +[ project ] # After project header +# After project header line +name = "foo" +edition = "2021" +# After project table +"#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("fix --edition --allow-no-vcs") + .masquerade_as_nightly_cargo(&["edition2024"]) + .with_stderr( + "\ +[MIGRATING] Cargo.toml from 2021 edition to 2024 +[FIXED] Cargo.toml (1 fix) +[CHECKING] foo v0.0.0 ([CWD]) +[MIGRATING] src/lib.rs from 2021 edition to 2024 +[FINISHED] `dev` profile [unoptimized + debuginfo] target(s) in [..]s +", + ) + .run(); + assert_eq!( + p.read_file("Cargo.toml"), + r#" +cargo-features = ["edition2024"] + +# Before package +[ package ] # After package header +# After package header line +name = "foo" +edition = "2021" +# After project table +"# + ); +}