From a44c520c93f9433fa93adfab70a490210ba6e887 Mon Sep 17 00:00:00 2001 From: Augie Fackler Date: Wed, 8 Jan 2025 14:10:26 -0500 Subject: [PATCH 1/6] rustc: add support for split debuginfo (aka fission) This requires some helper logic in the process wrapper so we can move dwo files to predictable output locations, but otherwise it's mostly straightforward. This requires a bazel built after d2c79821cd45b30eeaec2efc3bd5fbd26f37d3c2, but that's old enough it landed in bazel 8. --- rust/private/rustc.bzl | 38 ++++++++++++++++++++++++++++++-------- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 69e0d0550c..5c27511d1f 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -1682,6 +1682,18 @@ def rustc_compile_action( elif ctx.attr.require_explicit_unstable_features == -1: require_explicit_unstable_features = toolchain.require_explicit_unstable_features + use_split_debuginfo = cc_common.is_enabled(feature_configuration = feature_configuration, feature_name = "per_object_debug_info") and ctx.fragments.cpp.fission_active_for_current_compilation_mode() + if use_split_debuginfo: + rust_flags = rust_flags + [ + "--codegen=split-debuginfo=unpacked", + "--codegen=debuginfo=full", + ] + fission_directory = crate_info.name + "_fission" + if output_hash: + fission_directory = fission_directory + "-" + output_hash + dwo_outputs = ctx.actions.declare_directory(fission_directory, sibling = crate_info.output) + rust_flags.append("-Zsplit-dwarf-out-dir=%s" % dwo_outputs.path) + args, env_from_args = construct_arguments( ctx = ctx, attr = attr, @@ -1777,6 +1789,9 @@ def rustc_compile_action( dsym_folder = ctx.actions.declare_directory(crate_info.output.basename + ".dSYM", sibling = crate_info.output) action_outputs.append(dsym_folder) + if use_split_debuginfo: + action_outputs.append(dwo_outputs) + if ctx.executable._process_wrapper: # Run as normal ctx.actions.run( @@ -1840,15 +1855,19 @@ def rustc_compile_action( else: fail("No process wrapper was defined for {}".format(ctx.label)) + cco_args = {} if experimental_use_cc_common_link: # Wrap the main `.o` file into a compilation output suitable for # cc_common.link. The main `.o` file is useful in both PIC and non-PIC # modes. - compilation_outputs = cc_common.create_compilation_outputs( - objects = depset([output_o]), - pic_objects = depset([output_o]), - ) - + cco_args["objects"] = depset([output_o]) + cco_args["pic_objects"] = depset([output_o]) + if use_split_debuginfo: + cco_args["dwo_objects"] = depset([dwo_outputs]) # buildifier: disable=uninitialized + cco_args["pic_dwo_objects"] = depset([dwo_outputs]) # buildifier: disable=uninitialized + compilation_outputs = cc_common.create_compilation_outputs(**cco_args) + debug_context = cc_common.create_debug_context(compilation_outputs) + if experimental_use_cc_common_link: malloc_library = ctx.attr._custom_malloc or ctx.attr.malloc # Collect the linking contexts of the standard library and dependencies. @@ -2021,7 +2040,7 @@ def rustc_compile_action( else: providers.extend([crate_info, dep_info]) - providers += establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library) + providers += establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library, debug_context) output_group_info = {} @@ -2155,7 +2174,7 @@ def _add_codegen_units_flags(toolchain, emit, args): args.add("-Ccodegen-units={}".format(toolchain._codegen_units)) -def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library): +def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_configuration, interface_library, debug_context = None): """If the produced crate is suitable yield a CcInfo to allow for interop with cc rules Args: @@ -2234,7 +2253,10 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co ) cc_infos = [ - CcInfo(linking_context = linking_context), + CcInfo( + linking_context = linking_context, + debug_context = debug_context, + ), toolchain.stdlib_linkflags, ] From 169eb361efbf486b77ecad4210fbb940c15574c0 Mon Sep 17 00:00:00 2001 From: Jorge Gorbe Moya Date: Thu, 18 Jun 2026 16:03:56 +0000 Subject: [PATCH 2/6] Fix buildifier-lint errors in #3168 --- rust/private/rustc.bzl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 5c27511d1f..5699512622 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -1790,7 +1790,7 @@ def rustc_compile_action( action_outputs.append(dsym_folder) if use_split_debuginfo: - action_outputs.append(dwo_outputs) + action_outputs.append(dwo_outputs) # buildifier: disable=uninitialized if ctx.executable._process_wrapper: # Run as normal @@ -2185,6 +2185,7 @@ def establish_cc_info(ctx, attr, crate_info, toolchain, cc_toolchain, feature_co cc_toolchain (CcToolchainInfo): The current `CcToolchainInfo` feature_configuration (FeatureConfiguration): Feature configuration to be queried. interface_library (File): Optional interface library for cdylib crates on Windows. + debug_context (CcDebugContextInfo): The current debug context. Returns: list: A list containing the `CcInfo` provider and optionally `AllocatorLibrariesImplInfo` From 2feb8928f545855f8f8dad873f1965f842dcee4c Mon Sep 17 00:00:00 2001 From: Jorge Gorbe Moya Date: Fri, 19 Jun 2026 09:08:22 +0000 Subject: [PATCH 3/6] Check for the `per_object_debug_info` feature only if `feature_configuration` is present. --- rust/private/rustc.bzl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 5699512622..22393aa51a 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -1682,7 +1682,11 @@ def rustc_compile_action( elif ctx.attr.require_explicit_unstable_features == -1: require_explicit_unstable_features = toolchain.require_explicit_unstable_features - use_split_debuginfo = cc_common.is_enabled(feature_configuration = feature_configuration, feature_name = "per_object_debug_info") and ctx.fragments.cpp.fission_active_for_current_compilation_mode() + use_split_debuginfo = ( + feature_configuration and + cc_common.is_enabled(feature_configuration = feature_configuration, feature_name = "per_object_debug_info") and + ctx.fragments.cpp.fission_active_for_current_compilation_mode() + ) if use_split_debuginfo: rust_flags = rust_flags + [ "--codegen=split-debuginfo=unpacked", From 535a292653a633bb2abebb5fab8f2979b072f5f4 Mon Sep 17 00:00:00 2001 From: Jorge Gorbe Moya Date: Fri, 26 Jun 2026 15:06:27 +0000 Subject: [PATCH 4/6] Support path mapping for -Zsplit-dwarf-out-dir. Add tests. The `rust_flags` argument in `construct_arguments` now supports (format_string, File) tuples as elements in addition to strings, when using the list format. This allows bazel to apply path mapping on the dwarf output directory flag. Also added analysis tests for `--fission=yes` and `--fission=no`. The test cases were written by Gemini and reviewed by me. Assisted-by: Gemini --- rust/private/rustc.bzl | 78 ++++++---- .../debug_info/debug_info_analysis_test.bzl | 141 ++++++++++++++++++ 2 files changed, 188 insertions(+), 31 deletions(-) diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl index 22393aa51a..4210c765b3 100644 --- a/rust/private/rustc.bzl +++ b/rust/private/rustc.bzl @@ -884,9 +884,14 @@ def _should_add_oso_prefix(toolchain): def _extract_allowed_unstable_features_from_flags(rust_flags, all_allowed_unstable_features): other_flags = [] for flag in rust_flags: - if flag.startswith("-Zallow-features="): - all_allowed_unstable_features.extend(flag.removeprefix("-Zallow-features=").split(",")) + if type(flag) == "string": + if flag.startswith("-Zallow-features="): + all_allowed_unstable_features.extend(flag.removeprefix("-Zallow-features=").split(",")) + else: + other_flags.append(flag) else: + # It's a tuple/list (format_string, File), or at least not a string. + # We assume it's not a -Zallow-features flag. other_flags.append(flag) return other_flags @@ -952,21 +957,22 @@ def construct_arguments( ambiguous_libs (dict): Ambiguous libs, see `_disambiguate_libs` output_hash (str): The hashed path of the crate root rust_flags (list or Args): Additional flags to pass to rustc. Accepts - either a plain `list[str]` (folded into the main `rustc_flags` - `Args` so flags intermix with the rest of the command line, - with any `-Zallow-features=` entries extracted and merged - with `unstable_rust_features_config`) or a `ctx.actions.args()` - `Args` object (returned on the `args` struct as - `extra_rustc_flags` and appended to `args.all` as a separate + either a plain `list[str | (str, File)]` (folded into the main + `rustc_flags` `Args` so flags intermix with the rest of the + command line, with any `-Zallow-features=` entries extracted and + merged with `unstable_rust_features_config`) or a + `ctx.actions.args()` `Args` object (returned on the `args` struct + as `extra_rustc_flags` and appended to `args.all` as a separate entry, since `Args` cannot be merged with one another). The `Args` form is opaque at analysis time, so any `-Zallow-features=` it carries passes through to rustc unchanged — callers that need it merged with - `unstable_rust_features_config` should keep using the - `list[str]` form. Use the `Args` form when the caller needs - `Args.add_all` features such as `map_each` (e.g. for - `File`-derived flags that must be rewritten by Bazel path - mapping). + `unstable_rust_features_config` should keep using the list form. + Use the `Args` form when the caller needs `Args.add_all` features + such as `map_each`. For individual `File`-derived flags that must + be rewritten by Bazel path mapping, they can be passed as + `(format_string, File)` tuples within the list form (e.g. + `("-Zsplit-dwarf-out-dir=%s", dwo_outputs)`). out_dir (File, optional): The build script's output directory. When provided, the directory is handed to `process_wrapper` via an explicit `--out-dir ` arg sourced from a @@ -1228,27 +1234,37 @@ def construct_arguments( uniquify = True, ) - # `rust_flags` is either a plain `list[str]` or a `ctx.actions.args()` - # `Args` object. Lists are folded into the main `rustc_flags` `Args` - # here, with any `-Zallow-features=` entries extracted into - # `all_allowed_unstable_features` so they can be merged with - # `unstable_rust_features_config` and re-emitted as a single arg - # below. `Args` inputs cannot be merged with another `Args` and are - # opaque at analysis time, so we capture the caller's `Args` here - # and append it as a separate entry in `args.all` (after the - # main `rustc_flags` `Args`, consistent with the existing "later - # flags win" semantics). Any `-Zallow-features=` baked into an - # `Args` value passes through to rustc unchanged — callers that - # need it merged with `unstable_rust_features_config` should keep - # using the `list[str]` form. + # `rust_flags` is either a plain `list[str | (format_string, File)]` or a + # `ctx.actions.args()` `Args` object. + # + # - Lists are folded into the main `rustc_flags` `Args` here, with any + # `-Zallow-features=` entries extracted into + # `all_allowed_unstable_features` so they can be merged with + # `unstable_rust_features_config` and re-emitted as a single arg below. + # + # - `Args` inputs cannot be merged with another `Args` and are opaque at + # analysis time, so we capture the caller's `Args` here and append it as a + # separate entry in `args.all` (after the main `rustc_flags` `Args`, + # consistent with the existing "later flags win" semantics). Any + # `-Zallow-features=` baked into an `Args` value passes through to rustc + # unchanged — callers that need it merged with + # `unstable_rust_features_config` should keep using the list form. rust_flags_args = None if type(rust_flags) == "Args": rust_flags_args = rust_flags elif rust_flags: - rustc_flags.add_all( - _extract_allowed_unstable_features_from_flags(rust_flags, all_allowed_unstable_features), - map_each = map_flag, - ) + for flag in _extract_allowed_unstable_features_from_flags(rust_flags, all_allowed_unstable_features): + if type(flag) in ["tuple", "list"] and len(flag) == 2: + rustc_flags.add_all( + [flag[1]], + format_each = flag[0], + expand_directories = False, + ) + else: + if map_flag: + flag = map_flag(flag) + if flag != None: + rustc_flags.add(flag) # Gather data path from crate_info since it is inherited from real crate for rust_doc and rust_test # Deduplicate data paths due to https://github.com/bazelbuild/bazel/issues/14681 @@ -1696,7 +1712,7 @@ def rustc_compile_action( if output_hash: fission_directory = fission_directory + "-" + output_hash dwo_outputs = ctx.actions.declare_directory(fission_directory, sibling = crate_info.output) - rust_flags.append("-Zsplit-dwarf-out-dir=%s" % dwo_outputs.path) + rust_flags.append(("-Zsplit-dwarf-out-dir=%s", dwo_outputs)) args, env_from_args = construct_arguments( ctx = ctx, diff --git a/test/unit/debug_info/debug_info_analysis_test.bzl b/test/unit/debug_info/debug_info_analysis_test.bzl index ad5bbf6b36..e873cc22a4 100644 --- a/test/unit/debug_info/debug_info_analysis_test.bzl +++ b/test/unit/debug_info/debug_info_analysis_test.bzl @@ -1,7 +1,15 @@ """Analysis tests for debug info in cdylib and bin targets.""" load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts") +load("@rules_cc//cc/common:cc_info.bzl", "CcInfo") load("//rust:defs.bzl", "rust_binary", "rust_shared_library", "rust_test") +load( + "//test/unit:common.bzl", + "assert_argv_contains", + "assert_argv_contains_not", + "assert_argv_contains_prefix", + "assert_argv_contains_prefix_not", +) def _pdb_file_test_impl(ctx, expect_pdb_file): env = analysistest.begin(ctx) @@ -70,6 +78,102 @@ def _dsym_folder_test_impl(ctx): dsym_folder_test = analysistest.make(_dsym_folder_test_impl) +def _fission_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + + action = None + for a in target.actions: + if a.mnemonic == "Rustc": + action = a + break + asserts.true(env, action != None, "Expected to find Rustc action") + + assert_argv_contains(env, action, "--codegen=split-debuginfo=unpacked") + assert_argv_contains(env, action, "--codegen=debuginfo=full") + assert_argv_contains_prefix(env, action, "-Zsplit-dwarf-out-dir=") + + # Verify that the fission directory is in the action outputs. + outputs = action.outputs.to_list() + fission_dir_prefix = target.label.name + "_fission" + found_fission_dir = False + for out in outputs: + if out.basename.startswith(fission_dir_prefix): + found_fission_dir = True + asserts.true(env, out.is_directory, "Expected fission output to be a directory") + break + asserts.true(env, found_fission_dir, "Expected to find fission directory in action outputs") + + # Verify CcInfo contains the debug_context with the dwo files (if CcInfo is provided). + if CcInfo in target: + cc_info = target[CcInfo] + asserts.true(env, hasattr(cc_info, "_debug_context"), "Expected CcInfo to have _debug_context") + debug_context = cc_info._debug_context + asserts.true(env, debug_context != None, "Expected _debug_context to be not None") + + dwo_files = debug_context.files.to_list() + asserts.equals(env, 1, len(dwo_files)) + asserts.true( + env, + dwo_files[0].basename.startswith(fission_dir_prefix), + "Expected dwo file basename to start with %s, got %s" % (fission_dir_prefix, dwo_files[0].basename), + ) + + pic_dwo_files = debug_context.pic_files.to_list() + asserts.equals(env, 1, len(pic_dwo_files)) + asserts.true( + env, + pic_dwo_files[0].basename.startswith(fission_dir_prefix), + "Expected pic dwo file basename to start with %s, got %s" % (fission_dir_prefix, pic_dwo_files[0].basename), + ) + + return analysistest.end(env) + +fission_test = analysistest.make( + _fission_test_impl, + config_settings = { + "//command_line_option:features": ["per_object_debug_info"], + "//command_line_option:fission": ["yes"], + }, +) + +def _no_fission_test_impl(ctx): + env = analysistest.begin(ctx) + target = analysistest.target_under_test(env) + + action = None + for a in target.actions: + if a.mnemonic == "Rustc": + action = a + break + asserts.true(env, action != None, "Expected to find Rustc action") + + assert_argv_contains_not(env, action, "--codegen=split-debuginfo=unpacked") + assert_argv_contains_not(env, action, "--codegen=debuginfo=full") + assert_argv_contains_prefix_not(env, action, "-Zsplit-dwarf-out-dir=") + + # Verify no fission directory in outputs. + outputs = action.outputs.to_list() + fission_dir_name = target.label.name + "_fission" + for out in outputs: + asserts.true(env, out.basename != fission_dir_name, "Did not expect fission directory in outputs") + + # CcInfo's debug_context should not contain dwo files. + if CcInfo in target: + cc_info = target[CcInfo] + if hasattr(cc_info, "debug_context") and cc_info.debug_context: + asserts.equals(env, 0, len(cc_info.debug_context.files.to_list())) + asserts.equals(env, 0, len(cc_info.debug_context.pic_files.to_list())) + + return analysistest.end(env) + +no_fission_test = analysistest.make( + _no_fission_test_impl, + config_settings = { + "//command_line_option:fission": ["no"], + }, +) + def debug_info_analysis_test_suite(name): """Analysis tests for debug info in cdylib and bin targets. @@ -171,12 +275,49 @@ def debug_info_analysis_test_suite(name): target_compatible_with = ["@platforms//os:macos"], ) + # Fission tests + fission_test( + name = "lib_fission_test", + target_under_test = ":mylib", + target_compatible_with = ["@platforms//os:linux"], + ) + no_fission_test( + name = "lib_no_fission_test", + target_under_test = ":mylib", + ) + + fission_test( + name = "bin_fission_test", + target_under_test = ":myrustbin", + target_compatible_with = ["@platforms//os:linux"], + ) + no_fission_test( + name = "bin_no_fission_test", + target_under_test = ":myrustbin", + ) + + fission_test( + name = "test_fission_test", + target_under_test = ":myrusttest", + target_compatible_with = ["@platforms//os:linux"], + ) + no_fission_test( + name = "test_no_fission_test", + target_under_test = ":myrusttest", + ) + native.test_suite( name = name, tests = [ ":lib_dsym_test", ":bin_dsym_test", ":test_dsym_test", + ":lib_fission_test", + ":lib_no_fission_test", + ":bin_fission_test", + ":bin_no_fission_test", + ":test_fission_test", + ":test_no_fission_test", ] + [ ":lib_pdb_test_{}".format(compilation_mode) for compilation_mode in pdb_file_tests From ad83f0ee77f50c3e21b12bb46de78bddb27c65fa Mon Sep 17 00:00:00 2001 From: Jorge Gorbe Moya Date: Mon, 29 Jun 2026 11:12:49 +0000 Subject: [PATCH 5/6] Mark fission tests as incompatible with stable. Fission support requires passing `-Zsplit-dwarf-out-dir` which is not available in stable rustc. --- .../unit/debug_info/debug_info_analysis_test.bzl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/test/unit/debug_info/debug_info_analysis_test.bzl b/test/unit/debug_info/debug_info_analysis_test.bzl index e873cc22a4..6773eb5bd7 100644 --- a/test/unit/debug_info/debug_info_analysis_test.bzl +++ b/test/unit/debug_info/debug_info_analysis_test.bzl @@ -174,6 +174,16 @@ no_fission_test = analysistest.make( }, ) +# Fission requires `-Zsplit-dwarf-out-dir` which is a nightly-only flag. +# Even though these are analysis tests (which normally don't execute actions), +# `bazel coverage` forces compilation of the `target_under_test`. To prevent +# compilation failures on the stable channel during coverage runs, we restrict +# Fission tests to the nightly channel. +_FISSION_COMPATIBILITY = ["@platforms//os:linux"] + select({ + "//rust/toolchain/channel:nightly": [], + "//conditions:default": ["@platforms//:incompatible"], +}) + def debug_info_analysis_test_suite(name): """Analysis tests for debug info in cdylib and bin targets. @@ -279,7 +289,7 @@ def debug_info_analysis_test_suite(name): fission_test( name = "lib_fission_test", target_under_test = ":mylib", - target_compatible_with = ["@platforms//os:linux"], + target_compatible_with = _FISSION_COMPATIBILITY, ) no_fission_test( name = "lib_no_fission_test", @@ -289,7 +299,7 @@ def debug_info_analysis_test_suite(name): fission_test( name = "bin_fission_test", target_under_test = ":myrustbin", - target_compatible_with = ["@platforms//os:linux"], + target_compatible_with = _FISSION_COMPATIBILITY, ) no_fission_test( name = "bin_no_fission_test", @@ -299,7 +309,7 @@ def debug_info_analysis_test_suite(name): fission_test( name = "test_fission_test", target_under_test = ":myrusttest", - target_compatible_with = ["@platforms//os:linux"], + target_compatible_with = _FISSION_COMPATIBILITY, ) no_fission_test( name = "test_no_fission_test", From a0b39d0cea8bc4e34ea1c30d9c171c1b9ff67b3c Mon Sep 17 00:00:00 2001 From: Jorge Gorbe Moya Date: Mon, 29 Jun 2026 11:40:09 +0000 Subject: [PATCH 6/6] Use cc_info._debug_context to get the debug context in tests. --- test/unit/debug_info/debug_info_analysis_test.bzl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/unit/debug_info/debug_info_analysis_test.bzl b/test/unit/debug_info/debug_info_analysis_test.bzl index 6773eb5bd7..d9fb89c5b1 100644 --- a/test/unit/debug_info/debug_info_analysis_test.bzl +++ b/test/unit/debug_info/debug_info_analysis_test.bzl @@ -161,9 +161,9 @@ def _no_fission_test_impl(ctx): # CcInfo's debug_context should not contain dwo files. if CcInfo in target: cc_info = target[CcInfo] - if hasattr(cc_info, "debug_context") and cc_info.debug_context: - asserts.equals(env, 0, len(cc_info.debug_context.files.to_list())) - asserts.equals(env, 0, len(cc_info.debug_context.pic_files.to_list())) + if hasattr(cc_info, "_debug_context") and cc_info._debug_context: + asserts.equals(env, 0, len(cc_info._debug_context.files.to_list())) + asserts.equals(env, 0, len(cc_info._debug_context.pic_files.to_list())) return analysistest.end(env)