Skip to content
Open
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
6 changes: 6 additions & 0 deletions rust/private/rust.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -732,6 +732,12 @@ _common_attrs = {
"edition": attr.string(
doc = "The rust edition to use for this crate. Defaults to the edition specified in the rust_toolchain.",
),
"extra_outdirs": attr.string_list(
doc = dedent("""\
List of additional output directories which are expected to be written by the compiler.
The paths are always relative to the output directory of the current Bazel package.
"""),
),
"lint_config": attr.label(
doc = "Set of lints to apply when building this crate.",
providers = [LintsInfo],
Expand Down
12 changes: 12 additions & 0 deletions rust/private/rustc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,18 @@ def rustc_compile_action(
interface_library = ctx.actions.declare_file(crate_info.output.basename + ".lib", sibling = crate_info.output)
outputs.append(interface_library)

extra_outdirs_outputs = []
if hasattr(ctx.attr, "extra_outdirs"):
extra_outdirs_outputs = [ctx.actions.declare_directory(outdir) for outdir in ctx.attr.extra_outdirs]
outputs.extend(extra_outdirs_outputs)

# Pass the output directory paths to proc macros via environment variables
# Format: EXTRA_OUTDIRS_PATHS=dir1:path1,dir2:path2
extra_outdirs_paths = []
for outdir, outdir_output in zip(ctx.attr.extra_outdirs, extra_outdirs_outputs):
extra_outdirs_paths.append("{}:{}".format(outdir, outdir_output.path))
env["EXTRA_OUTDIRS_PATHS"] = ",".join(extra_outdirs_paths)

# The action might generate extra output that we don't want to include in the `DefaultInfo` files.
action_outputs = list(outputs)
if rustc_output:
Expand Down
51 changes: 51 additions & 0 deletions test/unit/extra_outdirs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
load("@bazel_skylib//rules:select_file.bzl", "select_file")
load("@rules_shell//shell:sh_test.bzl", "sh_test")
load("//rust:defs.bzl", "rust_library", "rust_proc_macro")
load(":extra_outdirs_test.bzl", "extra_outdirs_test_suite")

rust_proc_macro(
name = "write_outdirs_macro",
srcs = ["proc_macro.rs"],
edition = "2018",
visibility = ["//test:__subpackages__"],
)

rust_library(
name = "lib",
srcs = ["lib.rs"],
edition = "2018",
)

rust_library(
name = "lib_with_outdirs",
srcs = ["lib_with_outdirs.rs"],
edition = "2018",
extra_outdirs = [
"test_dir",
"another_dir",
],
proc_macro_deps = [":write_outdirs_macro"],
)

extra_outdirs_test_suite(
name = "extra_outdirs_test_suite",
)

select_file(
name = "lib_with_outdirs_select_file",
srcs = ":lib_with_outdirs",
subpath = "test_dir",
visibility = ["//visibility:public"],
)

sh_test(
name = "outdirs_content_test",
srcs = ["outdirs_content_test.sh"],
args = [
"$(rlocationpath :lib_with_outdirs_select_file)",
],
data = [
":lib_with_outdirs_select_file",
],
deps = ["@rules_shell//shell/runfiles"],
)
112 changes: 112 additions & 0 deletions test/unit/extra_outdirs/extra_outdirs_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
"""Unittest to verify extra_outdirs attribute adds directories to action outputs."""

load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
load("//test/unit:common.bzl", "assert_action_mnemonic")

def _extra_outdirs_present_test(ctx):
env = analysistest.begin(ctx)
target = analysistest.target_under_test(env)

# Find the Rustc action
rustc_action = [action for action in target.actions if action.mnemonic == "Rustc"][0]
assert_action_mnemonic(env, rustc_action, "Rustc")

# Get all outputs from the action
outputs = rustc_action.outputs.to_list()

# Check that the expected directories are in the outputs
expected_dirs = sorted(ctx.attr.expected_outdirs)
found_dirs = []

for output in outputs:
# Check if this output is a directory
# and if its basename matches one of our expected directories
if output.is_directory:
if output.basename in expected_dirs:
found_dirs.append(output.basename)

# Sort found directories for consistent comparison
found_dirs = sorted(found_dirs)

# Verify all expected directories were found
asserts.equals(
env,
found_dirs,
expected_dirs,
"Expected to find directories {expected} in action outputs, but found {found}".format(
expected = expected_dirs,
found = found_dirs,
),
)

return analysistest.end(env)

def _extra_outdirs_not_present_test(ctx):
env = analysistest.begin(ctx)
target = analysistest.target_under_test(env)

# Find the Rustc action
rustc_action = [action for action in target.actions if action.mnemonic == "Rustc"][0]
assert_action_mnemonic(env, rustc_action, "Rustc")

# Get all outputs from the action
outputs = rustc_action.outputs.to_list()

# Check that no extra directories are present
# We expect only the standard outputs (rlib, rmeta if pipelining, etc.)
# but not any extra_outdirs directories
unexpected_dirs = []
for output in outputs:
if output.is_directory:
# Standard directories like .dSYM are okay, but we shouldn't have
# any of the extra_outdirs we're testing for
if output.basename in ["test_dir", "another_dir"]:
unexpected_dirs.append(output.basename)

asserts.equals(
env,
[],
unexpected_dirs,
"Expected no extra_outdirs directories, but found {found}".format(
found = unexpected_dirs,
),
)

return analysistest.end(env)

extra_outdirs_present_test = analysistest.make(
_extra_outdirs_present_test,
attrs = {
"expected_outdirs": attr.string_list(
mandatory = True,
doc = "List of expected output directory names",
),
},
)

extra_outdirs_not_present_test = analysistest.make(_extra_outdirs_not_present_test)

def extra_outdirs_test_suite(name):
"""Entry-point macro called from the BUILD file.

Args:
name (str): Name of the macro.
"""
extra_outdirs_not_present_test(
name = "extra_outdirs_not_present_test",
target_under_test = ":lib",
)

extra_outdirs_present_test(
name = "extra_outdirs_present_test",
target_under_test = ":lib_with_outdirs",
expected_outdirs = ["test_dir", "another_dir"],
)

native.test_suite(
name = name,
tests = [
":extra_outdirs_not_present_test",
":extra_outdirs_present_test",
],
)
1 change: 1 addition & 0 deletions test/unit/extra_outdirs/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
pub fn call() {}
5 changes: 5 additions & 0 deletions test/unit/extra_outdirs/lib_with_outdirs.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
use write_outdirs_macro::write_to_outdirs;

write_to_outdirs!();

pub fn call() {}
26 changes: 26 additions & 0 deletions test/unit/extra_outdirs/outdirs_content_test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
#! /bin/bash
ls -alR

# --- begin runfiles.bash initialization v3 ---
# Copy-pasted from the Bazel Bash runfiles library v3.
set -uo pipefail; set +e; f=bazel_tools/tools/bash/runfiles/runfiles.bash
# shellcheck disable=SC1090
source "${RUNFILES_DIR:-/dev/null}/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "${RUNFILES_MANIFEST_FILE:-/dev/null}" | cut -f2- -d' ')" 2>/dev/null || \
source "$0.runfiles/$f" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
source "$(grep -sm1 "^$f " "$0.exe.runfiles_manifest" | cut -f2- -d' ')" 2>/dev/null || \
{ echo>&2 "ERROR: cannot find $f"; exit 1; }; f=; set -e
# --- end runfiles.bash initialization v3 ---
set -euo pipefail

for dir in "$@"; do
if [ ! -d "$(rlocation "$dir")" ]; then
echo "Directory $dir does not exist"
exit 1
fi
if [ ! -f "$(rlocation "$dir")/marker.txt" ]; then
echo "Marker file in directory $dir does not exist"
exit 1
fi
done
33 changes: 33 additions & 0 deletions test/unit/extra_outdirs/proc_macro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Similar to
// https://github.com/napi-rs/napi-rs/blob/main/crates/macro/src/expand/typedef/type_def.rs#L11-L12
// this proc macro has a side-effect of writing extra metadata directories.
use proc_macro::TokenStream;
use std::env;
use std::fs;
use std::path::PathBuf;

#[proc_macro]
pub fn write_to_outdirs(_item: TokenStream) -> TokenStream {
// Read the output directory paths from Bazel
// Format: EXTRA_OUTDIRS_PATHS=dir1:path1,dir2:path2
let outdirs_paths = env::var("EXTRA_OUTDIRS_PATHS")
.expect("EXTRA_OUTDIRS_PATHS environment variable must be set");

// Write to the output directories declared by Bazel
for entry in outdirs_paths.split(',') {
if let Some((_dir, path)) = entry.split_once(':') {
let dir_path = PathBuf::from(path.trim());

// Create the directory if it doesn't exist
if let Err(e) = fs::create_dir_all(&dir_path) {
panic!("Failed to create directory {}: {:?}", dir_path.display(), e);
}
// Write a marker file to ensure the directory is created
let marker_file = dir_path.join("marker.txt");
if let Err(e) = fs::write(&marker_file, "created by proc-macro") {
panic!("Failed to write marker file to {}: {:?}", marker_file.display(), e);
}
}
}
TokenStream::new()
}