diff --git a/cargo/private/cargo_build_script_runner/bin.rs b/cargo/private/cargo_build_script_runner/bin.rs index d1fe235b15..210d5d90af 100644 --- a/cargo/private/cargo_build_script_runner/bin.rs +++ b/cargo/private/cargo_build_script_runner/bin.rs @@ -113,10 +113,12 @@ fn run_buildrs() -> Result<(), String> { .ok_or_else(|| "Failed while getting file name".to_string())?; let link = manifest_dir.join(file_name); - symlink_if_not_exists(&path, &link) - .map_err(|err| format!("Failed to symlink {path:?} to {link:?}: {err}"))?; - - exec_root_links.push(link) + // An entry that already exists belongs to CARGO_MANIFEST_DIR and must not be cleaned. + if symlink_if_not_exists(&path, &link) + .map_err(|err| format!("Failed to symlink {path:?} to {link:?}: {err}"))? + { + exec_root_links.push(link) + } } } @@ -383,10 +385,14 @@ fn set_script_runfiles_env(script_path: &Path, command: &mut Command) { } /// Create a symlink from `link` to `original` if `link` doesn't already exist. -fn symlink_if_not_exists(original: &Path, link: &Path) -> Result<(), String> { - symlink(original, link) - .or_else(swallow_already_exists) - .map_err(|err| format!("Failed to create symlink: {err}")) +/// +/// Returns whether the symlink was created. +fn symlink_if_not_exists(original: &Path, link: &Path) -> Result { + match symlink(original, link) { + Ok(()) => Ok(true), + Err(err) if err.kind() == std::io::ErrorKind::AlreadyExists => Ok(false), + Err(err) => Err(format!("Failed to create symlink: {err}")), + } } fn resolve_rundir(rundir: &str, exec_root: &Path, manifest_dir: &Path) -> Result { @@ -406,14 +412,6 @@ fn resolve_rundir(rundir: &str, exec_root: &Path, manifest_dir: &Path) -> Result Ok(exec_root.join(rundir_path)) } -fn swallow_already_exists(err: std::io::Error) -> std::io::Result<()> { - if err.kind() == std::io::ErrorKind::AlreadyExists { - Ok(()) - } else { - Err(err) - } -} - /// A representation of expected command line arguments. struct Args { progname: String, diff --git a/cargo/tests/cargo_build_script/symlink_exec_root/BUILD.bazel b/cargo/tests/cargo_build_script/symlink_exec_root/BUILD.bazel index 75f2407dd7..1b01af60fc 100644 --- a/cargo/tests/cargo_build_script/symlink_exec_root/BUILD.bazel +++ b/cargo/tests/cargo_build_script/symlink_exec_root/BUILD.bazel @@ -37,21 +37,63 @@ cargo_build_script( # This is an empty test file, it is only needed to trigger the build script. write_file( - name = "test_exec_root_access_rs", - out = "test_exec_root_access.rs", + name = "empty", + out = "empty.rs", content = [""], ) rust_test( name = "test_exec_root_access_feature_enabled", - srcs = ["test_exec_root_access.rs"], + srcs = ["empty.rs"], edition = "2021", deps = [":test_exec_root_access.build.feature_enabled"], ) rust_test( name = "test_exec_root_access_feature_disabled", - srcs = ["test_exec_root_access.rs"], + srcs = ["empty.rs"], edition = "2021", deps = [":test_exec_root_access.build.feature_disabled"], ) + +############################################################################### +# Test that a CARGO_MANIFEST_DIR entry which collides with an exec root entry +# survives the run. +# +# The data file below puts a real `external` directory in CARGO_MANIFEST_DIR, +# and `external` is always a top-level exec root entry, so the runner cannot +# symlink it. It must then leave that directory alone rather than deleting it +# along with the symlinks it did create. +############################################################################### + +write_file( + name = "exec_root_collision_file", + out = "external/exec_root_collision.txt", + content = ["This file makes 'external' a real directory in CARGO_MANIFEST_DIR."], +) + +symlink_execroot_cargo_build_script( + name = "test_exec_root_collision.build.feature_enabled", + script = ":test_exec_root_collision.build.feature_disabled", + target_compatible_with = select({ + "@platforms//os:windows": ["@platforms//:incompatible"], + "//conditions:default": [], + }), +) + +cargo_build_script( + name = "test_exec_root_collision.build.feature_disabled", + srcs = ["test_exec_root_collision.build.rs"], + crate_name = "test_exec_root_collision", + data = [ + ":exec_root_collision_file", + ], + edition = "2021", +) + +rust_test( + name = "test_exec_root_collision_feature_enabled", + srcs = ["empty.rs"], + edition = "2021", + deps = [":test_exec_root_collision.build.feature_enabled"], +) diff --git a/cargo/tests/cargo_build_script/symlink_exec_root/test_exec_root_collision.build.rs b/cargo/tests/cargo_build_script/symlink_exec_root/test_exec_root_collision.build.rs new file mode 100644 index 0000000000..0dca2e836e --- /dev/null +++ b/cargo/tests/cargo_build_script/symlink_exec_root/test_exec_root_collision.build.rs @@ -0,0 +1,16 @@ +//! A Cargo build script binary used in unit tests for the Bazel `cargo_build_script` rule + +fn main() { + // `external` is a top-level exec root entry, so the runner will try to symlink it into + // CARGO_MANIFEST_DIR, where this data file has already created a real directory of the + // same name. The pre-existing directory must win, and must still be readable. + let collision = std::path::Path::new("external/exec_root_collision.txt"); + assert!( + collision.is_file(), + "'external/exec_root_collision.txt' must be readable from CARGO_MANIFEST_DIR" + ); + assert_eq!( + std::fs::read_to_string(collision).unwrap(), + "This file makes 'external' a real directory in CARGO_MANIFEST_DIR." + ); +}