[Lldb-commits] [lldb] [llvm] [lldb] Build liblldb exports from script-interpreter imports (PR #201392)

Jonas Devlieghere via lldb-commits lldb-commits at lists.llvm.org
Thu Jun 4 09:28:53 PDT 2026


https://github.com/JDevlieghere updated https://github.com/llvm/llvm-project/pull/201392

>From 21b7f4eac5a7e61b851bfc8aa9d0fc8bcc6a78dd Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Wed, 3 Jun 2026 09:03:48 -0700
Subject: [PATCH 1/2] [lldb] Build liblldb exports from script-interpreter
 imports

With LLDB_ENABLE_DYNAMIC_SCRIPTINTERPRETERS, the script interpreter
plugins are built as standalone shared libraries that resolve their
lldb_private/llvm references through liblldb's re-exports. liblldb
was falling back to liblldb-private.exports, which whitelists the
lldb_private/llvm namespaces wholesale.

Generate the export list per build instead. For each plugin, run
llvm-nm on its compiled objects, keep undefined references in
namespaces liblldb owns, subtract the plugin's own definitions, and
concatenate the per-plugin lists with liblldb.exports.

The plugin's SHARED target LINK_LIBS liblldb, which makes the
plugin's compile order depend on liblldb. Wiring the exports file
in via add_llvm_symbol_exports' built-in target dependency would
form a cycle through that order-only edge. Add NO_TARGET_DEPENDENCY
to add_llvm_symbol_exports so we can apply the file via LINK_DEPENDS
alone; that still triggers a relink when the symbol set changes,
without participating in compile-order tracking.

Windows is unchanged: msvc_extract_private_symbols.py was already
doing per-symbol extraction at namespace granularity.

Assisted-by: Claude
---
 lldb/cmake/modules/AddLLDB.cmake              |  61 +++++++++++
 lldb/cmake/modules/LLDBConcatFiles.cmake      |  18 ++++
 ...ract-dynamic-script-interpreter-exports.py | 100 ++++++++++++++++++
 lldb/source/API/CMakeLists.txt                |  27 ++++-
 .../ScriptInterpreter/Lua/CMakeLists.txt      |   5 +
 .../ScriptInterpreter/Python/CMakeLists.txt   |   9 +-
 llvm/cmake/modules/AddLLVM.cmake              |  11 ++
 7 files changed, 228 insertions(+), 3 deletions(-)
 create mode 100644 lldb/cmake/modules/LLDBConcatFiles.cmake
 create mode 100644 lldb/scripts/extract-dynamic-script-interpreter-exports.py

diff --git a/lldb/cmake/modules/AddLLDB.cmake b/lldb/cmake/modules/AddLLDB.cmake
index e317cd6baa02d..394dcb1ab6cf0 100644
--- a/lldb/cmake/modules/AddLLDB.cmake
+++ b/lldb/cmake/modules/AddLLDB.cmake
@@ -60,6 +60,67 @@ function(_lldb_propagate_links_liblldb name libs)
   set_target_properties(${name} PROPERTIES LLDB_LINKS_LIBLLDB ${_links_liblldb})
 endfunction()
 
+# Records a target's undefined LLDB symbols for re-export by liblldb.
+# The output path is appended to the LLDB_DYNAMIC_SCRIPTINTERPRETER_SYMBOL_FILES
+# global property.
+function(lldb_record_dynamic_script_interpreter_exports target)
+  set(symbol_file
+    "${CMAKE_CURRENT_BINARY_DIR}/${target}.undefined.symbols")
+
+  # llvm-nm is registered as a CMake target after the LLDB subdirectory is
+  # processed, so a forward target reference is required. LLVM_NM, when set,
+  # points to a prebuilt host tool for cross-compiled builds.
+  if(LLVM_NM)
+    set(nm_exe "${LLVM_NM}")
+    set(nm_dep)
+  else()
+    set(nm_exe "$<TARGET_FILE:llvm-nm>")
+    set(nm_dep llvm-nm)
+  endif()
+
+  set(extra_args)
+  if(APPLE)
+    list(APPEND extra_args --mach-o)
+  endif()
+
+  add_custom_command(
+    OUTPUT ${symbol_file}
+    COMMAND "${Python3_EXECUTABLE}"
+      ${LLDB_SOURCE_DIR}/scripts/extract-dynamic-script-interpreter-exports.py
+      --nm ${nm_exe}
+      ${extra_args}
+      -o ${symbol_file}
+      $<TARGET_OBJECTS:${target}>
+    DEPENDS
+      ${LLDB_SOURCE_DIR}/scripts/extract-dynamic-script-interpreter-exports.py
+      $<TARGET_OBJECTS:${target}>
+      ${nm_dep}
+    COMMAND_EXPAND_LISTS
+    VERBATIM
+    COMMENT "Extracting LLDB symbols required by ${target}"
+  )
+
+  add_custom_target(${target}-exports DEPENDS ${symbol_file})
+  set_target_properties(${target}-exports PROPERTIES FOLDER "LLDB/API")
+
+  set_property(GLOBAL APPEND PROPERTY
+    LLDB_DYNAMIC_SCRIPTINTERPRETER_SYMBOL_FILES ${symbol_file})
+endfunction()
+
+# Applies an exports file to a target's link without inserting the
+# order-only edge that add_llvm_symbol_exports normally adds. That edge
+# propagates through CMake's compile-order graph and forms a cycle when
+# the exports list is itself derived from a target that links the same
+# library. LINK_DEPENDS still triggers a relink when the file's content
+# changes (Ninja and Makefile generators only).
+function(lldb_apply_exports_file target export_file)
+  add_llvm_symbol_exports(${target} ${export_file}
+    NO_TARGET_DEPENDENCY
+    OUTPUT_FILE_VAR native_export_file)
+  set_property(TARGET ${target} APPEND PROPERTY LINK_DEPENDS
+    ${native_export_file})
+endfunction()
+
 # Configure-time scan: every #include "lldb/<Module>/..." in <SOURCES> and in
 # PUBLIC_HEADER_DIR (recursive) must reference OWN_MODULE or a name in
 # ALLOWED_MODULES, otherwise FATAL_ERROR. Catches accidental cross-module
diff --git a/lldb/cmake/modules/LLDBConcatFiles.cmake b/lldb/cmake/modules/LLDBConcatFiles.cmake
new file mode 100644
index 0000000000000..ac832806f8dad
--- /dev/null
+++ b/lldb/cmake/modules/LLDBConcatFiles.cmake
@@ -0,0 +1,18 @@
+# Concatenate the input files (positional args 2..N) into the output
+# file (positional arg 1).
+#
+# Usage: cmake -P LLDBConcatFiles.cmake <output> <input1> [<input2> ...]
+
+if(CMAKE_ARGC LESS 5)
+  message(FATAL_ERROR
+    "LLDBConcatFiles.cmake requires <output> and at least one <input>.")
+endif()
+
+set(_out "${CMAKE_ARGV3}")
+file(WRITE "${_out}" "")
+
+math(EXPR _last "${CMAKE_ARGC} - 1")
+foreach(_i RANGE 4 ${_last})
+  file(READ "${CMAKE_ARGV${_i}}" _data)
+  file(APPEND "${_out}" "${_data}")
+endforeach()
diff --git a/lldb/scripts/extract-dynamic-script-interpreter-exports.py b/lldb/scripts/extract-dynamic-script-interpreter-exports.py
new file mode 100644
index 0000000000000..5e0d36e8169ed
--- /dev/null
+++ b/lldb/scripts/extract-dynamic-script-interpreter-exports.py
@@ -0,0 +1,100 @@
+"""Emit the LLDB-side imports of a script interpreter plugin's objects.
+
+The result is fed into liblldb's exports file so the dynamic loader can
+satisfy the plugin's references at runtime without growing liblldb's
+re-export surface beyond what the plugin actually consumes.
+"""
+
+import argparse
+import subprocess
+import sys
+
+
+# Mangled-name prefixes liblldb owns. Symbols outside these namespaces
+# are resolved through other libraries on the loader path.
+EXPORT_PREFIXES = (
+    "_ZN12lldb_private",
+    "_ZNK12lldb_private",
+    "_ZTVN12lldb_private",
+    "_ZTSN12lldb_private",
+    "_ZTIN12lldb_private",
+    "_ZN4lldb",
+    "_ZNK4lldb",
+    "_ZTVN4lldb",
+    "_ZTSN4lldb",
+    "_ZTIN4lldb",
+    "_ZN4llvm",
+    "_ZNK4llvm",
+    "_ZTVN4llvm",
+    "_ZTSN4llvm",
+    "_ZTIN4llvm",
+)
+
+
+def normalize(sym, mach_o):
+    # Mach-O reports an extra leading underscore that the .exports file
+    # format does not carry.
+    if mach_o and sym.startswith("_"):
+        return sym[1:]
+    return sym
+
+
+def collect(nm, obj, mach_o):
+    """Return (undefined, defined) symbol-name sets for `obj`.
+
+    Both halves are needed so cross-object references that the plugin's
+    own objects satisfy can be subtracted before emitting.
+    """
+    undefined = set()
+    defined = set()
+
+    out = subprocess.check_output(
+        [nm, "--undefined-only", "--just-symbol-name", obj], text=True
+    )
+    for line in out.splitlines():
+        sym = line.strip()
+        if sym:
+            undefined.add(normalize(sym, mach_o))
+
+    out = subprocess.check_output(
+        [nm, "--defined-only", "--extern-only", "--just-symbol-name", obj],
+        text=True,
+    )
+    for line in out.splitlines():
+        sym = line.strip()
+        if sym:
+            defined.add(normalize(sym, mach_o))
+
+    return undefined, defined
+
+
+def main():
+    parser = argparse.ArgumentParser(description=__doc__)
+    parser.add_argument("--nm", required=True, help="Path to llvm-nm.")
+    parser.add_argument("-o", "--output", required=True, help="Output exports file.")
+    parser.add_argument(
+        "--mach-o",
+        action="store_true",
+        help="Strip the leading underscore Mach-O prepends " "to every symbol.",
+    )
+    parser.add_argument(
+        "objects", nargs="+", help="Object files (or archives) to scan."
+    )
+    args = parser.parse_args()
+
+    undefined = set()
+    defined = set()
+    for obj in args.objects:
+        u, d = collect(args.nm, obj, args.mach_o)
+        undefined.update(u)
+        defined.update(d)
+
+    syms = {s for s in (undefined - defined) if s.startswith(EXPORT_PREFIXES)}
+
+    with open(args.output, "w", newline="\n") as f:
+        for s in sorted(syms):
+            f.write(s + "\n")
+
+
+if __name__ == "__main__":
+    sys.exit(main())
diff --git a/lldb/source/API/CMakeLists.txt b/lldb/source/API/CMakeLists.txt
index 311c739b550f8..65eebfd5f847c 100644
--- a/lldb/source/API/CMakeLists.txt
+++ b/lldb/source/API/CMakeLists.txt
@@ -159,7 +159,32 @@ set_target_properties(liblldb
 
 target_compile_definitions(liblldb PRIVATE LLDB_IN_LIBLLDB)
 if (NOT CMAKE_SYSTEM_NAME MATCHES "Windows")
-  if (NOT LLDB_EXPORT_ALL_SYMBOLS)
+  if (LLDB_ENABLE_DYNAMIC_SCRIPTINTERPRETERS AND NOT LLDB_EXPORT_ALL_SYMBOLS_EXPORTS_FILE)
+    # Each script interpreter plugin contributes a list of its undefined
+    # LLDB symbols. Concatenating those with liblldb.exports yields the
+    # smallest export set that satisfies dynamically loaded plugins.
+    get_property(plugin_symbol_files GLOBAL
+      PROPERTY LLDB_DYNAMIC_SCRIPTINTERPRETER_SYMBOL_FILES)
+    set(generated_exports_file
+      ${CMAKE_CURRENT_BINARY_DIR}/liblldb-script-interpreter.exports)
+    add_custom_command(
+      OUTPUT ${generated_exports_file}
+      COMMAND ${CMAKE_COMMAND} -P
+        ${LLDB_SOURCE_DIR}/cmake/modules/LLDBConcatFiles.cmake
+        ${generated_exports_file}
+        ${CMAKE_CURRENT_SOURCE_DIR}/liblldb.exports
+        ${plugin_symbol_files}
+      DEPENDS
+        ${LLDB_SOURCE_DIR}/cmake/modules/LLDBConcatFiles.cmake
+        ${CMAKE_CURRENT_SOURCE_DIR}/liblldb.exports
+        ${plugin_symbol_files}
+      VERBATIM
+      COMMENT "Merging script interpreter export lists for liblldb"
+    )
+    message(STATUS "Symbols (liblldb): exporting the lldb namespace plus "
+                   "script interpreter dependencies")
+    lldb_apply_exports_file(liblldb ${generated_exports_file})
+  elseif (NOT LLDB_EXPORT_ALL_SYMBOLS)
     # If we're not exporting all symbols, we'll want to explicitly set
     # the exported symbols here.  This prevents 'log enable --stack ...'
     # from working on some systems but limits the liblldb size.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Lua/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Lua/CMakeLists.txt
index 7d553bfc9d87a..03a7670de0040 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Lua/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Lua/CMakeLists.txt
@@ -14,6 +14,11 @@ if (LLDB_ENABLE_DYNAMIC_SCRIPTINTERPRETERS)
   )
   add_lua_wrapper(lldbPluginScriptInterpreterLua)
 
+  if (NOT CMAKE_SYSTEM_NAME MATCHES "Windows")
+    lldb_record_dynamic_script_interpreter_exports(
+      lldbPluginScriptInterpreterLua)
+  endif()
+
   # Static variant linked directly by unit tests. Separate compilation is
   # required so llvm::Error RTTI (ErrorInfoBase::ID) has a single address
   # shared between the test binary and the plugin code.
diff --git a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
index 03ef6b10b06c4..a5c2363bd59d3 100644
--- a/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
+++ b/lldb/source/Plugins/ScriptInterpreter/Python/CMakeLists.txt
@@ -69,6 +69,11 @@ if (LLDB_ENABLE_DYNAMIC_SCRIPTINTERPRETERS)
       ${PYTHON_SABI_LIBRARY_DIRS})
   endif()
 
