[lldb] [llvm] [WIP][lldb] Add mechanism for auto-loading Python scripts from pre-configured paths (PR #187031)

Michael Buch via llvm-commits llvm-commits at lists.llvm.org
Tue Mar 17 07:46:54 PDT 2026


https://github.com/Michael137 updated https://github.com/llvm/llvm-project/pull/187031

>From 5cd3f6b5d0b784987dd3903cfddc85d4fc748c2a Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Fri, 13 Mar 2026 15:40:12 +0000
Subject: [PATCH 1/2] TEMPORARY: initial libc++ test

---
 libc++.1/libcxx_1.py | 471 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 471 insertions(+)
 create mode 100644 libc++.1/libcxx_1.py

diff --git a/libc++.1/libcxx_1.py b/libc++.1/libcxx_1.py
new file mode 100644
index 0000000000000..b2cf2c47ebbe1
--- /dev/null
+++ b/libc++.1/libcxx_1.py
@@ -0,0 +1,471 @@
+"""
+Python LLDB synthetic child provider for libc++ std::map
+
+Converted from LibCxxMap.cpp
+
+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
+"""
+
+import lldb
+
+
+# The flattened layout of the std::__tree_iterator::__ptr_ looks
+# as follows:
+#
+# The following shows the contiguous block of memory:
+#
+#        +-----------------------------+ class __tree_end_node
+# __ptr_ | pointer __left_;            |
+#        +-----------------------------+ class __tree_node_base
+#        | pointer __right_;           |
+#        | __parent_pointer __parent_; |
+#        | bool __is_black_;           |
+#        +-----------------------------+ class __tree_node
+#        | __node_value_type __value_; | <<< our key/value pair
+#        +-----------------------------+
+#
+# where __ptr_ has type __iter_pointer.
+
+
+class MapEntry:
+    """Wrapper around an LLDB ValueObject representing a tree node entry."""
+
+    def __init__(self, entry_sp=None):
+        self.m_entry_sp = entry_sp
+
+    def left(self):
+        """Get the left child pointer (offset 0)."""
+        if not self.m_entry_sp:
+            return None
+        return self.m_entry_sp.CreateChildAtOffset("", 0, self.m_entry_sp.GetType())
+
+    def right(self):
+        """Get the right child pointer (offset = address size)."""
+        if not self.m_entry_sp:
+            return None
+        addr_size = self.m_entry_sp.GetProcess().GetAddressByteSize()
+        return self.m_entry_sp.CreateChildAtOffset(
+            "", addr_size, self.m_entry_sp.GetType()
+        )
+
+    def parent(self):
+        """Get the parent pointer (offset = 2 * address size)."""
+        if not self.m_entry_sp:
+            return None
+        addr_size = self.m_entry_sp.GetProcess().GetAddressByteSize()
+        return self.m_entry_sp.CreateChildAtOffset(
+            "", 2 * addr_size, self.m_entry_sp.GetType()
+        )
+
+    def value(self):
+        """Get the unsigned integer value of the entry (pointer address)."""
+        if not self.m_entry_sp:
+            return 0
+        return self.m_entry_sp.GetValueAsUnsigned(0)
+
+    def error(self):
+        """Check if the entry has an error."""
+        if not self.m_entry_sp:
+            return True
+        return self.m_entry_sp.GetError().Fail()
+
+    def null(self):
+        """Check if the entry is null (value == 0)."""
+        return self.value() == 0
+
+    def get_entry(self):
+        """Get the underlying ValueObject."""
+        return self.m_entry_sp
+
+    def set_entry(self, entry):
+        """Set the underlying ValueObject."""
+        self.m_entry_sp = entry
+
+    def __eq__(self, other):
+        if not isinstance(other, MapEntry):
+            return False
+        if self.m_entry_sp is None and other.m_entry_sp is None:
+            return True
+        if self.m_entry_sp is None or other.m_entry_sp is None:
+            return False
+        return self.m_entry_sp.GetLoadAddress() == other.m_entry_sp.GetLoadAddress()
+
+
+class MapIterator:
+    """Iterator for traversing the red-black tree backing std::map."""
+
+    def __init__(self, entry=None, depth=0):
+        self.m_entry = MapEntry(entry) if entry else MapEntry()
+        self.m_max_depth = depth
+        self.m_error = False
+
+    def value(self):
+        """Get the current entry."""
+        return self.m_entry.get_entry()
+
+    def advance(self, count):
+        """Advance the iterator by count steps and return the entry."""
+        if self.m_error:
+            return None
+
+        steps = 0
+        while count > 0:
+            self._next()
+            count -= 1
+            steps += 1
+            if self.m_error or self.m_entry.null() or (steps > self.m_max_depth):
+                return None
+
+        return self.m_entry.get_entry()
+
+    def _next(self):
+        """
+        Mimics libc++'s __tree_next algorithm, which libc++ uses
+        in its __tree_iterator::operator++.
+        """
+        if self.m_entry.null():
+            return
+
+        right = MapEntry(self.m_entry.right())
+        if not right.null():
+            self.m_entry = self._tree_min(right)
+            return
+
+        steps = 0
+        while not self._is_left_child(self.m_entry):
+            if self.m_entry.error():
+                self.m_error = True
+                return
+            self.m_entry.set_entry(self.m_entry.parent())
+            steps += 1
+            if steps > self.m_max_depth:
+                self.m_entry = MapEntry()
+                return
+
+        self.m_entry = MapEntry(self.m_entry.parent())
+
+    def _tree_min(self, x):
+        """Mimics libc++'s __tree_min algorithm."""
+        if x.null():
+            return MapEntry()
+
+        left = MapEntry(x.left())
+        steps = 0
+        while not left.null():
+            if left.error():
+                self.m_error = True
+                return MapEntry()
+            x = MapEntry(left.get_entry())  # Create new MapEntry to avoid aliasing
+            left = MapEntry(x.left())
+            steps += 1
+            if steps > self.m_max_depth:
+                return MapEntry()
+
+        return x
+
+    def _is_left_child(self, x):
+        """Check if x is a left child of its parent."""
+        if x.null():
+            return False
+        rhs = MapEntry(x.parent())
+        rhs.set_entry(rhs.left())
+        return x.value() == rhs.value()
+
+
+def _get_first_value_of_libcxx_compressed_pair(pair):
+    """
+    Get the first value from a libc++ compressed pair.
+    Handles both old and new layouts.
+    """
+    # Try __value_ first (newer layout)
+    value_sp = pair.GetChildMemberWithName("__value_")
+    if value_sp and value_sp.IsValid():
+        return value_sp
+
+    # Try __first_ (older layout)
+    first_sp = pair.GetChildMemberWithName("__first_")
+    if first_sp and first_sp.IsValid():
+        return first_sp
+
+    return None
+
+
+def _get_value_or_old_compressed_pair(tree, value_name, pair_name):
+    """
+    Try to get a value member directly, or fall back to compressed pair layout.
+    Returns (value_sp, is_compressed_pair).
+    """
+    # Try new layout first (direct member)
+    value_sp = tree.GetChildMemberWithName(value_name)
+    if value_sp and value_sp.IsValid():
+        return (value_sp, False)
+
+    # Fall back to old compressed pair layout
+    pair_sp = tree.GetChildMemberWithName(pair_name)
+    if pair_sp and pair_sp.IsValid():
+        return (pair_sp, True)
+
+    return (None, False)
+
+
+class LibcxxStdMapSyntheticProvider:
+    """Synthetic children provider for libc++ std::map."""
+
+    def __init__(self, valobj, internal_dict):
+        self.valobj = valobj
+        self.m_tree = None
+        self.m_root_node = None
+        self.m_node_ptr_type = None
+        self.m_count = None
+        self.m_iterators = {}
+
+    def num_children(self):
+        """Calculate the number of children (map size)."""
+        if self.m_count is not None:
+            return self.m_count
+
+        if self.m_tree is None:
+            return 0
+
+        # Try new layout (__size_) or old compressed pair layout (__pair3_)
+        size_sp, is_compressed_pair = _get_value_or_old_compressed_pair(
+            self.m_tree, "__size_", "__pair3_"
+        )
+
+        if not size_sp:
+            return 0
+
+        if is_compressed_pair:
+            return self._calculate_num_children_for_old_compressed_pair_layout(size_sp)
+
+        self.m_count = size_sp.GetValueAsUnsigned(0)
+        return self.m_count
+
+    def _calculate_num_children_for_old_compressed_pair_layout(self, pair):
+        """Handle old libc++ compressed pair layout."""
+        node_sp = _get_first_value_of_libcxx_compressed_pair(pair)
+
+        if not node_sp:
+            return 0
+
+        self.m_count = node_sp.GetValueAsUnsigned(0)
+        return self.m_count
+
+    def get_child_index(self, name):
+        """Get the index of a child with the given name (e.g., "[0]" -> 0)."""
+        try:
+            if name.startswith("[") and name.endswith("]"):
+                return int(name[1:-1])
+        except ValueError:
+            pass
+        return None
+
+    def get_child_at_index(self, index):
+        """Get the child at the given index."""
+        num_children = self.num_children()
+        if index >= num_children:
+            return None
+
+        if self.m_tree is None or self.m_root_node is None:
+            return None
+
+        key_val_sp = self._get_key_value_pair(index, num_children)
+        if not key_val_sp:
+            # This will stop all future searches until an update() happens
+            self.m_tree = None
+            return None
+
+        # Create a synthetic child with the appropriate name
+        name = "[%d]" % index
+        potential_child_sp = key_val_sp.Clone(name)
+
+        if potential_child_sp and potential_child_sp.IsValid():
+            num_child_children = potential_child_sp.GetNumChildren()
+
+            # Handle __cc_ or __cc wrapper (1 child case)
+            if num_child_children == 1:
+                child0_sp = potential_child_sp.GetChildAtIndex(0)
+                if child0_sp:
+                    child_name = child0_sp.GetName()
+                    if child_name in ("__cc_", "__cc"):
+                        potential_child_sp = child0_sp.Clone(name)
+
+            # Handle __cc_ and __nc wrapper (2 children case)
+            elif num_child_children == 2:
+                child0_sp = potential_child_sp.GetChildAtIndex(0)
+                child1_sp = potential_child_sp.GetChildAtIndex(1)
+                if child0_sp and child1_sp:
+                    child0_name = child0_sp.GetName()
+                    child1_name = child1_sp.GetName()
+                    if child0_name in ("__cc_", "__cc") and child1_name == "__nc":
+                        potential_child_sp = child0_sp.Clone(name)
+
+        return potential_child_sp
+
+    def update(self):
+        """Update the cached state. Called when the underlying value changes."""
+        self.m_count = None
+        self.m_tree = None
+        self.m_root_node = None
+        self.m_iterators.clear()
+
+        self.m_tree = self.valobj.GetChildMemberWithName("__tree_")
+        if not self.m_tree or not self.m_tree.IsValid():
+            return False
+
+        self.m_root_node = self.m_tree.GetChildMemberWithName("__begin_node_")
+
+        # Get the __node_pointer type from the tree's type
+        tree_type = self.m_tree.GetType()
+        self.m_node_ptr_type = tree_type.FindDirectNestedType("__node_pointer")
+
+        return False
+
+    def has_children(self):
+        """Check if this object has children."""
+        return True
+
+    def _get_key_value_pair(self, idx, max_depth):
+        """
+        Returns the ValueObject for the __tree_node type that
+        holds the key/value pair of the node at index idx.
+        """
+        iterator = MapIterator(self.m_root_node, max_depth)
+
+        advance_by = idx
+        if idx > 0:
+            # If we have already created the iterator for the previous
+            # index, we can start from there and advance by 1.
+            if idx - 1 in self.m_iterators:
+                iterator = self.m_iterators[idx - 1]
+                advance_by = 1
+
+        iterated_sp = iterator.advance(advance_by)
+        if not iterated_sp:
+            # This tree is garbage - stop
+            return None
+
+        if not self.m_node_ptr_type or not self.m_node_ptr_type.IsValid():
+            return None
+
+        # iterated_sp is a __iter_pointer at this point.
+        # We can cast it to a __node_pointer (which is what libc++ does).
+        value_type_sp = iterated_sp.Cast(self.m_node_ptr_type)
+        if not value_type_sp or not value_type_sp.IsValid():
+            return None
+
+        # Finally, get the key/value pair.
+        value_type_sp = value_type_sp.GetChildMemberWithName("__value_")
+        if not value_type_sp or not value_type_sp.IsValid():
+            return None
+
+        self.m_iterators[idx] = iterator
+
+        return value_type_sp
+
+
+class LibCxxMapIteratorSyntheticProvider:
+    """Synthetic children provider for libc++ std::map::iterator."""
+
+    def __init__(self, valobj, internal_dict):
+        self.valobj = valobj
+        self.m_pair_sp = None
+
+    def num_children(self):
+        """Map iterators always have 2 children (first and second)."""
+        return 2
+
+    def get_child_index(self, name):
+        """Get the index of a child with the given name."""
+        if not self.m_pair_sp:
+            return None
+        return self.m_pair_sp.GetIndexOfChildWithName(name)
+
+    def get_child_at_index(self, index):
+        """Get the child at the given index."""
+        if not self.m_pair_sp:
+            return None
+        return self.m_pair_sp.GetChildAtIndex(index)
+
+    def update(self):
+        """Update the cached state."""
+        self.m_pair_sp = None
+
+        if not self.valobj.IsValid():
+            return False
+
+        target = self.valobj.GetTarget()
+        if not target or not target.IsValid():
+            return False
+
+        # valobj is a std::map::iterator
+        # ...which is a __map_iterator<__tree_iterator<..., __node_pointer, ...>>
+        #
+        # Then, __map_iterator::__i_ is a __tree_iterator
+        tree_iter_sp = self.valobj.GetChildMemberWithName("__i_")
+        if not tree_iter_sp or not tree_iter_sp.IsValid():
+            return False
+
+        # Type is __tree_iterator::__node_pointer
+        # (We could alternatively also get this from the template argument)
+        tree_iter_type = tree_iter_sp.GetType()
+        node_pointer_type = tree_iter_type.FindDirectNestedType("__node_pointer")
+        if not node_pointer_type or not node_pointer_type.IsValid():
+            return False
+
+        # __ptr_ is a __tree_iterator::__iter_pointer
+        iter_pointer_sp = tree_iter_sp.GetChildMemberWithName("__ptr_")
+        if not iter_pointer_sp or not iter_pointer_sp.IsValid():
+            return False
+
+        # Cast the __iter_pointer to a __node_pointer (which stores our key/value pair)
+        node_pointer_sp = iter_pointer_sp.Cast(node_pointer_type)
+        if not node_pointer_sp or not node_pointer_sp.IsValid():
+            return False
+
+        key_value_sp = node_pointer_sp.GetChildMemberWithName("__value_")
+        if not key_value_sp or not key_value_sp.IsValid():
+            return False
+
+        # Create the synthetic child, which is a pair where the key and value can be
+        # retrieved by querying the synthetic provider for
+        # get_child_index("first") and get_child_index("second")
+        # respectively.
+        #
+        # std::map stores the actual key/value pair in value_type::__cc_ (or
+        # previously __cc).
+        key_value_sp = key_value_sp.Clone("pair")
+        if key_value_sp.GetNumChildren() == 1:
+            child0_sp = key_value_sp.GetChildAtIndex(0)
+            if child0_sp:
+                child_name = child0_sp.GetName()
+                if child_name in ("__cc_", "__cc"):
+                    key_value_sp = child0_sp.Clone("pair")
+
+        self.m_pair_sp = key_value_sp
+        return False
+
+    def has_children(self):
+        """Check if this object has children."""
+        return True
+
+
+def __lldb_init_module(debugger, internal_dict):
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::map<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::set<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::multiset<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::multimap<.+> >$" -l libcxx_1.LibcxxStdMapSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(
+        f'type synthetic add -x "^std::__[[:alnum:]]+::__map_(const_)?iterator<.+>$" -l libcxx_1.LibCxxMapIteratorSyntheticProvider -w "cplusplus-py"'
+    )
+    debugger.HandleCommand(f"type category enable cplusplus-py")

