[libc-commits] [libc] [llvm] [libc][bazel] Use LLVM-libc startup objects in full-build tests (PR #219262)

Jackson Stogel via libc-commits libc-commits at lists.llvm.org
Mon Sep 14 11:56:00 PDT 2026


https://github.com/jtstogel updated https://github.com/llvm/llvm-project/pull/219262

>From 9ad8cd69cc99cece23fdd281b1dc21fea2757d4a Mon Sep 17 00:00:00 2001
From: jtstogel <jtstogel at gmail.com>
Date: Sat, 12 Sep 2026 17:18:13 -0700
Subject: [PATCH] [bazel][libc][test] Allow LLVM-libc tests to be run in
 full-build mode

This PR makes the required changes to run LLVM-libc full build tests in Bazel. After this PR, most tests pass:

```
bazel test @llvm-project//libc/test/... --config=ci -- at llvm-project//libc:build_mode=full --keep_going
Executed 680 out of 853 tests: 681 tests pass and 172 fail to build.
```

Follow on PRs will fix specific tests that are failing (lots of missing deps etc).

This PR mostly involves propagating the correct dependencies and compiler/linker options from [`add_libc_hermetic`](https://github.com/llvm/llvm-project/blob/4098f568c46e06b6df470111868b4c165dd80f4d/libc/cmake/modules/LLVMLibCTestRules.cmake#L750). Slightly more involved changes:
- `BazelFilePath.cpp` also had to be fixed so that it doesn't depend on the non-namespaced `getenv` function. This PR models after https://github.com/llvm/llvm-project/commit/ee407f7e7069cccfdc1815de07e17cebf83d19f9 in order to conditionally use LLVM-libc's getenv under full-build mode.
- All tests have a dependency against `crt1.o` when run under full-build. This PR updates `merge_relocatable_object` so it also exposes a `CcInfo` with the merged object file and its transitive deps. In order to do so, this PR also makes the logic a little more careful about PIC vs non-PIC deps.
---
 libc/test/UnitTest/BazelFilePath.cpp          |  16 ++-
 .../llvm-project-overlay/libc/BUILD.bazel     |   8 +-
 .../libc/startup/linux/startup_rules.bzl      | 111 ++++++++++++++++--
 .../libc/test/UnitTest/BUILD.bazel            |  51 +++++++-
 .../libc/test/libc_test_rules.bzl             |  76 +++++++++---
 5 files changed, 226 insertions(+), 36 deletions(-)

diff --git a/libc/test/UnitTest/BazelFilePath.cpp b/libc/test/UnitTest/BazelFilePath.cpp
index 03ac56f083b9c3..a2ae6b4f915579 100644
--- a/libc/test/UnitTest/BazelFilePath.cpp
+++ b/libc/test/UnitTest/BazelFilePath.cpp
@@ -8,18 +8,28 @@
 
 #include "LibcTest.h"
 
-#include <stdlib.h>
-
 #include "src/__support/CPP/string.h"
 #include "src/__support/c_string.h"
 #include "src/__support/macros/config.h"
 
+#ifdef LIBC_FULL_BUILD
+#include "src/stdlib/getenv.h"
+
+#define LIBC_IMPL LIBC_NAMESPACE
+
+#else // Overlay mode
+#include <stdlib.h>
+
+#define LIBC_IMPL
+#endif
+
 namespace LIBC_NAMESPACE_DECL {
 namespace testing {
 
 CString libc_make_test_file_path_func(const char *file_name) {
   // This is the path to the folder bazel wants the test outputs written to.
-  const char *UNDECLARED_OUTPUTS_PATH = getenv("TEST_UNDECLARED_OUTPUTS_DIR");
+  const char *UNDECLARED_OUTPUTS_PATH =
+      LIBC_IMPL::getenv("TEST_UNDECLARED_OUTPUTS_DIR");
   // Do something sensible if not run under bazel, otherwise this may segfault
   // when constructing the string.
   if (UNDECLARED_OUTPUTS_PATH == nullptr)
diff --git a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
index 2004a67ef10172..073d432a15b968 100644
--- a/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/libc/BUILD.bazel
@@ -1480,10 +1480,10 @@ libc_support_library(
 libc_support_library(
     name = "types_char8_t",
     hdrs = ["hdr/types/char8_t.h"],
-    deps = [
-        ":hdr_uchar_overlay",
-        ":llvm_libc_types_char8_t",
-    ],
+    deps = select({
+        ":full_build": [":llvm_libc_types_char8_t"],
+        "//conditions:default": [":hdr_uchar_overlay"],
+    }),
 )
 
 ############################### Support libraries ##############################
diff --git a/utils/bazel/llvm-project-overlay/libc/startup/linux/startup_rules.bzl b/utils/bazel/llvm-project-overlay/libc/startup/linux/startup_rules.bzl
index 224d7474c4206c..d626acc489a11c 100644
--- a/utils/bazel/llvm-project-overlay/libc/startup/linux/startup_rules.bzl
+++ b/utils/bazel/llvm-project-overlay/libc/startup/linux/startup_rules.bzl
@@ -11,16 +11,44 @@ load("@rules_cc//cc/common:cc_common.bzl", "cc_common")
 load("@rules_cc//cc/common:cc_info.bzl", "CcInfo")
 load("//libc:libc_build_rules.bzl", "libc_startup_library")
 
-def _get_compilation_outputs(deps):
-    outputs = []
+def _get_object_files(deps):
+    """Gets object files that are directly provided by a target in deps.
+
+    Args:
+        deps: Targets from which to extract object files.
+
+    Returns:
+        A tuple of (object files, PIC object files) from linker inputs of deps.
+    """
+    objects = []
+    pic_objects = []
     for dep in deps:
-        if OutputGroupInfo in dep and "compilation_outputs" in dep[OutputGroupInfo]:
-            outputs.extend(dep[OutputGroupInfo].compilation_outputs.to_list())
-    return outputs
+        if CcInfo not in dep:
+            fail("CcInfo not found in dep {}".format(dep.label))
+
+        for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list():
+            if linker_input.owner != dep.label:
+                continue  # Only interested in directly owned linker inputs.
+
+            for lib in linker_input.libraries:
+                objects.extend(lib.objects or [])
+                pic_objects.extend(lib.pic_objects or [])
+
+    return objects, pic_objects
+
+def _get_object_files_preferring_pic(deps):
+    """Returns object files from deps, and whether or not they are PIC."""
+    objects, pic_objects = _get_object_files(deps)
+    if pic_objects:
+        return pic_objects, True
+    elif objects:
+        return objects, False
+    else:
+        fail("No object files found in deps")
 
 def _extract_object_file_impl(ctx):
     output = ctx.actions.declare_file(ctx.label.name + ".o")
-    input_objs = _get_compilation_outputs([ctx.attr.dep])
+    input_objs, _ = _get_object_files_preferring_pic([ctx.attr.dep])
     if len(input_objs) != 1:
         fail("Expected exactly one input object, got: {}".format(input_objs))
 
@@ -49,7 +77,7 @@ def libc_startup_object(name, src, **kwargs):
     Args:
         name: The name of the target.
         src: The C++ source file to compile.
-        **kwargs: Other arguments to
+        **kwargs: Other arguments to libc_startup_library.
     """
     library_name = name + "_lib"
     libc_startup_library(
@@ -96,11 +124,64 @@ def _filter_flags(
 
     return filtered_flags
 
+def _merged_relocatable_object_linking_context(
+        ctx,
+        merged_object,
+        is_pic,
+        feature_configuration,
+        cc_toolchain):
+    """Creates a linking context for a merged relocatable object.
+
+    This linking context consists of the merged object file
+    and all the linking contexts of its indirect deps.
+
+    Args:
+      ctx: The context of the rule.
+      merged_object: The merged relocatable object file.
+      is_pic: Whether the merged object is PIC.
+      feature_configuration: The feature configuration of the rule.
+      cc_toolchain: The cc toolchain of the rule.
+
+    Returns:
+      A linking context that may be used to depend on the merged relocatable object.
+    """
+
+    # Gather transitive inputs that only originate from indirect deps.
+    # Direct deps have already been merged into merged_object
+    # and so shouldn't be propagated.
+    direct_dep_labels = set([dep.label for dep in ctx.attr.deps])
+    indirect_dep_linker_inputs = [
+        linker_input
+        for dep in ctx.attr.deps
+        for linker_input in dep[CcInfo].linking_context.linker_inputs.to_list()
+        if linker_input.owner not in direct_dep_labels
+    ]
+    indirect_deps_linking_context = cc_common.create_linking_context(
+        linker_inputs = depset(indirect_dep_linker_inputs),
+    )
+
+    linking_context, _ = cc_common.create_linking_context_from_compilation_outputs(
+        actions = ctx.actions,
+        name = ctx.label.name,
+        feature_configuration = feature_configuration,
+        cc_toolchain = cc_toolchain,
+        compilation_outputs = cc_common.create_compilation_outputs(
+            # PIC objects are usable downstream even in non-PIC executables.
+            objects = depset([merged_object]),
+            pic_objects = depset([merged_object]) if is_pic else None,
+        ),
+        linking_contexts = [indirect_deps_linking_context],
+    )
+    return linking_context
+
 def _merge_relocatable_object_impl(ctx):
     cc_toolchain = find_cc_toolchain(ctx)
     output = ctx.actions.declare_file(ctx.label.name + ".o")
 
-    input_objs = _get_compilation_outputs(ctx.attr.deps)
+    # A more general approach would be to generate both PIC and non-PIC merged
+    # objects, but just preferring PIC when available should be fine until there
+    # is a specific need to propagate both.
+    input_objs, is_pic = _get_object_files_preferring_pic(ctx.attr.deps)
 
     feature_configuration = cc_common.configure_features(
         ctx = ctx,
@@ -152,7 +233,18 @@ def _merge_relocatable_object_impl(ctx):
         use_default_shell_env = True,
     )
 
-    return [DefaultInfo(files = depset([output]))]
+    return [
+        DefaultInfo(files = depset([output])),
+        CcInfo(
+            linking_context = _merged_relocatable_object_linking_context(
+                ctx,
+                merged_object = output,
+                is_pic = is_pic,
+                feature_configuration = feature_configuration,
+                cc_toolchain = cc_toolchain,
+            ),
+        ),
+    ]
 
 merge_relocatable_object = rule(
     implementation = _merge_relocatable_object_impl,
@@ -170,4 +262,5 @@ merge_relocatable_object = rule(
     },
     toolchains = use_cc_toolchain(),
     fragments = ["cpp"],
+    provides = [CcInfo, DefaultInfo],
 )
diff --git a/utils/bazel/llvm-project-overlay/libc/test/UnitTest/BUILD.bazel b/utils/bazel/llvm-project-overlay/libc/test/UnitTest/BUILD.bazel
index 35704e71d13069..afc165786cada5 100644
--- a/utils/bazel/llvm-project-overlay/libc/test/UnitTest/BUILD.bazel
+++ b/utils/bazel/llvm-project-overlay/libc/test/UnitTest/BUILD.bazel
@@ -78,20 +78,65 @@ libc_test_library(
         "//libc:func_realloc",
         "//libc:hdr_stdint_proxy",
         "//libc:llvm_libc_macros_stdfix_macros",
-        "//llvm:Support",
-    ],
+    ] + select({
+        "//libc:full_build": [
+            "//libc:clock",
+            "//libc:close",
+            "//libc:exit",
+            "//libc:fflush",
+            "//libc:fork",
+            "//libc:getenv",
+            "//libc:kill",
+            "//libc:llvm_libc_macros_poll_macros",
+            "//libc:llvm_libc_macros_signal_macros",
+            "//libc:llvm_libc_macros_sys_wait_macros",
+            "//libc:llvm_libc_types_pid_t",
+            "//libc:llvm_libc_types_struct_pollfd",
+            "//libc:pipe",
+            "//libc:poll",
+            "//libc:stderr",
+            "//libc:stdout",
+            "//libc:strsignal",
+            "//libc:waitpid",
+        ],
+        "//conditions:default": [],
+    }),
     # Force linking in this library's `main()` to surface
     # a duplicate symbol error if a test defines its own main.
     alwayslink = True,
 )
 
+libc_test_library(
+    name = "HermeticTestUtils",
+    srcs = ["HermeticTestUtils.cpp"],
+    # Only intended to be used with full-build tests.
+    target_compatible_with = select({
+        "//libc:full_build": [],
+        "//conditions:default": ["@platforms//:incompatible"],
+    }),
+    deps = [
+        "//libc:__support_common",
+        "//libc:__support_libc_errno",
+        "//libc:__support_macros_config",
+        "//libc:atexit",
+        "//libc:bcmp",
+        "//libc:bzero",
+        "//libc:hdr_errno_macros",
+        "//libc:hdr_stdint_proxy",
+        "//libc:memcmp",
+        "//libc:memcpy",
+        "//libc:memmove",
+        "//libc:memset",
+    ],
+)
+
 libc_test_library(
     name = "LibcCTest",
     srcs = ["LibcCTest.cpp"],
     hdrs = ["LibcCTest.h"],
     deps = [
         ":LibcUnitTest",
-        "//libc:public_headers_deps",
+        "//libc:llvm_libc_common_h",
     ],
 )
 
diff --git a/utils/bazel/llvm-project-overlay/libc/test/libc_test_rules.bzl b/utils/bazel/llvm-project-overlay/libc/test/libc_test_rules.bzl
index ab2f98d7c4e07d..11112590c4dae1 100644
--- a/utils/bazel/llvm-project-overlay/libc/test/libc_test_rules.bzl
+++ b/utils/bazel/llvm-project-overlay/libc/test/libc_test_rules.bzl
@@ -13,37 +13,43 @@ When performing tests we make sure to always use the internal version.
 """
 
 load("@rules_cc//cc:defs.bzl", "cc_library", "cc_test")
-load("//libc:libc_build_rules.bzl", "libc_common_copts")
+load("//libc:libc_build_rules.bzl", "libc_common_copts", "libc_common_deps")
 load("//libc:libc_configure_options.bzl", "LIBC_CONFIGURE_OPTIONS")
 
-_FULL_BUILD_COPTS = [
-    "-nostdlib++",
-    "-nostdlib",
-    "-DLIBC_FULL_BUILD",
-    "-DLIBC_COPT_USE_C_ASSERT",
-]
-
-_TEST_DEFINES = ["LIBC_TEST_SUBPROCESS_TESTS=1"]
+_TEST_DEFINES = [
+    "LIBC_TEST_SUBPROCESS_TESTS=1",
+] + select({
+    "//libc:full_build": [
+        "TARGET_SUPPORTS_CLOCK",
+    ],
+    "//conditions:default": [],
+})
 
 def libc_test(
         name,
+        srcs = [],
         copts = [],
         deps = [],
         local_defines = [],
+        linkopts = [],
         c_test = False,
         full_build = False,
+        target_compatible_with = [],
+        tags = [],
         **kwargs):
     """Add target for a libc test.
 
     Args:
       name: Test target name
+      srcs: The list of sources for this test.
       copts: The list of options to add to the C++ compilation command.
       deps: The list of libc functions and libraries to be linked in.
       local_defines: The list of target local_defines if any.
+      linkopts: Link options for the cc_test.
       c_test: Whether this test is a C unit test (uses LibcCTest).
-      full_build: Whether to compile with LIBC_FULL_BUILD and disallow
-          use of system headers. This is useful for tests that include both
-          LLVM libc headers and proxy headers to avoid conflicting definitions.
+      full_build: Whether the test should only be run in full-build mode.
+      target_compatible_with: Constraints the target is compatible with.
+      tags: Tags for the cc_test.
       **kwargs: Attributes relevant for a cc_test.
     """
     deps = deps + [
@@ -55,33 +61,68 @@ def libc_test(
         "//libc:func_free",
         "//libc:func_malloc",
         "//libc:func_realloc",
-    ]
+    ] + select({
+        "//libc:full_build": [
+            "//libc/startup/linux:crt1",
+
+            # The compiler may emit references to symbols it expects to exist,
+            # like `memset`. These dependencies hermetically provide them.
+            "//libc/test/UnitTest:HermeticTestUtils",
+            "//libc:__stack_chk_fail",  # Needed if -fstack-protector is set.
+        ],
+        "//conditions:default": [],
+    })
+
     if c_test:
         deps = deps + ["//libc/test/UnitTest:LibcCTest"]
     else:
         deps = deps + ["//libc/test/UnitTest:LibcUnitTest"]
 
-    tags = kwargs.pop("tags", [])
+    linkopts = linkopts + select({
+        "//libc:full_build": [
+            "-nolibc",
+            "-nostartfiles",
+            "-nostdlib++",
+            "-static",
+        ],
+        "//conditions:default": [],
+    })
+
     if full_build:
-        copts = copts + _FULL_BUILD_COPTS
+        target_compatible_with = target_compatible_with + select({
+            "//libc:full_build": [],
+            "//conditions:default": ["@platforms//:incompatible"],
+        })
 
         # Temporarily disable full_build tests (currently broken) to unblock CI.
+        # CI needs to be configured to separately run full-build tests
+        # and to avoid these tests in overlay mode.
         tags = tags + ["manual", "nobuildkite", "notap"]
+
     cc_test(
         name = name,
+        srcs = srcs,
         local_defines = local_defines + _TEST_DEFINES + LIBC_CONFIGURE_OPTIONS,
-        deps = deps,
+        deps = deps + libc_common_deps(),
         copts = copts + libc_common_copts(),
+        target_compatible_with = target_compatible_with,
+        linkopts = linkopts,
         linkstatic = 1,
         tags = tags,
         **kwargs
     )
 
-def libc_test_library(name, copts = [], local_defines = [], **kwargs):
+def libc_test_library(
+        name,
+        deps = [],
+        copts = [],
+        local_defines = [],
+        **kwargs):
     """Add target for library used in libc tests.
 
     Args:
       name: Library target name.
+      deps: See cc_library.deps.
       copts: See cc_library.copts.
       local_defines: See cc_library.local_defines.
       **kwargs: Other attributes relevant to cc_library (e.g. "deps").
@@ -89,6 +130,7 @@ def libc_test_library(name, copts = [], local_defines = [], **kwargs):
     cc_library(
         name = name,
         testonly = True,
+        deps = deps + libc_common_deps(),
         copts = copts + libc_common_copts(),
         local_defines = local_defines + _TEST_DEFINES + LIBC_CONFIGURE_OPTIONS,
         linkstatic = 1,



More information about the libc-commits mailing list