diff --git a/src/cargo/ops/cargo_compile/mod.rs b/src/cargo/ops/cargo_compile/mod.rs index f95e907e6a6..cc90bddea93 100644 --- a/src/cargo/ops/cargo_compile/mod.rs +++ b/src/cargo/ops/cargo_compile/mod.rs @@ -60,7 +60,7 @@ use crate::util::log_message::LogMessage; use crate::util::{CargoResult, StableHasher}; mod compile_filter; -use annotate_snippets::Level; +use annotate_snippets::{Group, Level, Origin}; pub use compile_filter::{CompileFilter, FilterRule, LibRule}; pub(super) mod unit_generator; @@ -535,6 +535,27 @@ pub fn create_bcx<'a, 'gctx>( .extend(args); } + // Validate target src path for each root unit + let mut error_count: usize = 0; + for unit in &units { + if let Some(target_src_path) = unit.target.src_path().path() { + validate_target_path_as_source_file( + gctx, + target_src_path, + unit.target.name(), + unit.target.kind(), + unit.pkg.manifest_path(), + &mut error_count, + )? + } + } + if error_count > 0 { + let plural: &str = if error_count > 1 { "s" } else { "" }; + anyhow::bail!( + "could not compile due to {error_count} previous target resolution error{plural}" + ); + } + if honor_rust_version.unwrap_or(true) { let rustc_version = target_data.rustc.version.clone().into(); @@ -602,6 +623,105 @@ where `` is the latest version supporting rustc {rustc_version}" Ok(bcx) } +// Checks if a target path exists and is a source file, not a directory +fn validate_target_path_as_source_file( + gctx: &GlobalContext, + target_path: &std::path::Path, + target_name: &str, + target_kind: &TargetKind, + unit_manifest_path: &std::path::Path, + error_count: &mut usize, +) -> CargoResult<()> { + if !target_path.exists() { + *error_count += 1; + + let err_msg = format!( + "can't find {} `{}` at path `{}`", + target_kind.description(), + target_name, + target_path.display() + ); + + let group = Group::with_title(Level::ERROR.primary_title(err_msg)).element(Origin::path( + unit_manifest_path.to_str().unwrap_or_default(), + )); + + gctx.shell().print_report(&[group], true)?; + } else if target_path.is_dir() { + *error_count += 1; + + // suggest setting the path to a likely entrypoint + let main_rs = target_path.join("main.rs"); + let lib_rs = target_path.join("lib.rs"); + + let suggested_files_opt = match target_kind { + TargetKind::Lib(_) => { + if lib_rs.exists() { + Some(format!("`{}`", lib_rs.display())) + } else { + None + } + } + TargetKind::Bin => { + if main_rs.exists() { + Some(format!("`{}`", main_rs.display())) + } else { + None + } + } + TargetKind::Test => { + if main_rs.exists() { + Some(format!("`{}`", main_rs.display())) + } else { + None + } + } + TargetKind::ExampleBin => { + if main_rs.exists() { + Some(format!("`{}`", main_rs.display())) + } else { + None + } + } + TargetKind::Bench => { + if main_rs.exists() { + Some(format!("`{}`", main_rs.display())) + } else { + None + } + } + TargetKind::ExampleLib(_) => { + if lib_rs.exists() { + Some(format!("`{}`", lib_rs.display())) + } else { + None + } + } + TargetKind::CustomBuild => None, + }; + + let err_msg = format!( + "path `{}` for {} `{}` is a directory, but a source file was expected.", + target_path.display(), + target_kind.description(), + target_name, + ); + let mut group = Group::with_title(Level::ERROR.primary_title(err_msg)).element( + Origin::path(unit_manifest_path.to_str().unwrap_or_default()), + ); + + if let Some(suggested_files) = suggested_files_opt { + group = group.element( + Level::HELP.message(format!("an entry point exists at {}", suggested_files)), + ); + } + + gctx.shell().print_report(&[group], true)?; + } + + Ok(()) +} + /// This is used to rebuild the unit graph, sharing host dependencies if possible, /// and applying other unit adjustments based on the whole graph. /// diff --git a/tests/testsuite/bad_config.rs b/tests/testsuite/bad_config.rs index ca90a7eaa9a..a19395f63c9 100644 --- a/tests/testsuite/bad_config.rs +++ b/tests/testsuite/bad_config.rs @@ -3755,3 +3755,436 @@ please set bin.path in Cargo.toml "#]]) .run(); } + +#[cargo_test] +fn nonexistent_example_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[example]] + name = "bar" + path = "examples/null.rs" + "#, + ) + .build(); + p.cargo("check --examples") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] can't find example `bar` at path `[ROOT]/foo/examples/null.rs` + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn nonexistent_library_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [lib] + path = "src/null.rs" + "#, + ) + .file("src/lib.rs", "fn bar() {}") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] can't find lib `foo` at path `[ROOT]/foo/src/null.rs` + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn nonexistent_binary_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[bin]] + name = "null" + path = "src/null.rs" + "#, + ) + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] can't find bin `null` at path `[ROOT]/foo/src/null.rs` + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn nonexistent_test_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[test]] + name = "null" + path = "src/null.rs" + "#, + ) + .build(); + p.cargo("test") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] can't find integration-test `null` at path `[ROOT]/foo/src/null.rs` + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn nonexistent_bench_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[bench]] + name = "null" + path = "src/null.rs" + "#, + ) + .build(); + p.cargo("bench") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] can't find bench `null` at path `[ROOT]/foo/src/null.rs` + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_example_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[example]] + name = "bar" + path = "examples/bar" + "#, + ) + .file("examples/bar/.temp", "") + .build(); + p.cargo("check --example bar") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/examples/bar` for example `bar` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_library_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [lib] + path = "src/null" + "#, + ) + .file("src/null/.temp", "") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for lib `foo` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_binary_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[bin]] + name = "null" + path = "src/null" + "#, + ) + .file("src/null/.temp", "") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for bin `null` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_test_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[test]] + name = "null" + path = "src/null" + "#, + ) + .file("src/null/.temp", "") + .build(); + p.cargo("test") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for integration-test `null` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_bench_target_path() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[bench]] + name = "null" + path = "src/null" + "#, + ) + .file("src/null/.temp", "") + .build(); + p.cargo("bench") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for bench `null` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_example_target_path_with_entrypoint() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[example]] + name = "bar" + path = "examples/bar" + "#, + ) + .file("examples/bar/main.rs", "fn main() {}") + .build(); + p.cargo("check --example bar") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/examples/bar` for example `bar` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml + = [HELP] an entry point exists at `[ROOT]/foo/examples/bar/main.rs` +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_library_target_path_with_entrypoint() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [lib] + path = "src/null" + "#, + ) + .file("src/null/lib.rs", "fn foo() {}") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for lib `foo` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml + = [HELP] an entry point exists at `[ROOT]/foo/src/null/lib.rs` +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_binary_target_path_with_entrypoint() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[bin]] + name = "null" + path = "src/null" + "#, + ) + .file("src/null/main.rs", "fn main() {}") + .build(); + p.cargo("check") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for bin `null` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml + = [HELP] an entry point exists at `[ROOT]/foo/src/null/main.rs` +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_test_target_path_with_entrypoint() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[test]] + name = "null" + path = "src/null" + "#, + ) + .file("src/null/main.rs", "fn main() {}") + .build(); + p.cargo("test") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for integration-test `null` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml + = [HELP] an entry point exists at `[ROOT]/foo/src/null/main.rs` +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +} + +#[cargo_test] +fn directory_as_bench_target_path_with_entrypoint() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "1.0.0" + edition = "2024" + + [[bench]] + name = "null" + path = "src/null" + "#, + ) + .file("src/null/main.rs", "fn main() {}") + .build(); + p.cargo("bench") + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] path `[ROOT]/foo/src/null` for bench `null` is a directory, but a source file was expected. + --> [ROOT]/foo/Cargo.toml + = [HELP] an entry point exists at `[ROOT]/foo/src/null/main.rs` +[ERROR] could not compile due to 1 previous target resolution error + +"#]]) + .run(); +}