https://github.com/aviralg updated 
https://github.com/llvm/llvm-project/pull/212847

>From 48538cf4cca64090d5435dc43361c14effc0e9dc Mon Sep 17 00:00:00 2001
From: Aviral Goel <[email protected]>
Date: Fri, 24 Jul 2026 12:15:16 -0700
Subject: [PATCH] [clang][ssaf] Add multi-arch create subcommand

This change adds `clang-ssaf-linker multi-arch create` for bundling
per-architecture artifacts into a unified multi-architecture bundle.
---
 .../Core/EntityLinker/LUSummaryEncoding.h     |   3 +
 .../EntityLinker/MultiArchSharedLibrary.h     |   3 +
 .../EntityLinker/MultiArchStaticLibrary.h     |   3 +
 .../Core/EntityLinker/StaticLibrary.h         |   2 +
 .../Core/Model/BuildNamespace.h               |   4 +
 .../clang/ScalableStaticAnalysis/Tool/Utils.h |  14 +
 .../Core/Model/BuildNamespace.cpp             |   4 +
 .../ssaf-linker/Inputs/bad-artifact.json      |   3 +
 .../ssaf-linker/Inputs/lib-aarch64.json       |   9 +
 .../ssaf-linker/Inputs/lib-arm64.json         |   9 +
 .../ssaf-linker/Inputs/lib-ios-versioned.json |   9 +
 .../Scalable/ssaf-linker/Inputs/lib-ios.json  |   9 +
 .../ssaf-linker/Inputs/lib-otherns.json       |   9 +
 .../ssaf-linker/Inputs/lib-x86_64.json        |   9 +
 .../ssaf-linker/Inputs/libfoo-2arch.json      |  37 ++
 .../Inputs/libmulti-empty-wrapper.json        |   8 +
 .../Inputs/libmulti-multiarch-wrongns.json    |   8 +
 .../Inputs/libshared-empty-wrapper.json       |  10 +
 .../Inputs/libshared-multiarch-wrongns.json   |  10 +
 .../Scalable/ssaf-linker/Inputs/lu-arm64.json |  13 +
 .../ssaf-linker/Inputs/lu-ios-libfoo.json     |  13 +
 .../ssaf-linker/Inputs/lu-macosx-libfoo.json  |  13 +
 .../ssaf-linker/Inputs/lu-otherns.json        |  13 +
 .../ssaf-linker/Inputs/lu-x86_64.json         |  13 +
 .../ssaf-linker/Outputs/libfoo-3arch.json     |  50 +++
 .../ssaf-linker/Outputs/libmulti-1arch.json   |  18 +
 .../ssaf-linker/Outputs/libmulti-2arch.json   |  27 ++
 .../ssaf-linker/Outputs/libmulti-3arch.json   |  36 ++
 .../ssaf-linker/Outputs/libshared-1arch.json  |  24 ++
 .../ssaf-linker/Outputs/libshared-2arch.json  |  37 ++
 .../Analysis/Scalable/ssaf-linker/help.test   |   1 +
 .../ssaf-linker/multi-arch-create.test        | 296 ++++++++++++++++
 .../Scalable/ssaf-linker/multi-arch-help.test |  22 ++
 .../Scalable/ssaf-linker/multi-arch-time.test |  36 ++
 .../ssaf-linker/multi-arch-verbose.test       |  55 +++
 clang/tools/clang-ssaf-linker/CMakeLists.txt  |   1 +
 .../clang-ssaf-linker/MultiArchCreateCLI.cpp  | 335 ++++++++++++++++++
 .../clang-ssaf-linker/MultiArchCreateCLI.h    | 111 ++++++
 clang/tools/clang-ssaf-linker/SSAFLinker.cpp  | 107 ++++--
 .../BuildNamespaceTest.cpp                    |  13 +
 40 files changed, 1362 insertions(+), 35 deletions(-)
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/bad-artifact.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-aarch64.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-arm64.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios-versioned.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-otherns.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-x86_64.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/libfoo-2arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-empty-wrapper.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-multiarch-wrongns.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-empty-wrapper.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-multiarch-wrongns.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-arm64.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-ios-libfoo.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-macosx-libfoo.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-otherns.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-x86_64.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Outputs/libfoo-3arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-1arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-2arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-3arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-1arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-2arch.json
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/multi-arch-create.test
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/multi-arch-help.test
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/multi-arch-time.test
 create mode 100644 
clang/test/Analysis/Scalable/ssaf-linker/multi-arch-verbose.test
 create mode 100644 clang/tools/clang-ssaf-linker/MultiArchCreateCLI.cpp
 create mode 100644 clang/tools/clang-ssaf-linker/MultiArchCreateCLI.h

diff --git 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h
 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h
index 308196d38a84a..a801f5ba46f2e 100644
--- 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h
+++ 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h
@@ -26,6 +26,8 @@
 
 namespace clang::ssaf {
 
+class MultiArchCreateCLI;
+
 /// Represents a link unit summary in its serialized encoding.
 ///
 /// LUSummaryEncoding holds the combined entity summary data from multiple
@@ -33,6 +35,7 @@ namespace clang::ssaf {
 /// entity linker and contains deduplicated and patched entity summaries.
 class LUSummaryEncoding {
   friend class EntityLinker;
+  friend class MultiArchCreateCLI;
   friend class MultiArchSharedLibrary;
   friend class SerializationFormat;
   friend class TestFixture;
diff --git 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.h
 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.h
index 8c3f4345ee5f8..e4d33297ab8b0 100644
--- 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.h
+++ 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.h
@@ -24,6 +24,8 @@
 
 namespace clang::ssaf {
 
+class MultiArchCreateCLI;
+
 /// Represents a multi-architecture shared library.
 ///
 /// A MultiArchSharedLibrary bundles per-architecture LUSummaryEncoding
@@ -34,6 +36,7 @@ namespace clang::ssaf {
 /// namespace identifying that shared library, and every member's
 /// \c LUNamespace must equal it exactly.
 class MultiArchSharedLibrary {
+  friend class MultiArchCreateCLI;
   friend class SerializationFormat;
   friend class TestFixture;
 
diff --git 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
index 6ff7ed347a791..f5ddafbb9d577 100644
--- 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
+++ 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
@@ -23,6 +23,8 @@
 
 namespace clang::ssaf {
 
+class MultiArchCreateCLI;
+
 /// Represents a multi-architecture static library.
 ///
 /// A MultiArchStaticLibrary bundles per-architecture StaticLibrary members. 
All
@@ -30,6 +32,7 @@ namespace clang::ssaf {
 /// architectures; the wrapper's \c Namespace identifies that shared library 
and
 /// every member's namespace must agree on its name.
 class MultiArchStaticLibrary {
+  friend class MultiArchCreateCLI;
   friend class SerializationFormat;
   friend class TestFixture;
 
diff --git 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/StaticLibrary.h 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/StaticLibrary.h
index 7358a73c32da1..c74fc4b16e0b2 100644
--- 
a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/StaticLibrary.h
+++ 
b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/StaticLibrary.h
@@ -23,6 +23,7 @@
 
 namespace clang::ssaf {
 
+class MultiArchCreateCLI;
 class StaticLibraryCreateCLI;
 
 /// Represents a static library of translation unit summary encodings.
@@ -41,6 +42,7 @@ class StaticLibraryCreateCLI;
 /// static-library tool never decodes per-entity payloads, and the linker
 /// consumes them as-is during its selective inclusion pass.
 class StaticLibrary {
+  friend class MultiArchCreateCLI;
   friend class MultiArchStaticLibrary;
   friend class SerializationFormat;
   friend class StaticLibraryCreateCLI;
diff --git 
a/clang/include/clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h 
b/clang/include/clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h
index d503b16bf294c..4e4368a06ed89 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h
@@ -60,6 +60,10 @@ class BuildNamespace {
   /// \returns A BuildNamespace with CompilationUnit kind.
   static BuildNamespace makeCompilationUnit(llvm::StringRef CompilationId);
 
+  /// Returns a copy of this namespace with its kind replaced by \p Kind,
+  /// preserving the name.
+  BuildNamespace withKind(BuildNamespaceKind Kind) const;
+
   bool operator==(const BuildNamespace &Other) const;
   bool operator!=(const BuildNamespace &Other) const;
   bool operator<(const BuildNamespace &Other) const;
diff --git a/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h 
b/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h
index f02bb8152cfd1..8ceb539a055de 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h
@@ -22,6 +22,7 @@
 #include "llvm/Support/CommandLine.h"
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FormatVariadic.h"
+#include "llvm/Support/WithColor.h"
 #include <string>
 
 namespace clang::ssaf {
@@ -47,6 +48,19 @@ template <typename... Ts>
 
 [[noreturn]] void fail(llvm::Error Err);
 
+/// Number of spaces per indentation level used by info().
+constexpr unsigned IndentationWidth = 2;
+
+/// Prints an indented note to stderr when Verbose is set.
+template <typename... Ts>
+inline void info(bool Verbose, unsigned Level, const char *Fmt, Ts &&...Args) {
+  if (Verbose) {
+    llvm::WithColor::note()
+        << std::string(Level * IndentationWidth, ' ') << "- "
+        << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
+  }
+}
+
 
//===----------------------------------------------------------------------===//
 // Plugin Loading
 
//===----------------------------------------------------------------------===//
diff --git a/clang/lib/ScalableStaticAnalysis/Core/Model/BuildNamespace.cpp 
b/clang/lib/ScalableStaticAnalysis/Core/Model/BuildNamespace.cpp
index f68ed961f71b9..345f375788014 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/Model/BuildNamespace.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/Model/BuildNamespace.cpp
@@ -20,6 +20,10 @@ BuildNamespace::makeCompilationUnit(llvm::StringRef 
CompilationId) {
                         CompilationId.str()};
 }
 
+BuildNamespace BuildNamespace::withKind(BuildNamespaceKind Kind) const {
+  return BuildNamespace{Kind, Name};
+}
+
 bool BuildNamespace::operator==(const BuildNamespace &Other) const {
   return asTuple() == Other.asTuple();
 }
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/bad-artifact.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/bad-artifact.json
new file mode 100644
index 0000000000000..fb5e09c2c59a7
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/bad-artifact.json
@@ -0,0 +1,3 @@
+{
+  "type": "BogusKind"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-aarch64.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-aarch64.json
new file mode 100644
index 0000000000000..c0e7cf25306c8
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-aarch64.json
@@ -0,0 +1,9 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "libmulti"
+  },
+  "target_triple": "aarch64-apple-macosx",
+  "type": "StaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-arm64.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-arm64.json
new file mode 100644
index 0000000000000..a14f0724adb8a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-arm64.json
@@ -0,0 +1,9 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "libmulti"
+  },
+  "target_triple": "arm64-apple-macosx",
+  "type": "StaticLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios-versioned.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios-versioned.json
new file mode 100644
index 0000000000000..836fa93a1fa7a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios-versioned.json
@@ -0,0 +1,9 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "libmulti"
+  },
+  "target_triple": "arm64-apple-ios17.0",
+  "type": "StaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios.json
new file mode 100644
index 0000000000000..0ae42468f899a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-ios.json
@@ -0,0 +1,9 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "libmulti"
+  },
+  "target_triple": "arm64-apple-ios",
+  "type": "StaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-otherns.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-otherns.json
new file mode 100644
index 0000000000000..9f928ad5ebbd0
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-otherns.json
@@ -0,0 +1,9 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "otherlib"
+  },
+  "target_triple": "x86_64-apple-macosx",
+  "type": "StaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-x86_64.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-x86_64.json
new file mode 100644
index 0000000000000..ac8bb1830f5f1
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lib-x86_64.json
@@ -0,0 +1,9 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "libmulti"
+  },
+  "target_triple": "x86_64-apple-macosx",
+  "type": "StaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libfoo-2arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libfoo-2arch.json
new file mode 100644
index 0000000000000..d1de17900eb84
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libfoo-2arch.json
@@ -0,0 +1,37 @@
+{
+  "members": [
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libfoo"
+        }
+      ],
+      "target_triple": "arm64-apple-macosx",
+      "type": "LUSummary"
+    },
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libfoo"
+        }
+      ],
+      "target_triple": "x86_64-apple-macosx",
+      "type": "LUSummary"
+    }
+  ],
+  "namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libfoo"
+    }
+  ],
+  "type": "MultiArchSharedLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-empty-wrapper.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-empty-wrapper.json
new file mode 100644
index 0000000000000..f153b0a312cd0
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-empty-wrapper.json
@@ -0,0 +1,8 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "MultiArchStaticLibrary",
+    "name": "libmulti"
+  },
+  "type": "MultiArchStaticLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-multiarch-wrongns.json
 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-multiarch-wrongns.json
