pants-backend-cmake 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. pants_backend_cmake-0.1.0/LICENSE-MIT.txt +21 -0
  2. pants_backend_cmake-0.1.0/PKG-INFO +181 -0
  3. pants_backend_cmake-0.1.0/README.md +142 -0
  4. pants_backend_cmake-0.1.0/pants_backend_cmake/__init__.py +0 -0
  5. pants_backend_cmake-0.1.0/pants_backend_cmake/goals/__init__.py +0 -0
  6. pants_backend_cmake-0.1.0/pants_backend_cmake/goals/package.py +48 -0
  7. pants_backend_cmake-0.1.0/pants_backend_cmake/ninja/__init__.py +0 -0
  8. pants_backend_cmake-0.1.0/pants_backend_cmake/ninja/graph.py +170 -0
  9. pants_backend_cmake-0.1.0/pants_backend_cmake/ninja/parser.py +384 -0
  10. pants_backend_cmake-0.1.0/pants_backend_cmake/register.py +35 -0
  11. pants_backend_cmake-0.1.0/pants_backend_cmake/subsystems/__init__.py +0 -0
  12. pants_backend_cmake-0.1.0/pants_backend_cmake/subsystems/cmake.py +39 -0
  13. pants_backend_cmake-0.1.0/pants_backend_cmake/subsystems/ninja.py +32 -0
  14. pants_backend_cmake-0.1.0/pants_backend_cmake/target_types.py +222 -0
  15. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/__init__.py +0 -0
  16. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/canonicalize.py +98 -0
  17. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/compile.py +113 -0
  18. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/configure.py +407 -0
  19. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/generator.py +320 -0
  20. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/link.py +266 -0
  21. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/probes.py +74 -0
  22. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/replay.py +246 -0
  23. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/scan.py +92 -0
  24. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/targets.py +133 -0
  25. pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/toolchain.py +109 -0
  26. pants_backend_cmake-0.1.0/pyproject.toml +57 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2022 Tom Solberg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,181 @@