+  if (NOT CMAKE_SYSTEM_NAME MATCHES "Windows")
+    lldb_record_dynamic_script_interpreter_exports(
+      lldbPluginScriptInterpreterPython)
+  endif()
+
   # Static variant linked directly by unit tests. Separate compilation is
   # required so llvm::Error RTTI (ErrorInfoBase::ID) has a single address
   # shared between the test binary and the plugin code.
@@ -89,8 +94,8 @@ if (LLDB_ENABLE_DYNAMIC_SCRIPTINTERPRETERS)
   )
   target_include_directories(lldbStaticScriptInterpreterPython
     PUBLIC ${Python3_INCLUDE_DIRS})
-
-  target_link_directories(lldbStaticScriptInterpreterPython PUBLIC ${PYTHON_SABI_LIBRARY_DIRS})
+  target_link_directories(lldbStaticScriptInterpreterPython
+    PUBLIC ${PYTHON_SABI_LIBRARY_DIRS})
 else()
   add_lldb_library(lldbPluginScriptInterpreterPython PLUGIN
     ${python_plugin_sources}
diff --git a/llvm/cmake/modules/AddLLVM.cmake b/llvm/cmake/modules/AddLLVM.cmake
index f4a73d1fc8943..a267166eb6c2d 100644
--- a/llvm/cmake/modules/AddLLVM.cmake
+++ b/llvm/cmake/modules/AddLLVM.cmake
@@ -147,6 +147,8 @@ function(llvm_update_pch name)
 endfunction()
 
 function(add_llvm_symbol_exports target_name export_file)
+  cmake_parse_arguments(ARG "NO_TARGET_DEPENDENCY" "OUTPUT_FILE_VAR" "" ${ARGN})
+
   if("${CMAKE_SYSTEM_NAME}" MATCHES "Darwin")
     set(native_export_file "${target_name}.exports")
     add_custom_command(OUTPUT ${native_export_file}
@@ -239,6 +241,15 @@ function(add_llvm_symbol_exports target_name export_file)
   set_property(DIRECTORY APPEND
     PROPERTY ADDITIONAL_MAKE_CLEAN_FILES ${native_export_file})
 
+  if(ARG_OUTPUT_FILE_VAR)
+    set(${ARG_OUTPUT_FILE_VAR}
+      "${CMAKE_CURRENT_BINARY_DIR}/${native_export_file}" PARENT_SCOPE)
+  endif()
+
+  if(ARG_NO_TARGET_DEPENDENCY)
+    return()
+  endif()
+
   add_dependencies(${target_name} ${target_name}_exports)
 
   # Add dependency to *_exports later -- CMake issue 14747

>From 8b561536bc77ee21d5d4d306594c52c4dc687d19 Mon Sep 17 00:00:00 2001
From: Jonas Devlieghere <jonas at devlieghere.com>
Date: Thu, 4 Jun 2026 09:28:37 -0700
Subject: [PATCH 2/2] Address Alex' feedback

---
 lldb/scripts/extract-dynamic-script-interpreter-exports.py | 2 +-
 lldb/source/API/CMakeLists.txt                             | 2 ++
 2 files changed, 3 insertions(+), 1 deletion(-)

diff --git a/lldb/scripts/extract-dynamic-script-interpreter-exports.py b/lldb/scripts/extract-dynamic-script-interpreter-exports.py
index 5e0d36e8169ed..b3da994b2bef1 100644
--- a/lldb/scripts/extract-dynamic-script-interpreter-exports.py
+++ b/lldb/scripts/extract-dynamic-script-interpreter-exports.py
@@ -75,7 +75,7 @@ def main():
     parser.add_argument(
         "--mach-o",
         action="store_true",
-        help="Strip the leading underscore Mach-O prepends " "to every symbol.",
+        help="Strip the leading underscore Mach-O prepends to every symbol.",
     )
     parser.add_argument(
         "objects", nargs="+", help="Object files (or archives) to scan."
diff --git a/lldb/source/API/CMakeLists.txt b/lldb/source/API/CMakeLists.txt
index 65eebfd5f847c..76ee0ecd60499 100644
--- a/lldb/source/API/CMakeLists.txt
+++ b/lldb/source/API/CMakeLists.txt
@@ -181,6 +181,8 @@ if (NOT CMAKE_SYSTEM_NAME MATCHES "Windows")
       VERBATIM
       COMMENT "Merging script interpreter export lists for liblldb"
     )
+    add_custom_target(liblldb-script-interpreter-exports
+      DEPENDS ${generated_exports_file})
     message(STATUS "Symbols (liblldb): exporting the lldb namespace plus "
                    "script interpreter dependencies")
     lldb_apply_exports_file(liblldb ${generated_exports_file})



More information about the lldb-commits mailing list