new file mode 100644
index 0000000000000..b75b9872aaf93
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libmulti-multiarch-wrongns.json
@@ -0,0 +1,8 @@
+{
+  "members": [],
+  "namespace": {
+    "kind": "MultiArchStaticLibrary",
+    "name": "otherlib"
+  },
+  "type": "MultiArchStaticLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-empty-wrapper.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-empty-wrapper.json
new file mode 100644
index 0000000000000..9acdc0470c71b
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-empty-wrapper.json
@@ -0,0 +1,10 @@
+{
+  "members": [],
+  "namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libshared"
+    }
+  ],
+  "type": "MultiArchSharedLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-multiarch-wrongns.json
 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-multiarch-wrongns.json
new file mode 100644
index 0000000000000..53ca1cbddee0a
--- /dev/null
+++ 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libshared-multiarch-wrongns.json
@@ -0,0 +1,10 @@
+{
+  "members": [],
+  "namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "othershared"
+    }
+  ],
+  "type": "MultiArchSharedLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-arm64.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-arm64.json
new file mode 100644
index 0000000000000..06d5d82913237
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-arm64.json
@@ -0,0 +1,13 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libshared"
+    }
+  ],
+  "target_triple": "arm64-apple-macosx",
+  "type": "LUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-ios-libfoo.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-ios-libfoo.json
new file mode 100644
index 0000000000000..275d54b0da457
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-ios-libfoo.json
@@ -0,0 +1,13 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libfoo"
+    }
+  ],
+  "target_triple": "arm64-apple-ios",
+  "type": "LUSummary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-macosx-libfoo.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-macosx-libfoo.json
new file mode 100644
index 0000000000000..26a2fdf99fc8b
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-macosx-libfoo.json
@@ -0,0 +1,13 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libfoo"
+    }
+  ],
+  "target_triple": "arm64-apple-macosx",
+  "type": "LUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-otherns.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-otherns.json
new file mode 100644
index 0000000000000..e5dc71f9be509
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-otherns.json
@@ -0,0 +1,13 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "othershared"
+    }
+  ],
+  "target_triple": "x86_64-apple-macosx",
+  "type": "LUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-x86_64.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-x86_64.json
new file mode 100644
index 0000000000000..71f0eff931829
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/lu-x86_64.json
@@ -0,0 +1,13 @@
+{
+  "data": [],
+  "id_table": [],
+  "linkage_table": [],
+  "lu_namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libshared"
+    }
+  ],
+  "target_triple": "x86_64-apple-macosx",
+  "type": "LUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libfoo-3arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libfoo-3arch.json
new file mode 100644
index 0000000000000..fbc389c33f086
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libfoo-3arch.json
@@ -0,0 +1,50 @@
+{
+  "members": [
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libfoo"
+        }
+      ],
+      "target_triple": "arm64-apple-ios",
+      "type": "LUSummary"
+    },
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libfoo"
+        }
+      ],
+      "target_triple": "arm64-apple-macosx",
+      "type": "LUSummary"
+    },
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libfoo"
+        }
+      ],
+      "target_triple": "x86_64-apple-macosx",
+      "type": "LUSummary"
+    }
+  ],
+  "namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libfoo"
+    }
+  ],
+  "type": "MultiArchSharedLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-1arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-1arch.json
new file mode 100644
index 0000000000000..08d8ab9f6045d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-1arch.json
@@ -0,0 +1,18 @@
+{
+  "members": [
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libmulti"
+      },
+      "target_triple": "arm64-apple-macosx",
+      "type": "StaticLibrary"
+    }
+  ],
+  "namespace": {
+    "kind": "MultiArchStaticLibrary",
+    "name": "libmulti"
+  },
+  "type": "MultiArchStaticLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-2arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-2arch.json
new file mode 100644
index 0000000000000..498fc1a650cb2
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-2arch.json
@@ -0,0 +1,27 @@
+{
+  "members": [
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libmulti"
+      },
+      "target_triple": "arm64-apple-macosx",
+      "type": "StaticLibrary"
+    },
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libmulti"
+      },
+      "target_triple": "x86_64-apple-macosx",
+      "type": "StaticLibrary"
+    }
+  ],
+  "namespace": {
+    "kind": "MultiArchStaticLibrary",
+    "name": "libmulti"
+  },
+  "type": "MultiArchStaticLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-3arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-3arch.json
new file mode 100644
index 0000000000000..2eac4efea795c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libmulti-3arch.json
@@ -0,0 +1,36 @@
+{
+  "members": [
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libmulti"
+      },
+      "target_triple": "arm64-apple-ios",
+      "type": "StaticLibrary"
+    },
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libmulti"
+      },
+      "target_triple": "arm64-apple-macosx",
+      "type": "StaticLibrary"
+    },
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libmulti"
+      },
+      "target_triple": "x86_64-apple-macosx",
+      "type": "StaticLibrary"
+    }
+  ],
+  "namespace": {
+    "kind": "MultiArchStaticLibrary",
+    "name": "libmulti"
+  },
+  "type": "MultiArchStaticLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-1arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-1arch.json
new file mode 100644
index 0000000000000..bd069da05fda0
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-1arch.json
@@ -0,0 +1,24 @@
+{
+  "members": [
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libshared"
+        }
+      ],
+      "target_triple": "arm64-apple-macosx",
+      "type": "LUSummary"
+    }
+  ],
+  "namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libshared"
+    }
+  ],
+  "type": "MultiArchSharedLibrary"
+}
diff --git 
a/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-2arch.json 
b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-2arch.json
new file mode 100644
index 0000000000000..9d1e293db8287
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Outputs/libshared-2arch.json
@@ -0,0 +1,37 @@
+{
+  "members": [
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libshared"
+        }
+      ],
+      "target_triple": "arm64-apple-macosx",
+      "type": "LUSummary"
+    },
+    {
+      "data": [],
+      "id_table": [],
+      "linkage_table": [],
+      "lu_namespace": [
+        {
+          "kind": "LinkUnit",
+          "name": "libshared"
+        }
+      ],
+      "target_triple": "x86_64-apple-macosx",
+      "type": "LUSummary"
+    }
+  ],
+  "namespace": [
+    {
+      "kind": "LinkUnit",
+      "name": "libshared"
+    }
+  ],
+  "type": "MultiArchSharedLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test 
b/clang/test/Analysis/Scalable/ssaf-linker/help.test
index 6f897f4ca9228..e4a696d86b165 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -9,6 +9,7 @@
 // CHECK-EMPTY:
 // CHECK-NEXT: SUBCOMMANDS:
 // CHECK-EMPTY:
