[clang] [clang][ssaf] Add clang-ssaf-orchestrator-make (PR #231583)

Jan Korous via cfe-commits cfe-commits at lists.llvm.org
Tue Oct 13 10:03:17 PDT 2026


https://github.com/jkorous-apple updated https://github.com/llvm/llvm-project/pull/231583

>From 79484b74a0091861450f7622a4aa0315fdf85eea Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Fri, 9 Oct 2026 19:09:50 -0700
Subject: [PATCH] [clang][ssaf] Add clang-ssaf-orchestrator-make

Add a tool that runs an SSAF workflow over a project built with make. It
extracts a summary for every translation unit, links the summaries the
way the build links the objects, and can run whole-program analysis on
the result.

The tool works alongside the build rather than replacing it. It puts a
directory of links named cc, clang, c++, clang++ and ld at the front of
PATH, all pointing to one shim program. Each shim runs the build's own
tool unchanged and then the matching SSAF step. A compiler that PATH
does not reach, such as one named by an absolute path or run through a
launcher, is not analyzed.

The clang shim asks the driver with -### how a command decomposes into
jobs, and runs each frontend job again with the SSAF clang and the
--ssaf-* options. The ld shim records which artifacts each link consumed
and links their summaries with clang-ssaf-linker. The output that
nothing else consumes is the link unit; with --analysis,
clang-ssaf-analyzer runs over its summary.

A failed SSAF step never fails the build. It is logged, and the run
exits with an error once the build is done.

The tool is documented in
clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md and its
design in developer-docs/MakeOrchestratorShims.md. Unit tests cover the
internals, and lit tests run make over small projects; the lit suite
also runs the unit tests.
---
 .../developer-docs/MakeOrchestratorShims.md   | 133 ++++++
 .../user-docs/MakeProjects.md                 | 252 +++++++++++
 .../ssaf-orchestrator-make/bad-args.test      |  13 +
 .../ssaf-orchestrator-make/bare-linker.test   |  46 ++
 .../build-failures.test                       |  35 ++
 .../extract-summaries.test                    |  67 +++
 .../failure-handling.test                     |  78 ++++
 .../Scalable/ssaf-orchestrator-make/help.test |  27 ++
 .../incremental-tree.test                     |  58 +++
 .../ssaf-orchestrator-make/interposition.test |  88 ++++
 .../ssaf-orchestrator-make/link-driver.test   |  30 ++
 .../link-unit-selection.test                  |  56 +++
 .../ssaf-orchestrator-make/lit.local.cfg      |  62 +++
 .../path-interposition.test                   |  42 ++
 .../recursive-make.test                       |  61 +++
 .../same-basename-objects.test                |  61 +++
 .../shim-outside-a-run.test                   |  17 +
 .../ssaf-orchestrator-make/unittests.test     |   8 +
 .../unreachable-compiler.test                 |  27 ++
 .../whole-program-analysis.test               |  59 +++
 clang/test/CMakeLists.txt                     |   1 +
 clang/tools/CMakeLists.txt                    |   1 +
 .../CMakeLists.txt                            |  94 ++++
 .../clang-ssaf-orchestrator-make/README.md    |  51 +++
 .../bin/clang-ssaf-orchestrator-make          |  15 +
 .../clang_ssaf_orchestrator_make/__init__.py  |  10 +
 .../build_graph.py                            |  73 +++
 .../clang_ssaf_orchestrator_make/config.py    | 115 +++++
 .../config_orchestrator.py                    | 142 ++++++
 .../config_shims.py                           |  31 ++
 .../failure_logs.py                           |  48 ++
 .../interposition.py                          |  31 ++
 .../logging_diagnostics.py                    |  41 ++
 .../clang_ssaf_orchestrator_make/options.py   | 147 +++++++
 .../orchestrator.py                           | 211 +++++++++
 .../shims/__init__.py                         |   3 +
 .../shims/clang.py                            | 128 ++++++
 .../shims/commands.py                         |  49 +++
 .../shims/driver_jobs.py                      |  58 +++
 .../clang_ssaf_orchestrator_make/shims/ld.py  |  88 ++++
 .../clang_ssaf_orchestrator_make/shims/run.py |  23 +
 .../shims/shim.py                             | 183 ++++++++
 .../ssaf_artifact_repository.py               |  58 +++
 .../lib/clang_ssaf_orchestrator_make/utils.py |  54 +++
 .../version.py.in                             |   5 +
 .../clang-ssaf-orchestrator-make/shims/shim   |  45 ++
 .../tests/__init__.py                         |   3 +
 .../tests/configs.py                          |  84 ++++
 .../tests/test_build_graph.py                 | 222 ++++++++++
 .../tests/test_clang_shim.py                  | 244 +++++++++++
 .../tests/test_commands.py                    | 114 +++++
 .../tests/test_config.py                      |  86 ++++
 .../tests/test_config_orchestrator.py         | 348 +++++++++++++++
 .../tests/test_config_shims.py                |  63 +++
 .../tests/test_driver_jobs.py                 | 150 +++++++
 .../tests/test_failure_logs.py                |  93 ++++
 .../tests/test_interposition.py               |  69 +++
 .../tests/test_ld_shim.py                     | 132 ++++++
 .../tests/test_logging_diagnostics.py         | 101 +++++
 .../tests/test_options.py                     |  82 ++++
 .../tests/test_orchestrator.py                | 354 +++++++++++++++
 .../tests/test_run.py                         |  84 ++++
 .../tests/test_shim.py                        | 414 ++++++++++++++++++
 .../tests/test_ssaf_artifact_repository.py    | 102 +++++
 .../tests/test_utils.py                       |  94 ++++
 65 files changed, 5764 insertions(+)
 create mode 100644 clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
 create mode 100644 clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/bad-args.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/bare-linker.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/build-failures.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/extract-summaries.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/failure-handling.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/incremental-tree.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/interposition.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-unit-selection.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/path-interposition.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/recursive-make.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/same-basename-objects.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/shim-outside-a-run.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/unittests.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/unreachable-compiler.test
 create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/whole-program-analysis.test
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/README.md
 create mode 100755 clang/tools/clang-ssaf-orchestrator-make/bin/clang-ssaf-orchestrator-make
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/__init__.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/build_graph.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/interposition.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/__init__.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/driver_jobs.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/run.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/utils.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/version.py.in
 create mode 100755 clang/tools/clang-ssaf-orchestrator-make/shims/shim
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/__init__.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_build_graph.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_config_shims.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_driver_jobs.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_failure_logs.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_interposition.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_run.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_shim.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py
 create mode 100644 clang/tools/clang-ssaf-orchestrator-make/tests/test_utils.py

diff --git a/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
new file mode 100644
index 00000000000000..c815a30b0425d5
--- /dev/null
+++ b/clang/docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md
@@ -0,0 +1,133 @@
+# Make Orchestrator Shims
+
+`clang-ssaf-orchestrator-make` analyses a project by riding along with its
+build. It does that with a set of small programs — *shims* — that stand in
+for the tools the build invokes. This describes how they are selected, what
+each one does, and how they learn what run they are part of; the user-facing
+behaviour is in {doc}`../user-docs/MakeProjects`.
+
+## Setting up the build's environment
+
+At the start of a run the orchestrator does two things to the environment the
+build will see.
+
+It fills `<output-dir>/shims/` with links named after the build tools, all
+pointing at one shim program, and prepends that directory to `PATH`. A build
+can spell the compiler several ways, so several names lead to the same shim:
+
+```text
+cc, clang, c++, clang++  ->  clang
+ld                       ->  ld
+```
+
+It also writes the configuration for the shims into a JSON document and puts
+that document's path in `SSAF_CONFIG`. A shim that finds no `SSAF_CONFIG`
+reports an error and exits without running anything.
+
+## Running the binary tool
+
+A shim has to run the tool the build asked for, by the name the build spelled,
+but on the modified `PATH` that name finds the shim itself.
+`SSAFWorkflowConfig.restored_path_env_var` therefore holds the build's `PATH`
+as it was before the shim link directory went in front of it.
+`Shim.bin_tool_path` looks the tool up on that `PATH`, and
+`Shim.get_unmodified_env` gives the tool an environment with that `PATH`
+restored and `SSAF_CONFIG` removed.
+
+## The shim program
+
+`libexec/ssaf-orchestrator-shims/` holds one program, `shim`, and every link
+points at it. It puts the package on `sys.path`, looks up the name it was
+invoked under in `SHIM_FOR_LINK_NAME`, the table the links were made from, and
+imports the module that stands in for that tool. It is a file rather than a
+package module because it is symlinked under several names, and `argv[0]` is
+what tells it which tool it stands in for.
+
+The shim bodies live in the package, as `clang_ssaf_orchestrator_make.shims`,
+split by what they do:
+
+```text
+shim.py          the Shim class and its diagnostics
+commands.py      Files, is_object, artifact_of, summary_of,
+                 filter_build_artifacts, unique
+driver_jobs.py   Job and the -### listing reader
+run.py           run_shim
+clang.py ld.py
+```
+
+Every shim has the same shape, which `run_shim` implements: run the tool the
+build asked for, then do the SSAF work. A failure in the first half is the
+build's own. A failure in the second is logged, and the build carries on.
+
+## The clang shim has two roles
+
+One program serves both, and a marker on the command line tells them apart.
+
+**Invoked as the clang driver.** The build spelled `cc`, `clang`, `c++` or
+`clang++`, and `PATH` found the link. The shim runs the binary tool on the
+build's command line untouched, asks it with `-###` how that command
+decomposes into jobs, and forwards each job to the shim that stands in for its
+tool.
+
+**Invoked as cc1 by the shim's driver role.** A forwarded job ends in
+`--ssaf-shim-skip-binary-tool-invocation`, which only the driver role appends.
+The binary tool has therefore already run, and the shim does only the SSAF
+work: it runs the SSAF clang over the job with the `--ssaf-*` options added.
+`Shim.__init__` removes the marker as the command enters the shim, so nothing
+downstream sees it.
+
+## Matching a job to a shim
+
+For each job in the driver's `-###` listing, take the program the job names,
+strip its directory and any trailing `-<digits>`, and look the result up among
+the links:
+
+```text
+/…/toolchains/…/usr/bin/clang   ->  clang     ->  <output-dir>/shims/clang
+/…/build/bin/clang-<N>          ->  clang     ->  <output-dir>/shims/clang
+/usr/bin/ld                     ->  ld        ->  <output-dir>/shims/ld
+/usr/bin/dsymutil               ->  dsymutil  ->  no link; nothing stands in
+```
+
+The suffix is stripped because a driver names its own binary `clang` when
+installed and `clang-<N>`, after its major version, in a build tree. A job
+with no matching link is noted and skipped: the build's compiler has already
+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.
+
+Everything a shim writes is named for a build artifact's path plus a suffix,
+such as `ssaf-artifacts/<path>.summary.json`.
+
+## Failure logs
+
+A failure log is a diagnostic about a command rather than something the
+command produced, so it lives apart, under `failure-logs/`. The run counts
+every log at its end, which is how a failure a shim logged and moved past
+still fails the run.
+
+A log is named for the artifact it concerns: the output of the command that
+failed, or the input whose summary is missing. A command that names no output,
+such as `clang -c a.c b.c`, and an internal error in a shim are logged instead
+under the directory the command ran in, named for a hash of the command:
+
+```text
+failure-logs/<path>.<kind>.log        named for an artifact
+failure-logs/<dir>/<hash>.<kind>.log  named for a command
+```
+
+The kind says what went wrong; the kinds are the values of `SSAFFailureKind`:
+
+```text
+extract      summary extraction for a compilation
+link         clang-ssaf-linker over a link'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
+internal     an unhandled exception inside a shim
+```
diff --git a/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
new file mode 100644
index 00000000000000..1bf14c500979b2
--- /dev/null
+++ b/clang/docs/ScalableStaticAnalysis/user-docs/MakeProjects.md
@@ -0,0 +1,252 @@
+# Make-Based Projects
+
+Running SSAF over a whole project means running summary extraction for every
+translation unit the project compiles, and linking those summaries the way the
+build links the objects they describe. `clang-ssaf-orchestrator-make` does that
+for projects built with make. The design based on tool shims is documented in
+{doc}`../developer-docs/MakeOrchestratorShims`.
+
+The build is executed normally, and each step it takes is paired with an SSAF
+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.
+
+The mapping onto the SSAF tools is direct:
+
+```{eval-rst}
+.. list-table::
+   :header-rows: 1
+
+   * - Build step
+     - SSAF step
+   * - ``$(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``
+```
+
+## Usage
+
+Run it from the project root; that is where the build runs.
+
+There are two workflows, the second going one step further than the first:
+
+- **Summary extraction and linking**, with `--extract-summaries`. Every
+  translation unit gets a summary, and the summaries are linked the way the
+  build links the objects, into one link unit summary.
+- **Whole-program analysis**, adding `--analysis`. `clang-ssaf-analyzer` runs
+  the named analyses over the link unit summary.
+
+Summary extraction and linking:
+
+```console
+$ clang-ssaf-orchestrator-make \
+    --extract-summaries=PointerFlow,UnsafeBufferUsage \
+    -o ssaf-output \
+    -- make -j8
+```
+
+Whole-program analysis:
+
+```console
+$ clang-ssaf-orchestrator-make \
+    --extract-summaries=PointerFlow,UnsafeBufferUsage \
+    --analysis=UnsafeBufferReachableAnalysisResult \
+    -o ssaf-output \
+    -- make -j8
+```
+
+For a project configured by autoconf, run `configure` first and give the
+orchestrator only the `make` invocation.
+
+## Options
+
+```{eval-rst}
+.. list-table::
+   :header-rows: 1
+
+   * - Option
+     - Purpose
+   * - ``--extract-summaries=<name,...>``
+     - Summary extractors to run over every translation unit. Required.
+   * - ``--analysis=<name>``
+     - Whole-program analysis to run on the link unit summary. Repeatable.
+       Without it the run stops after summary extraction.
+   * - ``-o``, ``--output-dir=<dir>``
+     - Where the SSAF artifacts go, made absolute. Defaults to
+       ``ssaf-output``. It must be absent, empty, or an earlier run's.
+   * - ``--ssaf-clang=<path>``
+     - SSAF-capable clang used for the SSAF compilations. Defaults to
+       ``clang`` on ``PATH``.
+   * - ``--ssaf-linker=<path>``
+     - Path to ``clang-ssaf-linker``. Defaults to the name on ``PATH``.
+   * - ``--ssaf-analyzer=<path>``
+     - Path to ``clang-ssaf-analyzer``. Defaults to the name on ``PATH``, and
+       is only required with ``--analysis``.
+   * - ``--ssaf-extra-clang-arg=<arg>``
+     - Extra argument for every SSAF compilation. Repeatable.
+   * - ``-k``, ``--keep-going``
+     - Exit successfully even though some SSAF steps failed. The failures are
+       still reported.
+   * - ``-v``, ``--verbose``
+     - Print every command the orchestrator and the shims run, on stderr.
+   * - ``--version``
+     - The clang version this was built from.
+```
+
+Each `--ssaf-*` tool must resolve to one executable file. A value naming more
+than one word, a directory, something that is not executable, or a path inside
+our own installed shim directory — which is what an earlier run's links on
+`PATH` look like — is refused, and every unresolvable tool is reported at
+once.
+
+## Artifacts
+
+A build artifact is named by its resolved **path**, so two spellings of one
+file name the same artifact. That path is what a buildstep record names, and
+the value of `--ssaf-compilation-unit-id=`.
+
+An SSAF artifact is filed under the same path with its leading separator
+dropped, so an object at `/work/parse/build/parser.o` gets a summary at
+`<output-dir>/ssaf-artifacts/work/parse/build/parser.o.summary.json`.
+
+No part of the path is elided, because the path is what tells two
+translation units apart. A build that compiles one source into
+`build/first/parser.o` and another into `build/second/parser.o` has two
+translation units, and only the whole path tells them apart.
+
+```{eval-rst}
+.. list-table::
+   :header-rows: 1
+
+   * - Path
+     - Contents
+   * - ``<output-dir>/ssaf-artifacts/<path>.summary.json``
+     - What SSAF holds for ``<path>``: a TU summary where the path names a
+       compilation's object, and whatever the SSAF linker wrote where the
+       build assembled it.
+   * - ``<output-dir>/build-graph/<path>.buildstep.json``
+     - Which build artifacts were inputs for the ``<path>`` build artifact.
+   * - ``<output-dir>/wpa.json``
+     - Whole-program analysis result.
+   * - ``<output-dir>/config-analysis.json``
+     - Configuration for the analysis pass.
+   * - ``<output-dir>/shims/``
+     - Links to the shims named so they interpose the binary tools, recreated
+       on every run.
+   * - ``<output-dir>/.clang-ssaf-orchestrator-make``
+     - SSAF ownership attribute for cleanup.
+   * - ``<output-dir>/failure-logs/<path>.<kind>.log``
+     - What went wrong for ``<path>``: a command writing it whose SSAF step
+       failed, or an input that had no summary.
+   * - ``<output-dir>/failure-logs/<dir>/<hash>.<kind>.log``
+     - What went wrong for a command that names no output, and any internal
+       error in a shim; ``<hash>`` names the command and ``<dir>`` is where
+       it ran. Every ``.log`` anywhere under the output directory counts
+       against the run; a log is always a failure.
+```
+
+The buildstep records are what the top-most link unit is found from. An
+input contributes the summary filed under its own id or nothing at all,
+so an artifact whose SSAF step failed takes the artifacts behind it down
+with it, and the run says so and exits non-zero.
+
+## Failure handling
+
+A failing build step fails the run with the build's own exit status, and no
+SSAF step runs for it.
+
+A failing SSAF step never fails the build: it is reported and logged to a
+`.log` under `<output-dir>/failure-logs/`, and the build carries on. The run
+exits non-zero afterwards, because the artifacts are incomplete. `-k` makes the
+run exit zero instead; the failures are still reported, so the message appears
+either way.
+
+## Interposition
+
+The orchestrator populates `<output-dir>/shims/` with links named after the
+tools a build invokes — `cc`, `clang`, `c++`, `clang++` and `ld` —
+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
+exactly as it was written, and at the SSAF compilation with it.
+
+## What is not supported
+
+**Compilers other than clang.** Every compilation gets an SSAF step replaying
+the frontend job the build's own compiler said it would run, and that replay
+carries the `--ssaf-*` options, which only clang has. A project built by GCC
+therefore cannot be analysed, and the orchestrator does not pretend otherwise:
+`gcc` and `g++` are not among the names it interposes, so such a build is left
+alone rather than analysed into an empty result. On a platform where `gcc` and
+`g++` are clang this means a build spelling those names is not analysed
+either. GNU make defaults `CXX` to `g++`,
+so a Makefile that never sets it needs `CXX=clang++` on the make command line.
+The clang the build uses and the one named by `--ssaf-clang` must also be
+compatible with each other, since the frontend command line one produces is
+replayed by the other.
+
+**Compiler launchers**, such as `ccache`, `distcc`, `sccache` and `xcrun`.
+Run the build without the launcher instead, by overriding the variable that
+adds it on the make command line, as in `-- make CC=clang CXX=clang++`.
+
+**A compiler named by an absolute path**, in a recipe or through a variable:
+
+```make
+CXX = /opt/toolchain/bin/clang++     # PATH is never consulted
+CC := $(shell xcrun -find clang)     # an absolute path at parse time
+```
+
+Name the compiler by its bare name on the make command line instead, with its
+directory on `PATH`:
+
+```console
+$ PATH=/opt/toolchain/bin:$PATH clang-ssaf-orchestrator-make \
+    --extract-summaries=PointerFlow -- make CXX=clang++
+```
+
+**Inputs named indirectly on a link line.** A link that outgrows `ARG_MAX` and
+passes its objects through ld64's `-filelist`, given to `ld` or to the driver,
+is read as it stands, so those objects are not seen and the step contributes
+no link unit summary.
+
+**Linkers not named `ld`.** A link the driver hands to `ld.lld`, `ld64.lld`,
+`ld.gold` or `mold`, through `-fuse-ld=` or the toolchain's default, has no
+shim standing in for it, so it contributes nothing, and a project linked that
+way has no link unit.
+
+**Frontend command lines.** A recipe running `clang -cc1 …` directly, or
+`clang @file.rsp` whose first token is `-cc1`, is compiled, but its SSAF step
+fails: the driver cannot list the jobs of a frontend command line.
+
+**Assembler sources.** A `.s` or `.S` input carries a `-cc1as` job, whose SSAF
+step fails.
+
+## Implementation details
+
+How a compilation decomposes is worked out by asking the compiler driver, using
+the `-###` option.
+
+Because a compilation's SSAF step replays the build's own action, it writes the
+build's outputs a second time — the object, and any dependency file or
+serialized diagnostics the job asked for — with the SSAF clang rather than the
+build's. Build products are not preserved; this is an analysis run, not a
+build.
+
+Every build gets make's `-B` appended, because a translation unit that make
+considers up to date is not compiled and so gets no SSAF step. A build command
+that is not make draws a warning.
+
+Each run starts by deleting the output directory, recreating it and marking it
+with a `.clang-ssaf-orchestrator-make` file. An existing output directory is
+deleted only when it is empty or carries that mark, and never when it is, or
+contains, the project.
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/bad-args.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/bad-args.test
new file mode 100644
index 00000000000000..cd04850a31fe61
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/bad-args.test
@@ -0,0 +1,13 @@
+
+# RUN: not %clang-ssaf-orchestrator-make 2>&1 | FileCheck %s --check-prefix=NO-EXTRACTORS
+# NO-EXTRACTORS: error: the following arguments are required: --extract-summaries
+
+# RUN: not %clang-ssaf-orchestrator-make --extract-summaries=PointerFlow 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=NO-BUILD
+# NO-BUILD: no build command given
+
+# RUN: not %clang-ssaf-orchestrator-make --extract-summaries=PointerFlow \
+# RUN:   --ssaf-clang=/no/such/clang -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=NO-CLANG
+# NO-CLANG: required tools not resolvable:
+# NO-CLANG: --ssaf-clang: not a file: /no/such/clang
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/bare-linker.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/bare-linker.test
new file mode 100644
index 00000000000000..28222ab28548d7
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/bare-linker.test
@@ -0,0 +1,46 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# REQUIRES: system-darwin, aarch64-registered-target
+# REQUIRES: ssaf-orchestrator-make-sdkroot
+
+# 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: 3
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+
+# 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/prg.buildstep.json \
+# RUN:   | FileCheck %s --check-prefix=UNIT
+# UNIT-DAG: {{[^ ]*}}/build/main.o
+# UNIT-DAG: {{[^ ]*}}/build/helper.o
+
+# RUN: ls %t/build/prg
+
+#--- Makefile
+CXX = clang++
+LDFLAGS = -lSystem -syslibroot $(SDKROOT) -arch arm64 -platform_version macos 15.0 15.0
+all: build/prg
+build/%.o: src/%.cpp
+	$(CXX) -arch arm64 -c -o $@ $<
+build/prg: build/main.o build/helper.o
+	$(LD) -o $@ build/main.o build/helper.o $(LDFLAGS) -lc++
+
+#--- src/helper.cpp
+int helper(int *p) { return p[0]; }
+
+#--- src/main.cpp
+int helper(int *p);
+int main() {
+  int values[2] = {1, 2};
+  return helper(values);
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/build-failures.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/build-failures.test
new file mode 100644
index 00000000000000..f06a7494b87cd5
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/build-failures.test
@@ -0,0 +1,35 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# A build command that cannot be run at all.
+# RUN: cd %t
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- %t/no-such-build-tool 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=UNRUNNABLE
+# UNRUNNABLE: error: cannot run '{{.*}}/no-such-build-tool'
+
+# A build that runs and fails takes the run down with its own status.
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make broken 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=FAILED
+# FAILED: error: build failed (exit {{[1-9][0-9]*}})
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+broken: src/broken.cpp
+	$(CXX) -c -o build/broken.o $<
+build/prg: src/a.cpp
+	$(CXX) -o $@ $<
+
+#--- src/a.cpp
+int main() { return 0; }
+
+#--- src/broken.cpp
+this is not C++ at all
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/extract-summaries.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/extract-summaries.test
new file mode 100644
index 00000000000000..79dfa32bee2f8f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/extract-summaries.test
@@ -0,0 +1,67 @@
+
+# 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,UnsafeBufferUsage \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make \
+# RUN:   | FileCheck %s --check-prefix=STDOUT
+
+# STDOUT: summaries: 3
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+
+# RUN: ls %t/build/a.o %t/build/b.o %t/build/prg
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/a.o.summary.json \
+# RUN:   | FileCheck %s --check-prefix=CU-A
+# CU-A: "name": "{{[^ ]*}}/build/a.o"
+# RUN: cat %t/out/ssaf-artifacts%t/build/b.o.summary.json \
+# RUN:   | FileCheck %s --check-prefix=CU-B
+# CU-B: "name": "{{[^ ]*}}/build/b.o"
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/a.o.summary.json | FileCheck %s --check-prefix=KIND
+# KIND: "type": "TUSummary"
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/prg.summary.json \
+# RUN:   | FileCheck %s --check-prefix=LU
+# LU: "kind": "LinkUnit"
+# LU: "type": "LUSummary"
+
+# The SSAF step replays the build's own job, so it writes the build's outputs
+# a second time -- including the depfile -MMD asked for, and in the directory
+# the job named rather than the one the SSAF step ran in.
+# RUN: rm %t/build/a.d
+# RUN: cd %t
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out2 -- make
+# RUN: ls %t/build/a.d
+# RUN: ls %t && not ls %t/a.d
+
+#--- Makefile
+CXX = clang++
+CXXFLAGS = -MMD -MP
+all: build/prg
+build/%.o: src/%.cpp
+	$(CXX) $(CXXFLAGS) -c -o $@ $<
+build/prg: build/a.o build/b.o
+	$(CXX) -o $@ build/a.o build/b.o
+
+#--- src/a.cpp
+int sum(int *p, int n) {
+  int total = 0;
+  for (int i = 0; i < n; ++i)
+    total += p[i];
+  return total;
+}
+
+#--- src/b.cpp
+int sum(int *p, int n);
+int main() {
+  int values[4] = {1, 2, 3, 4};
+  return sum(values, 4);
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/failure-handling.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/failure-handling.test
new file mode 100644
index 00000000000000..b6e7bd6ed977d6
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/failure-handling.test
@@ -0,0 +1,78 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+# RUN: chmod +x %t/failing-clang %t/one-failing-clang
+
+# RUN: cd %t
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%t/failing-clang \
+# RUN:     -o %t/out -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=FAILED
+# FAILED: no summaries
+
+# RUN: ls %t/build/a.o %t/build/b.o %t/build/prg
+# RUN: grep 'stub refuses' %t/out/failure-logs%t/build/a.o.extract.log
+
+# RUN: ls %t/out/ssaf-artifacts%t/build && not ls %t/out/ssaf-artifacts%t/build/a.o.summary.json
+
+# RUN: env BINARY_CLANG=%ssaf-orchestrator-make-clang \
+# RUN:     not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%t/one-failing-clang \
+# RUN:     -o %t/out2 -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=PARTIAL
+# PARTIAL: 1 SSAF step(s) failed; see the log files under {{[^ ]*}}/out2
+
+# RUN: ls %t/out2/ssaf-artifacts%t/build | FileCheck %s --check-prefix=PARTIAL-TU \
+# RUN:   --implicit-check-not={{a\.o\.summary\.json}}
+# PARTIAL-TU: {{b\.o\.summary\.json}}
+# RUN: ls %t/out2/ssaf-artifacts%t/build/prg.summary.json
+
+# RUN: env BINARY_CLANG=%ssaf-orchestrator-make-clang \
+# RUN:     %clang-ssaf-orchestrator-make -k \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%t/one-failing-clang \
+# RUN:     -o %t/out3 -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=KEPT-GOING
+# KEPT-GOING: 1 SSAF step(s) failed
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/a.o build/b.o
+	$(CXX) -o $@ build/a.o build/b.o
+
+#--- src/a.cpp
+int b();
+int main() { return b(); }
+
+#--- src/b.cpp
+int b() { return 0; }
+
+#--- failing-clang
+#!/bin/sh
+for arg in "$@"; do
+  case "$arg" in
+    --ssaf-tu-summary-file=*)
+      file="${arg#*=}"
+      mkdir -p "$(dirname "$file")"
+      echo '{}' > "$file"
+      ;;
+  esac
+done
+echo "stub refuses to extract" >&2
+exit 1
+
+#--- one-failing-clang
+#!/bin/sh
+"$BINARY_CLANG" "$@" || exit $?
+for arg in "$@"; do
+  case "$arg" in
+    *a.o.summary.json) echo "stub refuses to extract" >&2; exit 1 ;;
+  esac
+done
+exit 0
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
new file mode 100644
index 00000000000000..7796cbc785840d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/help.test
@@ -0,0 +1,27 @@
+
+# RUN: %clang-ssaf-orchestrator-make --help | FileCheck %s
+
+# CHECK: usage: clang-ssaf-orchestrator-make [options] [--] <build command>
+# CHECK: --version
+# CHECK: --output-dir
+# CHECK: --extract-summaries
+# CHECK: --ssaf-clang
+# CHECK: --analysis
+
+# RUN: %clang-ssaf-orchestrator-make --help | FileCheck %s --check-prefix=SSAF
+# SSAF: --ssaf-linker
+# SSAF: --ssaf-analyzer
+# SSAF: --ssaf-extra-clang-arg
+# SSAF: --keep-going
+# SSAF: --verbose
+
+# RUN: %clang-ssaf-orchestrator-make --version | FileCheck %s --check-prefix=VERSION
+# VERSION: clang-ssaf-orchestrator-make {{[0-9]+\.[0-9]+\.[0-9]+}}
+
+# The tool imports its own package, whatever else of that name PYTHONPATH holds,
+# so it and its shims are always the same code.
+# RUN: rm -rf %t && mkdir -p %t/clang_ssaf_orchestrator_make
+# RUN: echo 'raise SystemExit("a decoy was imported")' \
+# RUN:   > %t/clang_ssaf_orchestrator_make/__init__.py
+# RUN: env PYTHONPATH=%t %clang-ssaf-orchestrator-make --version \
+# RUN:   | FileCheck %s --check-prefix=VERSION
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/incremental-tree.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/incremental-tree.test
new file mode 100644
index 00000000000000..6129bf9ce6b52f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/incremental-tree.test
@@ -0,0 +1,58 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+
+# RUN: make -C %t CXX=%ssaf-orchestrator-make-clang
+# RUN: ls %t/build/a.o %t/build/b.o %t/build/prg
+
+# 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: 3
+# RUN: ls %t/out/ssaf-artifacts%t/build/a.o.summary.json %t/out/ssaf-artifacts%t/build/b.o.summary.json
+
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make other \
+# RUN:   | FileCheck %s --check-prefix=SECOND
+# SECOND: summaries: 2
+# SECOND: link-unit: {{[^ ]*}}/build/other
+
+# RUN: ls %t/out/build-graph%t/build/other.buildstep.json
+# RUN: not ls %t/out/build-graph%t/build/prg.buildstep.json
+# RUN: not ls %t/out/ssaf-artifacts%t/build/a.o.summary.json
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+other: build/other
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/a.o build/b.o
+	$(CXX) -o $@ build/a.o build/b.o
+build/other: build/c.o
+	$(CXX) -o $@ build/c.o
+
+#--- src/a.cpp
+int total(int *p, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += p[i];
+  return t;
+}
+
+#--- src/b.cpp
+int total(int *p, int n);
+int main() {
+  int values[2] = {1, 2};
+  return total(values, 2);
+}
+
+#--- src/c.cpp
+int main() { return 0; }
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/interposition.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/interposition.test
new file mode 100644
index 00000000000000..d6939670694abb
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/interposition.test
@@ -0,0 +1,88 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+# RUN: chmod +x %t/bin/c++ %t/fake-clang %t/fake-linker
+
+# RUN: cd %t
+# RUN: env BINARY_CXX=%ssaf-orchestrator-make-clang \
+# RUN:     PATH="%t/bin:%ssaf-orchestrator-make-env-path" \
+# RUN:     %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%t/fake-clang \
+# RUN:     --ssaf-linker=%t/fake-linker \
+# RUN:     --ssaf-extra-clang-arg=-DFROM_EXTRA \
+# RUN:     -o %t/out -- make
+
+# RUN: FileCheck %s --check-prefix=BINARY --input-file=%t/bin/binary.log \
+# RUN:   --implicit-check-not=--ssaf- --implicit-check-not=FROM_EXTRA
+# BINARY: -Wall -c -o build/a.o src/a.cpp
+# BINARY-NEXT: SSAF_CONFIG=unset
+
+# RUN: FileCheck %s --check-prefix=SSAF --input-file=%t/ssaf.log
+# SSAF: -cc1
+# SSAF-SAME: -triple
+# SSAF-SAME: -x c++
+# SSAF-SAME: --ssaf-extract-summaries=PointerFlow
+# SSAF-SAME: --ssaf-compilation-unit-id={{[^ ]*}}/build/a.o
+# SSAF-SAME: --ssaf-tu-summary-file={{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/a.o.summary.json
+# SSAF-SAME: -DFROM_EXTRA
+
+# RUN: FileCheck %s --check-prefix=SAME-JOB --input-file=%t/ssaf.log
+# SAME-JOB: -emit-obj
+# SAME-JOB-SAME: -o build/a.o
+
+# RUN: FileCheck %s --check-prefix=LINK --input-file=%t/link.log
+# LINK: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/a.o.summary.json -o {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.summary.json
+
+#--- Makefile
+CXX = c++
+all: build/prg
+build/a.o: src/a.cpp
+	$(CXX) -Wall -c -o $@ $<
+build/prg: build/a.o
+	$(CXX) -o $@ build/a.o
+
+#--- src/a.cpp
+int main() { return 0; }
+
+#--- bin/c++
+#!/bin/sh
+dir=$(cd "$(dirname "$0")" && pwd)
+for arg in "$@"; do
+  if [ "$arg" = "-###" ]; then exec "$BINARY_CXX" "$@"; fi
+done
+echo "$@" >> "$dir/binary.log"
+echo "SSAF_CONFIG=${SSAF_CONFIG:-unset}" >> "$dir/binary.log"
+exec "$BINARY_CXX" "$@"
+
+#--- fake-clang
+#!/bin/sh
+dir=$(cd "$(dirname "$0")" && pwd)
+echo "$@" >> "$dir/ssaf.log"
+for arg in "$@"; do
+  case "$arg" in
+    --ssaf-tu-summary-file=*)
+      file="${arg#*=}"
+      mkdir -p "$(dirname "$file")"
+      echo '{}' > "$file"
+      ;;
+  esac
+done
+exit 0
+
+#--- fake-linker
+#!/bin/sh
+dir=$(cd "$(dirname "$0")" && pwd)
+echo "$@" >> "$dir/link.log"
+prev=""
+for arg in "$@"; do
+  if [ "$prev" = "-o" ]; then
+    mkdir -p "$(dirname "$arg")"
+    name=$(basename "$arg" .json)
+    printf '{"type": "LUSummary", "lu_namespace": [{"kind": "LinkUnit", ' > "$arg"
+    printf '"name": "%s"}]}\n' "$name" >> "$arg"
+  fi
+  prev="$arg"
+done
+exit 0
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver.test
new file mode 100644
index 00000000000000..41bba34ad44bef
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver.test
@@ -0,0 +1,30 @@
+
+# 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: 2
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+
+# RUN: ls %t/build/prg
+# RUN: cat %t/out/ssaf-artifacts%t/build/prg.summary.json | FileCheck %s --check-prefix=LU
+# LU: "kind": "LinkUnit"
+
+#--- Makefile
+CXX = clang++
+LD = $(CXX)
+LDFLAGS = -stdlib=libc++
+all: build/prg
+build/a.o: src/a.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/a.o
+	$(LD) $(LDFLAGS) -o $@ build/a.o
+
+#--- src/a.cpp
+int main() { return 0; }
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
new file mode 100644
index 00000000000000..735ea6b7185665
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-unit-selection.test
@@ -0,0 +1,56 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/two/build %t/none/build
+
+# RUN: cd %t/two
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/two/out -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=AMBIGUOUS
+# AMBIGUOUS: produced 2 unconsumed outputs and none of them consumes the others
+# AMBIGUOUS-SAME: {{[^ ]*}}/build/one
+# AMBIGUOUS-SAME: {{[^ ]*}}/build/two
+# AMBIGUOUS-SAME: Build one of them on its own
+
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/two/out -- make build/two \
+# RUN:   | FileCheck %s --check-prefix=PICKED
+# PICKED: summaries: 2
+# PICKED: link-unit: {{[^ ]*}}/build/two
+
+# RUN: cd %t/none
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/none/out -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=NO-LINK
+# NO-LINK: produced no link unit
+
+#--- two/Makefile
+CXX = clang++
+all: build/one build/two
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+build/one: build/one.o
+	$(CXX) -o $@ $<
+build/two: build/two.o
+	$(CXX) -o $@ $<
+
+#--- two/src/one.cpp
+int main() { return 0; }
+
+#--- two/src/two.cpp
+int main() { return 1; }
+
+#--- none/Makefile
+CXX = clang++
+all: build/only.o
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+
+#--- none/src/only.cpp
+int only(int *p) { return p[0]; }
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
new file mode 100644
index 00000000000000..515a84e0831e8c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
@@ -0,0 +1,62 @@
+# -*- Python -*-
+
+import lit.util
+import os
+import platform
+
+if platform.system() != "Windows" and lit.util.which("make"):
+    config.available_features.add("ssaf-orchestrator-make-build")
+
+config.substitutions.append(
+    (
+        "%clang-ssaf-orchestrator-make",
+        '"%s" "%s"'
+        % (
+            config.python_executable,
+            os.path.join(config.llvm_tools_dir, "clang-ssaf-orchestrator-make"),
+        ),
+    )
+)
+
+config.substitutions.append(
+    ("%ssaf-orchestrator-make-python", '"%s"' % config.python_executable)
+)
+
+config.substitutions.append(
+    (
+        "%ssaf-orchestrator-make-libs",
+        os.path.join(os.path.dirname(config.llvm_tools_dir), "lib"),
+    )
+)
+
+config.substitutions.append(
+    (
+        "%ssaf-orchestrator-make-shims",
+        os.path.join(
+            os.path.dirname(config.llvm_tools_dir),
+            "libexec",
+            "ssaf-orchestrator-shims",
+        ),
+    )
+)
+
+if "SDKROOT" in config.environment:
+    config.available_features.add("ssaf-orchestrator-make-sdkroot")
+
+config.substitutions.append(
+    (
+        "%ssaf-orchestrator-make-src",
+        os.path.join(config.clang_src_dir, "tools", "clang-ssaf-orchestrator-make"),
+    )
+)
+
+config.substitutions.append(
+    ("%ssaf-orchestrator-make-env-path", config.environment["PATH"])
+)
+
+config.substitutions.append(
+    (
+        "%ssaf-orchestrator-make-clang",
+        config.clang if config.have_llvm_driver else os.path.realpath(config.clang),
+    )
+)
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/path-interposition.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/path-interposition.test
new file mode 100644
index 00000000000000..49a5bf2238492f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/path-interposition.test
@@ -0,0 +1,42 @@
+
+# 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=KEPT
+# KEPT: summaries: 2
+# KEPT: link-unit: {{[^ ]*}}/build/prg
+
+# RUN: ls %t/build/a.o %t/build/prg
+# RUN: cat %t/out/ssaf-artifacts%t/build/a.o.summary.json | FileCheck %s --check-prefix=CU
+# CU: "name": "{{[^ ]*}}/build/a.o"
+
+# Under -v every step says what it ran, on stderr so the result lines stay
+# machine-readable.
+# RUN: %clang-ssaf-orchestrator-make -v \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out2 -- make 2>&1 >/dev/null \
+# RUN:   | FileCheck %s --check-prefix=NOTES
+# NOTES-DAG: note: make -B
+# NOTES-DAG: note: binary: {{.*}}clang++
+# NOTES-DAG: note: ssaf: {{.*}}--ssaf-shim-skip-binary-tool-invocation
+
+#--- Makefile
+CXX = clang++ -DFROM_MAKEFILE=1
+all: build/prg
+build/a.o: src/a.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/a.o
+	$(CXX) -o $@ build/a.o
+
+#--- src/a.cpp
+#ifndef FROM_MAKEFILE
+#error "the Makefile's own CXX flags were lost"
+#endif
+int main() { return 0; }
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/recursive-make.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/recursive-make.test
new file mode 100644
index 00000000000000..770dd5242f059a
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/recursive-make.test
@@ -0,0 +1,61 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build %t/sub/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: 3
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+
+# RUN: ls %t/out/ssaf-artifacts%t/build/main.o.summary.json %t/out/ssaf-artifacts%t/sub/build/s.o.summary.json
+# RUN: cat %t/out/ssaf-artifacts%t/sub/build/s.o.summary.json \
+# RUN:   | FileCheck %s --check-prefix=SUB-CU
+# SUB-CU: "name": "{{[^ ]*}}/sub/build/s.o"
+
+# RUN: cat %t/out/build-graph%t/build/prg.buildstep.json \
+# RUN:   | FileCheck %s --check-prefix=UNIT
+# UNIT-DAG: {{[^ ]*}}/build/main.o
+# UNIT-DAG: {{[^ ]*}}/sub/build/s.o
+
+# The default output directory is relative, and a sub-make runs the shims from
+# a different directory than the one it was resolved against.
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -- make \
+# RUN:   | FileCheck %s --check-prefix=DEFAULT-OUT
+# DEFAULT-OUT: output-dir: {{.*}}/ssaf-output
+# RUN: ls %t/ssaf-output/ssaf-artifacts%t/build/main.o.summary.json \
+# RUN:      %t/ssaf-output/ssaf-artifacts%t/sub/build/s.o.summary.json
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+build/main.o: src/main.cpp
+	$(CXX) -c -o $@ $<
+sub/build/s.o:
+	$(MAKE) -C sub
+build/prg: build/main.o sub/build/s.o
+	$(CXX) -o $@ build/main.o sub/build/s.o
+
+#--- src/main.cpp
+int sub(int *p);
+int main() {
+  int values[1] = {1};
+  return sub(values);
+}
+
+#--- sub/Makefile
+CXX = clang++
+all: build/s.o
+build/s.o: src/s.cpp
+	$(CXX) -c -o $@ $<
+
+#--- sub/src/s.cpp
+int sub(int *p) { return p[0]; }
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/same-basename-objects.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/same-basename-objects.test
new file mode 100644
index 00000000000000..95dc087613bc2f
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/same-basename-objects.test
@@ -0,0 +1,61 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build/first %t/build/second
+
+# 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: 4
+# RUN: ls %t/out/ssaf-artifacts%t/build/first/parser.o.summary.json %t/out/ssaf-artifacts%t/build/second/parser.o.summary.json
+
+# RUN: cat %t/out/ssaf-artifacts%t/build/first/parser.o.summary.json | FileCheck %s --check-prefix=FIRST
+# FIRST: "name": "{{[^ ]*}}/build/first/parser.o"
+# RUN: cat %t/out/ssaf-artifacts%t/build/second/parser.o.summary.json | FileCheck %s --check-prefix=SECOND
+# SECOND: "name": "{{[^ ]*}}/build/second/parser.o"
+
+# RUN: cat %t/out/build-graph%t/build/prg.buildstep.json \
+# RUN:   | FileCheck %s --check-prefix=UNIT
+# UNIT-DAG: {{[^ ]*}}/build/first/parser.o
+# UNIT-DAG: {{[^ ]*}}/build/second/parser.o
+
+#--- Makefile
+CXX = clang++
+OBJS = build/main.o build/first/parser.o build/second/parser.o
+all: build/prg
+build/main.o: src/main.cpp
+	$(CXX) -c -o $@ $<
+build/first/parser.o: src/first/parser.cpp
+	$(CXX) -c -o $@ $<
+build/second/parser.o: src/second/parser.cpp
+	$(CXX) -c -o $@ $<
+build/prg: $(OBJS)
+	$(CXX) -o $@ $(OBJS)
+
+#--- src/first/parser.cpp
+int first_total(int *p, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += p[i];
+  return t;
+}
+
+#--- src/second/parser.cpp
+int second_total(int *p, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += p[i] * 2;
+  return t;
+}
+
+#--- src/main.cpp
+int first_total(int *p, int n);
+int second_total(int *p, int n);
+int main() {
+  int values[2] = {1, 2};
+  return first_total(values, 2) + second_total(values, 2);
+}
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/shim-outside-a-run.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/shim-outside-a-run.test
new file mode 100644
index 00000000000000..5ce38a94fab3ce
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/shim-outside-a-run.test
@@ -0,0 +1,17 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && mkdir -p %t
+# RUN: ln -s %ssaf-orchestrator-make-shims/shim %t/ld
+
+# A link left behind by an interrupted run still knows which tool it stands
+# in for, and refuses to stand in for it.
+# RUN: not env -u SSAF_CONFIG %t/ld -o %t/prg 2>&1 | FileCheck %s
+# CHECK: error: missing configuration file
+# CHECK-NEXT: SSAF_CONFIG is not set
+
+# Reached under a name no link carries -- its own, say -- it says so rather
+# than guessing.
+# RUN: not env -u SSAF_CONFIG %ssaf-orchestrator-make-shims/shim 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=NOT-A-LINK
+# NOT-A-LINK: error: no shim stands in for shim
+# NOT-A-LINK-NEXT: reached through the links an orchestrator run creates
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/unittests.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/unittests.test
new file mode 100644
index 00000000000000..ce50b1cf96807c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/unittests.test
@@ -0,0 +1,8 @@
+
+# RUN: env PYTHONDONTWRITEBYTECODE=1 PYTHONPATH=%ssaf-orchestrator-make-libs \
+# RUN:     %ssaf-orchestrator-make-python -m unittest discover \
+# RUN:   -s %ssaf-orchestrator-make-src/tests -t %ssaf-orchestrator-make-src \
+# RUN:   > %t.log 2>&1
+# RUN: FileCheck %s --input-file=%t.log --implicit-check-not=FAILED
+
+# CHECK: {{^OK}}
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/unreachable-compiler.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/unreachable-compiler.test
new file mode 100644
index 00000000000000..883d766763c3f0
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/unreachable-compiler.test
@@ -0,0 +1,27 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+# RUN: sed -e "s|@CXX@|%ssaf-orchestrator-make-clang|g" %t/Makefile.in > %t/Makefile
+
+# 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-prefix=MISSED
+# MISSED: no summaries
+
+# RUN: ls %t/build/a.o %t/build/prg
+# RUN: not ls %t/out/ssaf-artifacts%t/build/*.summary.json
+
+#--- Makefile.in
+CXX = @CXX@
+all: build/prg
+build/a.o: src/a.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/a.o
+	$(CXX) -o $@ build/a.o
+
+#--- src/a.cpp
+int main() { return 0; }
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/whole-program-analysis.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/whole-program-analysis.test
new file mode 100644
index 00000000000000..28f6c716d0d412
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/whole-program-analysis.test
@@ -0,0 +1,59 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+# RUN: chmod +x %t/failing-analyzer
+
+# RUN: cd %t
+# RUN: %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow,UnsafeBufferUsage \
+# RUN:     --analysis=UnsafeBufferReachableAnalysisResult \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     -o %t/out -- make \
+# RUN:   | FileCheck %s --check-prefix=STDOUT
+
+# STDOUT: summaries: 3
+# STDOUT: link-unit: {{[^ ]*}}/build/prg
+# STDOUT: link-unit-summary: {{[^ ]*}}/out/ssaf-artifacts{{[^ ]*}}/build/prg.summary.json
+# STDOUT: analysis-result: {{[^ ]*}}/out/wpa.json
+# STDOUT: output-dir: {{[^ ]*}}/out
+
+# RUN: FileCheck %s --check-prefix=WPA --input-file=%t/out/wpa.json
+# WPA: "type": "WPASuite"
+
+# A failing analysis fails the run, which says how the analyzer exited.
+# RUN: not %clang-ssaf-orchestrator-make \
+# RUN:     --extract-summaries=PointerFlow \
+# RUN:     --analysis=UnsafeBufferReachableAnalysisResult \
+# RUN:     --ssaf-clang=%ssaf-orchestrator-make-clang \
+# RUN:     --ssaf-analyzer=%t/failing-analyzer \
+# RUN:     -o %t/out2 -- make 2>&1 \
+# RUN:   | FileCheck %s --check-prefix=FAILED
+# FAILED: error: whole-program analysis failed (exit 3)
+
+#--- Makefile
+CXX = clang++
+all: build/prg
+build/%.o: src/%.cpp
+	$(CXX) -c -o $@ $<
+build/prg: build/main.o build/parse.o
+	$(CXX) -o $@ build/main.o build/parse.o
+
+#--- src/parse.cpp
+int total(int *data, int n) {
+  int t = 0;
+  for (int i = 0; i < n; ++i)
+    t += data[i];
+  return t;
+}
+
+#--- src/main.cpp
+int total(int *data, int n);
+int main() {
+  int values[4] = {1, 2, 3, 4};
+  return total(values, 4);
+}
+
+#--- failing-analyzer
+#!/bin/sh
+exit 3
diff --git a/clang/test/CMakeLists.txt b/clang/test/CMakeLists.txt
index 4063f58ad0e080..3ef3cf764d2157 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -107,6 +107,7 @@ list(APPEND CLANG_TEST_DEPS
   clang-ssaf-analyzer
   clang-ssaf-format
   clang-ssaf-linker
+  clang-ssaf-orchestrator-make
   clang-ssaf-src-edit-merge
   clang-sycl-linker
   diagtool
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index 855af780d4e02a..535c3071700f24 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -18,6 +18,7 @@ add_clang_subdirectory(clang-scan-deps)
 add_clang_subdirectory(clang-ssaf-analyzer)
 add_clang_subdirectory(clang-ssaf-format)
 add_clang_subdirectory(clang-ssaf-linker)
+add_clang_subdirectory(clang-ssaf-orchestrator-make)
 add_clang_subdirectory(clang-ssaf-src-edit-merge)
 add_clang_subdirectory(clang-sycl-linker)
 add_clang_subdirectory(clang-installapi)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt b/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt
new file mode 100644
index 00000000000000..a29c137eb30fcb
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/CMakeLists.txt
@@ -0,0 +1,94 @@
+set(SsafOrchestratorMakeLib
+    "__init__.py"
+    "build_graph.py"
+    "config.py"
+    "config_orchestrator.py"
+    "config_shims.py"
+    "failure_logs.py"
+    "interposition.py"
+    "logging_diagnostics.py"
+    "options.py"
+    "orchestrator.py"
+    "ssaf_artifact_repository.py"
+    "utils.py")
+
+set(SsafOrchestratorMakeShimLib
+    "__init__.py"
+    "clang.py"
+    "commands.py"
+    "driver_jobs.py"
+    "ld.py"
+    "run.py"
+    "shim.py")
+
+set(SsafOrchestratorMakePackageDir "lib/clang_ssaf_orchestrator_make")
+set(SsafOrchestratorMakeShimDir "libexec/ssaf-orchestrator-shims")
+
+# Copy one file into the build tree and install it, recording the copy so the
+# tool's target can depend on it. kind is FILES or PROGRAMS.
+function(add_ssaf_orchestrator_make_file source build_dir destination kind)
+  get_filename_component(name "${source}" NAME)
+  set(copied "${CMAKE_BINARY_DIR}/${build_dir}/${name}")
+  add_custom_command(OUTPUT "${copied}"
+                     COMMAND ${CMAKE_COMMAND} -E make_directory
+                       "${CMAKE_BINARY_DIR}/${build_dir}"
+                     COMMAND ${CMAKE_COMMAND} -E copy
+                       "${CMAKE_CURRENT_SOURCE_DIR}/${source}" "${copied}"
+                     DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/${source}")
+  set(SSAF_ORCHESTRATOR_MAKE_DEPENDS
+      ${SSAF_ORCHESTRATOR_MAKE_DEPENDS} "${copied}" PARENT_SCOPE)
+  install(${kind} "${source}"
+          DESTINATION "${destination}"
+          COMPONENT clang-ssaf-orchestrator-make)
+endfunction()
+
+add_ssaf_orchestrator_make_file("bin/clang-ssaf-orchestrator-make"
+                                "bin" "${CMAKE_INSTALL_BINDIR}" PROGRAMS)
+
+foreach(lib ${SsafOrchestratorMakeLib})
+  add_ssaf_orchestrator_make_file(
+    "${SsafOrchestratorMakePackageDir}/${lib}"
+    "${SsafOrchestratorMakePackageDir}"
+    "${SsafOrchestratorMakePackageDir}" FILES)
+endforeach()
+
+# Substituted at configure time into this directory's own build area, then
+# copied like every other file, so that deleting the package from the build
+# tree leaves a rule to put it back.
+configure_file(
+  "${CMAKE_CURRENT_SOURCE_DIR}/${SsafOrchestratorMakePackageDir}/version.py.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/version.py")
+set(version_py "${CMAKE_BINARY_DIR}/${SsafOrchestratorMakePackageDir}/version.py")
+add_custom_command(OUTPUT "${version_py}"
+                   COMMAND ${CMAKE_COMMAND} -E make_directory
+                     "${CMAKE_BINARY_DIR}/${SsafOrchestratorMakePackageDir}"
+                   COMMAND ${CMAKE_COMMAND} -E copy
+                     "${CMAKE_CURRENT_BINARY_DIR}/version.py" "${version_py}"
+                   DEPENDS "${CMAKE_CURRENT_BINARY_DIR}/version.py")
+list(APPEND SSAF_ORCHESTRATOR_MAKE_DEPENDS "${version_py}")
+install(FILES "${CMAKE_CURRENT_BINARY_DIR}/version.py"
+        DESTINATION "${SsafOrchestratorMakePackageDir}"
+        COMPONENT clang-ssaf-orchestrator-make)
+
+foreach(lib ${SsafOrchestratorMakeShimLib})
+  add_ssaf_orchestrator_make_file(
+    "${SsafOrchestratorMakePackageDir}/shims/${lib}"
+    "${SsafOrchestratorMakePackageDir}/shims"
+    "${SsafOrchestratorMakePackageDir}/shims" FILES)
+endforeach()
+
+# One program, symlinked under every name a build might spell.
+add_ssaf_orchestrator_make_file(
+  "shims/shim"
+  "${SsafOrchestratorMakeShimDir}"
+  "${CMAKE_INSTALL_LIBEXECDIR}/ssaf-orchestrator-shims" PROGRAMS)
+
+add_custom_target(clang-ssaf-orchestrator-make ALL
+                  DEPENDS ${SSAF_ORCHESTRATOR_MAKE_DEPENDS})
+set_target_properties(clang-ssaf-orchestrator-make
+                      PROPERTIES FOLDER "Clang/Tools")
+if(NOT LLVM_ENABLE_IDE)
+  add_llvm_install_targets("install-clang-ssaf-orchestrator-make"
+                           DEPENDS clang-ssaf-orchestrator-make
+                           COMPONENT clang-ssaf-orchestrator-make)
+endif()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/README.md b/clang/tools/clang-ssaf-orchestrator-make/README.md
new file mode 100644
index 00000000000000..04c2337d5db906
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/README.md
@@ -0,0 +1,51 @@
+# 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
+performs is paired with an SSAF step that produces the corresponding summary,
+and the summaries are linked into one whole-program view.
+
+- Using it: [Make-Based Projects](
+  ../../docs/ScalableStaticAnalysis/user-docs/MakeProjects.md)
+- How the shims work: [Make Orchestrator Shims](
+  ../../docs/ScalableStaticAnalysis/developer-docs/MakeOrchestratorShims.md)
+
+## Layout
+
+| | |
+|---|---|
+| `bin/` | the entry script, installed to `bin/` |
+| `lib/clang_ssaf_orchestrator_make/` | the package, installed to `lib/` |
+| `…/shims/` | the shim bodies and their utilities |
+| `shims/` | the shim program, installed to `libexec/ssaf-orchestrator-shims/` |
+| `tests/` | the Python unit tests |
+
+The shim program is what a build reaches: at the start of a run the orchestrator
+fills `<output-dir>/shims/` with links named after the tools a build might
+spell, all pointing at it, and prepends that directory to `PATH`. It is
+symlinked under several names, so `argv[0]` is what tells it which tool it
+stands in for, and it finds the package by walking up from its own location —
+which is why it is a file beside the package rather than part of it.
+
+## Running the tests
+
+```
+PYTHONPATH=lib python3 -m unittest discover  # from this directory
+llvm-lit clang/test/Analysis/Scalable/ssaf-orchestrator-make
+```
+
+The lit suite runs the unit tests too, against the copy in the build tree, so
+`ninja check-clang` covers both. The tool is a declared dependency of
+`check-clang`.
+
+## Packaging
+
+CMake copies the Python files into the build tree and installs them under the
+`clang-ssaf-orchestrator-make` component. The component is not listed in any
+distribution cache, so a distribution build will not pick it up without being
+told to.
+
+There is no `pyproject.toml`. The entry point is installed by CMake, as it is
+for every other Python tool under `clang/tools/`, so a manifest here would
+declare an installation path that does not exist. The Python version floor is
+the monorepo's, recorded in the root `pyproject.toml`.
diff --git a/clang/tools/clang-ssaf-orchestrator-make/bin/clang-ssaf-orchestrator-make b/clang/tools/clang-ssaf-orchestrator-make/bin/clang-ssaf-orchestrator-make
new file mode 100755
index 00000000000000..92a3b272aa248e
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/bin/clang-ssaf-orchestrator-make
@@ -0,0 +1,15 @@
+#!/usr/bin/env python3
+# 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.path
+import sys
+
+this_dir = os.path.dirname(os.path.realpath(__file__))
+sys.path.insert(0, os.path.join(os.path.dirname(this_dir), "lib"))
+
+from clang_ssaf_orchestrator_make import orchestrator
+
+if __name__ == "__main__":
+    sys.exit(orchestrator.main(sys.argv[1:]))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/__init__.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/__init__.py
new file mode 100644
index 00000000000000..01f0dae478b111
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/__init__.py
@@ -0,0 +1,10 @@
+# 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
+
+PROGRAM = "clang-ssaf-orchestrator-make"
+
+try:
+    from .version import VERSION
+except ImportError:
+    VERSION = "unknown (not built)"
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/build_graph.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/build_graph.py
new file mode 100644
index 00000000000000..21e285a33bea2f
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/build_graph.py
@@ -0,0 +1,73 @@
+# 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 .utils import coalesce_root_path, make_parents, read_json, write_json
+
+BUILDSTEP_RECORD_SUFFIX = ".buildstep.json"
+
+
+class BuildArtifactId:
+    __slots__ = ("abs_path",)
+
+    def __init__(self, build_artifact_path):
+        self.abs_path = os.path.realpath(build_artifact_path)
+
+    def get_path(self):
+        return self.abs_path
+
+    def __eq__(self, other):
+        return isinstance(other, BuildArtifactId) and other.abs_path == self.abs_path
+
+    def __hash__(self):
+        return hash(self.abs_path)
+
+    def __repr__(self):
+        return "BuildArtifactId(%r)" % (self.abs_path,)
+
+
+class BuildGraph:
+    def __init__(self, build_graph_dir):
+        self.build_graph_dir = os.path.abspath(build_graph_dir)
+
+    def get_buildstep_record_path(self, output_build_artifact_id):
+        return coalesce_root_path(
+            self.build_graph_dir,
+            output_build_artifact_id.get_path() + BUILDSTEP_RECORD_SUFFIX,
+        )
+
+    def was_built(self, build_artifact_id):
+        return os.path.exists(self.get_buildstep_record_path(build_artifact_id))
+
+    def record_build_step(self, output_build_artifact_id, inputs):
+        path = make_parents(self.get_buildstep_record_path(output_build_artifact_id))
+        write_json(
+            path,
+            {
+                "output": output_build_artifact_id.get_path(),
+                "inputs": [each.get_path() for each in inputs],
+            },
+        )
+        return path
+
+    def get_recorded_build_steps(self):
+        steps = {}
+        for directory, _, filenames in os.walk(self.build_graph_dir):
+            for name in sorted(filenames):
+                if not name.endswith(BUILDSTEP_RECORD_SUFFIX):
+                    continue
+                document = read_json(os.path.join(directory, name))
+                if document is None or "output" not in document:
+                    continue
+                steps[document["output"]] = list(document.get("inputs", []))
+        return steps
+
+    @staticmethod
+    def find_topmost_outputs(steps):
+        consumed = set()
+        for inputs in steps.values():
+            consumed.update(inputs)
+        return sorted(output for output in steps if output not in consumed)
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
new file mode 100644
index 00000000000000..01caf368092ddd
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
@@ -0,0 +1,115 @@
+# 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 enum
+import os
+
+SSAF_CONFIG_ENV_VAR = "SSAF_CONFIG"
+
+SHIM_LINK_DIR = "shims"
+BUILD_GRAPH_DIR = "build-graph"
+SSAF_ARTIFACT_DIR = "ssaf-artifacts"
+FAILURE_LOG_DIR = "failure-logs"
+WPA_RESULT_FILE = "wpa.json"
+OUTPUT_DIR_MARKER = ".clang-ssaf-orchestrator-make"
+
+
+def shim_link_dir(output_dir):
+    return os.path.join(output_dir, SHIM_LINK_DIR)
+
+
+def build_graph_dir(output_dir):
+    return os.path.join(output_dir, BUILD_GRAPH_DIR)
+
+
+def ssaf_artifact_dir(output_dir):
+    return os.path.join(output_dir, SSAF_ARTIFACT_DIR)
+
+
+def failure_log_dir(output_dir):
+    return os.path.join(output_dir, FAILURE_LOG_DIR)
+
+
+def wpa_result_path(output_dir):
+    return os.path.join(output_dir, WPA_RESULT_FILE)
+
+
+def output_dir_marker_path(output_dir):
+    return os.path.join(output_dir, OUTPUT_DIR_MARKER)
+
+
+SHIM_FOR_LINK_NAME = {
+    "cc": "clang",
+    "clang": "clang",
+    "c++": "clang",
+    "clang++": "clang",
+    "ld": "ld",
+}
+
+
+class SSAFPassKind(str, enum.Enum):
+    ANALYSIS = "analysis"
+
+
+class SSAFPassConfig:
+    def __init__(self, ssaf_clang=None, ssaf_extra_clang_args=()):
+        self.ssaf_clang = ssaf_clang
+        self.ssaf_extra_clang_args = list(ssaf_extra_clang_args)
+
+    @property
+    def file_name(self):
+        return "config-%s.json" % self.pass_kind.value
+
+    def to_json(self):
+        fields = dict(vars(self))
+        fields["pass_kind"] = self.pass_kind.value
+        return fields
+
+
+class SSAFAnalysisPassConfig(SSAFPassConfig):
+    pass_kind = SSAFPassKind.ANALYSIS
+
+    def __init__(
+        self,
+        ssaf_clang=None,
+        ssaf_extra_clang_args=(),
+        extractors=None,
+        ssaf_linker=None,
+        analyses=(),
+    ):
+        super().__init__(ssaf_clang, ssaf_extra_clang_args)
+        self.extractors = extractors
+        self.ssaf_linker = ssaf_linker
+        self.analyses = list(analyses)
+
+
+class SSAFWorkflowConfig:
+    def __init__(
+        self,
+        output_dir=None,
+        installed_shim_dir_path=None,
+        restored_path_env_var="",
+        verbose=False,
+        build_command=(),
+        ssaf_analyzer=None,
+        keep_going=False,
+        build_environment=None,
+    ):
+        self.output_dir = output_dir
+        self.installed_shim_dir_path = installed_shim_dir_path
+        self.restored_path_env_var = restored_path_env_var
+        self.verbose = verbose
+        self.build_command = list(build_command)
+        self.ssaf_analyzer = ssaf_analyzer
+        self.keep_going = keep_going
+        self.build_environment = dict(build_environment or {})
+
+    @classmethod
+    def written_field_names(cls):
+        return frozenset(cls().to_json())
+
+    def to_json(self):
+        fields = dict(vars(self))
+        del fields["build_environment"]
+        return fields
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
new file mode 100644
index 00000000000000..a9c096ba3208fb
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_orchestrator.py
@@ -0,0 +1,142 @@
+# 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 shlex
+import shutil
+import sys
+
+from . import config, options
+
+
+def _resolve_ssaf_tool(
+    cl_option, default_tool_name, cl_option_value, installed_shim_dir_path, errors
+):
+    error_prefix = "%s: " % cl_option
+    words = (
+        shlex.split(cl_option_value)
+        if cl_option_value is not None
+        else [default_tool_name]
+    )
+    if not words:
+        errors.append(error_prefix + "nothing to run")
+        return None
+    if len(words) > 1:
+        errors.append(
+            error_prefix + "expected one executable, got '%s'" % cl_option_value
+        )
+        return None
+    name_or_path = words[0]
+    if os.sep in name_or_path:
+        candidate = os.path.abspath(name_or_path)
+    else:
+        candidate = shutil.which(name_or_path)
+    if not candidate:
+        errors.append(error_prefix + "%s: not found on PATH" % name_or_path)
+        return None
+    if not os.path.isfile(candidate):
+        errors.append(error_prefix + "not a file: %s" % candidate)
+        return None
+    if not os.access(candidate, os.X_OK):
+        errors.append(error_prefix + "not executable: %s" % candidate)
+        return None
+    if os.path.dirname(os.path.realpath(candidate)) == installed_shim_dir_path:
+        errors.append(
+            error_prefix + "%s is one of our own shims; an earlier run's shim links "
+            "are on PATH" % candidate
+        )
+        return None
+    return candidate
+
+
+def _may_be_cleared(output_dir):
+    try:
+        entries = os.listdir(output_dir)
+    except FileNotFoundError:
+        return True
+    except OSError:
+        return False
+    return not entries or config.OUTPUT_DIR_MARKER in entries
+
+
+def find_installed_shim_dir():
+    entry = sys.modules.get("__main__")
+    script = getattr(entry, "__file__", None) or sys.argv[0]
+    script_dir = os.path.dirname(os.path.realpath(script))
+    prefix = os.path.dirname(script_dir)
+    installed = os.path.join(prefix, "libexec", "ssaf-orchestrator-shims")
+    if os.path.isdir(installed):
+        return installed
+    return os.path.join(prefix, "shims")
+
+
+def configure(command_line_opts, env):
+    installed_shim_dir_path = find_installed_shim_dir()
+
+    project_root = os.path.realpath(os.getcwd())
+    output_dir = os.path.realpath(command_line_opts.output_dir)
+    if project_root == output_dir or project_root.startswith(output_dir + os.sep):
+        return None, options.Problem(
+            options.ENVIRONMENT_ERROR,
+            "--output-dir would contain the project, and its contents are "
+            "deleted at the start of a run: %s" % output_dir,
+        )
+    if not _may_be_cleared(output_dir):
+        return None, options.Problem(
+            options.ENVIRONMENT_ERROR,
+            "--output-dir is not empty and no earlier run made it, and its "
+            "contents are deleted at the start of a run: %s" % output_dir,
+        )
+
+    errors = []
+
+    ssaf_clang = _resolve_ssaf_tool(
+        "--ssaf-clang",
+        "clang",
+        command_line_opts.ssaf_clang,
+        installed_shim_dir_path,
+        errors,
+    )
+    ssaf_linker = _resolve_ssaf_tool(
+        "--ssaf-linker",
+        "clang-ssaf-linker",
+        command_line_opts.ssaf_linker,
+        installed_shim_dir_path,
+        errors,
+    )
+    ssaf_analyzer = (
+        _resolve_ssaf_tool(
+            "--ssaf-analyzer",
+            "clang-ssaf-analyzer",
+            command_line_opts.ssaf_analyzer,
+            installed_shim_dir_path,
+            errors,
+        )
+        if command_line_opts.analyses
+        else None
+    )
+
+    if errors:
+        return None, options.Problem(
+            options.ENVIRONMENT_ERROR, "required tools not resolvable:", errors
+        )
+
+    workflow_config = config.SSAFWorkflowConfig(
+        output_dir=output_dir,
+        installed_shim_dir_path=installed_shim_dir_path,
+        restored_path_env_var=env.get("PATH", ""),
+        verbose=command_line_opts.verbose,
+        build_command=command_line_opts.build_command,
+        ssaf_analyzer=ssaf_analyzer,
+        keep_going=command_line_opts.keep_going,
+        build_environment=env,
+    )
+    analysis_config = config.SSAFAnalysisPassConfig(
+        ssaf_clang=ssaf_clang,
+        ssaf_extra_clang_args=command_line_opts.ssaf_extra_clang_args,
+        extractors=command_line_opts.extract_summaries,
+        ssaf_linker=ssaf_linker,
+        analyses=command_line_opts.analyses,
+    )
+    return (workflow_config, analysis_config), None
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
new file mode 100644
index 00000000000000..bfed688447cebd
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config_shims.py
@@ -0,0 +1,31 @@
+# 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 json
+import os
+
+from . import config
+
+
+def from_json(written):
+    shared = config.SSAFWorkflowConfig.written_field_names()
+    workflow_config = config.SSAFWorkflowConfig(
+        **{key: value for key, value in written.items() if key in shared}
+    )
+    own = {
+        key: value
+        for key, value in written.items()
+        if key not in shared and key != "pass_kind"
+    }
+    config.SSAFPassKind(written["pass_kind"])
+    return workflow_config, config.SSAFAnalysisPassConfig(**own)
+
+
+def bootstrap_from_env(env=None):
+    env = os.environ if env is None else env
+    path = env.get(config.SSAF_CONFIG_ENV_VAR)
+    if not path:
+        return None, None
+    with open(path) as handle:
+        return from_json(json.load(handle))
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
new file mode 100644
index 00000000000000..fc242f39dc8fd5
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/failure_logs.py
@@ -0,0 +1,48 @@
+# 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 enum
+import hashlib
+import os
+
+from . import config
+from .utils import coalesce_root_path, find_with_suffix
+
+FAILURE_LOG_SUFFIX = ".log"
+
+
+class SSAFFailureKind(enum.Enum):
+    EXTRACT = "extract"
+    LINK = "link"
+    FORWARD = "forward"
+    MISSING = "missing"
+    DRIVER_JOBS = "driver-jobs"
+    INTERNAL = "internal"
+
+
+def _command_hash(command):
+    joined = "\0".join(command).encode("utf-8", "surrogateescape")
+    return hashlib.sha1(joined).hexdigest()[:12]
+
+
+def get_failure_log_path(output_dir, ssaf_failure_kind, command, build_artifact_id):
+    if build_artifact_id is None:
+        name = os.path.join(os.getcwd(), _command_hash(command))
+    else:
+        name = build_artifact_id.get_path()
+    return coalesce_root_path(
+        config.failure_log_dir(output_dir),
+        "%s.%s%s" % (name, ssaf_failure_kind.value, FAILURE_LOG_SUFFIX),
+    )
+
+
+def has_failure_log_for(output_dir, build_artifact_id):
+    return any(
+        os.path.exists(get_failure_log_path(output_dir, each, None, build_artifact_id))
+        for each in SSAFFailureKind
+    )
+
+
+def count_failure_logs(output_dir):
+    return len(find_with_suffix(output_dir, FAILURE_LOG_SUFFIX))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/interposition.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/interposition.py
new file mode 100644
index 00000000000000..14c05ac7517507
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/interposition.py
@@ -0,0 +1,31 @@
+# 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
+
+SHIM_PROGRAM = "shim"
+
+
+def create_shim_links(shim_link_dir_path, installed_shim_dir_path):
+    os.makedirs(shim_link_dir_path)
+    program = os.path.join(installed_shim_dir_path, SHIM_PROGRAM)
+    if not os.access(program, os.X_OK):
+        raise OSError("shim is not executable: %s" % program)
+    for name in config.SHIM_FOR_LINK_NAME:
+        os.symlink(program, os.path.join(shim_link_dir_path, name))
+    return shim_link_dir_path
+
+
+def make_environment(serialized_config_path, shim_link_dir_path, env):
+    result = dict(env)
+    result[config.SSAF_CONFIG_ENV_VAR] = serialized_config_path
+    build_path = result.get("PATH", "")
+    result["PATH"] = (
+        os.pathsep.join([shim_link_dir_path, build_path])
+        if build_path
+        else shim_link_dir_path
+    )
+    return result
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
new file mode 100644
index 00000000000000..771da5cb22b87b
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/logging_diagnostics.py
@@ -0,0 +1,41 @@
+# 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 sys
+
+from . import PROGRAM
+
+
+def report_note(verbose, message):
+    if verbose:
+        sys.stderr.write("%s: note: %s\n" % (PROGRAM, message))
+
+
+def report_warning(message):
+    sys.stderr.write("%s: warning: %s\n" % (PROGRAM, message))
+
+
+def report_error(message, details=()):
+    sys.stderr.write("%s: error: %s\n" % (PROGRAM, message))
+    for detail in details:
+        sys.stderr.write("  %s\n" % detail)
+
+
+def report_info(key, value):
+    sys.stdout.write("%s: %s\n" % (key, value))
+    sys.stdout.flush()
+
+
+def report_output_dir(output_dir):
+    report_info("output-dir", output_dir)
+
+
+def report_summaries(summaries, link_unit, summary_path):
+    report_info("summaries", summaries)
+    report_info("link-unit", link_unit)
+    report_info("link-unit-summary", summary_path)
+
+
+def report_analysis_result(wpa_result_path):
+    report_info("analysis-result", wpa_result_path)
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
new file mode 100644
index 00000000000000..4650432edd9caa
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/options.py
@@ -0,0 +1,147 @@
+# 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 argparse
+import collections
+
+from . import PROGRAM, VERSION
+
+Problem = collections.namedtuple(
+    "Problem", "exit_status message details", defaults=((),)
+)
+
+USAGE_ERROR = 2
+ENVIRONMENT_ERROR = 1
+
+
+def build_parser():
+    parser = argparse.ArgumentParser(
+        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 "
+        "is paired with an SSAF step that produces the corresponding summary.",
+    )
+    parser.add_argument("--version", action="version", version="%(prog)s " + VERSION)
+    parser.add_argument(
+        "-o",
+        "--output-dir",
+        metavar="<dir>",
+        default="ssaf-output",
+        help="Directory for the SSAF artifacts (default: %(default)s).",
+    )
+    parser.add_argument(
+        "--extract-summaries",
+        metavar="<name,...>",
+        required=True,
+        help="Comma-separated summary extractors to run over every "
+        "translation unit. Required.",
+    )
+    parser.add_argument(
+        "--ssaf-clang",
+        metavar="<path>",
+        default=None,
+        help="SSAF-capable clang used for the SSAF compilations "
+        "(default: clang on PATH).",
+    )
+    parser.add_argument(
+        "--analysis",
+        metavar="<name>",
+        action="append",
+        default=[],
+        dest="analyses",
+        help="Whole-program analysis to run on the link unit summary. "
+        "Repeatable. Without it the run stops after summary extraction.",
+    )
+    parser.add_argument(
+        "--ssaf-linker",
+        metavar="<path>",
+        default=None,
+        help="Path to clang-ssaf-linker (default: found on PATH).",
+    )
+    parser.add_argument(
+        "--ssaf-analyzer",
+        metavar="<path>",
+        default=None,
+        help="Path to clang-ssaf-analyzer (default: found on PATH).",
+    )
+    parser.add_argument(
+        "--ssaf-extra-clang-arg",
+        metavar="<arg>",
+        action="append",
+        default=[],
+        dest="ssaf_extra_clang_args",
+        help="Additional argument for every SSAF compilation. Repeatable.",
+    )
+    parser.add_argument(
+        "-k",
+        "--keep-going",
+        action="store_true",
+        help="Exit successfully even if some SSAF steps failed.",
+    )
+    parser.add_argument(
+        "-v",
+        "--verbose",
+        action="store_true",
+        help="Print every command the orchestrator and the shims run.",
+    )
+    parser.add_argument(
+        "build",
+        nargs=argparse.REMAINDER,
+        metavar="<build command>",
+        help="The build command. A '--' separator before it is optional.",
+    )
+    return parser
+
+
+class CommandLineOpts:
+    def __init__(
+        self,
+        build_command=(),
+        output_dir=None,
+        extract_summaries=None,
+        analyses=(),
+        ssaf_clang=None,
+        ssaf_extra_clang_args=(),
+        ssaf_linker=None,
+        ssaf_analyzer=None,
+        keep_going=False,
+        verbose=False,
+    ):
+        self.build_command = list(build_command)
+        self.output_dir = output_dir
+        self.extract_summaries = extract_summaries
+        self.analyses = list(analyses)
+        self.ssaf_clang = ssaf_clang
+        self.ssaf_extra_clang_args = list(ssaf_extra_clang_args)
+        self.ssaf_linker = ssaf_linker
+        self.ssaf_analyzer = ssaf_analyzer
+        self.keep_going = keep_going
+        self.verbose = verbose
+
+
+def resolve(argv):
+    args = build_parser().parse_args(argv)
+
+    build_command = list(args.build)
+    if build_command and build_command[0] == "--":
+        build_command = build_command[1:]
+    if not build_command:
+        return None, Problem(USAGE_ERROR, "no build command given")
+
+    return (
+        CommandLineOpts(
+            build_command=build_command,
+            output_dir=args.output_dir,
+            extract_summaries=args.extract_summaries,
+            analyses=args.analyses,
+            ssaf_clang=args.ssaf_clang,
+            ssaf_extra_clang_args=args.ssaf_extra_clang_args,
+            ssaf_linker=args.ssaf_linker,
+            ssaf_analyzer=args.ssaf_analyzer,
+            keep_going=args.keep_going,
+            verbose=args.verbose,
+        ),
+        None,
+    )
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
new file mode 100644
index 00000000000000..eb5470da8820da
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/orchestrator.py
@@ -0,0 +1,211 @@
+# 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 json
+import os
+import shutil
+import re
+import subprocess
+
+from . import (
+    build_graph,
+    config,
+    config_orchestrator,
+    failure_logs,
+    interposition,
+    logging_diagnostics,
+    options,
+)
+from .ssaf_artifact_repository import (
+    SSAFArtifactKind,
+    SSAFArtifactRepository,
+    get_ssaf_artifact_id,
+)
+
+
+def warn_if_not_make(build_command):
+    supported = frozenset(["make", "gmake"])
+    for word in build_command:
+        if word in ("env", "/usr/bin/env") or re.match(r"^[A-Za-z_]\w*=", word):
+            continue
+        if os.path.basename(word) in supported:
+            return
+        not_make = word
+        break
+    else:
+        return
+    logging_diagnostics.report_warning(
+        "'%s' is not make; -B will be appended to it anyway, and "
+        "nothing forces the rebuild the artifacts need" % not_make
+    )
+
+
+def write_pass_config(workflow_config, pass_config):
+    path = os.path.join(workflow_config.output_dir, pass_config.file_name)
+    document = workflow_config.to_json()
+    document.update(pass_config.to_json())
+    temporary = "%s.%d.tmp" % (path, os.getpid())
+    with open(temporary, "w") as handle:
+        json.dump(document, handle, indent=2, sort_keys=True)
+    os.replace(temporary, path)
+    return path
+
+
+def _exit_status(returncode):
+    return 128 - returncode if returncode < 0 else returncode
+
+
+def run_build_with_toolchain_interposition(
+    workflow_config, serialized_config_path, shim_link_dir_path, failure_description
+):
+    # Without -B, make skips an up-to-date artifact, so it is never analysed.
+    argv = workflow_config.build_command + ["-B"]
+    logging_diagnostics.report_note(workflow_config.verbose, " ".join(argv))
+    try:
+        status = _exit_status(
+            subprocess.call(
+                argv,
+                env=interposition.make_environment(
+                    serialized_config_path,
+                    shim_link_dir_path,
+                    workflow_config.build_environment,
+                ),
+            )
+        )
+    except OSError as error:
+        return options.Problem(127, "cannot run '%s': %s" % (argv[0], error))
+    if status != 0:
+        return options.Problem(
+            status, "%s failed (exit %d)" % (failure_description, status)
+        )
+    return None
+
+
+def clear_output_dir(output_dir):
+    shutil.rmtree(output_dir, ignore_errors=True)
+    os.makedirs(output_dir)
+    open(config.output_dir_marker_path(output_dir), "w").close()
+
+
+def _no_link_unit(message):
+    return None, None, options.Problem(1, message)
+
+
+def get_topmost_link_unit(ssaf_artifact_repo, output_dir):
+    graph = build_graph.BuildGraph(config.build_graph_dir(output_dir))
+    candidates = graph.find_topmost_outputs(graph.get_recorded_build_steps())
+    if not candidates:
+        return _no_link_unit(
+            "the build produced no link unit; whole-program analysis needs one, "
+            "so build a target that links"
+        )
+    if len(candidates) > 1:
+        return _no_link_unit(
+            "the build produced %d unconsumed outputs and none of them consumes "
+            "the others: %s. Build one of them on its own."
+            % (len(candidates), ", ".join(candidates))
+        )
+
+    build_artifact_id = build_graph.BuildArtifactId(candidates[0])
+    ssaf_artifact_id = get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.SUMMARY)
+    if not ssaf_artifact_repo.has_artifact(ssaf_artifact_id):
+        return _no_link_unit(
+            "no summary for '%s'; the SSAF step that produces it did not run "
+            "or failed" % build_artifact_id.get_path()
+        )
+    summary = ssaf_artifact_repo.get_path_for_artifact(ssaf_artifact_id)
+    return build_artifact_id, summary, None
+
+
+def run_ssaf_analysis_pass(
+    workflow_config, analysis_config, ssaf_artifact_repo, shim_link_dir_path
+):
+    serialized_config_path = write_pass_config(workflow_config, analysis_config)
+    problem = run_build_with_toolchain_interposition(
+        workflow_config, serialized_config_path, shim_link_dir_path, "build"
+    )
+    if problem is not None:
+        return problem
+
+    summaries = ssaf_artifact_repo.count_summaries()
+    if summaries == 0:
+        return options.Problem(1, "no summaries were extracted")
+
+    link_unit, summary, problem = get_topmost_link_unit(
+        ssaf_artifact_repo, workflow_config.output_dir
+    )
+    if problem is not None:
+        return problem
+
+    logging_diagnostics.report_summaries(summaries, link_unit.get_path(), summary)
+
+    if analysis_config.analyses:
+        analyze = [
+            workflow_config.ssaf_analyzer,
+            summary,
+            "-o",
+            config.wpa_result_path(workflow_config.output_dir),
+        ]
+        for name in analysis_config.analyses:
+            analyze += ["-a", name]
+        logging_diagnostics.report_note(workflow_config.verbose, " ".join(analyze))
+        status = _exit_status(subprocess.call(analyze))
+        if status != 0:
+            return options.Problem(
+                status, "whole-program analysis failed (exit %d)" % status
+            )
+        logging_diagnostics.report_analysis_result(
+            config.wpa_result_path(workflow_config.output_dir)
+        )
+
+    return None
+
+
+def run_ssaf_workflow(workflow_config, analysis_config):
+    warn_if_not_make(workflow_config.build_command)
+
+    ssaf_artifact_repo = SSAFArtifactRepository(
+        config.ssaf_artifact_dir(workflow_config.output_dir)
+    )
+    try:
+        clear_output_dir(workflow_config.output_dir)
+        shim_link_dir_path = interposition.create_shim_links(
+            config.shim_link_dir(workflow_config.output_dir),
+            workflow_config.installed_shim_dir_path,
+        )
+    except OSError as error:
+        return options.Problem(
+            options.ENVIRONMENT_ERROR,
+            "cannot prepare the output directory: %s" % error,
+        )
+
+    problem = run_ssaf_analysis_pass(
+        workflow_config, analysis_config, ssaf_artifact_repo, shim_link_dir_path
+    )
+    if problem is not None:
+        return problem
+
+    logging_diagnostics.report_output_dir(workflow_config.output_dir)
+
+    failures = failure_logs.count_failure_logs(workflow_config.output_dir)
+    if failures:
+        return options.Problem(
+            0 if workflow_config.keep_going else 1,
+            "%d SSAF step(s) failed; see the log files under %s"
+            % (failures, workflow_config.output_dir),
+        )
+    return None
+
+
+def main(argv=None, env=None):
+    env = dict(os.environ) if env is None else env
+    command_line_opts, problem = options.resolve(argv)
+    if problem is None:
+        configs, problem = config_orchestrator.configure(command_line_opts, env)
+    if problem is None:
+        problem = run_ssaf_workflow(*configs)
+    if problem is not None:
+        logging_diagnostics.report_error(problem.message, problem.details)
+        return problem.exit_status
+    return 0
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/__init__.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/__init__.py
new file mode 100644
index 00000000000000..44390e67aacf47
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/__init__.py
@@ -0,0 +1,3 @@
+# 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
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
new file mode 100644
index 00000000000000..7525dfc22b7ee7
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/clang.py
@@ -0,0 +1,128 @@
+# 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 re
+import subprocess
+
+from .. import 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 .driver_jobs import Job, parse_driver_job_listing
+from .shim import SKIP_BINARY_TOOL_INVOCATION
+
+
+def _shim_name_for(tool):
+    """Strip the directory and any version suffix from a tool's path."""
+    return re.sub(r"-\d+$", "", os.path.basename(tool))
+
+
+def _output_build_artifact_id(job, cwd):
+    if not job.output:
+        return None
+    return BuildArtifactId(os.path.join(cwd, job.output))
+
+
+def _invoke_shim_for_job(shim, job, cwd):
+    name = _shim_name_for(job.argv[0]) if job.argv else ""
+    link = os.path.join(shim.shim_link_dir, name) if name else ""
+    if not link or not os.path.isfile(link):
+        logging_diagnostics.report_note(
+            shim.workflow_config.verbose,
+            "no shim stands in for %s; the compiler has already run it" % name,
+        )
+        return
+    shim.run(
+        [link] + job.argv[1:] + [SKIP_BINARY_TOOL_INVOCATION],
+        SSAFFailureKind.FORWARD,
+        _output_build_artifact_id(job, cwd),
+        relay_output=True,
+    )
+
+
+def _get_driver_jobs(shim, listing_command, cwd):
+    logging_diagnostics.report_note(
+        shim.workflow_config.verbose, "binary: %s" % " ".join(listing_command)
+    )
+    completed = subprocess.run(
+        listing_command,
+        cwd=cwd,
+        env=shim.get_unmodified_env(),
+        stdout=subprocess.PIPE,
+        stderr=subprocess.PIPE,
+    )
+    return parse_driver_job_listing(completed.stderr), completed
+
+
+def _invoke_all_jobs(shim, bin_tool_path, cwd):
+    listing_command = [bin_tool_path, "-###"] + shim.argv[1:]
+    jobs, completed = _get_driver_jobs(shim, listing_command, cwd)
+    if not jobs:
+        if completed.returncode != 0:
+            shim.log_failure(
+                listing_command,
+                completed,
+                SSAFFailureKind.DRIVER_JOBS,
+                _output_build_artifact_id(Job(list(shim.argv[1:])), cwd),
+            )
+        else:
+            logging_diagnostics.report_note(
+                shim.workflow_config.verbose, "no jobs for %s" % " ".join(shim.argv[1:])
+            )
+        return
+
+    for job in jobs:
+        _invoke_shim_for_job(shim, job, cwd)
+
+
+def _summary_extraction_args(shim, build_artifact_id):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    summary = utils.make_parents(
+        ssaf_artifact_repo.get_path_for_artifact(
+            get_ssaf_artifact_id(build_artifact_id, SSAFArtifactKind.SUMMARY)
+        )
+    )
+    extra = [
+        "--ssaf-extract-summaries=" + shim.pass_config.extractors,
+        "--ssaf-compilation-unit-id=" + build_artifact_id.get_path(),
+        "--ssaf-tu-summary-file=" + summary,
+    ]
+    return summary, extra + shim.pass_config.ssaf_extra_clang_args
+
+
+def _run_clang_driven_tool(shim, cwd):
+    job = Job(list(shim.argv[1:]))
+
+    if not job.output:
+        logging_diagnostics.report_note(
+            shim.workflow_config.verbose, "no output, no SSAF step to run: %r" % (job,)
+        )
+        return
+
+    output = os.path.join(cwd, job.output)
+    build_artifact_id = BuildArtifactId(output)
+    artifact, extra = _summary_extraction_args(shim, build_artifact_id)
+    ssaf_command = [shim.pass_config.ssaf_clang] + shim.argv[1:] + extra
+
+    if not shim.run(ssaf_command, SSAFFailureKind.EXTRACT, build_artifact_id):
+        try:
+            os.remove(artifact)
+        except OSError:
+            pass
+        logging_diagnostics.report_error(
+            "discarded incomplete artifact for %s" % build_artifact_id.get_path()
+        )
+
+
+def invoke_ssaf_tools(shim, cwd):
+    if shim.skip_binary_tool_invocation:
+        _run_clang_driven_tool(shim, cwd)
+    else:
+        _invoke_all_jobs(shim, shim.bin_tool_path(), cwd)
+
+
+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
new file mode 100644
index 00000000000000..2eaf808e87095f
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/commands.py
@@ -0,0 +1,49 @@
+# 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 collections
+import os
+
+from ..build_graph import BuildArtifactId
+from ..ssaf_artifact_repository import SSAFArtifactKind, get_ssaf_artifact_id
+
+Files = collections.namedtuple("Files", "inputs output")
+
+_OBJECT_EXTENSIONS = frozenset([".o", ".obj"])
+
+
+def is_object(filename):
+    return os.path.splitext(filename)[1] in _OBJECT_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):
+        return None
+    return ssaf_artifact_repo.get_path_for_artifact(ssaf_artifact_id)
+
+
+def summary_of(ssaf_artifact_repo, build_artifact_id):
+    return artifact_of(ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.SUMMARY)
+
+
+def filter_build_artifacts(build_graph, ssaf_artifact_repo, cwd, files):
+    kept = []
+    for each in files:
+        build_artifact_id = BuildArtifactId(os.path.join(cwd, each))
+        if artifact_of(
+            ssaf_artifact_repo, build_artifact_id, SSAFArtifactKind.SUMMARY
+        ) or build_graph.was_built(build_artifact_id):
+            kept.append(build_artifact_id)
+    return kept
+
+
+def unique(items):
+    seen = set()
+    kept = []
+    for item in items:
+        if item not in seen:
+            seen.add(item)
+            kept.append(item)
+    return kept
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/driver_jobs.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/driver_jobs.py
new file mode 100644
index 00000000000000..5f0e0e96f5d85f
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/driver_jobs.py
@@ -0,0 +1,58 @@
+# 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 re
+
+_JOB_PREFIX = b' "'
+
+_ESCAPE_PAIR = re.compile(rb'\\[\\"$]')
+_ARGUMENT = re.compile(rb'"((?:[^"\\]|\\.)*)"', re.DOTALL)
+_UNESCAPE = re.compile(rb'\\(["\\$])')
+
+
+def _quotes_balanced(record):
+    return _ESCAPE_PAIR.sub(b"", record).count(b'"') % 2 == 0
+
+
+def _records(data):
+    lines = data.split(b"\n")
+    found = []
+    index = 0
+    while index < len(lines):
+        line = lines[index]
+        if line.startswith(_JOB_PREFIX):
+            record = line
+            while not _quotes_balanced(record) and index + 1 < len(lines):
+                index += 1
+                record += b"\n" + lines[index]
+            found.append(record)
+        index += 1
+    return found
+
+
+def _tokenize(record):
+    return [
+        os.fsdecode(_UNESCAPE.sub(rb"\1", match.group(1)))
+        for match in _ARGUMENT.finditer(record)
+    ]
+
+
+class Job:
+    def __init__(self, argv):
+        self.argv = argv
+
+    @property
+    def output(self):
+        for index, arg in enumerate(self.argv):
+            if arg == "-o" and index + 1 < len(self.argv):
+                return self.argv[index + 1]
+        return None
+
+    def __repr__(self):
+        return "Job(argv=%r, output=%r)" % (self.argv, self.output)
+
+
+def parse_driver_job_listing(data):
+    return [Job(_tokenize(record)) for record in _records(data)]
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
new file mode 100644
index 00000000000000..d7fe1d65ccd726
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/ld.py
@@ -0,0 +1,88 @@
+# 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 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_object,
+    summary_of,
+    unique,
+)
+
+
+def identify_inputs_output(argv):
+    inputs = []
+    output = None
+    index = 1
+    while index < len(argv):
+        arg = argv[index]
+        if arg == "-o" and index + 1 < len(argv):
+            output = argv[index + 1]
+            index += 2
+            continue
+        if not arg.startswith("-") and is_object(arg):
+            inputs.append(arg)
+        index += 1
+    return Files(inputs, output) if inputs else None
+
+
+def link_summaries(shim, build_artifact_id, input_build_artifact_ids):
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    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 link" % 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] + summaries + ["-o", target],
+        SSAFFailureKind.LINK,
+        build_artifact_id,
+    )
+
+
+def invoke_ssaf_tools(shim, cwd):
+    files = identify_inputs_output(shim.argv)
+    if files is None:
+        logging_diagnostics.report_note(
+            shim.workflow_config.verbose, "nothing linked, no SSAF step to run"
+        )
+        return
+
+    ssaf_artifact_repo = shim.ssaf_artifact_repo
+    output = os.path.join(cwd, files.output or "a.out")
+    build_artifact_id = BuildArtifactId(output)
+    input_build_artifact_ids = filter_build_artifacts(
+        shim.build_graph, ssaf_artifact_repo, cwd, files.inputs
+    )
+
+    shim.build_graph.record_build_step(
+        build_artifact_id,
+        inputs=input_build_artifact_ids,
+    )
+    link_summaries(shim, build_artifact_id, input_build_artifact_ids)
+
+
+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/run.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/run.py
new file mode 100644
index 00000000000000..a345390a319bc7
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/run.py
@@ -0,0 +1,23 @@
+# 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_shims
+from .shim import Shim, error_no_config
+
+
+def run_shim(invoke_ssaf_tools, argv, env=None):
+    workflow_config, pass_config = config_shims.bootstrap_from_env(env)
+    if workflow_config is None:
+        return error_no_config()
+
+    shim = Shim(workflow_config, pass_config, argv)
+    cwd = os.getcwd()
+    if not shim.skip_binary_tool_invocation:
+        status = shim.run_bin_tool(shim.bin_tool_path(), cwd)
+        if status != 0:
+            return status
+
+    return shim.protect(lambda: invoke_ssaf_tools(shim, cwd))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
new file mode 100644
index 00000000000000..114135950896bb
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/shims/shim.py
@@ -0,0 +1,183 @@
+# 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 signal
+import subprocess
+import sys
+import traceback
+
+from .. import build_graph, config, failure_logs, logging_diagnostics, utils
+from ..failure_logs import SSAFFailureKind
+from ..ssaf_artifact_repository import SSAFArtifactRepository
+
+SKIP_BINARY_TOOL_INVOCATION = "--ssaf-shim-skip-binary-tool-invocation"
+
+
+def error_no_config():
+    logging_diagnostics.report_error(
+        "missing configuration file",
+        [
+            "%s is not set, so this is not an orchestrator run"
+            % config.SSAF_CONFIG_ENV_VAR
+        ],
+    )
+    return 1
+
+
+class Shim:
+    def __init__(self, workflow_config, pass_config, argv):
+        self.workflow_config = workflow_config
+        self.pass_config = pass_config
+        argv = list(argv)
+        self.skip_binary_tool_invocation = (
+            len(argv) > 1 and argv[-1] == SKIP_BINARY_TOOL_INVOCATION
+        )
+        if self.skip_binary_tool_invocation:
+            del argv[-1]
+        self.argv = argv
+        self.invoked_as = os.path.basename(self.argv[0])
+        self._ssaf_artifact_repo = None
+        self._build_graph = None
+
+    @property
+    def ssaf_artifact_repo(self):
+        if self._ssaf_artifact_repo is None:
+            self._ssaf_artifact_repo = SSAFArtifactRepository(
+                config.ssaf_artifact_dir(self.workflow_config.output_dir)
+            )
+        return self._ssaf_artifact_repo
+
+    @property
+    def build_graph(self):
+        if self._build_graph is None:
+            self._build_graph = build_graph.BuildGraph(
+                config.build_graph_dir(self.workflow_config.output_dir)
+            )
+        return self._build_graph
+
+    @property
+    def shim_link_dir(self):
+        return config.shim_link_dir(self.workflow_config.output_dir)
+
+    def log_failure(self, command, completed, ssaf_failure_kind, build_artifact_id):
+        log_path = utils.make_parents(
+            failure_logs.get_failure_log_path(
+                self.workflow_config.output_dir,
+                ssaf_failure_kind,
+                command,
+                build_artifact_id,
+            )
+        )
+        with open(log_path, "wb") as handle:
+            handle.write(("$ " + " ".join(command) + "\n").encode("utf-8"))
+            handle.write(completed.stdout or b"")
+            handle.write(completed.stderr or b"")
+        logging_diagnostics.report_error(
+            "SSAF step failed (exit %d), see %s" % (completed.returncode, log_path)
+        )
+
+    def log_missing_summary(self, build_artifact_id, input_build_artifact_id):
+        output_dir = self.workflow_config.output_dir
+        path = input_build_artifact_id.get_path()
+        if failure_logs.has_failure_log_for(output_dir, input_build_artifact_id):
+            logging_diagnostics.report_note(
+                self.workflow_config.verbose,
+                "no summary for %s; already reported" % path,
+            )
+            return
+        message = (
+            "no summary for %s, consumed by %s; nothing it was assembled from "
+            "was analysed" % (path, build_artifact_id.get_path())
+        )
+        log = utils.make_parents(
+            failure_logs.get_failure_log_path(
+                output_dir, SSAFFailureKind.MISSING, self.argv, input_build_artifact_id
+            )
+        )
+        with open(log, "w") as handle:
+            handle.write(message + "\n")
+        logging_diagnostics.report_error(message)
+
+    def get_unmodified_env(self):
+        result = dict(os.environ)
+        result.pop(config.SSAF_CONFIG_ENV_VAR, None)
+        result["PATH"] = self.workflow_config.restored_path_env_var
+        return result
+
+    def bin_tool_path(self):
+        return shutil.which(
+            self.invoked_as, path=self.workflow_config.restored_path_env_var
+        )
+
+    def run_bin_tool(self, bin_tool_path, cwd):
+        if not bin_tool_path:
+            logging_diagnostics.report_error(
+                "cannot find the binary %s" % self.invoked_as
+            )
+            return 127
+        command = [bin_tool_path] + self.argv[1:]
+        logging_diagnostics.report_note(
+            self.workflow_config.verbose, "binary: %s" % " ".join(command)
+        )
+        try:
+            status = subprocess.call(command, cwd=cwd, env=self.get_unmodified_env())
+        except OSError as error:
+            logging_diagnostics.report_error("cannot run %s: %s" % (command[0], error))
+            return 127
+        if status < 0:
+            return self._propagate_process_return_status(status)
+        return status
+
+    def _propagate_process_return_status(self, return_status):
+        signal_number = -return_status
+        try:
+            signal.signal(signal_number, signal.SIG_DFL)
+            os.kill(os.getpid(), signal_number)
+        except OSError:
+            pass
+        return 128 + signal_number
+
+    def run(self, command, ssaf_failure_kind, build_artifact_id, relay_output=False):
+        logging_diagnostics.report_note(
+            self.workflow_config.verbose, "ssaf: %s" % " ".join(command)
+        )
+        try:
+            completed = subprocess.run(
+                command, stdout=subprocess.PIPE, stderr=subprocess.STDOUT
+            )
+            if relay_output and completed.stdout:
+                sys.stderr.write(completed.stdout.decode("utf-8", "replace"))
+                sys.stderr.flush()
+        except OSError as error:
+            logging_diagnostics.report_error("cannot run %s: %s" % (command[0], error))
+            return False
+        if completed.returncode == 0:
+            return True
+        self.log_failure(command, completed, ssaf_failure_kind, build_artifact_id)
+        return False
+
+    def protect(self, work):
+        try:
+            work()
+        except Exception:  # noqa: BLE001 - deliberately everything
+            logging_diagnostics.report_error("internal error in %s" % self.invoked_as)
+            traceback.print_exc(file=sys.stderr)
+            try:
+                path = utils.make_parents(
+                    failure_logs.get_failure_log_path(
+                        self.workflow_config.output_dir,
+                        SSAFFailureKind.INTERNAL,
+                        self.argv,
+                        None,
+                    )
+                )
+                with open(path, "w") as handle:
+                    handle.write("%s\n\n" % " ".join(self.argv))
+                    traceback.print_exc(file=handle)
+            except Exception:
+                logging_diagnostics.report_error("and its log could not be written")
+        # A shim must never be the reason a build fails.
+        return 0
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
new file mode 100644
index 00000000000000..674d5c4ccbbf66
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py
@@ -0,0 +1,58 @@
+# 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 enum
+import os
+
+from .utils import coalesce_root_path, find_with_suffix
+
+
+class SSAFArtifactKind(enum.Enum):
+    SUMMARY = ".summary.json"
+
+
+class SSAFArtifactId:
+    __slots__ = ("build_artifact_id", "ssaf_artifact_kind")
+
+    def __init__(self, build_artifact_id, ssaf_artifact_kind):
+        self.build_artifact_id = build_artifact_id
+        self.ssaf_artifact_kind = ssaf_artifact_kind
+
+    def get_build_artifact_id(self):
+        return self.build_artifact_id
+
+    def __repr__(self):
+        return "SSAFArtifactId(%r, %r)" % (
+            self.build_artifact_id,
+            self.ssaf_artifact_kind,
+        )
+
+
+def get_ssaf_artifact_id(build_artifact_id, ssaf_artifact_kind):
+    return SSAFArtifactId(build_artifact_id, ssaf_artifact_kind)
+
+
+class SSAFArtifactRepository:
+    def __init__(self, ssaf_artifact_dir):
+        self.ssaf_artifact_dir = os.path.abspath(ssaf_artifact_dir)
+
+    def get_build_artifact_path(self, ssaf_artifact_id):
+        return ssaf_artifact_id.get_build_artifact_id().get_path()
+
+    def get_path_for_artifact(self, ssaf_artifact_id):
+        return coalesce_root_path(
+            self.ssaf_artifact_dir,
+            self.get_build_artifact_path(ssaf_artifact_id)
+            + ssaf_artifact_id.ssaf_artifact_kind.value,
+        )
+
+    def has_artifact(self, ssaf_artifact_id):
+        return os.path.exists(self.get_path_for_artifact(ssaf_artifact_id))
+
+    def _all_paths_of_kind(self, ssaf_artifact_kind):
+        return find_with_suffix(self.ssaf_artifact_dir, ssaf_artifact_kind.value)
+
+    def count_summaries(self):
+        return len(self._all_paths_of_kind(SSAFArtifactKind.SUMMARY))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/utils.py b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/utils.py
new file mode 100644
index 00000000000000..eae135ef4a6266
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/utils.py
@@ -0,0 +1,54 @@
+# 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 errno
+import json
+import os
+
+from . import logging_diagnostics
+
+
+def coalesce_root_path(root, path):
+    coalesced = os.path.normpath(os.path.join(root, path.lstrip(os.sep)))
+    if coalesced != root and not coalesced.startswith(root + os.sep):
+        raise ValueError("path leaves %s: %s" % (root, coalesced))
+    return coalesced
+
+
+def find_with_suffix(root, suffix):
+    found = []
+    for directory, _, filenames in os.walk(root):
+        for name in sorted(filenames):
+            if name.endswith(suffix):
+                found.append(os.path.join(directory, name))
+    return found
+
+
+def make_parents(path):
+    directory = os.path.dirname(path)
+    if directory:
+        try:
+            os.makedirs(directory)
+        except OSError as error:
+            if error.errno != errno.EEXIST:
+                raise
+    return path
+
+
+def read_json(path):
+    try:
+        with open(path) as handle:
+            return json.load(handle)
+    except OSError:
+        return None
+    except ValueError as error:
+        logging_diagnostics.report_error("%s is not valid JSON: %s" % (path, error))
+        return None
+
+
+def write_json(path, document):
+    temporary = "%s.%d.tmp" % (path, os.getpid())
+    with open(temporary, "w") as handle:
+        json.dump(document, handle, indent=2)
+    os.replace(temporary, path)
diff --git a/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/version.py.in b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/version.py.in
new file mode 100644
index 00000000000000..69b39a828847af
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/version.py.in
@@ -0,0 +1,5 @@
+# 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
+
+VERSION = "@CLANG_VERSION@"
diff --git a/clang/tools/clang-ssaf-orchestrator-make/shims/shim b/clang/tools/clang-ssaf-orchestrator-make/shims/shim
new file mode 100755
index 00000000000000..122b203fd2728a
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/shims/shim
@@ -0,0 +1,45 @@
+#!/usr/bin/env python3
+# 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 sys
+from importlib import import_module
+
+
+def _add_package_to_path():
+    here = os.path.dirname(os.path.realpath(__file__))
+    while True:
+        candidate = os.path.join(here, "lib")
+        if os.path.isdir(os.path.join(candidate, "clang_ssaf_orchestrator_make")):
+            sys.path.insert(0, candidate)
+            return
+        parent = os.path.dirname(here)
+        if parent == here:
+            raise ImportError(
+                "cannot find clang_ssaf_orchestrator_make beside %s" % __file__
+            )
+        here = parent
+
+
+_add_package_to_path()
+
+from clang_ssaf_orchestrator_make import config  # noqa: E402
+from clang_ssaf_orchestrator_make import logging_diagnostics  # noqa: E402
+
+
+def main(argv):
+    invoked_as = os.path.basename(argv[0])
+    name = config.SHIM_FOR_LINK_NAME.get(invoked_as)
+    if name is None:
+        logging_diagnostics.report_error(
+            "no shim stands in for %s" % invoked_as,
+            ["this is reached through the links an orchestrator run creates"],
+        )
+        return 1
+    return import_module("clang_ssaf_orchestrator_make.shims." + name).main(argv)
+
+
+if __name__ == "__main__":
+    sys.exit(main(sys.argv))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/__init__.py b/clang/tools/clang-ssaf-orchestrator-make/tests/__init__.py
new file mode 100644
index 00000000000000..44390e67aacf47
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/__init__.py
@@ -0,0 +1,3 @@
+# 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
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py b/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
new file mode 100644
index 00000000000000..9a359514de7bc2
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/configs.py
@@ -0,0 +1,84 @@
+# 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 contextlib
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import build_graph, config, logging_diagnostics
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+
+
+def analysis_config(**overrides):
+    fields = dict(
+        ssaf_clang="/bin/clang",
+        ssaf_extra_clang_args=[],
+        extractors="PointerFlow",
+        ssaf_linker="/bin/clang-ssaf-linker",
+        analyses=[],
+    )
+    fields.update(overrides)
+    return config.SSAFAnalysisPassConfig(**fields)
+
+
+def workflow_config(env=None, output_dir="/out", verbose=False):
+    env = env or {}
+    return config.SSAFWorkflowConfig(
+        output_dir=output_dir,
+        installed_shim_dir_path="/libexec",
+        restored_path_env_var=env.get("PATH", ""),
+        verbose=verbose,
+        build_command=["make"],
+        ssaf_analyzer="/bin/clang-ssaf-analyzer",
+        keep_going=False,
+        build_environment=env,
+    )
+
+
+ at contextlib.contextmanager
+def recorded_notes():
+    notes = []
+    with mock.patch.object(
+        logging_diagnostics,
+        "report_note",
+        lambda verbose, message: notes.append(message),
+    ):
+        yield notes
+
+
+class RecordingShim:
+    """A Shim whose SSAF invocations are recorded rather than run."""
+
+    def __init__(self, argv, output_dir, pass_config=None):
+        from clang_ssaf_orchestrator_make.shims import shim as shim_module
+
+        self.commands = []
+        self.relayed = []
+        self.missing = []
+        self._shim = shim_module.Shim(
+            workflow_config(output_dir=output_dir),
+            pass_config or analysis_config(),
+            argv,
+        )
+        self._shim.run = self._run
+        self._shim.log_missing_summary = (
+            lambda build_artifact_id, input_build_artifact_id: self.missing.append(
+                input_build_artifact_id.get_path()
+            )
+        )
+
+    def _run(self, command, ssaf_failure_kind, build_artifact_id, relay_output=False):
+        self.commands.append(((ssaf_failure_kind, build_artifact_id), command))
+        self.relayed.append(relay_output)
+        return True
+
+    def __getattr__(self, name):
+        return getattr(self._shim, name)
+
+
+def repository_for(output_dir):
+    return repository.SSAFArtifactRepository(config.ssaf_artifact_dir(output_dir))
+
+
+def build_graph_for(output_dir):
+    return build_graph.BuildGraph(config.build_graph_dir(output_dir))
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_build_graph.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_build_graph.py
new file mode 100644
index 00000000000000..f5caa9363858ae
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_build_graph.py
@@ -0,0 +1,222 @@
+# 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 io
+import os
+import shutil
+import tempfile
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import build_graph, utils
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+
+from . import configs
+
+
+def _id(build_artifact_path):
+    return build_graph.BuildArtifactId(build_artifact_path)
+
+
+class BuildArtifactIds(unittest.TestCase):
+    def test_an_id_is_the_path_without_the_leading_separator(self):
+        self.assertEqual(
+            "p/build/foo.o",
+            build_graph.BuildArtifactId("/p/build/foo.o").get_path().lstrip(os.sep),
+        )
+
+    def test_the_id_is_independent_of_how_the_path_was_spelled(self):
+        self.assertEqual(
+            build_graph.BuildArtifactId("/p/build/foo.o"),
+            build_graph.BuildArtifactId("/p/./build/../build/foo.o"),
+        )
+
+    def test_nothing_is_left_to_climb_out_of_the_output_directory_with(self):
+        build_artifact_id = build_graph.BuildArtifactId("/elsewhere/../deep/foo.o")
+        self.assertEqual("/deep/foo.o", build_artifact_id.get_path())
+        self.assertNotIn("..", build_artifact_id.get_path())
+
+    def test_two_paths_are_two_ids(self):
+        first = build_graph.BuildArtifactId("/a/foo.o")
+        second = build_graph.BuildArtifactId("/b/foo.o")
+        self.assertNotEqual(first, second)
+        self.assertEqual(first, build_graph.BuildArtifactId("/a/foo.o"))
+
+    def test_same_basename_in_different_directories_do_not_collide(self):
+        self.assertNotEqual(
+            build_graph.BuildArtifactId("/p/build/arm64/parser.o"),
+            build_graph.BuildArtifactId("/p/build/x86_64/parser.o"),
+        )
+        self.assertEqual(
+            "p/build/arm64/parser.o",
+            build_graph.BuildArtifactId("/p/build/arm64/parser.o")
+            .get_path()
+            .lstrip(os.sep),
+        )
+
+    def test_no_part_of_the_path_is_elided(self):
+        self.assertEqual(
+            "p/a/very/deep/nested/dir/parser.o",
+            build_graph.BuildArtifactId("/p/a/very/deep/nested/dir/parser.o")
+            .get_path()
+            .lstrip(os.sep),
+        )
+
+
+class SymlinkedPaths(unittest.TestCase):
+    def setUp(self):
+        self.tmp = os.path.realpath(tempfile.mkdtemp())
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.real = os.path.join(self.tmp, "real")
+        os.makedirs(os.path.join(self.real, "build"))
+        self.link = os.path.join(self.tmp, "link")
+        os.symlink(self.real, self.link)
+
+    def test_an_artifact_reached_through_a_symlink_gets_the_real_path_id(self):
+        directly = os.path.join(self.real, "build", "a.o")
+        self.assertEqual(
+            directly,
+            build_graph.BuildArtifactId(
+                os.path.join(self.link, "build", "a.o")
+            ).get_path(),
+        )
+
+
+class BuildstepRecords(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.ssaf_artifact_repo = configs.repository_for(self.tmp)
+        self.graph = configs.build_graph_for(self.tmp)
+
+    def test_round_trip(self):
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/build/prg"),
+            [_id("/a.o"), _id("/b.o")],
+        )
+        self.assertEqual(
+            {"/p/build/prg": ["/a.o", "/b.o"]}, self.graph.get_recorded_build_steps()
+        )
+
+    def test_several_build_steps_in_nested_directories(self):
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/libx.a"),
+            [_id("/a.o")],
+        )
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/build/sub/prg"),
+            [_id("/p/libx.a")],
+        )
+        self.assertEqual(
+            {"/p/libx.a", "/p/build/sub/prg"},
+            set(self.graph.get_recorded_build_steps()),
+        )
+
+    def test_one_output_has_one_record_and_the_last_call_wins(self):
+        first = self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/libx.a"),
+            [_id("/a.o")],
+        )
+        second = self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/libx.a"),
+            [_id("/b.o")],
+        )
+        self.assertEqual(first, second)
+        self.assertEqual(["/b.o"], self.graph.get_recorded_build_steps()["/p/libx.a"])
+
+    def test_a_record_whose_output_extends_another_is_not_read_back(self):
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/prg.o"),
+            [_id("/a.o")],
+        )
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/prg"),
+            [_id("/p/prg.o")],
+        )
+        self.assertEqual(["/p/prg.o"], self.graph.get_recorded_build_steps()["/p/prg"])
+
+    def test_a_summary_beside_a_record_is_not_read_as_one(self):
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/prg"),
+            [_id("/a.o")],
+        )
+        open(
+            utils.make_parents(
+                self.ssaf_artifact_repo.get_path_for_artifact(
+                    repository.get_ssaf_artifact_id(
+                        build_graph.BuildArtifactId("/p/prg"),
+                        repository.SSAFArtifactKind.SUMMARY,
+                    )
+                )
+            ),
+            "w",
+        ).close()
+        self.assertEqual(["/a.o"], self.graph.get_recorded_build_steps()["/p/prg"])
+
+    def test_a_truncated_record_is_skipped_and_reported(self):
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId("/p/good"),
+            [_id("/a.o")],
+        )
+        broken = utils.make_parents(
+            self.graph.get_buildstep_record_path(
+                build_graph.BuildArtifactId("/p/broken")
+            )
+        )
+        with open(broken, "w") as handle:
+            handle.write('{"kind": "li')
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.assertEqual(["/p/good"], list(self.graph.get_recorded_build_steps()))
+        self.assertIn("is not valid JSON", stream.getvalue())
+        self.assertIn(broken, stream.getvalue())
+
+    def test_reading_an_absent_directory_yields_nothing(self):
+        self.assertEqual(
+            {},
+            configs.build_graph_for(self.tmp + "/none").get_recorded_build_steps(),
+        )
+
+
+class TopMostOutputs(unittest.TestCase):
+    def _topmost(self, steps):
+        return build_graph.BuildGraph.find_topmost_outputs(steps)
+
+    def test_single_binary(self):
+        self.assertEqual(["/p/prg"], self._topmost({"/p/prg": ["/a.o", "/b.o"]}))
+
+    def test_anything_another_step_consumed_is_not_top_most(self):
+        self.assertEqual(
+            ["/p/prg"],
+            self._topmost(
+                {"/p/libx.a": [_id("/a.o")], "/p/prg": ["main.o", "/p/libx.a"]}
+            ),
+        )
+
+    def test_intermediate_binary_is_not_top_most(self):
+        self.assertEqual(
+            ["/p/prg"],
+            self._topmost({"part.o": [_id("/a.o"), _id("/b.o")], "/p/prg": ["part.o"]}),
+        )
+
+    def test_an_unconsumed_output_is_a_candidate_whatever_produced_it(self):
+        self.assertEqual(["/p/libx.a"], self._topmost({"/p/libx.a": ["/a.o"]}))
+
+    def test_a_library_nothing_links_is_a_candidate_beside_a_binary(self):
+        self.assertEqual(
+            ["/p/libx.a", "/p/prg"],
+            self._topmost({"/p/libx.a": [_id("/a.o")], "/p/prg": ["main.o"]}),
+        )
+
+    def test_several_unrelated_binaries(self):
+        self.assertEqual(
+            ["tests", "tool"], self._topmost({"tool": [_id("/a.o")], "tests": ["b.o"]})
+        )
+
+    def test_nothing_recorded(self):
+        self.assertEqual([], self._topmost({}))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
new file mode 100644
index 00000000000000..720d8bcbe089f1
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
@@ -0,0 +1,244 @@
+# 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 stat
+import tempfile
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make.shims import clang, shim as shim_module
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+from clang_ssaf_orchestrator_make import build_graph
+from clang_ssaf_orchestrator_make.failure_logs import SSAFFailureKind
+
+from . import configs
+
+
+class ForwardingAJob(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.out = os.path.join(self.tmp, "out")
+        self.links = os.path.join(self.out, "shims")
+        os.makedirs(self.links)
+        for name in ("clang", "ld"):
+            path = os.path.join(self.links, name)
+            open(path, "w").close()
+            os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
+        self.shim = configs.RecordingShim(["c++", "-o", "prg"], self.out)
+
+    def _job(self, argv):
+        return clang.Job(argv)
+
+    def test_a_job_whose_tool_has_a_link_is_forwarded_with_the_marker(self):
+        clang._invoke_shim_for_job(
+            self.shim, self._job(["/toolchain/bin/ld", "-o", "prg", "a.o"]), self.tmp
+        )
+        failure, command = self.shim.commands[0]
+        self.assertEqual(
+            (
+                SSAFFailureKind.FORWARD,
+                build_graph.BuildArtifactId(os.path.join(self.tmp, "prg")),
+            ),
+            failure,
+        )
+        self.assertEqual(os.path.join(self.links, "ld"), command[0])
+        self.assertEqual(["-o", "prg", "a.o"], command[1:-1])
+        self.assertEqual(shim_module.SKIP_BINARY_TOOL_INVOCATION, command[-1])
+        self.assertEqual([True], self.shim.relayed)
+
+    def test_a_versioned_tool_still_finds_its_link(self):
+        clang._invoke_shim_for_job(
+            self.shim, self._job(["/build/bin/clang-23", "-cc1", "-o", "a.o"]), self.tmp
+        )
+        self.assertEqual(os.path.join(self.links, "clang"), self.shim.commands[0][1][0])
+
+    def test_a_job_with_no_link_is_noted_and_skipped(self):
+        with configs.recorded_notes() as notes:
+            clang._invoke_shim_for_job(
+                self.shim, self._job(["/usr/bin/dsymutil", "prg"]), self.tmp
+            )
+        self.assertEqual([], self.shim.commands)
+        self.assertIn("no shim stands in for dsymutil", notes[0])
+
+    def test_a_job_with_no_program_at_all_is_skipped(self):
+        clang._invoke_shim_for_job(self.shim, self._job([]), self.tmp)
+        self.assertEqual([], self.shim.commands)
+
+    def test_a_job_with_no_output_is_logged_under_its_command(self):
+        clang._invoke_shim_for_job(
+            self.shim, self._job(["/usr/bin/ld", "a.o"]), self.tmp
+        )
+        self.assertEqual((SSAFFailureKind.FORWARD, None), self.shim.commands[0][0])
+
+
+class ListingTheJobs(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.shim = configs.RecordingShim(
+            ["c++", "-c", "-o", "build/a.o", "a.cpp"],
+            os.path.join(self.tmp, "out"),
+        )
+
+    def _completed(self, stderr, returncode=0):
+        return mock.Mock(stderr=stderr, stdout=b"", returncode=returncode)
+
+    def test_a_listing_with_no_jobs_is_noted(self):
+        with mock.patch(
+            "subprocess.run", return_value=self._completed(b"")
+        ), configs.recorded_notes() as notes:
+            clang._invoke_all_jobs(self.shim, "/bin/clang", self.tmp)
+        self.assertEqual([], self.shim.commands)
+        self.assertEqual("binary: /bin/clang -### -c -o build/a.o a.cpp", notes[0])
+        self.assertIn("no jobs for", notes[1])
+
+    def test_a_driver_that_failed_is_logged_rather_than_noted(self):
+        logged = []
+        self.shim._shim.log_failure = lambda *args: logged.append(args)
+        with mock.patch(
+            "subprocess.run", return_value=self._completed(b"", returncode=1)
+        ):
+            clang._invoke_all_jobs(self.shim, "/bin/clang", self.tmp)
+        command, _, failure_kind, build_artifact_id = logged[0]
+        self.assertEqual(
+            ["/bin/clang", "-###", "-c", "-o", "build/a.o", "a.cpp"], command
+        )
+        self.assertEqual(SSAFFailureKind.DRIVER_JOBS, failure_kind)
+        self.assertEqual(
+            build_graph.BuildArtifactId(os.path.join(self.tmp, "build", "a.o")),
+            build_artifact_id,
+        )
+
+    def test_the_driver_is_asked_with_the_builds_own_arguments(self):
+        with mock.patch("subprocess.run", return_value=self._completed(b"")) as ran:
+            clang._invoke_all_jobs(self.shim, "/bin/clang", self.tmp)
+        self.assertEqual(
+            ["/bin/clang", "-###", "-c", "-o", "build/a.o", "a.cpp"],
+            ran.call_args[0][0],
+        )
+
+
+class TheSsafArguments(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.out = os.path.join(self.tmp, "out")
+
+    def _id(self, relative):
+        return build_graph.BuildArtifactId(os.path.join(self.tmp, relative))
+
+    def _shim(self, pass_config=None):
+        return configs.RecordingShim(
+            ["clang", "-cc1", "-o", "build/a.o", "a.cpp"],
+            self.out,
+            pass_config,
+        )
+
+    def test_extraction_names_the_unit_and_where_its_summary_goes(self):
+        shim = self._shim()
+        summary, extra = clang._summary_extraction_args(shim, self._id("build/a.o"))
+        self.assertIn("--ssaf-extract-summaries=PointerFlow", extra)
+        self.assertIn(
+            "--ssaf-compilation-unit-id=" + self._id("build/a.o").get_path(), extra
+        )
+        self.assertIn("--ssaf-tu-summary-file=" + summary, extra)
+        self.assertTrue(os.path.isdir(os.path.dirname(summary)))
+
+    def test_the_extra_clang_arguments_come_last(self):
+        shim = self._shim(configs.analysis_config(ssaf_extra_clang_args=["-DX"]))
+        _, extra = clang._summary_extraction_args(shim, self._id("build/a.o"))
+        self.assertEqual("-DX", extra[-1])
+
+
+class RunningTheFrontend(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.out = os.path.join(self.tmp, "out")
+
+    def _shim(self, argv):
+        return configs.RecordingShim(argv, self.out)
+
+    def _id(self, relative):
+        return build_graph.BuildArtifactId(os.path.join(self.tmp, relative))
+
+    def test_a_job_with_no_output_runs_nothing(self):
+        shim = self._shim(["clang", "-cc1", "-E", "a.cpp"])
+        with configs.recorded_notes() as notes:
+            clang._run_clang_driven_tool(shim, self.tmp)
+        self.assertEqual([], shim.commands)
+        self.assertIn("no output", notes[0])
+
+    def test_the_ssaf_clang_gets_the_jobs_own_arguments_then_the_options(self):
+        shim = self._shim(["clang", "-cc1", "-o", "build/a.o", "a.cpp"])
+        clang._run_clang_driven_tool(shim, self.tmp)
+        failure, command = shim.commands[0]
+        self.assertEqual((SSAFFailureKind.EXTRACT, self._id("build/a.o")), failure)
+        self.assertEqual("/bin/clang", command[0])
+        self.assertEqual(["-cc1", "-o", "build/a.o", "a.cpp"], command[1:5])
+        self.assertTrue(command[5].startswith("--ssaf-"))
+
+    def test_an_incomplete_artifact_is_removed_and_reported(self):
+        shim = self._shim(["clang", "-cc1", "-o", "build/a.o", "a.cpp"])
+        shim._shim.run = lambda *args: False
+        stream = mock.MagicMock()
+        with mock.patch("sys.stderr", stream):
+            clang._run_clang_driven_tool(shim, self.tmp)
+        records = configs.repository_for(self.out)
+        summary = records.get_path_for_artifact(
+            repository.get_ssaf_artifact_id(
+                build_graph.BuildArtifactId(os.path.join(self.tmp, "build", "a.o")),
+                repository.SSAFArtifactKind.SUMMARY,
+            )
+        )
+        self.assertFalse(os.path.exists(summary))
+
+    def test_the_marker_chooses_the_frontend_half(self):
+        shim = self._shim(
+            [
+                "clang",
+                "-cc1",
+                "-o",
+                "build/a.o",
+                "a.cpp",
+                shim_module.SKIP_BINARY_TOOL_INVOCATION,
+            ]
+        )
+        self.assertTrue(shim.skip_binary_tool_invocation)
+        clang.invoke_ssaf_tools(shim, self.tmp)
+        self.assertEqual(
+            (SSAFFailureKind.EXTRACT, self._id("build/a.o")), shim.commands[0][0]
+        )
+
+
+class ShimNameForTool(unittest.TestCase):
+    def test_the_directory_is_dropped(self):
+        self.assertEqual("ld", clang._shim_name_for("/usr/bin/ld"))
+
+    def test_a_version_suffix_is_dropped(self):
+        self.assertEqual("clang", clang._shim_name_for("/opt/llvm/bin/clang-23"))
+
+    def test_an_unversioned_name_is_left_alone(self):
+        self.assertEqual(
+            "clang",
+            clang._shim_name_for(
+                "/Applications/Xcode.app/Contents/Developer/Toolchains/"
+                "XcodeDefault.xctoolchain/usr/bin/clang"
+            ),
+        )
+
+    def test_only_a_trailing_numeric_suffix_is_dropped(self):
+        self.assertEqual("clang-cl", clang._shim_name_for("/x/clang-cl"))
+        self.assertEqual("ld64.lld", clang._shim_name_for("/x/ld64.lld"))
+
+    def test_a_suffixed_link_name_still_reaches_its_link(self):
+        for name in ("cc", "clang", "c++", "clang++"):
+            self.assertEqual(name, clang._shim_name_for("/x/%s-14" % name))
+
+
+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
new file mode 100644
index 00000000000000..6990f7c304c5cd
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_commands.py
@@ -0,0 +1,114 @@
+# 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 build_graph, utils
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+
+from . import configs
+from clang_ssaf_orchestrator_make.shims import commands
+
+
+class Extensions(unittest.TestCase):
+    def test_object_extensions(self):
+        for name in ("a.o", "build/a.o", "a.obj", "A.O".lower()):
+            self.assertTrue(commands.is_object(name), name)
+        for name in ("a.a", "a.c", "a", "a.o.tmp", "a.O"):
+            self.assertFalse(commands.is_object(name), name)
+
+
+class Unique(unittest.TestCase):
+    def test_the_first_of_each_is_kept_in_order(self):
+        self.assertEqual(["b", "a", "c"], commands.unique(["b", "a", "b", "c", "a"]))
+
+    def test_nothing_stays_nothing(self):
+        self.assertEqual([], commands.unique([]))
+
+    def test_the_input_is_not_modified(self):
+        items = ["a", "a"]
+        commands.unique(items)
+        self.assertEqual(["a", "a"], items)
+
+
+class InputsAndOutput(unittest.TestCase):
+    def test_it_carries_what_went_in_and_what_came_out(self):
+        files = commands.Files(["a.o"], "prg")
+        self.assertEqual(["a.o"], files.inputs)
+        self.assertEqual("prg", files.output)
+
+
+class _OverABuild(unittest.TestCase):
+    def setUp(self):
+        self.root = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.root, True)
+        self.out = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.out, True)
+        self.ssaf_artifact_repo = configs.repository_for(self.out)
+        self.build_graph = configs.build_graph_for(self.out)
+
+    def _path(self, relative):
+        return os.path.join(self.root, relative)
+
+    def _id(self, relative):
+        return build_graph.BuildArtifactId(self._path(relative))
+
+    def _write(self, relative, ssaf_artifact_kind):
+        path = self.ssaf_artifact_repo.get_path_for_artifact(
+            repository.get_ssaf_artifact_id(self._id(relative), ssaf_artifact_kind)
+        )
+        open(utils.make_parents(path), "w").close()
+        return path
+
+
+class SummaryOf(_OverABuild):
+    def test_an_object_resolves_to_its_own_summary(self):
+        want = self._write("build/a.o", repository.SSAFArtifactKind.SUMMARY)
+        self.assertEqual(
+            want, commands.summary_of(self.ssaf_artifact_repo, self._id("build/a.o"))
+        )
+
+    def test_an_artifact_the_build_did_not_produce_resolves_to_nothing(self):
+        self.assertIsNone(
+            commands.summary_of(
+                self.ssaf_artifact_repo,
+                build_graph.BuildArtifactId("/usr/lib/libSystem.dylib"),
+            )
+        )
+
+    def test_an_assembled_artifact_does_not_reach_past_its_own(self):
+        self._write("build/a.o", repository.SSAFArtifactKind.SUMMARY)
+        self.build_graph.record_build_step(
+            self._id("build/lib.a"), [self._id("build/a.o")]
+        )
+        self.assertIsNone(
+            commands.summary_of(self.ssaf_artifact_repo, self._id("build/lib.a"))
+        )
+
+
+class FilterBuildArtifacts(_OverABuild):
+    def _kept(self, *files):
+        return commands.filter_build_artifacts(
+            self.build_graph, self.ssaf_artifact_repo, self.root, list(files)
+        )
+
+    def test_a_compiled_object_is_kept(self):
+        self._write("build/a.o", repository.SSAFArtifactKind.SUMMARY)
+        self.assertEqual([self._id("build/a.o")], self._kept("build/a.o"))
+
+    def test_an_assembled_object_with_only_a_record_is_kept(self):
+        self.build_graph.record_build_step(
+            self._id("build/combined.o"), [self._id("build/a.o")]
+        )
+        self.assertEqual([self._id("build/combined.o")], self._kept("build/combined.o"))
+
+    def test_what_the_build_never_touched_is_dropped(self):
+        self.assertEqual([], self._kept("third_party/prebuilt.a", "/usr/lib/libz.a"))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
new file mode 100644
index 00000000000000..e0aafdc425bcd9
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config.py
@@ -0,0 +1,86 @@
+# 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 json
+import unittest
+
+from clang_ssaf_orchestrator_make import config
+
+from . import configs
+
+
+class Passes(unittest.TestCase):
+    def test_a_pass_names_its_own_configuration_file(self):
+        self.assertEqual("config-analysis.json", configs.analysis_config().file_name)
+
+    def test_a_pass_kind_is_stored_as_its_plain_string(self):
+        written = json.loads(json.dumps(configs.analysis_config().to_json()))
+        self.assertEqual("analysis", written["pass_kind"])
+
+    def test_a_pass_carries_only_the_tools_it_runs(self):
+        self.assertEqual(
+            {
+                "pass_kind",
+                "ssaf_clang",
+                "ssaf_extra_clang_args",
+                "extractors",
+                "ssaf_linker",
+                "analyses",
+            },
+            set(configs.analysis_config().to_json()),
+        )
+
+    def test_a_pass_knows_nothing_of_the_run_it_belongs_to(self):
+        run_only = config.SSAFWorkflowConfig.written_field_names()
+        self.assertEqual(set(), run_only & set(configs.analysis_config().to_json()))
+
+
+class OutputDirPaths(unittest.TestCase):
+    def test_the_shim_links_and_the_analysis_result_sit_at_the_root(self):
+        self.assertEqual("/out/shims", config.shim_link_dir("/out"))
+        self.assertEqual("/out/wpa.json", config.wpa_result_path("/out"))
+
+    def test_each_owner_has_a_directory_of_its_own(self):
+        self.assertEqual("/out/build-graph", config.build_graph_dir("/out"))
+        self.assertEqual("/out/ssaf-artifacts", config.ssaf_artifact_dir("/out"))
+        self.assertEqual("/out/failure-logs", config.failure_log_dir("/out"))
+
+
+class WorkflowConfig(unittest.TestCase):
+    def test_it_carries_what_is_true_of_the_run_whichever_pass_is_running(self):
+        self.assertEqual(
+            {
+                "output_dir",
+                "installed_shim_dir_path",
+                "restored_path_env_var",
+                "verbose",
+                "build_command",
+                "ssaf_analyzer",
+                "keep_going",
+            },
+            set(configs.workflow_config().to_json()),
+        )
+
+    def test_the_build_environment_is_never_written(self):
+        workflow_config = configs.workflow_config(
+            env={"SECRET": "x", "PATH": "/usr/bin"}
+        )
+        self.assertEqual("x", workflow_config.build_environment["SECRET"])
+        self.assertNotIn("build_environment", workflow_config.to_json())
+        self.assertNotIn("SECRET", json.dumps(workflow_config.to_json()))
+
+    def test_every_written_key_is_also_reachable_as_an_attribute(self):
+        workflow_config = configs.workflow_config()
+        written = workflow_config.to_json()
+        self.assertEqual(
+            written,
+            {name: getattr(workflow_config, name) for name in written},
+        )
+
+    def test_an_absent_path_is_empty_so_the_live_path_is_never_searched(self):
+        self.assertEqual("", config.SSAFWorkflowConfig().restored_path_env_var)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
new file mode 100644
index 00000000000000..6a512c24bf9dd8
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_orchestrator.py
@@ -0,0 +1,348 @@
+# 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 json
+import os
+import shutil
+import stat
+import sys
+import tempfile
+import types
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import config, config_orchestrator, options
+
+
+class Executables:
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.elsewhere = os.path.join(self.tmp, "nowhere")
+
+    def _executable(self, name):
+        path = os.path.join(self.tmp, name)
+        with open(path, "w") as handle:
+            handle.write("#!/bin/sh\n")
+        os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
+        return path
+
+
+class ResolveSsafTool(Executables, unittest.TestCase):
+    def _resolve(self, option_value, installed_shim_dir_path=None, name="sh"):
+        errors = []
+        path = config_orchestrator._resolve_ssaf_tool(
+            "--ssaf-clang",
+            name,
+            option_value,
+            installed_shim_dir_path or self.elsewhere,
+            errors,
+        )
+        return path, errors
+
+    def test_no_value_takes_the_default_name_from_path(self):
+        planted = self._executable("default-tool")
+        with mock.patch.dict(os.environ, {"PATH": self.tmp}):
+            path, errors = self._resolve(None, name="default-tool")
+        self.assertEqual([], errors)
+        self.assertEqual(planted, path)
+
+    def test_the_tool_is_made_absolute(self):
+        self._executable("cc")
+        with mock.patch.dict(os.environ, {"PATH": self.tmp}):
+            path, errors = self._resolve("cc")
+        self.assertEqual([], errors)
+        self.assertEqual(os.path.join(self.tmp, "cc"), path)
+
+    def test_anything_but_one_executable_is_refused(self):
+        tool = self._executable("cc")
+        path, errors = self._resolve("%s -std=gnu11" % tool)
+        self.assertIsNone(path)
+        self.assertEqual(
+            ["--ssaf-clang: expected one executable, got '%s -std=gnu11'" % tool],
+            errors,
+        )
+
+    def test_a_missing_tool_is_reported(self):
+        path, errors = self._resolve("no-such-tool-xyz")
+        self.assertIsNone(path)
+        self.assertIn("not found on PATH", errors[0])
+
+    def test_a_directory_is_not_a_tool(self):
+        path, errors = self._resolve(self.tmp)
+        self.assertIsNone(path)
+        self.assertIn("not a file", errors[0])
+
+    def test_a_non_executable_is_reported(self):
+        plain = os.path.join(self.tmp, "plain")
+        with open(plain, "w") as handle:
+            handle.write("")
+        path, errors = self._resolve(plain)
+        self.assertIsNone(path)
+        self.assertIn("not executable", errors[0])
+
+    def test_an_empty_value_is_reported_rather_than_run(self):
+        path, errors = self._resolve("   ")
+        self.assertIsNone(path)
+        self.assertIn("nothing to run", errors[0])
+
+    def test_one_of_our_own_shims_is_refused(self):
+        shim = self._executable("cc")
+        path, errors = self._resolve(shim, os.path.realpath(self.tmp))
+        self.assertIsNone(path)
+        self.assertIn("one of our own shims", errors[0])
+
+    def test_refusals_accumulate_in_the_order_they_were_asked(self):
+        errors = []
+        for option_name in ("--ssaf-clang", "--ssaf-linker"):
+            config_orchestrator._resolve_ssaf_tool(
+                option_name, "sh", "/no/such/tool", self.elsewhere, errors
+            )
+        self.assertEqual(
+            [
+                "--ssaf-clang: not a file: /no/such/tool",
+                "--ssaf-linker: not a file: /no/such/tool",
+            ],
+            errors,
+        )
+
+
+class Configure(Executables, unittest.TestCase):
+    def setUp(self):
+        super().setUp()
+        self.clang = self._executable("clang")
+        self.linker = self._executable("clang-ssaf-linker")
+        self.analyzer = self._executable("clang-ssaf-analyzer")
+        self.project = os.path.join(self.tmp, "project")
+        os.makedirs(self.project)
+        self.addCleanup(os.chdir, os.getcwd())
+        os.chdir(self.project)
+        patch = mock.patch.object(
+            config_orchestrator, "find_installed_shim_dir", return_value=self.elsewhere
+        )
+        patch.start()
+        self.addCleanup(patch.stop)
+
+    def _configure(self, *extra, env=None):
+        argv = [
+            "--extract-summaries=PointerFlow",
+            "--ssaf-clang=" + self.clang,
+            "--ssaf-linker=" + self.linker,
+        ]
+        argv.extend(extra)
+        return self._configure_argv(argv, env)
+
+    def _configure_argv(self, argv, env=None):
+        command_line_opts, problem = options.resolve(argv)
+        self.assertIsNone(problem)
+        configs, problem = config_orchestrator.configure(
+            command_line_opts, {} if env is None else env
+        )
+        return (configs or (None, None)) + (problem,)
+
+    def test_a_valid_command_line_yields_the_configuration(self):
+        workflow_config, analysis_config, problem = self._configure("--", "make", "-j8")
+        self.assertIsNone(problem)
+        self.assertEqual(["make", "-j8"], workflow_config.build_command)
+        self.assertEqual("PointerFlow", analysis_config.extractors)
+        self.assertEqual(self.clang, analysis_config.ssaf_clang)
+
+    def test_the_configuration_holds_the_resolved_tool_not_what_was_typed(self):
+        argv = [
+            "--extract-summaries=PointerFlow",
+            "--ssaf-clang=clang",
+            "--ssaf-linker=clang-ssaf-linker",
+            "--ssaf-analyzer=clang-ssaf-analyzer",
+            "--analysis=a",
+            "--",
+            "make",
+        ]
+        with mock.patch.dict(os.environ, {"PATH": self.tmp}):
+            workflow_config, analysis_config, problem = self._configure_argv(argv)
+        self.assertIsNone(problem)
+        self.assertEqual(self.clang, analysis_config.ssaf_clang)
+        self.assertEqual(self.linker, analysis_config.ssaf_linker)
+        self.assertEqual(self.analyzer, workflow_config.ssaf_analyzer)
+
+    def test_the_environment_reaches_the_config_but_not_the_file(self):
+        workflow_config, _, problem = self._configure(
+            "--", "make", env={"PATH": "/usr/bin", "SECRET": "x"}
+        )
+        self.assertIsNone(problem)
+        self.assertEqual("x", workflow_config.build_environment["SECRET"])
+        self.assertEqual("/usr/bin", workflow_config.restored_path_env_var)
+        self.assertNotIn("SECRET", json.dumps(workflow_config.to_json()))
+
+    def test_an_output_dir_holding_the_project_is_refused(self):
+        for value in (".", "..", os.getcwd()):
+            workflow_config, _, problem = self._configure("-o", value, "--", "make")
+            self.assertIsNone(workflow_config, value)
+            self.assertEqual(options.ENVIRONMENT_ERROR, problem.exit_status)
+            self.assertIn("would contain the project", problem.message)
+
+    def test_a_directory_no_run_made_is_not_deleted(self):
+        other = os.path.join(self.project, "src")
+        os.makedirs(other)
+        open(os.path.join(other, "main.c"), "w").close()
+        workflow_config, _, problem = self._configure("-o", other, "--", "make")
+        self.assertIsNone(workflow_config)
+        self.assertEqual(options.ENVIRONMENT_ERROR, problem.exit_status)
+        self.assertIn("no earlier run made it", problem.message)
+        self.assertEqual(["main.c"], os.listdir(other))
+
+    def test_an_empty_directory_is_taken(self):
+        empty = os.path.join(self.tmp, "empty")
+        os.makedirs(empty)
+        workflow_config, _, problem = self._configure("-o", empty, "--", "make")
+        self.assertIsNone(problem)
+        self.assertEqual(os.path.realpath(empty), workflow_config.output_dir)
+
+    def test_a_directory_an_earlier_run_made_is_taken(self):
+        earlier = os.path.join(self.tmp, "earlier")
+        os.makedirs(os.path.join(earlier, "ssaf-artifacts"))
+        open(config.output_dir_marker_path(earlier), "w").close()
+        _, _, problem = self._configure("-o", earlier, "--", "make")
+        self.assertIsNone(problem)
+
+    def test_a_relative_output_dir_is_made_absolute(self):
+        workflow_config, _, problem = self._configure("-o", "somewhere", "--", "make")
+        self.assertIsNone(problem)
+        self.assertEqual(
+            os.path.join(os.getcwd(), "somewhere"), workflow_config.output_dir
+        )
+
+    def test_an_unresolvable_ssaf_tool(self):
+        workflow_config, _, problem = self._configure_argv(
+            [
+                "--extract-summaries=PointerFlow",
+                "--ssaf-clang=/no/such/clang",
+                "--ssaf-linker=" + self.linker,
+                "--",
+                "make",
+            ]
+        )
+        self.assertIsNone(workflow_config)
+        self.assertEqual(options.ENVIRONMENT_ERROR, problem.exit_status)
+        self.assertEqual(1, len(problem.details))
+        self.assertIn("/no/such/clang", problem.details[0])
+
+    def test_the_ssaf_tools_take_no_flags(self):
+        workflow_config, _, problem = self._configure_argv(
+            [
+                "--extract-summaries=PointerFlow",
+                "--ssaf-clang=%s -O2" % self.clang,
+                "--ssaf-linker=" + self.linker,
+                "--",
+                "make",
+            ]
+        )
+        self.assertIsNone(workflow_config)
+        self.assertEqual(
+            ["--ssaf-clang: expected one executable, got '%s -O2'" % self.clang],
+            problem.details,
+        )
+
+    def test_a_detail_names_the_option_and_the_path(self):
+        *_, problem = self._configure_argv(
+            [
+                "--extract-summaries=PointerFlow",
+                "--ssaf-clang=/no/such/clang",
+                "--ssaf-linker=" + self.linker,
+                "--",
+                "make",
+            ]
+        )
+        self.assertEqual(["--ssaf-clang: not a file: /no/such/clang"], problem.details)
+
+    def test_a_tool_absent_from_path_names_what_was_looked_for(self):
+        with mock.patch.dict(os.environ, {"PATH": self.elsewhere}):
+            workflow_config, _, problem = self._configure_argv(
+                [
+                    "--extract-summaries=PointerFlow",
+                    "--ssaf-clang=" + self.clang,
+                    "--",
+                    "make",
+                ]
+            )
+        self.assertIsNone(workflow_config)
+        self.assertEqual(
+            ["--ssaf-linker: clang-ssaf-linker: not found on PATH"],
+            problem.details,
+        )
+
+    def test_every_unresolvable_tool_is_reported_at_once(self):
+        workflow_config, _, problem = self._configure_argv(
+            [
+                "--extract-summaries=PointerFlow",
+                "--ssaf-clang=/no/such/clang",
+                "--ssaf-linker=/no/such/linker",
+                "--ssaf-analyzer=/no/such/analyzer",
+                "--analysis=a",
+                "--",
+                "make",
+            ]
+        )
+        self.assertIsNone(workflow_config)
+        self.assertEqual(
+            [
+                "--ssaf-clang: not a file: /no/such/clang",
+                "--ssaf-linker: not a file: /no/such/linker",
+                "--ssaf-analyzer: not a file: /no/such/analyzer",
+            ],
+            problem.details,
+        )
+
+    def test_the_analyzer_is_only_required_when_an_analysis_is_asked_for(self):
+        workflow_config, _, problem = self._configure("--", "make")
+        self.assertIsNone(problem)
+        self.assertIsNone(workflow_config.ssaf_analyzer)
+
+    def test_a_tool_no_pass_will_run_is_not_resolved_at_all(self):
+        workflow_config, _, problem = self._configure(
+            "--ssaf-analyzer=/no/such/analyzer", "--", "make"
+        )
+        self.assertIsNone(problem)
+        self.assertIsNone(workflow_config.ssaf_analyzer)
+
+
+class InstalledShims(Executables, unittest.TestCase):
+    def _found_from(self, script):
+        entry = types.SimpleNamespace(__file__=script)
+        with mock.patch.dict(sys.modules, {"__main__": entry}):
+            return config_orchestrator.find_installed_shim_dir()
+
+    def test_an_installed_layout_finds_libexec(self):
+        prefix = os.path.join(self.tmp, "prefix")
+        libexec = os.path.join(prefix, "libexec", "ssaf-orchestrator-shims")
+        os.makedirs(libexec)
+        os.makedirs(os.path.join(prefix, "bin"))
+        found = self._found_from(
+            os.path.join(prefix, "bin", "clang-ssaf-orchestrator-make")
+        )
+        self.assertEqual(os.path.realpath(libexec), found)
+
+    def test_a_source_tree_falls_back_to_shims(self):
+        root = os.path.join(self.tmp, "tool")
+        os.makedirs(os.path.join(root, "bin"))
+        found = self._found_from(
+            os.path.join(root, "bin", "clang-ssaf-orchestrator-make")
+        )
+        self.assertEqual(os.path.realpath(os.path.join(root, "shims")), found)
+
+    def test_the_result_is_resolved_so_the_own_shim_guard_can_compare_it(self):
+        prefix = os.path.join(self.tmp, "real")
+        libexec = os.path.join(prefix, "libexec", "ssaf-orchestrator-shims")
+        os.makedirs(libexec)
+        os.makedirs(os.path.join(prefix, "bin"))
+        through = os.path.join(self.tmp, "through")
+        os.symlink(prefix, through)
+        found = self._found_from(
+            os.path.join(through, "bin", "clang-ssaf-orchestrator-make")
+        )
+        self.assertEqual(os.path.realpath(found), found)
+        self.assertEqual(os.path.realpath(libexec), found)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_shims.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_shims.py
new file mode 100644
index 00000000000000..3e43137e6ca581
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_config_shims.py
@@ -0,0 +1,63 @@
+# 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 config, config_shims
+
+from . import configs
+
+
+class Serialization(unittest.TestCase):
+    def test_nothing_is_read_outside_an_orchestrator_run(self):
+        self.assertEqual((None, None), config_shims.bootstrap_from_env({}))
+
+
+class ReadingBack(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+
+    def _written(self, **overrides):
+        document = configs.workflow_config().to_json()
+        document.update(configs.analysis_config().to_json())
+        document.update(overrides)
+        return document
+
+    def test_an_analysis_document_rebuilds_an_analysis_pass(self):
+        _, pass_config = config_shims.from_json(self._written())
+        self.assertIsInstance(pass_config, config.SSAFAnalysisPassConfig)
+
+    def test_a_document_with_no_pass_kind_is_refused(self):
+        document = self._written()
+        del document["pass_kind"]
+        with self.assertRaises(KeyError):
+            config_shims.from_json(document)
+
+    def test_an_unknown_pass_kind_is_refused(self):
+        with self.assertRaises(ValueError):
+            config_shims.from_json(self._written(pass_kind="something-else"))
+
+    def test_an_unexpected_key_is_refused(self):
+        with self.assertRaises(TypeError):
+            config_shims.from_json(self._written(unexpected="x"))
+
+    def test_nothing_is_read_when_the_variable_is_empty(self):
+        self.assertEqual(
+            (None, None),
+            config_shims.bootstrap_from_env({config.SSAF_CONFIG_ENV_VAR: ""}),
+        )
+
+    def test_a_variable_naming_a_file_that_is_not_there(self):
+        with self.assertRaises(OSError):
+            config_shims.bootstrap_from_env(
+                {config.SSAF_CONFIG_ENV_VAR: os.path.join(self.tmp, "absent")}
+            )
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_driver_jobs.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_driver_jobs.py
new file mode 100644
index 00000000000000..7bb079bb2bb110
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_driver_jobs.py
@@ -0,0 +1,150 @@
+# 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 unittest
+
+from clang_ssaf_orchestrator_make.shims import driver_jobs as under_test
+
+PREAMBLE = (
+    b"clang version 24.0.0git\n"
+    b"Target: arm64-apple-darwin24.6.0\n"
+    b"Thread model: posix\n"
+    b"InstalledDir: /opt/llvm/bin\n"
+)
+
+
+def listing(*jobs):
+    return PREAMBLE + b"".join(b" " + job + b"\n" for job in jobs)
+
+
+def _program(job):
+    return os.path.basename(job.argv[0])
+
+
+CC1_COMPILE = (
+    b'"/opt/llvm/bin/clang" "-cc1" "-triple" "arm64-apple-macosx15.0.0" '
+    b'"-emit-obj" "-o" "build/a.o" "-x" "c++" "src/a.cpp"'
+)
+LINK = b'"/usr/bin/ld" "-o" "build/prg" "build/a.o" "-lc++"'
+
+
+class Preamble(unittest.TestCase):
+    def test_preamble_is_not_mistaken_for_jobs(self):
+        self.assertEqual(under_test.parse_driver_job_listing(PREAMBLE), [])
+
+    def test_build_config_line_some_builds_add_is_ignored(self):
+        data = PREAMBLE + b"Build config: +assertions\n" + b" " + CC1_COMPILE + b"\n"
+        self.assertEqual(len(under_test.parse_driver_job_listing(data)), 1)
+
+    def test_in_process_marker_is_not_a_job(self):
+        data = PREAMBLE + b" (in-process)\n" + b" " + CC1_COMPILE + b"\n"
+        jobs = under_test.parse_driver_job_listing(data)
+        self.assertEqual(len(jobs), 1)
+        self.assertEqual(jobs[0].argv[1], "-cc1")
+
+    def test_diagnostics_before_and_after_the_preamble_are_ignored(self):
+        data = (
+            b"clang: error: unknown argument: '--nope'\n"
+            + PREAMBLE
+            + b"clang: warning: argument unused [-Wunused-command-line-argument]\n"
+            + b" "
+            + CC1_COMPILE
+            + b"\n"
+        )
+        self.assertEqual(len(under_test.parse_driver_job_listing(data)), 1)
+
+    def test_empty_listing(self):
+        self.assertEqual(under_test.parse_driver_job_listing(b""), [])
+
+
+class Escaping(unittest.TestCase):
+    def _value(self, quoted):
+        data = listing(b'"/opt/llvm/bin/clang" "-cc1" "-D" ' + quoted)
+        return under_test.parse_driver_job_listing(data)[0].argv[-1]
+
+    def test_space_is_raw_inside_quotes(self):
+        self.assertEqual(self._value(b'"A=x y"'), "A=x y")
+
+    def test_escaped_double_quote(self):
+        self.assertEqual(self._value(b'"A=\\"q\\""'), 'A="q"')
+
+    def test_escaped_backslash(self):
+        self.assertEqual(self._value(b'"A=a\\\\b"'), "A=a\\b")
+
+    def test_escaped_dollar(self):
+        self.assertEqual(self._value(b'"A=\\$(PWD)/x"'), "A=$(PWD)/x")
+
+    def test_tab_is_raw(self):
+        self.assertEqual(self._value(b'"A=a\tb"'), "A=a\tb")
+
+    def test_apostrophe_and_backtick_are_raw(self):
+        self.assertEqual(self._value(b'"A=it\'s `x`"'), "A=it's `x`")
+
+    def test_shell_metacharacters_are_raw(self):
+        self.assertEqual(self._value(b'"A=a;b|c&d"'), "A=a;b|c&d")
+
+    def test_empty_argument(self):
+        self.assertEqual(self._value(b'""'), "")
+
+    def test_non_utf8_bytes_survive(self):
+        value = self._value(b'"A=\xff\xfe"')
+        self.assertEqual(value.encode("utf-8", "surrogateescape"), b"A=\xff\xfe")
+
+    def test_newline_inside_an_argument_does_not_end_the_job(self):
+        data = listing(b'"/opt/llvm/bin/clang" "-cc1" "-D" "A=a\nb" "-x" "c++" "a.cpp"')
+        jobs = under_test.parse_driver_job_listing(data)
+        self.assertEqual(len(jobs), 1)
+        self.assertEqual(jobs[0].argv[3], "A=a\nb")
+        self.assertEqual(jobs[0].argv[-1], "a.cpp")
+
+
+class Classification(unittest.TestCase):
+    def test_an_assembler_job_is_read_like_any_other(self):
+        data = listing(b'"/opt/llvm/bin/clang" "-cc1as" "-triple" "arm64--" "a.s"')
+        job = under_test.parse_driver_job_listing(data)[0]
+        self.assertEqual("/opt/llvm/bin/clang", job.argv[0])
+        self.assertEqual("-cc1as", job.argv[1])
+
+    def test_the_output_is_the_key_the_artifact_takes(self):
+        job = under_test.parse_driver_job_listing(listing(CC1_COMPILE))[0]
+        self.assertEqual(job.output, "build/a.o")
+
+    def test_the_repr_names_what_a_reader_of_a_verbose_run_needs(self):
+        job = under_test.parse_driver_job_listing(listing(CC1_COMPILE))[0]
+        self.assertEqual("Job(argv=%r, output='build/a.o')" % (job.argv,), repr(job))
+
+    def test_missing_option_value(self):
+        job = under_test.parse_driver_job_listing(
+            listing(b'"/opt/llvm/bin/clang" "-cc1" "-o"')
+        )[0]
+        self.assertIsNone(job.output)
+
+
+class Decomposition(unittest.TestCase):
+    def test_compile_and_link(self):
+        jobs = under_test.parse_driver_job_listing(listing(CC1_COMPILE, LINK))
+        self.assertEqual(["clang", "ld"], [_program(j) for j in jobs])
+
+    def test_multiple_architectures_have_distinct_outputs(self):
+        arm = (
+            b'"/opt/llvm/bin/clang" "-cc1" "-triple" "arm64-apple-macosx15.0.0" '
+            b'"-o" "/tmp/p-aaa/p-arm64.o" "-x" "c++" "p.cpp"'
+        )
+        intel = (
+            b'"/opt/llvm/bin/clang" "-cc1" "-triple" "x86_64-apple-macosx15.0.0" '
+            b'"-o" "/tmp/p-bbb/p-x86_64.o" "-x" "c++" "p.cpp"'
+        )
+        lipo = b'"/usr/bin/lipo" "-create" "-output" "p.o" "/tmp/p-aaa/p-arm64.o"'
+        jobs = under_test.parse_driver_job_listing(listing(arm, intel, lipo))
+        self.assertEqual(["clang", "clang", "lipo"], [_program(j) for j in jobs])
+        self.assertNotEqual(jobs[0].output, jobs[1].output)
+
+    def test_link_only_has_no_frontend_job(self):
+        jobs = under_test.parse_driver_job_listing(listing(LINK))
+        self.assertEqual(["ld"], [_program(j) for j in jobs])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_failure_logs.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_failure_logs.py
new file mode 100644
index 00000000000000..f3d06fa37fdf63
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_failure_logs.py
@@ -0,0 +1,93 @@
+# 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 unittest import mock
+
+from clang_ssaf_orchestrator_make import build_graph, config, failure_logs, utils
+from clang_ssaf_orchestrator_make.failure_logs import SSAFFailureKind
+
+
+class NamedForTheCommand(unittest.TestCase):
+    def _path(self, command, cwd="/p"):
+        with mock.patch("os.getcwd", return_value=cwd):
+            return failure_logs.get_failure_log_path(
+                "/out", SSAFFailureKind.FORWARD, command, None
+            )
+
+    def test_two_commands_get_two_logs(self):
+        self.assertNotEqual(self._path(["ld", "a.o"]), self._path(["ld", "b.o"]))
+
+    def test_one_command_in_two_directories_gets_two_logs(self):
+        self.assertNotEqual(
+            self._path(["ld", "a.o"], "/p/lib1"), self._path(["ld", "a.o"], "/p/lib2")
+        )
+
+    def test_the_same_command_in_the_same_directory_gets_the_same_log(self):
+        self.assertEqual(self._path(["ld", "a.o"]), self._path(["ld", "a.o"]))
+
+
+class HasFailureLogFor(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+
+    def _write(self, ssaf_failure_kind, build_artifact_path):
+        path = failure_logs.get_failure_log_path(
+            self.tmp,
+            ssaf_failure_kind,
+            None,
+            build_graph.BuildArtifactId(build_artifact_path),
+        )
+        open(utils.make_parents(path), "w").close()
+
+    def _has_log(self, build_artifact_path):
+        return failure_logs.has_failure_log_for(
+            self.tmp, build_graph.BuildArtifactId(build_artifact_path)
+        )
+
+    def test_a_log_of_any_kind_is_a_log_for_its_artifact(self):
+        for each in SSAFFailureKind:
+            self._write(each, "/p/%s.o" % each.value)
+            self.assertTrue(self._has_log("/p/%s.o" % each.value), each)
+
+    def test_a_log_for_a_longer_name_is_not_a_log_for_this_one(self):
+        self._write(SSAFFailureKind.EXTRACT, "/p/a.o.x.o")
+        self.assertFalse(self._has_log("/p/a.o"))
+
+
+class CountFailureLogs(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+
+    def _write(self, directory, name):
+        path = utils.make_parents(os.path.join(directory, name))
+        open(path, "w").close()
+
+    def test_an_output_directory_that_is_not_there_counts_nothing(self):
+        self.assertEqual(
+            0, failure_logs.count_failure_logs(os.path.join(self.tmp, "none"))
+        )
+
+    def test_logs_about_artifacts_and_about_commands_add_up(self):
+        self._write(config.failure_log_dir(self.tmp), "p/a.o.extract.log")
+        self._write(config.failure_log_dir(self.tmp), "p/0123456789ab.internal.log")
+        self.assertEqual(2, failure_logs.count_failure_logs(self.tmp))
+
+    def test_a_log_anywhere_under_the_output_directory_is_counted(self):
+        self._write(config.ssaf_artifact_dir(self.tmp), "p/a.o.extract.log")
+        self.assertEqual(1, failure_logs.count_failure_logs(self.tmp))
+
+    def test_what_is_not_a_log_is_not_counted(self):
+        self._write(config.ssaf_artifact_dir(self.tmp), "p/a.o.summary.json")
+        self._write(self.tmp, "wpa.json")
+        self.assertEqual(0, failure_logs.count_failure_logs(self.tmp))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_interposition.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_interposition.py
new file mode 100644
index 00000000000000..12df308875ecb5
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_interposition.py
@@ -0,0 +1,69 @@
+# 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 stat
+import tempfile
+import unittest
+
+from clang_ssaf_orchestrator_make import config, interposition
+
+
+class ShimLinks(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.installed_shim_dir_path = os.path.join(self.tmp, "libexec")
+        os.makedirs(self.installed_shim_dir_path)
+        self.program = os.path.join(
+            self.installed_shim_dir_path, interposition.SHIM_PROGRAM
+        )
+        with open(self.program, "w") as handle:
+            handle.write("#!/bin/sh\n")
+        os.chmod(self.program, os.stat(self.program).st_mode | stat.S_IXUSR)
+        self.shim_link_dir_path = os.path.join(self.tmp, "shims")
+
+    def test_every_tool_name_is_created_every_run(self):
+        interposition.create_shim_links(
+            self.shim_link_dir_path, self.installed_shim_dir_path
+        )
+        self.assertEqual(
+            sorted(config.SHIM_FOR_LINK_NAME),
+            sorted(os.listdir(self.shim_link_dir_path)),
+        )
+
+    def test_every_name_is_a_link_to_the_one_program(self):
+        interposition.create_shim_links(
+            self.shim_link_dir_path, self.installed_shim_dir_path
+        )
+        for name in config.SHIM_FOR_LINK_NAME:
+            link = os.path.join(self.shim_link_dir_path, name)
+            self.assertTrue(os.path.islink(link), link)
+            self.assertEqual(self.program, os.readlink(link))
+
+    def test_a_program_that_cannot_be_run_is_refused(self):
+        os.chmod(self.program, 0o644)
+        with self.assertRaises(OSError):
+            interposition.create_shim_links(
+                self.shim_link_dir_path, self.installed_shim_dir_path
+            )
+
+
+class BuildEnvironment(unittest.TestCase):
+    def test_a_build_with_no_path_sees_only_the_shim_link_directory(self):
+        for env in ({}, {"PATH": ""}):
+            result = interposition.make_environment("/out/c.json", "/out/shims", env)
+            self.assertEqual("/out/shims", result["PATH"], env)
+
+    def test_the_shim_directory_goes_in_front_of_the_builds_own_path(self):
+        env = interposition.make_environment(
+            "/out/config-analysis.json", "/out/shims", {"PATH": "/usr/bin"}
+        )
+        self.assertEqual(os.pathsep.join(["/out/shims", "/usr/bin"]), env["PATH"])
+        self.assertEqual("/out/config-analysis.json", env[config.SSAF_CONFIG_ENV_VAR])
+
+
+if __name__ == "__main__":
+    unittest.main()
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
new file mode 100644
index 00000000000000..2b2e5ec35c146d
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ld_shim.py
@@ -0,0 +1,132 @@
+# 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.shims import ld
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+from clang_ssaf_orchestrator_make import build_graph
+
+from . import configs
+
+
+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)
+        self.assertEqual("prg", files.output)
+
+    def test_any_object_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)
+
+    def test_a_value_with_no_object_extension_is_not_named(self):
+        files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", "-l", "x"])
+        self.assertEqual(["a.o"], files.inputs)
+
+    def test_a_link_with_no_objects_reads_as_nothing(self):
+        self.assertIsNone(ld.identify_inputs_output(["ld", "-v"]))
+
+    def test_bare_tool_reads_as_nothing(self):
+        self.assertIsNone(ld.identify_inputs_output(["ld"]))
+
+
+class Output(unittest.TestCase):
+    def test_partial_link_output_is_not_read_back_as_an_input(self):
+        files = ld.identify_inputs_output(
+            ["ld", "-r", "-o", "combined.o", "a.o", "b.o"]
+        )
+        self.assertEqual("combined.o", files.output)
+        self.assertEqual(["a.o", "b.o"], files.inputs)
+
+    def test_only_a_whole_argument_names_the_output(self):
+        for option in (
+            "-object_path_lto",
+            "-objc_abi_version",
+            "-order_file",
+            "-order_file_statistics",
+            "-oso_prefix",
+            "-object-file-name=x.o",
+        ):
+            files = ld.identify_inputs_output(["ld", "-o", "prg", "a.o", option])
+            self.assertEqual("prg", files.output, option)
+
+    def test_a_joined_output_is_not_read(self):
+        files = ld.identify_inputs_output(["ld", "-obuild/prg", "a.o"])
+        self.assertIsNone(files.output)
+
+
+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"))
+        self.ssaf_artifact_repo = configs.repository_for(self.out)
+        self.graph = configs.build_graph_for(self.out)
+
+    def _id(self, relative):
+        return build_graph.BuildArtifactId(os.path.join(self.root, relative))
+
+    def _summary(self, relative):
+        open(
+            utils.make_parents(
+                self.ssaf_artifact_repo.get_path_for_artifact(
+                    repository.get_ssaf_artifact_id(
+                        build_graph.BuildArtifactId(os.path.join(self.root, relative)),
+                        repository.SSAFArtifactKind.SUMMARY,
+                    )
+                )
+            ),
+            "w",
+        ).close()
+
+    def _run(self, arguments, pass_config=None):
+        shim = configs.RecordingShim(
+            ["ld", "-o", "build/prg"] + arguments, self.out, pass_config
+        )
+        with configs.recorded_notes() as notes:
+            ld.invoke_ssaf_tools(shim, self.root)
+        self.notes = notes
+        return shim
+
+    def _recorded_inputs(self):
+        return self.graph.get_recorded_build_steps()[self._id("build/prg").get_path()]
+
+    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"])
+        self.assertEqual([self._id("build/a.o").get_path()], self._recorded_inputs())
+        self.assertEqual([], shim.missing)
+        _, command = shim.commands[0]
+        self.assertIn(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId(os.path.join(self.root, "build/a.o")),
+                    repository.SSAFArtifactKind.SUMMARY,
+                )
+            ),
+            command,
+        )
+
+    def test_an_lto_scratch_object_is_not_an_input(self):
+        self._summary("build/a.o")
+        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_a_link_of_nothing_we_know_runs_no_step(self):
+        shim = self._run(["build/a.o"])
+        self.assertEqual([], shim.commands)
+        self.assertEqual([], shim.missing)
+        self.assertIn("nothing to link", self.notes[-1])
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
new file mode 100644
index 00000000000000..18b1ef57533e2b
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_logging_diagnostics.py
@@ -0,0 +1,101 @@
+# 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 io
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import logging_diagnostics
+
+
+class Diagnostics(unittest.TestCase):
+    def _stderr(self, work):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            work()
+        return stream.getvalue()
+
+    def test_an_error_says_so(self):
+        self.assertEqual(
+            "clang-ssaf-orchestrator-make: error: it broke\n",
+            self._stderr(lambda: logging_diagnostics.report_error("it broke")),
+        )
+
+    def test_details_are_indented_under_their_error(self):
+        self.assertEqual(
+            "clang-ssaf-orchestrator-make: error: two reasons\n"
+            "  the first\n"
+            "  the second\n",
+            self._stderr(
+                lambda: logging_diagnostics.report_error(
+                    "two reasons", ["the first", "the second"]
+                )
+            ),
+        )
+
+    def test_a_warning_says_so(self):
+        self.assertEqual(
+            "clang-ssaf-orchestrator-make: warning: careful\n",
+            self._stderr(lambda: logging_diagnostics.report_warning("careful")),
+        )
+
+    def test_an_error_is_not_mistakable_for_a_note(self):
+        error = self._stderr(lambda: logging_diagnostics.report_error("same words"))
+        note = self._stderr(lambda: logging_diagnostics.report_note(True, "same words"))
+        self.assertNotEqual(error, note)
+
+    def test_a_note_is_silent_unless_asked_for(self):
+        self.assertEqual(
+            "", self._stderr(lambda: logging_diagnostics.report_note(False, "quiet"))
+        )
+        self.assertEqual(
+            "clang-ssaf-orchestrator-make: note: loud\n",
+            self._stderr(lambda: logging_diagnostics.report_note(True, "loud")),
+        )
+
+
+class Results(unittest.TestCase):
+    def _stdout(self, work):
+        stream = io.StringIO()
+        with mock.patch("sys.stdout", stream):
+            work()
+        return stream.getvalue()
+
+    def test_the_output_directory(self):
+        self.assertEqual(
+            "output-dir: /out\n",
+            self._stdout(lambda: logging_diagnostics.report_output_dir("/out")),
+        )
+
+    def test_the_summaries_and_the_link_unit(self):
+        self.assertEqual(
+            "summaries: 3\n"
+            "link-unit: p/build/prg\n"
+            "link-unit-summary: /out/p/build/prg.summary.json\n",
+            self._stdout(
+                lambda: logging_diagnostics.report_summaries(
+                    3, "p/build/prg", "/out/p/build/prg.summary.json"
+                )
+            ),
+        )
+
+    def test_the_analysis_result(self):
+        self.assertEqual(
+            "analysis-result: /out/wpa.json\n",
+            self._stdout(
+                lambda: logging_diagnostics.report_analysis_result("/out/wpa.json")
+            ),
+        )
+
+    def test_results_are_flushed_as_they_are_written(self):
+        flushes = []
+        stream = io.StringIO()
+        stream.flush = lambda: flushes.append(stream.getvalue())
+        with mock.patch("sys.stdout", stream):
+            logging_diagnostics.report_summaries(1, "prg", "/out/prg.json")
+        self.assertEqual(3, len(flushes))
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
new file mode 100644
index 00000000000000..a1ac1cbdf4eaeb
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_options.py
@@ -0,0 +1,82 @@
+# 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 contextlib
+import os
+import shutil
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import options
+
+
+class Resolve(unittest.TestCase):
+    def _resolve(self, *extra):
+        return options.resolve(["--extract-summaries=PointerFlow"] + list(extra))
+
+    def test_a_valid_command_line_yields_the_options_as_typed(self):
+        command_line_opts, problem = self._resolve("--", "make", "-j8")
+        self.assertIsNone(problem)
+        self.assertEqual(["make", "-j8"], command_line_opts.build_command)
+        self.assertEqual("PointerFlow", command_line_opts.extract_summaries)
+
+    def test_nothing_on_disk_is_consulted(self):
+        command_line_opts, problem = self._resolve(
+            "--ssaf-clang=/no/such/clang",
+            "-o",
+            "/no/such/output",
+            "--",
+            "make",
+        )
+        self.assertIsNone(problem)
+        self.assertEqual("/no/such/clang", command_line_opts.ssaf_clang)
+        self.assertEqual("/no/such/output", command_line_opts.output_dir)
+
+    def test_the_separator_is_not_part_of_the_build_command(self):
+        command_line_opts, problem = self._resolve("--", "make")
+        self.assertIsNone(problem)
+        self.assertEqual(["make"], command_line_opts.build_command)
+
+    def test_no_build_command_is_a_usage_error(self):
+        command_line_opts, problem = self._resolve()
+        self.assertIsNone(command_line_opts)
+        self.assertEqual(options.USAGE_ERROR, problem.exit_status)
+        self.assertIn("no build command", problem.message)
+
+    def test_a_separator_with_nothing_after_it_is_the_same(self):
+        command_line_opts, problem = self._resolve("--")
+        self.assertIsNone(command_line_opts)
+        self.assertEqual(options.USAGE_ERROR, problem.exit_status)
+
+    def test_resolving_makes_none_of_the_calls_configure_makes(self):
+        # Not every filesystem call: argparse reads the locale through
+        # gettext. These are the calls the configuration layer makes, and the
+        # ones that would mean resolution had started doing its job.
+        watched = (
+            (os, "getcwd"),
+            (os, "listdir"),
+            (os, "access"),
+            (os.path, "isdir"),
+            (os.path, "realpath"),
+            (shutil, "which"),
+        )
+        with contextlib.ExitStack() as stack:
+            for module, name in watched:
+                stack.enter_context(
+                    mock.patch.object(
+                        module,
+                        name,
+                        side_effect=AssertionError("%s called" % name),
+                    )
+                )
+            command_line_opts, problem = self._resolve(
+                "--ssaf-clang=/no/such/clang", "-o", "nowhere", "--", "make"
+            )
+        self.assertIsNone(problem)
+        self.assertEqual("/no/such/clang", command_line_opts.ssaf_clang)
+        self.assertEqual("nowhere", command_line_opts.output_dir)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
new file mode 100644
index 00000000000000..1b967192f9835e
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_orchestrator.py
@@ -0,0 +1,354 @@
+# 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 io
+import json
+import os
+import shutil
+import tempfile
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import build_graph, config, config_orchestrator
+from clang_ssaf_orchestrator_make import config_shims, options, orchestrator, utils
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+
+from . import configs
+
+
+class MakeWarning(unittest.TestCase):
+    def _warning(self, build_command):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            orchestrator.warn_if_not_make(build_command)
+        return stream.getvalue()
+
+    def test_make_is_silent(self):
+        self.assertEqual("", self._warning(["make"]))
+
+    def test_gmake_is_silent(self):
+        self.assertEqual("", self._warning(["gmake", "-j8"]))
+
+    def test_an_absolute_make_is_silent(self):
+        self.assertEqual("", self._warning(["/usr/bin/make", "-C", "sub"]))
+
+    def test_anything_else_warns_but_does_not_refuse(self):
+        warning = self._warning(["./build.sh"])
+        self.assertIn("./build.sh", warning)
+        self.assertIn("not make", warning)
+        self.assertIn("-B", warning)
+
+    def test_an_env_prefix_is_looked_past_not_objected_to(self):
+        self.assertEqual("", self._warning(["env", "V=1", "make"]))
+        self.assertEqual("", self._warning(["V=1", "make", "-j8"]))
+
+    def test_the_warning_names_the_word_that_is_not_make(self):
+        self.assertIn("'ninja' is not make", self._warning(["env", "V=1", "ninja"]))
+        self.assertIn("'cmake' is not make", self._warning(["cmake", "--build", "."]))
+
+    def test_a_command_of_nothing_but_assignments_says_nothing(self):
+        for build_command in ([], ["V=1"], ["env", "V=1"]):
+            self.assertEqual("", self._warning(build_command), build_command)
+
+    def test_an_env_prefix_around_something_else_still_warns(self):
+        self.assertIn("not make", self._warning(["env", "V=1", "ninja"]))
+
+    def test_the_warning_names_what_will_be_appended(self):
+        self.assertIn("-B", self._warning(["ninja"]))
+
+
+class Main(unittest.TestCase):
+    def _main(self, argv):
+        self.run_calls = []
+        with mock.patch("sys.stderr", io.StringIO()) as stream, mock.patch.object(
+            orchestrator,
+            "run_ssaf_workflow",
+            lambda *args: self.run_calls.append(args),
+        ):
+            return orchestrator.main(argv, {}), stream.getvalue()
+
+    def test_a_usage_error_stops_before_the_build(self):
+        status, message = self._main(["--extract-summaries=PointerFlow"])
+        self.assertEqual(options.USAGE_ERROR, status)
+        self.assertIn("no build command", message)
+        self.assertEqual([], self.run_calls)
+
+    def test_an_environment_error_stops_before_the_build(self):
+        status, message = self._main(
+            [
+                "--extract-summaries=PointerFlow",
+                "--ssaf-clang=/no/such/clang",
+                "--",
+                "make",
+            ]
+        )
+        self.assertEqual(options.ENVIRONMENT_ERROR, status)
+        self.assertIn("--ssaf-clang", message)
+        self.assertEqual([], self.run_calls)
+
+    def test_a_configured_run_is_handed_its_two_configurations(self):
+        configured = ("workflow", "analysis")
+        with mock.patch.object(
+            config_orchestrator, "configure", return_value=(configured, None)
+        ):
+            status, _ = self._main(["--extract-summaries=X", "--", "make"])
+        self.assertEqual(0, status)
+        self.assertEqual([configured], self.run_calls)
+
+
+class ClearOutputDir(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+
+    def test_what_a_previous_run_left_is_removed(self):
+        os.makedirs(os.path.join(self.tmp, "shims"))
+        open(os.path.join(self.tmp, "wpa.json"), "w").close()
+        orchestrator.clear_output_dir(self.tmp)
+        self.assertEqual([config.OUTPUT_DIR_MARKER], os.listdir(self.tmp))
+
+    def test_an_output_directory_that_is_not_there_yet_is_made(self):
+        output_dir = os.path.join(self.tmp, "out")
+        orchestrator.clear_output_dir(output_dir)
+        self.assertEqual([config.OUTPUT_DIR_MARKER], os.listdir(output_dir))
+
+
+class PassConfigFile(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.workflow_config = config.SSAFWorkflowConfig(
+            output_dir=self.tmp,
+            installed_shim_dir_path="/libexec",
+            build_command=["make"],
+        )
+        self.analysis_config = config.SSAFAnalysisPassConfig(
+            ssaf_clang="/bin/clang",
+            extractors="PointerFlow",
+            ssaf_linker="/bin/clang-ssaf-linker",
+        )
+
+    def test_a_pass_is_written_under_the_output_directory(self):
+        path = orchestrator.write_pass_config(
+            self.workflow_config, self.analysis_config
+        )
+        self.assertEqual(os.path.join(self.tmp, self.analysis_config.file_name), path)
+        self.assertTrue(os.path.isfile(path))
+
+    def test_nothing_is_left_behind_by_the_write(self):
+        orchestrator.write_pass_config(self.workflow_config, self.analysis_config)
+        self.assertEqual(["config-analysis.json"], os.listdir(self.tmp))
+
+    def test_the_document_does_not_carry_its_own_path(self):
+        path = orchestrator.write_pass_config(
+            self.workflow_config, self.analysis_config
+        )
+        with open(path) as handle:
+            written = json.load(handle)
+        self.assertNotIn("serialized_config_path", written)
+        self.assertNotIn(path, written.values())
+
+    def test_an_output_directory_that_does_not_exist_is_not_silently_skipped(self):
+        workflow_config = configs.workflow_config(
+            output_dir=os.path.join(self.tmp, "absent")
+        )
+        with self.assertRaises(OSError):
+            orchestrator.write_pass_config(workflow_config, self.analysis_config)
+
+    def test_a_shim_reads_back_the_run_and_its_own_pass(self):
+        path = orchestrator.write_pass_config(
+            self.workflow_config, self.analysis_config
+        )
+        workflow_config, pass_config = config_shims.bootstrap_from_env(
+            {config.SSAF_CONFIG_ENV_VAR: path}
+        )
+        self.assertEqual(self.workflow_config.to_json(), workflow_config.to_json())
+        self.assertIsInstance(pass_config, config.SSAFAnalysisPassConfig)
+        self.assertEqual("PointerFlow", pass_config.extractors)
+
+
+class RunBuild(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.workflow_config = configs.workflow_config(output_dir=self.tmp)
+        self.workflow_config.build_command = ["make", "-j8"]
+
+    def _run(self, **kwargs):
+        with mock.patch("subprocess.call", **kwargs) as called:
+            self.called = called
+            return orchestrator.run_build_with_toolchain_interposition(
+                self.workflow_config, "/out/config.json", "/out/shims", "build"
+            )
+
+    def test_a_build_that_succeeds_is_no_problem(self):
+        self.assertIsNone(self._run(return_value=0))
+
+    def test_the_rebuild_flag_is_appended_to_the_build_command(self):
+        self._run(return_value=0)
+        self.assertEqual(["make", "-j8", "-B"], self.called.call_args[0][0])
+
+    def test_the_build_runs_where_we_were_invoked_and_sees_the_shims(self):
+        self._run(return_value=0)
+        self.assertNotIn("cwd", self.called.call_args[1])
+        env = self.called.call_args[1]["env"]
+        self.assertTrue(env["PATH"].startswith("/out/shims"))
+        self.assertEqual("/out/config.json", env[config.SSAF_CONFIG_ENV_VAR])
+
+    def test_a_build_that_fails_carries_its_own_status(self):
+        problem = self._run(return_value=2)
+        self.assertEqual(2, problem.exit_status)
+        self.assertEqual("build failed (exit 2)", problem.message)
+
+    def test_a_build_killed_by_a_signal_exits_as_a_shell_would(self):
+        problem = self._run(return_value=-15)
+        self.assertEqual(143, problem.exit_status)
+
+    def test_a_build_command_that_cannot_be_run_at_all(self):
+        problem = self._run(side_effect=OSError(2, "No such file"))
+        self.assertEqual(127, problem.exit_status)
+        self.assertIn("cannot run 'make'", problem.message)
+
+    def test_the_description_names_which_build_failed(self):
+        with mock.patch("subprocess.call", return_value=1):
+            problem = orchestrator.run_build_with_toolchain_interposition(
+                self.workflow_config, "/c.json", "/s", "rebuild for something"
+            )
+        self.assertEqual("rebuild for something failed (exit 1)", problem.message)
+
+
+class TheAnalysisPass(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.workflow_config = configs.workflow_config(output_dir=self.tmp)
+        self.ssaf_artifact_repo = configs.repository_for(self.tmp)
+        prg = build_graph.BuildArtifactId("/p/prg")
+        configs.build_graph_for(self.tmp).record_build_step(prg, [])
+        self.summary = utils.make_parents(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    prg, repository.SSAFArtifactKind.SUMMARY
+                )
+            )
+        )
+        with open(self.summary, "w") as handle:
+            json.dump({"lu_namespace": [{"kind": "LinkUnit", "name": "prg"}]}, handle)
+
+    def _run(self, analyses, status=0):
+        with mock.patch.object(
+            orchestrator, "run_build_with_toolchain_interposition", return_value=None
+        ), mock.patch("subprocess.call", return_value=status) as called, mock.patch(
+            "sys.stdout", io.StringIO()
+        ):
+            problem = orchestrator.run_ssaf_analysis_pass(
+                self.workflow_config,
+                configs.analysis_config(analyses=analyses),
+                self.ssaf_artifact_repo,
+                "/out/shims",
+            )
+        return problem, called
+
+    def test_without_an_analysis_no_analyzer_runs(self):
+        problem, called = self._run([])
+        self.assertIsNone(problem)
+        called.assert_not_called()
+
+    def test_the_analyzer_runs_every_analysis_over_the_link_unit_summary(self):
+        problem, called = self._run(["A", "B"])
+        self.assertIsNone(problem)
+        self.assertEqual(
+            [
+                "/bin/clang-ssaf-analyzer",
+                self.summary,
+                "-o",
+                config.wpa_result_path(self.tmp),
+                "-a",
+                "A",
+                "-a",
+                "B",
+            ],
+            called.call_args[0][0],
+        )
+
+    def test_a_failed_analysis_fails_the_run_with_its_own_status(self):
+        problem, _ = self._run(["A"], status=3)
+        self.assertEqual(3, problem.exit_status)
+        self.assertEqual("whole-program analysis failed (exit 3)", problem.message)
+
+    def test_an_analyzer_killed_by_a_signal_exits_as_a_shell_would(self):
+        problem, _ = self._run(["A"], status=-9)
+        self.assertEqual(137, problem.exit_status)
+
+
+class TopMostLinkUnit(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.ssaf_artifact_repo = configs.repository_for(self.tmp)
+        self.graph = configs.build_graph_for(self.tmp)
+
+    def _record(self, output_path, inputs):
+        inputs = [build_graph.BuildArtifactId(each) for each in inputs]
+        self.graph.record_build_step(
+            build_graph.BuildArtifactId(output_path),
+            inputs,
+        )
+
+    def _summary(self, build_artifact_path):
+        path = utils.make_parents(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId(build_artifact_path),
+                    repository.SSAFArtifactKind.SUMMARY,
+                )
+            )
+        )
+        document = {
+            "lu_namespace": [
+                {"kind": "LinkUnit", "name": os.path.basename(build_artifact_path)}
+            ]
+        }
+        with open(path, "w") as handle:
+            json.dump(document, handle)
+
+    def test_no_link_unit_at_all(self):
+        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+            self.ssaf_artifact_repo, self.tmp
+        )
+        self.assertIsNone(link_unit)
+        self.assertIn("produced no link unit", problem.message)
+
+    def test_several_that_consume_none_of_each_other(self):
+        self._record("/p/one", ["a.o"])
+        self._record("/p/two", ["b.o"])
+        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+            self.ssaf_artifact_repo, self.tmp
+        )
+        self.assertIsNone(link_unit)
+        self.assertIn("produced 2 unconsumed outputs", problem.message)
+        self.assertIn("p/one", problem.message)
+        self.assertIn("p/two", problem.message)
+
+    def test_one_whose_summary_was_never_written(self):
+        self._record("/p/prg", ["a.o"])
+        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+            self.ssaf_artifact_repo, self.tmp
+        )
+        self.assertIsNone(link_unit)
+        self.assertIn("no summary", problem.message)
+        self.assertIn("p/prg", problem.message)
+
+    def test_one_with_its_summary_is_the_answer(self):
+        self._record("/p/prg", ["a.o"])
+        self._summary("/p/prg")
+        link_unit, _, problem = orchestrator.get_topmost_link_unit(
+            self.ssaf_artifact_repo, self.tmp
+        )
+        self.assertIsNone(problem)
+        self.assertEqual("/p/prg", link_unit.get_path())
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_run.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_run.py
new file mode 100644
index 00000000000000..bcfd52713c869c
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_run.py
@@ -0,0 +1,84 @@
+# 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 io
+import shutil
+import tempfile
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import config, failure_logs, orchestrator
+from clang_ssaf_orchestrator_make.shims import run
+from clang_ssaf_orchestrator_make.shims import shim as shim_module
+
+from . import configs
+
+
+class RunShim(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.env = {
+            config.SSAF_CONFIG_ENV_VAR: orchestrator.write_pass_config(
+                configs.workflow_config(output_dir=self.tmp), configs.analysis_config()
+            )
+        }
+        self.binary_status = 0
+        self.binary_runs = []
+        self.ssaf_runs = []
+        patch = mock.patch.object(
+            shim_module.Shim, "run_bin_tool", lambda shim, path, cwd: self._binary(shim)
+        )
+        patch.start()
+        self.addCleanup(patch.stop)
+
+    def _binary(self, shim):
+        self.binary_runs.append(shim.argv)
+        return self.binary_status
+
+    def _run_shim(self, argv, work=lambda: None):
+        def invoke_ssaf_tools(shim, cwd):
+            self.ssaf_runs.append(shim.argv)
+            work()
+
+        return run.run_shim(invoke_ssaf_tools, argv, self.env)
+
+    def test_a_shim_reached_through_its_link_runs_the_tool_then_the_ssaf_half(self):
+        self.assertEqual(0, self._run_shim(["/out/shims/ld", "-o", "prg", "a.o"]))
+        self.assertEqual([["/out/shims/ld", "-o", "prg", "a.o"]], self.binary_runs)
+        self.assertEqual([["/out/shims/ld", "-o", "prg", "a.o"]], self.ssaf_runs)
+
+    def test_a_forwarded_job_does_not_run_the_tool_again(self):
+        argv = ["/out/shims/ld", "-o", "prg", "a.o"]
+        self.assertEqual(
+            0, self._run_shim(argv + [shim_module.SKIP_BINARY_TOOL_INVOCATION])
+        )
+        self.assertEqual([], self.binary_runs)
+        self.assertEqual([argv], self.ssaf_runs)
+
+    def test_a_tool_that_fails_is_the_shims_status_and_no_ssaf_step_runs(self):
+        self.binary_status = 2
+        self.assertEqual(2, self._run_shim(["ld", "-o", "prg"]))
+        self.assertEqual([], self.ssaf_runs)
+
+    def test_an_internal_error_lets_the_build_go_on_and_is_counted(self):
+        def explode():
+            raise ValueError("deliberate")
+
+        with mock.patch("sys.stderr", io.StringIO()):
+            self.assertEqual(0, self._run_shim(["ld", "-o", "prg"], explode))
+        self.assertEqual(1, failure_logs.count_failure_logs(self.tmp))
+
+    def test_outside_a_run_nothing_runs(self):
+        with mock.patch("sys.stderr", io.StringIO()):
+            status = run.run_shim(
+                lambda shim, cwd: self.ssaf_runs.append(shim.argv), ["ld"], {}
+            )
+        self.assertEqual(1, status)
+        self.assertEqual([], self.binary_runs)
+        self.assertEqual([], self.ssaf_runs)
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_shim.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_shim.py
new file mode 100644
index 00000000000000..aa9deb789b3425
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_shim.py
@@ -0,0 +1,414 @@
+# 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 io
+import os
+import shutil
+import signal
+import stat
+import sys
+import tempfile
+import types
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import config
+from clang_ssaf_orchestrator_make.shims import shim as shim_utils
+from clang_ssaf_orchestrator_make import build_graph
+from clang_ssaf_orchestrator_make import failure_logs
+from clang_ssaf_orchestrator_make.failure_logs import SSAFFailureKind
+
+from . import configs
+
+
+class BinaryTool(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.shim_link_dir_path = os.path.join(self.tmp, "shims")
+        self.binary_tools = os.path.join(self.tmp, "binary")
+        for directory in (self.shim_link_dir_path, self.binary_tools):
+            os.makedirs(directory)
+        for name in ("cc", "clang", "c++", "clang++", "ld", "ar"):
+            self._executable(self.binary_tools, name)
+        self.workflow_config = configs.workflow_config(
+            output_dir=os.path.join(self.tmp, "out")
+        )
+        self.workflow_config.restored_path_env_var = self.binary_tools
+
+    def _executable(self, directory, name):
+        path = os.path.join(directory, name)
+        with open(path, "w") as handle:
+            handle.write("#!/bin/sh\n")
+        os.chmod(path, os.stat(path).st_mode | stat.S_IXUSR)
+        return path
+
+    def _shim(self, name, *extra):
+        argv = [os.path.join(self.shim_link_dir_path, name), "-c", "a.c"]
+        return shim_utils.Shim(
+            self.workflow_config,
+            config.SSAFAnalysisPassConfig(),
+            argv + list(extra),
+        )
+
+    def test_a_name_on_path_runs_that_same_name(self):
+        shim = self._shim("clang")
+        self.assertFalse(shim.skip_binary_tool_invocation)
+        self.assertEqual(os.path.join(self.binary_tools, "clang"), shim.bin_tool_path())
+
+    def test_a_name_on_path_is_resolved_off_our_own_path(self):
+        self.assertEqual(
+            os.path.join(self.binary_tools, "cc"),
+            self._shim("cc").bin_tool_path(),
+        )
+
+    def test_the_marker_says_the_binary_tool_has_already_run(self):
+        shim = self._shim("ld", shim_utils.SKIP_BINARY_TOOL_INVOCATION)
+        self.assertTrue(shim.skip_binary_tool_invocation)
+
+    def test_without_the_marker_the_shim_owns_the_binary_tool(self):
+        shim = self._shim("ld")
+        self.assertFalse(shim.skip_binary_tool_invocation)
+        self.assertEqual(os.path.join(self.binary_tools, "ld"), shim.bin_tool_path())
+
+    def test_the_marker_never_reaches_the_binary_tool(self):
+        shim = self._shim("ld", shim_utils.SKIP_BINARY_TOOL_INVOCATION)
+        self.assertEqual(["-c", "a.c"], shim.argv[1:])
+        self.assertNotIn(shim_utils.SKIP_BINARY_TOOL_INVOCATION, shim.argv)
+
+    def test_a_name_that_no_longer_resolves_yields_no_words(self):
+        os.remove(os.path.join(self.binary_tools, "ar"))
+        self.assertIsNone(self._shim("ar").bin_tool_path())
+
+
+class ShimEnvironment(unittest.TestCase):
+    def setUp(self):
+        self.workflow_config = config.SSAFWorkflowConfig(
+            installed_shim_dir_path="/libexec",
+            restored_path_env_var="/usr/bin",
+        )
+        self.shim = shim_utils.Shim(
+            self.workflow_config, config.SSAFAnalysisPassConfig(), ["cc"]
+        )
+
+    def test_the_binary_tool_sees_the_build_s_own_path(self):
+        env = {
+            config.SSAF_CONFIG_ENV_VAR: "/out/config-analysis.json",
+            "PATH": "/out/shims:/usr/bin",
+            "HOME": "/home/jan",
+        }
+        with mock.patch.dict(os.environ, env, clear=True):
+            cleaned = self.shim.get_unmodified_env()
+        self.assertNotIn(config.SSAF_CONFIG_ENV_VAR, cleaned)
+        self.assertEqual("/usr/bin", cleaned["PATH"])
+        self.assertEqual("/home/jan", cleaned["HOME"])
+
+
+class MissingSummary(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"))
+        workflow_config = config.SSAFWorkflowConfig(output_dir=self.out)
+        self.shim = shim_utils.Shim(
+            workflow_config,
+            config.SSAFAnalysisPassConfig(),
+            ["ld", "-o", "build/prg"],
+        )
+
+    def _id(self, relative):
+        return build_graph.BuildArtifactId(os.path.join(self.root, relative))
+
+    def _report(self, path):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.shim.log_missing_summary(
+                self._id("build/prg"), build_graph.BuildArtifactId(path)
+            )
+        return stream.getvalue()
+
+    def _logs(self):
+        logs = config.failure_log_dir(self.out)
+        found = []
+        for directory, _, filenames in os.walk(logs):
+            found.extend(
+                os.path.relpath(os.path.join(directory, name), logs)
+                for name in filenames
+                if name.endswith(".log")
+            )
+        return sorted(found)
+
+    def _log(self, ssaf_failure_kind, build_artifact_id):
+        return os.path.relpath(
+            failure_logs.get_failure_log_path(
+                self.out, ssaf_failure_kind, None, build_artifact_id
+            ),
+            config.failure_log_dir(self.out),
+        )
+
+    def test_an_artifact_with_no_summary_is_a_counted_failure(self):
+        message = self._report(os.path.join(self.root, "build", "libx.a"))
+        self.assertEqual(
+            "clang-ssaf-orchestrator-make: error: no summary for %s, consumed "
+            "by %s; nothing it was assembled from was analysed\n"
+            % (
+                self._id("build/libx.a").get_path(),
+                self._id("build/prg").get_path(),
+            ),
+            message,
+        )
+        self.assertEqual(
+            [self._log(SSAFFailureKind.MISSING, self._id("build/libx.a"))],
+            self._logs(),
+        )
+
+    def test_where_the_artifact_lives_does_not_change_that(self):
+        slice_object = os.path.join(self.tmp, "slices", "a-arm64.o")
+        build_artifact_id = build_graph.BuildArtifactId(slice_object)
+        self.assertIn("no summary for", self._report(slice_object))
+        self.assertEqual(
+            [self._log(SSAFFailureKind.MISSING, build_artifact_id)],
+            self._logs(),
+        )
+
+    def test_an_already_reported_artifact_is_not_reported_twice(self):
+        obj = os.path.join(self.root, "build", "a.o")
+        existing = failure_logs.get_failure_log_path(
+            self.out, SSAFFailureKind.EXTRACT, None, self._id("build/a.o")
+        )
+        os.makedirs(os.path.dirname(existing))
+        with open(existing, "w") as handle:
+            handle.write("the SSAF compilation failed\n")
+        self.assertEqual("", self._report(obj))
+        self.assertEqual(
+            [self._log(SSAFFailureKind.EXTRACT, self._id("build/a.o"))],
+            self._logs(),
+        )
+
+
+class WithoutConfiguration(unittest.TestCase):
+    def test_a_shim_run_outside_a_run_says_so(self):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            status = shim_utils.error_no_config()
+        self.assertEqual(1, status)
+        self.assertEqual(
+            "clang-ssaf-orchestrator-make: error: missing configuration file\n"
+            "  %s is not set, so this is not an orchestrator run\n"
+            % config.SSAF_CONFIG_ENV_VAR,
+            stream.getvalue(),
+        )
+
+
+class Argv(unittest.TestCase):
+    def _shim(self, argv):
+        return shim_utils.Shim(configs.workflow_config(), None, argv)
+
+    def test_the_marker_is_taken_only_off_the_end(self):
+        shim = self._shim(["ld", shim_utils.SKIP_BINARY_TOOL_INVOCATION, "a.o"])
+        self.assertFalse(shim.skip_binary_tool_invocation)
+        self.assertIn(shim_utils.SKIP_BINARY_TOOL_INVOCATION, shim.argv)
+
+    def test_a_command_of_nothing_but_the_marker_keeps_it(self):
+        shim = self._shim([shim_utils.SKIP_BINARY_TOOL_INVOCATION])
+        self.assertFalse(shim.skip_binary_tool_invocation)
+
+    def test_the_tool_is_named_by_the_link_it_was_reached_through(self):
+        shim = self._shim(["/out/shims/c++", "-c", "a.c"])
+        self.assertEqual("c++", shim.invoked_as)
+
+    def test_the_artifact_repository_is_built_once(self):
+        shim = self._shim(["ld", "-o", "prg"])
+        self.assertIs(shim.ssaf_artifact_repo, shim.ssaf_artifact_repo)
+
+
+class RunBinaryTool(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.shim = shim_utils.Shim(
+            configs.workflow_config(output_dir=self.tmp),
+            config.SSAFAnalysisPassConfig(),
+            ["ld", "-o", "prg"],
+        )
+
+    def _stderr(self, work):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.status = work()
+        return stream.getvalue()
+
+    def test_a_tool_that_cannot_be_found_is_a_missing_command(self):
+        message = self._stderr(lambda: self.shim.run_bin_tool(None, self.tmp))
+        self.assertEqual(127, self.status)
+        self.assertIn("cannot find the binary ld", message)
+
+    def test_a_tool_that_cannot_be_run_is_a_missing_command(self):
+        message = self._stderr(
+            lambda: self.shim.run_bin_tool(os.path.join(self.tmp, "nope"), self.tmp)
+        )
+        self.assertEqual(127, self.status)
+        self.assertIn("cannot run", message)
+
+    def test_the_tool_runs_as_the_build_asked_in_the_builds_environment(self):
+        self.shim.workflow_config.restored_path_env_var = "/usr/bin"
+        live = {config.SSAF_CONFIG_ENV_VAR: "/out/c.json", "PATH": "/out/shims"}
+        with mock.patch.dict(os.environ, live), mock.patch(
+            "subprocess.call", return_value=3
+        ) as called:
+            status = self.shim.run_bin_tool("/usr/bin/ld", self.tmp)
+        self.assertEqual(3, status)
+        self.assertEqual(["/usr/bin/ld", "-o", "prg"], called.call_args[0][0])
+        self.assertEqual(self.tmp, called.call_args[1]["cwd"])
+        env = called.call_args[1]["env"]
+        self.assertNotIn(config.SSAF_CONFIG_ENV_VAR, env)
+        self.assertEqual("/usr/bin", env["PATH"])
+
+    def test_a_signal_the_tool_died_of_is_re_raised(self):
+        killed = []
+        with mock.patch("signal.signal") as signalled, mock.patch(
+            "os.kill", side_effect=lambda pid, number: killed.append(number)
+        ):
+            status = self.shim._propagate_process_return_status(-signal.SIGTERM)
+        self.assertEqual([signal.SIGTERM], killed)
+        self.assertEqual((signal.SIGTERM, signal.SIG_DFL), signalled.call_args[0])
+        self.assertEqual(128 + signal.SIGTERM, status)
+
+    def test_a_signal_that_cannot_be_reset_is_not_raised_on_ourselves(self):
+        with mock.patch("signal.signal", side_effect=OSError(22, "bad")), mock.patch(
+            "os.kill"
+        ) as killed:
+            self.assertEqual(137, self.shim._propagate_process_return_status(-9))
+        killed.assert_not_called()
+
+
+class Protect(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.shim = shim_utils.Shim(
+            configs.workflow_config(output_dir=self.tmp),
+            config.SSAFAnalysisPassConfig(),
+            ["ld", "-o", "prg"],
+        )
+
+    def test_work_that_succeeds_is_not_reported(self):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.assertEqual(0, self.shim.protect(lambda: None))
+        self.assertEqual("", stream.getvalue())
+
+    def test_an_internal_error_is_reported_logged_and_swallowed(self):
+        def explode():
+            raise ValueError("deliberate")
+
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.assertEqual(0, self.shim.protect(explode))
+        self.assertIn("internal error in ld", stream.getvalue())
+        self.assertIn("deliberate", stream.getvalue())
+        log = failure_logs.get_failure_log_path(
+            self.tmp, SSAFFailureKind.INTERNAL, self.shim.argv, None
+        )
+        self.assertTrue(os.path.isfile(log))
+        with open(log) as handle:
+            self.assertIn("deliberate", handle.read())
+
+    def test_a_log_that_cannot_be_written_is_itself_reported(self):
+        def explode():
+            raise ValueError("deliberate")
+
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream), mock.patch(
+            "clang_ssaf_orchestrator_make.utils.make_parents", side_effect=OSError
+        ):
+            self.assertEqual(0, self.shim.protect(explode))
+        self.assertIn("could not be written", stream.getvalue())
+
+
+class RunningAnSsafStep(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.shim = shim_utils.Shim(
+            configs.workflow_config(output_dir=self.tmp),
+            config.SSAFAnalysisPassConfig(),
+            ["ld", "-o", "prg"],
+        )
+        self.build_artifact_id = build_graph.BuildArtifactId("/p/prg")
+
+    def _run(self, command):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            succeeded = self.shim.run(
+                command, SSAFFailureKind.LINK, self.build_artifact_id
+            )
+        return succeeded, stream.getvalue()
+
+    def test_what_a_step_says_is_kept_from_the_build_unless_relayed(self):
+        command = [sys.executable, "-c", "print('from the step')"]
+        _, quiet = self._run(command)
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.shim.run(
+                command, SSAFFailureKind.FORWARD, self.build_artifact_id, True
+            )
+        self.assertNotIn("from the step", quiet)
+        self.assertIn("from the step", stream.getvalue())
+
+
+class FailureLog(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.shim = shim_utils.Shim(
+            configs.workflow_config(output_dir=self.tmp),
+            config.SSAFAnalysisPassConfig(),
+            ["ld", "-o", "prg"],
+        )
+        self.build_artifact_id = build_graph.BuildArtifactId("/p/build/a.o")
+
+    def _log(self):
+        return failure_logs.get_failure_log_path(
+            self.tmp, SSAFFailureKind.EXTRACT, None, self.build_artifact_id
+        )
+
+    def _log_failure(self, command, completed):
+        self.shim.log_failure(
+            command, completed, SSAFFailureKind.EXTRACT, self.build_artifact_id
+        )
+
+    def test_the_command_and_its_output_are_recorded(self):
+        completed = types.SimpleNamespace(
+            returncode=2, stdout=b"it broke\n", stderr=None
+        )
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self._log_failure(["ssaf-clang", "a.c"], completed)
+        with open(self._log()) as handle:
+            self.assertEqual("$ ssaf-clang a.c\nit broke\n", handle.read())
+        self.assertIn("exit 2", stream.getvalue())
+        self.assertIn(self._log(), stream.getvalue())
+
+    def test_a_tool_that_said_nothing_still_gets_a_log(self):
+        completed = types.SimpleNamespace(returncode=1, stdout=None, stderr=None)
+        with mock.patch("sys.stderr", io.StringIO()):
+            self._log_failure(["ssaf-clang"], completed)
+        with open(self._log()) as handle:
+            self.assertEqual("$ ssaf-clang\n", handle.read())
+
+    def test_what_a_tool_wrote_on_its_own_stderr_is_recorded_too(self):
+        completed = types.SimpleNamespace(
+            returncode=1, stdout=b"", stderr=b"error: unknown argument\n"
+        )
+        with mock.patch("sys.stderr", io.StringIO()):
+            self._log_failure(["clang", "-###"], completed)
+        with open(self._log()) as handle:
+            self.assertEqual("$ clang -###\nerror: unknown argument\n", handle.read())
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.py
new file mode 100644
index 00000000000000..f8b63c9b1acff3
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_ssaf_artifact_repository.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
+import shutil
+import tempfile
+import unittest
+
+from clang_ssaf_orchestrator_make import build_graph, utils
+from clang_ssaf_orchestrator_make import ssaf_artifact_repository as repository
+
+from . import configs
+
+
+class OutputDirIsNeverLeft(unittest.TestCase):
+    def test_an_artifact_anywhere_stays_inside_the_output_directory(self):
+        ssaf_artifact_repo = configs.repository_for("/out")
+        build_artifact_id = build_graph.BuildArtifactId("/elsewhere/a.o")
+        self.assertTrue(
+            ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_artifact_id, repository.SSAFArtifactKind.SUMMARY
+                )
+            ).startswith("/out/ssaf-artifacts/")
+        )
+
+
+class Paths(unittest.TestCase):
+    def setUp(self):
+        self.ssaf_artifact_repo = configs.repository_for("/out")
+        self.graph = configs.build_graph_for("/out")
+
+    def test_artifact_paths(self):
+        self.assertEqual(
+            "/out/ssaf-artifacts/p/build/a.o.summary.json",
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/build/a.o"),
+                    repository.SSAFArtifactKind.SUMMARY,
+                )
+            ),
+        )
+        self.assertEqual(
+            "/out/ssaf-artifacts/p/prg.summary.json",
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/prg"),
+                    repository.SSAFArtifactKind.SUMMARY,
+                )
+            ),
+        )
+
+
+class Construction(unittest.TestCase):
+    def test_constructing_it_creates_no_directory(self):
+        output_dir = os.path.join(tempfile.mkdtemp(), "out")
+        self.addCleanup(shutil.rmtree, os.path.dirname(output_dir), True)
+        paths = repository.SSAFArtifactRepository(output_dir)
+        self.assertEqual(output_dir, paths.ssaf_artifact_dir)
+        self.assertFalse(os.path.exists(output_dir))
+
+
+class Counts(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.ssaf_artifact_repo = configs.repository_for(self.tmp)
+        self.graph = configs.build_graph_for(self.tmp)
+
+    def _write(self, path):
+        open(utils.make_parents(path), "w").close()
+
+    def test_an_output_directory_that_does_not_exist_counts_nothing(self):
+        absent = configs.repository_for(os.path.join(self.tmp, "none"))
+        self.assertEqual(0, absent.count_summaries())
+
+    def test_each_kind_is_counted_apart_at_any_depth(self):
+        self._write(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/a.o"),
+                    repository.SSAFArtifactKind.SUMMARY,
+                )
+            )
+        )
+        self._write(
+            self.ssaf_artifact_repo.get_path_for_artifact(
+                repository.get_ssaf_artifact_id(
+                    build_graph.BuildArtifactId("/p/deep/b.o"),
+                    repository.SSAFArtifactKind.SUMMARY,
+                )
+            )
+        )
+        self._write(
+            self.graph.get_buildstep_record_path(build_graph.BuildArtifactId("/p/prg"))
+        )
+        self.assertEqual(2, self.ssaf_artifact_repo.count_summaries())
+
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/clang/tools/clang-ssaf-orchestrator-make/tests/test_utils.py b/clang/tools/clang-ssaf-orchestrator-make/tests/test_utils.py
new file mode 100644
index 00000000000000..f44d649fd8765d
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_utils.py
@@ -0,0 +1,94 @@
+# 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 io
+import json
+import os
+import shutil
+import tempfile
+import unittest
+from unittest import mock
+
+from clang_ssaf_orchestrator_make import utils
+
+
+class MakeParents(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+
+    def test_it_returns_the_path_it_was_given(self):
+        path = os.path.join(self.tmp, "a", "b", "c.json")
+        self.assertEqual(path, utils.make_parents(path))
+        self.assertTrue(os.path.isdir(os.path.dirname(path)))
+
+    def test_an_existing_directory_is_not_an_error(self):
+        path = os.path.join(self.tmp, "a", "c.json")
+        utils.make_parents(path)
+        utils.make_parents(path)
+        self.assertTrue(os.path.isdir(os.path.dirname(path)))
+
+    def test_a_bare_name_has_no_parent_to_make(self):
+        self.assertEqual("c.json", utils.make_parents("c.json"))
+
+    def test_a_parent_that_cannot_be_made_is_not_swallowed(self):
+        blocked = os.path.join(self.tmp, "file")
+        open(blocked, "w").close()
+        with self.assertRaises(OSError):
+            utils.make_parents(os.path.join(blocked, "a", "c.json"))
+
+
+class Json(unittest.TestCase):
+    def setUp(self):
+        self.tmp = tempfile.mkdtemp()
+        self.addCleanup(shutil.rmtree, self.tmp, True)
+        self.path = os.path.join(self.tmp, "doc.json")
+
+    def test_a_document_round_trips(self):
+        utils.write_json(self.path, {"a": [1, 2]})
+        self.assertEqual({"a": [1, 2]}, utils.read_json(self.path))
+
+    def test_writing_leaves_no_temporary_behind(self):
+        utils.write_json(self.path, {})
+        self.assertEqual(["doc.json"], os.listdir(self.tmp))
+
+    def test_a_write_replaces_rather_than_appends(self):
+        utils.write_json(self.path, {"a": 1})
+        utils.write_json(self.path, {"b": 2})
+        self.assertEqual({"b": 2}, utils.read_json(self.path))
+
+    def test_an_absent_file_reads_as_nothing_and_says_nothing(self):
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.assertIsNone(utils.read_json(os.path.join(self.tmp, "absent")))
+        self.assertEqual("", stream.getvalue())
+
+    def test_a_corrupt_file_reads_as_nothing_and_says_so(self):
+        with open(self.path, "w") as handle:
+            handle.write('{"a": ')
+        stream = io.StringIO()
+        with mock.patch("sys.stderr", stream):
+            self.assertIsNone(utils.read_json(self.path))
+        self.assertIn("is not valid JSON", stream.getvalue())
+        self.assertIn(self.path, stream.getvalue())
+
+    def test_the_two_cases_are_distinguishable(self):
+        absent = io.StringIO()
+        corrupt = io.StringIO()
+        with open(self.path, "w") as handle:
+            handle.write("{")
+        with mock.patch("sys.stderr", absent):
+            utils.read_json(os.path.join(self.tmp, "absent"))
+        with mock.patch("sys.stderr", corrupt):
+            utils.read_json(self.path)
+        self.assertNotEqual(absent.getvalue(), corrupt.getvalue())
+
+    def test_what_is_written_is_readable_json(self):
+        utils.write_json(self.path, {"kind": "link", "inputs": ["a.o"]})
+        with open(self.path) as handle:
+            self.assertEqual({"kind": "link", "inputs": ["a.o"]}, json.load(handle))
+
+
+if __name__ == "__main__":
+    unittest.main()



More information about the cfe-commits mailing list