[libc-commits] [libc] 7410007 - [libc] Allow hermetic tests to use external startup objects (#212557)

via libc-commits libc-commits at lists.llvm.org
Thu Oct 8 02:03:33 PDT 2026


Author: Simi Pallipurath
Date: 2026-10-08T10:03:28+01:00
New Revision: 7410007b21584c391df9022981f5e724d95fe73e

URL: https://github.com/llvm/llvm-project/commit/7410007b21584c391df9022981f5e724d95fe73e
DIFF: https://github.com/llvm/llvm-project/commit/7410007b21584c391df9022981f5e724d95fe73e.diff

LOG: [libc] Allow hermetic tests to use external startup objects (#212557)

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

Added: 
    libc/test/src/__support/external_startup_link_test.cpp
    libc/test/src/__support/minimal_external_crt1_for_test.cpp

Modified: 
    libc/CMakeLists.txt
    libc/cmake/modules/LLVMLibCTestRules.cmake
    libc/test/src/__support/CMakeLists.txt

Removed: 
    


################################################################################
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..ef157c8456a3a 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()
@@ -751,20 +759,50 @@ 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)
-  if(NOT TARGET libc.startup.${LIBC_TARGET_OS}.crt1)
-    message(VERBOSE "Skipping ${fq_target_name} as it is not available on ${LIBC_TARGET_OS}.")
-    return()
-  endif()
+  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;C_TEST" # Optional arguments
-    "SUITE;CXX_STANDARD" # Single value arguments
+    "SUITE;CXX_STANDARD;STARTUP" # Single value arguments
     "SRCS;HDRS;DEPENDS;ARGS;ENV;COMPILE_OPTIONS;LINK_LIBRARIES;FLAGS;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}.")
+      endif()
+      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)
+    if(LIBC_CMAKE_VERBOSE_LOGGING)
+      message(STATUS "Skipping ${fq_target_name} as it has no startup provider.")
+    endif()
+    return()
+  endif()
+
   if(NOT HERMETIC_TEST_SUITE)
     message(FATAL_ERROR "SUITE not specified for ${fq_target_name}")
   endif()
@@ -772,14 +810,11 @@ 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_target_dep}
     # 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
@@ -818,6 +853,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.
@@ -1003,7 +1048,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}

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