[llvm] [dsymutil] Add --embed-resource to copy files into dSYM bundles. (PR #190663)

Ryan Mansfield via llvm-commits llvm-commits at lists.llvm.org
Mon Apr 6 12:45:36 PDT 2026


https://github.com/rjmansfield created https://github.com/llvm/llvm-project/pull/190663

Add a new --embed-resource flag that copies files or directories into the dSYM bundle's Contents/Resources/ directory during generation. 

Projects often need to embed files such as LLDB Python scripts into dSYM bundles, and this is usually done with a post dsym generation script, which may race stripping and code signing steps.

rdar://50633614

>From 988e8364e4c18cc77bf795ec14f055ad3f82c7ca Mon Sep 17 00:00:00 2001
From: Ryan Mansfield <ryan_mansfield at apple.com>
Date: Mon, 6 Apr 2026 15:33:56 -0400
Subject: [PATCH] [dsymutil] Add --embed-resource to copy files into dSYM
 bundles.

Add a new --embed-resource flag that copies files or directories into
the dSYM bundle's Contents/Resources/ directory during generation.

rdar://50633614
---
 llvm/docs/CommandGuide/dsymutil.rst          |  7 +++
 llvm/test/tools/dsymutil/cmdline.test        |  4 ++
 llvm/test/tools/dsymutil/embed-resource.test | 65 ++++++++++++++++++++
 llvm/tools/dsymutil/DwarfLinkerForBinary.cpp | 60 ++++++++++++++++++
 llvm/tools/dsymutil/DwarfLinkerForBinary.h   |  2 +
 llvm/tools/dsymutil/LinkUtils.h              |  3 +
 llvm/tools/dsymutil/Options.td               |  9 +++
 llvm/tools/dsymutil/dsymutil.cpp             | 27 ++++++++
 8 files changed, 177 insertions(+)
 create mode 100644 llvm/test/tools/dsymutil/embed-resource.test

diff --git a/llvm/docs/CommandGuide/dsymutil.rst b/llvm/docs/CommandGuide/dsymutil.rst
index c40cd18f32d19..9aa805cc6aa43 100644
--- a/llvm/docs/CommandGuide/dsymutil.rst
+++ b/llvm/docs/CommandGuide/dsymutil.rst
@@ -64,6 +64,13 @@ OPTIONS
  for dSYM files with  debug information about symbols present in those
  libraries.
 
+.. option:: --embed-resource <src-path>=<bundle-relative-path>
+
+ Copy a file or directory into the dSYM bundle's ``Contents/Resources/``
+ directory. The argument is ``<source-path>=<destination-relative-to-Resources>``.
+ If the source is a directory, its contents are copied recursively. This option
+ can be specified multiple times.
+
 .. option:: --fat64
 
  Use a 64-bit header when emitting universal binaries.
diff --git a/llvm/test/tools/dsymutil/cmdline.test b/llvm/test/tools/dsymutil/cmdline.test
index 7b815f6ef232c..189ad671f01de 100644
--- a/llvm/test/tools/dsymutil/cmdline.test
+++ b/llvm/test/tools/dsymutil/cmdline.test
@@ -12,6 +12,7 @@ CHECK: -build-variant-suffix <suffix=buildvariant>
 CHECK: -disallow <path>
 CHECK: -dump-debug-map
 CHECK: -D <path>
+CHECK: -embed-resource <src-path>=<bundle-relative-path>
 CHECK: -fat64
 CHECK: -flat
 CHECK: -gen-reproducer
@@ -57,3 +58,6 @@ CONFLICT: error: --quiet and --verbose cannot be specified together
 
 RUN: not dsymutil -y file1 --allow file2 2>&1 | FileCheck --check-prefix=CONFLICT2 %s
 CONFLICT2: error: -y and --allow/--disallow cannot be specified together
+
+RUN: not dsymutil --flat --embed-resource /tmp/a.py=Python/a.py file1 2>&1 | FileCheck --check-prefix=FLATEMBED %s
+FLATEMBED: error: --embed-resource is not supported with --flat
diff --git a/llvm/test/tools/dsymutil/embed-resource.test b/llvm/test/tools/dsymutil/embed-resource.test
new file mode 100644
index 0000000000000..27b5ddfc21c92
--- /dev/null
+++ b/llvm/test/tools/dsymutil/embed-resource.test
@@ -0,0 +1,65 @@
+Test the --embed-resource option.
+
+Create a temp resource file and embed it into the dSYM bundle.
+
+RUN: rm -rf %t.dir && mkdir -p %t.dir
+RUN: echo "# test resource" > %t.dir/test.py
+RUN: dsymutil -oso-prepend-path=%p %p/Inputs/basic.macho.x86_64 \
+RUN:   --embed-resource %t.dir/test.py=Python/test.py \
+RUN:   -o %t.dir/basic.macho.x86_64.dSYM
+RUN: diff %t.dir/test.py %t.dir/basic.macho.x86_64.dSYM/Contents/Resources/Python/test.py
+
+Verify multiple --embed-resource options.
+
+RUN: rm -rf %t.dir/basic.macho.x86_64.dSYM
+RUN: echo "second" > %t.dir/second.txt
+RUN: dsymutil -oso-prepend-path=%p %p/Inputs/basic.macho.x86_64 \
+RUN:   --embed-resource %t.dir/test.py=Python/test.py \
+RUN:   --embed-resource %t.dir/second.txt=Scripts/second.txt \
+RUN:   -o %t.dir/basic.macho.x86_64.dSYM
+RUN: diff %t.dir/test.py %t.dir/basic.macho.x86_64.dSYM/Contents/Resources/Python/test.py
+RUN: diff %t.dir/second.txt %t.dir/basic.macho.x86_64.dSYM/Contents/Resources/Scripts/second.txt
+
+Verify verbose output.
+
+RUN: rm -rf %t.dir/basic.macho.x86_64.dSYM
+RUN: dsymutil -oso-prepend-path=%p %p/Inputs/basic.macho.x86_64 \
+RUN:   --embed-resource %t.dir/test.py=Python/test.py \
+RUN:   --verbose -o %t.dir/basic.macho.x86_64.dSYM 2>&1 \
+RUN:   | FileCheck --check-prefix=VERBOSE %s
+
+VERBOSE: embed resource {{.*}}test.py -> {{.*}}Resources{{.}}Python{{.}}test.py
+
+Verify error on missing source file.
+
+RUN: not dsymutil -oso-prepend-path=%p %p/Inputs/basic.macho.x86_64 \
+RUN:   --embed-resource %t.dir/nonexistent.py=Python/foo.py \
+RUN:   -o %t.dir/basic.macho.x86_64.dSYM 2>&1 \
+RUN:   | FileCheck --check-prefix=MISSING %s
+
+MISSING: error: cannot embed resource {{.*}}nonexistent.py
+
+Verify directory embedding copies all files recursively.
+
+RUN: rm -rf %t.dir/basic.macho.x86_64.dSYM
+RUN: mkdir -p %t.dir/pydir/sub
+RUN: echo "file_a" > %t.dir/pydir/a.py
+RUN: echo "file_b" > %t.dir/pydir/b.py
+RUN: echo "file_sub" > %t.dir/pydir/sub/c.py
+RUN: dsymutil -oso-prepend-path=%p %p/Inputs/basic.macho.x86_64 \
+RUN:   --embed-resource %t.dir/pydir=Python \
+RUN:   -o %t.dir/basic.macho.x86_64.dSYM
+RUN: diff %t.dir/pydir/a.py %t.dir/basic.macho.x86_64.dSYM/Contents/Resources/Python/a.py
+RUN: diff %t.dir/pydir/b.py %t.dir/basic.macho.x86_64.dSYM/Contents/Resources/Python/b.py
+RUN: diff %t.dir/pydir/sub/c.py %t.dir/basic.macho.x86_64.dSYM/Contents/Resources/Python/sub/c.py
+
+Verify error cases.
+
+RUN: not dsymutil --embed-resource noequals file1 2>&1 | FileCheck --check-prefix=BADEMBED %s
+BADEMBED: error: invalid --embed-resource argument 'noequals': expected <src-path>=<dst-path>
+
+RUN: not dsymutil --embed-resource /tmp/a.py=../../etc/passwd file1 2>&1 | FileCheck --check-prefix=ESCAPE %s
+ESCAPE: error: invalid --embed-resource destination '../../etc/passwd': must be a relative path within the bundle
+
+RUN: not dsymutil --embed-resource /tmp/a.py=/etc/passwd file1 2>&1 | FileCheck --check-prefix=ABSOLUTE %s
+ABSOLUTE: error: invalid --embed-resource destination '/etc/passwd': must be a relative path within the bundle
diff --git a/llvm/tools/dsymutil/DwarfLinkerForBinary.cpp b/llvm/tools/dsymutil/DwarfLinkerForBinary.cpp
index 7114e1924daa7..d4f7c36dd6046 100644
--- a/llvm/tools/dsymutil/DwarfLinkerForBinary.cpp
+++ b/llvm/tools/dsymutil/DwarfLinkerForBinary.cpp
@@ -533,6 +533,63 @@ Error DwarfLinkerForBinary::copySwiftInterfaces(StringRef Architecture) const {
   return Error::success();
 }
 
+Error DwarfLinkerForBinary::copyEmbeddedResources() const {
+  if (!Options.ResourceDir || Options.EmbedResources.empty())
+    return Error::success();
+
+  auto copyOneFile = [&](StringRef SrcPath,
+                         StringRef DstPath) -> std::error_code {
+    if (auto EC = sys::fs::create_directories(sys::path::parent_path(DstPath),
+                                              true, sys::fs::perms::all_all))
+      return EC;
+
+    if (Options.Verbose)
+      outs() << "embed resource " << SrcPath << " -> " << DstPath << '\n';
+
+    return sys::fs::copy_file(SrcPath, DstPath);
+  };
+
+  for (const auto &[Src, Dst] : Options.EmbedResources) {
+    bool IsDir = false;
+    if (auto EC = sys::fs::is_directory(Src, IsDir))
+      return make_error<StringError>("cannot embed resource " + Src + ": " +
+                                         toString(errorCodeToError(EC)),
+                                     EC);
+
+    if (IsDir) {
+      std::error_code EC;
+      for (sys::fs::recursive_directory_iterator I(Src, EC), E; I != E && !EC;
+           I.increment(EC)) {
+        if (I->type() == sys::fs::file_type::directory_file)
+          continue;
+        StringRef FilePath = I->path();
+        StringRef Relative = FilePath.substr(StringRef(Src).size());
+        if (!Relative.empty() && sys::path::is_separator(Relative.front()))
+          Relative = Relative.drop_front();
+        SmallString<128> DestPath;
+        sys::path::append(DestPath, *Options.ResourceDir, Dst, Relative);
+        if (auto CopyEC = copyOneFile(FilePath, DestPath))
+          return make_error<StringError>("cannot embed resource " + FilePath +
+                                             ": " +
+                                             toString(errorCodeToError(CopyEC)),
+                                         CopyEC);
+      }
+      if (EC)
+        return make_error<StringError>("cannot read directory " + Src + ": " +
+                                           toString(errorCodeToError(EC)),
+                                       EC);
+    } else {
+      SmallString<128> DestPath;
+      sys::path::append(DestPath, *Options.ResourceDir, Dst);
+      if (auto EC = copyOneFile(Src, DestPath))
+        return make_error<StringError>("cannot embed resource " + Src + ": " +
+                                           toString(errorCodeToError(EC)),
+                                       EC);
+    }
+  }
+  return Error::success();
+}
+
 void DwarfLinkerForBinary::copySwiftReflectionMetadata(
     const llvm::dsymutil::DebugMapObject *Obj, classic::DwarfStreamer *Streamer,
     std::vector<uint64_t> &SectionToOffsetInDwarf,
@@ -877,6 +934,9 @@ bool DwarfLinkerForBinary::linkImpl(
       return error(toString(std::move(E)));
   }
 
+  if (auto E = copyEmbeddedResources())
+    return error(toString(std::move(E)));
+
   auto MapTriple = Map.getTriple();
   if ((MapTriple.isOSDarwin() || MapTriple.isOSBinFormatMachO()) &&
       !Map.getBinaryPath().empty() &&
diff --git a/llvm/tools/dsymutil/DwarfLinkerForBinary.h b/llvm/tools/dsymutil/DwarfLinkerForBinary.h
index d71ac0cd3eefd..507a523ba4530 100644
--- a/llvm/tools/dsymutil/DwarfLinkerForBinary.h
+++ b/llvm/tools/dsymutil/DwarfLinkerForBinary.h
@@ -275,6 +275,8 @@ class DwarfLinkerForBinary {
 
   Error copySwiftInterfaces(StringRef Architecture) const;
 
+  Error copyEmbeddedResources() const;
+
   void copySwiftReflectionMetadata(
       const llvm::dsymutil::DebugMapObject *Obj,
       classic::DwarfStreamer *Streamer,
diff --git a/llvm/tools/dsymutil/LinkUtils.h b/llvm/tools/dsymutil/LinkUtils.h
index 27f671152a114..9db5efd02852e 100644
--- a/llvm/tools/dsymutil/LinkUtils.h
+++ b/llvm/tools/dsymutil/LinkUtils.h
@@ -88,6 +88,9 @@ struct LinkOptions {
   /// The Resources directory in the .dSYM bundle.
   std::optional<std::string> ResourceDir;
 
+  /// Resources to embed in the dSYM bundle's Contents/Resources/ directory.
+  std::vector<std::pair<std::string, std::string>> EmbedResources;
+
   /// Virtual File System.
   llvm::IntrusiveRefCntPtr<llvm::vfs::FileSystem> VFS =
       vfs::getRealFileSystem();
diff --git a/llvm/tools/dsymutil/Options.td b/llvm/tools/dsymutil/Options.td
index 9b262660fe99a..520025ab957c5 100644
--- a/llvm/tools/dsymutil/Options.td
+++ b/llvm/tools/dsymutil/Options.td
@@ -229,6 +229,15 @@ def build_variant_suffix: Separate<["--", "-"], "build-variant-suffix">,
   Group<grp_general>;
 def: Joined<["--", "-"], "build-variant-suffix=">, Alias<build_variant_suffix>;
 
+def embed_resource: Separate<["--", "-"], "embed-resource">,
+  MetaVarName<"<src-path>=<bundle-relative-path>">,
+  HelpText<"Copy a file or directory into the dSYM bundle's Resources directory. "
+           "The argument is <source-path>=<destination-relative-to-Resources>. "
+           "If the source is a directory, its contents are copied recursively. "
+           "This option can be specified multiple times.">,
+  Group<grp_general>;
+def: Joined<["--", "-"], "embed-resource=">, Alias<embed_resource>;
+
 def allow: Separate<["--", "-"], "allow">,
   MetaVarName<"<path>">,
   HelpText<"Only process debug map objects listed in the YAML file at <path>. "
diff --git a/llvm/tools/dsymutil/dsymutil.cpp b/llvm/tools/dsymutil/dsymutil.cpp
index 108b840ce1c68..e986304830213 100644
--- a/llvm/tools/dsymutil/dsymutil.cpp
+++ b/llvm/tools/dsymutil/dsymutil.cpp
@@ -216,6 +216,11 @@ static Error verifyOptions(const DsymutilOptions &Options) {
         "--allow and --disallow cannot be specified together",
         errc::invalid_argument);
 
+  if (Options.Flat && !Options.LinkOpts.EmbedResources.empty())
+    return make_error<StringError>(
+        "--embed-resource is not supported with --flat",
+        errc::invalid_argument);
+
   return Error::success();
 }
 
@@ -415,6 +420,28 @@ static Expected<DsymutilOptions> getOptions(opt::InputArgList &Args) {
   if (opt::Arg *BuildVariantSuffix = Args.getLastArg(OPT_build_variant_suffix))
     Options.LinkOpts.BuildVariantSuffix = BuildVariantSuffix->getValue();
 
+  for (auto *Arg : Args.filtered(OPT_embed_resource)) {
+    StringRef Val = Arg->getValue();
+    auto [Src, Dst] = Val.split('=');
+    if (Src.empty() || Dst.empty())
+      return make_error<StringError>("invalid --embed-resource argument '" +
+                                         Val +
+                                         "': expected <src-path>=<dst-path>",
+                                     inconvertibleErrorCode());
+
+    // Reject destinations that would escape the Resources directory.
+    SmallString<128> NormalizedDst(Dst);
+    sys::path::remove_dots(NormalizedDst, /*remove_dot_dot=*/true);
+    if (sys::path::is_absolute(NormalizedDst) ||
+        NormalizedDst.starts_with(".."))
+      return make_error<StringError>(
+          "invalid --embed-resource destination '" + Dst +
+              "': must be a relative path within the bundle",
+          inconvertibleErrorCode());
+    Options.LinkOpts.EmbedResources.emplace_back(Src.str(),
+                                                 std::string(NormalizedDst));
+  }
+
   for (auto *SearchPath : Args.filtered(OPT_dsym_search_path))
     Options.LinkOpts.DSYMSearchPaths.push_back(SearchPath->getValue());
 



More information about the llvm-commits mailing list