[libc-commits] [libc] [Libc] Support LLVM libc minor variant overlay builds. (PR #212774)
Simi Pallipurath via libc-commits
libc-commits at lists.llvm.org
Wed Jul 29 07:22:47 PDT 2026
https://github.com/simpal01 updated https://github.com/llvm/llvm-project/pull/212774
>From d11897a98e85438ca75cdc9c794a4aae63d08d2c Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Wed, 29 Jul 2026 10:34:05 +0100
Subject: [PATCH 1/2] [Libc] Support LLVM libc minor variant overlay builds.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
This change adds support for building and testing LLVM
libc as a minor variant layer, where LLVM libc may
provide only part of libc and rely on an base libc
layer for startup objects and some symbols.
In this mode, a downstream project can use LIBC_CONFIG_PATH
to provide the selected entrypoint lists for the overlay.
LLVM libc then only builds the entrypoints relevant to that
minor variant, rather than building the full libc entrypoint
set and partitioning it later. Because the build intentionally
does not see or build the whole libc, missing symbols, startup
objects, and support entrypoints may need to be treated as
provided by the base libc layer. This is why minor-variant
builds need explicit base-layer handling instead of relying
only on LLVM libc's own entrypoint graph.
Changes include:
- Adds new CMake options:
- LLVM_LIBC_ENABLE_MINOR_VARIANT
- LLVM_LIBC_INCLUDE_STARTUP
- LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP
- LIBC_HERMETIC_TEST_LINK_LIBRARIES_DEFAULT
- LLVM_LIBC_SUBARCHIVE_NAME
- Makes startup object inclusion configurable.
- startup/ is now added only when LLVM_LIBC_FULL_BUILD,
LLVM_LIBC_INCLUDE_STARTUP, and non-SPIR-V conditions
are satisfied.
- Hermetic tests no longer always require LLVM libc’s
internal crt1; they can use external test link options
instead.
- Updates hermetic test linking.
- Extra default hermetic test libraries can be supplied through
LIBC_HERMETIC_TEST_LINK_LIBRARIES_DEFAULT.
- Missing entrypoints that are expected to come from the base
libc layer are filtered in minor-variant mode.
- Adds minor-variant compile-time behavior.
- Test builds define LIBC_ENABLE_MINOR_VARIANT when
LLVM_LIBC_ENABLE_MINOR_VARIANT is enabled.
- Makes hermetic test shim symbols weak in minor-variant mode.
- Symbols such as memcpy, memmove, memset, malloc, free, realloc,
atexit, and clock can now be overridden by stronger definitions
from the base libc layer.
- Allows overriding the main libc archive name in full-build mode
using LLVM_LIBC_SUBARCHIVE_NAME.
Overall, this enables LLVM libc to be built as a partial libc overlay/subarchive
while keeping hermetic tests usable in environments where startup code or selected
entrypoints are provided externally.
---
libc/CMakeLists.txt | 9 ++-
libc/cmake/modules/LLVMLibCTestRules.cmake | 75 +++++++++++++++++++---
libc/lib/CMakeLists.txt | 6 ++
libc/test/UnitTest/HermeticTestUtils.cpp | 35 +++++++---
libc/test/UnitTest/LibcTest.cpp | 6 ++
5 files changed, 111 insertions(+), 20 deletions(-)
diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index af820ba6ab6c0..314fb48263ee9 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -90,6 +90,8 @@ endif()
set(LIBC_LINK_OPTIONS_DEFAULT "" CACHE STRING "Arguments used when linking.")
set(LIBC_TEST_LINK_OPTIONS_DEFAULT "" CACHE STRING "Common link options for all the tests.")
+set(LIBC_HERMETIC_TEST_LINK_LIBRARIES_DEFAULT "" CACHE STRING
+ "Common link libraries for all hermetic tests.")
set(LIBC_TEST_CMD "" CACHE STRING
"The full test command in the form <command> binary=@BINARY@, if using another program to test (e.g. QEMU)")
@@ -152,6 +154,11 @@ if(LIBC_TARGET_OS_IS_GPU)
endif()
option(LLVM_LIBC_FULL_BUILD "Build and test LLVM libc as if it is the full libc" ${default_to_full_build})
+option(LLVM_LIBC_INCLUDE_STARTUP "Build LLVM libc startup objects" ON)
+option(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP
+ "Link hermetic tests against LLVM libc's own startup object"
+ ${LLVM_LIBC_INCLUDE_STARTUP})
+option(LLVM_LIBC_ENABLE_MINOR_VARIANT "Build LLVM libc as a minor variant overlay" OFF)
option(LLVM_LIBC_IMPLEMENTATION_DEFINED_TEST_BEHAVIOR "Build LLVM libc tests assuming our implementation-defined behavior" ON)
option(LLVM_LIBC_ENABLE_LINTING "Enables linting of libc source files" OFF)
option(LLVM_LIBC_ALL_HEADERS "Outputs all functions in header files, regardless of whether they are enabled on this target" OFF)
@@ -429,7 +436,7 @@ add_subdirectory(hdr)
add_subdirectory(src)
add_subdirectory(utils)
-if(LLVM_LIBC_FULL_BUILD AND NOT LIBC_TARGET_ARCHITECTURE_IS_SPIRV)
+if(LLVM_LIBC_FULL_BUILD AND LLVM_LIBC_INCLUDE_STARTUP AND NOT LIBC_TARGET_ARCHITECTURE_IS_SPIRV)
# The startup system can potentially depend on the library components so add
# it after the library implementation directories.
add_subdirectory(startup)
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index fdc7764c65177..7c34b7f7f0184 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -21,6 +21,10 @@ function(_get_common_test_compile_options output_var c_test flags)
libc_add_definition(compile_options
"LIBC_TEST_FLOAT_RANGE_COUNT=${LIBC_TEST_FLOAT_RANGE_COUNT}")
+ if(LLVM_LIBC_ENABLE_MINOR_VARIANT)
+ list(APPEND compile_options "-DLIBC_ENABLE_MINOR_VARIANT")
+ endif()
+
# EXPECT_DEATH and ASSERT_DEATH might be quite slow. LIBC_TEST_SKIP_DEATH_TESTS
# will make those tests no-op to reduce the overall test time.
if(LIBC_TEST_SKIP_DEATH_TESTS)
@@ -195,6 +199,14 @@ function(get_object_files_for_test result skipped_entrypoints_list)
if(fq_target_name STREQUAL "libc.test.include.issignaling_c_test.__unit__" OR fq_target_name STREQUAL "libc.test.include.iscanonical_c_test.__unit__")
string(REPLACE ".__internal__" "" object_file_raw ${object_file_raw})
endif()
+ # Adding libc.src.stdlib.exit normally contributes the internal entrypoint
+ # object. External baremetal startup code can reference the public C exit
+ # symbol, so use the public-packaging object for exit in this configuration.
+ if(dep STREQUAL "libc.src.stdlib.exit" AND
+ NOT LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP AND
+ LIBC_TARGET_OS_IS_BAREMETAL)
+ string(REPLACE ".__internal__" "" object_file_raw ${object_file_raw})
+ endif()
list(APPEND dep_obj ${object_file_raw})
endif()
endif()
@@ -229,6 +241,26 @@ function(get_link_options link_options)
set(${link_options} "${link_opts}" PARENT_SCOPE)
endfunction()
+function(_filter_minor_variant_base_entrypoints skipped_entrypoints_var)
+ if(NOT LLVM_LIBC_ENABLE_MINOR_VARIANT)
+ return()
+ endif()
+
+ if(DEFINED TARGET_LLVMLIBC_BASE_ENTRYPOINTS)
+ set(base_layer_entrypoints ${TARGET_LLVMLIBC_BASE_ENTRYPOINTS})
+ else()
+ set(base_layer_entrypoints ${TARGET_LLVMLIBC_ENTRYPOINTS})
+ endif()
+
+ foreach(skipped_entrypoint IN LISTS ${skipped_entrypoints_var})
+ if(skipped_entrypoint IN_LIST base_layer_entrypoints)
+ list(REMOVE_ITEM ${skipped_entrypoints_var} ${skipped_entrypoint})
+ endif()
+ endforeach()
+
+ set(${skipped_entrypoints_var} ${${skipped_entrypoints_var}} PARENT_SCOPE)
+endfunction()
+
# Rule to add a libc unittest.
# Usage
# add_libc_unittest(
@@ -569,6 +601,18 @@ function(add_integration_test test_name)
libc.src.strings.bzero
)
+ if(NOT LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP AND LIBC_TARGET_OS_IS_BAREMETAL)
+ # External baremetal startup objects still need libc's process startup and
+ # termination entrypoints, even when the test itself does not depend on them.
+ list(APPEND fq_deps_list
+ libc.src.stdlib.atexit
+ libc.src.stdlib.exit
+ )
+ if(LLVM_LIBC_INCLUDE_STARTUP)
+ list(APPEND fq_deps_list libc.startup.baremetal.init)
+ endif()
+ endif()
+
if(libc.src.compiler.__stack_chk_fail IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS)
# __stack_chk_fail should always be included if supported to allow building
# libc with the stack protector enabled.
@@ -744,10 +788,26 @@ endfunction(add_integration_test)
# LOADER_ARGS <list of special args to loaders (like the GPU loader)>
# )
function(add_libc_hermetic test_name)
- if(NOT TARGET libc.startup.${LIBC_TARGET_OS}.crt1)
- message(VERBOSE "Skipping ${fq_target_name} as it is not available on ${LIBC_TARGET_OS}.")
+ get_fq_target_name(${test_name} fq_target_name)
+ get_fq_target_name(${test_name}.libc fq_libc_target_name)
+
+ set(startup_target libc.startup.${LIBC_TARGET_OS}.crt1)
+ set(startup_deps "")
+ if(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
+ if(NOT TARGET ${startup_target})
+ message(VERBOSE "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
+ return()
+ endif()
+ list(APPEND startup_deps ${startup_target})
+ endif()
+
+ if(NOT startup_deps AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
+ message(VERBOSE
+ "Skipping ${fq_target_name} as it has no internal startup target "
+ "and no external test link options.")
return()
endif()
+
cmake_parse_arguments(
"HERMETIC_TEST"
"IS_GPU_BENCHMARK;NO_RUN_POSTBUILD" # Optional arguments
@@ -763,14 +823,9 @@ function(add_libc_hermetic test_name)
message(FATAL_ERROR "The SRCS list for add_integration_test is missing.")
endif()
- get_fq_target_name(${test_name} fq_target_name)
- get_fq_target_name(${test_name}.libc fq_libc_target_name)
-
get_fq_deps_list(fq_deps_list ${HERMETIC_TEST_DEPENDS})
list(APPEND fq_deps_list
- # Hermetic tests use the platform's startup object. So, their deps also
- # have to be collected.
- libc.startup.${LIBC_TARGET_OS}.crt1
+ ${startup_deps}
# We always add the memory functions objects. This is because the
# compiler's codegen can emit calls to the C memory functions.
libc.src.__support.StringUtil.error_to_string
@@ -811,6 +866,7 @@ function(add_libc_hermetic test_name)
# collect the object files with public names of entrypoints.
get_object_files_for_test(
link_object_files skipped_entrypoints_list ${fq_deps_list})
+ _filter_minor_variant_base_entrypoints(skipped_entrypoints_list)
if(skipped_entrypoints_list)
if(LIBC_CMAKE_VERBOSE_LOGGING)
set(msg "Skipping hermetic test ${fq_target_name} as it has missing deps: "
@@ -910,10 +966,11 @@ function(add_libc_hermetic test_name)
target_link_libraries(
${fq_build_target_name}
PRIVATE
- libc.startup.${LIBC_TARGET_OS}.crt1
+ ${startup_deps}
${link_libraries}
LibcHermeticTestSupport.hermetic
${fq_target_name}.__libc__
+ ${LIBC_HERMETIC_TEST_LINK_LIBRARIES_DEFAULT}
${compiler_runtime}
)
add_dependencies(${fq_build_target_name}
diff --git a/libc/lib/CMakeLists.txt b/libc/lib/CMakeLists.txt
index 8ef0329471521..7063c413f481c 100644
--- a/libc/lib/CMakeLists.txt
+++ b/libc/lib/CMakeLists.txt
@@ -1,11 +1,17 @@
set(libc_archive_targets "")
set(libc_archive_names "")
set(libc_archive_entrypoint_lists "")
+set(LLVM_LIBC_SUBARCHIVE_NAME "" CACHE STRING
+ "Optional archive name to use for the libc archive output.")
if(LLVM_LIBC_FULL_BUILD)
list(APPEND libc_archive_names c m mvec)
list(APPEND libc_archive_targets libc libm libmvec)
list(APPEND libc_archive_entrypoint_lists
TARGET_LIBC_ENTRYPOINTS TARGET_LIBM_ENTRYPOINTS TARGET_LIBMVEC_ENTRYPOINTS)
+ if(LLVM_LIBC_SUBARCHIVE_NAME)
+ list(REMOVE_AT libc_archive_names 0)
+ list(INSERT libc_archive_names 0 "${LLVM_LIBC_SUBARCHIVE_NAME}")
+ endif()
else()
list(APPEND libc_archive_names llvmlibc)
list(APPEND libc_archive_targets libc)
diff --git a/libc/test/UnitTest/HermeticTestUtils.cpp b/libc/test/UnitTest/HermeticTestUtils.cpp
index 5ce661b2202c0..7480ab7d66ec1 100644
--- a/libc/test/UnitTest/HermeticTestUtils.cpp
+++ b/libc/test/UnitTest/HermeticTestUtils.cpp
@@ -53,32 +53,47 @@ static uint8_t *ptr = memory;
extern "C" {
+#ifdef LIBC_ENABLE_MINOR_VARIANT
+#define LIBC_HERMETIC_TEST_SHIM [[gnu::weak]]
+#else
+#define LIBC_HERMETIC_TEST_SHIM
+#endif
+
// Hermetic tests rely on the following memory functions. This is because the
// compiler code generation can emit calls to them. We want to map the external
// entrypoint to the internal implementation of the function used for testing.
// This is done manually as not all targets support aliases.
-int bcmp(const void *lhs, const void *rhs, size_t count) {
+LIBC_HERMETIC_TEST_SHIM int bcmp(const void *lhs, const void *rhs,
+ size_t count) {
return LIBC_NAMESPACE::bcmp(lhs, rhs, count);
}
-void bzero(void *ptr, size_t count) { LIBC_NAMESPACE::bzero(ptr, count); }
-int memcmp(const void *lhs, const void *rhs, size_t count) {
+LIBC_HERMETIC_TEST_SHIM void bzero(void *ptr, size_t count) {
+ LIBC_NAMESPACE::bzero(ptr, count);
+}
+LIBC_HERMETIC_TEST_SHIM int memcmp(const void *lhs, const void *rhs,
+ size_t count) {
return LIBC_NAMESPACE::memcmp(lhs, rhs, count);
}
-void *memcpy(void *__restrict dst, const void *__restrict src, size_t count) {
+LIBC_HERMETIC_TEST_SHIM void *memcpy(void *__restrict dst,
+ const void *__restrict src,
+ size_t count) {
return LIBC_NAMESPACE::memcpy(dst, src, count);
}
-void *memmove(void *dst, const void *src, size_t count) {
+LIBC_HERMETIC_TEST_SHIM void *memmove(void *dst, const void *src,
+ size_t count) {
return LIBC_NAMESPACE::memmove(dst, src, count);
}
-void *memset(void *ptr, int value, size_t count) {
+LIBC_HERMETIC_TEST_SHIM void *memset(void *ptr, int value, size_t count) {
return LIBC_NAMESPACE::memset(ptr, value, count);
}
// This is needed if the test was compiled with '-fno-use-cxa-atexit'.
-int atexit(void (*func)(void)) { return LIBC_NAMESPACE::atexit(func); }
+LIBC_HERMETIC_TEST_SHIM int atexit(void (*func)(void)) {
+ return LIBC_NAMESPACE::atexit(func);
+}
-void *malloc(size_t s) {
+LIBC_HERMETIC_TEST_SHIM void *malloc(size_t s) {
// Keep the bump pointer aligned on an eight byte boundary.
s = ((s + ALIGNMENT - 1) / ALIGNMENT) * ALIGNMENT;
void *mem = ptr;
@@ -86,9 +101,9 @@ void *malloc(size_t s) {
return static_cast<uint64_t>(ptr - memory) >= MEMORY_SIZE ? nullptr : mem;
}
-void free(void *) {}
+LIBC_HERMETIC_TEST_SHIM void free(void *) {}
-void *realloc(void *mem, size_t s) {
+LIBC_HERMETIC_TEST_SHIM void *realloc(void *mem, size_t s) {
if (mem == nullptr)
return malloc(s);
uint8_t *newmem = reinterpret_cast<uint8_t *>(malloc(s));
diff --git a/libc/test/UnitTest/LibcTest.cpp b/libc/test/UnitTest/LibcTest.cpp
index b03ceb6c7c77d..338e1ac95fc51 100644
--- a/libc/test/UnitTest/LibcTest.cpp
+++ b/libc/test/UnitTest/LibcTest.cpp
@@ -24,7 +24,13 @@
#include <time.h>
#include "src/time/clock.h"
+#ifdef LIBC_ENABLE_MINOR_VARIANT
+extern "C" [[gnu::weak]] clock_t clock() noexcept {
+ return LIBC_NAMESPACE::clock();
+}
+#else
extern "C" clock_t clock() noexcept { return LIBC_NAMESPACE::clock(); }
+#endif
#define LIBC_TEST_USE_CLOCK
#endif
>From 3476374b04c00cf87a9fed02f509e23544a8ec42 Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Wed, 29 Jul 2026 14:57:02 +0100
Subject: [PATCH 2/2] fixup! [Libc] Support LLVM libc minor variant overlay
builds.
---
libc/CMakeLists.txt | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index 314fb48263ee9..a6ea81bbdfb28 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -157,7 +157,7 @@ option(LLVM_LIBC_FULL_BUILD "Build and test LLVM libc as if it is the full libc"
option(LLVM_LIBC_INCLUDE_STARTUP "Build LLVM libc startup objects" ON)
option(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP
"Link hermetic tests against LLVM libc's own startup object"
- ${LLVM_LIBC_INCLUDE_STARTUP})
+ ON)
option(LLVM_LIBC_ENABLE_MINOR_VARIANT "Build LLVM libc as a minor variant overlay" OFF)
option(LLVM_LIBC_IMPLEMENTATION_DEFINED_TEST_BEHAVIOR "Build LLVM libc tests assuming our implementation-defined behavior" ON)
option(LLVM_LIBC_ENABLE_LINTING "Enables linting of libc source files" OFF)
More information about the libc-commits
mailing list