[clang] [clang][ssaf] Link static libraries and multi-arch static libraries (PR #215349)

Aviral Goel via cfe-commits cfe-commits at lists.llvm.org
Tue Aug 11 14:53:48 PDT 2026


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

>From f3f200de85080a90b1ed586046168d2e7a6cd4dd Mon Sep 17 00:00:00 2001
From: Aviral Goel <goel.aviral at gmail.com>
Date: Tue, 11 Aug 2026 11:13:42 -0700
Subject: [PATCH] [clang][ssaf] Link static libraries and multi-arch static
 libraries

This change introduces support for linking static libraries and multi-arch
static libraries. To implement this, we have added two `link` overloads to the
`EntityLinker`: a static library folds in as a bundle of TU summaries, and a
multi-arch static library contributes only the static library with the matching
target triple. The target triple is supplied through a new optional flag,
`--target-triple`. If unspecified, it is inferred from the first input, if
possible.
---
 .../Core/EntityLinker/EntityLinker.h          |  54 +++-
 .../EntityLinker/MultiArchStaticLibrary.h     |   3 +
 .../Core/EntityLinker/StaticLibrary.h         |  12 +-
 .../Core/Support/FormatProviders.h            |   8 +
 .../clang/ScalableStaticAnalysis/Tool/Utils.h |  15 +
 .../Core/EntityLinker/EntityLinker.cpp        |  78 +++++
 .../lib/ScalableStaticAnalysis/Tool/Utils.cpp |  21 ++
 .../ssaf-linker/Inputs/libord-reversed.json   |  62 ++++
 .../ssaf-linker/Inputs/libtwo-2arch.json      |  39 +++
 .../Scalable/ssaf-linker/Inputs/tu-linux.json |  11 +
 .../ssaf-linker/Inputs/tu-x86_64.json         |  11 +
 .../Analysis/Scalable/ssaf-linker/help.test   |  23 +-
 .../Analysis/Scalable/ssaf-linker/io.test     |  12 +-
 .../Scalable/ssaf-linker/linking-errors.test  | 120 ++++++-
 .../Scalable/ssaf-linker/linking.test         | 123 ++++++++
 .../Analysis/Scalable/ssaf-linker/time.test   |   6 +-
 .../Scalable/ssaf-linker/verbose.test         |  53 +++-
 clang/tools/clang-ssaf-linker/CMakeLists.txt  |   1 +
 clang/tools/clang-ssaf-linker/LinkCLI.cpp     | 296 ++++++++++++++++++
 clang/tools/clang-ssaf-linker/LinkCLI.h       | 106 +++++++
 clang/tools/clang-ssaf-linker/SSAFLinker.cpp  | 130 +-------
 21 files changed, 1032 insertions(+), 152 deletions(-)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/libord-reversed.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/libtwo-2arch.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-linux.json
 create mode 100644 clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-x86_64.json
 create mode 100644 clang/tools/clang-ssaf-linker/LinkCLI.cpp
 create mode 100644 clang/tools/clang-ssaf-linker/LinkCLI.h

diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h
index f07def1a9b344..9a90b941bddfc 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h
@@ -8,6 +8,8 @@
 //
 //  This file defines the EntityLinker class that combines multiple TU summaries
 //  into a unified LU summary by deduplicating entities and patching summaries.
+//  TU summaries may be supplied individually, bundled in a static library, or
+//  bundled in one architecture member of a multi-architecture static library.
 //
 //===----------------------------------------------------------------------===//
 
@@ -24,17 +26,27 @@
 
 namespace clang::ssaf {
 
+class LinkCLI;
+class MultiArchStaticLibrary;
+class StaticLibrary;
 class TUSummaryEncoding;
 
 class EntityLinker {
+  friend class LinkCLI;
+
   LUSummaryEncoding Output;
+
+  // Namespaces of the TU summaries folded in so far, whether supplied directly
+  // or as members of a library. Doubles as the count of linked translation
+  // units, which is the only place that number is known once a library or a
+  // multi-arch member has been expanded.
   std::set<BuildNamespace> ProcessedTUNamespaces;
 
 public:
   /// Constructs an EntityLinker to link TU summaries into a LU summary.
   ///
-  /// \param TargetTriple The target triple of the link unit. Every linked TU
-  ///        must report the same triple.
+  /// \param TargetTriple The target triple of the link unit. Every linked
+  ///        input must report the same triple.
   /// \param LUNamespace The namespace identifying this link unit.
   EntityLinker(llvm::Triple TargetTriple, NestedBuildNamespace LUNamespace)
       : Output(std::move(TargetTriple), std::move(LUNamespace)) {}
@@ -45,11 +57,34 @@ class EntityLinker {
   /// and merges them into a single data store.
   ///
   /// \param Summary The TU summary to link. Ownership is transferred.
-  /// \returns Error if the TU namespace has already been linked or if patching
-  ///          fails, success otherwise. Corrupted summary data (missing linkage
-  ///          information, duplicate entity IDs, etc.) triggers a fatal error.
+  /// \returns Error if \p Summary reports a different target triple than this
+  ///          link unit, if its TU namespace has already been linked, or if
+  ///          patching fails; success otherwise. Corrupted summary data
+  ///          (missing linkage information, duplicate entity IDs, etc.)
+  ///          triggers a fatal error.
   llvm::Error link(std::unique_ptr<TUSummaryEncoding> Summary);
 
+  /// Links every member of a static library into the LU summary.
+  ///
+  /// Members are folded in unconditionally, in an unspecified order, exactly as
+  /// if each had been passed as an individual TU summary.
+  ///
+  /// \param Library The static library to link. Ownership is transferred.
+  /// \returns Error if \p Library reports a different target triple than this
+  ///          link unit or if any member fails to link, success otherwise.
+  llvm::Error link(std::unique_ptr<StaticLibrary> Library);
+
+  /// Links the architecture member matching this link unit into the LU summary.
+  ///
+  /// Members for other architectures are discarded.
+  ///
+  /// \param Library The multi-arch static library to link. Ownership is
+  ///        transferred.
+  /// \returns Error if \p Library has no member whose target triple equals this
+  ///          link unit's, or if the selected member fails to link; success
+  ///          otherwise.
+  llvm::Error link(std::unique_ptr<MultiArchStaticLibrary> Library);
+
   /// Returns the accumulated LU summary.
   ///
   /// \returns LU summary containing all the deduplicated and patched entity
@@ -57,6 +92,15 @@ class EntityLinker {
   LUSummaryEncoding takeOutput() && { return std::move(Output); }
 
 private:
+  /// Checks that an input belongs to this link unit's target.
+  ///
+  /// \param TargetTriple The triple of the input being linked.
+  /// \param InputNamespace The namespace naming that input in the diagnostic.
+  /// \returns Error if \p TargetTriple differs from this link unit's, success
+  ///          otherwise.
+  llvm::Error checkTargetTriple(const llvm::Triple &TargetTriple,
+                                const BuildNamespace &InputNamespace) const;
+
   /// Resolves a TU entity name to an LU entity name and ID.
   ///
   /// \param OldName The entity name in the TU namespace.
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
index f5ddafbb9d577..b900d5458e9b5 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h
@@ -23,6 +23,7 @@
 
 namespace clang::ssaf {
 
+class LinkCLI;
 class MultiArchCreateCLI;
 
 /// Represents a multi-architecture static library.
@@ -32,6 +33,8 @@ class MultiArchCreateCLI;
 /// architectures; the wrapper's \c Namespace identifies that shared library and
 /// every member's namespace must agree on its name.
 class MultiArchStaticLibrary {
+  friend class EntityLinker;
+  friend class LinkCLI;
   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 c74fc4b16e0b2..db1477a42e9b5 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 LinkCLI;
 class MultiArchCreateCLI;
 class StaticLibraryCreateCLI;
 
@@ -30,8 +31,8 @@ class StaticLibraryCreateCLI;
 ///
 /// A StaticLibrary bundles member translation units without performing
 /// entity resolution, mirroring the role of ar / libtool -static / lib.exe
-/// in native build pipelines. It is consumed by the EntityLinker for
-/// selective inclusion when passed as a command line argument.
+/// in native build pipelines. It is consumed by the EntityLinker when passed
+/// as a command line argument.
 ///
 /// Static libraries are single-architecture: every member's target triple
 /// must equal the library's. Multi-architecture static libraries are
@@ -40,8 +41,13 @@ class StaticLibraryCreateCLI;
 ///
 /// Members are stored as encoded TUSummaryEncoding objects: the
 /// static-library tool never decodes per-entity payloads, and the linker
-/// consumes them as-is during its selective inclusion pass.
+/// consumes them as-is while folding them into its link unit.
+///
+/// TODO: The linker currently folds in every member. Restrict inclusion to
+/// the members a link unit actually references, as native linkers do.
 class StaticLibrary {
+  friend class EntityLinker;
+  friend class LinkCLI;
   friend class MultiArchCreateCLI;
   friend class MultiArchStaticLibrary;
   friend class SerializationFormat;
diff --git a/clang/include/clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h b/clang/include/clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h
index e4a7b54033924..e671a7c2d088a 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h
@@ -22,6 +22,7 @@
 #include "clang/ScalableStaticAnalysis/Core/WholeProgramAnalysis/AnalysisName.h"
 #include "llvm/Support/FormatProviders.h"
 #include "llvm/Support/raw_ostream.h"
+#include "llvm/TargetParser/Triple.h"
 
 namespace llvm {
 
@@ -88,6 +89,13 @@ template <> struct format_provider<clang::ssaf::AnalysisName> {
   }
 };
 
+template <> struct format_provider<llvm::Triple> {
+  static void format(const llvm::Triple &Val, raw_ostream &OS,
+                     StringRef Style) {
+    OS << llvm::Triple::normalize(Val.str());
+  }
+};
+
 } // namespace llvm
 
 #endif // LLVM_CLANG_SCALABLESTATICANALYSIS_CORE_SUPPORT_FORMATPROVIDERS_H
diff --git a/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h b/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h
index 8ceb539a055de..968c97a552852 100644
--- a/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h
+++ b/clang/include/clang/ScalableStaticAnalysis/Tool/Utils.h
@@ -23,6 +23,7 @@
 #include "llvm/Support/Error.h"
 #include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/WithColor.h"
+#include "llvm/TargetParser/Triple.h"
 #include <string>
 
 namespace clang::ssaf {
@@ -76,6 +77,20 @@ void loadPlugins(llvm::ArrayRef<std::string> Paths);
 void initTool(int argc, const char **argv, llvm::StringRef Version,
               llvm::cl::OptionCategory &Category, llvm::StringRef ToolHeading);
 
+//===----------------------------------------------------------------------===//
+// Target Triples
+//===----------------------------------------------------------------------===//
+
+/// Parses and validates a target triple supplied on the command line.
+///
+///
+/// \param FlagName The option supplying \p Value, named in the diagnostic.
+/// \param Value The triple as spelled by the user. Must not be empty.
+/// \returns The parsed triple. Calls fail() and exits if the architecture is
+///          unrecognized.
+llvm::Triple parseTargetTripleOrFail(llvm::StringRef FlagName,
+                                     llvm::StringRef Value);
+
 //===----------------------------------------------------------------------===//
 // Data Structures
 //===----------------------------------------------------------------------===//
diff --git a/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp b/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp
index 462978932a53d..b703efd8324fb 100644
--- a/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.cpp
@@ -8,11 +8,15 @@
 
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h"
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/EntitySummaryEncoding.h"
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchStaticLibrary.h"
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/StaticLibrary.h"
 #include "clang/ScalableStaticAnalysis/Core/EntityLinker/TUSummaryEncoding.h"
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityLinkage.h"
 #include "clang/ScalableStaticAnalysis/Core/Model/EntityName.h"
 #include "clang/ScalableStaticAnalysis/Core/Support/ErrorBuilder.h"
 #include "clang/ScalableStaticAnalysis/Core/Support/FormatProviders.h"
+#include "llvm/ADT/STLExtras.h"
+#include "llvm/ADT/StringExtras.h"
 #include <cassert>
 
 using namespace clang::ssaf;
@@ -44,6 +48,16 @@ static constexpr const char *FailedToInsertEntityIntoOutputSummary =
 static constexpr const char *DuplicateTUNamespace =
     "failed to link TU summary: duplicate {0}";
 
+static constexpr const char *LinkingStaticLibraryMember =
+    "failed to link member {0} of static library {1}";
+
+static constexpr const char *MismatchedTargetTriple =
+    "target triple '{0}' of {1} does not match link unit target triple '{2}'";
+
+static constexpr const char *NoMemberForTargetTriple =
+    "multi-arch static library {0} has no member for target triple '{1}' "
+    "(available: {2})";
+
 } // namespace ErrorMessages
 
 static NestedBuildNamespace
@@ -180,7 +194,25 @@ EntityLinker::patch(const std::vector<EntitySummaryEncoding *> &PatchTargets,
   return llvm::Error::success();
 }
 
+llvm::Error
+EntityLinker::checkTargetTriple(const llvm::Triple &TargetTriple,
+                                const BuildNamespace &InputNamespace) const {
+  if (TargetTriple != Output.TargetTriple) {
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::MismatchedTargetTriple,
+                                TargetTriple, InputNamespace,
+                                Output.TargetTriple)
+        .build();
+  }
+  return llvm::Error::success();
+}
+
 llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
+  if (auto Err =
+          checkTargetTriple(Summary->TargetTriple, Summary->TUNamespace)) {
+    return Err;
+  }
+
   auto [_, Inserted] = ProcessedTUNamespaces.insert(Summary->TUNamespace);
   if (!Inserted) {
     return ErrorBuilder::create(std::errc::invalid_argument,
@@ -195,3 +227,49 @@ llvm::Error EntityLinker::link(std::unique_ptr<TUSummaryEncoding> Summary) {
   auto PatchTargets = merge(SummaryRef, EntityResolutionTable);
   return patch(PatchTargets, EntityResolutionTable);
 }
+
+llvm::Error EntityLinker::link(std::unique_ptr<StaticLibrary> Library) {
+  if (auto Err = checkTargetTriple(Library->TargetTriple, Library->Namespace)) {
+    return Err;
+  }
+
+  while (!Library->Members.empty()) {
+    auto Node = Library->Members.extract(Library->Members.begin());
+    const BuildNamespace MemberNamespace = Node.value()->TUNamespace;
+
+    if (auto Err = link(std::move(Node.value()))) {
+      return ErrorBuilder::wrap(std::move(Err))
+          .context(ErrorMessages::LinkingStaticLibraryMember, MemberNamespace,
+                   Library->Namespace)
+          .build();
+    }
+  }
+
+  return llvm::Error::success();
+}
+
+llvm::Error
+EntityLinker::link(std::unique_ptr<MultiArchStaticLibrary> Library) {
+  auto MatchingMember = llvm::find_if(
+      Library->Members, [this](const std::unique_ptr<StaticLibrary> &Member) {
+        return Member->TargetTriple == Output.TargetTriple;
+      });
+
+  if (MatchingMember == Library->Members.end()) {
+    auto TargetTriples = llvm::map_range(
+        Library->Members, [](const std::unique_ptr<StaticLibrary> &Member) {
+          return llvm::Triple::normalize(Member->TargetTriple.str());
+        });
+    std::string Available = Library->Members.empty()
+                                ? std::string("none")
+                                : llvm::join(TargetTriples, ", ");
+
+    return ErrorBuilder::create(std::errc::invalid_argument,
+                                ErrorMessages::NoMemberForTargetTriple,
+                                Library->Namespace, Output.TargetTriple,
+                                Available)
+        .build();
+  }
+
+  return link(std::move(Library->Members.extract(MatchingMember).value()));
+}
diff --git a/clang/lib/ScalableStaticAnalysis/Tool/Utils.cpp b/clang/lib/ScalableStaticAnalysis/Tool/Utils.cpp
index 6a740f57741d7..03139f378186f 100644
--- a/clang/lib/ScalableStaticAnalysis/Tool/Utils.cpp
+++ b/clang/lib/ScalableStaticAnalysis/Tool/Utils.cpp
@@ -56,6 +56,9 @@ constexpr const char *FileAlreadyExists = "File already exists";
 
 constexpr const char *FailedToLoadPlugin = "failed to load plugin '{0}': {1}";
 
+constexpr const char *InvalidTargetTriple =
+    "invalid {0} '{1}': unrecognized architecture";
+
 } // namespace ErrorMessages
 
 llvm::StringRef ToolName;
@@ -148,6 +151,24 @@ void clang::ssaf::loadPlugins(llvm::ArrayRef<std::string> Paths) {
   }
 }
 
+llvm::Triple clang::ssaf::parseTargetTripleOrFail(llvm::StringRef FlagName,
+                                                  llvm::StringRef Value) {
+  assert(!Value.empty() &&
+         "parseTargetTripleOrFail: triple value cannot be empty");
+
+  // Normalize so the components are moved to their proper places.
+  llvm::Triple T(llvm::Triple::normalize(Value));
+
+  // Only the architecture is validated. Validating vendor or OS rejects real
+  // targets like x86_64-unknown-linux-gnu. A misspelled vendor or OS is instead
+  // caught as a triple mismatch during linking or library creation.
+  if (T.getArch() == llvm::Triple::UnknownArch) {
+    fail(ErrorMessages::InvalidTargetTriple, FlagName, Value);
+  }
+
+  return T;
+}
+
 void clang::ssaf::initTool(int argc, const char **argv, llvm::StringRef Version,
                            llvm::cl::OptionCategory &Category,
                            llvm::StringRef ToolHeading) {
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libord-reversed.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libord-reversed.json
new file mode 100644
index 0000000000000..0356200cb9aa5
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libord-reversed.json
@@ -0,0 +1,62 @@
+{
+  "members": [
+    {
+      "data": [],
+      "id_table": [
+        {
+          "id": 0,
+          "name": {
+            "suffix": "",
+            "usr": "c:@F at only_in_b#"
+          }
+        }
+      ],
+      "linkage_table": [
+        {
+          "id": 0,
+          "linkage": {
+            "type": "Internal"
+          }
+        }
+      ],
+      "target_triple": "arm64-apple-macosx",
+      "tu_namespace": {
+        "kind": "CompilationUnit",
+        "name": "tu-b.cpp"
+      },
+      "type": "TUSummary"
+    },
+    {
+      "data": [],
+      "id_table": [
+        {
+          "id": 0,
+          "name": {
+            "suffix": "",
+            "usr": "c:@F at only_in_a#"
+          }
+        }
+      ],
+      "linkage_table": [
+        {
+          "id": 0,
+          "linkage": {
+            "type": "Internal"
+          }
+        }
+      ],
+      "target_triple": "arm64-apple-macosx",
+      "tu_namespace": {
+        "kind": "CompilationUnit",
+        "name": "tu-a.cpp"
+      },
+      "type": "TUSummary"
+    }
+  ],
+  "namespace": {
+    "kind": "StaticLibrary",
+    "name": "libord"
+  },
+  "target_triple": "arm64-apple-macosx",
+  "type": "StaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libtwo-2arch.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libtwo-2arch.json
new file mode 100644
index 0000000000000..c52597151dae3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/libtwo-2arch.json
@@ -0,0 +1,39 @@
+{
+  "members": [
+    {
+      "members": [],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libtwo"
+      },
+      "target_triple": "arm64-apple-macosx",
+      "type": "StaticLibrary"
+    },
+    {
+      "members": [
+        {
+          "data": [],
+          "id_table": [],
+          "linkage_table": [],
+          "target_triple": "x86_64-apple-macosx",
+          "tu_namespace": {
+            "kind": "CompilationUnit",
+            "name": "x86.cpp"
+          },
+          "type": "TUSummary"
+        }
+      ],
+      "namespace": {
+        "kind": "StaticLibrary",
+        "name": "libtwo"
+      },
+      "target_triple": "x86_64-apple-macosx",
+      "type": "StaticLibrary"
+    }
+  ],
+  "namespace": {
+    "kind": "MultiArchStaticLibrary",
+    "name": "libtwo"
+  },
+  "type": "MultiArchStaticLibrary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-linux.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-linux.json
new file mode 100644
index 0000000000000..9bf5495c9e2f1
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-linux.json
@@ -0,0 +1,11 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "linux.cpp"
+  },
+  "id_table": [],
+  "linkage_table": [],
+  "data": [],
+  "target_triple": "x86_64-unknown-linux-gnu",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-x86_64.json b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-x86_64.json
new file mode 100644
index 0000000000000..a482c204f77c7
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-linker/Inputs/tu-x86_64.json
@@ -0,0 +1,11 @@
+{
+  "tu_namespace": {
+    "kind": "CompilationUnit",
+    "name": "x86.cpp"
+  },
+  "id_table": [],
+  "linkage_table": [],
+  "data": [],
+  "target_triple": "x86_64-apple-macosx",
+  "type": "TUSummary"
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/help.test b/clang/test/Analysis/Scalable/ssaf-linker/help.test
index 7b0c21f066016..45d8e11a15e30 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/help.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/help.test
@@ -15,14 +15,15 @@
 // CHECK-NEXT:   Type "clang-ssaf-linker{{(\.exe)?}} <subcommand> --help" to get more help on a specific subcommand
 // 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
-// CHECK-NEXT:   --version           - Display the version of this program
+// 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:   --target-triple=<triple> - Target triple of the link unit (defaults to the first input's; required when the first input is a multi-arch static library with several members)
+// CHECK-NEXT:   --time                   - Enable timing
+// CHECK-NEXT:   --verbose                - Enable verbose output
+// CHECK-NEXT:   --version                - Display the version of this program
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/io.test b/clang/test/Analysis/Scalable/ssaf-linker/io.test
index 304f97db656f6..b2b17c9f094ea 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/io.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/io.test
@@ -6,15 +6,23 @@
 // Malformed JSON input.
 // RUN: not clang-ssaf-linker %S/Inputs/tu-malformed.json -o %t/out.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=BAD-JSON
-// BAD-JSON:      clang-ssaf-linker: error: reading TUSummary from file '{{.*}}tu-malformed.json'
+// BAD-JSON:      clang-ssaf-linker: error: Reading artifact '{{.*}}tu-malformed.json'
+// BAD-JSON-NEXT: reading ArtifactEncoding from file '{{.*}}tu-malformed.json'
 // BAD-JSON-NEXT: {{.*}}: Invalid JSON value{{.*}}
 
 // Missing required fields in otherwise valid JSON.
 // RUN: not clang-ssaf-linker %S/Inputs/tu-missing-fields.json -o %t/out.json 2>&1 \
 // RUN:        | FileCheck %s --match-full-lines --check-prefix=MISSING-FIELDS
-// MISSING-FIELDS:      clang-ssaf-linker: error: reading TUSummary from file '{{.*}}tu-missing-fields.json'
+// MISSING-FIELDS:      clang-ssaf-linker: error: Reading artifact '{{.*}}tu-missing-fields.json'
+// MISSING-FIELDS-NEXT: reading ArtifactEncoding from file '{{.*}}tu-missing-fields.json'
 // MISSING-FIELDS-NEXT: failed to read IdTable from field 'id_table': expected JSON array
 
+// An unrecognized artifact kind names every kind the linker can read.
+// RUN: not clang-ssaf-linker %S/Inputs/bad-artifact.json -o %t/out.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-ARTIFACT
+// BAD-ARTIFACT: clang-ssaf-linker: error: Reading artifact '{{.*}}bad-artifact.json'
+// BAD-ARTIFACT: unknown value 'BogusKind' for field 'type'
+
 // Output file already exists.
 // RUN: touch %t/out.json
 // RUN: not clang-ssaf-linker %S/Inputs/tu-empty.json -o %t/out.json 2>&1 \
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
index 5bda8128ac0e1..e03c7799b2519 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking-errors.test
@@ -5,24 +5,132 @@
 
 // Linking the same TU namespace twice produces an error.
 // RUN: not clang-ssaf-linker %S/Inputs/tu-empty.json %S/Inputs/tu-empty.json -o %t/lu.json 2>&1 \
-// RUN:   | FileCheck %s --match-full-lines --check-prefix=DUP-NS
-// DUP-NS:      clang-ssaf-linker: error: Linking summary '{{.*}}tu-empty.json'
-// DUP-NS-NEXT: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp)
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=DUP-NS-TU
+// DUP-NS-TU:      clang-ssaf-linker: error: Linking artifact '{{.*}}tu-empty.json'
+// DUP-NS-TU-NEXT: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, empty.cpp)
+
+// A static library member colliding with an already linked TU summary names the
+// member and the library it came from, on top of the input file context.
+// RUN: clang-ssaf-linker static-library create %S/Inputs/tu-1.json %S/Inputs/tu-2.json --namespace libtwo -o %t/libtwo.json
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %t/libtwo.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --match-full-lines --check-prefix=DUP-MEMBER
+// DUP-MEMBER:      clang-ssaf-linker: error: Linking artifact '{{.*}}libtwo.json'
+// DUP-MEMBER-NEXT: failed to link member BuildNamespace(CompilationUnit, tu1.cpp) of static library BuildNamespace(StaticLibrary, libtwo)
+// DUP-MEMBER-NEXT: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, tu1.cpp)
 
 // Entity ID object in summary data blob with '@' key alongside extra keys is a fatal error.
 // RUN: not clang-ssaf-linker %S/Inputs/tu-invalid-entity-id-multikey.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-MULTIKEY
-// INVALID-ID-MULTIKEY:      clang-ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-multikey.json'
+// INVALID-ID-MULTIKEY:      clang-ssaf-linker: error: Linking artifact '{{.*}}tu-invalid-entity-id-multikey.json'
 // INVALID-ID-MULTIKEY-NEXT: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
 
 // Entity ID object in summary data blob with a non-uint64 '@' value is a fatal error.
 // RUN: not clang-ssaf-linker %S/Inputs/tu-invalid-entity-id-value.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-VALUE
-// INVALID-ID-VALUE:      clang-ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-value.json'
+// INVALID-ID-VALUE:      clang-ssaf-linker: error: Linking artifact '{{.*}}tu-invalid-entity-id-value.json'
 // INVALID-ID-VALUE-NEXT: failed to read EntityId: expected JSON object with a single '@' key mapped to a number (unsigned 64-bit integer)
 
 // Entity ID reference in summary data blob pointing to an ID absent from the resolution table
 // RUN: not clang-ssaf-linker %S/Inputs/tu-invalid-entity-id-ref.json -o %t/lu.json 2>&1 \
 // RUN:   | FileCheck %s --match-full-lines --check-prefix=INVALID-ID-REF
-// INVALID-ID-REF:      clang-ssaf-linker: error: Linking summary '{{.*}}tu-invalid-entity-id-ref.json'
+// INVALID-ID-REF:      clang-ssaf-linker: error: Linking artifact '{{.*}}tu-invalid-entity-id-ref.json'
 // INVALID-ID-REF-NEXT: failed to patch EntityId: 'EntityId(99)' not found in entity resolution table
+
+// ============================================================================
+// Target triple mismatches
+//
+// The link unit's triple comes from the first input (or --target-triple), and
+// every later input must agree with it. EntityLinker reports the disagreement
+// and the command line adds the input's path as context.
+// ============================================================================
+
+// A TU summary disagreeing with a triple inferred from a preceding TU summary,
+// with no --target-triple involved at all.
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %S/Inputs/tu-x86_64.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=TRIPLE-INFERRED-TU
+// TRIPLE-INFERRED-TU: target triple 'x86_64-apple-macosx' of BuildNamespace(CompilationUnit, x86.cpp) does not match link unit target triple 'arm64-apple-macosx'
+
+// A static library disagreeing with a preceding input.
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %S/Inputs/lib-x86_64.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=TRIPLE-MISMATCH-LIB
+// TRIPLE-MISMATCH-LIB:      clang-ssaf-linker: error: Linking artifact '{{.*}}lib-x86_64.json'
+// TRIPLE-MISMATCH-LIB-NEXT: target triple 'x86_64-apple-macosx' of BuildNamespace(StaticLibrary, libmulti) does not match link unit target triple 'arm64-apple-macosx'
+
+// A TU summary disagreeing with a triple inferred from a preceding library,
+// rather than from a preceding TU summary or --target-triple.
+// RUN: not clang-ssaf-linker %S/Inputs/lib-x86_64.json %S/Inputs/tu-1.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=TRIPLE-FROM-LIB
+// TRIPLE-FROM-LIB: target triple 'arm64-apple-macosx' of BuildNamespace(CompilationUnit, tu1.cpp) does not match link unit target triple 'x86_64-apple-macosx'
+
+// A multi-arch static library carrying no member for the link unit's triple
+// reports the members it does carry.
+// RUN: not clang-ssaf-linker --target-triple arm64-apple-ios %S/Inputs/libtwo-2arch.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MEMBER-MISSING
+// MEMBER-MISSING:      clang-ssaf-linker: error: Linking artifact '{{.*}}libtwo-2arch.json'
+// MEMBER-MISSING-NEXT: multi-arch static library BuildNamespace(MultiArchStaticLibrary, libtwo) has no member for target triple 'arm64-apple-ios' (available: arm64-apple-macosx, x86_64-apple-macosx)
+
+// A bundle with no members at all still reports which triple was wanted.
+// RUN: not clang-ssaf-linker --target-triple arm64-apple-macosx %S/Inputs/libmulti-empty-wrapper.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=EMPTY-BUNDLE
+// EMPTY-BUNDLE: multi-arch static library BuildNamespace(MultiArchStaticLibrary, libmulti) has no member for target triple 'arm64-apple-macosx' (available: none)
+
+// ============================================================================
+// Target triple inference failures
+// ============================================================================
+
+// More than one member in the first input: the architecture has to be chosen.
+// RUN: not clang-ssaf-linker %S/Inputs/libtwo-2arch.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=AMBIGUOUS-MEMBERS
+// AMBIGUOUS-MEMBERS: cannot infer target triple from '{{.*}}libtwo-2arch.json': multi-arch static library has 2 members; pass --target-triple to select one
+
+// No members in the first input: there is nothing to infer from.
+// RUN: not clang-ssaf-linker %S/Inputs/libmulti-empty-wrapper.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=INFER-NO-MEMBERS
+// INFER-NO-MEMBERS: cannot infer target triple from '{{.*}}libmulti-empty-wrapper.json': multi-arch static library has no members; pass --target-triple
+
+// ============================================================================
+// Shared-library inputs are not linkable yet
+// ============================================================================
+
+// RUN: not clang-ssaf-linker %S/Inputs/lu-arm64.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=LU-FIRST
+// LU-FIRST: '{{.*}}lu-arm64.json' is a link unit summary: linking against shared libraries is not yet supported
+
+// RUN: not clang-ssaf-linker %S/Inputs/libfoo-2arch.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=MULTI-ARCH-SHARED-INPUT
+// MULTI-ARCH-SHARED-INPUT: '{{.*}}libfoo-2arch.json' is a multi-arch shared library: linking against shared libraries is not yet supported
+
+// Rejected wherever they appear, not just in first position.
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %S/Inputs/lu-arm64.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=LU-SECOND
+// LU-SECOND: '{{.*}}lu-arm64.json' is a link unit summary: linking against shared libraries is not yet supported
+
+// ============================================================================
+// Malformed --target-triple
+//
+// Only the architecture is validated; a vendor or OS spelled oddly is accepted
+// and simply means "unspecified", so linking.test covers those as acceptances
+// rather than rejections.
+// ============================================================================
+
+// RUN: not clang-ssaf-linker --target-triple bogus %S/Inputs/tu-1.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=BAD-TRIPLE-ARCH
+// BAD-TRIPLE-ARCH: clang-ssaf-linker: error: invalid --target-triple 'bogus': unrecognized architecture
+
+// ============================================================================
+// Error ordering: inputs are folded in as they are read, so the first input
+// that cannot be accepted is the one reported. These two runs share the same
+// three inputs in different orders and must report different errors.
+// ============================================================================
+
+// The triple mismatch at input 2 is reported; input 3's duplicate namespace is
+// never reached.
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %S/Inputs/lib-x86_64.json %S/Inputs/tu-1.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ORDER-TRIPLE-FIRST --implicit-check-not="duplicate BuildNamespace"
+// ORDER-TRIPLE-FIRST: target triple 'x86_64-apple-macosx' of BuildNamespace(StaticLibrary, libmulti) does not match link unit target triple 'arm64-apple-macosx'
+
+// The duplicate namespace at input 2 is reported; input 3's triple mismatch is
+// never reached.
+// RUN: not clang-ssaf-linker %S/Inputs/tu-1.json %S/Inputs/tu-1.json %S/Inputs/lib-x86_64.json -o %t/lu.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=ORDER-DUP-FIRST --implicit-check-not="does not match link unit target triple"
+// ORDER-DUP-FIRST: failed to link TU summary: duplicate BuildNamespace(CompilationUnit, tu1.cpp)
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/linking.test b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
index a6bd0c8df22c8..8708c4c64f003 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/linking.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/linking.test
@@ -40,3 +40,126 @@
 // RUN: cd %t && clang-ssaf-linker %S/Inputs/tu-1.json -o lu-1.json
 // RUN: diff %S/Outputs/lu-1.json %t/lu-1.json
 // RUN: rm %t/lu-1.json
+
+// ============================================================================
+// Static libraries are folded in as bundles of TU summaries
+//
+// The library below bundles tu-1 and tu-2, so linking it is equivalent to
+// passing those two TU summaries on the command line. Members are folded in TU
+// namespace order, which here matches the command line order above.
+// ============================================================================
+
+// Both libraries are derived from tu-1 and tu-2 rather than stored as fixtures:
+// static-library-create.test and multi-arch-create.test already pin what these
+// two commands produce, so a checked-in copy would duplicate them.
+// RUN: clang-ssaf-linker static-library create %S/Inputs/tu-1.json %S/Inputs/tu-2.json --namespace libtwo -o %t/libtwo.json
+// RUN: clang-ssaf-linker multi-arch create %t/libtwo.json -o %t/libtwo-1arch.json
+
+// RUN: clang-ssaf-linker %t/libtwo.json -o %t/lu-1+2.json
+// RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
+// RUN: rm %t/lu-1+2.json
+
+// A static library with no members contributes nothing, exactly as an empty TU
+// summary does.
+// RUN: clang-ssaf-linker %S/Inputs/lib-arm64.json -o %t/lu-empty.json
+// RUN: diff %S/Outputs/lu-empty.json %t/lu-empty.json
+// RUN: rm %t/lu-empty.json
+
+// Members are folded in TU namespace order, not in the order they happen to
+// appear in the library file. Inputs/libord-reversed.json lists tu-b.cpp before
+// tu-a.cpp, yet tu-a.cpp's entity is assigned the first entity ID.
+// RUN: clang-ssaf-linker %S/Inputs/libord-reversed.json -o %t/lu-ord.json
+// RUN: FileCheck %s --check-prefix=FOLD-ORDER --input-file=%t/lu-ord.json
+// FOLD-ORDER: "id_table": [
+// FOLD-ORDER: "id": 0,
+// FOLD-ORDER: "name": "tu-a.cpp"
+// FOLD-ORDER: "id": 1,
+// FOLD-ORDER: "name": "tu-b.cpp"
+// RUN: rm %t/lu-ord.json
+
+// Libraries and bare TU summaries mix freely in one invocation.
+// RUN: clang-ssaf-linker %S/Inputs/tu-empty.json %t/libtwo.json -o %t/lu-1+2.json
+// RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
+// RUN: rm %t/lu-1+2.json
+
+// ============================================================================
+// Multi-arch static libraries contribute the member matching the link unit
+// ============================================================================
+
+// A single-member bundle names its target unambiguously, so no --target-triple
+// is needed and the result matches linking the member directly.
+// RUN: clang-ssaf-linker %t/libtwo-1arch.json -o %t/lu-1+2.json
+// RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
+// RUN: rm %t/lu-1+2.json
+
+// A bundle in a later position is resolved against the triple the first input
+// already fixed, rather than contributing one of its own.
+// RUN: clang-ssaf-linker %S/Inputs/tu-empty.json %t/libtwo-1arch.json -o %t/lu-1+2.json
+// RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
+// RUN: rm %t/lu-1+2.json
+
+// Only the selected member is folded in. Inputs/libtwo-2arch.json pairs an
+// empty arm64 member with an x86_64 member that has a member, so selecting arm64
+// must produce an empty link unit -- folding the wrong member, or both, would
+// pull x86.cpp in.
+// RUN: clang-ssaf-linker --target-triple arm64-apple-macosx %S/Inputs/libtwo-2arch.json -o %t/lu-empty.json
+// RUN: diff %S/Outputs/lu-empty.json %t/lu-empty.json
+// RUN: rm %t/lu-empty.json
+
+// Members are matched on the triple's canonical components, not its spelling:
+// "aarch64" selects the "arm64" member. The link unit records the spelling that
+// was requested rather than the member's own.
+// RUN: clang-ssaf-linker --target-triple aarch64-apple-macosx %S/Inputs/libtwo-2arch.json -o %t/lu-aarch64.json
+// RUN: FileCheck %s --check-prefix=ALIAS-MEMBER --input-file=%t/lu-aarch64.json
+// ALIAS-MEMBER: "target_triple": "aarch64-apple-macosx",
+// RUN: rm %t/lu-aarch64.json
+
+// An OS version does not distinguish a member either, and the link unit again
+// records the requested spelling.
+// RUN: clang-ssaf-linker --target-triple arm64-apple-macosx15.0 %S/Inputs/libtwo-2arch.json -o %t/lu-versioned.json
+// RUN: FileCheck %s --check-prefix=VERSIONED-MEMBER --input-file=%t/lu-versioned.json
+// VERSIONED-MEMBER: "target_triple": "arm64-apple-macosx15.0",
+// RUN: rm %t/lu-versioned.json
+
+// An explicit --target-triple agreeing with the inputs is accepted and does not
+// change the result.
+// RUN: clang-ssaf-linker --target-triple arm64-apple-macosx %S/Inputs/tu-1.json %S/Inputs/tu-2.json -o %t/lu-1+2.json
+// RUN: diff %S/Outputs/lu-1+2.json %t/lu-1+2.json
+// RUN: rm %t/lu-1+2.json
+
+// ============================================================================
+// Target triples are not restricted to Apple platforms
+//
+// "unknown" and "none" are the conventional spellings for an unspecified
+// vendor or OS, so a triple that uses them is well formed and takes part in
+// triple resolution like any other.
+// ============================================================================
+
+// Inference needs no flag off-Apple either: the link unit adopts the input's
+// own triple and records it.
+// RUN: clang-ssaf-linker %S/Inputs/tu-linux.json -o %t/lu-linux.json
+// RUN: FileCheck %s --check-prefix=LINUX-INFERRED --input-file=%t/lu-linux.json
+// LINUX-INFERRED: "target_triple": "x86_64-unknown-linux-gnu",
+// RUN: rm %t/lu-linux.json
+
+// RUN: not clang-ssaf-linker --target-triple wasm32-unknown-unknown %S/Inputs/libtwo-2arch.json -o %t/lu-wasm.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=WASM-TRIPLE
+// WASM-TRIPLE: has no member for target triple 'wasm32-unknown-unknown' (available: arm64-apple-macosx, x86_64-apple-macosx)
+
+// A component that belongs in a later field is not mistaken for an
+// unrecognized OS: "elf" here names an object format, and the triple is
+// reported in the normalized spelling the link unit would record.
+// RUN: not clang-ssaf-linker --target-triple riscv64-unknown-elf %S/Inputs/lib-arm64.json -o %t/lu-riscv.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=RISCV-TRIPLE
+// RISCV-TRIPLE: does not match link unit target triple 'riscv64-unknown-unknown-elf'
+
+// A component dropped off the end of the triple is absent rather than
+// malformed: arm64-apple names an architecture and a vendor but no OS.
+// RUN: not clang-ssaf-linker --target-triple arm64-apple %S/Inputs/lib-arm64.json -o %t/lu-no-os.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-OS-TRIPLE
+// NO-OS-TRIPLE: does not match link unit target triple 'arm64-apple'
+
+// "none" is likewise a conventional spelling for an unspecified component.
+// RUN: not clang-ssaf-linker --target-triple armv7-none-eabi %S/Inputs/lib-arm64.json -o %t/lu-eabi.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=EABI-TRIPLE
+// EABI-TRIPLE: does not match link unit target triple 'armv7-unknown-none-eabi'
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/time.test b/clang/test/Analysis/Scalable/ssaf-linker/time.test
index 3e5809692f049..665198da4c4b6 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/time.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/time.test
@@ -10,8 +10,8 @@
 // CHECK-NEXT: ===-------------------------------------------------------------------------===
 // CHECK-NEXT:   Total Execution Time: {{[0-9.]+}} seconds ({{[0-9.]+}} wall clock)
 // CHECK:      {{.*}}---Wall Time---{{.*}}
-// CHECK-DAG:  {{.*}}Write Summary
-// CHECK-DAG:  {{.*}}Read Summaries
-// CHECK-DAG:  {{.*}}Link Summaries
+// CHECK-DAG:  {{.*}}Write Link Unit Summary
+// CHECK-DAG:  {{.*}}Read Artifacts
+// CHECK-DAG:  {{.*}}Link Artifacts
 // CHECK-DAG:  {{.*}}Validate Input
 // CHECK:      {{.*}}Total
diff --git a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
index d731748ad7547..57578792746da 100644
--- a/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
+++ b/clang/test/Analysis/Scalable/ssaf-linker/verbose.test
@@ -7,14 +7,55 @@
 // RUN:           | FileCheck %s --match-full-lines
 // CHECK:      note: - Linking started.
 // CHECK-NEXT: note:   - Validating input.
-// CHECK-NEXT: note:     - Validated output summary path '{{.*}}lu-1+2.json'.
-// CHECK-NEXT: note:     - Validated 2 input summary paths.
-// CHECK-NEXT: note:   - Linking input.
-// CHECK-NEXT: note:     - Constructing linker.
-// CHECK-NEXT: note:     - Linking summaries.
+// CHECK-NEXT: note:     - Validated output path '{{.*}}lu-1+2.json'.
+// CHECK-NEXT: note:     - Validated 2 input artifact path(s).
+// CHECK-NEXT: note:   - Creating link unit.
+// CHECK-NEXT: note:     - Linking artifacts.
 // CHECK-NEXT: note:       - [1/2] Reading '{{.*}}tu-1.json'.
+// CHECK-NEXT: note:       - Target triple: 'arm64-apple-macosx' (inferred from '{{.*}}tu-1.json').
 // CHECK-NEXT: note:       - [1/2] Linking '{{.*}}tu-1.json'.
 // CHECK-NEXT: note:       - [2/2] Reading '{{.*}}tu-2.json'.
 // CHECK-NEXT: note:       - [2/2] Linking '{{.*}}tu-2.json'.
-// CHECK-NEXT: note:     - Writing output summary to '{{.*}}lu-1+2.json'.
+// CHECK-NEXT: note:     - Linked 2 translation unit(s).
+// CHECK-NEXT: note:     - Target namespace: 'NestedBuildNamespace([BuildNamespace(LinkUnit, lu-1+2)])'.
+// CHECK-NEXT: note:   - Writing link unit summary to '{{.*}}lu-1+2.json'.
 // CHECK-NEXT: note: - Linking finished.
+
+// A library input reports how many members it contributes, and the closing
+// count is of translation units folded in, not of inputs on the command line.
+// Only the lines that differ from the run above are checked.
+
+// RUN: clang-ssaf-linker static-library create %S/Inputs/tu-1.json %S/Inputs/tu-2.json --namespace libtwo -o %t/libtwo.json
+// RUN: clang-ssaf-linker --verbose %t/libtwo.json %S/Inputs/lib-arm64.json -o %t/lu-libs.json 2>&1 \
+// RUN:           | FileCheck %s --match-full-lines --check-prefix=LIBS
+// LIBS:      note:       - [1/2] Linking '{{.*}}libtwo.json' (static library, 2 member(s)).
+// LIBS-NEXT: note:       - [2/2] Reading '{{.*}}lib-arm64.json'.
+// LIBS-NEXT: note:       - [2/2] Linking '{{.*}}lib-arm64.json' (static library, 0 member(s)).
+// LIBS-NEXT: note:     - Linked 2 translation unit(s).
+
+// A multi-arch input reports how many members it carries.
+
+// RUN: clang-ssaf-linker multi-arch create %t/libtwo.json -o %t/libtwo-1arch.json
+// RUN: clang-ssaf-linker --verbose %t/libtwo-1arch.json -o %t/lu-member.json 2>&1 \
+// RUN:           | FileCheck %s --match-full-lines --check-prefix=MEMBER
+// MEMBER:      note:       - [1/1] Reading '{{.*}}libtwo-1arch.json'.
+// MEMBER-NEXT: note:       - Target triple: 'arm64-apple-macosx' (inferred from '{{.*}}libtwo-1arch.json').
+// MEMBER-NEXT: note:       - [1/1] Linking '{{.*}}libtwo-1arch.json' (multi-arch static library, 1 member(s)).
+// MEMBER-NEXT: note:     - Linked 2 translation unit(s).
+
+// An explicit --target-triple is reported as such rather than as inferred.
+// Which member it selects is asserted by content in linking.test, not here; the
+// selected member of Inputs/libtwo-2arch.json is empty, hence no linked units.
+
+// RUN: clang-ssaf-linker --verbose --target-triple arm64-apple-macosx %S/Inputs/libtwo-2arch.json -o %t/lu-explicit.json 2>&1 \
+// RUN:           | FileCheck %s --check-prefix=EXPLICIT
+// EXPLICIT:      note:       - Target triple: 'arm64-apple-macosx' (from --target-triple).
+// EXPLICIT-NEXT: note:       - [1/1] Linking '{{.*}}libtwo-2arch.json' (multi-arch static library, 2 member(s)).
+// EXPLICIT-NEXT: note:     - Linked 0 translation unit(s).
+
+// A run that cannot fix a target triple reports no triple and links nothing.
+
+// RUN: not clang-ssaf-linker --verbose %S/Inputs/libtwo-2arch.json -o %t/lu-none.json 2>&1 \
+// RUN:   | FileCheck %s --check-prefix=NO-TRIPLE --implicit-check-not="Target triple"
+// NO-TRIPLE:      note:       - [1/1] Reading '{{.*}}libtwo-2arch.json'.
+// NO-TRIPLE-NEXT: error: cannot infer target triple from '{{.*}}libtwo-2arch.json': multi-arch static library has 2 members; pass --target-triple to select one
diff --git a/clang/tools/clang-ssaf-linker/CMakeLists.txt b/clang/tools/clang-ssaf-linker/CMakeLists.txt
index c51c1f25ff9cc..4527a9a27ee24 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
+  LinkCLI.cpp
   MultiArchCreateCLI.cpp
   StaticLibraryCreateCLI.cpp
   SSAFLinker.cpp
diff --git a/clang/tools/clang-ssaf-linker/LinkCLI.cpp b/clang/tools/clang-ssaf-linker/LinkCLI.cpp
new file mode 100644
index 0000000000000..1394aca11c49e
--- /dev/null
+++ b/clang/tools/clang-ssaf-linker/LinkCLI.cpp
@@ -0,0 +1,296 @@
+//===- LinkCLI.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 default (no subcommand) linking action. Inputs are read one
+//  at a time and folded into the link unit as they are read, so the first
+//  input that cannot be accepted is the one reported.
+//
+//  The target triple is fixed from the first input (or from --target-triple)
+//  before the linker is constructed. Validating every later input against it
+//  happens here rather than in EntityLinker: choosing which inputs belong to
+//  a target is a command line concern, and EntityLinker treats a mismatch as
+//  a fatal precondition violation.
+//
+//===----------------------------------------------------------------------===//
+
+#include "LinkCLI.h"
+
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/MultiArchSharedLibrary.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/Sequence.h"
+#include "llvm/Support/Path.h"
+#include <cassert>
+#include <memory>
+#include <utility>
+#include <variant>
+
+using namespace llvm;
+using namespace clang::ssaf;
+
+namespace path = llvm::sys::path;
+
+namespace {
+
+//===----------------------------------------------------------------------===//
+// Error Messages
+//===----------------------------------------------------------------------===//
+
+constexpr const char *ReadingArtifact = "Reading artifact '{0}'";
+
+constexpr const char *LinkingArtifact = "Linking artifact '{0}'";
+
+constexpr const char *NoInputs =
+    "no input artifacts: at least one input is required";
+
+constexpr const char *NoMembersToInferFrom =
+    "cannot infer target triple from '{0}': multi-arch static library has no "
+    "members; pass --target-triple";
+
+constexpr const char *AmbiguousMembersToInferFrom =
+    "cannot infer target triple from '{0}': multi-arch static library has {1} "
+    "members; pass --target-triple to select one";
+
+constexpr const char *UnsupportedSharedInput =
+    "'{0}' is a {1}: linking against shared libraries is not yet supported";
+
+constexpr const char *LinkUnitSummaryName = "link unit summary";
+constexpr const char *MultiArchSharedLibraryName = "multi-arch shared library";
+
+//===----------------------------------------------------------------------===//
+// ArtifactEncoding Helpers
+//===----------------------------------------------------------------------===//
+
+/// Returns the human readable kind of an artifact the linker cannot consume.
+///
+/// Only the shared-library family reaches this: every linkable alternative is
+/// handled before it is called. The static_assert makes a new alternative a
+/// compile error here rather than an unhandled case at runtime.
+llvm::StringRef unsupportedInputKindName(const ArtifactEncoding &E) {
+  static_assert(std::variant_size_v<ArtifactEncoding> == 5,
+                "unsupportedInputKindName must cover every ArtifactEncoding "
+                "alternative the linker cannot consume");
+
+  if (std::holds_alternative<LUSummaryEncoding>(E)) {
+    return LinkUnitSummaryName;
+  }
+
+  assert(
+      std::holds_alternative<MultiArchSharedLibrary>(E) &&
+      "linkable ArtifactEncoding alternatives must be handled by the caller");
+  return MultiArchSharedLibraryName;
+}
+
+} // namespace
+
+namespace clang::ssaf {
+
+void LinkCLI::run(llvm::TimerGroup &TG, llvm::ArrayRef<std::string> InputPaths,
+                  llvm::StringRef OutputPath, llvm::StringRef TargetTriple,
+                  bool Verbose, bool Time) {
+  this->InputPaths = InputPaths;
+  this->OutputPath = OutputPath;
+  this->TargetTriple = TargetTriple;
+  this->Verbose = Verbose;
+  this->Time = Time;
+
+  llvm::Timer TValidate("validate", "Validate Input", TG);
+  llvm::Timer TRead("read", "Read Artifacts", TG);
+  llvm::Timer TLink("link", "Link Artifacts", TG);
+  llvm::Timer TWrite("write", "Write Link Unit Summary", TG);
+
+  // Nesting depth for indenting verbose notes.
+  const unsigned Level = 0;
+
+  info(Verbose, Level, "Linking started.");
+
+  validate(Level + 1, TValidate);
+
+  LUSummaryEncoding Output = link(Level + 1, TRead, TLink);
+
+  write(Output, Level + 1, TWrite);
+
+  info(Verbose, Level, "Linking finished.");
+
+  // A second run() should start from a clean slate.
+  InputFiles.clear();
+  ExplicitTriple.reset();
+  ResolvedTriple.reset();
+}
+
+void LinkCLI::validate(unsigned Level, llvm::Timer &TValidate) {
+  info(Verbose, Level, "Validating input.");
+
+  llvm::TimeRegion _(Time ? &TValidate : nullptr);
+
+  OutputFile = FormatFile::fromOutputPath(OutputPath);
+  LinkUnitName = path::stem(OutputFile.Path).str();
+  info(Verbose, Level + 1, "Validated output path '{0}'.", OutputFile.Path);
+
+  if (InputPaths.empty()) {
+    fail(NoInputs);
+  }
+  for (const auto &InputPath : InputPaths) {
+    InputFiles.push_back(FormatFile::fromInputPath(InputPath));
+  }
+  info(Verbose, Level + 1, "Validated {0} input artifact path(s).",
+       InputFiles.size());
+
+  if (!TargetTriple.empty()) {
+    ExplicitTriple = parseTargetTripleOrFail("--target-triple", TargetTriple);
+  }
+}
+
+LUSummaryEncoding LinkCLI::link(unsigned Level, llvm::Timer &TRead,
+                                llvm::Timer &TLink) {
+  info(Verbose, Level, "Creating link unit.");
+
+  const unsigned InputLevel = Level + 1;
+  info(Verbose, InputLevel, "Linking artifacts.");
+
+  // The target triple comes from the first input, so it has to be read before
+  // the linker can be constructed.
+  constexpr size_t FirstIndex = 0;
+  ArtifactEncoding First = readInput(FirstIndex, InputLevel + 1, TRead);
+
+  ResolvedTriple = resolveTargetTriple(First, InputLevel + 1);
+
+  NestedBuildNamespace LUNamespace(
+      BuildNamespace(BuildNamespaceKind::LinkUnit, LinkUnitName));
+  EntityLinker EL(*ResolvedTriple, LUNamespace);
+
+  linkInput(EL, std::move(First), FirstIndex, InputLevel + 1, TLink);
+  for (size_t Index : llvm::seq<size_t>(FirstIndex + 1, InputFiles.size())) {
+    linkInput(EL, readInput(Index, InputLevel + 1, TRead), Index,
+              InputLevel + 1, TLink);
+  }
+
+  info(Verbose, InputLevel, "Linked {0} translation unit(s).",
+       EL.ProcessedTUNamespaces.size());
+  info(Verbose, InputLevel, "Target namespace: '{0}'.", LUNamespace);
+
+  return std::move(EL).takeOutput();
+}
+
+ArtifactEncoding LinkCLI::readInput(size_t Index, unsigned Level,
+                                    llvm::Timer &TRead) {
+  const FormatFile &InputFile = InputFiles[Index];
+  info(Verbose, Level, "[{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);
+}
+
+llvm::Triple LinkCLI::resolveTargetTriple(const ArtifactEncoding &First,
+                                          unsigned Level) {
+  if (ExplicitTriple) {
+    info(Verbose, Level, "Target triple: '{0}' (from --target-triple).",
+         *ExplicitTriple);
+    return *ExplicitTriple;
+  }
+
+  llvm::StringRef SourceFile = InputFiles[0].Path;
+
+  auto Inferred = [&]() -> llvm::Triple {
+    if (const auto *TU = std::get_if<TUSummaryEncoding>(&First)) {
+      return TU->getTargetTriple();
+    }
+
+    if (const auto *SL = std::get_if<StaticLibrary>(&First)) {
+      return SL->TargetTriple;
+    }
+
+    if (const auto *MASL = std::get_if<MultiArchStaticLibrary>(&First)) {
+      // A single member names the target unambiguously; anything else needs the
+      // architecture to be chosen on the command line.
+      if (MASL->Members.empty()) {
+        fail(NoMembersToInferFrom, SourceFile);
+      }
+      if (MASL->Members.size() > 1) {
+        fail(AmbiguousMembersToInferFrom, SourceFile, MASL->Members.size());
+      }
+      return (*MASL->Members.begin())->TargetTriple;
+    }
+
+    fail(UnsupportedSharedInput, SourceFile, unsupportedInputKindName(First));
+  }();
+
+  info(Verbose, Level, "Target triple: '{0}' (inferred from '{1}').", Inferred,
+       SourceFile);
+
+  return Inferred;
+}
+
+void LinkCLI::linkInput(EntityLinker &EL, ArtifactEncoding Encoding,
+                        size_t Index, unsigned Level, llvm::Timer &TLink) {
+  llvm::StringRef SourceFile = InputFiles[Index].Path;
+
+  auto foldIn = [&](llvm::Error Err) {
+    if (Err) {
+      fail(ErrorBuilder::wrap(std::move(Err))
+               .context(LinkingArtifact, SourceFile)
+               .build());
+    }
+  };
+
+  if (auto *TU = std::get_if<TUSummaryEncoding>(&Encoding)) {
+    info(Verbose, Level, "[{0}/{1}] Linking '{2}'.", Index + 1,
+         InputFiles.size(), SourceFile);
+    llvm::TimeRegion _(Time ? &TLink : nullptr);
+
+    foldIn(EL.link(std::make_unique<TUSummaryEncoding>(std::move(*TU))));
+    return;
+  }
+
+  if (auto *SL = std::get_if<StaticLibrary>(&Encoding)) {
+    info(Verbose, Level,
+         "[{0}/{1}] Linking '{2}' (static library, {3} member(s)).", Index + 1,
+         InputFiles.size(), SourceFile, SL->Members.size());
+    llvm::TimeRegion _(Time ? &TLink : nullptr);
+
+    foldIn(EL.link(std::make_unique<StaticLibrary>(std::move(*SL))));
+    return;
+  }
+
+  if (auto *MASL = std::get_if<MultiArchStaticLibrary>(&Encoding)) {
+    info(Verbose, Level,
+         "[{0}/{1}] Linking '{2}' (multi-arch static library, {3} member(s)).",
+         Index + 1, InputFiles.size(), SourceFile, MASL->Members.size());
+    llvm::TimeRegion _(Time ? &TLink : nullptr);
+
+    foldIn(EL.link(std::make_unique<MultiArchStaticLibrary>(std::move(*MASL))));
+    return;
+  }
+
+  fail(UnsupportedSharedInput, SourceFile, unsupportedInputKindName(Encoding));
+}
+
+void LinkCLI::write(const LUSummaryEncoding &Output, unsigned Level,
+                    llvm::Timer &TWrite) {
+  info(Verbose, Level, "Writing link unit summary to '{0}'.", OutputFile.Path);
+
+  llvm::TimeRegion _(Time ? &TWrite : nullptr);
+
+  if (auto Err =
+          OutputFile.Format->writeLUSummaryEncoding(Output, OutputFile.Path)) {
+    fail(std::move(Err));
+  }
+}
+
+} // namespace clang::ssaf
diff --git a/clang/tools/clang-ssaf-linker/LinkCLI.h b/clang/tools/clang-ssaf-linker/LinkCLI.h
new file mode 100644
index 0000000000000..678306c0ac4a7
--- /dev/null
+++ b/clang/tools/clang-ssaf-linker/LinkCLI.h
@@ -0,0 +1,106 @@
+//===- LinkCLI.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 the default (no subcommand) action of
+//  `clang-ssaf-linker`: linking TU summaries, static libraries, and members of
+//  multi-arch static libraries into one LU summary. There is no `link` verb or
+//  subcommand; the class name only mirrors its sibling action classes.
+//
+//  The class is intentionally independent of the tool's cl::opt globals.
+//  Every input it needs is passed to run(), so the class can be reused or
+//  unit-tested outside the driver.
+//
+//===----------------------------------------------------------------------===//
+
+#ifndef LLVM_CLANG_TOOLS_CLANG_SSAF_LINKER_LINKCLI_H
+#define LLVM_CLANG_TOOLS_CLANG_SSAF_LINKER_LINKCLI_H
+
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.h"
+#include "clang/ScalableStaticAnalysis/Core/EntityLinker/LUSummaryEncoding.h"
+#include "clang/ScalableStaticAnalysis/Core/Serialization/SerializationFormat.h"
+#include "clang/ScalableStaticAnalysis/Tool/Utils.h"
+#include "llvm/ADT/ArrayRef.h"
+#include "llvm/ADT/StringRef.h"
+#include "llvm/Support/Timer.h"
+#include "llvm/TargetParser/Triple.h"
+#include <cstddef>
+#include <optional>
+#include <string>
+#include <vector>
+
+namespace clang::ssaf {
+
+/// Runs the default linking action for `clang-ssaf-linker`.
+class LinkCLI {
+public:
+  /// Orchestrates validation, linking, and serialization of the LU summary.
+  /// Non-recoverable errors call fail() from Tool/Utils.h and terminate the
+  /// process.
+  void run(llvm::TimerGroup &TG, llvm::ArrayRef<std::string> InputPaths,
+           llvm::StringRef OutputPath, llvm::StringRef TargetTriple,
+           bool Verbose, bool Time);
+
+private:
+  /// Validates the output path and every input path, derives the link unit
+  /// name, and validates TargetTriple if it is set.
+  void validate(unsigned Level, llvm::Timer &TValidate);
+
+  /// Reads the inputs and folds each into one link unit, in command line
+  /// order.
+  ///
+  /// \returns The accumulated LU summary.
+  LUSummaryEncoding link(unsigned Level, llvm::Timer &TRead,
+                         llvm::Timer &TLink);
+
+  /// Reads the artifact at \p Index.
+  ArtifactEncoding readInput(size_t Index, unsigned Level, llvm::Timer &TRead);
+
+  /// Determines the link unit's target triple.
+  ///
+  /// An explicit --target-triple wins. Otherwise the triple is inferred from
+  /// \p First: its own for a TU summary or a static library, and its sole
+  /// member's for a single-member multi-arch static library. Any other shape
+  /// cannot be inferred from and requires --target-triple.
+  llvm::Triple resolveTargetTriple(const ArtifactEncoding &First,
+                                   unsigned Level);
+
+  /// Folds one input into \p EL, reporting whatever EntityLinker rejects --
+  /// including an input that does not belong to the resolved target -- with the
+  /// input's path as context.
+  void linkInput(EntityLinker &EL, ArtifactEncoding Encoding, size_t Index,
+                 unsigned Level, llvm::Timer &TLink);
+
+  /// Serializes the LU summary to the validated output path.
+  void write(const LUSummaryEncoding &Output, unsigned Level,
+             llvm::Timer &TWrite);
+
+  // Arguments captured by run() before dispatching to linking methods.
+  // InputPaths, OutputPath, and TargetTriple are non-owning: they alias the
+  // driver's cl::opt storage, which outlives the call.
+  llvm::ArrayRef<std::string> InputPaths;
+  llvm::StringRef OutputPath;
+  llvm::StringRef TargetTriple;
+  bool Verbose = false;
+  bool Time = false;
+
+  // State populated during validate() and consumed by later phases.
+  FormatFile OutputFile;
+  std::vector<FormatFile> InputFiles;
+  std::string LinkUnitName;
+
+  // Set by validate() only when TargetTriple was passed and valid.
+  std::optional<llvm::Triple> ExplicitTriple;
+
+  // The link unit's target triple, fixed by resolveTargetTriple() before the
+  // first input is folded in. Every later input must match it.
+  std::optional<llvm::Triple> ResolvedTriple;
+};
+
+} // namespace clang::ssaf
+
+#endif // LLVM_CLANG_TOOLS_CLANG_SSAF_LINKER_LINKCLI_H
diff --git a/clang/tools/clang-ssaf-linker/SSAFLinker.cpp b/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
index 9d72e9f338328..79fc34e4deb2d 100644
--- a/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
+++ b/clang/tools/clang-ssaf-linker/SSAFLinker.cpp
@@ -7,7 +7,8 @@
 //===----------------------------------------------------------------------===//
 //
 //  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
+//  link N inputs -- TU summaries, static libraries, and members of multi-arch
+//  static libraries -- 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
@@ -15,32 +16,21 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "LinkCLI.h"
 #include "MultiArchCreateCLI.h"
 #include "StaticLibraryCreateCLI.h"
 
-#include "clang/ScalableStaticAnalysis/Core/EntityLinker/EntityLinker.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/SSAFForceLinker.h" // IWYU pragma: keep
 #include "clang/ScalableStaticAnalysis/Tool/Utils.h"
-#include "llvm/ADT/STLExtras.h"
-#include "llvm/ADT/SmallVector.h"
 #include "llvm/Support/CommandLine.h"
-#include "llvm/Support/FormatVariadic.h"
 #include "llvm/Support/InitLLVM.h"
-#include "llvm/Support/Path.h"
 #include "llvm/Support/Timer.h"
-#include "llvm/Support/WithColor.h"
 #include "llvm/Support/raw_ostream.h"
-#include <memory>
 #include <string>
 
 using namespace llvm;
 using namespace clang::ssaf;
 
-namespace path = llvm::sys::path;
-
 namespace {
 
 //===----------------------------------------------------------------------===//
@@ -66,6 +56,14 @@ cl::opt<std::string> OutputPath("o", cl::desc("Output file path"),
                                 cl::value_desc("path"), cl::Required,
                                 cl::cat(SsafLinkerCategory));
 
+cl::opt<std::string> TargetTriple(
+    "target-triple",
+    cl::desc(
+        "Target triple of the link unit (defaults to the first input's; "
+        "required when the first input is a multi-arch static library with "
+        "several members)"),
+    cl::value_desc("triple"), cl::cat(SsafLinkerCategory));
+
 // --verbose and --time apply to every subcommand.
 cl::opt<bool> Verbose("verbose", cl::desc("Enable verbose output"),
                       cl::init(false), cl::cat(SsafLinkerCategory),
@@ -152,8 +150,6 @@ constexpr const char *MultiArchCreateVerb = "create";
 
 namespace LocalErrorMessages {
 
-constexpr const char *LinkingSummary = "Linking summary '{0}'";
-
 constexpr const char *UnknownStaticLibraryVerb =
     "unknown static-library verb '{0}': expected 'create'";
 
@@ -163,110 +159,12 @@ constexpr const char *UnknownMultiArchVerb =
 } // namespace LocalErrorMessages
 
 //===----------------------------------------------------------------------===//
-// link action
+// default (no subcommand) link action
 //===----------------------------------------------------------------------===//
 
-struct LinkerInput {
-  std::vector<FormatFile> InputFiles;
-  FormatFile OutputFile;
-  std::string LinkUnitName;
-};
-
-LinkerInput validateLinkInput(llvm::TimerGroup &TG) {
-  llvm::Timer TValidate("validate", "Validate Input", TG);
-  LinkerInput LI;
-
-  {
-    llvm::TimeRegion _(Time ? &TValidate : nullptr);
-
-    LI.OutputFile = FormatFile::fromOutputPath(OutputPath);
-    LI.LinkUnitName = path::stem(LI.OutputFile.Path).str();
-  }
-
-  info(Verbose, 2, "Validated output summary path '{0}'.", LI.OutputFile.Path);
-
-  {
-    llvm::TimeRegion _(Time ? &TValidate : nullptr);
-    for (const auto &InputPath : InputPaths) {
-      LI.InputFiles.push_back(FormatFile::fromInputPath(InputPath));
-    }
-  }
-
-  info(Verbose, 2, "Validated {0} input summary paths.", LI.InputFiles.size());
-
-  return LI;
-}
-
 void runLink(llvm::TimerGroup &TG) {
-  info(Verbose, 0, "Linking started.");
-
-  LinkerInput LI;
-  {
-    info(Verbose, 1, "Validating input.");
-    LI = validateLinkInput(TG);
-  }
-
-  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.
-  EntityLinker EL(llvm::Triple("arm64-apple-macosx"),
-                  NestedBuildNamespace(BuildNamespace(
-                      BuildNamespaceKind::LinkUnit, LI.LinkUnitName)));
-
-  llvm::Timer TRead("read", "Read Summaries", TG);
-  llvm::Timer TLink("link", "Link Summaries", TG);
-  llvm::Timer TWrite("write", "Write Summary", TG);
-
-  info(Verbose, 2, "Linking summaries.");
-
-  for (auto [Index, InputFile] : llvm::enumerate(LI.InputFiles)) {
-    std::unique_ptr<TUSummaryEncoding> Summary;
-
-    {
-      info(Verbose, 3, "[{0}/{1}] Reading '{2}'.", (Index + 1),
-           LI.InputFiles.size(), InputFile.Path);
-
-      llvm::TimeRegion _(Time ? &TRead : nullptr);
-
-      auto ExpectedSummaryEncoding =
-          InputFile.Format->readTUSummaryEncoding(InputFile.Path);
-      if (!ExpectedSummaryEncoding) {
-        fail(ExpectedSummaryEncoding.takeError());
-      }
-
-      Summary = std::make_unique<TUSummaryEncoding>(
-          std::move(*ExpectedSummaryEncoding));
-    }
-
-    {
-      info(Verbose, 3, "[{0}/{1}] Linking '{2}'.", (Index + 1),
-           LI.InputFiles.size(), InputFile.Path);
-
-      llvm::TimeRegion _(Time ? &TLink : nullptr);
-
-      if (auto Err = EL.link(std::move(Summary))) {
-        fail(ErrorBuilder::wrap(std::move(Err))
-                 .context(LocalErrorMessages::LinkingSummary, InputFile.Path)
-                 .build());
-      }
-    }
-  }
-
-  {
-    info(Verbose, 2, "Writing output summary to '{0}'.", LI.OutputFile.Path);
-
-    llvm::TimeRegion _(Time ? &TWrite : nullptr);
-
-    auto Output = std::move(EL).takeOutput();
-    if (auto Err = LI.OutputFile.Format->writeLUSummaryEncoding(
-            Output, LI.OutputFile.Path)) {
-      fail(std::move(Err));
-    }
-  }
-
-  info(Verbose, 0, "Linking finished.");
+  LinkCLI LC;
+  LC.run(TG, InputPaths, OutputPath, TargetTriple, Verbose, Time);
 }
 
 //===----------------------------------------------------------------------===//



More information about the cfe-commits mailing list