Skip to content

Commit e410375

Browse files
committed
feat(rust_common): allow compile actions to declare that extra folders will be created
This technique is sometimes needed when a proc_macro derives information from the build, and it needs to be consumed by some other tool sort cursor wrote a new unit test for this PR fmt
1 parent cdaf15f commit e410375

File tree

7 files changed

+195
-0
lines changed

7 files changed

+195
-0
lines changed

rust/private/rust.bzl

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,11 @@ _common_attrs = {
732732
"edition": attr.string(
733733
doc = "The rust edition to use for this crate. Defaults to the edition specified in the rust_toolchain.",
734734
),
735+
"extra_outdirs": attr.string_list(
736+
doc = dedent("""\
737+
List of additional output directories which are expected to be written by the compiler.
738+
"""),
739+
),
735740
"lint_config": attr.label(
736741
doc = "Set of lints to apply when building this crate.",
737742
providers = [LintsInfo],

rust/private/rustc.bzl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1482,6 +1482,9 @@ def rustc_compile_action(
14821482
interface_library = ctx.actions.declare_file(crate_info.output.basename + ".lib", sibling = crate_info.output)
14831483
outputs.append(interface_library)
14841484

1485+
if hasattr(ctx.attr, "extra_outdirs"):
1486+
outputs.extend([ctx.actions.declare_directory(outdir) for outdir in ctx.attr.extra_outdirs])
1487+
14851488
# The action might generate extra output that we don't want to include in the `DefaultInfo` files.
14861489
action_outputs = list(outputs)
14871490
if rustc_output:
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
load("//rust:defs.bzl", "rust_library", "rust_proc_macro")
2+
load(":extra_outdirs_test.bzl", "extra_outdirs_test_suite")
3+
4+
rust_proc_macro(
5+
name = "write_outdirs_macro",
6+
srcs = ["proc_macro.rs"],
7+
edition = "2018",
8+
visibility = ["//test:__subpackages__"],
9+
)
10+
11+
rust_library(
12+
name = "lib",
13+
srcs = ["lib.rs"],
14+
edition = "2018",
15+
)
16+
17+
rust_library(
18+
name = "lib_with_outdirs",
19+
srcs = ["lib_with_outdirs.rs"],
20+
edition = "2018",
21+
extra_outdirs = [
22+
"test_dir",
23+
"another_dir",
24+
],
25+
proc_macro_deps = [":write_outdirs_macro"],
26+
rustc_env = {
27+
"EXTRA_OUTDIRS": "test_dir,another_dir",
28+
},
29+
)
30+
31+
extra_outdirs_test_suite(
32+
name = "extra_outdirs_test_suite",
33+
)
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
"""Unittest to verify extra_outdirs attribute adds directories to action outputs."""
2+
3+
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
4+
load("//test/unit:common.bzl", "assert_action_mnemonic")
5+
6+
def _extra_outdirs_present_test(ctx):
7+
env = analysistest.begin(ctx)
8+
target = analysistest.target_under_test(env)
9+
10+
# Find the Rustc action
11+
rustc_action = [action for action in target.actions if action.mnemonic == "Rustc"][0]
12+
assert_action_mnemonic(env, rustc_action, "Rustc")
13+
14+
# Get all outputs from the action
15+
outputs = rustc_action.outputs.to_list()
16+
17+
# Check that the expected directories are in the outputs
18+
expected_dirs = ctx.attr.expected_outdirs
19+
found_dirs = []
20+
21+
for output in outputs:
22+
# Check if this output is a directory (directories don't have extensions)
23+
# and if its basename matches one of our expected directories
24+
if output.is_directory:
25+
if output.basename in expected_dirs:
26+
found_dirs.append(output.basename)
27+
28+
# Verify all expected directories were found
29+
asserts.equals(
30+
env,
31+
sorted(found_dirs),
32+
sorted(expected_dirs),
33+
"Expected to find directories {expected} in action outputs, but found {found}".format(
34+
expected = expected_dirs,
35+
found = found_dirs,
36+
),
37+
)
38+
39+
return analysistest.end(env)
40+
41+
def _extra_outdirs_not_present_test(ctx):
42+
env = analysistest.begin(ctx)
43+
target = analysistest.target_under_test(env)
44+
45+
# Find the Rustc action
46+
rustc_action = [action for action in target.actions if action.mnemonic == "Rustc"][0]
47+
assert_action_mnemonic(env, rustc_action, "Rustc")
48+
49+
# Get all outputs from the action
50+
outputs = rustc_action.outputs.to_list()
51+
52+
# Check that no extra directories are present
53+
# We expect only the standard outputs (rlib, rmeta if pipelining, etc.)
54+
# but not any extra_outdirs directories
55+
unexpected_dirs = []
56+
for output in outputs:
57+
if output.is_directory:
58+
# Standard directories like .dSYM are okay, but we shouldn't have
59+
# any of the extra_outdirs we're testing for
60+
if output.basename in ["test_dir", "another_dir"]:
61+
unexpected_dirs.append(output.basename)
62+
63+
asserts.equals(
64+
env,
65+
[],
66+
unexpected_dirs,
67+
"Expected no extra_outdirs directories, but found {found}".format(
68+
found = unexpected_dirs,
69+
),
70+
)
71+
72+
return analysistest.end(env)
73+
74+
extra_outdirs_present_test = analysistest.make(
75+
_extra_outdirs_present_test,
76+
attrs = {
77+
"expected_outdirs": attr.string_list(
78+
mandatory = True,
79+
doc = "List of expected output directory names",
80+
),
81+
},
82+
)
83+
84+
extra_outdirs_not_present_test = analysistest.make(_extra_outdirs_not_present_test)
85+
86+
def extra_outdirs_test_suite(name):
87+
"""Entry-point macro called from the BUILD file.
88+
89+
Args:
90+
name (str): Name of the macro.
91+
"""
92+
extra_outdirs_not_present_test(
93+
name = "extra_outdirs_not_present_test",
94+
target_under_test = ":lib",
95+
)
96+
97+
extra_outdirs_present_test(
98+
name = "extra_outdirs_present_test",
99+
target_under_test = ":lib_with_outdirs",
100+
expected_outdirs = ["test_dir", "another_dir"],
101+
)
102+
103+
native.test_suite(
104+
name = name,
105+
tests = [
106+
":extra_outdirs_not_present_test",
107+
":extra_outdirs_present_test",
108+
],
109+
)
110+

test/unit/extra_outdirs/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
pub fn call() {}
2+
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
use write_outdirs_macro::write_to_outdirs;
2+
3+
write_to_outdirs!();
4+
5+
pub fn call() {}
6+
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// Similar to
2+
// https://github.com/napi-rs/napi-rs/blob/main/crates/macro/src/expand/typedef/type_def.rs#L11-L12
3+
// this proc macro has a side-effect of writing extra metadata directories.
4+
use proc_macro::TokenStream;
5+
use std::env;
6+
use std::fs;
7+
use std::path::PathBuf;
8+
9+
#[proc_macro]
10+
pub fn write_to_outdirs(_item: TokenStream) -> TokenStream {
11+
// Read the list of directories to write to from an environment variable
12+
if let Ok(outdirs) = env::var("EXTRA_OUTDIRS") {
13+
// Get the manifest directory (package directory) as the base path
14+
let manifest_dir = env::var("CARGO_MANIFEST_DIR")
15+
.unwrap_or_else(|_| ".".to_string());
16+
17+
for dir in outdirs.split(',') {
18+
let dir = dir.trim();
19+
if !dir.is_empty() {
20+
// Construct the full path: manifest_dir + directory name
21+
let dir_path = PathBuf::from(&manifest_dir).join(dir);
22+
// Create the directory if it doesn't exist
23+
if let Err(e) = fs::create_dir_all(&dir_path) {
24+
panic!("Failed to create directory {}: {:?}", dir_path.display(), e);
25+
}
26+
// Write a marker file to ensure the directory is created
27+
let marker_file = dir_path.join("marker.txt");
28+
if let Err(e) = fs::write(&marker_file, "created by proc-macro") {
29+
panic!("Failed to write marker file to {}: {:?}", marker_file.display(), e);
30+
}
31+
}
32+
}
33+
}
34+
TokenStream::new()
35+
}
36+

0 commit comments

Comments
 (0)