Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
May 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
}