+// CHECK-NEXT:   multi-arch     - Operations on multi-architecture 
StaticLibraries and SharedLibraries
 // CHECK-NEXT:   static-library - Operations on StaticLibraries
 // CHECK-EMPTY:
 // CHECK-NEXT:   Type "clang-ssaf-linker{{(\.exe)?}} <subcommand> --help" to 
get more help on a specific subcommand
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-create.test 
b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-create.test
new file mode 100644
index 0000000000000..a3cd120249d63
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-create.test
@@ -0,0 +1,296 @@
+// Tests for `clang-ssaf-linker multi-arch create`.
+//
+// These tests exercise the CLI shape (subcommand dispatch, verb positional,
+// flag surface) and the happy / error paths of MultiArchStaticLibrary and
+// MultiArchSharedLibrary construction, mirroring `lipo -create`'s handling
+// of single- and multi-arch inputs.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// ============================================================================
+// Happy path (static family): two bare StaticLibrary inputs; the bundle's
+// identity is inferred from the inputs' own namespace
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-x86_64.json -o %t/libmulti-2arch.json
+// RUN: diff %S/Outputs/libmulti-2arch.json %t/libmulti-2arch.json
+
+// ============================================================================
+// Happy path (static family): a single bare StaticLibrary input, producing a
+// one-slice MultiArchStaticLibrary
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json -o 
%t/libmulti-1arch.json
+// RUN: diff %S/Outputs/libmulti-1arch.json %t/libmulti-1arch.json
+
+// ============================================================================
+// Happy path (static family): flatten an existing multi-arch bundle + a bare
+// input of a new triple, re-bundling into a 3-member MultiArchStaticLibrary
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %t/libmulti-2arch.json 
%S/Inputs/lib-ios.json -o %t/libmulti-3arch.json
+// RUN: diff %S/Outputs/libmulti-3arch.json %t/libmulti-3arch.json
+
+// ============================================================================
+// Happy path (static family): three bare inputs in one invocation, producing
+// the same bundle as building it up two at a time
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-x86_64.json %S/Inputs/lib-ios.json -o %t/libmulti-3bare.json
+// RUN: diff %S/Outputs/libmulti-3arch.json %t/libmulti-3bare.json
+
+// ============================================================================
+// Happy path: a wrapper that contributes no slices of its own still fixes the
+// bundle's identity, so a later bare input joins the namespace the wrapper
+// named rather than one of its own
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create 
%S/Inputs/libmulti-empty-wrapper.json %S/Inputs/lib-arm64.json -o 
%t/libmulti-from-wrapper.json
+// RUN: diff %S/Outputs/libmulti-1arch.json %t/libmulti-from-wrapper.json
+
+// RUN: clang-ssaf-linker multi-arch create 
%S/Inputs/libshared-empty-wrapper.json %S/Inputs/lu-arm64.json -o 
%t/libshared-from-wrapper.json
+// RUN: diff %S/Outputs/libshared-1arch.json %t/libshared-from-wrapper.json
+
+// An identity established by an empty wrapper is enforced on later inputs just
+// as one established by a member is.
+// RUN: not clang-ssaf-linker multi-arch create 
%S/Inputs/libmulti-empty-wrapper.json %S/Inputs/lib-otherns.json -o 
%t/nsmismatch-from-wrapper.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NS-MISMATCH-FROM-WRAPPER
+// NS-MISMATCH-FROM-WRAPPER: namespace BuildNamespace(StaticLibrary, otherlib) 
from '{{.*}}lib-otherns.json' does not match expected namespace 
BuildNamespace(StaticLibrary, libmulti)
+
+// RUN: not clang-ssaf-linker multi-arch create 
%S/Inputs/libshared-empty-wrapper.json %S/Inputs/lu-otherns.json -o 
%t/nsmismatch-from-shared-wrapper.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NS-MISMATCH-FROM-SHARED-WRAPPER
+// NS-MISMATCH-FROM-SHARED-WRAPPER: namespace 
NestedBuildNamespace([BuildNamespace(LinkUnit, othershared)]) from 
'{{.*}}lu-otherns.json' does not match expected namespace 
NestedBuildNamespace([BuildNamespace(LinkUnit, libshared)])
+
+// ============================================================================
+// Happy path (shared family): two bare LUSummaryEncoding inputs
+// ============================================================================
+
+// Like the static family, a shared-family bundle's identity is inferred from
+// its members' own namespace.
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json 
%S/Inputs/lu-x86_64.json -o %t/libshared-2arch.json
+// RUN: diff %S/Outputs/libshared-2arch.json %t/libshared-2arch.json
+
+// ============================================================================
+// Happy path (shared family): a single bare LUSummaryEncoding input, producing
+// a one-slice MultiArchSharedLibrary
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json -o 
%t/libshared-1arch.json
+// RUN: diff %S/Outputs/libshared-1arch.json %t/libshared-1arch.json
+
+// ============================================================================
+// Happy path (shared family): flatten an existing multi-arch bundle + a bare
+// input of a new triple, re-bundling into a 3-member MultiArchSharedLibrary
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/libfoo-2arch.json 
%S/Inputs/lu-ios-libfoo.json -o %t/libfoo-3arch.json
+// RUN: diff %S/Outputs/libfoo-3arch.json %t/libfoo-3arch.json
+
+// ============================================================================
+// Mixed-family rejection
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lu-arm64.json -o %t/mix1.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MIXED-STATIC-THEN-SHARED
+// MIXED-STATIC-THEN-SHARED: input '{{.*}}lu-arm64.json' is a shared-library 
artifact, but a preceding input established this bundle as static-library
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json 
%S/Inputs/lib-arm64.json -o %t/mix2.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MIXED-SHARED-THEN-STATIC
+// MIXED-SHARED-THEN-STATIC: input '{{.*}}lib-arm64.json' is a static-library 
artifact, but a preceding input established this bundle as shared-library
+
+// ============================================================================
+// Raw TUSummaryEncoding rejection
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/tu-empty.json -o 
%t/tu-reject.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INVALID-KIND
+// INVALID-KIND: '{{.*}}tu-empty.json' is a raw TU summary, not a valid input 
to multi-arch create: run static-library create or an entity-linking step first
+
+// A TU summary is rejected wherever it appears, not just in first position
+// (the first input having already established the family).
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/tu-empty.json -o %t/tu-reject-static.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INVALID-KIND-STATIC
+// INVALID-KIND-STATIC: '{{.*}}tu-empty.json' is a raw TU summary, not a valid 
input to multi-arch create
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json 
%S/Inputs/tu-empty.json -o %t/tu-reject-shared.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INVALID-KIND-SHARED
+// INVALID-KIND-SHARED: '{{.*}}tu-empty.json' is a raw TU summary, not a valid 
input to multi-arch create
+
+// ============================================================================
+// Malformed artifact: a read/decode failure is reported with the reading
+// context, not swallowed or misclassified
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/bad-artifact.json -o 
%t/bad.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-ARTIFACT
+// BAD-ARTIFACT: Reading artifact '{{.*}}bad-artifact.json'
+// BAD-ARTIFACT: unknown value 'BogusKind' for field 'type'
+
+// ============================================================================
+// Duplicate architecture slice
+// ============================================================================
+
+// Across two bare inputs (the same file passed twice).
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-arm64.json -o %t/dup-bare.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-BARE
+// DUP-BARE: duplicate architecture slice 'arm64-apple-macosx' contributed by 
both '{{.*}}lib-arm64.json' and '{{.*}}lib-arm64.json'
+
+// Straddling a flattened multi-arch input and a separate bare input.
+// RUN: not clang-ssaf-linker multi-arch create %t/libmulti-2arch.json 
%S/Inputs/lib-arm64.json -o %t/dup-flattened.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-FLATTENED
+// DUP-FLATTENED: duplicate architecture slice 'arm64-apple-macosx' 
contributed by both '{{.*}}libmulti-2arch.json' and '{{.*}}lib-arm64.json'
+
+// Slices collide on the triple's canonical components, not on its spelling:
+// "arm64" and "aarch64" name the same architecture, so the second input is a
+// duplicate even though the two triple strings differ. The diagnostic names
+// the retained slice's spelling and both contributing files.
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-aarch64.json -o %t/dup-alias.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-ALIAS
+// DUP-ALIAS: duplicate architecture slice 'arm64-apple-macosx' contributed by 
both '{{.*}}lib-arm64.json' and '{{.*}}lib-aarch64.json'
+
+// Likewise, an OS version does not distinguish a slice: arm64-apple-ios and
+// arm64-apple-ios17.0 are the same slice.
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-ios.json 
%S/Inputs/lib-ios-versioned.json -o %t/dup-osversion.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-OSVERSION
+// DUP-OSVERSION: duplicate architecture slice 'arm64-apple-ios' contributed 
by both '{{.*}}lib-ios.json' and '{{.*}}lib-ios-versioned.json'
+
+// The same rejection applies to the shared family, bare and flattened.
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json 
%S/Inputs/lu-arm64.json -o %t/dup-shared-bare.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-SHARED-BARE
+// DUP-SHARED-BARE: duplicate architecture slice 'arm64-apple-macosx' 
contributed by both '{{.*}}lu-arm64.json' and '{{.*}}lu-arm64.json'
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/libfoo-2arch.json 
%S/Inputs/lu-macosx-libfoo.json -o %t/dup-shared-flattened.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=DUP-SHARED-FLATTENED
+// DUP-SHARED-FLATTENED: duplicate architecture slice 'arm64-apple-macosx' 
contributed by both '{{.*}}libfoo-2arch.json' and '{{.*}}lu-macosx-libfoo.json'
+
+// ============================================================================
+// Error ordering: inputs are folded into the bundle as they are read, so the
+// first input that cannot be accepted is the one reported — whichever kind of
+// problem it has. These two runs share the same three inputs in different
+// orders and must report different errors.
+// ============================================================================
+
+// The duplicate at input 2 is reported; input 3's namespace is never reached.
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-arm64.json %S/Inputs/lib-otherns.json -o %t/order-dup-first.json 
2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ORDER-DUP-FIRST 
--implicit-check-not="does not match expected namespace"
+// ORDER-DUP-FIRST: duplicate architecture slice 'arm64-apple-macosx' 
contributed by both '{{.*}}lib-arm64.json' and '{{.*}}lib-arm64.json'
+
+// The namespace mismatch at input 2 is reported; input 3's duplicate slice is
+// never reached.
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-otherns.json %S/Inputs/lib-arm64.json -o %t/order-ns-first.json 
2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ORDER-NS-FIRST 
--implicit-check-not="duplicate architecture slice"
+// ORDER-NS-FIRST: namespace BuildNamespace(StaticLibrary, otherlib) from 
'{{.*}}lib-otherns.json' does not match expected namespace 
BuildNamespace(StaticLibrary, libmulti)
+
+// ============================================================================
+// Namespace mismatch
+// ============================================================================
+
+// A bare StaticLibrary member's own namespace disagrees with the target
+// (inferred from the first input).
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/lib-otherns.json -o %t/nsmismatch-bare.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NS-MISMATCH-BARE
+// NS-MISMATCH-BARE: namespace BuildNamespace(StaticLibrary, otherlib) from 
'{{.*}}lib-otherns.json' does not match expected namespace 
BuildNamespace(StaticLibrary, libmulti)
+
+// A MultiArchStaticLibrary wrapper's own namespace disagrees with the target
+// (inferred from the first input).
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 
%S/Inputs/libmulti-multiarch-wrongns.json -o %t/nsmismatch-wrapper.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NS-MISMATCH-WRAPPER
+// NS-MISMATCH-WRAPPER: namespace BuildNamespace(MultiArchStaticLibrary, 
otherlib) from '{{.*}}libmulti-multiarch-wrongns.json' does not match expected 
namespace BuildNamespace(MultiArchStaticLibrary, libmulti)
+
+// A bare LUSummaryEncoding member's own namespace disagrees with the target
+// (inferred from the first input).
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json 
%S/Inputs/lu-otherns.json -o %t/nsmismatch-shared.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NS-MISMATCH-SHARED
+// NS-MISMATCH-SHARED: namespace 
NestedBuildNamespace([BuildNamespace(LinkUnit, othershared)]) from 
'{{.*}}lu-otherns.json' does not match expected namespace 
NestedBuildNamespace([BuildNamespace(LinkUnit, libshared)])
+
+// A MultiArchSharedLibrary wrapper's own namespace disagrees with the target
+// (inferred from the first input).
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lu-arm64.json 
%S/Inputs/libshared-multiarch-wrongns.json -o %t/nsmismatch-shared-wrapper.json 
2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NS-MISMATCH-SHARED-WRAPPER
+// NS-MISMATCH-SHARED-WRAPPER: namespace 
NestedBuildNamespace([BuildNamespace(LinkUnit, othershared)]) from 
'{{.*}}libshared-multiarch-wrongns.json' does not match expected namespace 
NestedBuildNamespace([BuildNamespace(LinkUnit, libshared)])
+
+// ============================================================================
+// Zero candidates after flattening (every input is an empty multi-arch bundle)
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create 
%S/Inputs/libmulti-empty-wrapper.json -o %t/zerocand.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-CANDIDATES
+// NO-CANDIDATES: no candidate members could be derived from the given inputs: 
at least one member is required
+
+// RUN: not clang-ssaf-linker multi-arch create 
%S/Inputs/libshared-empty-wrapper.json -o %t/zerocand-shared.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-CANDIDATES-SHARED
+// NO-CANDIDATES-SHARED: no candidate members could be derived from the given 
inputs: at least one member is required
+
+// ============================================================================
+// Empty input list
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create -o %t/no-inputs.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-INPUTS
+// NO-INPUTS: no input artifacts: at least one input is required
+
+// ============================================================================
+// Missing output flag
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-OUTPUT
+// NO-OUTPUT: for the -o option: must be specified at least once!
+
+// ============================================================================
+// Unusable input paths
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/tu-badext.txt -o 
%t/badext.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-EXTENSION
+// BAD-EXTENSION: clang-ssaf-linker: error: failed to validate path 
'{{.*}}tu-badext.txt': No format registered for extension 'txt'
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/does-not-exist.json 
-o %t/missing.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MISSING-INPUT
+// MISSING-INPUT: clang-ssaf-linker: error: failed to validate path 
'{{.*}}does-not-exist.json': Path does not exist
+
+// ============================================================================
+// Output already exists
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch create %S/Inputs/lib-arm64.json -o 
%t/libmulti-2arch.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=OUTPUT-EXISTS
+// OUTPUT-EXISTS: clang-ssaf-linker: error: failed to validate path 
'{{.*}}libmulti-2arch.json': File already exists
+
+// ============================================================================
+// Unknown verb
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch extract %S/Inputs/lib-arm64.json -o 
%t/unknown.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=UNKNOWN-VERB
+// UNKNOWN-VERB: unknown multi-arch verb 'extract': expected 'create'
+
+// ============================================================================
+// Missing verb
+// ============================================================================
+
+// RUN: not clang-ssaf-linker multi-arch -o %t/no-verb.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-VERB
+// NO-VERB: Not enough positional command line arguments specified!
+
+// ============================================================================
+// Order independence: members are keyed by target triple, so the bundle is
+// byte-identical however the inputs are ordered on the command line
+// ============================================================================
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lib-x86_64.json 
%S/Inputs/lib-arm64.json -o %t/libmulti-reversed.json
+// RUN: diff %S/Outputs/libmulti-2arch.json %t/libmulti-reversed.json
+
+// RUN: clang-ssaf-linker multi-arch create %S/Inputs/lu-x86_64.json 
%S/Inputs/lu-arm64.json -o %t/libshared-reversed.json
+// RUN: diff %S/Outputs/libshared-2arch.json %t/libshared-reversed.json
+
+// ============================================================================
+// Round-trip: produced bundles parse cleanly with clang-ssaf-format
+// ============================================================================
+
+// RUN: clang-ssaf-format --type multi-arch-static-library 
%t/libmulti-3arch.json -o %t/libmulti-3arch-roundtrip.json
+// RUN: diff %t/libmulti-3arch.json %t/libmulti-3arch-roundtrip.json
+
+// RUN: clang-ssaf-format --type multi-arch-shared-library 
%t/libfoo-3arch.json -o %t/libfoo-3arch-roundtrip.json
+// RUN: diff %t/libfoo-3arch.json %t/libfoo-3arch-roundtrip.json
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-help.test 
b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-help.test
new file mode 100644
index 0000000000000..85e6e9b87afa8
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-help.test
@@ -0,0 +1,22 @@
+// Test the --help output for the `multi-arch` subcommand.
+
+// RUN: clang-ssaf-linker multi-arch --help-list-hidden \
+// RUN:           | FileCheck %s --match-full-lines
+
+// CHECK:      OVERVIEW: SSAF Linker
+// CHECK-EMPTY:
+// CHECK-NEXT: SUBCOMMAND 'multi-arch': Operations on multi-architecture 
StaticLibraries and SharedLibraries
+// CHECK-EMPTY:
+// CHECK-NEXT: USAGE: clang-ssaf-linker{{(\.exe)?}} multi-arch [options] 
<verb> <static-library or shared-library files>
+// CHECK-EMPTY:
+// CHECK-NEXT: OPTIONS:
+// CHECK-NEXT:   -h                  - Alias for --help
+// CHECK-NEXT:   --help              - Display available options 
(--help-hidden for more)
+// CHECK-NEXT:   --help-hidden       - Display all available options
+// CHECK-NEXT:   --help-list         - Display list of available options 
(--help-list-hidden for more)
+// CHECK-NEXT:   --help-list-hidden  - Display list of all available options
+// CHECK-NEXT:   -o <path>           - Output file path
+// CHECK-NEXT:   --print-all-options - Print all option values after command 
line parsing
+// CHECK-NEXT:   --print-options     - Print non-default options after command 
line parsing
+// CHECK-NEXT:   --time              - Enable timing
+// CHECK-NEXT:   --verbose           - Enable verbose output
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-time.test 
b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-time.test
new file mode 100644
index 0000000000000..31be44026018c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-time.test
@@ -0,0 +1,36 @@
+// Test the --time flag on the `multi-arch create` subcommand.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// RUN: clang-ssaf-linker multi-arch create --time \
+// RUN:     %S/Inputs/lib-arm64.json %S/Inputs/lib-x86_64.json -o %t/libt.json 
2>&1 \
+// RUN:   | FileCheck %s --match-full-lines
+
+// CHECK:      
===-------------------------------------------------------------------------===
+// CHECK-NEXT: {{[ ]+}}SSAF Linker
+// CHECK-NEXT: 
===-------------------------------------------------------------------------===
+// CHECK-NEXT:   Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall 
clock)
+// CHECK:      {{.*}}---Wall Time---{{.*}}
+// CHECK-DAG:  {{.*}}Validate Input
+// CHECK-DAG:  {{.*}}Read Artifacts
+// CHECK-DAG:  {{.*}}Bundle Input
+// CHECK-DAG:  {{.*}}Write Multi-Arch Bundle
+// CHECK:      {{.*}}Total
+
+// The shared family runs the same phases, so it reports the same timers.
+
+// RUN: clang-ssaf-linker multi-arch create --time \
+// RUN:     %S/Inputs/lu-arm64.json %S/Inputs/lu-x86_64.json -o %t/libts.json 
2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=SHARED
+
+// SHARED:      
===-------------------------------------------------------------------------===
+// SHARED-NEXT: {{[ ]+}}SSAF Linker
+// SHARED-NEXT: 
===-------------------------------------------------------------------------===
+// SHARED-NEXT:   Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall 
clock)
+// SHARED:      {{.*}}---Wall Time---{{.*}}
+// SHARED-DAG:  {{.*}}Validate Input
+// SHARED-DAG:  {{.*}}Read Artifacts
+// SHARED-DAG:  {{.*}}Bundle Input
+// SHARED-DAG:  {{.*}}Write Multi-Arch Bundle
+// SHARED:      {{.*}}Total
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-verbose.test 
b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-verbose.test
new file mode 100644
index 0000000000000..0f62fe0a1d873
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/multi-arch-verbose.test
@@ -0,0 +1,55 @@
+// Test the --verbose flag on the `multi-arch create` subcommand.
+
+// RUN: rm -rf %t
+// RUN: mkdir -p %t
+
+// RUN: clang-ssaf-linker multi-arch create --verbose \
+// RUN:     %S/Inputs/lib-arm64.json %S/Inputs/lib-x86_64.json -o %t/libv.json 
2>&1 \
+// RUN:   | FileCheck %s --match-full-lines
+
+// CHECK:      note: - Bundling started.
+// CHECK-NEXT: note:   - Validating input.
+// CHECK-NEXT: note:     - Validated output path '{{.*}}libv.json'.
+// CHECK-NEXT: note:     - Validated 2 input artifact path(s).
+// CHECK-NEXT: note:   - Creating bundle.
+// CHECK-NEXT: note:     - Bundling members.
+// CHECK-NEXT: note:       - [1/2] Reading '{{.*}}lib-arm64.json'.
+// CHECK-NEXT: note:       - [1/2] Bundling '{{.*}}lib-arm64.json'.
+// CHECK-NEXT: note:       - [2/2] Reading '{{.*}}lib-x86_64.json'.
+// CHECK-NEXT: note:       - [2/2] Bundling '{{.*}}lib-x86_64.json'.
+// CHECK-NEXT: note:     - Bundled 2 member(s).
+// CHECK-NEXT: note:     - Target namespace: 
'BuildNamespace(MultiArchStaticLibrary, libmulti)'.
+// CHECK-NEXT: note:   - Writing bundle to '{{.*}}libv.json'.
+// CHECK-NEXT: note: - Bundling finished.
+
+// The shared family reports a NestedBuildNamespace target, and flattening a
+// multi-arch input contributes more slices than there are inputs.
+
+// RUN: clang-ssaf-linker multi-arch create --verbose \
+// RUN:     %S/Inputs/libfoo-2arch.json %S/Inputs/lu-ios-libfoo.json -o 
%t/libfoov.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=SHARED
+
+// SHARED:      note: - Bundling started.
+// SHARED-NEXT: note:   - Validating input.
+// SHARED-NEXT: note:     - Validated output path '{{.*}}libfoov.json'.
+// SHARED-NEXT: note:     - Validated 2 input artifact path(s).
+// SHARED-NEXT: note:   - Creating bundle.
+// SHARED-NEXT: note:     - Bundling members.
+// SHARED-NEXT: note:       - [1/2] Reading '{{.*}}libfoo-2arch.json'.
+// SHARED-NEXT: note:       - [1/2] Bundling '{{.*}}libfoo-2arch.json'.
+// SHARED-NEXT: note:       - [2/2] Reading '{{.*}}lu-ios-libfoo.json'.
+// SHARED-NEXT: note:       - [2/2] Bundling '{{.*}}lu-ios-libfoo.json'.
+// SHARED-NEXT: note:     - Bundled 3 member(s).
+// SHARED-NEXT: note:     - Target namespace: 
'NestedBuildNamespace([BuildNamespace(LinkUnit, libfoo)])'.
+// SHARED-NEXT: note:   - Writing bundle to '{{.*}}libfoov.json'.
+// SHARED-NEXT: note: - Bundling finished.
+
+// A run that fails before assembling reports neither a target namespace nor a
+// slice count.
+
+// RUN: not clang-ssaf-linker multi-arch create --verbose \
+// RUN:     %S/Inputs/libmulti-empty-wrapper.json -o %t/libnone.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-TARGET --implicit-check-not="Target 
namespace"
+
+// NO-TARGET:      note:       - [1/1] Bundling 
'{{.*}}libmulti-empty-wrapper.json'.
+// NO-TARGET-NEXT: error: no candidate members could be derived from the given 
inputs: at least one member is required
diff --git a/clang/tools/clang-ssaf-linker/CMakeLists.txt 
b/clang/tools/clang-ssaf-linker/CMakeLists.txt
index 448b8cbb8cf14..c51c1f25ff9cc 100644
--- a/clang/tools/clang-ssaf-linker/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-linker/CMakeLists.txt
@@ -5,6 +5,7 @@ set(LLVM_LINK_COMPONENTS
   )
 
 add_clang_tool(clang-ssaf-linker
+  MultiArchCreateCLI.cpp
   StaticLibraryCreateCLI.cpp
   SSAFLinker.cpp
   )