1
+ Metadata-Version: 2.1
2
+ Name: pants_backend_cmake
3
+ Version: 0.1.0
4
+ Summary: A CMake backend for the Pants build system that replays the generated Ninja graph as cached per-edge processes.
5
+ Keywords: pantsbuild,pants,backend,cmake,ninja,c,c++
6
+ Author-Email: Tom Solberg <me@sbg.dev>
7
+ License: MIT License
8
+
9
+ Copyright (c) 2022 Tom Solberg
10
+
11
+ Permission is hereby granted, free of charge, to any person obtaining a copy
12
+ of this software and associated documentation files (the "Software"), to deal
13
+ in the Software without restriction, including without limitation the rights
14
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15
+ copies of the Software, and to permit persons to whom the Software is
16
+ furnished to do so, subject to the following conditions:
17
+
18
+ The above copyright notice and this permission notice shall be included in all
19
+ copies or substantial portions of the Software.
20
+
21
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
22
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
23
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
24
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
25
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
26
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
27
+ SOFTWARE.
28
+
29
+ Classifier: Development Status :: 3 - Alpha
30
+ Classifier: Programming Language :: Python
31
+ Classifier: Topic :: Software Development :: Build Tools
32
+ Project-URL: Repository, https://github.com/tgolsson/pants-backends
33
+ Project-URL: Code, https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/cmake
34
+ Project-URL: Changelog, https://github.com/tgolsson/pants-backends/tree/main/pants-plugins/cmake/CHANGELOG.md
35
+ Requires-Python: >=3.11
36
+ Requires-Dist: pants_backend_cc>=0.1.0
37
+ Requires-Dist: pants_backend_platforms>=0.1.0
38
+ Description-Content-Type: text/markdown
39
+
40
+ # pants-backend-cmake
41
+
42
+ A [Pants](https://www.pantsbuild.org) backend for CMake projects. CMake stays the source of truth:
43
+ the backend runs `cmake -G Ninja` once, reads the generated `build.ninja`, and replays every edge
44
+ (one translation unit, one archive, one link) as its own cached Pants process. Nothing in
45
+ `CMakeLists.txt` is declared twice, and the unit of caching is the edge, not the project.
46
+ CMake provides the build artefacts; tests, packaging and dependencies are Pants' business.
47
+
48
+ This release targets Pants **2.32.x**. Install it with `pants-backend-cmake==0.1.0` in
49
+ `[GLOBAL].plugins` in `pants.toml`, and enable `pants_backend_cmake` in
50
+ `[GLOBAL].backend_packages`.
51
+
52
+ **Status: alpha.** Linux hosts, cross builds through a `cmake_toolchain_file`, Ninja generator,
53
+ local execution only (the compilers are found on the machine).
54
+
55
+ [`examples/cc`](https://github.com/tgolsson/pants-backends/tree/main/examples/cc) has a `cmake_project` with a static library and an executable, and a
56
+ `cc_binary` and a `cc_test` that link the library.
57
+
58
+ ## Targets
59
+
60
+ - `cmake_project`: the project. `sources` is what CMake reads: CMake files, configure-time
61
+ inputs, and any source or header no dependency provides. Files matching
62
+ `configure_placeholders` (sources and headers by default) reach configure as zero-byte
63
+ placeholders, so editing one never reruns configure; everything else is given as is.
64
+ `dependencies` are `files`, `cc_sources` and codegen targets producing either (such as
65
+ `cpp_generate`); their files are mounted for the compile and link steps, and configure sees
66
+ only those matching `configure_placeholders`. A path both `sources` and a dependency
67
+ provide is an error (drop it with `"!gen/registry.cpp"`). `cmake_args` and `build_dir`
68
+ (default `build`) are passed to CMake. `coverage_cmake_args` are added to `cmake_args` for
69
+ the build coverage consumers link (see C/C++ tools).
70
+
71
+ The project is a target generator: it generates a `cmake_target` `<project>#<name>` for every
72
+ library and executable CMake defines. They are found by a configure that sees `sources` as
73
+ above (files matching `configure_placeholders` as placeholders, the rest as they are) and,
74
+ as placeholders, the paths its explicit `dependencies` provide that match
75
+ `configure_placeholders`, so no generator runs. Only explicit dependencies count, less those
76
+ a `!` entry names, and only `files`, `cc_sources` and targets whose backend declares their
77
+ outputs ahead of time (a `DeclaredOutputsFieldSet` member, such as `cpp_generate`) provide
78
+ paths. It configures once for each platform the project targets (its `platforms` and those
79
+ in `overrides`, the host when unset) that this host has a toolchain for.
80
+ `platforms` and `build_type` on the project apply to the generated targets, and
81
+ `overrides={"app": {...}}` adjusts one of them (but not its `dependencies`, `project` or
82
+ `cmake_name`). Each depends on `<project>#_inputs` (a `cmake_project_inputs`), which owns the
83
+ project's files and dependencies, so `--changed-dependents` reaches it. Goals that replace a
84
+ generator with what it generates (`lint ::`) run the project's own tools, such as
85
+ clang-tidy, on `#_inputs`, with its `build_type`.
86
+
87
+ A setup mistake (a path both `sources` and a dependency provide, a dependency address that
88
+ does not resolve, an unknown platform name, a `build_dir` outside the project) or a failed
89
+ configure does not stop the rest of the repository: the project, or just that platform for a
90
+ failed configure, generates no targets and the error is logged. A platform without a working
91
+ toolchain is a warning, and one no `cc_toolchain` builds for is logged only at debug level.
92
+ It is logged whenever Pants generates the project's targets: on every run without pantsd, and
93
+ with pantsd again after the generation's inputs change (a BUILD file or CMake file edit) or
94
+ pantsd restarts. `pants package` of a hand-written `cmake_target` naming the project raises
95
+ the error, as does `pants lint <project>#_inputs` with clang-tidy enabled, for the host only.
96
+ - `cmake_target`: one CMake `STATIC`, `SHARED`, `MODULE` library or `EXECUTABLE`, named by
97
+ `project` (the `cmake_project`'s address, inferred as a dependency unless the project
98
+ generated it) and `cmake_name` (defaults to the target name); `build_type` sets its
99
+ configure's `CMAKE_BUILD_TYPE`, and when unset the project's `build_type` applies (for a
100
+ parametrized project, the one `project` names, as in `:proj@build_type=Release`). Generated
101
+ ones are the usual way; a hand-written one works the same. `pants package` writes its outputs
102
+ at their build paths, `dist/<build_dir>/libfoo.a`, and those for other `platforms` under
103
+ `dist/<platform>/` (see Platforms). Object libraries are internal and are not addressed.
104
+
105
+ ## C/C++ tools
106
+
107
+ A static or shared `cmake_target` is a [pants-backend-cc](../cc) library, so a `cc_test` or
108
+ `cc_binary` depending on it compiles and links in the project's build directory as CMake would
109
+ build a consumer of it (see How it works); nothing PRIVATE leaks. `clang_tidy_paths` on
110
+ `cmake_project` selects what `pants_backend_cc.lint.clang_tidy` lints.
111
+ A `cpp_generate` target (`pants_backend_cc.generate`) can be a dependency of a `cmake_project`
112
+ or `cc_test`.
113
+
114
+ For `pants test --use-coverage` with `pants_backend_cc.coverage`, a `cc_test` links the
115
+ project's coverage build: a configure of its own with `coverage_cmake_args` added, which should
116
+ make the compiles write gcov notes and the links bring the gcov runtime (`--coverage`, or the
117
+ project's own switch). Its replay keeps each compile's `.gcno`, and the test gets the notes and
118
+ inputs of every compile behind the libraries on its link line. A project without
119
+ `coverage_cmake_args` serves its usual build, and other consumers never see the coverage build.
120
+
121
+ ## Platforms
122
+
123
+ The compilers and `ar` come from the `cc_toolchain` ([pants-backend-cc](../cc)) for the host
124
+ and the platform being built; without any, `gcc`, `g++` and `ar` for the host. A
125
+ `cmake_target` builds for its `platforms`, and for the host when a `cc_test` or `cc_binary`
126
+ links it. Enable `pants_backend_platforms` and `pants_backend_cc` to declare `platform` and
127
+ `cc_toolchain` targets.
128
+
129
+ The backend adds `cmake_toolchain_file` to `cc_toolchain`, passed as `CMAKE_TOOLCHAIN_FILE`: a
130
+ cross build needs one for `CMAKE_SYSTEM_NAME`. See the field's help for what it may contain.
131
+
132
+ ## Subsystems
133
+
134
+ - `[cmake]`: the pinned CMake download (`version`, `known_versions`).
135
+ - `[ninja]`: the pinned Ninja download. Ninja is only needed by configure; the replay never
136
+ runs it.
137
+
138
+ ## How it works
139
+
140
+ 1. **Configure**: `cmake -S <project> -B <build_dir> -G Ninja` in a sandbox, with the
141
+ consumer probes injected through `CMAKE_PROJECT_INCLUDE`. The generated files are kept,
142
+ except the cache, the compiler-id artefacts, the logs and the probe sources; every absolute
143
+ sandbox path in them is rewritten to a placeholder, the paths of CMake's own binaries
144
+ (read from `CMakeCache.txt`) to bare names, and the regenerate edge and its
145
+ "missing input is not an error" twin are dropped, so the configured tree has the same
146
+ digest in every sandbox.
147
+ 2. **Discovery**: the Ninja graph is parsed; CMake's `# ... build statements for <TYPE> target
148
+ <name>` comments give each edge its target and kind; a target's outputs are what its link
149
+ and symlink edges write; the edges one target needs are found by walking producers and
150
+ stopping at other targets' outputs.
151
+ 3. **Replay** (`pants package`): one rule per edge, awaiting its producers, each a process with
152
+ exactly its own inputs: the source, the headers it includes (from a per-TU `-M` scan), the
153
+ configure-time generated files, and the outputs of the producers of its declared inputs.
154
+ The engine schedules the dataflow and memoizes shared edges.
155
+ 4. **Link interface** (`cc_test`): configure injects a *consumer probe* per library and
156
+ language, a hidden executable linking the library (`util_rules/probes.py`). A consumer
157
+ compiles with the probe's compile line (PUBLIC and INTERFACE requirements, transitively,
158
+ plus the project's flags) and links with its link line (the link closure and CMake's
159
+ driver) against the replayed libraries, in the build directory.
160
+
161
+ ## Limitations
162
+
163
+ - Order-only dependencies are not followed. Generated sources produced by
164
+ `add_custom_command` and discovered only through them are an error at planning time:
165
+ check them in and list them as sources, or generate them with a codegen dependency.
166
+ - A scan that fails, or that finds an include the project does not provide, is an error.
167
+ Compiler launchers (`CMAKE_<LANG>_COMPILER_LAUNCHER`, `CMAKE_<LANG>_CLANG_TIDY`) are
168
+ stripped for the scan but kept for the compile, where they need their own environment.
169
+ - Ninja `dyndep` (C++ modules), `subninja` and validation inputs are not handled.
170
+ - Two dependencies providing one path with different contents fail with the engine's
171
+ digest-merge error, which does not name the targets.
172
+ - Debug and coverage builds embed the compile sandbox path (`DW_AT_comp_dir`, `.gcda`
173
+ paths); `-ffile-prefix-map` is not injected. `pants_backend_cc.coverage` maps the `.gcda`
174
+ paths itself.
175
+ - Parametrized projects package to the same `dist/<build_dir>/` path.
176
+ - The `cmake_target`s one `cc_test` or `cc_binary` links must belong to one project. The
177
+ probes are added in the top-level directory's scope, so directory-scoped settings of a
178
+ subdirectory do not reach a consumer. The backend owns `CMAKE_PROJECT_INCLUDE`; a project
179
+ that sets it itself loses it.
180
+ - `cmake_args` are the same for every platform.
181
+ - Pants on Windows is out of scope, and a macOS host is untested.
@@ -0,0 +1,142 @@
1
+ # pants-backend-cmake
2
+
3
+ A [Pants](https://www.pantsbuild.org) backend for CMake projects. CMake stays the source of truth:
4
+ the backend runs `cmake -G Ninja` once, reads the generated `build.ninja`, and replays every edge
5
+ (one translation unit, one archive, one link) as its own cached Pants process. Nothing in
6
+ `CMakeLists.txt` is declared twice, and the unit of caching is the edge, not the project.
7
+ CMake provides the build artefacts; tests, packaging and dependencies are Pants' business.
8
+
9
+ This release targets Pants **2.32.x**. Install it with `pants-backend-cmake==0.1.0` in
10
+ `[GLOBAL].plugins` in `pants.toml`, and enable `pants_backend_cmake` in
11
+ `[GLOBAL].backend_packages`.
12
+
13
+ **Status: alpha.** Linux hosts, cross builds through a `cmake_toolchain_file`, Ninja generator,
14
+ local execution only (the compilers are found on the machine).
15
+
16
+ [`examples/cc`](https://github.com/tgolsson/pants-backends/tree/main/examples/cc) has a `cmake_project` with a static library and an executable, and a
17
+ `cc_binary` and a `cc_test` that link the library.
18
+
19
+ ## Targets
20
+
21
+ - `cmake_project`: the project. `sources` is what CMake reads: CMake files, configure-time
22
+ inputs, and any source or header no dependency provides. Files matching
23
+ `configure_placeholders` (sources and headers by default) reach configure as zero-byte
24
+ placeholders, so editing one never reruns configure; everything else is given as is.
25
+ `dependencies` are `files`, `cc_sources` and codegen targets producing either (such as
26
+ `cpp_generate`); their files are mounted for the compile and link steps, and configure sees
27
+ only those matching `configure_placeholders`. A path both `sources` and a dependency
28
+ provide is an error (drop it with `"!gen/registry.cpp"`). `cmake_args` and `build_dir`
29
+ (default `build`) are passed to CMake. `coverage_cmake_args` are added to `cmake_args` for
30
+ the build coverage consumers link (see C/C++ tools).
31
+
32
+ The project is a target generator: it generates a `cmake_target` `<project>#<name>` for every
33
+ library and executable CMake defines. They are found by a configure that sees `sources` as
34
+ above (files matching `configure_placeholders` as placeholders, the rest as they are) and,
35
+ as placeholders, the paths its explicit `dependencies` provide that match
36
+ `configure_placeholders`, so no generator runs. Only explicit dependencies count, less those
37
+ a `!` entry names, and only `files`, `cc_sources` and targets whose backend declares their
38
+ outputs ahead of time (a `DeclaredOutputsFieldSet` member, such as `cpp_generate`) provide
39
+ paths. It configures once for each platform the project targets (its `platforms` and those
40
+ in `overrides`, the host when unset) that this host has a toolchain for.
41
+ `platforms` and `build_type` on the project apply to the generated targets, and
42
+ `overrides={"app": {...}}` adjusts one of them (but not its `dependencies`, `project` or
43
+ `cmake_name`). Each depends on `<project>#_inputs` (a `cmake_project_inputs`), which owns the
44
+ project's files and dependencies, so `--changed-dependents` reaches it. Goals that replace a
45
+ generator with what it generates (`lint ::`) run the project's own tools, such as
46
+ clang-tidy, on `#_inputs`, with its `build_type`.
47
+
48
+ A setup mistake (a path both `sources` and a dependency provide, a dependency address that
49
+ does not resolve, an unknown platform name, a `build_dir` outside the project) or a failed
50
+ configure does not stop the rest of the repository: the project, or just that platform for a
51
+ failed configure, generates no targets and the error is logged. A platform without a working
52
+ toolchain is a warning, and one no `cc_toolchain` builds for is logged only at debug level.
53
+ It is logged whenever Pants generates the project's targets: on every run without pantsd, and
54
+ with pantsd again after the generation's inputs change (a BUILD file or CMake file edit) or
55
+ pantsd restarts. `pants package` of a hand-written `cmake_target` naming the project raises
56
+ the error, as does `pants lint <project>#_inputs` with clang-tidy enabled, for the host only.
57
+ - `cmake_target`: one CMake `STATIC`, `SHARED`, `MODULE` library or `EXECUTABLE`, named by
58
+ `project` (the `cmake_project`'s address, inferred as a dependency unless the project
59
+ generated it) and `cmake_name` (defaults to the target name); `build_type` sets its
60
+ configure's `CMAKE_BUILD_TYPE`, and when unset the project's `build_type` applies (for a
61
+ parametrized project, the one `project` names, as in `:proj@build_type=Release`). Generated
62
+ ones are the usual way; a hand-written one works the same. `pants package` writes its outputs
63
+ at their build paths, `dist/<build_dir>/libfoo.a`, and those for other `platforms` under
64
+ `dist/<platform>/` (see Platforms). Object libraries are internal and are not addressed.
65
+
66
+ ## C/C++ tools
67
+
68
+ A static or shared `cmake_target` is a [pants-backend-cc](../cc) library, so a `cc_test` or
69
+ `cc_binary` depending on it compiles and links in the project's build directory as CMake would
70
+ build a consumer of it (see How it works); nothing PRIVATE leaks. `clang_tidy_paths` on
71
+ `cmake_project` selects what `pants_backend_cc.lint.clang_tidy` lints.
72
+ A `cpp_generate` target (`pants_backend_cc.generate`) can be a dependency of a `cmake_project`
73
+ or `cc_test`.
74
+
75
+ For `pants test --use-coverage` with `pants_backend_cc.coverage`, a `cc_test` links the
76
+ project's coverage build: a configure of its own with `coverage_cmake_args` added, which should
77
+ make the compiles write gcov notes and the links bring the gcov runtime (`--coverage`, or the
78
+ project's own switch). Its replay keeps each compile's `.gcno`, and the test gets the notes and
79
+ inputs of every compile behind the libraries on its link line. A project without
80
+ `coverage_cmake_args` serves its usual build, and other consumers never see the coverage build.
81
+
82
+ ## Platforms
83
+
84
+ The compilers and `ar` come from the `cc_toolchain` ([pants-backend-cc](../cc)) for the host
85
+ and the platform being built; without any, `gcc`, `g++` and `ar` for the host. A
86
+ `cmake_target` builds for its `platforms`, and for the host when a `cc_test` or `cc_binary`
87
+ links it. Enable `pants_backend_platforms` and `pants_backend_cc` to declare `platform` and
88
+ `cc_toolchain` targets.
89
+
90
+ The backend adds `cmake_toolchain_file` to `cc_toolchain`, passed as `CMAKE_TOOLCHAIN_FILE`: a
91
+ cross build needs one for `CMAKE_SYSTEM_NAME`. See the field's help for what it may contain.
92
+
93
+ ## Subsystems
94
+
95
+ - `[cmake]`: the pinned CMake download (`version`, `known_versions`).
96
+ - `[ninja]`: the pinned Ninja download. Ninja is only needed by configure; the replay never
97
+ runs it.
98
+
99
+ ## How it works
100
+
101
+ 1. **Configure**: `cmake -S <project> -B <build_dir> -G Ninja` in a sandbox, with the
102
+ consumer probes injected through `CMAKE_PROJECT_INCLUDE`. The generated files are kept,
103
+ except the cache, the compiler-id artefacts, the logs and the probe sources; every absolute
104
+ sandbox path in them is rewritten to a placeholder, the paths of CMake's own binaries
105
+ (read from `CMakeCache.txt`) to bare names, and the regenerate edge and its
106
+ "missing input is not an error" twin are dropped, so the configured tree has the same
107
+ digest in every sandbox.
108
+ 2. **Discovery**: the Ninja graph is parsed; CMake's `# ... build statements for <TYPE> target
109
+ <name>` comments give each edge its target and kind; a target's outputs are what its link
110
+ and symlink edges write; the edges one target needs are found by walking producers and
111
+ stopping at other targets' outputs.
112
+ 3. **Replay** (`pants package`): one rule per edge, awaiting its producers, each a process with
113
+ exactly its own inputs: the source, the headers it includes (from a per-TU `-M` scan), the
114
+ configure-time generated files, and the outputs of the producers of its declared inputs.
115
+ The engine schedules the dataflow and memoizes shared edges.
116
+ 4. **Link interface** (`cc_test`): configure injects a *consumer probe* per library and
117
+ language, a hidden executable linking the library (`util_rules/probes.py`). A consumer
118
+ compiles with the probe's compile line (PUBLIC and INTERFACE requirements, transitively,
119
+ plus the project's flags) and links with its link line (the link closure and CMake's
120
+ driver) against the replayed libraries, in the build directory.
121
+
122
+ ## Limitations
123
+
124
+ - Order-only dependencies are not followed. Generated sources produced by
125
+ `add_custom_command` and discovered only through them are an error at planning time:
126
+ check them in and list them as sources, or generate them with a codegen dependency.
127
+ - A scan that fails, or that finds an include the project does not provide, is an error.
128
+ Compiler launchers (`CMAKE_<LANG>_COMPILER_LAUNCHER`, `CMAKE_<LANG>_CLANG_TIDY`) are
129
+ stripped for the scan but kept for the compile, where they need their own environment.
130
+ - Ninja `dyndep` (C++ modules), `subninja` and validation inputs are not handled.
131
+ - Two dependencies providing one path with different contents fail with the engine's
132
+ digest-merge error, which does not name the targets.
133
+ - Debug and coverage builds embed the compile sandbox path (`DW_AT_comp_dir`, `.gcda`
134
+ paths); `-ffile-prefix-map` is not injected. `pants_backend_cc.coverage` maps the `.gcda`
135
+ paths itself.
136
+ - Parametrized projects package to the same `dist/<build_dir>/` path.
137
+ - The `cmake_target`s one `cc_test` or `cc_binary` links must belong to one project. The
138
+ probes are added in the top-level directory's scope, so directory-scoped settings of a
139
+ subdirectory do not reach a consumer. The backend owns `CMAKE_PROJECT_INCLUDE`; a project
140
+ that sets it itself loses it.
141
+ - `cmake_args` are the same for every platform.
142
+ - Pants on Windows is out of scope, and a macOS host is untested.
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ from pants.core.goals.package import BuiltPackage, PackageFieldSet
6
+ from pants.engine.rules import collect_rules, implicitly, rule
7
+ from pants.engine.unions import UnionRule
8
+
9
+ from pants_backend_cmake.target_types import CMakeTargetNameField, CMakeTargetProjectField
10
+ from pants_backend_cmake.util_rules.replay import BuildTargetRequest, build_target
11
+ from pants_backend_cmake.util_rules.targets import CMakeTargetRequest, resolve_cmake_target
12
+ from pants_backend_platforms.util_rules.contract import (
13
+ PackageForPlatformsRequest,
14
+ PlatformBuild,
15
+ PlatformBuildFieldSet,
16
+ package_for_platforms,
17
+ )
18
+ from pants_backend_platforms.util_rules.platforms import TargetPlatform
19
+
20
+
21
+ @dataclass(frozen=True)
22
+ class CMakeTargetFieldSet(PackageFieldSet, PlatformBuildFieldSet):
23
+ required_fields = (CMakeTargetProjectField, CMakeTargetNameField)
24
+
25
+ project: CMakeTargetProjectField
26
+ cmake_name: CMakeTargetNameField
27
+
28
+
29
+ @rule(desc="Build CMake target for a platform")
30
+ async def build_cmake_target_for_platform(
31
+ field_set: CMakeTargetFieldSet, platform: TargetPlatform
32
+ ) -> PlatformBuild:
33
+ resolved = await resolve_cmake_target(CMakeTargetRequest(field_set.address, platform), **implicitly())
34
+ built = await build_target(BuildTargetRequest(resolved.project, resolved.name), **implicitly())
35
+ return PlatformBuild(built.digest, built.outputs)
36
+
37
+
38
+ @rule(desc="Package CMake target")
39
+ async def package_cmake_target(field_set: CMakeTargetFieldSet) -> BuiltPackage:
40
+ return await package_for_platforms(PackageForPlatformsRequest(field_set), **implicitly())
41
+
42
+
43
+ def rules():
44
+ return [
45
+ *collect_rules(),
46
+ UnionRule(PackageFieldSet, CMakeTargetFieldSet),
47
+ UnionRule(PlatformBuildFieldSet, CMakeTargetFieldSet),
48
+ ]
@@ -0,0 +1,170 @@
1
+ """Discovery on a parsed Ninja graph: CMake targets, their final outputs, and the edges one
2
+ target needs, bounded at other addressable targets' outputs."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from pants.util.frozendict import FrozenDict
9
+
10
+ from pants_backend_cmake.ninja.parser import NinjaEdge, NinjaGraph
11
+ from pants_backend_cmake.util_rules.probes import PROBE_PREFIX
12
+
13
+ ADDRESSABLE_KINDS = frozenset({"STATIC_LIBRARY", "SHARED_LIBRARY", "MODULE_LIBRARY", "EXECUTABLE"})
14
+ _SYMLINK_RULES = frozenset({"CMAKE_SYMLINK_LIBRARY", "CMAKE_SYMLINK_EXECUTABLE"})
15
+
16
+
17
+ class UnsupportedGraphError(Exception):
18
+ pass
19
+
20
+
21
+ def is_link_edge(edge: NinjaEdge) -> bool:
22
+ """The edge that writes a target's artefact: CMake names the rules `<LANG>_<KIND>_LINKER__...`;
23
+ versioned libraries add a symlink edge next to it."""
24
+ return "_LINKER__" in edge.rule or edge.rule in _SYMLINK_RULES
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class CMakeTargetInfo:
29
+ name: str
30
+ kind: str
31
+ final_outputs: tuple[str, ...]
32
+
33
+ @property
34
+ def addressable(self) -> bool:
35
+ """Libraries and executables, except the consumer probes."""
36
+ return self.kind in ADDRESSABLE_KINDS and not self.name.startswith(PROBE_PREFIX)
37
+
38
+
39
+ def target_edges(graph: NinjaGraph, name: str) -> list[NinjaEdge]:
40
+ return [edge for edge in graph.edges if edge.target == name]
41
+
42
+
43
+ def discover_targets(graph: NinjaGraph) -> FrozenDict[str, CMakeTargetInfo]:
44
+ """Every CMake target the generator wrote a section for. A library's or executable's final
45
+ outputs are what its link and symlink edges write (`libfoo.so.1.2`, `libfoo.so.1`,
46
+ `libfoo.so`); for other kinds, the outputs of its command edges that none of its own edges
47
+ consume."""
48
+ by_target: dict[str, list[NinjaEdge]] = {}
49
+ for edge in graph.edges:
50
+ if edge.target is not None:
51
+ by_target.setdefault(edge.target, []).append(edge)
52
+
53
+ targets = {}
54
+ for name, edges in by_target.items():
55
+ link_outputs = [out for edge in edges if is_link_edge(edge) for out in edge.all_outputs]
56
+ if link_outputs:
57
+ final = tuple(link_outputs)
58
+ else:
59
+ consumed: set[str] = set()
60
+ for edge in edges:
61
+ consumed.update(edge.inputs, edge.implicit_inputs, edge.order_only)
62
+ produced = [out for edge in edges if not edge.is_phony for out in edge.all_outputs]
63
+ final = tuple(out for out in produced if out not in consumed)
64
+ targets[name] = CMakeTargetInfo(
65
+ name=name, kind=graph.target_kinds.get(name, "UNKNOWN"), final_outputs=final
66
+ )
67
+ return FrozenDict(targets)
68
+
69
+
70
+ def boundary_outputs(targets: FrozenDict[str, CMakeTargetInfo], name: str) -> dict[str, str]:
71
+ """Final output path to owning target, for every addressable target other than `name`."""
72
+ boundary: dict[str, str] = {}
73
+ for other in targets.values():
74
+ if not other.addressable or other.name == name:
75
+ continue
76
+ for out in other.final_outputs:
77
+ if out in boundary:
78
+ raise UnsupportedGraphError(
79
+ f"`{out}` is a final output of both {boundary[out]} and {other.name}"
80
+ )
81
+ boundary[out] = other.name
82
+ return boundary
83
+
84
+
85
+ @dataclass(frozen=True)
86
+ class BuildPlan:
87
+ target: str
88
+ edges: tuple[NinjaEdge, ...]
89
+ """The non-phony edges the target needs, each its own process; the engine orders them."""
90
+ dependencies: tuple[str, ...]
91
+ """Other addressable targets whose final outputs this plan consumes."""
92
+ sources: tuple[str, ...]
93
+ """Leaf inputs: paths no edge produces."""
94
+ outputs: tuple[str, ...]
95
+
96
+
97
+ def plan_target(graph: NinjaGraph, name: str, targets: FrozenDict[str, CMakeTargetInfo]) -> BuildPlan:
98
+ """Walk producers from the target's final outputs through explicit and implicit inputs.
99
+ Stop at another addressable target's final output (a dependency) or at a path nothing
100
+ produces (a source). Order-only inputs are not followed: they sequence a Ninja build but a
101
+ replayed command only reads its declared inputs. A generated file that is reachable only
102
+ through an order-only input (an `add_custom_command` output a source includes) would be
103
+ missing from the replay, so that is an error rather than a silent miss."""
104
+ info = targets[name]
105
+ if not info.final_outputs:
106
+ raise UnsupportedGraphError(f"CMake target `{name}` ({info.kind}) has no outputs to build")
107
+ boundary = boundary_outputs(targets, name)
108
+ producers = graph.producers
109
+
110
+ edges: dict[NinjaEdge, None] = {}
111
+ dependencies: dict[str, None] = {}
112
+ sources: dict[str, None] = {}
113
+ seen: set[str] = set()
114
+ stack = list(info.final_outputs)
115
+ while stack:
116
+ path = stack.pop()
117
+ if path in seen:
118
+ continue
119
+ seen.add(path)
120
+ if path in boundary:
121
+ dependencies[boundary[path]] = None
122
+ continue
123
+ edge = producers.get(path)
124
+ if edge is None:
125
+ sources[path] = None
126
+ continue
127
+ if edge in edges:
128
+ continue
129
+ edges[edge] = None
130
+ stack.extend(edge.dependencies)
131
+
132
+ generated = _generated_via_order_only(graph, edges, boundary)
133
+ if generated:
134
+ raise UnsupportedGraphError(
135
+ f"CMake target `{name}` depends on generated files only through order-only inputs, "
136
+ f"which the replay does not follow: {sorted(generated)}. Check the generated files "
137
+ "in, or list them as sources of the consuming target."
138
+ )
139
+
140
+ return BuildPlan(
141
+ target=name,
142
+ edges=tuple(edge for edge in edges if not edge.is_phony),
143
+ dependencies=tuple(dependencies),
144
+ sources=tuple(sources),
145
+ outputs=info.final_outputs,
146
+ )
147
+
148
+
149
+ def _generated_via_order_only(
150
+ graph: NinjaGraph, edges: dict[NinjaEdge, None], boundary: dict[str, str]
151
+ ) -> set[str]:
152
+ """Custom-command outputs the plan's edges reach only through order-only inputs."""
153
+ producers = graph.producers
154
+ missing: set[str] = set()
155
+ seen: set[str] = set()
156
+ stack = [p for edge in edges for p in edge.order_only]
157
+ while stack:
158
+ path = stack.pop()
159
+ if path in seen or path in boundary:
160
+ continue
161
+ seen.add(path)
162
+ producer = producers.get(path)
163
+ if producer is None:
164
+ continue
165
+ if producer.is_phony:
166
+ stack.extend(producer.dependencies)
167
+ stack.extend(producer.order_only)
168
+ elif producer.rule == "CUSTOM_COMMAND" and producer not in edges:
169
+ missing.add(path)
170
+ return missing