[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