>From dc2a69275a69dbbc244bd10308387f62e07a715f Mon Sep 17 00:00:00 2001
From: Michael Buch <michaelbuch12 at gmail.com>
Date: Tue, 10 Mar 2026 14:17:59 +0000
Subject: [PATCH 2/2] [lldb] Add mechanism for auto-loading Python scripts from
 pre-configured paths

https://discourse.llvm.org/t/rfc-lldb-moving-libc-data-formatters-out-of-lldb/89591
---
 lldb/include/lldb/Core/Debugger.h             |   5 +
 lldb/include/lldb/Target/Platform.h           |  62 ++-
 lldb/source/Core/CMakeLists.txt               |  18 +
 lldb/source/Core/CoreProperties.td            |   4 +
 lldb/source/Core/Debugger.cpp                 |  51 ++
 lldb/source/Core/Module.cpp                   |   3 +
 lldb/source/Core/SafeAutoloadPaths.inc.in     |   1 +
 .../Platform/MacOSX/PlatformDarwin.cpp        | 107 +---
 .../Plugins/Platform/MacOSX/PlatformDarwin.h  |   4 +-
 lldb/source/Target/Platform.cpp               | 121 ++++-
 lldb/source/Target/TargetProperties.td        |   4 +
 .../Darwin/dsym-takes-priority.test           |  31 ++
 .../safe-path-fallback-no-dsym-script.test    |  24 +
 .../SafeAutoLoad/UNIX/basic-load.test         |  24 +
 .../SafeAutoLoad/UNIX/last-path-wins.test     |  32 ++
 .../UNIX/no-match-nested-dir.test             |  24 +
 .../UNIX/no-match-wrong-dirname.test          |  24 +
 .../UNIX/no-match-wrong-scriptname.test       |  24 +
 .../UNIX/no-paths-configured.test             |  22 +
 .../UNIX/special-chars-sanitized.test         |  24 +
 .../SafeAutoLoad/UNIX/submodule-import.test   |  30 ++
 .../SafeAutoLoad/UNIX/subpackage-import.test  |  30 ++
 lldb/unittests/Platform/CMakeLists.txt        |   1 +
 .../unittests/Platform/PlatformDarwinTest.cpp | 156 +-----
 lldb/unittests/Platform/PlatformTest.cpp      | 471 ++++++++++++++++++
 lldb/unittests/Platform/TestUtils.cpp         |  42 ++
 lldb/unittests/Platform/TestUtils.h           |  59 +++
 27 files changed, 1139 insertions(+), 259 deletions(-)
 create mode 100644 lldb/source/Core/SafeAutoloadPaths.inc.in
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
 create mode 100644 lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
 create mode 100644 lldb/unittests/Platform/TestUtils.cpp
 create mode 100644 lldb/unittests/Platform/TestUtils.h

