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.
- pants_backend_cmake-0.1.0/LICENSE-MIT.txt +21 -0
- pants_backend_cmake-0.1.0/PKG-INFO +181 -0
- pants_backend_cmake-0.1.0/README.md +142 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/__init__.py +0 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/goals/__init__.py +0 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/goals/package.py +48 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/ninja/__init__.py +0 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/ninja/graph.py +170 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/ninja/parser.py +384 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/register.py +35 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/subsystems/__init__.py +0 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/subsystems/cmake.py +39 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/subsystems/ninja.py +32 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/target_types.py +222 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/__init__.py +0 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/canonicalize.py +98 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/compile.py +113 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/configure.py +407 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/generator.py +320 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/link.py +266 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/probes.py +74 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/replay.py +246 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/scan.py +92 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/targets.py +133 -0
- pants_backend_cmake-0.1.0/pants_backend_cmake/util_rules/toolchain.py +109 -0
- 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.
|
|
File without changes
|
|
File without changes
|
|
@@ -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
|
+
]
|
|
File without changes
|
|
@@ -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
|