[llvm-branch-commits] [clang] [llvm] [clang][ssaf] Interpose ar in clang-ssaf-orchestrator-make (PR #232545)

Jan Korous via llvm-branch-commits llvm-branch-commits at lists.llvm.org
Thu Oct 15 16:55:23 PDT 2026


https://github.com/jkorous-apple created https://github.com/llvm/llvm-project/pull/232545

Add an ar shim so that static libraries built by the project take part
in the analysis. It records the members of each archive it writes and
links their summaries with clang-ssaf-linker static-library create. In
the transformation pass it merges their edits, as the ld shim does for a
link.

The ld shim now takes archives as inputs, so a program linked against an
in-project library includes the translation units behind it. The tests
now archive as well, so they also need an ar that works.

This relands 4d2bc2c3a7a4 (#231618), reverted with its stack in
782610a701b4 (#232274). The GN change comes from 73109fa8894a (#232113).

Assisted-by: Claude



>From d8d73e663910320040a0ec17be8ed3ea2006ab96 Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Thu, 15 Oct 2026 14:30:57 -0700
Subject: [PATCH] [clang][ssaf] Interpose ar in clang-ssaf-orchestrator-make

Add an ar shim so that static libraries built by the project take part
in the analysis. It records the members of each archive it writes and
links their summaries with clang-ssaf-linker static-library create. In
the transformation pass it merges their edits, as the ld shim does for a
link.

The ld shim now takes archives as inputs, so a program linked against an
in-project library includes the translation units behind it. The tests
now archive as well, so they also need an ar that works.

This relands 4d2bc2c3a7a4 (#231618), reverted with its stack in
782610a701b4 (#232274). The GN change comes from 73109fa8894a (#232113).

Assisted-by: Claude
---
 .../developer-docs/MakeOrchestratorShims.md   |  17 +-
 .../user-docs/MakeProjects.md                 |  20 +--
 .../ssaf-orchestrator-make/library-only.test  |  35 +++++
 .../link-unit-selection.test                  |  37 ++++-
 .../ssaf-orchestrator-make/link-units.test    |  54 +++++++
 .../ssaf-orchestrator-make/lit.local.cfg      |   9 +-
 .../prebuilt-archive.test                     |  44 ++++++
 .../CMakeLists.txt                            |   1 +
 .../clang-ssaf-orchestrator-make/README.md    |   2 +-
 .../clang_ssaf_orchestrator_make/config.py    |   1 +
 .../failure_logs.py                           |   1 +
 .../clang_ssaf_orchestrator_make/options.py   |   2 +-
 .../clang_ssaf_orchestrator_make/shims/ar.py  | 102 ++++++++++++
 .../shims/commands.py                         |   5 +
 .../clang_ssaf_orchestrator_make/shims/ld.py  |   3 +-
 .../tests/test_ar_shim.py                     | 148 ++++++++++++++++++
 .../tests/test_commands.py                    |  12 +-
 .../tests/test_ld_shim.py                     |  33 +++-
 .../clang-ssaf-orchestrator-make/BUILD.gn     |   1 +
 19 files changed, 494 insertions(+), 33 deletions(-)
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/library-only.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-units.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/prebuilt-archive.test
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py

diff --git a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
index f11c6d3cd3c48..0d6d3c10ef2e0 100644
--- a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
+++ b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
@@ -18,6 +18,7 @@ can spell the compiler several ways, so several names lead to the same shim:
 ```text
 cc, clang, c++, clang++  ->  clang
 ld                       ->  ld
+ar                       ->  ar
 ```
 
 It also writes the configuration for the shims into a JSON document and puts
@@ -48,11 +49,11 @@ split by what they do:
 
 ```text
 shim.py          the Shim class, its diagnostics, merge_edits
-commands.py      Files, is_object, artifact_of, summary_of,
+commands.py      Files, is_object, is_archive, artifact_of, summary_of,
                  filter_build_artifacts, unique
 driver_jobs.py   Job and the -### listing reader
 run.py           run_shim
-clang.py ld.py
+clang.py ld.py ar.py
 ```
 
 Every shim has the same shape, which `run_shim` implements: run the tool the
@@ -96,10 +97,11 @@ run it, and there is no SSAF step for its tool.
 
 ## Recording what the build assembled
 
-`ld` reads its command line for the artifact it produces and the artifacts it
-consumes, and records them in `<output-dir>/build-graph/<path>.buildstep.json`.
-The top-most link unit is found from these records, and they tell an artifact
-the build assembled from one it never touched.
+`ld` and `ar` each read their command line for the artifact it produces and
+the artifacts it consumes, and record them in
+`<output-dir>/build-graph/<path>.buildstep.json`. The top-most link unit is
+found from these records, and they tell an artifact the build assembled from
+one it never touched.
 
 Everything a shim writes is named for a build artifact's path plus a suffix,
 such as `ssaf-artifacts/<path>.summary.json`.
@@ -127,7 +129,8 @@ The kind says what went wrong; the kinds are the values of `SSAFFailureKind`:
 extract      summary extraction for a compilation
 transform    source-edit generation for a compilation
 link         clang-ssaf-linker over a link's inputs
-merge        clang-ssaf-src-edit-merge over a link's inputs
+archive      the same for an archive
+merge        clang-ssaf-src-edit-merge over a link's or an archive's inputs
 forward      handing a job to the shim that stands in for its tool
 missing      an artifact with nothing analysed behind it
 driver-jobs  the driver refused to list a command's jobs
diff --git a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
index 531c8c57d635e..648da537ae1d7 100644
--- a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
@@ -12,9 +12,9 @@ step:
 - A **compilation** gets a second compilation — the frontend job the build's
   own compiler said it would run, with the `--ssaf-*` options appended and
   nothing taken away, so extraction sees exactly what the build compiles.
-- A **link** gets a `clang-ssaf-linker` invocation over the summaries of what
-  went into it. It is not a copy of the build's command and carries no
-  `--ssaf-*` options.
+- An **archive** and a **link** get a `clang-ssaf-linker`
+  invocation over the summaries of what went into them. These are not copies
+  of the build's command and carry no `--ssaf-*` options.
 
 The mapping onto the SSAF tools is direct:
 
@@ -27,8 +27,10 @@ The mapping onto the SSAF tools is direct:
    * - ``$(CXX) -c -o parser.o parser.cpp``
      - ``clang -cc1 … -o parser.o -x c++ parser.cpp
        --ssaf-extract-summaries=…``
-   * - ``$(CXX) -o app main.o parser.o``
-     - ``clang-ssaf-linker main.json parser.json``
+   * - ``$(AR) rcs libparse.a parser.o lexer.o``
+     - ``clang-ssaf-linker static-library create parser.json lexer.json``
+   * - ``$(CXX) -o app main.o libparse.a``
+     - ``clang-ssaf-linker main.json libparse.json``
 ```
 
 ## Usage
@@ -199,10 +201,10 @@ either way.
 ## Interposition
 
 The orchestrator populates `<output-dir>/shims/` with links named after the
-tools a build invokes — `cc`, `clang`, `c++`, `clang++`, `ld`, `ld.lld` and
-`ld64.lld` — each pointing at the shim that stands in for it, and prepends
-that directory to `PATH`. It also puts the path of a configuration file in
-`SSAF_CONFIG`.
+tools a build invokes — `cc`, `clang`, `c++`, `clang++`, `ld`, `ld.lld`,
+`ld64.lld` and `ar` — each pointing at the shim that stands in for it, and
+prepends that directory to `PATH`. It also puts the path of a configuration
+file in `SSAF_CONFIG`.
 
 It changes nothing else in the build: no variable is replaced, so whatever the
 Makefile put in `CXX` — flags, a dialect, a define — arrives at the compiler
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/library-only.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/library-only.test
new file mode 100644
index 0000000000000..9ddf3bb8a22c3
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/library-only.test
@@ -0,0 +1,35 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# RUN: cd %t
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make 2>&1 \
+# RUN:   | FileCheck %s
+
+# CHECK: '{{[^ ]*}}/build/libx.a' is not a link unit
+# CHECK-SAME: build a target that links
+
+# The archive was summarised; it is simply not something to analyse.
+# RUN: cat %t/out/ssaf-artifacts%t/build/libx.a.summary.json \
+# RUN:   | FileCheck %s --check-prefix=LIB
+# LIB: "type": "StaticLibrary"
+
+#--- Makefile
+CXX = clang++
+all: build/libx.a
+build/a.o: src/a.cpp
+	$(CXX) -c -o $@ $<
+build/libx.a: build/a.o
+	$(AR) rcs $@ build/a.o
+
+#--- src/a.cpp
+int total(int *data, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += data[i];
+  return t;
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-unit-selection.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-unit-selection.test
index 735ea6b718566..5ec7da3020164 100644
--- a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-unit-selection.test
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-unit-selection.test
@@ -1,7 +1,7 @@
 
 # REQUIRES: ssaf-orchestrator-make-build
 # RUN: rm -rf %t && split-file %s %t
-# RUN: mkdir -p %t/two/build %t/none/build
+# RUN: mkdir -p %t/two/build %t/none/build %t/lib/build
 
 # RUN: cd %t/two
 # RUN: not %clang-ssaf-orchestrator-make \
@@ -22,6 +22,24 @@
 # PICKED: summaries: 2
 # PICKED: link-unit: {{[^ ]*}}/build/two
 
+# RUN: cd %t/lib
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/lib/out -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=LIB-AND-BINARY
+# LIB-AND-BINARY: produced 2 unconsumed outputs and none of them consumes the
+# LIB-AND-BINARY-SAME: others
+# LIB-AND-BINARY-SAME: {{[^ ]*}}/build/libextra.a
+# LIB-AND-BINARY-SAME: {{[^ ]*}}/build/prg
+
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/lib/out -- make build/prg \
+# RUN:   | FileCheck %s --check-prefix=LIB-PICKED
+# LIB-PICKED: link-unit: {{[^ ]*}}/build/prg
+
 # RUN: cd %t/none
 # RUN: not %clang-ssaf-orchestrator-make \
 # RUN:     --extract-summaries=PointerFlow \
@@ -46,6 +64,23 @@ int main() { return 0; }
 #--- two/src/two.cpp
 int main() { return 1; }
 
+#--- lib/Makefile
+CXX = clang++
+AR = ar
+all: build/prg build/libextra.a
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/main.o
+	$(CXX) -o $@ $<
+build/libextra.a: build/extra.o
+	$(AR) rcs $@ $<
+
+#--- lib/src/main.cpp
+int main() { return 0; }
+
+#--- lib/src/extra.cpp
+int extra(int *p) { return p[0]; }
+
 #--- none/Makefile
 CXX = clang++
 all: build/only.o
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-units.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-units.test
new file mode 100644
index 0000000000000..5f6b450c07f77
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-units.test
@@ -0,0 +1,54 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# RUN: cd %t
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make \
+# RUN:   | FileCheck %s --check-prefix=STDOUT
+
+# STDOUT: summaries: 5
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+# STDOUT: link-unit-summary: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.summary.json
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/libhelp.a.summary.json | FileCheck %s --check-prefix=LIB
+# LIB: "type": "StaticLibrary"
+# RUN: cat %t/out/ssaf-artifacts%t/build/prg.summary.json | FileCheck %s --check-prefix=LU
+# LU: "type": "LUSummary"
+
+# RUN: cat %t/out/build-graph%t/build/libhelp.a.buildstep.json \
+# RUN:   | FileCheck %s --check-prefix=UNIT-LIB
+# UNIT-LIB-DAG: build/helper.o
+# UNIT-LIB-DAG: build/other.o
+# RUN: cat %t/out/build-graph%t/build/prg.buildstep.json \
+# RUN:   | FileCheck %s --check-prefix=UNIT-PRG
+# UNIT-PRG-DAG: build/main.o
+# UNIT-PRG-DAG: build/libhelp.a
+
+#--- Makefile
+CXX = clang++
+LIBOBJS = build/helper.o build/other.o
+all: build/prg
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+build/libhelp.a: $(LIBOBJS)
+	$(AR) rcs $@ $(LIBOBJS)
+build/prg: build/main.o build/libhelp.a
+	$(CXX) -o $@ build/main.o build/libhelp.a
+
+#--- src/helper.cpp
+int helper(int *p) { return p[0]; }
+
+#--- src/other.cpp
+int other(int *p) { return p[1]; }
+
+#--- src/main.cpp
+int helper(int *p);
+int other(int *p);
+int main() {
+  int values[2] = {1, 2};
+  return helper(values) + other(values);
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
index 5c2bcb499b2c8..9a7d11f583cce 100644
--- a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
@@ -28,20 +28,23 @@ def _succeeds(command, stdin=None):
     return done is not None and done.returncode == 0
 
 
-# The tests build projects with make, so they need GNU make and a clang that
-# compiles and links C++ programs for the host.
+# The tests build projects with make, so they need GNU make, and a clang and
+# an ar that compile, archive and link C++ programs for the host.
 def _can_build_projects():
     path = config.environment["PATH"]
     make = lit.util.which("make", path)
     version = _run([make, "--version"]) if make else None
     if not version or version.returncode or not version.stdout.startswith(b"GNU Make"):
         return False
+    ar = lit.util.which("ar", path)
     with tempfile.TemporaryDirectory() as tmp:
         obj = os.path.join(tmp, "a.o")
         cxx = [config.clang, "--driver-mode=g++"]
         main = b"int main() { return 0; }\n"
         return (
-            _succeeds(cxx + ["-x", "c++", "-", "-c", "-o", obj], main)
+            ar is not None
+            and _succeeds(cxx + ["-x", "c++", "-", "-c", "-o", obj], main)
+            and _succeeds([ar, "rcs", os.path.join(tmp, "liba.a"), obj])
             and _succeeds(cxx + [obj, "-o", os.path.join(tmp, "a.out")])
         )
 
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/prebuilt-archive.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/prebuilt-archive.test
new file mode 100644
index 0000000000000..d903e2417f6f1
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/prebuilt-archive.test
@@ -0,0 +1,44 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# Built before the run, so the output directory holds nothing under its id.
+# RUN: %clang -c -o %t/third_party/vendor.o %t/third_party/vendor.c
+# RUN: ar rcs %t/third_party/libvendor.a %t/third_party/vendor.o
+
+# RUN: cd %t
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make \
+# RUN:   | FileCheck %s --check-prefix=STDOUT
+
+# STDOUT: summaries: 2
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+
+# RUN: not ls %t/out/failure-logs
+
+# RUN: cat %t/out/build-graph%t/build/prg.buildstep.json \
+# RUN:   | FileCheck %s --check-prefix=UNIT
+# UNIT-NOT: libvendor.a
+# UNIT: build/main.o
+# UNIT-NOT: libvendor.a
+
+#--- Makefile
+CC = clang
+all: build/prg
+build/main.o: src/main.c
+	$(CC) -c -o $@ $<
+build/prg: build/main.o
+	$(CC) -o $@ build/main.o third_party/libvendor.a
+
+#--- third_party/vendor.c
+int vendor(int *p) { return p[0]; }
+
+#--- src/main.c
+int vendor(int *p);
+int main(void) {
+  int values[2] = {1, 2};
+  return vendor(values);
+}
diff --git a/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt b/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt
index 27ebbfb36b1aa..aaedbb8c4f3aa 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt
+++ b/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt
@@ -14,6 +14,7 @@ set(SsafOrchestratorMakeLib
 
 set(SsafOrchestratorMakeShimLib
     "__init__.py"
+    "ar.py"
     "clang.py"
     "commands.py"
     "driver_jobs.py"
diff --git a/clang/tools/clang-ssaf-orchestrator-make/README.md b/clang/tools/clang-ssaf-orchestrator-make/README.md
index 04c2337d5db90..6a9b691a90ccf 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/README.md
+++ b/clang/tools/clang-ssaf-orchestrator-make/README.md
@@ -1,7 +1,7 @@
 # clang-ssaf-orchestrator-make
 
 Runs a Scalable Static Analysis Framework analysis over a project by riding
-along with its `make` build. Each compilation and link step the build
+along with its `make` build. Each compilation, archive and link step the build
 performs is paired with an SSAF step that produces the corresponding summary,
 and the summaries are linked into one whole-program view.
 
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
index 88e491f9f338d..bceca7b4907cb 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
@@ -47,6 +47,7 @@ def output_dir_marker_path(output_dir):
     "ld": "ld",
     "ld.lld": "ld",
     "ld64.lld": "ld",
+    "ar": "ar",
 }
 
 
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
index 84f52c5b3f9fe..80ca84d7f52a9 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
@@ -16,6 +16,7 @@ class SSAFFailureKind(enum.Enum):
     EXTRACT = "extract"
     TRANSFORM = "transform"
     LINK = "link"
+    ARCHIVE = "archive"
     MERGE = "merge"
     FORWARD = "forward"
     MISSING = "missing"
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
index 3a962f6274be0..3e720de577bf3 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
@@ -20,7 +20,7 @@ def build_parser():
         prog=PROGRAM,
         usage="%(prog)s [options] [--] <build command>",
         description="Run an SSAF analysis over a project as it builds. The "
-        "build is executed normally; each compilation and link step "
+        "build is executed normally; each compilation, archive and link step "
         "is paired with an SSAF step that produces the corresponding summary.",
     )
     parser.add_argument("--version", action="version", version="%(prog)s " + VERSION)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py
new file mode 100644
index 0000000000000..70237e0e6f888
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ar.py
@@ -0,0 +1,102 @@
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import os
+
+from .. import config, logging_diagnostics, utils
+from ..build_graph import BuildArtifactId
+from ..failure_logs import SSAFFailureKind
+from ..ssaf_artifact_repository import SSAFArtifactKind, get_ssaf_artifact_id
+from . import run
+from .commands import (
+    Files,
+    filter_build_artifacts,
+    is_archive,
+    is_object,
+    summary_of,
+    unique,
+)
+from .shim import merge_edits
+
+
+_CREATING_OPERATIONS = frozenset("rqm")
+
+
+def identify_inputs_output(argv):
+    args = []
+    index = 1
+    while index < len(argv):
+        arg = argv[index]
+        if arg in ("--plugin", "--target"):
+            index += 2
+            continue
+        if arg.startswith("--"):
+            index += 1
+            continue
+        args.append(arg)
+        index += 1
+
+    if len(args) < 2:
+        return None
+    if not set(args[0].lstrip("-")) & _CREATING_OPERATIONS:
+        return None
+    archive = args[1]
+    if not is_archive(archive):
+        return None
+    return Files([m for m in args[2:] if is_object(m)], archive)
+
+
+def invoke_ssaf_tools(shim, cwd):
+    files = identify_inputs_output(shim.argv)
+    if files is None:
+        logging_diagnostics.report_note(
+            shim.workflow_config.verbose, "not archive creation, no SSAF step to run"
+        )
+        return
+
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    output = os.path.join(cwd, files.output)
+    build_artifact_id = BuildArtifactId(output)
+    input_build_artifact_ids = filter_build_artifacts(
+        shim.build_graph, ssaf_artifact_repo, cwd, files.inputs
+    )
+
+    if shim.pass_config.pass_kind == config.SSAFPassKind.TRANSFORMATION:
+        merge_edits(shim, build_artifact_id, input_build_artifact_ids)
+        return
+
+    shim.build_graph.record_build_step(
+        build_artifact_id,
+        inputs=input_build_artifact_ids,
+    )
+    summaries = []
+    for each in input_build_artifact_ids:
+        found = summary_of(ssaf_artifact_repo, each)
+        if found:
+            summaries.append(found)
+        else:
+            shim.log_missing_summary(build_artifact_id, each)
+    summaries = unique(summaries)
+    if not summaries:
+        logging_diagnostics.report_note(
+            shim.workflow_config.verbose,
+            "no summaries feed %s, nothing to archive" % build_artifact_id.get_path(),
+        )
+        return
+    target = utils.make_parents(
+        ssaf_artifact_repo.get_path_for_artifact(
+            get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.SUMMARY)
+        )
+    )
+    shim.run(
+        [shim.pass_config.ssaf_linker, "static-library", "create"]
+        + summaries
+        + ["-o", target, "--namespace=" + os.path.basename(output)],
+        SSAFFailureKind.ARCHIVE,
+        build_artifact_id,
+    )
+
+
+def main(argv, env=None):
+    return run.run_shim(invoke_ssaf_tools, argv, env)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
index d4794cb4e734c..f290ae9097797 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
@@ -11,12 +11,17 @@
 Files = collections.namedtuple("Files", "inputs output")
 
 _OBJECT_EXTENSIONS = frozenset([".o", ".obj"])
+_ARCHIVE_EXTENSIONS = frozenset([".a"])
 
 
 def is_object(filename):
     return os.path.splitext(filename)[1] in _OBJECT_EXTENSIONS
 
 
+def is_archive(filename):
+    return os.path.splitext(filename)[1] in _ARCHIVE_EXTENSIONS
+
+
 def artifact_of(ssaf_artifact_repo, build_artifact_id, ssaf_artifact_kind):
     ssaf_artifact_id = get_ssaf_artifact_id(build_artifact_id, ssaf_artifact_kind)
     if not ssaf_artifact_repo.has_artifact(ssaf_artifact_id):
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
index 54c5c759ed5bf..3d1e5924623ed 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
@@ -12,6 +12,7 @@
 from .commands import (
     Files,
     filter_build_artifacts,
+    is_archive,
     is_object,
     summary_of,
     unique,
@@ -29,7 +30,7 @@ def identify_inputs_output(argv):
             output = argv[index + 1]
             index += 2
             continue
-        if not arg.startswith("-") and is_object(arg):
+        if not arg.startswith("-") and (is_object(arg) or is_archive(arg)):
             inputs.append(arg)
         index += 1
     return Files(inputs, output) if inputs else None
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py
new file mode 100644
index 0000000000000..cfe3daaceabf3
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ar_shim.py
@@ -0,0 +1,148 @@
+# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
+# See https://llvm.org/LICENSE.txt for license information.
+# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
+
+import os
+import shutil
+import tempfile
+import unittest
+
+from clang_ssaf_orchestrator_make import utils
+from clang_ssaf_orchestrator_make.failure_logs import SSAFFailureKind
+from clang_ssaf_orchestrator_make.shims import ar
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+from clang_ssaf_orchestrator_make import build_graph
+
+from . import configs
+
+
+class SsafStep(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.root = os.path.join(self.tmp, "project")
+        self.out = os.path.join(self.tmp, "out")
+        os.makedirs(os.path.join(self.root, "build"))
+
+    def _shim(self, argv, pass_config=None):
+        return configs.RecordingShim(argv, self.out, pass_config)
+
+    def _id(self, relative):
+        return build_graph.BuildArtifactId(os.path.join(self.root, relative))
+
+    def _summary(self, relative):
+        records = configs.repository_for(self.out)
+        open(
+            utils.make_parents(
+                records.get_path_for_artifact(
+                    repository.get_ssaf_artifact_id(
+                        build_graph.BuildArtifactId(os.path.join(self.root, relative)),
+                        repository.SSAFArtifactKind.SUMMARY,
+                    )
+                )
+            ),
+            "w",
+        ).close()
+
+    def test_a_command_that_creates_no_archive_runs_no_step(self):
+        shim = self._shim(["ar", "t", "build/libx.a"])
+        with configs.recorded_notes() as notes:
+            ar.invoke_ssaf_tools(shim, self.root)
+        self.assertEqual([], shim.commands)
+        self.assertIn("not archive creation", notes[0])
+
+    def test_a_member_the_run_never_touched_is_not_an_input(self):
+        shim = self._shim(["ar", "rcs", "build/libx.a", "build/a.o"])
+        ar.invoke_ssaf_tools(shim, self.root)
+        self.assertEqual([], shim.commands)
+        self.assertEqual([], shim.missing)
+        libx = self._id("build/libx.a")
+        self.assertTrue(configs.build_graph_for(self.out).was_built(libx))
+        self.assertEqual(
+            [],
+            configs.build_graph_for(self.out).get_recorded_build_steps()[
+                libx.get_path()
+            ],
+        )
+
+    def test_the_summaries_that_exist_are_handed_to_the_linker(self):
+        self._summary("build/a.o")
+        shim = self._shim(["ar", "rcs", "build/libx.a", "build/a.o"])
+        ar.invoke_ssaf_tools(shim, self.root)
+        self.assertEqual(1, len(shim.commands))
+        failure, command = shim.commands[0]
+        self.assertEqual((SSAFFailureKind.ARCHIVE, self._id("build/libx.a")), failure)
+        self.assertEqual(
+            ["/bin/clang-ssaf-linker", "static-library", "create"], command[:3]
+        )
+        self.assertIn("--namespace=libx.a", command)
+
+    def test_the_namespace_is_the_archives_file_name_not_its_path(self):
+        self._summary("build/a.o")
+        shim = self._shim(["ar", "rcs", "build/libx.a", "build/a.o"])
+        ar.invoke_ssaf_tools(shim, self.root)
+        _, command = shim.commands[0]
+        self.assertNotIn("--namespace=build/libx.a", command)
+
+    def test_a_member_that_is_not_an_object_is_not_an_input(self):
+        self._summary("build/a.o")
+        shim = self._shim(["ar", "rcs", "build/libx.a", "build/a.o", "README"])
+        ar.invoke_ssaf_tools(shim, self.root)
+        self.assertEqual([], shim.missing)
+        self.assertEqual(
+            [self._id("build/a.o").get_path()],
+            configs.build_graph_for(self.out).get_recorded_build_steps()[
+                self._id("build/libx.a").get_path()
+            ],
+        )
+
+    def test_in_the_transformation_pass_the_edits_are_merged_instead(self):
+        shim = self._shim(
+            ["ar", "rcs", "build/libx.a", "build/a.o"],
+            configs.transformation_config(),
+        )
+        with configs.recorded_notes() as notes:
+            ar.invoke_ssaf_tools(shim, self.root)
+        self.assertEqual(
+            [
+                "no edits feed %s, nothing to merge"
+                % self._id("build/libx.a").get_path()
+            ],
+            notes,
+        )
+
+
+class Archive(unittest.TestCase):
+    def test_archive(self):
+        files = ar.identify_inputs_output(["ar", "rcs", "libx.a", "a.o", "b.o"])
+        self.assertEqual("libx.a", files.output)
+        self.assertEqual(["a.o", "b.o"], files.inputs)
+
+    def test_leading_dash_operation(self):
+        files = ar.identify_inputs_output(["ar", "-rcs", "libx.a", "a.o"])
+        self.assertEqual(["a.o"], files.inputs)
+
+    def test_plugin_option_is_not_the_operation(self):
+        files = ar.identify_inputs_output(
+            ["ar", "--plugin", "/usr/lib/liblto.so", "rcs", "libx.a", "a.o"]
+        )
+        self.assertEqual(["a.o"], files.inputs)
+
+    def test_non_creating_operations_read_as_nothing(self):
+        self.assertIsNone(ar.identify_inputs_output(["ar", "t", "libx.a"]))
+
+    def test_every_creating_operation_is_recognised(self):
+        for operation in ("r", "q", "m"):
+            files = ar.identify_inputs_output(["ar", operation + "cs", "libx.a", "a.o"])
+            self.assertEqual(["a.o"], files.inputs, operation)
+            self.assertEqual("libx.a", files.output, operation)
+
+    def test_non_archive_output_reads_as_nothing(self):
+        self.assertIsNone(ar.identify_inputs_output(["ar", "rcs", "notlib", "a.o"]))
+
+    def test_too_few_arguments(self):
+        self.assertIsNone(ar.identify_inputs_output(["ar", "rcs"]))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
index f7d28f2af2a8a..1db814be328f3 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
@@ -21,6 +21,12 @@ def test_object_extensions(self):
         for name in ("a.a", "a.c", "a", "a.o.tmp", "a.O"):
             self.assertFalse(commands.is_object(name), name)
 
+    def test_archive_extensions(self):
+        for name in ("libx.a", "build/libx.a"):
+            self.assertTrue(commands.is_archive(name), name)
+        for name in ("libx.so", "libx.dylib", "libx", "libx.a.tmp"):
+            self.assertFalse(commands.is_archive(name), name)
+
 
 class Unique(unittest.TestCase):
     def test_the_first_of_each_is_kept_in_order(self):
@@ -104,11 +110,11 @@ def test_an_object_with_only_source_edits_is_kept(self):
         self._write("build/a.o", repository.SSAFArtifactKind.EDITS)
         self.assertEqual([self._id("build/a.o")], self._kept("build/a.o"))
 
-    def test_an_assembled_object_with_only_a_record_is_kept(self):
+    def test_an_archive_with_only_a_record_is_kept(self):
         self.build_graph.record_build_step(
-            self._id("build/combined.o"), [self._id("build/a.o")]
+            self._id("build/x.a"), [self._id("build/a.o")]
         )
-        self.assertEqual([self._id("build/combined.o")], self._kept("build/combined.o"))
+        self.assertEqual([self._id("build/x.a")], self._kept("build/x.a"))
 
     def test_what_the_build_never_touched_is_dropped(self):
         self.assertEqual([], self._kept("third_party/prebuilt.a", "/usr/lib/libz.a"))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
index a6ae6c1872375..b4f802fc8ae07 100644
--- a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
@@ -16,15 +16,21 @@
 
 
 class Inputs(unittest.TestCase):
-    def test_objects(self):
-        files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", "b.o", "-lm"])
-        self.assertEqual(["a.o", "b.o"], files.inputs)
+    def test_objects_and_archives(self):
+        files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", "libx.a", "-lm"])
+        self.assertEqual(["a.o", "libx.a"], files.inputs)
         self.assertEqual("prg", files.output)
 
-    def test_any_object_is_named_whatever_precedes_it(self):
+    def test_any_object_or_archive_is_named_whatever_precedes_it(self):
         for option in ("-force_load", "-weak_library", "-filelist", "-bundle"):
-            files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", option, "b.o"])
-            self.assertEqual(["a.o", "b.o"], files.inputs, option)
+            files = ld.identify_inputs_output(
+                ["ld", "-o", "prg", "a.o", option, "libx.a"]
+            )
+            self.assertEqual(["a.o", "libx.a"], files.inputs, option)
+
+    def test_an_option_value_that_looks_like_an_archive_is_named_as_one(self):
+        files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", "-l", "x.a"])
+        self.assertEqual(["a.o", "x.a"], files.inputs)
 
     def test_a_value_with_no_object_extension_is_not_named(self):
         files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", "-l", "x"])
@@ -102,7 +108,7 @@ def _recorded_inputs(self):
 
     def test_only_what_the_run_produced_is_linked(self):
         self._summary("build/a.o")
-        shim = self._run(["build/a.o", "third_party/prebuilt.o", "/usr/lib/crt1.o"])
+        shim = self._run(["build/a.o", "third_party/prebuilt.a", "/usr/lib/libz.a"])
         self.assertEqual([self._id("build/a.o").get_path()], self._recorded_inputs())
         self.assertEqual([], shim.missing)
         _, command = shim.commands[0]
@@ -121,6 +127,19 @@ def test_an_lto_scratch_object_is_not_an_input(self):
         self._run(["build/a.o", "-object_path_lto", "/tmp/lto.o"])
         self.assertEqual([self._id("build/a.o").get_path()], self._recorded_inputs())
 
+    def test_an_option_value_that_looks_like_an_archive_is_not_an_input(self):
+        self._summary("build/a.o")
+        self._run(["build/a.o", "-l", "x.a"])
+        self.assertEqual([self._id("build/a.o").get_path()], self._recorded_inputs())
+
+    def test_an_archive_with_nothing_behind_it_is_reported(self):
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId(os.path.join(self.root, "build/libx.a")),
+            [self._id("build/a.o")],
+        )
+        shim = self._run(["build/libx.a"])
+        self.assertEqual([self._id("build/libx.a").get_path()], shim.missing)
+
     def test_a_link_of_nothing_we_know_runs_no_step(self):
         shim = self._run(["build/a.o"])
         self.assertEqual([], shim.commands)
diff --git a/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn b/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn
index f79551c068775..a2a32d747f010 100644
--- a/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn
@@ -46,6 +46,7 @@ write_cmake_config("version") {
 copy("shims") {
   sources = [
     "lib/clang_ssaf_orchestrator_make/shims/__init__.py",
+    "lib/clang_ssaf_orchestrator_make/shims/ar.py",
     "lib/clang_ssaf_orchestrator_make/shims/clang.py",
     "lib/clang_ssaf_orchestrator_make/shims/commands.py",
     "lib/clang_ssaf_orchestrator_make/shims/driver_jobs.py",



More information about the llvm-branch-commits mailing list