diff --git a/lldb/include/lldb/Core/Debugger.h b/lldb/include/lldb/Core/Debugger.h
index 96f586a6f1989..e8f830337d26a 100644
--- a/lldb/include/lldb/Core/Debugger.h
+++ b/lldb/include/lldb/Core/Debugger.h
@@ -76,6 +76,9 @@ struct TestingProperties : public Properties {
   TestingProperties();
   bool GetInjectVarLocListError() const;
   static TestingProperties &GetGlobalTestingProperties();
+  void SetSafeAutoLoadPaths(FileSpecList paths);
+  void AppendSafeAutoLoadPaths(FileSpec path);
+  FileSpecList GetSafeAutoLoadPaths() const;
 };
 #endif
 
@@ -131,6 +134,8 @@ class Debugger : public std::enable_shared_from_this<Debugger>,
   static void AssertCallback(llvm::StringRef message, llvm::StringRef backtrace,
                              llvm::StringRef prompt);
 
+  static FileSpecList GetSafeAutoLoadPaths();
+
   void Clear();
 
   void DispatchClientTelemetry(const lldb_private::StructuredDataImpl &entry);
diff --git a/lldb/include/lldb/Target/Platform.h b/lldb/include/lldb/Target/Platform.h
index 1ba7516f0102c..423a8e65e91d9 100644
--- a/lldb/include/lldb/Target/Platform.h
+++ b/lldb/include/lldb/Target/Platform.h
@@ -44,8 +44,53 @@ class ProcessInstanceInfoMatch;
 typedef std::vector<ProcessInstanceInfo> ProcessInstanceInfoList;
 
 class ModuleCache;
+class ScriptInterpreter;
 enum MmapFlags { eMmapFlagsPrivate = 1, eMmapFlagsAnon = 2 };
 
