[libc-commits] [libc] [libc] Allow hermetic tests to use external startup objects (PR #212557)
Simi Pallipurath via libc-commits
libc-commits at lists.llvm.org
Wed Oct 7 04:25:42 PDT 2026
https://github.com/simpal01 updated https://github.com/llvm/llvm-project/pull/212557
>From 4f0d4b315be9ff40bec7510522c870544fdc7fd9 Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Tue, 28 Jul 2026 17:33:03 +0100
Subject: [PATCH 1/3] [libc] Allow hermetic tests to use external startup
objects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Adds LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP to
control whether libc hermetic tests link against LLVM libc’s
own crt1.o.
When the option is OFF, hermetic tests no longer require
or link libc.startup.${LIBC_TARGET_OS}.crt1. This allows
baremetal configurations to provide startup externally through
LIBC_TEST_LINK_OPTIONS_DEFAULT while still running hermetic tests.
For baremetal external-startup mode, the patch also adds the
required process startup/termination dependencies and ensures
exit uses the public packaging object, since external startup
code may reference the public C exit symbol.
Tested on Arm Toolchain for Embedded: https://github.com/arm/arm-toolchain
Assisted-by: Codex
---
libc/CMakeLists.txt | 3 ++
libc/cmake/modules/LLVMLibCTestRules.cmake | 52 +++++++++++++++++++---
2 files changed, 50 insertions(+), 5 deletions(-)
diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index f44a84d5ae48a..86ddcd2df264f 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -262,6 +262,9 @@ if(LIBC_ENABLE_COVERAGE)
CACHE INTERNAL "Clang profile runtime")
endif()
endif()
+option(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP
+ "Link hermetic tests against LLVM libc's own startup object"
+ ON)
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)
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index 917e0ce2a9b05..bcbd4ba5d067d 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -210,6 +210,14 @@ function(get_object_files_for_test result skipped_entrypoints_list)
if(fq_target_name STREQUAL "libc.test.include.issignaling_c_test" OR fq_target_name STREQUAL "libc.test.include.iscanonical_c_test")
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()
@@ -753,10 +761,24 @@ endfunction()
# 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 startup provider.")
return()
endif()
+
cmake_parse_arguments(
"HERMETIC_TEST"
"IS_GPU_BENCHMARK;NO_RUN_POSTBUILD;C_TEST" # Optional arguments
@@ -772,9 +794,6 @@ 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
@@ -804,6 +823,19 @@ function(add_libc_hermetic test_name)
if(LIBC_TEST_SUBPROCESS_TESTS AND NOT HERMETIC_TEST_C_TEST)
list(APPEND fq_deps_list
libc.test.UnitTest.LibcDeathTestExecutors
+=======
+ ${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
+ libc.src.string.memcmp
+ libc.src.string.memcpy
+ libc.src.string.memmove
+ libc.src.string.memset
+ libc.src.strings.bcmp
+ libc.src.strings.bzero
+ # Syscalls used by death tests.
+>>>>>>> 968c82934d28 ([libc] Allow hermetic tests to use external startup objects)
libc.src.poll.poll
libc.src.signal.kill
libc.src.stdio.fflush
@@ -818,6 +850,16 @@ function(add_libc_hermetic test_name)
)
endif()
+ 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
+ libc.startup.baremetal.init
+ )
+ 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.
>From 4c977b16b58035bbf0cc7aba106af43cdc55bd8d Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Tue, 22 Sep 2026 22:18:57 +0100
Subject: [PATCH 2/3] fixup! [libc] Allow hermetic tests to use external
startup objects
---
libc/cmake/modules/LLVMLibCTestRules.cmake | 31 ++++++++--------------
1 file changed, 11 insertions(+), 20 deletions(-)
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index bcbd4ba5d067d..e7cbdef2358d8 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -765,17 +765,21 @@ function(add_libc_hermetic test_name)
get_fq_target_name(${test_name}.libc fq_libc_target_name)
set(startup_target libc.startup.${LIBC_TARGET_OS}.crt1)
- set(startup_deps "")
+ set(startup_target_dep "")
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}.")
+ if(LIBC_CMAKE_VERBOSE_LOGGING)
+ message(STATUS "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
+ endif()
return()
endif()
- list(APPEND startup_deps ${startup_target})
+ set(startup_target_dep ${startup_target})
endif()
- if(NOT startup_deps AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
- message(VERBOSE "Skipping ${fq_target_name} as it has no startup provider.")
+ if(NOT startup_target_dep AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
+ if(LIBC_CMAKE_VERBOSE_LOGGING)
+ message(STATUS "Skipping ${fq_target_name} as it has no startup provider.")
+ endif()
return()
endif()
@@ -796,9 +800,9 @@ function(add_libc_hermetic test_name)
get_fq_deps_list(fq_deps_list ${HERMETIC_TEST_DEPENDS})
list(APPEND fq_deps_list
+ ${startup_target_dep}
# Hermetic tests use the platform's startup object. So, their deps also
# have to be collected.
- libc.startup.${LIBC_TARGET_OS}.crt1
# 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
@@ -823,19 +827,6 @@ function(add_libc_hermetic test_name)
if(LIBC_TEST_SUBPROCESS_TESTS AND NOT HERMETIC_TEST_C_TEST)
list(APPEND fq_deps_list
libc.test.UnitTest.LibcDeathTestExecutors
-=======
- ${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
- libc.src.string.memcmp
- libc.src.string.memcpy
- libc.src.string.memmove
- libc.src.string.memset
- libc.src.strings.bcmp
- libc.src.strings.bzero
- # Syscalls used by death tests.
->>>>>>> 968c82934d28 ([libc] Allow hermetic tests to use external startup objects)
libc.src.poll.poll
libc.src.signal.kill
libc.src.stdio.fflush
@@ -1045,7 +1036,7 @@ function(add_libc_hermetic test_name)
target_link_libraries(
${fq_build_target_name}
PRIVATE
- libc.startup.${LIBC_TARGET_OS}.crt1
+ ${startup_target_dep}
${HERMETIC_TEST_LINK_LIBRARIES}
${fq_target_name}.__libc__
${coverage_link_libs}
>From 843ae8fba74041d9e66946078f3b4af49641bad8 Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Tue, 6 Oct 2026 14:51:02 +0100
Subject: [PATCH 3/3] fixup! [libc] Allow hermetic tests to use external
startup objects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Add STARTUP support to add_libc_hermetic.
- Allow external startup objects when internal startup is disabled.
- Validate conflicting or missing startup targets.
- Add a bare-metal link test using a minimal external crt1.
- Verify LLVM libc’s internal crt1 is not linked accidentally
by adding a new test that produces a duplicate symbol error if
LLVM libc's internal crt1 is also linked with the external startup
object.
---
libc/cmake/modules/LLVMLibCTestRules.cmake | 28 +++++++++++++------
libc/test/src/__support/CMakeLists.txt | 20 +++++++++++++
.../__support/external_startup_link_test.cpp | 8 ++++++
.../minimal_external_crt1_for_test.cpp | 7 +++++
4 files changed, 55 insertions(+), 8 deletions(-)
create mode 100644 libc/test/src/__support/external_startup_link_test.cpp
create mode 100644 libc/test/src/__support/minimal_external_crt1_for_test.cpp
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index e7cbdef2358d8..1911a03777a5f 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -759,14 +759,28 @@ endfunction()
# COMPILE_OPTIONS <list of special compile options for the test>
# LINK_LIBRARIES <list of linking libraries for this target>
# LOADER_ARGS <list of special args to loaders (like the GPU loader)>
+# STARTUP <startup object target used when internal startup is disabled>
# )
function(add_libc_hermetic test_name)
get_fq_target_name(${test_name} fq_target_name)
get_fq_target_name(${test_name}.libc fq_libc_target_name)
+ cmake_parse_arguments(
+ "HERMETIC_TEST"
+ "IS_GPU_BENCHMARK;NO_RUN_POSTBUILD" # Optional arguments
+ "SUITE;CXX_STANDARD;STARTUP" # Single value arguments
+ "SRCS;HDRS;DEPENDS;ARGS;ENV;COMPILE_OPTIONS;LINK_LIBRARIES;LOADER_ARGS" # Multi-value arguments
+ ${ARGN}
+ )
+
set(startup_target libc.startup.${LIBC_TARGET_OS}.crt1)
set(startup_target_dep "")
if(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
+ if(HERMETIC_TEST_STARTUP)
+ message(FATAL_ERROR
+ "Hermetic test ${fq_target_name} provides an external STARTUP target "
+ "while internal startup is enabled.")
+ endif()
if(NOT TARGET ${startup_target})
if(LIBC_CMAKE_VERBOSE_LOGGING)
message(STATUS "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
@@ -774,6 +788,12 @@ function(add_libc_hermetic test_name)
return()
endif()
set(startup_target_dep ${startup_target})
+ elseif(HERMETIC_TEST_STARTUP)
+ get_fq_dep_name(startup_target_dep ${HERMETIC_TEST_STARTUP})
+ if(NOT TARGET ${startup_target_dep})
+ message(FATAL_ERROR
+ "Hermetic test ${fq_target_name} startup target ${startup_target_dep} does not exist.")
+ endif()
endif()
if(NOT startup_target_dep AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
@@ -783,14 +803,6 @@ function(add_libc_hermetic test_name)
return()
endif()
- cmake_parse_arguments(
- "HERMETIC_TEST"
- "IS_GPU_BENCHMARK;NO_RUN_POSTBUILD;C_TEST" # Optional arguments
- "SUITE;CXX_STANDARD" # Single value arguments
- "SRCS;HDRS;DEPENDS;ARGS;ENV;COMPILE_OPTIONS;LINK_LIBRARIES;FLAGS;LOADER_ARGS" # Multi-value arguments
- ${ARGN}
- )
-
if(NOT HERMETIC_TEST_SUITE)
message(FATAL_ERROR "SUITE not specified for ${fq_target_name}")
endif()
diff --git a/libc/test/src/__support/CMakeLists.txt b/libc/test/src/__support/CMakeLists.txt
index 2c92302d04b2c..c71385d98898b 100644
--- a/libc/test/src/__support/CMakeLists.txt
+++ b/libc/test/src/__support/CMakeLists.txt
@@ -300,6 +300,26 @@ add_libc_test(
libc.src.__support.memory_size
)
+if(LIBC_TARGET_OS_IS_BAREMETAL AND
+ NOT LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
+ add_object_library(
+ minimal_external_crt1_for_test
+ SRCS
+ minimal_external_crt1_for_test.cpp
+ )
+
+ add_libc_hermetic(
+ external_startup_link_test
+ NO_RUN_POSTBUILD
+ STARTUP
+ .minimal_external_crt1_for_test
+ SUITE
+ libc-support-tests
+ SRCS
+ external_startup_link_test.cpp
+ )
+endif()
+
add_libc_test(
str_to_float_comparison_test
NO_RUN_POSTBUILD
diff --git a/libc/test/src/__support/external_startup_link_test.cpp b/libc/test/src/__support/external_startup_link_test.cpp
new file mode 100644
index 0000000000000..749e34c3f7792
--- /dev/null
+++ b/libc/test/src/__support/external_startup_link_test.cpp
@@ -0,0 +1,8 @@
+// This file intentionally contains no test cases. CMake's add_executable
+// requires a source file, but successfully linking the executable is the test.
+// When LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP is disabled, a hermetic
+// test must link the minimal external startup object supplied by this test and
+// must not link LLVM libc's internal crt1. Both startup objects define _start,
+// so accidentally linking the internal crt1 as well causes a duplicate-symbol
+// error. A successful link therefore verifies that disabling internal startup
+// selects only the provided minimal_external_crt1_for_test.cpp object.
diff --git a/libc/test/src/__support/minimal_external_crt1_for_test.cpp b/libc/test/src/__support/minimal_external_crt1_for_test.cpp
new file mode 100644
index 0000000000000..9bf1852fb6349
--- /dev/null
+++ b/libc/test/src/__support/minimal_external_crt1_for_test.cpp
@@ -0,0 +1,7 @@
+// A minimal external crt1 object used only by link tests.
+// Executables linked with this object must not be run.
+
+extern "C" [[noreturn]] void _start() {
+ for (;;) {
+ }
+}
More information about the libc-commits
mailing list