Skip to content

Commit 7fb3e6d

Browse files
slackitodurin42
andauthored
rustc: add support for split debuginfo (bazelbuild#4092)
This is bazelbuild#3168 by @durin42, with fixes for some of the pre-merge checks. This PR adapts the original change to support path mapping when passing `-Zsplit-dwarf-out-dir=<path>`. In order to do this, it adds to `construct_arguments` the option to pass `(format_string, File)` as elements in the list form of `rustc_flags`. This PR also adds tests for the new functionality. The new changes on top of the original PR were written with assistance by Gemini. --------- Co-authored-by: Augie Fackler <augie@google.com>
1 parent 256b6b9 commit 7fb3e6d

2 files changed

Lines changed: 232 additions & 38 deletions

File tree

rust/private/rustc.bzl

Lines changed: 81 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -884,9 +884,14 @@ def _should_add_oso_prefix(toolchain):
884884
def _extract_allowed_unstable_features_from_flags(rust_flags, all_allowed_unstable_features):
885885
other_flags = []
886886
for flag in rust_flags:
887-
if flag.startswith("-Zallow-features="):
888-
all_allowed_unstable_features.extend(flag.removeprefix("-Zallow-features=").split(","))
887+
if type(flag) == "string":
888+
if flag.startswith("-Zallow-features="):
889+
all_allowed_unstable_features.extend(flag.removeprefix("-Zallow-features=").split(","))
890+
else:
891+
other_flags.append(flag)
889892
else:
893+
# It's a tuple/list (format_string, File), or at least not a string.
894+
# We assume it's not a -Zallow-features flag.
890895
other_flags.append(flag)
891896
return other_flags
892897

@@ -961,21 +966,22 @@ def construct_arguments(
961966
ambiguous_libs (dict): Ambiguous libs, see `_disambiguate_libs`
962967
output_hash (str): The hashed path of the crate root
963968
rust_flags (list or Args): Additional flags to pass to rustc. Accepts
964-
either a plain `list[str]` (folded into the main `rustc_flags`
965-
`Args` so flags intermix with the rest of the command line,
966-
with any `-Zallow-features=` entries extracted and merged
967-
with `unstable_rust_features_config`) or a `ctx.actions.args()`
968-
`Args` object (returned on the `args` struct as
969-
`extra_rustc_flags` and appended to `args.all` as a separate
969+
either a plain `list[str | (str, File)]` (folded into the main
970+
`rustc_flags` `Args` so flags intermix with the rest of the
971+
command line, with any `-Zallow-features=` entries extracted and
972+
merged with `unstable_rust_features_config`) or a
973+
`ctx.actions.args()` `Args` object (returned on the `args` struct
974+
as `extra_rustc_flags` and appended to `args.all` as a separate
970975
entry, since `Args` cannot be merged with one another). The
971976
`Args` form is opaque at analysis time, so any
972977
`-Zallow-features=` it carries passes through to rustc
973978
unchanged — callers that need it merged with
974-
`unstable_rust_features_config` should keep using the
975-
`list[str]` form. Use the `Args` form when the caller needs
976-
`Args.add_all` features such as `map_each` (e.g. for
977-
`File`-derived flags that must be rewritten by Bazel path
978-
mapping).
979+
`unstable_rust_features_config` should keep using the list form.
980+
Use the `Args` form when the caller needs `Args.add_all` features
981+
such as `map_each`. For individual `File`-derived flags that must
982+
be rewritten by Bazel path mapping, they can be passed as
983+
`(format_string, File)` tuples within the list form (e.g.
984+
`("-Zsplit-dwarf-out-dir=%s", dwo_outputs)`).
979985
out_dir (File, optional): The build script's output directory.
980986
When provided, the directory is handed to `process_wrapper`
981987
via an explicit `--out-dir <path>` arg sourced from a
@@ -1237,27 +1243,37 @@ def construct_arguments(
12371243
uniquify = True,
12381244
)
12391245

1240-
# `rust_flags` is either a plain `list[str]` or a `ctx.actions.args()`
1241-
# `Args` object. Lists are folded into the main `rustc_flags` `Args`
1242-
# here, with any `-Zallow-features=` entries extracted into
1243-
# `all_allowed_unstable_features` so they can be merged with
1244-
# `unstable_rust_features_config` and re-emitted as a single arg
1245-
# below. `Args` inputs cannot be merged with another `Args` and are
1246-
# opaque at analysis time, so we capture the caller's `Args` here
1247-
# and append it as a separate entry in `args.all` (after the
1248-
# main `rustc_flags` `Args`, consistent with the existing "later
1249-
# flags win" semantics). Any `-Zallow-features=` baked into an
1250-
# `Args` value passes through to rustc unchanged — callers that
1251-
# need it merged with `unstable_rust_features_config` should keep
1252-
# using the `list[str]` form.
1246+
# `rust_flags` is either a plain `list[str | (format_string, File)]` or a
1247+
# `ctx.actions.args()` `Args` object.
1248+
#
1249+
# - Lists are folded into the main `rustc_flags` `Args` here, with any
1250+
# `-Zallow-features=` entries extracted into
1251+
# `all_allowed_unstable_features` so they can be merged with
1252+
# `unstable_rust_features_config` and re-emitted as a single arg below.
1253+
#
1254+
# - `Args` inputs cannot be merged with another `Args` and are opaque at
1255+
# analysis time, so we capture the caller's `Args` here and append it as a
1256+
# separate entry in `args.all` (after the main `rustc_flags` `Args`,
1257+
# consistent with the existing "later flags win" semantics). Any
1258+
# `-Zallow-features=` baked into an `Args` value passes through to rustc
1259+
# unchanged — callers that need it merged with
1260+
# `unstable_rust_features_config` should keep using the list form.
12531261
rust_flags_args = None
12541262
if type(rust_flags) == "Args":
12551263
rust_flags_args = rust_flags
12561264
elif rust_flags:
1257-
rustc_flags.add_all(
1258-
_extract_allowed_unstable_features_from_flags(rust_flags, all_allowed_unstable_features),
1259-
map_each = map_flag,
1260-
)
1265+
for flag in _extract_allowed_unstable_features_from_flags(rust_flags, all_allowed_unstable_features):
1266+
if type(flag) in ["tuple", "list"] and len(flag) == 2:
1267+
rustc_flags.add_all(
1268+
[flag[1]],
1269+
format_each = flag[0],
1270+
expand_directories = False,
1271+
)
1272+
else:
1273+
if map_flag:
1274+
flag = map_flag(flag)
1275+
if flag != None:
1276+
rustc_flags.add(flag)
12611277

12621278
# Gather data path from crate_info since it is inherited from real crate for rust_doc and rust_test
12631279
# Deduplicate data paths due to https://github.com/bazelbuild/bazel/issues/14681
@@ -1706,6 +1722,22 @@ def rustc_compile_action(
17061722
elif ctx.attr.require_explicit_unstable_features == -1:
17071723
require_explicit_unstable_features = toolchain.require_explicit_unstable_features
17081724

1725+
use_split_debuginfo = (
1726+
feature_configuration and
1727+
cc_common.is_enabled(feature_configuration = feature_configuration, feature_name = "per_object_debug_info") and
1728+
ctx.fragments.cpp.fission_active_for_current_compilation_mode()
1729+
)
1730+
if use_split_debuginfo:
1731+
rust_flags = rust_flags + [
1732+
"--codegen=split-debuginfo=unpacked",
1733+
"--codegen=debuginfo=full",
1734+
]
1735+
fission_directory = crate_info.name + "_fission"
1736+
if output_hash:
1737+
fission_directory = fission_directory + "-" + output_hash
1738+
dwo_outputs = ctx.actions.declare_directory(fission_directory, sibling = crate_info.output)
1739+
rust_flags.append(("-Zsplit-dwarf-out-dir=%s", dwo_outputs))
1740+
17091741
args, env_from_args = construct_arguments(
17101742
ctx = ctx,
17111743
attr = attr,
@@ -1801,6 +1833,9 @@ def rustc_compile_action(
18011833
dsym_folder = ctx.actions.declare_directory(crate_info.output.basename + ".dSYM", sibling = crate_info.output)
18021834
action_outputs.append(dsym_folder)
18031835

1836+
if use_split_debuginfo:
1837+
action_outputs.append(dwo_outputs) # buildifier: disable=uninitialized
1838+
18041839
if ctx.executable._process_wrapper:
18051840
# Run as normal
18061841
ctx.actions.run(
@@ -1864,15 +1899,19 @@ def rustc_compile_action(
18641899
else:
18651900
fail("No process wrapper was defined for {}".format(ctx.label))
18661901

1902+
cco_args = {}
18671903
if experimental_use_cc_common_link:
18681904
# Wrap the main `.o` file into a compilation output suitable for
18691905
# cc_common.link. The main `.o` file is useful in both PIC and non-PIC
18701906
# modes.
1871-
compilation_outputs = cc_common.create_compilation_outputs(
1872-
objects = depset([output_o]),
1873-
pic_objects = depset([output_o]),
1874-
)
1875-
1907+
cco_args["objects"] = depset([output_o])
1908+
cco_args["pic_objects"] = depset([output_o])
1909+
if use_split_debuginfo:
1910+
cco_args["dwo_objects"] = depset([dwo_outputs]) # buildifier: disable=uninitialized
1911+
cco_args["pic_dwo_objects"] = depset([dwo_outputs]) # buildifier: disable=uninitialized
1912+
compilation_outputs = cc_common.create_compilation_outputs(**cco_args)
1913+
debug_context = cc_common.create_debug_context(compilation_outputs)
1914+
if experimental_use_cc_common_link:
18761915
malloc_library = ctx.attr._custom_malloc or ctx.attr.malloc
18771916

18781917
# Collect the linking contexts of the standard library and dependencies.
@@ -2045,7 +2084,7 @@ def rustc_compile_action(
20452084
else:
20462085
providers.extend([crate_info, dep_info])
20472086

2048-
providers += establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library)
2087+
providers += establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library, debug_context)
20492088

20502089
output_group_info = {}
20512090

@@ -2179,7 +2218,7 @@ def _add_codegen_units_flags(toolchain, emit, args):
21792218

21802219
args.add("-Ccodegen-units={}".format(toolchain._codegen_units))
21812220

2182-
def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library):
2221+
def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library, debug_context = None):
21832222
"""If the produced crate is suitable yield a CcInfo to allow for interop with cc rules
21842223
21852224
Args:
@@ -2190,6 +2229,7 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co
21902229
cc_toolchain (CcToolchainInfo): The current `CcToolchainInfo`
21912230
feature_configuration (FeatureConfiguration): Feature configuration to be queried.
21922231
interface_library (File): Optional interface library for cdylib crates on Windows.
2232+
debug_context (CcDebugContextInfo): The current debug context.
21932233
21942234
Returns:
21952235
list: A list containing the `CcInfo` provider and optionally `AllocatorLibrariesImplInfo`
@@ -2258,7 +2298,10 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co
22582298
)
22592299

22602300
cc_infos = [
2261-
CcInfo(linking_context = linking_context),
2301+
CcInfo(
2302+
linking_context = linking_context,
2303+
debug_context = debug_context,
2304+
),
22622305
toolchain.stdlib_linkflags,
22632306
]
22642307

test/unit/debug_info/debug_info_analysis_test.bzl

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
"""Analysis tests for debug info in cdylib and bin targets."""
22

33
load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts")
4+
load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
45
load("//rust:defs.bzl", "rust_binary", "rust_shared_library", "rust_test")
6+
load(
7+
"//test/unit:common.bzl",
8+
"assert_argv_contains",
9+
"assert_argv_contains_not",
10+
"assert_argv_contains_prefix",
11+
"assert_argv_contains_prefix_not",
12+
)
513

614
def _pdb_file_test_impl(ctx, expect_pdb_file):
715
env = analysistest.begin(ctx)
@@ -70,6 +78,112 @@ def _dsym_folder_test_impl(ctx):
7078

7179
dsym_folder_test = analysistest.make(_dsym_folder_test_impl)
7280

81+
def _fission_test_impl(ctx):
82+
env = analysistest.begin(ctx)
83+
target = analysistest.target_under_test(env)
84+
85+
action = None
86+
for a in target.actions:
87+
if a.mnemonic == "Rustc":
88+
action = a
89+
break
90+
asserts.true(env, action != None, "Expected to find Rustc action")
91+
92+
assert_argv_contains(env, action, "--codegen=split-debuginfo=unpacked")
93+
assert_argv_contains(env, action, "--codegen=debuginfo=full")
94+
assert_argv_contains_prefix(env, action, "-Zsplit-dwarf-out-dir=")
95+
96+
# Verify that the fission directory is in the action outputs.
97+
outputs = action.outputs.to_list()
98+
fission_dir_prefix = target.label.name + "_fission"
99+
found_fission_dir = False
100+
for out in outputs:
101+
if out.basename.startswith(fission_dir_prefix):
102+
found_fission_dir = True
103+
asserts.true(env, out.is_directory, "Expected fission output to be a directory")
104+
break
105+
asserts.true(env, found_fission_dir, "Expected to find fission directory in action outputs")
106+
107+
# Verify CcInfo contains the debug_context with the dwo files (if CcInfo is provided).
108+
if CcInfo in target:
109+
cc_info = target[CcInfo]
110+
asserts.true(env, hasattr(cc_info, "_debug_context"), "Expected CcInfo to have _debug_context")
111+
debug_context = cc_info._debug_context
112+
asserts.true(env, debug_context != None, "Expected _debug_context to be not None")
113+
114+
dwo_files = debug_context.files.to_list()
115+
asserts.equals(env, 1, len(dwo_files))
116+
asserts.true(
117+
env,
118+
dwo_files[0].basename.startswith(fission_dir_prefix),
119+
"Expected dwo file basename to start with %s, got %s" % (fission_dir_prefix, dwo_files[0].basename),
120+
)
121+
122+
pic_dwo_files = debug_context.pic_files.to_list()
123+
asserts.equals(env, 1, len(pic_dwo_files))
124+
asserts.true(
125+
env,
126+
pic_dwo_files[0].basename.startswith(fission_dir_prefix),
127+
"Expected pic dwo file basename to start with %s, got %s" % (fission_dir_prefix, pic_dwo_files[0].basename),
128+
)
129+
130+
return analysistest.end(env)
131+
132+
fission_test = analysistest.make(
133+
_fission_test_impl,
134+
config_settings = {
135+
"//command_line_option:features": ["per_object_debug_info"],
136+
"//command_line_option:fission": ["yes"],
137+
},
138+
)
139+
140+
def _no_fission_test_impl(ctx):
141+
env = analysistest.begin(ctx)
142+
target = analysistest.target_under_test(env)
143+
144+
action = None
145+
for a in target.actions:
146+
if a.mnemonic == "Rustc":
147+
action = a
148+
break
149+
asserts.true(env, action != None, "Expected to find Rustc action")
150+
151+
assert_argv_contains_not(env, action, "--codegen=split-debuginfo=unpacked")
152+
assert_argv_contains_not(env, action, "--codegen=debuginfo=full")
153+
assert_argv_contains_prefix_not(env, action, "-Zsplit-dwarf-out-dir=")
154+
155+
# Verify no fission directory in outputs.
156+
outputs = action.outputs.to_list()
157+
fission_dir_name = target.label.name + "_fission"
158+
for out in outputs:
159+
asserts.true(env, out.basename != fission_dir_name, "Did not expect fission directory in outputs")
160+
161+
# CcInfo's debug_context should not contain dwo files.
162+
if CcInfo in target:
163+
cc_info = target[CcInfo]
164+
if hasattr(cc_info, "_debug_context") and cc_info._debug_context:
165+
asserts.equals(env, 0, len(cc_info._debug_context.files.to_list()))
166+
asserts.equals(env, 0, len(cc_info._debug_context.pic_files.to_list()))
167+
168+
return analysistest.end(env)
169+
170+
no_fission_test = analysistest.make(
171+
_no_fission_test_impl,
172+
config_settings = {
173+
"//command_line_option:fission": ["no"],
174+
},
175+
)
176+
177+
# Fission requires `-Zsplit-dwarf-out-dir` which is a nightly-only flag.
178+
# Even though these are analysis tests (which normally don't execute actions),
179+
# `bazel coverage` forces compilation of the `target_under_test`. To prevent
180+
# compilation failures on the stable channel during coverage runs, we restrict
181+
# Fission tests to the nightly channel.
182+
_FISSION_COMPATIBILITY = ["@platforms//os:linux"] + select({
183+
"//rust/toolchain/channel:nightly": [],
184+
"//conditions:default": ["@platforms//:incompatible"],
185+
})
186+
73187
def debug_info_analysis_test_suite(name):
74188
"""Analysis tests for debug info in cdylib and bin targets.
75189
@@ -171,12 +285,49 @@ def debug_info_analysis_test_suite(name):
171285
target_compatible_with = ["@platforms//os:macos"],
172286
)
173287

288+
# Fission tests
289+
fission_test(
290+
name = "lib_fission_test",
291+
target_under_test = ":mylib",
292+
target_compatible_with = _FISSION_COMPATIBILITY,
293+
)
294+
no_fission_test(
295+
name = "lib_no_fission_test",
296+
target_under_test = ":mylib",
297+
)
298+
299+
fission_test(
300+
name = "bin_fission_test",
301+
target_under_test = ":myrustbin",
302+
target_compatible_with = _FISSION_COMPATIBILITY,
303+
)
304+
no_fission_test(
305+
name = "bin_no_fission_test",
306+
target_under_test = ":myrustbin",
307+
)
308+
309+
fission_test(
310+
name = "test_fission_test",
311+
target_under_test = ":myrusttest",
312+
target_compatible_with = _FISSION_COMPATIBILITY,
313+
)
314+
no_fission_test(
315+
name = "test_no_fission_test",
316+
target_under_test = ":myrusttest",
317+
)
318+
174319
native.test_suite(
175320
name = name,
176321
tests = [
177322
":lib_dsym_test",
178323
":bin_dsym_test",
179324
":test_dsym_test",
325+
":lib_fission_test",
326+
":lib_no_fission_test",
327+
":bin_fission_test",
328+
":bin_no_fission_test",
329+
":test_fission_test",
330+
":test_no_fission_test",
180331
] + [
181332
":lib_pdb_test_{}".format(compilation_mode)
182333
for compilation_mode in pdb_file_tests

0 commit comments

Comments
 (0)