+/// Holds an lldb_private::Module name and a "sanitized" version
+/// of it for the purposes of loading a script of that name by
+/// the relevant ScriptInterpreter.
+///
+/// E.g., for Python the sanitized name can't include:
+/// * Special characters: '-', ' ', '.'
+/// * Python keywords
+class SanitizedScriptingModuleName {
+public:
+  SanitizedScriptingModuleName(llvm::StringRef name,
+                               ScriptInterpreter *script_interpreter);
+
+  /// Returns \c true if this name is a keyword in the associated scripting
+  /// language.
+  bool IsKeyword() const { return !m_conflicting_keyword.empty(); }
+
+  /// Returns \c true if the original name has been sanitized (i.e., required
+  /// changes).
+  bool RequiredSanitization() const {
+    return m_sanitized_name != m_original_name;
+  }
+
+  llvm::StringRef GetSanitizedName() const { return m_sanitized_name; }
+  llvm::StringRef GetOriginalName() const { return m_original_name; }
+  llvm::StringRef GetConflictingKeyword() const {
+    return m_conflicting_keyword;
+  }
+
+  /// If the script name required sanitization, and a file with the original
+  /// (unsanitized) name exists, warn the user that it won't be loaded.
+  void WarnIfInvalidUnsanitizedScriptExists(Stream &os,
+                                            const FileSpec &original_fspec,
+                                            const FileSpec &fspec);
+
+private:
+  llvm::StringRef m_original_name;
+  std::string m_sanitized_name;
+
+  /// If the m_sanitized_name conflicts with a keyword for the ScriptInterpreter
+  /// language associated with this SanitizedScriptingModuleName, is set to the
+  /// conflicting keyword. Empty otherwise.
+  std::string m_conflicting_keyword;
+};
+
 class PlatformProperties : public Properties {
 public:
   PlatformProperties();
@@ -274,9 +319,22 @@ class Platform : public PluginInterface {
   //
   // Locating the file should happen only on the local computer or using the
   // current computers global settings.
+  FileSpecList LocateExecutableScriptingResources(Target *target,
+                                                  Module &module,
+                                                  Stream &feedback_stream);
+
   virtual FileSpecList
-  LocateExecutableScriptingResources(Target *target, Module &module,
-                                     Stream &feedback_stream);
+  LocateExecutableScriptingResourcesImpl(Target *target, Module &module,
+                                         Stream &feedback_stream);
+
+  /// Helper function for \c LocateExecutableScriptingResources
+  /// which gathers FileSpecs for executable scripts from
+  /// pre-configured "safe" auto-load paths.
+  ///
+  /// Looks for a script at:
+  ///   \c <safe-path>/<module-name>/<module-name>.py
+  static FileSpecList LocateExecutableScriptingResourcesFromSafePaths(
+      Stream &feedback_stream, FileSpec module_spec, const Target &target);
 
   /// \param[in] module_spec
   ///     The ModuleSpec of a binary to find.
diff --git a/lldb/source/Core/CMakeLists.txt b/lldb/source/Core/CMakeLists.txt
index df35bd5c025f3..8770039ae412e 100644
--- a/lldb/source/Core/CMakeLists.txt
+++ b/lldb/source/Core/CMakeLists.txt
@@ -85,6 +85,24 @@ add_lldb_library(lldbCore NO_PLUGIN_DEPENDENCIES
     clangDriver
   )
 
+set(LLDB_SAFE_AUTO_LOAD_PATHS "" CACHE STRING
+    "Semicolon-separated list of paths that LLDB will automatically load scripting resources from.")
+
+# Turn list of paths into a comma-separated list to be #include'd
+# into a C-array.
+set(_entries "")
+foreach(path IN LISTS LLDB_SAFE_AUTO_LOAD_PATHS)
+  string(REPLACE "\\" "\\\\" escaped_path "${path}")
+  list(APPEND _entries "\"${escaped_path}\",")
+endforeach()
+string(JOIN "\n" SAFE_PATH_ENTRIES "${_entries}")
+
+configure_file(
+  ${CMAKE_CURRENT_SOURCE_DIR}/SafeAutoloadPaths.inc.in
+  ${CMAKE_CURRENT_BINARY_DIR}/SafeAutoloadPaths.inc
+  @ONLY
+)
+
 add_dependencies(lldbCore
   LLDBCorePropertiesGen
   LLDBCorePropertiesEnumGen)
diff --git a/lldb/source/Core/CoreProperties.td b/lldb/source/Core/CoreProperties.td
index 03326b130e067..8cea0931868aa 100644
--- a/lldb/source/Core/CoreProperties.td
+++ b/lldb/source/Core/CoreProperties.td
@@ -60,6 +60,10 @@ let Definition = "testing", Path = "testing" in {
         Global,
         DefaultFalse,
         Desc<"Used for testing LLDB only. Hide locations of local variables.">;
+  def SafeAutoloadPaths : Property<"safe-auto-load-paths", "FileSpecList">,
+                          DefaultStringValue<"">,
+                          Desc<"List of paths that LLDB will automatically "
+                               "load scripting resources from.">;
 }
 #endif
 
diff --git a/lldb/source/Core/Debugger.cpp b/lldb/source/Core/Debugger.cpp
index fc099891443df..3d4bd5b55b434 100644
--- a/lldb/source/Core/Debugger.cpp
+++ b/lldb/source/Core/Debugger.cpp
@@ -32,6 +32,7 @@
 #include "lldb/Interpreter/CommandInterpreter.h"
 #include "lldb/Interpreter/CommandReturnObject.h"
 #include "lldb/Interpreter/OptionValue.h"
+#include "lldb/Interpreter/OptionValueFileSpecList.h"
 #include "lldb/Interpreter/OptionValueLanguage.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/OptionValueSInt64.h"
@@ -212,6 +213,27 @@ enum {
 };
 #endif
 
+static const FileSpecList &GetDefaultSafeAutoLoadPaths() {
+  static const FileSpecList sSafePaths = [] {
+    // FIXME: in c++20 this could be a std::array (with CTAD deduced size)
+    // and we could statically assert that all members are non-empty.
+    const llvm::SmallVector<llvm::StringRef> kVendorSafePaths = {
+#include "SafeAutoloadPaths.inc"
+    };
+    FileSpecList fspecs;
+    for (auto path : kVendorSafePaths) {
+      assert(!path.empty());
+      LLDB_LOG(GetLog(SystemLog::System), "Safe auto-load path configured: {0}",
+               path);
+      fspecs.EmplaceBack(path);
+    }
+
+    return fspecs;
+  }();
+
+  return sSafePaths;
+}
+
 #ifndef NDEBUG
 TestingProperties::TestingProperties() {
   m_collection_sp = std::make_shared<OptionValueProperties>("testing");
@@ -228,6 +250,27 @@ TestingProperties &TestingProperties::GetGlobalTestingProperties() {
   static TestingProperties g_testing_properties;
   return g_testing_properties;
 }
+
+void TestingProperties::SetSafeAutoLoadPaths(FileSpecList paths) {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  OptionValueFileSpecList *option_value =
+      m_collection_sp->GetPropertyAtIndexAsOptionValueFileSpecList(idx);
+  assert(option_value);
+  option_value->SetCurrentValue(std::move(paths));
+}
+
+void TestingProperties::AppendSafeAutoLoadPaths(FileSpec path) {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  OptionValueFileSpecList *option_value =
+      m_collection_sp->GetPropertyAtIndexAsOptionValueFileSpecList(idx);
+  assert(option_value);
+  option_value->AppendCurrentValue(path);
+}
+
+FileSpecList TestingProperties::GetSafeAutoLoadPaths() const {
+  const uint32_t idx = ePropertySafeAutoloadPaths;
+  return GetPropertyAtIndexAs<FileSpecList>(idx, GetDefaultSafeAutoLoadPaths());
+}
 #endif
 
 LoadPluginCallbackType Debugger::g_load_plugin_callback = nullptr;
@@ -2547,3 +2590,11 @@ StructuredData::DictionarySP Debugger::GetBuildConfiguration() {
   AddLLVMTargets(*config_up);
   return config_up;
 }
+
+FileSpecList Debugger::GetSafeAutoLoadPaths() {
+#ifndef NDEBUG
+  return TestingProperties::GetGlobalTestingProperties().GetSafeAutoLoadPaths();
+#else
+  return GetDefaultSafeAutoLoadPaths();
+#endif
+}
diff --git a/lldb/source/Core/Module.cpp b/lldb/source/Core/Module.cpp
index 79618ab91e27a..3f7a55bd3fed7 100644
--- a/lldb/source/Core/Module.cpp
+++ b/lldb/source/Core/Module.cpp
@@ -1489,6 +1489,9 @@ To run all discovered debug scripts in this session:
       return false;
     }
 
+    LLDB_LOG(GetLog(LLDBLog::Modules), "Auto-loading {0}",
+             scripting_fspec.GetPath());
+
     StreamString scripting_stream;
     scripting_fspec.Dump(scripting_stream.AsRawOstream());
     LoadScriptOptions options;
diff --git a/lldb/source/Core/SafeAutoloadPaths.inc.in b/lldb/source/Core/SafeAutoloadPaths.inc.in
new file mode 100644
index 0000000000000..4312239c10a63
--- /dev/null
+++ b/lldb/source/Core/SafeAutoloadPaths.inc.in
@@ -0,0 +1 @@
+ at SAFE_PATH_ENTRIES@
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
index 2e5b458ffe297..607b494b8cb8c 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.cpp
@@ -29,6 +29,7 @@
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/OptionValueString.h"
 #include "lldb/Interpreter/Options.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/CompileUnit.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Symbol/SymbolFile.h"
@@ -80,98 +81,6 @@ static Status ExceptionMaskValidator(const char *string, void *unused) {
   return {};
 }
 
-namespace {
-/// Holds an lldb_private::Module name and a "sanitized" version
-/// of it for the purposes of loading a script of that name by
-/// the relevant ScriptInterpreter.
-///
-/// E.g., for Python the sanitized name can't include:
-/// * Special characters: '-', ' ', '.'
-/// * Python keywords
-class SanitizedScriptingModuleName {
-public:
-  SanitizedScriptingModuleName(llvm::StringRef name,
-                               ScriptInterpreter *script_interpreter)
-      : m_original_name(name), m_sanitized_name(name.str()) {
-    // FIXME: for Python, don't allow certain characters in imported module
-    // filenames. Theoretically, different scripting languages may have
-    // different sets of forbidden tokens in filenames, and that should
-    // be dealt with by each ScriptInterpreter. For now, just replace dots
-    // with underscores. In order to support anything other than Python
-    // this will need to be reworked.
-    llvm::replace(m_sanitized_name, '.', '_');
-    llvm::replace(m_sanitized_name, ' ', '_');
-    llvm::replace(m_sanitized_name, '-', '_');
-    llvm::replace(m_sanitized_name, '+', 'x');
-
-    if (script_interpreter &&
-        script_interpreter->IsReservedWord(m_sanitized_name.c_str())) {
-      m_conflicting_keyword = m_sanitized_name;
-      m_sanitized_name.insert(m_sanitized_name.begin(), '_');
-    }
-  }
-
-  /// Returns \c true if this name is a keyword in the associated scripting
-  /// language.
-  bool IsKeyword() const { return !m_conflicting_keyword.empty(); }
-
-  /// Returns \c true if the original name has been sanitized (i.e., required
-  /// changes).
-  bool RequiredSanitization() const {
-    return m_sanitized_name != m_original_name;
-  }
-
-  llvm::StringRef GetSanitizedName() const { return m_sanitized_name; }
-  llvm::StringRef GetOriginalName() const { return m_original_name; }
-  llvm::StringRef GetConflictingKeyword() const {
-    return m_conflicting_keyword;
-  }
-
-  /// If we did some replacements of reserved characters, and a
-  /// file with the untampered name exists, then warn the user
-  /// that the file as-is shall not be loaded.
-  void WarnIfInvalidUnsanitizedScriptExists(Stream &os,
-                                            const FileSpec &original_fspec,
-                                            const FileSpec &fspec) const {
-    if (!RequiredSanitization())
-      return;
-
-    // Path to unsanitized script name doesn't exist. Nothing to warn about.
-    if (!FileSystem::Instance().Exists(original_fspec))
-      return;
-
-    std::string reason_for_complaint =
-        IsKeyword() ? llvm::formatv("conflicts with the keyword '{0}'",
-                                    GetConflictingKeyword())
-                          .str()
-                    : "contains reserved characters";
-
-    if (FileSystem::Instance().Exists(fspec))
-       os.Format(
-            "debug script '{0}' cannot be loaded because '{1}' {2}. "
-            "Ignoring '{1}' and loading '{3}' instead.\n",
-            original_fspec.GetPath(), original_fspec.GetFilename(),
-            std::move(reason_for_complaint), fspec.GetFilename());
-    else
-      os.Format(
-            "debug script '{0}' cannot be loaded because '{1}' {2}. "
-            "If you intend to have this script loaded, please rename it to "
-            "'{3}' and retry.\n",
-            original_fspec.GetPath(), original_fspec.GetFilename(),
-            std::move(reason_for_complaint), fspec.GetFilename());
-  }
-
-private:
-  llvm::StringRef m_original_name;
-  std::string m_sanitized_name;
-
-  /// If the m_sanitized_name conflicts with a keyword for the ScriptInterpreter
-  /// language associated with this SanitizedScriptingModuleName, is set to the
-  /// conflicting keyword. Empty otherwise.
-  std::string m_conflicting_keyword;
-};
-} // namespace
-
 /// Destructor.
 ///
 /// The destructor is virtual since this class is designed to be
@@ -292,6 +201,7 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
     Stream &feedback_stream, FileSpec module_spec, const Target &target,
     const FileSpec &symfile_spec) {
   FileSpecList file_list;
+
   while (module_spec.GetFilename()) {
     SanitizedScriptingModuleName sanitized_name(
         module_spec.GetFilename().GetStringRef(),
@@ -336,14 +246,10 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesFromDSYM(
   return file_list;
 }
 
-FileSpecList PlatformDarwin::LocateExecutableScriptingResources(
+FileSpecList PlatformDarwin::LocateExecutableScriptingResourcesImpl(
     Target *target, Module &module, Stream &feedback_stream) {
-  if (!target)
-    return {};
-
-  // For now only Python scripts supported for auto-loading.
-  if (target->GetDebugger().GetScriptLanguage() != eScriptLanguagePython)
-    return {};
+  assert(target);
+  assert(target->GetDebugger().GetScriptLanguage() == eScriptLanguagePython);
 
   // NB some extensions might be meaningful and should not be stripped -
   // "this.binary.file"
@@ -354,8 +260,7 @@ FileSpecList PlatformDarwin::LocateExecutableScriptingResources(
 
   const FileSpec &module_spec = module.GetFileSpec();
 
-  if (!module_spec)
-    return {};
+  assert(module_spec);
 
   SymbolFile *symfile = module.GetSymbolFile();
   if (!symfile)
diff --git a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
index e884bcba5c2cc..f17636e8b2e42 100644
--- a/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
+++ b/lldb/source/Plugins/Platform/MacOSX/PlatformDarwin.h
@@ -68,8 +68,8 @@ class PlatformDarwin : public PlatformPOSIX {
                            FileSpec &sym_file) override;
 
   FileSpecList
-  LocateExecutableScriptingResources(Target *target, Module &module,
-                                     Stream &feedback_stream) override;
+  LocateExecutableScriptingResourcesImpl(Target *target, Module &module_spec,
+                                         Stream &feedback_stream) override;
 
   Status GetSharedModule(const ModuleSpec &module_spec, Process *process,
                          lldb::ModuleSP &module_sp,
diff --git a/lldb/source/Target/Platform.cpp b/lldb/source/Target/Platform.cpp
index 647f8389dc41b..f22436decbea9 100644
--- a/lldb/source/Target/Platform.cpp
+++ b/lldb/source/Target/Platform.cpp
@@ -27,6 +27,7 @@
 #include "lldb/Interpreter/OptionValueFileSpec.h"
 #include "lldb/Interpreter/OptionValueProperties.h"
 #include "lldb/Interpreter/Property.h"
+#include "lldb/Interpreter/ScriptInterpreter.h"
 #include "lldb/Symbol/ObjectFile.h"
 #include "lldb/Target/ModuleCache.h"
 #include "lldb/Target/Platform.h"
@@ -41,6 +42,7 @@
 #include "lldb/Utility/StructuredData.h"
 #include "llvm/ADT/STLExtras.h"
 #include "llvm/Support/FileSystem.h"
+#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/Path.h"
 
 // Define these constants from POSIX mman.h rather than include the file so
@@ -72,6 +74,55 @@ enum {
 
 } // namespace
 
+SanitizedScriptingModuleName::SanitizedScriptingModuleName(
+    llvm::StringRef name, ScriptInterpreter *script_interpreter)
+    : m_original_name(name), m_sanitized_name(name.str()) {
+  // FIXME: for Python, don't allow certain characters in imported module
+  // filenames. Theoretically, different scripting languages may have
+  // different sets of forbidden tokens in filenames, and that should
+  // be dealt with by each ScriptInterpreter. For now, just replace dots
+  // with underscores. In order to support anything other than Python
+  // this will need to be reworked.
+  llvm::replace(m_sanitized_name, '.', '_');
+  llvm::replace(m_sanitized_name, ' ', '_');
+  llvm::replace(m_sanitized_name, '-', '_');
+  llvm::replace(m_sanitized_name, '+', 'x');
+
+  if (script_interpreter &&
+      script_interpreter->IsReservedWord(m_sanitized_name.c_str())) {
+    m_conflicting_keyword = m_sanitized_name;
+    m_sanitized_name.insert(m_sanitized_name.begin(), '_');
+  }
+}
+
+void SanitizedScriptingModuleName::WarnIfInvalidUnsanitizedScriptExists(
+    Stream &os, const FileSpec &original_fspec, const FileSpec &fspec) {
+  if (!RequiredSanitization())
+    return;
+
+  // Path to unsanitized script name doesn't exist. Nothing to warn about.
+  if (!FileSystem::Instance().Exists(original_fspec))
+    return;
+
+  std::string reason_for_complaint =
+      IsKeyword() ? llvm::formatv("conflicts with the keyword '{0}'",
+                                  GetConflictingKeyword())
+                        .str()
+                  : "contains reserved characters";
+
+  if (FileSystem::Instance().Exists(fspec))
+    os.Format("debug script '{0}' cannot be loaded because '{1}' {2}. "
+              "Ignoring '{1}' and loading '{3}' instead.\n",
+              original_fspec.GetPath(), original_fspec.GetFilename(),
+              std::move(reason_for_complaint), fspec.GetFilename());
+  else
+    os.Format("debug script '{0}' cannot be loaded because '{1}' {2}. "
+              "If you intend to have this script loaded, please rename it to "
+              "'{3}' and retry.\n",
+              original_fspec.GetPath(), original_fspec.GetFilename(),
+              std::move(reason_for_complaint), fspec.GetFilename());
+}
+
 llvm::StringRef PlatformProperties::GetSettingName() {
   static constexpr llvm::StringLiteral g_setting_name("platform");
   return g_setting_name;
@@ -155,10 +206,78 @@ Status Platform::GetFileWithUUID(const FileSpec &platform_file,
   return Status();
 }
 
+FileSpecList Platform::LocateExecutableScriptingResourcesFromSafePaths(
+    Stream &feedback_stream, FileSpec module_spec, const Target &target) {
+  FileSpecList paths = Debugger::GetSafeAutoLoadPaths();
+  FileSpecList file_list;
+
+  ScriptInterpreter *script_interpreter =
+      target.GetDebugger().GetScriptInterpreter();
+  if (!script_interpreter)
+    return file_list;
+
+  SanitizedScriptingModuleName sanitized_name(
+      module_spec.GetFileNameStrippingExtension().GetStringRef(),
+      target.GetDebugger().GetScriptInterpreter());
+
+  // Iterate in reverse so we consider the latest appended path first.
+  for (FileSpec path : llvm::reverse(paths)) {
+    path.AppendPathComponent(sanitized_name.GetOriginalName());
+
+    if (!FileSystem::Instance().Exists(path))
+      continue;
+
+    FileSpec script_fspec = path;
+    script_fspec.AppendPathComponent(
+        llvm::formatv("{0}.py", sanitized_name.GetSanitizedName()).str());
+    FileSystem::Instance().Resolve(script_fspec);
+
+    FileSpec orig_script_fspec = path;
+    orig_script_fspec.AppendPathComponent(
+        llvm::formatv("{0}.py", sanitized_name.GetOriginalName()).str());
+    FileSystem::Instance().Resolve(orig_script_fspec);
+
+    sanitized_name.WarnIfInvalidUnsanitizedScriptExists(
+        feedback_stream, orig_script_fspec, script_fspec);
+
+    if (FileSystem::Instance().Exists(script_fspec))
+      file_list.Append(script_fspec);
+
+    // If we successfully found a directory in a safe auto-load path
+    // stop looking at any other paths.
+    break;
+  }
+
+  return file_list;
+}
+
+FileSpecList
+Platform::LocateExecutableScriptingResourcesImpl(Target *target, Module &module,
+                                                 Stream &feedback_stream) {
+  return {};
+}
+
 FileSpecList
 Platform::LocateExecutableScriptingResources(Target *target, Module &module,
                                              Stream &feedback_stream) {
-  return FileSpecList();
+  if (!target)
+    return {};
+
+  // For now only Python scripts supported for auto-loading.
+  if (target->GetDebugger().GetScriptLanguage() != eScriptLanguagePython)
+    return {};
+
+  const FileSpec &module_spec = module.GetFileSpec();
+  if (!module_spec)
+    return {};
+
+  if (FileSpecList fspecs = LocateExecutableScriptingResourcesImpl(
+          target, module, feedback_stream);
+      !fspecs.IsEmpty())
+    return fspecs;
+
+  return LocateExecutableScriptingResourcesFromSafePaths(feedback_stream,
+                                                         module_spec, *target);
 }
 
 Status Platform::GetSharedModule(
diff --git a/lldb/source/Target/TargetProperties.td b/lldb/source/Target/TargetProperties.td
index 2361314d506ac..a4e18f5b59eea 100644
--- a/lldb/source/Target/TargetProperties.td
+++ b/lldb/source/Target/TargetProperties.td
@@ -180,6 +180,10 @@ let Definition = "target", Path = "target" in {
     DefaultEnumValue<"eLoadScriptFromSymFileWarn">,
     EnumValues<"OptionEnumValues(g_load_script_from_sym_file_values)">,
     Desc<"Allow LLDB to load scripting resources embedded in symbol files when available.">;
+  def SafeLoadPaths
+      : Property<"safe-load-paths", "String">,
+        DefaultStringValue<"">,
+        Desc<"Paths that LLDB can auto-load scripting resources from.">;
   def LoadCWDlldbinitFile: Property<"load-cwd-lldbinit", "Enum">,
     DefaultEnumValue<"eLoadCWDlldbinitWarn">,
     EnumValues<"OptionEnumValues(g_load_cwd_lldbinit_values)">,
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
new file mode 100644
index 0000000000000..60b875a643c88
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/dsym-takes-priority.test
@@ -0,0 +1,31 @@
+# REQUIRES: python, asserts, system-darwin
+#
+# Test that when both a dSYM script and a safe-path script exist,
+# the dSYM script takes priority.
+
+# RUN: split-file %s %t
+# RUN: %clang_host -g %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/TestModule.out.dSYM/Contents/Resources/Python
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/dsym_script.py %t/TestModule.out.dSYM/Contents/Resources/Python/TestModule.py
+# RUN: cp %t/safe_script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s --implicit-check-not=SAFE_PATH_SCRIPT
+
+# CHECK: DSYM_SCRIPT
+
+#--- main.c
+int main() { return 0; }
+
+#--- dsym_script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("DSYM_SCRIPT", file=sys.stderr)
+
+#--- safe_script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SAFE_PATH_SCRIPT", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
new file mode 100644
index 0000000000000..b00484ecdbe6d
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/Darwin/safe-path-fallback-no-dsym-script.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, system-darwin
+#
+# Test that when a dSYM exists but has no Python script inside,
+# the safe-path script is loaded as a fallback.
+
+# RUN: split-file %s %t
+# RUN: %clang_host -g %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: SAFE_PATH_FALLBACK
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SAFE_PATH_FALLBACK", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
new file mode 100644
index 0000000000000..0ee57c14130ae
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/basic-load.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+
+# Test that LLDB auto-loads <safe-path>/<module-name>/<module-name>.py on
+# module load.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: AUTOLOAD_SUCCESS
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("AUTOLOAD_SUCCESS", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
new file mode 100644
index 0000000000000..9caf25061b802
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/last-path-wins.test
@@ -0,0 +1,32 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that the last appended safe-auto-load path takes priority.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/first-path/TestModule
+# RUN: mkdir -p %t/second-path/TestModule
+
+# RUN: cp %t/first.py %t/first-path/TestModule/TestModule.py
+# RUN: cp %t/second.py %t/second-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/first-path' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/second-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: SECOND_PATH
+# CHECK-NOT: FIRST_PATH
+
+#--- main.c
+int main() { return 0; }
+
+#--- first.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("FIRST_PATH", file=sys.stderr)
+
+#--- second.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SECOND_PATH", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
new file mode 100644
index 0000000000000..aeee2e8cd473d
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-nested-dir.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts from nested subdirectories inside the
+# module directory.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule/nested
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/nested/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
new file mode 100644
index 0000000000000..345613a378a19
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-dirname.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts when the directory name doesn't match
+# the module name.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/WrongName
+
+# RUN: cp %t/script.py %t/safe-path/WrongName/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
new file mode 100644
index 0000000000000..f7fb13878d9a8
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-match-wrong-scriptname.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that LLDB does not load scripts when the script filename inside the
+# module directory doesn't match <module-name>.py.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/other.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
new file mode 100644
index 0000000000000..14e713aee5caa
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/no-paths-configured.test
@@ -0,0 +1,22 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that nothing loads when no safe-auto-load-paths are configured.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK-NOT: SHOULD_NOT_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SHOULD_NOT_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
new file mode 100644
index 0000000000000..c2ad4a96c1bd4
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/special-chars-sanitized.test
@@ -0,0 +1,24 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that a module name with special characters (dashes, dots, spaces) is
+# sanitized and the sanitized <module-name>.py is loaded.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o "%t/Test- Module.1.out"
+# RUN: mkdir -p "%t/safe-path/Test- Module.1"
+
+# RUN: cp %t/script.py "%t/safe-path/Test- Module.1/Test__Module_1.py"
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create "%t/Test- Module.1.out"' 2>&1 | FileCheck %s
+
+# CHECK: SANITIZED_LOAD
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+def __lldb_init_module(debugger, internal_dict):
+    print("SANITIZED_LOAD", file=sys.stderr)
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
new file mode 100644
index 0000000000000..a6fa1856aa588
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/submodule-import.test
@@ -0,0 +1,30 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that an auto-loaded script can import a sibling Python submodule
+# from the same directory.
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: cp %t/helper.py %t/safe-path/TestModule/helper.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: HELPER_LOADED
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+import helper
+def __lldb_init_module(debugger, internal_dict):
+    print(helper.get_marker(), file=sys.stderr)
+
+#--- helper.py
+def get_marker():
+    return "HELPER_LOADED"
diff --git a/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
new file mode 100644
index 0000000000000..754d6482b4d87
--- /dev/null
+++ b/lldb/test/Shell/Platform/SafeAutoLoad/UNIX/subpackage-import.test
@@ -0,0 +1,30 @@
+# REQUIRES: python, asserts, !system-windows
+#
+# Test that an auto-loaded script can import a sibling Python package
+# (a subdirectory with an __init__.py).
+
+# RUN: split-file %s %t
+# RUN: %clang_host %t/main.c -o %t/TestModule.out
+# RUN: mkdir -p %t/safe-path/TestModule/helpers
+
+# RUN: cp %t/script.py %t/safe-path/TestModule/TestModule.py
+# RUN: cp %t/init.py %t/safe-path/TestModule/helpers/__init__.py
+# RUN: %lldb -b \
+# RUN:   -o 'settings set target.load-script-from-symbol-file true' \
+# RUN:   -o 'settings append testing.safe-auto-load-paths %t/safe-path' \
+# RUN:   -o 'target create %t/TestModule.out' 2>&1 | FileCheck %s
+
+# CHECK: PACKAGE_LOADED
+
+#--- main.c
+int main() { return 0; }
+
+#--- script.py
+import sys
+import helpers
+def __lldb_init_module(debugger, internal_dict):
+    print(helpers.get_marker(), file=sys.stderr)
+
+#--- init.py
+def get_marker():
+    return "PACKAGE_LOADED"
diff --git a/lldb/unittests/Platform/CMakeLists.txt b/lldb/unittests/Platform/CMakeLists.txt
index a96636fc3fd59..f8755432bf6d7 100644
--- a/lldb/unittests/Platform/CMakeLists.txt
+++ b/lldb/unittests/Platform/CMakeLists.txt
@@ -1,4 +1,5 @@
 add_lldb_unittest(LLDBPlatformTests
+  TestUtils.cpp
   PlatformAppleSimulatorTest.cpp
   PlatformDarwinTest.cpp
   PlatformMacOSXTest.cpp
diff --git a/lldb/unittests/Platform/PlatformDarwinTest.cpp b/lldb/unittests/Platform/PlatformDarwinTest.cpp
index cd7cc80847302..cc24ac239db07 100644
--- a/lldb/unittests/Platform/PlatformDarwinTest.cpp
+++ b/lldb/unittests/Platform/PlatformDarwinTest.cpp
@@ -8,6 +8,8 @@
 
 #include "gtest/gtest.h"
 
+#include "TestUtils.h"
+
 #include "Plugins/Platform/MacOSX/PlatformDarwin.h"
 #include "Plugins/Platform/MacOSX/PlatformMacOSX.h"
 #include "Plugins/Platform/MacOSX/PlatformRemoteMacOSX.h"
@@ -16,7 +18,7 @@
 #include "lldb/Core/Debugger.h"
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Host/HostInfo.h"
-#include "lldb/Interpreter/ScriptInterpreter.h"
+#include "lldb/Target/Platform.h"
 
 #include "llvm/ADT/StringRef.h"
 #include "llvm/Support/FileSystem.h"
@@ -28,54 +30,6 @@
 using namespace lldb;
 using namespace lldb_private;
 
-namespace {
-class MockScriptInterpreterPython : public ScriptInterpreter {
-public:
-  MockScriptInterpreterPython(Debugger &debugger)
-      : ScriptInterpreter(debugger,
-                          lldb::ScriptLanguage::eScriptLanguagePython) {}
-
-  ~MockScriptInterpreterPython() override = default;
-
-  bool ExecuteOneLine(llvm::StringRef command, CommandReturnObject *,
-                      const ExecuteScriptOptions &) override {
-    return false;
-  }
-
-  void ExecuteInterpreterLoop() override {}
-
-  static void Initialize() {
-    PluginManager::RegisterPlugin(GetPluginNameStatic(),
-                                  GetPluginDescriptionStatic(),
-                                  lldb::eScriptLanguagePython, CreateInstance);
-  }
-
-  static void Terminate() { PluginManager::UnregisterPlugin(CreateInstance); }
-
-  bool IsReservedWord(const char *word) override {
-    return llvm::is_contained({"import", "mykeyword_1_1_1"},
-                              llvm::StringRef(word));
-  }
-
-  static lldb::ScriptInterpreterSP CreateInstance(Debugger &debugger) {
-    return std::make_shared<MockScriptInterpreterPython>(debugger);
-  }
-
-  static llvm::StringRef GetPluginNameStatic() {
-    return "MockScriptInterpreterPython";
-  }
-
-  static llvm::StringRef GetPluginDescriptionStatic() {
-    return "MockScriptInterpreterPython";
-  }
-
-  // PluginInterface protocol
-  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
-};
-
-LLDB_PLUGIN_DEFINE(MockScriptInterpreterPython)
-} // namespace
-
 struct PlatformDarwinLocateTest : public testing::Test {
 protected:
   void SetUp() override {
@@ -144,18 +98,6 @@ struct PlatformDarwinLocateTest : public testing::Test {
       subsystems;
 };
 
-static std::string CreateFile(llvm::StringRef filename,
-                              llvm::SmallString<128> parent_dir) {
-  llvm::SmallString<128> path(parent_dir);
-  llvm::sys::path::append(path, filename);
-  int fd;
-  std::error_code ret = llvm::sys::fs::openFileForWrite(path, fd);
-  assert(!ret && "Failed to create test file.");
-  ::close(fd);
-
-  return path.c_str();
-}
-
 TEST(PlatformDarwinTest, TestParseVersionBuildDir) {
   llvm::VersionTuple V;
   llvm::StringRef D;
@@ -526,98 +468,6 @@ TEST_F(
   EXPECT_TRUE(ss.GetString().empty());
 }
 
-TEST_F(
-    PlatformDarwinLocateTest,
-    LocateExecutableScriptingResourcesFromDSYM_ModuleNameIsKeywordAfterReplacement) {
-  // Test case where the module name contains "special characters" but after
-  // LLDB replaces those the filename is still a keyword. We ensure this by a
-  // special case in MockScriptInterpreterPython::IsReservedWord.
-
-  // Create dummy module file at <test-root>/mykeyword-1.1 1.o
-  FileSpec module_fspec(CreateFile("mykeyword-1.1 1.o", m_tmp_root_dir));
-  ASSERT_TRUE(module_fspec);
-
-  // Create dummy module file at
-  // <test-root>/.dSYM/Contents/Resources/DWARF/mykeyword-1.1 1.o
-  FileSpec dsym_module_fpec(
-      CreateFile("mykeyword-1.1 1.o", m_tmp_dsym_dwarf_dir));
-  ASSERT_TRUE(dsym_module_fpec);
-
-  CreateFile("mykeyword-1.1 1.py", m_tmp_dsym_python_dir);
-
-  StreamString ss;
-  FileSpecList fspecs =
-      std::static_pointer_cast<PlatformDarwin>(m_platform_sp)
-          ->LocateExecutableScriptingResourcesFromDSYM(
-              ss, module_fspec, *m_target_sp, dsym_module_fpec);
-  EXPECT_EQ(fspecs.GetSize(), 0u);
-  EXPECT_TRUE(
-      ss.GetString().contains("conflicts with the keyword 'mykeyword_1_1_1'"));
-}
-
-TEST_F(
-    PlatformDarwinLocateTest,
-    LocateExecutableScriptingResourcesFromDSYM_ModuleNameIsKeywordAfterReplacement_Match_Warning) {
-  // Like
-  // LocateExecutableScriptingResourcesFromDSYM_ModuleNameIsKeywordAfterReplacement
-  // but we place a script with all the replacement characters into the module
-  // directory so LLDB loads it. That will still produce a warning.
-
-  // Create dummy module file at <test-root>/mykeyword-1.1 1.o
-  FileSpec module_fspec(CreateFile("mykeyword-1.1 1.o", m_tmp_root_dir));
-  ASSERT_TRUE(module_fspec);
-
-  // Create dummy module file at
-  // <test-root>/.dSYM/Contents/Resources/DWARF/mykeyword-1.1 1.o
-  FileSpec dsym_module_fpec(
-      CreateFile("mykeyword-1.1 1.o", m_tmp_dsym_dwarf_dir));
-  ASSERT_TRUE(dsym_module_fpec);
-
-  CreateFile("mykeyword-1.1 1.py", m_tmp_dsym_python_dir);
-  CreateFile("_mykeyword_1_1_1.py", m_tmp_dsym_python_dir);
-
-  StreamString ss;
-  FileSpecList fspecs =
-      std::static_pointer_cast<PlatformDarwin>(m_platform_sp)
-          ->LocateExecutableScriptingResourcesFromDSYM(
-              ss, module_fspec, *m_target_sp, dsym_module_fpec);
-  EXPECT_EQ(fspecs.GetSize(), 1u);
-  EXPECT_EQ(fspecs.GetFileSpecAtIndex(0).GetFilename(), "_mykeyword_1_1_1.py");
-  EXPECT_TRUE(ss.GetString().contains("Ignoring 'mykeyword-1.1 1.py' and "
-                                      "loading '_mykeyword_1_1_1.py' instead"));
-}
-
-TEST_F(
-    PlatformDarwinLocateTest,
-    LocateExecutableScriptingResourcesFromDSYM_ModuleNameIsKeywordAfterReplacement_Match_NoWarning) {
-  // Like
-  // LocateExecutableScriptingResourcesFromDSYM_ModuleNameIsKeywordAfterReplacement_Match_Warning
-  // but we place a script with all the replacement characters into the module
-  // directory so LLDB loads it (but no script that matches the original module
-  // name, and hence generates no warning).
-
-  // Create dummy module file at <test-root>/mykeyword-1.1 1.o
-  FileSpec module_fspec(CreateFile("mykeyword-1.1 1.o", m_tmp_root_dir));
-  ASSERT_TRUE(module_fspec);
-
-  // Create dummy module file at
-  // <test-root>/.dSYM/Contents/Resources/DWARF/mykeyword-1.1 1.o
-  FileSpec dsym_module_fpec(
-      CreateFile("mykeyword-1.1 1.o", m_tmp_dsym_dwarf_dir));
-  ASSERT_TRUE(dsym_module_fpec);
-
-  CreateFile("_mykeyword_1_1_1.py", m_tmp_dsym_python_dir);
-
-  StreamString ss;
-  FileSpecList fspecs =
-      std::static_pointer_cast<PlatformDarwin>(m_platform_sp)
-          ->LocateExecutableScriptingResourcesFromDSYM(
-              ss, module_fspec, *m_target_sp, dsym_module_fpec);
-  EXPECT_EQ(fspecs.GetSize(), 1u);
-  EXPECT_EQ(fspecs.GetFileSpecAtIndex(0).GetFilename(), "_mykeyword_1_1_1.py");
-  EXPECT_TRUE(ss.Empty());
-}
-
 struct SpecialCharTestCase {
   char special_char;
   char replacement;
diff --git a/lldb/unittests/Platform/PlatformTest.cpp b/lldb/unittests/Platform/PlatformTest.cpp
index 6299197791fc4..be20baf5d1f33 100644
--- a/lldb/unittests/Platform/PlatformTest.cpp
+++ b/lldb/unittests/Platform/PlatformTest.cpp
@@ -8,8 +8,12 @@
 
 #include "gtest/gtest.h"
 
+#include "TestUtils.h"
+
 #include "Plugins/Platform/POSIX/PlatformPOSIX.h"
 #include "TestingSupport/SubsystemRAII.h"
+#include "TestingSupport/TestUtilities.h"
+#include "lldb/Core/Debugger.h"
 #include "lldb/Core/PluginManager.h"
 #include "lldb/Host/FileSystem.h"
 #include "lldb/Host/HostInfo.h"
@@ -163,3 +167,470 @@ TEST_F(PlatformTest, CreateUnknown) {
   ASSERT_EQ(list.Create("unknown-platform-name"), nullptr);
   ASSERT_EQ(list.GetOrCreate("dummy"), nullptr);
 }
+
+struct PlatformLocateSafePathTest : public PlatformTest {
+protected:
+  void SetUp() override {
+    std::call_once(TestUtilities::g_debugger_initialize_flag,
+                   []() { Debugger::Initialize(nullptr); });
+
+    ArchSpec arch("x86_64-apple-macosx-");
+    m_platform_sp = std::make_shared<PlatformArm>();
+    Platform::SetHostPlatform(m_platform_sp);
+
+    m_debugger_sp = Debugger::CreateInstance();
+
+    m_debugger_sp->GetTargetList().CreateTarget(*m_debugger_sp, "", arch,
+                                                lldb_private::eLoadDependentsNo,
+                                                m_platform_sp, m_target_sp);
+
+    ASSERT_TRUE(m_target_sp);
+    ASSERT_TRUE(m_platform_sp);
+
+    ASSERT_FALSE(llvm::sys::fs::createUniqueDirectory(
+        "locate-scripts-from-safe-paths-test", m_tmp_root_dir))
+        << "Failed to create test directory.";
+  };
+
+  void TearDown() override {
+    llvm::sys::fs::remove_directories(m_tmp_root_dir);
+    TestingProperties::GetGlobalTestingProperties().SetSafeAutoLoadPaths({});
+  }
+
+  DebuggerSP m_debugger_sp;
+  PlatformSP m_platform_sp;
+  TargetSP m_target_sp;
+
+  /// Root directory for m_tmp_dsym_dwarf_dir and m_tmp_dsym_python_dir
+  llvm::SmallString<128> m_tmp_root_dir;
+
+  SubsystemRAII<MockScriptInterpreterPython> subsystems;
+};
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NoSetting) {
+  // Tests LocateScriptingResourcesFromSafePaths finds no script if we don't set
+  // the safe path setting.
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  ASSERT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NoMatch) {
+  // Tests LocateScriptingResourcesFromSafePaths finds no directory to load
+  // from.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  // Directory name doesn't match the module name.
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  ASSERT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Match) {
+  // Tests LocateScriptingResourcesFromSafePaths locates the
+  // <module-name>/<module-name>.py script correctly.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule.py", module_dir);
+  // Other files should be ignored.
+  CreateFile("helper.py", module_dir);
+  CreateFile("not_a_script.txt", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_NestedDir) {
+  // Tests that a matching Python file nested inside a subdirectory is not
+  // picked up. Only <module-name>/<module-name>.py at the top level matters.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  // Create a nested directory that contains the matching Python file.
+  llvm::SmallString<128> nested_dir(module_dir);
+  llvm::sys::path::append(nested_dir, "nested");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(nested_dir));
+
+  CreateFile("TestModule.py", nested_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_MultiplePaths) {
+  // Tests LocateScriptingResourcesFromSafePaths locates the script from the
+  // last appended auto-load path.
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> path1(m_tmp_root_dir);
+  llvm::sys::path::append(path1, "AnotherSafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path1));
+
+  llvm::SmallString<128> path2(m_tmp_root_dir);
+  llvm::sys::path::append(path2, "AnotherAnotherSafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path2));
+
+  llvm::SmallString<128> path3(m_tmp_root_dir);
+  llvm::sys::path::append(path3, "EmptySafePath");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path3));
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  llvm::SmallString<128> path1_module_dir(path1);
+  llvm::sys::path::append(path1_module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path1_module_dir));
+
+  llvm::SmallString<128> path2_module_dir(path2);
+  llvm::sys::path::append(path2_module_dir, "NotTheTestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path2_module_dir));
+
+  llvm::SmallString<128> path3_module_dir(path3);
+  llvm::sys::path::append(path3_module_dir, "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(path3_module_dir));
+
+  // Place the correctly named script in each module directory.
+  CreateFile("TestModule.py", module_dir);
+  CreateFile("TestModule.py", path1_module_dir);
+  CreateFile("TestModule.py", path2_module_dir);
+  // Keep path3 (EmptySafePath) empty.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path1));
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path2));
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  // path1 was the last appended path with a matching directory.
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_TRUE(llvm::StringRef(file_specs.GetFileSpecAtIndex(0).GetPath())
+                  .contains("AnotherSafePath"));
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+
+  // Now add another safe path with a valid module directory but no
+  // TestModule.py inside. LLDB shouldn't fall back to other matching safe
+  // paths.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(path3));
+
+  file_specs = Platform::LocateExecutableScriptingResourcesFromSafePaths(
+      ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  // Now place the correctly named script in path3.
+  CreateFile("TestModule.py", path3_module_dir);
+
+  file_specs = Platform::LocateExecutableScriptingResourcesFromSafePaths(
+      ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_TRUE(llvm::StringRef(file_specs.GetFileSpecAtIndex(0).GetPath())
+                  .contains("EmptySafePath"));
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "TestModule.py");
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_NoMatch) {
+  // Module name has special characters. The directory exists but only contains
+  // a script with the original (unsanitized) name. No match.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  // Only the unsanitized name exists.
+  FileSpec orig_fspec(CreateFile("TestModule-1.1 1.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because"
+      " 'TestModule-1.1 1.py' contains reserved characters. If you intend to"
+      " have this script loaded, please rename it to 'TestModule_1_1_1.py' and "
+      "retry.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_Match_Warning) {
+  // Module name has special characters. Both the original and sanitized scripts
+  // exist. LLDB loads the sanitized one and warns.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("TestModule-1.1 1.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  CreateFile("TestModule_1_1_1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(),
+            "TestModule_1_1_1.py");
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because"
+      " 'TestModule-1.1 1.py' contains reserved characters. Ignoring"
+      " 'TestModule-1.1 1.py' and loading 'TestModule_1_1_1.py' instead.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_SpecialChars_Match_NoWarning) {
+  // Module name has special characters. Only the sanitized script exists.
+  // No warning.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule-1.1 1.o
+  FileSpec module_fspec(CreateFile("TestModule-1.1 1.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "TestModule-1.1 1");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("TestModule_1_1_1.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(),
+            "TestModule_1_1_1.py");
+  EXPECT_TRUE(ss.GetString().empty());
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_NoMatch) {
+  // Module name is a reserved keyword. Only the original script exists.
+  // Warns and returns nothing.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("import.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+
+  std::string expected = llvm::formatv(
+      "debug script '{0}' cannot be loaded because 'import.py' "
+      "conflicts with the keyword 'import'. If you intend to have this script "
+      "loaded, please rename it to '_import.py' and retry.\n",
+      orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_Match) {
+  // Module name is a reserved keyword. Both original and sanitized scripts
+  // exist. Loads the sanitized one and warns.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  FileSpec orig_fspec(CreateFile("import.py", module_dir));
+  ASSERT_TRUE(orig_fspec);
+
+  CreateFile("_import.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "_import.py");
+
+  std::string expected =
+      llvm::formatv("debug script '{0}' cannot be loaded because 'import.py' "
+                    "conflicts with the keyword 'import'. Ignoring 'import.py' "
+                    "and loading '_import.py' instead.\n",
+                    orig_fspec.GetPath());
+  EXPECT_EQ(ss.GetString(), expected);
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_Keyword_Match_NoWarning) {
+  // Module name is a reserved keyword. Only the sanitized script exists.
+  // No warning.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/import.o
+  FileSpec module_fspec(CreateFile("import.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> module_dir(m_tmp_root_dir);
+  llvm::sys::path::append(module_dir, "import");
+  ASSERT_FALSE(llvm::sys::fs::create_directory(module_dir));
+
+  CreateFile("_import.py", module_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 1u);
+  EXPECT_EQ(file_specs.GetFileSpecAtIndex(0).GetFilename(), "_import.py");
+  EXPECT_TRUE(ss.GetString().empty());
+}
+
+TEST_F(PlatformLocateSafePathTest,
+       LocateScriptingResourcesFromSafePaths_InnerDirectoryHasModuleName) {
+  // Test a directory structure like
+  // <safe-path>/TestModule/TestModule/TestModule.py. LLDB should not load that
+  // inner script.
+
+  TestingProperties::GetGlobalTestingProperties().AppendSafeAutoLoadPaths(
+      FileSpec(m_tmp_root_dir));
+
+  // Create dummy module file at <test-root>/TestModule.o
+  FileSpec module_fspec(CreateFile("TestModule.o", m_tmp_root_dir));
+  ASSERT_TRUE(module_fspec);
+
+  llvm::SmallString<128> inner_dir(m_tmp_root_dir);
+  llvm::sys::path::append(inner_dir, "TestModule", "TestModule");
+  ASSERT_FALSE(llvm::sys::fs::create_directories(inner_dir));
+
+  CreateFile("TestModule.py", inner_dir);
+
+  StreamString ss;
+  FileSpecList file_specs =
+      Platform::LocateExecutableScriptingResourcesFromSafePaths(
+          ss, module_fspec, *m_target_sp);
+
+  EXPECT_EQ(file_specs.GetSize(), 0u);
+  EXPECT_TRUE(ss.GetString().empty());
+}
diff --git a/lldb/unittests/Platform/TestUtils.cpp b/lldb/unittests/Platform/TestUtils.cpp
new file mode 100644
index 0000000000000..7330395c803f8
--- /dev/null
+++ b/lldb/unittests/Platform/TestUtils.cpp
@@ -0,0 +1,42 @@
+//===-- TestUtils.cpp -----------------------------------------------------===//
+//
+// 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 "TestUtils.h"
+
+#include "lldb/Core/PluginManager.h"
+
+using namespace lldb;
+using namespace lldb_private;
+
+MockScriptInterpreterPython::MockScriptInterpreterPython(Debugger &debugger)
+    : ScriptInterpreter(debugger, lldb::ScriptLanguage::eScriptLanguagePython) {
+}
+
+void MockScriptInterpreterPython::Initialize() {
+  PluginManager::RegisterPlugin(GetPluginNameStatic(),
+                                GetPluginDescriptionStatic(),
+                                lldb::eScriptLanguagePython, CreateInstance);
+}
+
+void MockScriptInterpreterPython::Terminate() {
+  PluginManager::UnregisterPlugin(CreateInstance);
+}
+
+LLDB_PLUGIN_DEFINE(MockScriptInterpreterPython)
+
+std::string lldb_private::CreateFile(llvm::StringRef filename,
+                                     llvm::SmallString<128> parent_dir) {
+  llvm::SmallString<128> path(parent_dir);
+  llvm::sys::path::append(path, filename);
+  int fd;
+  std::error_code ret = llvm::sys::fs::openFileForWrite(path, fd);
+  assert(!ret && "Failed to create test file.");
+  ::close(fd);
+
+  return path.c_str();
+}
diff --git a/lldb/unittests/Platform/TestUtils.h b/lldb/unittests/Platform/TestUtils.h
new file mode 100644
index 0000000000000..1cb7b0b20d621
--- /dev/null
+++ b/lldb/unittests/Platform/TestUtils.h
@@ -0,0 +1,59 @@
+//===-- TestUtils.h -------------------------------------------------------===//
+//
+// 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_UNITTESTS_PLATFORM_TESTUTILS
+#define LLDB_UNITTESTS_PLATFORM_TESTUTILS
+
+#include "lldb/Interpreter/ScriptInterpreter.h"
+
+namespace lldb_private {
+class Debugger;
+
+class MockScriptInterpreterPython : public ScriptInterpreter {
+public:
+  MockScriptInterpreterPython(Debugger &debugger);
+  ~MockScriptInterpreterPython() override = default;
+
+  bool ExecuteOneLine(llvm::StringRef command, CommandReturnObject *,
+                      const ExecuteScriptOptions &) override {
+    return false;
+  }
+
+  void ExecuteInterpreterLoop() override {}
+
+  static void Initialize();
+
+  static void Terminate();
+
+  bool IsReservedWord(const char *word) override {
+    return llvm::is_contained({"import", "mykeyword_1_1_1"},
+                              llvm::StringRef(word));
+  }
+
+  static lldb::ScriptInterpreterSP CreateInstance(Debugger &debugger) {
+    return std::make_shared<MockScriptInterpreterPython>(debugger);
+  }
+
+  static llvm::StringRef GetPluginNameStatic() {
+    return "MockScriptInterpreterPython";
+  }
+
+  static llvm::StringRef GetPluginDescriptionStatic() {
+    return "MockScriptInterpreterPython";
+  }
+
+  // PluginInterface protocol
+  llvm::StringRef GetPluginName() override { return GetPluginNameStatic(); }
+};
+
+std::string CreateFile(llvm::StringRef filename,
+                       llvm::SmallString<128> parent_dir);
+
+} // namespace lldb_private
+
+#endif // LLDB_UNITTESTS_PLATFORM_TESTUTILS_H_IN



More information about the llvm-commits mailing list