Skip to content

[lldb] Add PythonRuntimeLoader for runtime libpython lookup (NFC)#200524

Open
JDevlieghere wants to merge 1 commit into
llvm:mainfrom
JDevlieghere:PythonRuntimeLoader
Open

[lldb] Add PythonRuntimeLoader for runtime libpython lookup (NFC)#200524
JDevlieghere wants to merge 1 commit into
llvm:mainfrom
JDevlieghere:PythonRuntimeLoader

Conversation

@JDevlieghere
Copy link
Copy Markdown
Member

@JDevlieghere JDevlieghere commented May 29, 2026

Generalizes the Windows-only Python lookup in lldb/Host/windows/ PythonPathSetup into a cross-platform abstraction. Adds lldb_private::PythonRuntimeLoader in lldb/Host/PythonRuntimeLoader.h, whose Load() member dlopens libpython into the current process via llvm::sys::DynamicLibrary::getPermanentLibrary so the script interpreter plugin's stable-ABI references can resolve against a runtime-supplied Python.

The loader no-ops when Python is already in the process (lldb-in-python case), then walks: LLDB_PYTHON_LIBRARY env override, the build-time Python baked in via CMake, and a platform candidate list:

  • Darwin: HostInfoMacOSX::GetXcodeDeveloperDirectory() already covers DEVELOPER_DIR, the enclosing Xcode.app when LLDB ships inside one, and an xcrun --show-sdk-path fallback, joined against Python3.framework. Then explicit Command Line Tools, python.org, /opt/homebrew, and /usr/local prefixes. Bare libpython3.dylib as a last resort to hit the dyld cache.
  • Linux: libpython3.so plus descending stable-ABI SONAMEs.
  • Windows the LLDB_PYTHON_RUNTIME_LIBRARY_FILENAME bare name (resolved via the loader's default search list) and the exe-relative LLDB_PYTHON_DLL_RELATIVE_PATH fallback (built off HostInfo::GetProgramFileSpec). Pre-mapping python3xx.dll lets the script interpreter plugin's delay-load thunks resolve against the already-loaded module by base name on first use.

This commit only introduces the abstraction. No existing call site is changed, and the script interpreter plugin still hard-links libpython, which are part of two follow-up PRs.

Generalizes the Windows-only Python lookup in lldb/Host/windows/
PythonPathSetup into a cross-platform abstraction. Adds
lldb_private::PythonRuntimeLoader in lldb/Host/PythonRuntimeLoader.h,
whose Load() member dlopens libpython into the current process via
llvm::sys::DynamicLibrary::getPermanentLibrary so the script interpreter
plugin's stable-ABI references can resolve against a runtime-supplied
Python.

The loader no-ops when Python is already in the process (lldb-in-python
case), then walks: LLDB_PYTHON_LIBRARY env override, the build-time
Python baked in via CMake, and a platform candidate list:

- **Darwin**: HostInfoMacOSX::GetXcodeDeveloperDirectory() already
  covers DEVELOPER_DIR, the enclosing Xcode.app when LLDB ships inside
  one, and an `xcrun --show-sdk-path` fallback — joined against
  Python3.framework. Then explicit Command Line Tools, python.org,
  /opt/homebrew, and /usr/local prefixes. Bare libpython3.dylib as a
  last resort to hit the dyld cache.
- **Linux**: libpython3.so plus descending stable-ABI SONAMEs.
- **Windows** the LLDB_PYTHON_RUNTIME_LIBRARY_FILENAME bare name
  (resolved via the loader's default search list) and the exe-relative
  LLDB_PYTHON_DLL_RELATIVE_PATH fallback (built off
  HostInfo::GetProgramFileSpec). Pre-mapping python3xx.dll lets the
  script interpreter plugin's delay-load thunks resolve against the
  already-loaded module by base name on first use.

This commit only introduces the abstraction. No existing call site is
changed, and the script interpreter plugin still hard-links libpython.
@llvmorg-github-actions
Copy link
Copy Markdown

@llvm/pr-subscribers-lldb

Author: Jonas Devlieghere (JDevlieghere)

Changes

Generalizes the Windows-only Python lookup in lldb/Host/windows/ PythonPathSetup into a cross-platform abstraction. Adds lldb_private::PythonRuntimeLoader in lldb/Host/PythonRuntimeLoader.h, whose Load() member dlopens libpython into the current process via llvm::sys::DynamicLibrary::getPermanentLibrary so the script interpreter plugin's stable-ABI references can resolve against a runtime-supplied Python.

The loader no-ops when Python is already in the process (lldb-in-python case), then walks: LLDB_PYTHON_LIBRARY env override, the build-time Python baked in via CMake, and a platform candidate list:

  • Darwin: HostInfoMacOSX::GetXcodeDeveloperDirectory() already covers DEVELOPER_DIR, the enclosing Xcode.app when LLDB ships inside one, and an xcrun --show-sdk-path fallback, joined against Python3.framework. Then explicit Command Line Tools, python.org, /opt/homebrew, and /usr/local prefixes. Bare libpython3.dylib as a last resort to hit the dyld cache.
  • Linux: libpython3.so plus descending stable-ABI SONAMEs.
  • Windows the LLDB_PYTHON_RUNTIME_LIBRARY_FILENAME bare name (resolved via the loader's default search list) and the exe-relative LLDB_PYTHON_DLL_RELATIVE_PATH fallback (built off HostInfo::GetProgramFileSpec). Pre-mapping python3xx.dll lets the script interpreter plugin's delay-load thunks resolve against the already-loaded module by base name on first use.

This commit only introduces the abstraction. No existing call site is changed, and the script interpreter plugin still hard-links libpython, which are part of two follow-up PRs.


Patch is 27.04 KiB, truncated to 20.00 KiB below, full version: https://github.com/llvm/llvm-project/pull/200524.diff

12 Files Affected:

  • (modified) lldb/cmake/modules/LLDBConfig.cmake (+11)
  • (modified) lldb/include/lldb/Host/Config.h.cmake (+2)
  • (added) lldb/include/lldb/Host/PythonRuntimeLoader.h (+52)
  • (modified) lldb/include/module.modulemap (+1)
  • (modified) lldb/source/Host/CMakeLists.txt (+31)
  • (added) lldb/source/Host/common/PythonRuntimeLoader.cpp (+165)
  • (added) lldb/source/Host/common/PythonRuntimeLoaderInternal.h (+31)
  • (added) lldb/source/Host/linux/PythonRuntimeLoaderLinux.cpp (+42)
  • (added) lldb/source/Host/macosx/PythonRuntimeLoaderDarwin.cpp (+143)
  • (added) lldb/source/Host/windows/PythonRuntimeLoaderWindows.cpp (+100)
  • (modified) lldb/unittests/Host/CMakeLists.txt (+6)
  • (added) lldb/unittests/Host/PythonRuntimeLoaderTest.cpp (+34)
diff --git a/lldb/cmake/modules/LLDBConfig.cmake b/lldb/cmake/modules/LLDBConfig.cmake
index cf890fa9a9c44..4651fdd6b43b8 100644
--- a/lldb/cmake/modules/LLDBConfig.cmake
+++ b/lldb/cmake/modules/LLDBConfig.cmake
@@ -211,6 +211,17 @@ if (LLDB_ENABLE_PYTHON)
       "Path to use as PYTHONHOME in lldb. If a relative path is specified, it will be resolved at runtime relative to liblldb directory.")
   endif()
 
+  # Capture the build-time libpython path so Config.h can expose it as a
+  # runtime fallback for the dynamic Python plugin loader. Going through
+  # Config.h.cmake's R"(...)" substitution avoids the per-platform escaping
+  # hazards of stuffing the path into a target_compile_definitions string.
+  if(TARGET Python3::Python)
+    get_target_property(_Python3_LIB_PATH Python3::Python IMPORTED_LIBRARY_LOCATION)
+    if(_Python3_LIB_PATH)
+      set(LLDB_PYTHON_RUNTIME_LIBRARY_BUILD_PATH "${_Python3_LIB_PATH}")
+    endif()
+  endif()
+
   # Enable targeting the Python Limited C API.
   set(PYTHON_LIMITED_API_MIN_SWIG_VERSION "4.2")
   if (SWIG_VERSION VERSION_EQUAL "4.4.0" AND Python3_VERSION VERSION_GREATER_EQUAL "3.13")
diff --git a/lldb/include/lldb/Host/Config.h.cmake b/lldb/include/lldb/Host/Config.h.cmake
index ae32c001ee5dc..19fee25f96bfa 100644
--- a/lldb/include/lldb/Host/Config.h.cmake
+++ b/lldb/include/lldb/Host/Config.h.cmake
@@ -57,6 +57,8 @@
 
 #cmakedefine LLDB_PYTHON_HOME R"(${LLDB_PYTHON_HOME})"
 
+#cmakedefine LLDB_PYTHON_RUNTIME_LIBRARY_BUILD_PATH R"(${LLDB_PYTHON_RUNTIME_LIBRARY_BUILD_PATH})"
+
 #define LLDB_INSTALL_LIBDIR_BASENAME "${LLDB_INSTALL_LIBDIR_BASENAME}"
 
 #cmakedefine LLDB_GLOBAL_INIT_DIRECTORY R"(${LLDB_GLOBAL_INIT_DIRECTORY})"
diff --git a/lldb/include/lldb/Host/PythonRuntimeLoader.h b/lldb/include/lldb/Host/PythonRuntimeLoader.h
new file mode 100644
index 0000000000000..f046a902a2625
--- /dev/null
+++ b/lldb/include/lldb/Host/PythonRuntimeLoader.h
@@ -0,0 +1,52 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_HOST_PYTHONRUNTIMELOADER_H
+#define LLDB_HOST_PYTHONRUNTIMELOADER_H
+
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Error.h"
+
+namespace lldb_private {
+
+class PythonRuntimeLoader {
+public:
+  /// Make the Python runtime available in the current process so the
+  /// ScriptInterpreterPython plugin's undefined Python symbols can resolve.
+  ///
+  /// On POSIX this dlopens libpython into the process via
+  /// llvm::sys::DynamicLibrary::getPermanentLibrary, leaving Python's
+  /// stable-ABI symbols visible in the global namespace. On Windows this
+  /// configures the DLL search path so the plugin's delay-load thunks can
+  /// find python3xx.dll.
+  ///
+  /// No-op when Python is already loaded in the current process (i.e. when
+  /// LLDB is imported into a Python interpreter).
+  ///
+  /// Honors the LLDB_PYTHON_LIBRARY environment variable (full path to a
+  /// libpython binary or framework Python file). Otherwise walks a
+  /// platform-specific list of well-known locations (Xcode, Command Line
+  /// Tools, /Library/Frameworks, /opt/homebrew, /usr/local on Darwin; SONAME
+  /// variants on Linux).
+  ///
+  /// The first call drives the load; subsequent calls return the cached
+  /// outcome. Returns success on no-op, on first successful load, or on
+  /// builds without Python support. Returns an Error aggregating the
+  /// per-candidate failures when no Python runtime can be located.
+  static llvm::Error Load();
+
+  /// Path of the Python runtime that was loaded, for diagnostics. Empty if
+  /// Python was already in the process, if loading failed, or on builds
+  /// without Python support. Triggers the load on first call, mirroring
+  /// Load().
+  static llvm::StringRef GetLoadedPath();
+};
+
+} // namespace lldb_private
+
+#endif // LLDB_HOST_PYTHONRUNTIMELOADER_H
diff --git a/lldb/include/module.modulemap b/lldb/include/module.modulemap
index 8429ff9b331ad..02f2fd82c5a6f 100644
--- a/lldb/include/module.modulemap
+++ b/lldb/include/module.modulemap
@@ -46,6 +46,7 @@ module lldb_Host {
   module ProcessLaunchInfo { header "lldb/Host/ProcessLaunchInfo.h" export * }
   module ProcessRunLock { header "lldb/Host/ProcessRunLock.h" export * }
   module PseudoTerminal { header "lldb/Host/PseudoTerminal.h" export * }
+  module PythonRuntimeLoader { header "lldb/Host/PythonRuntimeLoader.h" export * }
   module SafeMachO { header "lldb/Host/SafeMachO.h" export * }
   module SocketAddress { header "lldb/Host/SocketAddress.h" export * }
   module Socket { header "lldb/Host/Socket.h" export * }
diff --git a/lldb/source/Host/CMakeLists.txt b/lldb/source/Host/CMakeLists.txt
index f7bd00457613a..ee67e83ec8c82 100644
--- a/lldb/source/Host/CMakeLists.txt
+++ b/lldb/source/Host/CMakeLists.txt
@@ -55,6 +55,12 @@ add_host_subdirectory(common
   common/ZipFileResolver.cpp
   )
 
+if (LLDB_ENABLE_PYTHON)
+  add_host_subdirectory(common
+    common/PythonRuntimeLoader.cpp
+    )
+endif()
+
 if (LLDB_ENABLE_LIBEDIT)
   add_host_subdirectory(common
     common/Editline.cpp
@@ -84,6 +90,11 @@ if (CMAKE_SYSTEM_NAME MATCHES "Windows")
     windows/ProcessRunLock.cpp
     windows/WindowsFileAction.cpp
     )
+  if (LLDB_ENABLE_PYTHON)
+    add_host_subdirectory(windows
+      windows/PythonRuntimeLoaderWindows.cpp
+      )
+  endif()
 else()
   add_host_subdirectory(posix
     posix/DomainSocket.cpp
@@ -110,6 +121,11 @@ else()
       macosx/cfcpp/CFCMutableSet.cpp
       macosx/cfcpp/CFCString.cpp
       )
+    if (LLDB_ENABLE_PYTHON)
+      add_host_subdirectory(macosx
+        macosx/PythonRuntimeLoaderDarwin.cpp
+        )
+    endif()
     if(APPLE_EMBEDDED)
       set_property(SOURCE macosx/Host.mm APPEND PROPERTY
                COMPILE_DEFINITIONS "NO_XPC_SERVICES=1")
@@ -124,6 +140,11 @@ else()
       linux/LibcGlue.cpp
       linux/Support.cpp
       )
+    if (LLDB_ENABLE_PYTHON)
+      add_host_subdirectory(linux
+        linux/PythonRuntimeLoaderLinux.cpp
+        )
+    endif()
     if (CMAKE_SYSTEM_NAME MATCHES "Android")
       add_host_subdirectory(android
         android/HostInfoAndroid.cpp
@@ -201,3 +222,13 @@ add_lldb_library(lldbHost NO_PLUGIN_DEPENDENCIES
     lldbHostMacOSXObjCXX
   )
 
+if (WIN32 AND LLDB_ENABLE_PYTHON)
+  if (DEFINED LLDB_PYTHON_RUNTIME_LIBRARY_FILENAME)
+    target_compile_definitions(lldbHost PRIVATE
+      LLDB_PYTHON_RUNTIME_LIBRARY_FILENAME="${LLDB_PYTHON_RUNTIME_LIBRARY_FILENAME}")
+  endif()
+  if (DEFINED LLDB_PYTHON_DLL_RELATIVE_PATH)
+    target_compile_definitions(lldbHost PRIVATE
+      LLDB_PYTHON_DLL_RELATIVE_PATH="${LLDB_PYTHON_DLL_RELATIVE_PATH}")
+  endif()
+endif()
diff --git a/lldb/source/Host/common/PythonRuntimeLoader.cpp b/lldb/source/Host/common/PythonRuntimeLoader.cpp
new file mode 100644
index 0000000000000..1b9f64d05cf5f
--- /dev/null
+++ b/lldb/source/Host/common/PythonRuntimeLoader.cpp
@@ -0,0 +1,165 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/Config.h"
+
+#if LLDB_ENABLE_PYTHON
+
+#include "lldb/Host/PythonRuntimeLoader.h"
+
+#include "PythonRuntimeLoaderInternal.h"
+#include "lldb/Utility/LLDBLog.h"
+#include "lldb/Utility/Log.h"
+#include "llvm/Support/DynamicLibrary.h"
+#include "llvm/Support/ErrorExtras.h"
+#include "llvm/Support/Threading.h"
+
+#include <cstdlib>
+#include <string>
+
+#if defined(_WIN32)
+#include "lldb/Host/windows/windows.h"
+#else
+#include <dlfcn.h>
+#endif
+
+namespace lldb_private {
+
+namespace {
+
+/// Process-wide state of the Python runtime load attempt. Populated once via
+/// GetState and observed read-only thereafter.
+struct PythonRuntimeState {
+  /// Path of the Python runtime that was loaded, empty if Python was already
+  /// in the process or no load was attempted.
+  std::string path;
+  /// Aggregated error message, empty if no attempt has been made or the load
+  /// succeeded.
+  std::string error_message;
+};
+
+/// True if libpython is already in the current process. This happens when
+/// LLDB is imported into a Python interpreter, in which case we must not
+/// dlopen another libpython on top of it.
+///
+/// Probe two stable-ABI symbols. Py_IsInitialized has existed forever
+/// (including incompatible Python 2), but Py_InitializeFromConfig is 3.8+.
+/// Requiring both rejects a stale Python 2 libpython, a stub, or another
+/// tool's vendored runtime that happens to export Py_IsInitialized.
+bool IsPythonAlreadyLoaded() {
+#if defined(_WIN32)
+  HMODULE main = ::GetModuleHandleW(nullptr);
+  return ::GetProcAddress(main, "Py_IsInitialized") != nullptr &&
+         ::GetProcAddress(main, "Py_InitializeFromConfig") != nullptr;
+#else
+  return ::dlsym(RTLD_DEFAULT, "Py_IsInitialized") != nullptr &&
+         ::dlsym(RTLD_DEFAULT, "Py_InitializeFromConfig") != nullptr;
+#endif
+}
+
+/// Returns success only if the load succeeded and the resulting library
+/// exposes Py_IsInitialized (a stable-ABI symbol).
+llvm::Error TryLoad(llvm::StringRef path) {
+  std::string err_msg;
+  llvm::sys::DynamicLibrary lib =
+      llvm::sys::DynamicLibrary::getPermanentLibrary(path.str().c_str(),
+                                                     &err_msg);
+  if (!lib.isValid())
+    return llvm::createStringErrorV("could not load '{0}': {1}", path, err_msg);
+
+  if (!lib.getAddressOfSymbol("Py_IsInitialized"))
+    return llvm::createStringErrorV(
+        "'{0}' does not export Py_IsInitialized; not a Python runtime", path);
+
+  return llvm::Error::success();
+}
+
+llvm::Error LoadPythonRuntimeImpl(std::string &out_path) {
+  if (IsPythonAlreadyLoaded())
+    return llvm::Error::success();
+
+  llvm::Error failures =
+      llvm::createStringError("could not locate the Python runtime library");
+
+  auto try_path = [&](llvm::StringRef path) -> bool {
+    if (llvm::Error err = TryLoad(path)) {
+      failures = llvm::joinErrors(std::move(failures), std::move(err));
+      return false;
+    }
+    out_path = std::string(path);
+    return true;
+  };
+
+  // Honor an explicit override via LLDB_PYTHON_LIBRARY. Empty values are
+  // ignored so an exported-but-unset variable doesn't trigger dlopen("").
+  if (const char *override_path = std::getenv("LLDB_PYTHON_LIBRARY");
+      override_path && *override_path) {
+    if (try_path(override_path)) {
+      consumeError(std::move(failures));
+      return llvm::Error::success();
+    }
+  }
+
+#ifdef LLDB_PYTHON_RUNTIME_LIBRARY_BUILD_PATH
+  // Try the Python lldb was linked against at build time. The stable-ABI
+  // guarantees most closely match what the plugin was built for, so prefer
+  // it over the platform search list when an env override hasn't been set
+  // (or has been set but didn't load).
+  if (try_path(LLDB_PYTHON_RUNTIME_LIBRARY_BUILD_PATH)) {
+    consumeError(std::move(failures));
+    return llvm::Error::success();
+  }
+#endif
+
+  // Walk platform-specific well-known locations. The first candidate that
+  // loads cleanly wins; the rest of the list is not enumerated.
+  bool found = false;
+  ForEachPythonRuntimeCandidate(
+      [&](llvm::StringRef candidate) { return found = try_path(candidate); });
+
+  if (found) {
+    consumeError(std::move(failures));
+    return llvm::Error::success();
+  }
+  return failures;
+}
+
+const PythonRuntimeState &GetState() {
+  static PythonRuntimeState g_state;
+  static llvm::once_flag g_once;
+  llvm::call_once(g_once, [] {
+    if (llvm::Error err = LoadPythonRuntimeImpl(g_state.path)) {
+      g_state.error_message = llvm::toString(std::move(err));
+      LLDB_LOG(GetLog(LLDBLog::Host), "Python runtime load failed: {0}",
+               g_state.error_message);
+    }
+  });
+  return g_state;
+}
+
+} // namespace
+
+llvm::Error PythonRuntimeLoader::Load() {
+  const PythonRuntimeState &state = GetState();
+  if (state.error_message.empty())
+    return llvm::Error::success();
+  return llvm::createStringError(state.error_message);
+}
+
+llvm::StringRef PythonRuntimeLoader::GetLoadedPath() { return GetState().path; }
+
+} // namespace lldb_private
+
+#else // !LLDB_ENABLE_PYTHON
+
+namespace lldb_private {
+llvm::Error PythonRuntimeLoader::Load() { return llvm::Error::success(); }
+llvm::StringRef PythonRuntimeLoader::GetLoadedPath() { return {}; }
+} // namespace lldb_private
+
+#endif // LLDB_ENABLE_PYTHON
diff --git a/lldb/source/Host/common/PythonRuntimeLoaderInternal.h b/lldb/source/Host/common/PythonRuntimeLoaderInternal.h
new file mode 100644
index 0000000000000..a27ed74c1eaa7
--- /dev/null
+++ b/lldb/source/Host/common/PythonRuntimeLoaderInternal.h
@@ -0,0 +1,31 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLDB_SOURCE_HOST_COMMON_PYTHONRUNTIMELOADERINTERNAL_H
+#define LLDB_SOURCE_HOST_COMMON_PYTHONRUNTIMELOADERINTERNAL_H
+
+#include "llvm/ADT/STLFunctionalExtras.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace lldb_private {
+
+/// Platform-specific candidate enumeration. Calls \p callback once per
+/// candidate path in priority order; stops at the first call that returns
+/// true. Implemented in PythonRuntimeLoaderDarwin.cpp /
+/// PythonRuntimeLoaderLinux.cpp / PythonRuntimeLoaderWindows.cpp.
+///
+/// Using a callback (rather than returning a vector) lets the caller
+/// short-circuit on the first candidate that loads cleanly, so platforms
+/// that synthesize candidates lazily (e.g. Darwin invokes `xcrun` only when
+/// hardcoded paths miss) don't pay for the more expensive ones up front.
+void ForEachPythonRuntimeCandidate(
+    llvm::function_ref<bool(llvm::StringRef)> callback);
+
+} // namespace lldb_private
+
+#endif // LLDB_SOURCE_HOST_COMMON_PYTHONRUNTIMELOADERINTERNAL_H
diff --git a/lldb/source/Host/linux/PythonRuntimeLoaderLinux.cpp b/lldb/source/Host/linux/PythonRuntimeLoaderLinux.cpp
new file mode 100644
index 0000000000000..7311756889313
--- /dev/null
+++ b/lldb/source/Host/linux/PythonRuntimeLoaderLinux.cpp
@@ -0,0 +1,42 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/Config.h"
+
+#if LLDB_ENABLE_PYTHON
+
+#include "../common/PythonRuntimeLoaderInternal.h"
+#include "llvm/ADT/STLFunctionalExtras.h"
+#include "llvm/ADT/StringRef.h"
+
+namespace lldb_private {
+
+void ForEachPythonRuntimeCandidate(
+    llvm::function_ref<bool(llvm::StringRef)> callback) {
+  // Bare names rely on the dynamic linker's search (LD_LIBRARY_PATH,
+  // ldconfig cache, default lib paths). Stable ABI guarantees any of these
+  // is sufficient for a stable-ABI plugin.
+  //
+  // libpython3.so is generally only present in -dev packages, so the
+  // versioned SONAMEs are tried as a fallback. The supported range is
+  // 3.8+ (the lower bound is the Python Stable ABI baseline LLDB already
+  // requires).
+  static constexpr llvm::StringLiteral kCandidates[] = {
+      "libpython3.so",        "libpython3.13.so.1.0", "libpython3.12.so.1.0",
+      "libpython3.11.so.1.0", "libpython3.10.so.1.0", "libpython3.9.so.1.0",
+      "libpython3.8.so.1.0",
+  };
+  for (llvm::StringRef candidate : kCandidates) {
+    if (callback(candidate))
+      return;
+  }
+}
+
+} // namespace lldb_private
+
+#endif // LLDB_ENABLE_PYTHON
diff --git a/lldb/source/Host/macosx/PythonRuntimeLoaderDarwin.cpp b/lldb/source/Host/macosx/PythonRuntimeLoaderDarwin.cpp
new file mode 100644
index 0000000000000..5df0ee1773f7e
--- /dev/null
+++ b/lldb/source/Host/macosx/PythonRuntimeLoaderDarwin.cpp
@@ -0,0 +1,143 @@
+//===----------------------------------------------------------------------===//
+//
+// Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+// See https://llvm.org/LICENSE.txt for license information.
+// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+//
+//===----------------------------------------------------------------------===//
+
+#include "lldb/Host/Config.h"
+
+#if LLDB_ENABLE_PYTHON
+
+#include "../common/PythonRuntimeLoaderInternal.h"
+#include "llvm/ADT/STLFunctionalExtras.h"
+#include "llvm/ADT/ScopeExit.h"
+#include "llvm/ADT/SmallString.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/FileSystem.h"
+#include "llvm/Support/MemoryBuffer.h"
+#include "llvm/Support/Path.h"
+#include "llvm/Support/Program.h"
+
+#include <cstdlib>
+#include <memory>
+#include <optional>
+#include <string>
+
+namespace lldb_private {
+
+namespace {
+
+// Apple Xcode and Command Line Tools ship Python3.framework, whose framework
+// binary is named "Python3" (matching the framework name).
+constexpr llvm::StringLiteral kAppleFrameworkSuffix =
+    "Library/Frameworks/Python3.framework/Versions/Current/Python3";
+// python.org and Homebrew distribute Python.framework, whose framework binary
+// is named "Python".
+constexpr llvm::StringLiteral kPythonOrgFrameworkSuffix =
+    "Library/Frameworks/Python.framework/Versions/Current/Python";
+
+/// Build an absolute path by joining \p base and \p relative; emit it via the
+/// callback only if the file exists. Returns the callback's result so the
+/// caller can short-circuit on first success.
+bool TryIfExists(llvm::function_ref<bool(llvm::StringRef)> callback,
+                 llvm::StringRef base, llvm::StringRef relative) {
+  llvm::SmallString<256> path(base);
+  llvm::sys::path::append(path, relative);
+  if (!llvm::sys::fs::exists(path))
+    return false;
+  return callback(path);
+}
+
+/// Invoke `xcrun -f python3` and follow the resulting interpreter path back
+/// to its enclosing Python3.framework. Empty string on any failure or when
+/// the framework binary is not at the conventional location relative to the
+/// developer dir.
+std::string FindPythonViaXcrun() {
+  llvm::ErrorOr<std::string> xcrun = llvm::sys::findProgramByName("xcrun");
+  if (!xcrun)
+    return {};
+
+  llvm::SmallString<128> stdout_path;
+  if (llvm::sys::fs::createTemporaryFile("xcrun-python3", "txt", stdout_path))
+    return {};
+  auto remove_temp =
+      llvm::scope_exit([&] { llvm::sys::fs::remove(stdout_path.str()); });
+
+  std::optional<llvm::StringRef> redirects[3] = {
+      llvm::StringRef(""), llvm::StringRef(stdout_path), llvm::StringRef("")};
+  llvm::StringRef args[] = {*xcrun, "-f", "python3"};
+  int rc =
+      llvm::sys::ExecuteAndWait(*xcrun, args, /*env=*/std::nullopt, redirects);
+  if (rc != 0)
+    return {};
+
+  llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>> buf =
+      llvm::MemoryBuffer::getFile(stdout_path.str());
+  if (!buf)
+    return {};
+
+  llvm::StringRef python = (*buf)->getBuffer().rtrim();
+  if (python.empty())
+    return {};
+
+  // python is e.g. /Applications/Xcode.app/Contents/Developer/usr/bin/python3.
+  // Validate the structure (.../usr/bin/python3) before deriving the developer
+  // dir; if xcrun returned a non-Xcode interpreter (e.g. a Homebrew shim)
+  // there's no framework at the expected location and we should bail.
+  if (llvm::sys::path::filename(python) != "python3")
+    return {};
+  llvm::StringRef parent = llvm::sys::path::parent_path(python);
+  if (llvm::sys::path::filename(parent) != "bin")
+    return {};
+  llvm::StringRef grandparent = llvm::sys::path::parent_path(parent);
+  if (llvm::sys::path::filename(grandparent) != "usr")
+    return {};
+  llvm::StringRef developer = llvm::sys::path::parent_path(grandparent);...
[truncated]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant