Skip to content

Commit

Permalink
Symlink in the exec-root so that relative paths will work, unchanged. (
Browse files Browse the repository at this point in the history
…bazelbuild#1781)

* Symlink in the exec-root so that relative paths will work, unchanged.

* Address feedback

* Address more feedback

* buildifier
  • Loading branch information
freeformstu authored and mir-cmg committed Jul 10, 2023
1 parent 8146c5d commit 95b239a
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 1 deletion.
57 changes: 57 additions & 0 deletions cargo/cargo_build_script_runner/bin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,27 @@ fn run_buildrs() -> Result<(), String> {
create_dir_all(&out_dir_abs)
.unwrap_or_else(|_| panic!("Failed to make output directory: {:?}", out_dir_abs));

if should_symlink_exec_root() {
// Symlink the execroot to the manifest_dir so that we can use relative paths in the arguments.
let exec_root_paths = std::fs::read_dir(&exec_root)
.map_err(|err| format!("Failed while listing exec root: {err:?}"))?;
for path in exec_root_paths {
let path = path
.map_err(|err| {
format!("Failed while getting path from exec root listing: {err:?}")
})?
.path();

let file_name = path
.file_name()
.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}"))?;
}
}

let target_env_vars =
get_target_env_vars(&rustc_env).expect("Error getting target env vars from rustc");

Expand Down Expand Up @@ -174,6 +195,42 @@ fn run_buildrs() -> Result<(), String> {
Ok(())
}

fn should_symlink_exec_root() -> bool {
env::var("RULES_RUST_SYMLINK_EXEC_ROOT")
.map(|s| s == "1")
.unwrap_or(false)
}

/// Create a symlink from `link` to `original` if `link` doesn't already exist.
#[cfg(windows)]
fn symlink_if_not_exists(original: &Path, link: &Path) -> Result<(), String> {
if original.is_dir() {
std::os::windows::fs::symlink_dir(original, link)
.or_else(swallow_already_exists)
.map_err(|err| format!("Failed to create directory symlink: {err}"))
} else {
std::os::windows::fs::symlink_file(original, link)
.or_else(swallow_already_exists)
.map_err(|err| format!("Failed to create file symlink: {err}"))
}
}

/// Create a symlink from `link` to `original` if `link` doesn't already exist.
#[cfg(not(windows))]
fn symlink_if_not_exists(original: &Path, link: &Path) -> Result<(), String> {
std::os::unix::fs::symlink(original, link)
.or_else(swallow_already_exists)
.map_err(|err| format!("Failed to create symlink: {err}"))
}

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 Options {
progname: String,
Expand Down
6 changes: 6 additions & 0 deletions cargo/features.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
"""Public Cargo features for Bazel."""

# `symlink-exec-root` feature will symlink the execroot to the build script execution directory.
#
# This is useful for building with hermetic C++ toolchains.
SYMLINK_EXEC_ROOT_FEATURE = "symlink-exec-root"
5 changes: 5 additions & 0 deletions cargo/private/cargo_build_script.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
load("@bazel_skylib//lib:paths.bzl", "paths")
load("@bazel_tools//tools/build_defs/cc:action_names.bzl", "CPP_COMPILE_ACTION_NAME", "C_COMPILE_ACTION_NAME")
load("@bazel_tools//tools/cpp:toolchain_utils.bzl", "find_cpp_toolchain")
load("//cargo:features.bzl", "SYMLINK_EXEC_ROOT_FEATURE")
load("//rust:defs.bzl", "rust_common")

# buildifier: disable=bzl-visibility
Expand All @@ -13,6 +14,7 @@ load("//rust/private:rustc.bzl", "BuildInfo", "get_compilation_mode_opts", "get_

# buildifier: disable=bzl-visibility
load("//rust/private:utils.bzl", "dedent", "expand_dict_value_locations", "find_cc_toolchain", "find_toolchain", _name_to_crate_name = "name_to_crate_name")
load(":features.bzl", "feature_enabled")

# Reexport for cargo_build_script_wrapper.bzl
name_to_crate_name = _name_to_crate_name
Expand Down Expand Up @@ -233,6 +235,9 @@ def _cargo_build_script_impl(ctx):
for dep_build_info in dep[rust_common.dep_info].transitive_build_infos.to_list():
build_script_inputs.append(dep_build_info.out_dir)

if feature_enabled(ctx, SYMLINK_EXEC_ROOT_FEATURE):
env["RULES_RUST_SYMLINK_EXEC_ROOT"] = "1"

ctx.actions.run(
executable = ctx.executable._cargo_build_script_runner,
arguments = [args],
Expand Down
3 changes: 3 additions & 0 deletions cargo/private/cargo_build_script_wrapper.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,9 @@ def cargo_build_script(
if "toolchains" in kwargs:
build_script_kwargs["toolchains"] = kwargs["toolchains"]

if "features" in kwargs:
build_script_kwargs["features"] = kwargs["features"]

rust_binary(
name = name + "_",
crate_features = crate_features,
Expand Down
24 changes: 24 additions & 0 deletions cargo/private/features.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
"""Feature helpers."""

def feature_enabled(ctx, feature_name, default = False):
"""Check if a feature is enabled.
If the feature is explicitly enabled or disabled, return accordingly.
In the case where the feature is not explicitly enabled or disabled, return the default value.
Args:
ctx: The context object.
feature_name: The name of the feature.
default: The default value to return if the feature is not explicitly enabled or disabled.
Returns:
Boolean defining whether the feature is enabled.
"""
if feature_name in ctx.disabled_features:
return False

if feature_name in ctx.features:
return True

return default
59 changes: 59 additions & 0 deletions test/cargo_build_script/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,62 @@ rust_test(
edition = "2018",
deps = [":empty_build_script"],
)

###############################################################################
# Test that the build script can access files in the exec root.
#
# All assertions are done in the build script. If it succeeds in execution, the
# test passes.
###############################################################################

write_file(
name = "cargo_manifest_dir_file",
out = "cargo_manifest_dir_file.txt",
content = ["This is a file to be found alongside the build script."],
)

cargo_build_script(
name = "test_exec_root_access.build.feature_enabled",
srcs = ["test_exec_root_access.build.rs"],
crate_name = "test_exec_root_access",
data = [
":cargo_manifest_dir_file.txt",
],
edition = "2021",
features = ["symlink-exec-root"],
target_compatible_with = select({
"@platforms//os:windows": ["@platforms//:incompatible"],
"//conditions:default": [],
}),
)

cargo_build_script(
name = "test_exec_root_access.build.feature_disabled",
srcs = ["test_exec_root_access.build.rs"],
crate_name = "test_exec_root_access",
data = [
":cargo_manifest_dir_file.txt",
],
edition = "2021",
)

# 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",
content = [""],
)

rust_test(
name = "test_exec_root_access_feature_enabled",
srcs = ["test_exec_root_access.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"],
edition = "2021",
deps = [":test_exec_root_access.build.feature_disabled"],
)
1 change: 0 additions & 1 deletion test/cargo_build_script/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ fn test_encoded_rustflags() {
assert_eq!(flags[1], "--verbose");
}


/// Ensure Make variables provided by the `toolchains` attribute are expandable.
fn test_toolchain_var() {
let tool = std::env::var("EXPANDED_TOOLCHAIN_VAR").unwrap();
Expand Down
102 changes: 102 additions & 0 deletions test/cargo_build_script/test_exec_root_access.build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
//! A Cargo build script binary used in unit tests for the Bazel `cargo_build_script` rule

use std::collections::HashSet;
use std::path::PathBuf;

fn main() {
// The cargo_build_script macro appends an underscore to the given name.
//
// This file would be the only expected source file within the CARGO_MANIFEST_DIR without
// any exec root symlink functionality.
let build_script = PathBuf::from(
std::env::args()
.next()
.expect("Unable to get the build script executable"),
);

let build_script_name = build_script
.file_name()
.expect("Unable to get the build script name")
.to_str()
.expect("Unable to convert the build script name to a string");

let mut root_files = std::fs::read_dir(".")
.expect("Unable to read the current directory")
.map(|entry| {
entry
.expect("Failed to get entry")
.file_name()
.into_string()
.expect("Failed to convert file name to string")
})
.collect::<HashSet<_>>();

assert!(
root_files.take(build_script_name).is_some(),
"Build script must be in the current directory"
);

let cargo_manifest_dir_file = root_files.take("cargo_manifest_dir_file.txt");
assert!(
cargo_manifest_dir_file.is_some(),
"'cargo_manifest_dir_file.txt' must be in the current directory"
);
assert_eq!(
std::fs::read_to_string(cargo_manifest_dir_file.unwrap()).unwrap(),
"This is a file to be found alongside the build script."
);

if symlink_feature_enabled() {
assert!(
root_files.take("bazel-out").is_some(),
"'bazel-out' must be in the current directory when the symlink feature is enabled"
);
assert!(
root_files.take("external").is_some(),
"'external' must be in the current directory when the symlink feature is enabled"
);
}

let remaining_files = root_files
.iter()
// An __action_home_<hash> directory is created in some remote execution builds.
.filter(|file| !file.starts_with("__action_home"))
.collect::<HashSet<_>>();

// If we're in a sandbox then there should be no other files in the current directory.
let is_in_sandbox = is_in_sandbox(&root_files);
assert_eq!(
remaining_files.is_empty(),
is_in_sandbox,
"There should not be any other files in the current directory, found {:?}",
root_files
);
}

/// Check if the symlink feature is enabled.
fn symlink_feature_enabled() -> bool {
std::env::var("RULES_RUST_SYMLINK_EXEC_ROOT")
.map(|v| v == "1")
.unwrap_or(false)
}

/// Check if the current directory is in a sandbox.
///
/// This is done by checking if the current directory contains a directory prefixed with
/// `local-spawn-runner`. If it does, then it is assumed to not be in a sandbox.
///
/// Non-sandboxed builds contain one or more directories in the exec root with the following
/// structure:
/// local-spawn-runner.6722268259075335658/
/// `-- work/
/// local-spawn-runner.3585764808440126801/
/// `-- work/
fn is_in_sandbox(cwd_files: &HashSet<String>) -> bool {
for file in cwd_files {
if file.starts_with("local-spawn-runner.") {
return false;
}
}

true
}

0 comments on commit 95b239a

Please sign in to comment.