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
11 changes: 11 additions & 0 deletions rust/private/rustc.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -1482,6 +1482,17 @@ 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
54 changes: 54 additions & 0 deletions test/unit/extra_outdirs/BUILD.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
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"],
rustc_env = {
"EXTRA_OUTDIRS": "test_dir,another_dir",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make sure the output dirs support location expansion? That's how I'd expect this to be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added to the docstring:

"The paths are always relative to the output directory of the current Bazel package."

I would not expect them to support location expansion, but only because https://bazel.build/rules/lib/toplevel/attr#output_list doesn't have an equivalent output_dir_list, so there's no way during analysis to resolve such expansions.

Seems like a missing feature in Bazel to me.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wait, why is output_dir_list needed? Does it matter if you're expanding a file or directory?

},
)

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"],
)
109 changes: 109 additions & 0 deletions test/unit/extra_outdirs/extra_outdirs_test.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
"""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 = 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)

# Verify all expected directories were found
asserts.equals(
env,
sorted(found_dirs),
sorted(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
52 changes: 52 additions & 0 deletions test/unit/extra_outdirs/proc_macro.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
// 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 list of directories to write to from an environment variable
let outdirs = env::var("EXTRA_OUTDIRS")
.expect("EXTRA_OUTDIRS environment variable must be set");

// 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");

// Create a map of directory name to output path
let mut path_map = std::collections::HashMap::new();
for entry in outdirs_paths.split(',') {
if let Some((dir, path)) = entry.split_once(':') {
path_map.insert(dir.trim(), path.trim());
}
}

// Write to the output directories declared by Bazel
for dir in outdirs.split(',') {
let dir = dir.trim();
if !dir.is_empty() {
// Get the output path for this directory
let dir_path = if let Some(path) = path_map.get(dir) {
PathBuf::from(path)
} else {
// Fallback to directory name if path not found
PathBuf::from(dir)
};

// 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()
}
Loading