diff --git a/clang/tools/clang-ssaf-linker/MultiArchCreateCLI.cpp 
b/clang/tools/clang-ssaf-linker/MultiArchCreateCLI.cpp
new file mode 100644
index 0000000000000..cd0ba4faf064b
--- /dev/null
+++ b/clang/tools/clang-ssaf-linker/MultiArchCreateCLI.cpp
@@ -0,0 +1,335 @@
+//===- MultiArchCreateCLI.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
+//
+//===----------------------------------------------------------------------===//
+//
+//  Implements the `multi-arch create` CLI action. The run() function picks the
+//  family from the first input and hands off to either createStaticLibrary() 
or
+//  createSharedLibrary().
+//
+//===----------------------------------------------------------------------===//
+
+#include "MultiArchCreateCLI.h"
+
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/TUSummaryEncoding.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h"
+#include "clang/ScalableStaticAnalysis/Core/Support/ErrorBuilder.h"
+#include "clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Timer.h"
+#include "llvm/TargetParser/Triple.h"
+#include <cassert>
+#include <memory>
+#include <string>
+#include <utility>
+#include <variant>
+
+using namespace llvm;
+using namespace clang::ssaf;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Error Messages
+//===----------------------------------------------------------------------===//
+
+constexpr const char *ReadingArtifact = "Reading artifact '{0}'";
+
+constexpr const char *NoInputs =
+    "no input artifacts: at least one input is required";
+
+constexpr const char *InvalidInputKind =
+    "'{0}' is a raw TU summary, not a valid input to multi-arch create: run "
+    "static-library create or an entity-linking step first";
+
+constexpr const char *MixedFamily =
+    "input '{0}' is a {1} artifact, but a preceding input established this "
+    "bundle as {2}";
+
+constexpr const char *NamespaceMismatch =
+    "namespace {0} from '{1}' does not match expected namespace {2}";
+
+constexpr const char *NoCandidateMembers =
+    "no candidate members could be derived from the given inputs: at least "
+    "one member is required";
+
+constexpr const char *DuplicateTriple =
+    "duplicate architecture slice '{0}' contributed by both '{1}' and '{2}'";
+
+constexpr const char *StaticFamilyName = "static-library";
+constexpr const char *SharedFamilyName = "shared-library";
+
+//===----------------------------------------------------------------------===//
+// ArtifactEncoding Helpers
+//===----------------------------------------------------------------------===//
+
+bool isStaticFamily(const ArtifactEncoding &E) {
+  return std::holds_alternative<StaticLibrary>(E) ||
+         std::holds_alternative<MultiArchStaticLibrary>(E);
+}
+
+bool isSharedFamily(const ArtifactEncoding &E) {
+  return std::holds_alternative<LUSummaryEncoding>(E) ||
+         std::holds_alternative<MultiArchSharedLibrary>(E);
+}
+
+bool isTUSummaryEncoding(const ArtifactEncoding &E) {
+  return std::holds_alternative<TUSummaryEncoding>(E);
+}
+
+} // namespace
+
+namespace clang::ssaf {
+
+void MultiArchCreateCLI::run(llvm::TimerGroup &TG,
+                             llvm::ArrayRef<std::string> InputPaths,
+                             llvm::StringRef OutputPath, bool Verbose,
+                             bool Time) {
+  this->InputPaths = InputPaths;
+  this->OutputPath = OutputPath;
+  this->Verbose = Verbose;
+  this->Time = Time;
+
+  llvm::Timer TValidate("validate", "Validate Input", TG);
+  llvm::Timer TRead("read", "Read Artifacts", TG);
+  llvm::Timer TBundle("bundle", "Bundle Input", TG);
+  llvm::Timer TWrite("write", "Write Multi-Arch Bundle", TG);
+
+  info(Verbose, 0, "Bundling started.");
+
+  info(Verbose, 1, "Validating input.");
+  validate(TValidate);
+
+  info(Verbose, 1, "Creating bundle.");
+  ArtifactEncoding Result = create(TRead, TBundle);
+
+  info(Verbose, 1, "Writing bundle to '{0}'.", OutputFile.Path);
+  write(Result, TWrite);
+
+  info(Verbose, 0, "Bundling finished.");
+
+  // Second run() should start from a clean slate.
+  InputFiles.clear();
+  SourceByMember.clear();
+}
+
+void MultiArchCreateCLI::validate(llvm::Timer &TValidate) {
+  llvm::TimeRegion _(Time ? &TValidate : nullptr);
+
+  OutputFile = FormatFile::fromOutputPath(OutputPath);
+  info(Verbose, 2, "Validated output path '{0}'.", OutputFile.Path);
+
+  if (InputPaths.empty()) {
+    fail(NoInputs);
+  }
+
+  for (const auto &InputPath : InputPaths) {
+    InputFiles.push_back(FormatFile::fromInputPath(InputPath));
+  }
+
+  info(Verbose, 2, "Validated {0} input artifact path(s).", InputFiles.size());
+}
+
+ArtifactEncoding MultiArchCreateCLI::create(llvm::Timer &TRead,
+                                            llvm::Timer &TBundle) {
+  info(Verbose, 2, "Bundling members.");
+
+  // The first input decides the bundle kind.
+  ArtifactEncoding First = readInput(0, TRead);
+
+  if (isStaticFamily(First)) {
+    return createStaticLibrary(std::move(First), TRead, TBundle);
+  } else if (isSharedFamily(First)) {
+    return createSharedLibrary(std::move(First), TRead, TBundle);
+  } else {
+    fail(InvalidInputKind, InputFiles[0].Path);
+  }
+}
+
+ArtifactEncoding MultiArchCreateCLI::readInput(size_t Index,
+                                               llvm::Timer &TRead) {
+  const FormatFile &InputFile = InputFiles[Index];
+  info(Verbose, 3, "[{0}/{1}] Reading '{2}'.", Index + 1, InputFiles.size(),
+       InputFile.Path);
+
+  llvm::TimeRegion _(Time ? &TRead : nullptr);
+  auto ExpectedEncoding =
+      InputFile.Format->readArtifactEncoding(InputFile.Path);
+  if (!ExpectedEncoding) {
+    fail(ErrorBuilder::wrap(ExpectedEncoding.takeError())
+             .context(ReadingArtifact, InputFile.Path)
+             .build());
+  }
+  return std::move(*ExpectedEncoding);
+}
+
+const BuildNamespace &
+MultiArchCreateCLI::staticFamilyNamespace(const ArtifactEncoding &E) {
+  assert(isStaticFamily(E) && "not a static-family artifact");
+  if (const auto *SL = std::get_if<StaticLibrary>(&E)) {
+    return SL->Namespace;
+  }
+  return std::get<MultiArchStaticLibrary>(E).Namespace;
+}
+
+const NestedBuildNamespace &
+MultiArchCreateCLI::sharedFamilyNamespace(const ArtifactEncoding &E) {
+  assert(isSharedFamily(E) && "not a shared-family artifact");
+  if (const auto *LU = std::get_if<LUSummaryEncoding>(&E)) {
+    return LU->LUNamespace;
+  }
+  return std::get<MultiArchSharedLibrary>(E).Namespace;
+}
+
+ArtifactEncoding MultiArchCreateCLI::createStaticLibrary(ArtifactEncoding 
First,
+                                                         llvm::Timer &TRead,
+                                                         llvm::Timer &TBundle) 
{
+  MultiArchStaticLibrary Bundle(staticFamilyNamespace(First).withKind(
+      BuildNamespaceKind::MultiArchStaticLibrary));
+
+  addStaticInput(Bundle, std::move(First), 0, TBundle);
+  for (size_t Index = 1; Index < InputFiles.size(); ++Index) {
+    addStaticInput(Bundle, readInput(Index, TRead), Index, TBundle);
+  }
+
+  if (Bundle.Members.empty()) {
+    fail(NoCandidateMembers);
+  }
+
+  info(Verbose, 2, "Bundled {0} member(s).", Bundle.Members.size());
+  info(Verbose, 2, "Target namespace: '{0}'.", Bundle.Namespace);
+
+  return ArtifactEncoding(std::move(Bundle));
+}
+
+ArtifactEncoding MultiArchCreateCLI::createSharedLibrary(ArtifactEncoding 
First,
+                                                         llvm::Timer &TRead,
+                                                         llvm::Timer &TBundle) 
{
+  MultiArchSharedLibrary Bundle(sharedFamilyNamespace(First));
+
+  addSharedInput(Bundle, std::move(First), 0, TBundle);
+  for (size_t Index = 1; Index < InputFiles.size(); ++Index) {
+    addSharedInput(Bundle, readInput(Index, TRead), Index, TBundle);
+  }
+
+  if (Bundle.Members.empty()) {
+    fail(NoCandidateMembers);
+  }
+
+  info(Verbose, 2, "Bundled {0} member(s).", Bundle.Members.size());
+  info(Verbose, 2, "Target namespace: '{0}'.", Bundle.Namespace);
+
+  return ArtifactEncoding(std::move(Bundle));
+}
+
+void MultiArchCreateCLI::addStaticInput(MultiArchStaticLibrary &Bundle,
+                                        ArtifactEncoding Encoding, size_t 
Index,
+                                        llvm::Timer &TBundle) {
+  llvm::StringRef SourceFile = InputFiles[Index].Path;
+  info(Verbose, 3, "[{0}/{1}] Bundling '{2}'.", Index + 1, InputFiles.size(),
+       SourceFile);
+  llvm::TimeRegion _(Time ? &TBundle : nullptr);
+
+  if (auto *SL = std::get_if<StaticLibrary>(&Encoding)) {
+    BuildNamespace Expected =
+        Bundle.Namespace.withKind(BuildNamespaceKind::StaticLibrary);
+    if (SL->Namespace != Expected) {
+      fail(NamespaceMismatch, SL->Namespace, SourceFile, Expected);
+    }
+    addStaticMember(Bundle, std::make_unique<StaticLibrary>(std::move(*SL)),
+                    SourceFile);
+    return;
+  }
+
+  if (auto *MASL = std::get_if<MultiArchStaticLibrary>(&Encoding)) {
+    if (MASL->Namespace != Bundle.Namespace) {
+      fail(NamespaceMismatch, MASL->Namespace, SourceFile, Bundle.Namespace);
+    }
+
+    while (!MASL->Members.empty()) {
+      auto Node = MASL->Members.extract(MASL->Members.begin());
+      addStaticMember(Bundle, std::move(Node.value()), SourceFile);
+    }
+    return;
+  }
+
+  if (isTUSummaryEncoding(Encoding)) {
+    fail(InvalidInputKind, SourceFile);
+  }
+
+  fail(MixedFamily, SourceFile, SharedFamilyName, StaticFamilyName);
+}
+
+void MultiArchCreateCLI::addSharedInput(MultiArchSharedLibrary &Bundle,
+                                        ArtifactEncoding Encoding, size_t 
Index,
+                                        llvm::Timer &TBundle) {
+  llvm::StringRef SourceFile = InputFiles[Index].Path;
+  info(Verbose, 3, "[{0}/{1}] Bundling '{2}'.", Index + 1, InputFiles.size(),
+       SourceFile);
+  llvm::TimeRegion _(Time ? &TBundle : nullptr);
+
+  if (auto *LU = std::get_if<LUSummaryEncoding>(&Encoding)) {
+    if (LU->LUNamespace != Bundle.Namespace) {
+      fail(NamespaceMismatch, LU->LUNamespace, SourceFile, Bundle.Namespace);
+    }
+    addSharedMember(Bundle, 
std::make_unique<LUSummaryEncoding>(std::move(*LU)),
+                    SourceFile);
+    return;
+  }
+
+  if (auto *MASharedL = std::get_if<MultiArchSharedLibrary>(&Encoding)) {
+    if (MASharedL->Namespace != Bundle.Namespace) {
+      fail(NamespaceMismatch, MASharedL->Namespace, SourceFile,
+           Bundle.Namespace);
+    }
+    while (!MASharedL->Members.empty()) {
+      auto Node = MASharedL->Members.extract(MASharedL->Members.begin());
+      addSharedMember(Bundle, std::move(Node.value()), SourceFile);
+    }
+    return;
+  }
+
+  if (isTUSummaryEncoding(Encoding)) {
+    fail(InvalidInputKind, SourceFile);
+  }
+
+  fail(MixedFamily, SourceFile, StaticFamilyName, SharedFamilyName);
+}
+
+void MultiArchCreateCLI::addStaticMember(MultiArchStaticLibrary &Bundle,
+                                         std::unique_ptr<StaticLibrary> Member,
+                                         llvm::StringRef SourceFile) {
+  auto [It, Inserted] = Bundle.Members.insert(std::move(Member));
+  if (!Inserted) {
+    fail(DuplicateTriple, llvm::Triple::normalize((*It)->TargetTriple.str()),
+         SourceByMember.lookup(It->get()), SourceFile);
+  }
+  SourceByMember[It->get()] = SourceFile;
+}
+
+void MultiArchCreateCLI::addSharedMember(
+    MultiArchSharedLibrary &Bundle, std::unique_ptr<LUSummaryEncoding> Member,
+    llvm::StringRef SourceFile) {
+  auto [It, Inserted] = Bundle.Members.insert(std::move(Member));
+  if (!Inserted) {
+    fail(DuplicateTriple, llvm::Triple::normalize((*It)->TargetTriple.str()),
+         SourceByMember.lookup(It->get()), SourceFile);
+  }
+  SourceByMember[It->get()] = SourceFile;
+}
+
+void MultiArchCreateCLI::write(const ArtifactEncoding &Bundle,
+                               llvm::Timer &TWrite) {
+  llvm::TimeRegion _(Time ? &TWrite : nullptr);
+
+  if (auto Err =
+          OutputFile.Format->writeArtifactEncoding(Bundle, OutputFile.Path)) {
+    fail(std::move(Err));
+  }
+}
+
+} // namespace clang::ssaf
diff --git a/clang/tools/clang-ssaf-linker/MultiArchCreateCLI.h 
b/clang/tools/clang-ssaf-linker/MultiArchCreateCLI.h
new file mode 100644
index 0000000000000..f1e811c90e9c7
--- /dev/null
+++ b/clang/tools/clang-ssaf-linker/MultiArchCreateCLI.h
@@ -0,0 +1,111 @@
+//===- MultiArchCreateCLI.h -------------------------------------*- C++ 
-*-===//
+//
+// 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
+//
+//===----------------------------------------------------------------------===//
+//
+//  Declares the CLI action class for `clang-ssaf-linker multi-arch create`.
+//  Bundles StaticLibrary/MultiArchStaticLibrary inputs into one
+//  MultiArchStaticLibrary, or LUSummaryEncoding/MultiArchSharedLibrary inputs
+//  into one MultiArchSharedLibrary.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_CLANG_SSAF_LINKER_MULTIARCHCREATECLI_H
+#define LLVM_CLANG_TOOLS_CLANG_SSAF_LINKER_MULTIARCHCREATECLI_H
+
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h"
+#include 
"clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.h"
+#include 
"clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h"
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/StaticLibrary.h"
+#include "clang/ScalableStaticAnalysis/Core/Model/BuildNamespace.h"
+#include 
"clang/ScalableStaticAnalysis/Core/Serialization/SerializationFormat.h"
+#include "clang/ScalableStaticAnalysis/Tool/Utils.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/DenseMap.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Timer.h"
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace clang::ssaf {
+
+/// Runs the `multi-arch create` action for `clang-ssaf-linker`.
+class MultiArchCreateCLI {
+public:
+  /// Orchestrates validation, construction, and serialization of the MultiArch
+  /// artifact.
+  void run(llvm::TimerGroup &TG, llvm::ArrayRef<std::string> InputPaths,
+           llvm::StringRef OutputPath, bool Verbose, bool Time);
+
+private:
+  /// Validates the output path and every input path.
+  void validate(llvm::Timer &TValidate);
+
+  /// Constructs the artifact from the inputs.
+  ArtifactEncoding create(llvm::Timer &TRead, llvm::Timer &TBundle);
+
+  /// Reads Artifact from file at \p Index.
+  ArtifactEncoding readInput(size_t Index, llvm::Timer &TRead);
+
+  /// Returns the namespace of the static-family input.
+  static const BuildNamespace &staticFamilyNamespace(const ArtifactEncoding 
&E);
+
+  /// Returns the namespace of the shared-family input.
+  static const NestedBuildNamespace &
+  sharedFamilyNamespace(const ArtifactEncoding &E);
+
+  /// Constructs a MultiArchStaticLibrary from the inputs.
+  ArtifactEncoding createStaticLibrary(ArtifactEncoding First,
+                                       llvm::Timer &TRead,
+                                       llvm::Timer &TBundle);
+
+  /// Constructs a MultiArchSharedLibrary from the inputs.
+  ArtifactEncoding createSharedLibrary(ArtifactEncoding First,
+                                       llvm::Timer &TRead,
+                                       llvm::Timer &TBundle);
+
+  /// Adds one input to the \p Bundle.
+  void addStaticInput(MultiArchStaticLibrary &Bundle, ArtifactEncoding 
Encoding,
+                      size_t Index, llvm::Timer &TBundle);
+  void addSharedInput(MultiArchSharedLibrary &Bundle, ArtifactEncoding 
Encoding,
+                      size_t Index, llvm::Timer &TBundle);
+
+  /// Inserts one member, failing on duplicate target triple.
+  void addStaticMember(MultiArchStaticLibrary &Bundle,
+                       std::unique_ptr<StaticLibrary> Member,
+                       llvm::StringRef SourceFile);
+  void addSharedMember(MultiArchSharedLibrary &Bundle,
+                       std::unique_ptr<LUSummaryEncoding> Member,
+                       llvm::StringRef SourceFile);
+
+  /// Serializes the artifact to the validated output path.
+  void write(const ArtifactEncoding &Bundle, llvm::Timer &TWrite);
+
+  // Arguments captured by run() before dispatching to bundling methods.
+  // InputPaths and OutputPath are non-owning: they alias the driver's cl::opt
+  // storage, which outlives the call.
+  llvm::ArrayRef<std::string> InputPaths;
+  llvm::StringRef OutputPath;
+  bool Verbose = false;
+  bool Time = false;
+
+  // State populated during validate() and consumed by later phases.
+  FormatFile OutputFile;
+  std::vector<FormatFile> InputFiles;
+
+  /// Maps each inserted member to the input file that contributed it, so a
+  /// duplicate can name both contributors. Keyed on the member's address, not
+  /// its triple spelling: the member sets are keyed by Triple enum components,
+  /// which fold alias spellings ("arm64" / "aarch64") and OS versions, so two
+  /// colliding slices can carry triple strings that differ. The values alias
+  /// InputFiles[I].Path, which outlives the run.
+  llvm::DenseMap<const void *, llvm::StringRef> SourceByMember;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_TOOLS_CLANG_SSAF_LINKER_MULTIARCHCREATECLI_H
diff --git a/clang/tools/clang-ssaf-linker/SSAFLinker.cpp 
b/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
index 308914bddf270..9d72e9f338328 100644
--- a/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
@@ -6,13 +6,16 @@
 //
 
//===----------------------------------------------------------------------===//
 //
-//  This file implements the SSAF entity linker tool. Its default behavior
-//  is to link N TU summaries into one LU summary via the EntityLinker
-//  framework. It also provides the `static-library` subcommand for
-//  bundling TU summaries into a StaticLibrary.
+//  This file implements the SSAF entity linker tool. Its default behavior is 
to
+//  link N TU summaries into one LU summary via the EntityLinker framework. It
+//  also provides the `static-library` subcommand for bundling TU summaries 
into
+//  a StaticLibrary, and the `multi-arch` subcommand for bundling StaticLibrary
+//  and SharedLibrary members (or existing multi-arch bundles) into
+//  MultiArchStaticLibrary or MultiArchSharedLibrary.
 //
 
//===----------------------------------------------------------------------===//
 
+#include "MultiArchCreateCLI.h"
 #include "StaticLibraryCreateCLI.h"
 
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h"
@@ -50,6 +53,11 @@ cl::OptionCategory SsafLinkerCategory("clang-ssaf-linker 
options");
 cl::SubCommand StaticLibraryCmd("static-library",
                                 "Operations on StaticLibraries");
 
+// The `multi-arch` subcommand groups all multi-architecture operations.
+cl::SubCommand MultiArchCmd("multi-arch",
+                            "Operations on multi-architecture StaticLibrary "
+                            "and SharedLibrary artifacts");
+
 // Top-level (default) `link` action positionals.
 cl::list<std::string> InputPaths(cl::Positional, cl::desc("<input files>"),
                                  cl::OneOrMore, cl::cat(SsafLinkerCategory));
@@ -62,12 +70,12 @@ cl::opt<std::string> OutputPath("o", cl::desc("Output file 
path"),
 cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
                       cl::init(false), cl::cat(SsafLinkerCategory),
                       cl::sub(cl::SubCommand::getTopLevel()),
-                      cl::sub(StaticLibraryCmd));
+                      cl::sub(StaticLibraryCmd), cl::sub(MultiArchCmd));
 
 cl::opt<bool> Time("time", cl::desc("Enable timing"), cl::init(false),
                    cl::cat(SsafLinkerCategory),
                    cl::sub(cl::SubCommand::getTopLevel()),
-                   cl::sub(StaticLibraryCmd));
+                   cl::sub(StaticLibraryCmd), cl::sub(MultiArchCmd));
 
 // The `static-library` subcommand's verb positional. Declared BEFORE
 // StaticLibraryInputs so cl-lib binds argv[0] under the subcommand to the
@@ -75,7 +83,6 @@ cl::opt<bool> Time("time", cl::desc("Enable timing"), 
cl::init(false),
 cl::opt<std::string> StaticLibraryVerb(cl::Positional, cl::Required,
                                        cl::sub(StaticLibraryCmd),
                                        cl::desc("<verb>"),
-                                       cl::value_desc("create"),
                                        cl::cat(SsafLinkerCategory));
 
 // The `static-library` subcommand's action-specific positional input
@@ -104,6 +111,25 @@ cl::opt<std::string> StaticLibraryTriple(
              "inputs when set)"),
     cl::value_desc("triple"), cl::cat(SsafLinkerCategory));
 
+// The `multi-arch` subcommand's verb positional. Declared BEFORE
+// MultiArchInputs so cl-lib binds argv[0] under the subcommand to the verb
+// rather than to the greedy input list.
+cl::opt<std::string> MultiArchVerb(cl::Positional, cl::Required,
+                                   cl::sub(MultiArchCmd), cl::desc("<verb>"),
+                                   cl::cat(SsafLinkerCategory));
+
+// The `multi-arch` subcommand's action-specific positional input list.
+// Currently consumed by `multi-arch create`.
+cl::list<std::string>
+    MultiArchInputs(cl::Positional, cl::sub(MultiArchCmd),
+                    cl::desc("<static-library or shared-library files>"),
+                    cl::cat(SsafLinkerCategory));
+
+cl::opt<std::string> MultiArchOutput("o", cl::Required, cl::sub(MultiArchCmd),
+                                     cl::desc("Output file path"),
+                                     cl::value_desc("path"),
+                                     cl::cat(SsafLinkerCategory));
+
 
//===----------------------------------------------------------------------===//
 // StaticLibrary Verbs
 
//===----------------------------------------------------------------------===//
@@ -112,6 +138,14 @@ cl::opt<std::string> StaticLibraryTriple(
 // UnknownStaticLibraryVerb below.
 constexpr const char *StaticLibraryCreateVerb = "create";
 
+//===----------------------------------------------------------------------===//
+// MultiArch Verbs
+//===----------------------------------------------------------------------===//
+
+// Verb strings for the `multi-arch` subcommand. Kept in sync with
+// UnknownMultiArchVerb below.
+constexpr const char *MultiArchCreateVerb = "create";
+
 
//===----------------------------------------------------------------------===//
 // Error Messages
 
//===----------------------------------------------------------------------===//
@@ -123,22 +157,10 @@ constexpr const char *LinkingSummary = "Linking summary 
'{0}'";
 constexpr const char *UnknownStaticLibraryVerb =
     "unknown static-library verb '{0}': expected 'create'";
 
-} // namespace LocalErrorMessages
-
-//===----------------------------------------------------------------------===//
-// Diagnostic Utilities
-//===----------------------------------------------------------------------===//
-
-constexpr unsigned IndentationWidth = 2;
+constexpr const char *UnknownMultiArchVerb =
+    "unknown multi-arch verb '{0}': expected 'create'";
 
-template <typename... Ts>
-void info(unsigned IndentationLevel, const char *Fmt, Ts &&...Args) {
-  if (Verbose) {
-    llvm::WithColor::note()
-        << std::string(IndentationLevel * IndentationWidth, ' ') << "- "
-        << llvm::formatv(Fmt, std::forward<Ts>(Args)...) << "\n";
-  }
-}
+} // namespace LocalErrorMessages
 
 
//===----------------------------------------------------------------------===//
 // link action
@@ -161,7 +183,7 @@ LinkerInput validateLinkInput(llvm::TimerGroup &TG) {
     LI.LinkUnitName = path::stem(LI.OutputFile.Path).str();
   }
 
-  info(2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
+  info(Verbose, 2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
 
   {
     llvm::TimeRegion _(Time ? &TValidate : nullptr);
@@ -170,22 +192,22 @@ LinkerInput validateLinkInput(llvm::TimerGroup &TG) {
     }
   }
 
-  info(2, "Validated {0} input summary paths.", LI.InputFiles.size());
+  info(Verbose, 2, "Validated {0} input summary paths.", LI.InputFiles.size());
 
   return LI;
 }
 
 void runLink(llvm::TimerGroup &TG) {
-  info(0, "Linking started.");
+  info(Verbose, 0, "Linking started.");
 
   LinkerInput LI;
   {
-    info(1, "Validating input.");
+    info(Verbose, 1, "Validating input.");
     LI = validateLinkInput(TG);
   }
 
-  info(1, "Linking input.");
-  info(2, "Constructing linker.");
+  info(Verbose, 1, "Linking input.");
+  info(Verbose, 2, "Constructing linker.");
 
   // TODO: The linker currently uses a hardcoded target triple. Architecture
   // tracking in the linker will be handled properly in a separate PR.
@@ -197,14 +219,14 @@ void runLink(llvm::TimerGroup &TG) {
   llvm::Timer TLink("link", "Link Summaries", TG);
   llvm::Timer TWrite("write", "Write Summary", TG);
 
-  info(2, "Linking summaries.");
+  info(Verbose, 2, "Linking summaries.");
 
   for (auto [Index, InputFile] : llvm::enumerate(LI.InputFiles)) {
     std::unique_ptr<TUSummaryEncoding> Summary;
 
     {
-      info(3, "[{0}/{1}] Reading '{2}'.", (Index + 1), LI.InputFiles.size(),
-           InputFile.Path);
+      info(Verbose, 3, "[{0}/{1}] Reading '{2}'.", (Index + 1),
+           LI.InputFiles.size(), InputFile.Path);
 
       llvm::TimeRegion _(Time ? &TRead : nullptr);
 
@@ -219,8 +241,8 @@ void runLink(llvm::TimerGroup &TG) {
     }
 
     {
-      info(3, "[{0}/{1}] Linking '{2}'.", (Index + 1), LI.InputFiles.size(),
-           InputFile.Path);
+      info(Verbose, 3, "[{0}/{1}] Linking '{2}'.", (Index + 1),
+           LI.InputFiles.size(), InputFile.Path);
 
       llvm::TimeRegion _(Time ? &TLink : nullptr);
 
@@ -233,7 +255,7 @@ void runLink(llvm::TimerGroup &TG) {
   }
 
   {
-    info(2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
+    info(Verbose, 2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
 
     llvm::TimeRegion _(Time ? &TWrite : nullptr);
 
@@ -244,7 +266,7 @@ void runLink(llvm::TimerGroup &TG) {
     }
   }
 
-  info(0, "Linking finished.");
+  info(Verbose, 0, "Linking finished.");
 }
 
 
//===----------------------------------------------------------------------===//
@@ -269,6 +291,19 @@ void runStaticLibrary(llvm::TimerGroup &TG) {
        StaticLibraryVerb.getValue());
 }
 
+//===----------------------------------------------------------------------===//
+// multi-arch subcommand dispatch
+//===----------------------------------------------------------------------===//
+
+void runMultiArch(llvm::TimerGroup &TG) {
+  if (MultiArchVerb == MultiArchCreateVerb) {
+    MultiArchCreateCLI MAC;
+    MAC.run(TG, MultiArchInputs, MultiArchOutput, Verbose, Time);
+    return;
+  }
+  fail(LocalErrorMessages::UnknownMultiArchVerb, MultiArchVerb.getValue());
+}
+
 } // namespace
 
 
//===----------------------------------------------------------------------===//
@@ -285,6 +320,8 @@ int main(int argc, const char **argv) {
 
   if (StaticLibraryCmd) {
     runStaticLibrary(Timers);
+  } else if (MultiArchCmd) {
+    runMultiArch(Timers);
   } else {
     // Default (no subcommand): run the linker pipeline.
     runLink(Timers);
diff --git a/clang/unittests/ScalableStaticAnalysis/BuildNamespaceTest.cpp 
b/clang/unittests/ScalableStaticAnalysis/BuildNamespaceTest.cpp
index 5089796a2b0b8..43d8b5f7e0e43 100644
--- a/clang/unittests/ScalableStaticAnalysis/BuildNamespaceTest.cpp
+++ b/clang/unittests/ScalableStaticAnalysis/BuildNamespaceTest.cpp
@@ -31,6 +31,19 @@ TEST(BuildNamespaceTest, DifferentKinds) {
   EXPECT_NE(CU, LU);
 }
 
+TEST(BuildNamespaceTest, WithKind) {
+  BuildNamespace Bare(BuildNamespaceKind::StaticLibrary, "libmulti");
+  BuildNamespace Wrapper =
+      Bare.withKind(BuildNamespaceKind::MultiArchStaticLibrary);
+
+  // The name is preserved and only the kind changes.
+  EXPECT_EQ(Wrapper, BuildNamespace(BuildNamespaceKind::MultiArchStaticLibrary,
+                                    "libmulti"));
+  // The original is left unmodified.
+  EXPECT_EQ(Bare,
+            BuildNamespace(BuildNamespaceKind::StaticLibrary, "libmulti"));
+}
+
 // NestedBuildNamespace Tests
 
 TEST(NestedBuildNamespaceTest, DefaultConstruction) {

_______________________________________________
cfe-commits mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/cfe-commits

Reply via email to