Skip to content

Commit

Permalink
Replace symlinks in the output of cargo build scripts (#3067)
Browse files Browse the repository at this point in the history
#2948 breaks building of rdkafka with `cmake` because of dangling
symlinks.

When building with latest version we get the following error:
```
ERROR: /home/wincent/.cache/bazel/_bazel_wincent/394c4c1d21c5490d4a70260a2cfccaf5/external/rules_rust~~crate~crates__rdkafka-sys-4.8.0-2.3.0/BUILD.bazel:68:19: error while validating output tree artifact external/rules_rust~~crate~crates__rdkafka-sys-4.8.0-2.3.0/_bs.out_dir: child lib/cmake/RdKafka/FindLZ4.cmake is a dangling symbolic link
ERROR: /home/wincent/.cache/bazel/_bazel_wincent/394c4c1d21c5490d4a70260a2cfccaf5/external/rules_rust~~crate~crates__rdkafka-sys-4.8.0-2.3.0/BUILD.bazel:68:19: Running Cargo build script rdkafka-sys failed: not all outputs were created or valid
Target @@rules_rust~~crate~crates__rdkafka-0.37.0//:rdkafka failed to build
Use --verbose_failures to see the command lines of failed build steps.
ERROR: /home/wincent/.cache/bazel/_bazel_wincent/394c4c1d21c5490d4a70260a2cfccaf5/external/rules_rust~~crate~crates__rdkafka-sys-4.8.0-2.3.0/BUILD.bazel:18:13 Compiling Rust rlib rdkafka_sys v4.8.0+2.3.0 (7 files) failed: not all outputs were created or valid
```
  • Loading branch information
havasd authored Jan 3, 2025
1 parent b9f51f5 commit 1d0fe8a
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 7 deletions.
132 changes: 125 additions & 7 deletions cargo/cargo_build_script_runner/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ fn run_buildrs() -> Result<(), String> {
command
.current_dir(&working_directory)
.envs(target_env_vars)
.env("OUT_DIR", out_dir_abs)
.env("OUT_DIR", &out_dir_abs)
.env("CARGO_MANIFEST_DIR", manifest_dir)
.env("RUSTC", rustc)
.env("RUST_BACKTRACE", "full");
Expand Down Expand Up @@ -228,9 +228,10 @@ fn run_buildrs() -> Result<(), String> {

// Delete any runfiles that do not need to be propagated to down stream dependents.
if let Some(cargo_manifest_maker) = cargo_manifest_maker {
cargo_manifest_maker.drain_runfiles_dir().unwrap();
cargo_manifest_maker
.drain_runfiles_dir(&out_dir_abs)
.unwrap();
}

Ok(())
}

Expand Down Expand Up @@ -568,18 +569,19 @@ impl RunfilesMaker {
}

/// Delete runfiles from the runfiles directory that do not match user defined suffixes
fn drain_runfiles_dir(&self) -> Result<(), String> {
fn drain_runfiles_dir(&self, out_dir: &Path) -> Result<(), String> {
if cfg!(target_family = "windows") {
// If symlinks are supported then symlinks will have been used.
let supports_symlinks = system_supports_symlinks(&self.output_dir)?;
if supports_symlinks {
self.drain_runfiles_dir_unix()
self.drain_runfiles_dir_unix()?;
} else {
self.drain_runfiles_dir_windows()
self.drain_runfiles_dir_windows()?;
}
} else {
self.drain_runfiles_dir_unix()
self.drain_runfiles_dir_unix()?;
}
replace_symlinks_in_out_dir(out_dir)
}
}

Expand Down Expand Up @@ -720,6 +722,56 @@ fn parse_rustc_cfg_output(stdout: &str) -> BTreeMap<String, String> {
.collect()
}

/// Iterates over the given directory recursively and resolves any symlinks
///
/// Symlinks shouldn't present in `out_dir` as those amy contain paths to sandboxes which doesn't exists anymore.
/// Therefore, bazel will fail because of dangling symlinks.
fn replace_symlinks_in_out_dir(out_dir: &Path) -> Result<(), String> {
if out_dir.is_dir() {
let out_dir_paths = std::fs::read_dir(out_dir).map_err(|e| {
format!(
"Failed to read directory `{}` with {:?}",
out_dir.display(),
e
)
})?;
for entry in out_dir_paths {
let entry =
entry.map_err(|e| format!("Failed to read directory entry with {:?}", e,))?;
let path = entry.path();

if path.is_symlink() {
let target_path = std::fs::read_link(&path).map_err(|e| {
format!("Failed to read symlink `{}` with {:?}", path.display(), e,)
})?;
// we don't want to replace relative symlinks
if target_path.is_relative() {
continue;
}
std::fs::remove_file(&path)
.map_err(|e| format!("Failed remove file `{}` with {:?}", path.display(), e))?;
std::fs::copy(&target_path, &path).map_err(|e| {
format!(
"Failed to copy `{} -> {}` with {:?}",
target_path.display(),
path.display(),
e
)
})?;
} else if path.is_dir() {
replace_symlinks_in_out_dir(&path).map_err(|e| {
format!(
"Failed to normalize nested directory `{}` with {}",
path.display(),
e,
)
})?;
}
}
}
Ok(())
}

fn main() {
std::process::exit(match run_buildrs() {
Ok(_) => 0,
Expand All @@ -735,6 +787,9 @@ fn main() {
mod test {
use super::*;

use std::fs;
use std::io::Write;

#[test]
fn rustc_cfg_parsing() {
let macos_output = r#"\
Expand Down Expand Up @@ -775,4 +830,67 @@ windows
assert_eq!(tree["CARGO_CFG_WINDOWS"], "");
assert_eq!(tree["CARGO_CFG_TARGET_FAMILY"], "windows");
}

fn prepare_output_dir_with_symlinks() -> PathBuf {
let test_tmp = PathBuf::from(std::env::var("TEST_TMPDIR").unwrap());
let out_dir = test_tmp.join("out_dir");
fs::create_dir(&out_dir).unwrap();
let nested_dir = out_dir.join("nested");
fs::create_dir(&nested_dir).unwrap();

let temp_dir_file = test_tmp.join("outside.txt");
let mut file = fs::File::create(&temp_dir_file).unwrap();
file.write_all(b"outside world").unwrap();
// symlink abs path outside of the out_dir
symlink(&temp_dir_file, &out_dir.join("outside.txt")).unwrap();

let inside_dir_file = out_dir.join("inside.txt");
let mut file = fs::File::create(inside_dir_file).unwrap();
file.write_all(b"inside world").unwrap();
// symlink relative next to the file in the out_dir
symlink(
&PathBuf::from("inside.txt"),
&out_dir.join("inside_link.txt"),
)
.unwrap();
// symlink relative within a subdir in the out_dir
symlink(
&PathBuf::from("..").join("inside.txt"),
&out_dir.join("nested").join("inside_link.txt"),
)
.unwrap();

out_dir
}

#[cfg(any(target_family = "windows", target_family = "unix"))]
#[test]
fn replace_symlinks_in_out_dir() {
let out_dir = prepare_output_dir_with_symlinks();
super::replace_symlinks_in_out_dir(&out_dir).unwrap();

// this should be replaced because it is an absolute symlink
let file_path = out_dir.join("outside.txt");
assert!(!file_path.is_symlink());
let contents = fs::read_to_string(file_path).unwrap();
assert_eq!(contents, "outside world");

// this is the file created inside the out_dir
let file_path = out_dir.join("inside.txt");
assert!(!file_path.is_symlink());
let contents = fs::read_to_string(file_path).unwrap();
assert_eq!(contents, "inside world");

// this is the symlink in the out_dir
let file_path = out_dir.join("inside_link.txt");
assert!(file_path.is_symlink());
let contents = fs::read_to_string(file_path).unwrap();
assert_eq!(contents, "inside world");

// this is the symlink in the out_dir under another directory which refers to ../inside.txt
let file_path = out_dir.join("nested").join("inside_link.txt");
assert!(file_path.is_symlink());
let contents = fs::read_to_string(file_path).unwrap();
assert_eq!(contents, "inside world");
}
}
20 changes: 20 additions & 0 deletions test/cargo_build_script/resolve_abs_symlink_out_dir/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
load("//cargo:defs.bzl", "cargo_build_script")
load("//rust:defs.bzl", "rust_test")

# We are testing the cargo build script behavior that it correctly resolves absolute path symlinks in the out_dir.
# Additionally, it keeps out_dir relative symlinks intact.

cargo_build_script(
name = "symlink_build_rs",
srcs = ["build.rs"],
data = ["data.txt"],
edition = "2018",
)

rust_test(
name = "test",
srcs = ["test.rs"],
data = [":symlink_build_rs"],
edition = "2018",
deps = [":symlink_build_rs"],
)
28 changes: 28 additions & 0 deletions test/cargo_build_script/resolve_abs_symlink_out_dir/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
use std::path::{Path, PathBuf};

#[cfg(target_family = "unix")]
fn symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) {
std::os::unix::fs::symlink(original, link).unwrap();
}

#[cfg(target_family = "windows")]
fn symlink(original: impl AsRef<Path>, link: impl AsRef<Path>) {
std::os::windows::fs::symlink_file(original, link).unwrap();
}

fn main() {
let path = "data.txt";
if !PathBuf::from(path).exists() {
panic!("File does not exist in path.");
}
let out_dir = std::env::var("OUT_DIR").unwrap();
let out_dir = PathBuf::from(&out_dir);
let original_cwd = std::env::current_dir().unwrap();
std::fs::copy(&path, &out_dir.join("data.txt")).unwrap();
std::env::set_current_dir(&out_dir).unwrap();
std::fs::create_dir("nested").unwrap();
symlink("data.txt", "relative_symlink.txt");
symlink("../data.txt", "nested/relative_symlink.txt");
std::env::set_current_dir(&original_cwd).unwrap();
println!("{}", out_dir.display());
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Resolved symlink file or relative symlink
17 changes: 17 additions & 0 deletions test/cargo_build_script/resolve_abs_symlink_out_dir/test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#[test]
pub fn test_compile_data_resolved_symlink() {
let data = include_str!(concat!(env!("OUT_DIR"), "/data.txt"));
assert_eq!("Resolved symlink file or relative symlink\n", data);
}

#[test]
pub fn test_compile_data_relative_symlink() {
let data = include_str!(concat!(env!("OUT_DIR"), "/relative_symlink.txt"));
assert_eq!("Resolved symlink file or relative symlink\n", data);
}

#[test]
pub fn test_compile_data_relative_nested_symlink() {
let data = include_str!(concat!(env!("OUT_DIR"), "/nested/relative_symlink.txt"));
assert_eq!("Resolved symlink file or relative symlink\n", data);
}

0 comments on commit 1d0fe8a

Please sign in to comment.