[clang] [llvm] [clang][ssaf] Add clang-ssaf-orchestrator-make (PR #232543)
Jan Korous via cfe-commits
cfe-commits at lists.llvm.org
Thu Oct 15 16:55:18 PDT 2026
https://github.com/jkorous-apple created https://github.com/llvm/llvm-project/pull/232543
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++, ld, ld.lld and
ld64.lld 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 supports POSIX hosts only, so it is neither built nor tested on
Windows. Its tests build small projects, so they run only where GNU make
is available, the default target is the host, and clang can link a C++
program for it.
This relands dbde204202ad (#231583) together with the fixes that
followed it, 9d9ad8a3dbc5 (#231678), 9d4c617fc399 (#231720) and
3e93f25d161f (#231930), after the revert in 782610a701b4 (#232274). The
GN build files come from 73109fa8894a (#232113).
Co-authored-by: Simon Tatham <simon.tatham at arm.com>
Assisted-by: Claude
>From 98914e8a8655486fff50f3d51ed7ff57ce448141 Mon Sep 17 00:00:00 2001
From: Jan Korous <jkorous at apple.com>
Date: Thu, 15 Oct 2026 14:30:56 -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++, ld, ld.lld and
ld64.lld 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 supports POSIX hosts only, so it is neither built nor tested on
Windows. Its tests build small projects, so they run only where GNU make
is available, the default target is the host, and clang can link a C++
program for it.
This relands dbde204202ad (#231583) together with the fixes that
followed it, 9d9ad8a3dbc5 (#231678), 9d4c617fc399 (#231720) and
3e93f25d161f (#231930), after the revert in 782610a701b4 (#232274). The
GN build files come from 73109fa8894a (#232113).
Co-authored-by: Simon Tatham <simon.tatham at arm.com>
Assisted-by: Claude
---
.../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 ++++
.../link-driver-ld-lld.test | 41 ++
.../link-driver-ld64-lld.test | 41 ++
.../ssaf-orchestrator-make/link-driver.test | 29 ++
.../link-unit-selection.test | 56 +++
.../ssaf-orchestrator-make/lit.local.cfg | 104 +++++
.../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 | 6 +
clang/tools/CMakeLists.txt | 3 +
.../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 | 117 +++++
.../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 | 260 +++++++++++
.../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 ++++
llvm/utils/gn/secondary/clang/test/BUILD.gn | 1 +
.../clang-ssaf-orchestrator-make/BUILD.gn | 63 +++
69 files changed, 5976 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-ld-lld.test
create mode 100644 clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver-ld64-lld.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
create mode 100644 llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn
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..1b0e939e7b4064
--- /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++`, `ld`, `ld.lld` and
+`ld64.lld` — each pointing at the shim that stands in for it, and prepends
+that directory to `PATH`. It also puts the path of a configuration file in
+`SSAF_CONFIG`.
+
+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..2feb92b773d027
--- /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)
+
+#--- 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-ld-lld.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver-ld-lld.test
new file mode 100644
index 00000000000000..e8fbf934c8e01c
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver-ld-lld.test
@@ -0,0 +1,41 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+# RUN: chmod +x %t/bin/ld.lld
+
+# A clang that links with lld, by default or through -fuse-ld=lld, runs ld.lld.
+# --ld-path makes this one run a fake ld.lld the same way.
+# 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++
+all: build/prg
+build/a.o: src/a.cpp
+ $(CXX) -c -o $@ $<
+build/prg: build/a.o
+ $(CXX) --ld-path=./bin/ld.lld -o $@ build/a.o
+
+#--- src/a.cpp
+int main() { return 0; }
+
+#--- bin/ld.lld
+#!/bin/sh
+prev=
+for arg in "$@"; do
+ if [ "$prev" = "-o" ]; then
+ : > "$arg"
+ fi
+ prev=$arg
+done
diff --git a/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver-ld64-lld.test b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver-ld64-lld.test
new file mode 100644
index 00000000000000..175170a714cadb
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver-ld64-lld.test
@@ -0,0 +1,41 @@
+
+# REQUIRES: ssaf-orchestrator-make-build
+# RUN: rm -rf %t && split-file %s %t
+# RUN: mkdir -p %t/build
+# RUN: chmod +x %t/bin/ld64.lld
+
+# On Darwin, a clang that links with lld, by default or through -fuse-ld=lld,
+# runs ld64.lld. --ld-path makes this one run a fake ld64.lld the same way.
+# 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++
+all: build/prg
+build/a.o: src/a.cpp
+ $(CXX) -c -o $@ $<
+build/prg: build/a.o
+ $(CXX) --ld-path=./bin/ld64.lld -o $@ build/a.o
+
+#--- src/a.cpp
+int main() { return 0; }
+
+#--- bin/ld64.lld
+#!/bin/sh
+prev=
+for arg in "$@"; do
+ if [ "$prev" = "-o" ]; then
+ : > "$arg"
+ fi
+ prev=$arg
+done
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..29d78ce5bdf61d
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/link-driver.test
@@ -0,0 +1,29 @@
+
+# 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)
+all: build/prg
+build/a.o: src/a.cpp
+ $(CXX) -c -o $@ $<
+build/prg: build/a.o
+ $(LD) -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..5c2bcb499b2c86
--- /dev/null
+++ b/clang/test/Analysis/Scalable/ssaf-orchestrator-make/lit.local.cfg
@@ -0,0 +1,104 @@
+# -*- Python -*-
+
+import lit.util
+import os
+import platform
+import subprocess
+import tempfile
+
+if platform.system() == "Windows":
+ config.unsupported = True
+
+
+def _run(command, stdin=None):
+ try:
+ return subprocess.run(
+ command,
+ input=stdin,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.DEVNULL,
+ env=config.environment,
+ )
+ except OSError:
+ return None
+
+
+def _succeeds(command, stdin=None):
+ done = _run(command, stdin)
+ return done is not None and done.returncode == 0
+
+
+# The tests build projects with make, so they need GNU make and a clang that
+# compiles and links C++ programs for the host.
+def _can_build_projects():
+ path = config.environment["PATH"]
+ make = lit.util.which("make", path)
+ version = _run([make, "--version"]) if make else None
+ if not version or version.returncode or not version.stdout.startswith(b"GNU Make"):
+ return False
+ with tempfile.TemporaryDirectory() as tmp:
+ obj = os.path.join(tmp, "a.o")
+ cxx = [config.clang, "--driver-mode=g++"]
+ main = b"int main() { return 0; }\n"
+ return (
+ _succeeds(cxx + ["-x", "c++", "-", "-c", "-o", obj], main)
+ and _succeeds(cxx + [obj, "-o", os.path.join(tmp, "a.out")])
+ )
+
+
+if "native" in config.available_features and _can_build_projects():
+ 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..72a4f2760df844 100644
--- a/clang/test/CMakeLists.txt
+++ b/clang/test/CMakeLists.txt
@@ -113,6 +113,12 @@ list(APPEND CLANG_TEST_DEPS
hmaptool
)
+if(NOT WIN32)
+ list(APPEND CLANG_TEST_DEPS
+ clang-ssaf-orchestrator-make
+ )
+endif()
+
if(CLANG_ENABLE_CIR)
list(APPEND CLANG_TEST_DEPS
cir-opt
diff --git a/clang/tools/CMakeLists.txt b/clang/tools/CMakeLists.txt
index 855af780d4e02a..182489653b8055 100644
--- a/clang/tools/CMakeLists.txt
+++ b/clang/tools/CMakeLists.txt
@@ -18,6 +18,9 @@ add_clang_subdirectory(clang-scan-deps)
add_clang_subdirectory(clang-ssaf-analyzer)
add_clang_subdirectory(clang-ssaf-format)
add_clang_subdirectory(clang-ssaf-linker)
+if(NOT WIN32)
+ add_clang_subdirectory(clang-ssaf-orchestrator-make)
+endif()
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..27ebbfb36b1aaa
--- /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 "${LLVM_BINARY_DIR}/${build_dir}/${name}")
+ add_custom_command(OUTPUT "${copied}"
+ COMMAND ${CMAKE_COMMAND} -E make_directory
+ "${LLVM_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 "${LLVM_BINARY_DIR}/${SsafOrchestratorMakePackageDir}/version.py")
+add_custom_command(OUTPUT "${version_py}"
+ COMMAND ${CMAKE_COMMAND} -E make_directory
+ "${LLVM_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..44160369c8181d
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/lib/clang_ssaf_orchestrator_make/config.py
@@ -0,0 +1,117 @@
+# 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",
+ "ld.lld": "ld",
+ "ld64.lld": "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..cb03251629d868
--- /dev/null
+++ b/clang/tools/clang-ssaf-orchestrator-make/tests/test_clang_shim.py
@@ -0,0 +1,260 @@
+# 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, config
+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 config.SHIM_FOR_LINK_NAME:
+ 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_link_run_by_ld_lld_reaches_the_ld_shim(self):
+ clang._invoke_shim_for_job(
+ self.shim, self._job(["/build/bin/ld.lld", "-o", "prg", "a.o"]), self.tmp
+ )
+ link = self.shim.commands[0][1][0]
+ self.assertEqual(os.path.join(self.links, "ld.lld"), link)
+ self.assertEqual("ld", config.SHIM_FOR_LINK_NAME[os.path.basename(link)])
+
+ def test_a_link_run_by_ld64_lld_reaches_the_ld_shim(self):
+ clang._invoke_shim_for_job(
+ self.shim, self._job(["/build/bin/ld64.lld", "-o", "prg", "a.o"]), self.tmp
+ )
+ link = self.shim.commands[0][1][0]
+ self.assertEqual(os.path.join(self.links, "ld64.lld"), link)
+ self.assertEqual("ld", config.SHIM_FOR_LINK_NAME[os.path.basename(link)])
+
+ 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()
diff --git a/llvm/utils/gn/secondary/clang/test/BUILD.gn b/llvm/utils/gn/secondary/clang/test/BUILD.gn
index 84486497551873..370c93f043f628 100644
--- a/llvm/utils/gn/secondary/clang/test/BUILD.gn
+++ b/llvm/utils/gn/secondary/clang/test/BUILD.gn
@@ -167,6 +167,7 @@ group("test") {
"//clang/tools/clang-ssaf-analyzer",
"//clang/tools/clang-ssaf-format",
"//clang/tools/clang-ssaf-linker",
+ "//clang/tools/clang-ssaf-orchestrator-make",
"//clang/tools/clang-ssaf-src-edit-merge",
"//clang/tools/clang-sycl-linker",
"//clang/tools/diagtool",
diff --git a/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn b/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn
new file mode 100644
index 00000000000000..f79551c068775d
--- /dev/null
+++ b/llvm/utils/gn/secondary/clang/tools/clang-ssaf-orchestrator-make/BUILD.gn
@@ -0,0 +1,63 @@
+import("//llvm/utils/gn/build/write_cmake_config.gni")
+import("//llvm/version.gni")
+
+# Python tool. Copies its scripts into the build dir, like scan-build.
+group("clang-ssaf-orchestrator-make") {
+ deps = [
+ ":bin",
+ ":lib",
+ ":shim",
+ ":shims",
+ ":version",
+ ]
+}
+
+copy("bin") {
+ sources = [ "bin/clang-ssaf-orchestrator-make" ]
+ outputs = [ "$root_build_dir/bin/{{source_file_part}}" ]
+}
+
+copy("lib") {
+ sources = [
+ "lib/clang_ssaf_orchestrator_make/__init__.py",
+ "lib/clang_ssaf_orchestrator_make/build_graph.py",
+ "lib/clang_ssaf_orchestrator_make/config.py",
+ "lib/clang_ssaf_orchestrator_make/config_orchestrator.py",
+ "lib/clang_ssaf_orchestrator_make/config_shims.py",
+ "lib/clang_ssaf_orchestrator_make/failure_logs.py",
+ "lib/clang_ssaf_orchestrator_make/interposition.py",
+ "lib/clang_ssaf_orchestrator_make/logging_diagnostics.py",
+ "lib/clang_ssaf_orchestrator_make/options.py",
+ "lib/clang_ssaf_orchestrator_make/orchestrator.py",
+ "lib/clang_ssaf_orchestrator_make/ssaf_artifact_repository.py",
+ "lib/clang_ssaf_orchestrator_make/utils.py",
+ ]
+ outputs = [
+ "$root_build_dir/lib/clang_ssaf_orchestrator_make/{{source_file_part}}",
+ ]
+}
+
+write_cmake_config("version") {
+ input = "lib/clang_ssaf_orchestrator_make/version.py.in"
+ output = "$root_build_dir/lib/clang_ssaf_orchestrator_make/version.py"
+ values = [ "CLANG_VERSION=$llvm_version" ]
+}
+
+copy("shims") {
+ sources = [
+ "lib/clang_ssaf_orchestrator_make/shims/__init__.py",
+ "lib/clang_ssaf_orchestrator_make/shims/clang.py",
+ "lib/clang_ssaf_orchestrator_make/shims/commands.py",
+ "lib/clang_ssaf_orchestrator_make/shims/driver_jobs.py",
+ "lib/clang_ssaf_orchestrator_make/shims/ld.py",
+ "lib/clang_ssaf_orchestrator_make/shims/run.py",
+ "lib/clang_ssaf_orchestrator_make/shims/shim.py",
+ ]
+ outputs = [ "$root_build_dir/lib/clang_ssaf_orchestrator_make/shims/{{source_file_part}}" ]
+}
+
+copy("shim") {
+ sources = [ "shims/shim" ]
+ outputs =
+ [ "$root_build_dir/libexec/ssaf-orchestrator-shims/{{source_file_part}}" ]
+}
More information about the cfe-commits
mailing list