[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