cmakeless 0.5.0__py3-none-any.whl
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.
- cmakeless/__init__.py +70 -0
- cmakeless/__main__.py +10 -0
- cmakeless/_constants.py +7 -0
- cmakeless/_parallel.py +48 -0
- cmakeless/_version.py +7 -0
- cmakeless/api/__init__.py +52 -0
- cmakeless/api/_context.py +246 -0
- cmakeless/api/commands.py +167 -0
- cmakeless/api/dependencies.py +268 -0
- cmakeless/api/options.py +102 -0
- cmakeless/api/presets.py +101 -0
- cmakeless/api/project.py +1143 -0
- cmakeless/api/targets.py +740 -0
- cmakeless/api/toolchains.py +99 -0
- cmakeless/api/when.py +234 -0
- cmakeless/cli.py +266 -0
- cmakeless/deps/__init__.py +30 -0
- cmakeless/deps/conan.py +153 -0
- cmakeless/deps/fetchcontent.py +177 -0
- cmakeless/deps/find_package.py +144 -0
- cmakeless/deps/lockfile.py +156 -0
- cmakeless/deps/provider.py +137 -0
- cmakeless/deps/registry.py +210 -0
- cmakeless/deps/resolver.py +160 -0
- cmakeless/deps/vcpkg.py +210 -0
- cmakeless/driver/__init__.py +14 -0
- cmakeless/driver/cmake_driver.py +289 -0
- cmakeless/driver/error_translation.py +148 -0
- cmakeless/driver/file_api.py +121 -0
- cmakeless/driver/generators.py +159 -0
- cmakeless/emitter/__init__.py +14 -0
- cmakeless/emitter/cmake_emitter.py +1270 -0
- cmakeless/emitter/presets_emitter.py +132 -0
- cmakeless/emitter/sanitizers.py +102 -0
- cmakeless/emitter/toolchain_emitter.py +50 -0
- cmakeless/emitter/when_emitter.py +75 -0
- cmakeless/errors.py +96 -0
- cmakeless/model/__init__.py +29 -0
- cmakeless/model/nodes.py +659 -0
- cmakeless/model/validate.py +1166 -0
- cmakeless/observer.py +114 -0
- cmakeless/py.typed +0 -0
- cmakeless-0.5.0.dist-info/METADATA +339 -0
- cmakeless-0.5.0.dist-info/RECORD +47 -0
- cmakeless-0.5.0.dist-info/WHEEL +4 -0
- cmakeless-0.5.0.dist-info/entry_points.txt +2 -0
- cmakeless-0.5.0.dist-info/licenses/LICENSE +153 -0
cmakeless/__init__.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""CMakeless: write your C++ builds in Python. Keep CMake. Lose the pain.
|
|
6
|
+
|
|
7
|
+
This module is the ONLY public import surface. Everything under
|
|
8
|
+
cmakeless.model, cmakeless.emitter, cmakeless.driver, and cmakeless.deps is
|
|
9
|
+
private machinery.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from cmakeless._version import __version__
|
|
13
|
+
from cmakeless.api.commands import Command, CustomTarget
|
|
14
|
+
from cmakeless.api.dependencies import Dependency
|
|
15
|
+
from cmakeless.api.options import Option
|
|
16
|
+
from cmakeless.api.presets import Preset
|
|
17
|
+
from cmakeless.api.project import Project
|
|
18
|
+
from cmakeless.api.targets import Executable, Library, PythonModule, Test
|
|
19
|
+
from cmakeless.api.toolchains import Toolchain
|
|
20
|
+
from cmakeless.api.when import When
|
|
21
|
+
from cmakeless.deps.registry import RegistryEntry
|
|
22
|
+
from cmakeless.deps.registry import register as register_dependency
|
|
23
|
+
from cmakeless.driver.file_api import TargetInfo
|
|
24
|
+
from cmakeless.errors import (
|
|
25
|
+
CMakeError,
|
|
26
|
+
CmakelessError,
|
|
27
|
+
ConfigurationError,
|
|
28
|
+
DependencyError,
|
|
29
|
+
Diagnostic,
|
|
30
|
+
ToolchainError,
|
|
31
|
+
)
|
|
32
|
+
from cmakeless.observer import (
|
|
33
|
+
BuildEvent,
|
|
34
|
+
ConsoleObserver,
|
|
35
|
+
Observer,
|
|
36
|
+
StepFailed,
|
|
37
|
+
StepFinished,
|
|
38
|
+
StepStarted,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"BuildEvent",
|
|
43
|
+
"CMakeError",
|
|
44
|
+
"CmakelessError",
|
|
45
|
+
"Command",
|
|
46
|
+
"ConfigurationError",
|
|
47
|
+
"ConsoleObserver",
|
|
48
|
+
"CustomTarget",
|
|
49
|
+
"Dependency",
|
|
50
|
+
"DependencyError",
|
|
51
|
+
"Diagnostic",
|
|
52
|
+
"Executable",
|
|
53
|
+
"Library",
|
|
54
|
+
"Observer",
|
|
55
|
+
"Option",
|
|
56
|
+
"Preset",
|
|
57
|
+
"Project",
|
|
58
|
+
"PythonModule",
|
|
59
|
+
"RegistryEntry",
|
|
60
|
+
"StepFailed",
|
|
61
|
+
"StepFinished",
|
|
62
|
+
"StepStarted",
|
|
63
|
+
"TargetInfo",
|
|
64
|
+
"Test",
|
|
65
|
+
"Toolchain",
|
|
66
|
+
"ToolchainError",
|
|
67
|
+
"When",
|
|
68
|
+
"__version__",
|
|
69
|
+
"register_dependency",
|
|
70
|
+
]
|
cmakeless/__main__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Entry point for 'python -m cmakeless'."""
|
|
6
|
+
|
|
7
|
+
from cmakeless.cli import main
|
|
8
|
+
|
|
9
|
+
if __name__ == "__main__":
|
|
10
|
+
raise SystemExit(main())
|
cmakeless/_constants.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Shared literals with no internal dependencies, safe for any module to import."""
|
|
6
|
+
|
|
7
|
+
BUILD_SCRIPT_NAME = "cmakelessfile.py"
|
cmakeless/_parallel.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Free-threaded helpers: detection and a small parallel-map primitive.
|
|
6
|
+
|
|
7
|
+
The immutable model layer is what makes parallelism safe here: frozen
|
|
8
|
+
dataclasses are shared across threads with no locks and no copies. On a
|
|
9
|
+
standard GIL build these helpers still work, degrading to interleaved I/O
|
|
10
|
+
concurrency, which is where most of the benefit lives anyway.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
from collections.abc import Callable, Iterable, Sequence
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def gil_enabled() -> bool:
|
|
21
|
+
"""Report whether the running interpreter still holds the GIL.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
True on a standard build, False on a free-threaded interpreter;
|
|
25
|
+
True as well on versions predating the detection hook.
|
|
26
|
+
"""
|
|
27
|
+
is_enabled: Callable[[], bool] | None = getattr(sys, "_is_gil_enabled", None)
|
|
28
|
+
if is_enabled is None:
|
|
29
|
+
return True
|
|
30
|
+
return is_enabled()
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def parallel_map[T, R](function: Callable[[T], R], items: Sequence[T]) -> list[R]:
|
|
34
|
+
"""Apply a function to every item concurrently, preserving input order.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
function: The work to run per item; must be thread-safe.
|
|
38
|
+
items: The inputs; an empty sequence returns an empty list without
|
|
39
|
+
starting a pool.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The results in the same order as ``items``.
|
|
43
|
+
"""
|
|
44
|
+
if not items:
|
|
45
|
+
return []
|
|
46
|
+
with ThreadPoolExecutor(max_workers=len(items)) as pool:
|
|
47
|
+
results: Iterable[R] = pool.map(function, items)
|
|
48
|
+
return list(results)
|
cmakeless/_version.py
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Single source of truth for the package version, read by hatchling at build time."""
|
|
6
|
+
|
|
7
|
+
__version__ = "0.5.0"
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Layer 1: what the user touches.
|
|
6
|
+
|
|
7
|
+
Friendly, forgiving, mutable while the user is describing the build.
|
|
8
|
+
Project.build() is the boundary: it freezes everything into the immutable
|
|
9
|
+
model, validates, and only then proceeds.
|
|
10
|
+
|
|
11
|
+
This package re-exports every public class and function from its submodules,
|
|
12
|
+
so `from cmakeless.api import ...` reaches the whole layer-1 surface.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from cmakeless.api.commands import Command, CustomTarget
|
|
16
|
+
from cmakeless.api.dependencies import Dependencies, Dependency
|
|
17
|
+
from cmakeless.api.options import Option
|
|
18
|
+
from cmakeless.api.presets import Preset
|
|
19
|
+
from cmakeless.api.project import Project
|
|
20
|
+
from cmakeless.api.targets import Executable, Library, PythonModule, Test
|
|
21
|
+
from cmakeless.api.toolchains import Toolchain
|
|
22
|
+
from cmakeless.api.when import When
|
|
23
|
+
from cmakeless.observer import (
|
|
24
|
+
BuildEvent,
|
|
25
|
+
ConsoleObserver,
|
|
26
|
+
Observer,
|
|
27
|
+
StepFailed,
|
|
28
|
+
StepFinished,
|
|
29
|
+
StepStarted,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"BuildEvent",
|
|
34
|
+
"Command",
|
|
35
|
+
"ConsoleObserver",
|
|
36
|
+
"CustomTarget",
|
|
37
|
+
"Dependencies",
|
|
38
|
+
"Dependency",
|
|
39
|
+
"Executable",
|
|
40
|
+
"Library",
|
|
41
|
+
"Observer",
|
|
42
|
+
"Option",
|
|
43
|
+
"Preset",
|
|
44
|
+
"Project",
|
|
45
|
+
"PythonModule",
|
|
46
|
+
"StepFailed",
|
|
47
|
+
"StepFinished",
|
|
48
|
+
"StepStarted",
|
|
49
|
+
"Test",
|
|
50
|
+
"Toolchain",
|
|
51
|
+
"When",
|
|
52
|
+
]
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# This Source Code Form is subject to the terms of the Mozilla Public
|
|
2
|
+
# License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
3
|
+
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
4
|
+
|
|
5
|
+
"""Private runtime context shared between the CLI and the API layer.
|
|
6
|
+
|
|
7
|
+
Two pieces of state live here:
|
|
8
|
+
|
|
9
|
+
- Description mode: while a parent project loads a subproject's
|
|
10
|
+
cmakelessfile.py, the child's Project must register itself instead of
|
|
11
|
+
building. A capture stack makes the child's project.build() call a
|
|
12
|
+
harmless no-op.
|
|
13
|
+
- Verb override: 'cmakeless configure' runs the same cmakelessfile.py as
|
|
14
|
+
'cmakeless build'; the override tells the project.build() facade which
|
|
15
|
+
verb the user actually asked for.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
from collections.abc import Generator
|
|
21
|
+
from contextlib import contextmanager
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
from typing import TYPE_CHECKING
|
|
24
|
+
|
|
25
|
+
from cmakeless.errors import ConfigurationError
|
|
26
|
+
|
|
27
|
+
if TYPE_CHECKING:
|
|
28
|
+
from cmakeless.api.project import Project
|
|
29
|
+
|
|
30
|
+
_capture_stack: list[list[Project]] = []
|
|
31
|
+
_loading_scripts: list[Path] = []
|
|
32
|
+
_verb_override: list[str] = []
|
|
33
|
+
_generator_override: list[str] = []
|
|
34
|
+
_preset_override: list[str] = []
|
|
35
|
+
_sanitize_override: list[tuple[str, ...]] = []
|
|
36
|
+
_prefix_override: list[str] = []
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@contextmanager
|
|
40
|
+
def capturing_projects() -> Generator[list[Project]]:
|
|
41
|
+
"""Enter description mode; projects created inside are captured, not built.
|
|
42
|
+
|
|
43
|
+
Yields:
|
|
44
|
+
The live list that collects every Project constructed while the
|
|
45
|
+
context is active.
|
|
46
|
+
"""
|
|
47
|
+
captured: list[Project] = []
|
|
48
|
+
_capture_stack.append(captured)
|
|
49
|
+
try:
|
|
50
|
+
yield captured
|
|
51
|
+
finally:
|
|
52
|
+
_capture_stack.pop()
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def register_project(project: Project) -> None:
|
|
56
|
+
"""Hand a freshly constructed project to the active capture list, if any.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
project: The project that was just constructed.
|
|
60
|
+
"""
|
|
61
|
+
if _capture_stack:
|
|
62
|
+
_capture_stack[-1].append(project)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def in_description_mode() -> bool:
|
|
66
|
+
"""Tell whether a subproject's cmakelessfile.py is currently being loaded.
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
True while at least one capturing_projects() context is active.
|
|
70
|
+
"""
|
|
71
|
+
return bool(_capture_stack)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@contextmanager
|
|
75
|
+
def loading_script(script: Path) -> Generator[None]:
|
|
76
|
+
"""Guard against subproject recursion (a child adding its own ancestor).
|
|
77
|
+
|
|
78
|
+
Args:
|
|
79
|
+
script: The cmakelessfile.py about to be executed.
|
|
80
|
+
|
|
81
|
+
Yields:
|
|
82
|
+
Nothing; the guard is active for the duration of the context.
|
|
83
|
+
|
|
84
|
+
Raises:
|
|
85
|
+
ConfigurationError: When the script is already being loaded higher
|
|
86
|
+
up the chain, which would recurse forever.
|
|
87
|
+
"""
|
|
88
|
+
resolved = script.resolve()
|
|
89
|
+
if resolved in _loading_scripts:
|
|
90
|
+
chain = " -> ".join(str(path) for path in [*_loading_scripts, resolved])
|
|
91
|
+
raise ConfigurationError(
|
|
92
|
+
f"Subproject cycle detected: {chain}. A subproject cannot add one of "
|
|
93
|
+
f"its own ancestors; remove the add_subproject() call that closes "
|
|
94
|
+
f"the loop."
|
|
95
|
+
)
|
|
96
|
+
_loading_scripts.append(resolved)
|
|
97
|
+
try:
|
|
98
|
+
yield
|
|
99
|
+
finally:
|
|
100
|
+
_loading_scripts.pop()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@contextmanager
|
|
104
|
+
def verb_override(verb: str) -> Generator[None]:
|
|
105
|
+
"""Make project.build() perform the given CLI verb instead of a full build.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
verb: The verb the CLI user asked for ("build", "configure", "clean").
|
|
109
|
+
|
|
110
|
+
Yields:
|
|
111
|
+
Nothing; the override is active for the duration of the context.
|
|
112
|
+
"""
|
|
113
|
+
_verb_override.append(verb)
|
|
114
|
+
try:
|
|
115
|
+
yield
|
|
116
|
+
finally:
|
|
117
|
+
_verb_override.pop()
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def active_verb() -> str:
|
|
121
|
+
"""Look up the verb project.build() should perform.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
The innermost overridden verb, or "build" when none is active.
|
|
125
|
+
"""
|
|
126
|
+
return _verb_override[-1] if _verb_override else "build"
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@contextmanager
|
|
130
|
+
def generator_override(generator: str | None) -> Generator[None]:
|
|
131
|
+
"""Make projects prefer the generator the CLI user asked for.
|
|
132
|
+
|
|
133
|
+
Args:
|
|
134
|
+
generator: The generator name from --generator, or None for no
|
|
135
|
+
preference (the context is then a no-op).
|
|
136
|
+
|
|
137
|
+
Yields:
|
|
138
|
+
Nothing; the override is active for the duration of the context.
|
|
139
|
+
"""
|
|
140
|
+
if generator is None:
|
|
141
|
+
yield
|
|
142
|
+
return
|
|
143
|
+
_generator_override.append(generator)
|
|
144
|
+
try:
|
|
145
|
+
yield
|
|
146
|
+
finally:
|
|
147
|
+
_generator_override.pop()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def active_generator() -> str | None:
|
|
151
|
+
"""Look up the generator preference set by the CLI.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
The innermost overridden generator name, or None when unset.
|
|
155
|
+
"""
|
|
156
|
+
return _generator_override[-1] if _generator_override else None
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@contextmanager
|
|
160
|
+
def preset_override(preset: str | None) -> Generator[None]:
|
|
161
|
+
"""Make projects configure and build with the preset the CLI asked for.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
preset: The preset name from --preset, or None for no preference
|
|
165
|
+
(the context is then a no-op).
|
|
166
|
+
|
|
167
|
+
Yields:
|
|
168
|
+
Nothing; the override is active for the duration of the context.
|
|
169
|
+
"""
|
|
170
|
+
if preset is None:
|
|
171
|
+
yield
|
|
172
|
+
return
|
|
173
|
+
_preset_override.append(preset)
|
|
174
|
+
try:
|
|
175
|
+
yield
|
|
176
|
+
finally:
|
|
177
|
+
_preset_override.pop()
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def active_preset() -> str | None:
|
|
181
|
+
"""Look up the preset preference set by the CLI.
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
The innermost overridden preset name, or None when unset.
|
|
185
|
+
"""
|
|
186
|
+
return _preset_override[-1] if _preset_override else None
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
@contextmanager
|
|
190
|
+
def sanitize_override(sanitizers: tuple[str, ...]) -> Generator[None]:
|
|
191
|
+
"""Make 'cmakeless test' run under the given sanitizers.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
sanitizers: Sanitizer names from --sanitize; empty makes the
|
|
195
|
+
context a no-op.
|
|
196
|
+
|
|
197
|
+
Yields:
|
|
198
|
+
Nothing; the override is active for the duration of the context.
|
|
199
|
+
"""
|
|
200
|
+
if not sanitizers:
|
|
201
|
+
yield
|
|
202
|
+
return
|
|
203
|
+
_sanitize_override.append(sanitizers)
|
|
204
|
+
try:
|
|
205
|
+
yield
|
|
206
|
+
finally:
|
|
207
|
+
_sanitize_override.pop()
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def active_sanitize() -> tuple[str, ...]:
|
|
211
|
+
"""Look up the sanitizer selection set by the CLI.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
The innermost overridden sanitizer names, or an empty tuple.
|
|
215
|
+
"""
|
|
216
|
+
return _sanitize_override[-1] if _sanitize_override else ()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@contextmanager
|
|
220
|
+
def prefix_override(prefix: str | None) -> Generator[None]:
|
|
221
|
+
"""Make 'cmakeless install' install into the given prefix.
|
|
222
|
+
|
|
223
|
+
Args:
|
|
224
|
+
prefix: The installation prefix from --prefix, or None for
|
|
225
|
+
CMake's default (the context is then a no-op).
|
|
226
|
+
|
|
227
|
+
Yields:
|
|
228
|
+
Nothing; the override is active for the duration of the context.
|
|
229
|
+
"""
|
|
230
|
+
if prefix is None:
|
|
231
|
+
yield
|
|
232
|
+
return
|
|
233
|
+
_prefix_override.append(prefix)
|
|
234
|
+
try:
|
|
235
|
+
yield
|
|
236
|
+
finally:
|
|
237
|
+
_prefix_override.pop()
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def active_prefix() -> str | None:
|
|
241
|
+
"""Look up the installation prefix set by the CLI.
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
The innermost overridden prefix, or None when unset.
|
|
245
|
+
"""
|
|
246
|
+
return _prefix_override[-1] if _prefix_override else None
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Custom build steps: Command and CustomTarget, created via Project.
|
|
2
|
+
|
|
3
|
+
A Command is a build-time step that produces files other targets or
|
|
4
|
+
commands consume; a CustomTarget is an always-runnable named target with no
|
|
5
|
+
file output (asset cooking, lint, docs).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Sequence
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
from cmakeless.errors import ConfigurationError
|
|
14
|
+
from cmakeless.model.nodes import CommandModel, CustomTargetModel
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class Command:
|
|
18
|
+
"""A build-time step; add_sources()/depends= on its output wires the edge.
|
|
19
|
+
|
|
20
|
+
Attributes:
|
|
21
|
+
outputs: The files this command produces, project-root-relative
|
|
22
|
+
(read-only property).
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
output: Sequence[str],
|
|
29
|
+
command: Sequence[str],
|
|
30
|
+
depends: Sequence[str | Command] = (),
|
|
31
|
+
comment: str | None,
|
|
32
|
+
script: str,
|
|
33
|
+
) -> None:
|
|
34
|
+
"""Describe a build-time step.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
output: Files this command produces, project-root-relative.
|
|
38
|
+
command: The argument vector to run; never a shell string.
|
|
39
|
+
depends: Files (or other Command handles) that trigger a re-run.
|
|
40
|
+
comment: Shown while the command runs, or None.
|
|
41
|
+
script: Display name of the owning build description, used in
|
|
42
|
+
error messages.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
ConfigurationError: When ``output`` or ``command`` is empty.
|
|
46
|
+
"""
|
|
47
|
+
if not output:
|
|
48
|
+
raise ConfigurationError(
|
|
49
|
+
f"add_command() in {script} needs at least one output=..., "
|
|
50
|
+
f"naming the file(s) it produces."
|
|
51
|
+
)
|
|
52
|
+
if not command:
|
|
53
|
+
raise ConfigurationError(
|
|
54
|
+
f"add_command() in {script} needs a non-empty command=[...] to run."
|
|
55
|
+
)
|
|
56
|
+
self._outputs = tuple(output)
|
|
57
|
+
self._command = tuple(command)
|
|
58
|
+
self._depends_raw = tuple(depends)
|
|
59
|
+
self._comment = comment
|
|
60
|
+
|
|
61
|
+
@property
|
|
62
|
+
def outputs(self) -> tuple[str, ...]:
|
|
63
|
+
"""The files this command produces, project-root-relative."""
|
|
64
|
+
return self._outputs
|
|
65
|
+
|
|
66
|
+
def __repr__(self) -> str:
|
|
67
|
+
"""Developer-facing representation.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
The outputs and command of this build step.
|
|
71
|
+
"""
|
|
72
|
+
return f"Command(output={list(self._outputs)!r}, command={list(self._command)!r})"
|
|
73
|
+
|
|
74
|
+
def _freeze(self) -> CommandModel:
|
|
75
|
+
"""Freeze this builder into its immutable model node.
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
The CommandModel; validation happens on the frozen project.
|
|
79
|
+
"""
|
|
80
|
+
return CommandModel(
|
|
81
|
+
outputs=tuple(Path(output) for output in self._outputs),
|
|
82
|
+
command=self._command,
|
|
83
|
+
depends=_freeze_depends(self._depends_raw),
|
|
84
|
+
comment=self._comment,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class CustomTarget:
|
|
89
|
+
"""An always-runnable named target with no file output.
|
|
90
|
+
|
|
91
|
+
Attributes:
|
|
92
|
+
name: The CMake target name (read-only property).
|
|
93
|
+
"""
|
|
94
|
+
|
|
95
|
+
def __init__(
|
|
96
|
+
self,
|
|
97
|
+
name: str,
|
|
98
|
+
*,
|
|
99
|
+
command: Sequence[str],
|
|
100
|
+
depends: Sequence[str | Command] = (),
|
|
101
|
+
script: str,
|
|
102
|
+
) -> None:
|
|
103
|
+
"""Describe an always-run target.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
name: The CMake target name; shares the project's target
|
|
107
|
+
namespace.
|
|
108
|
+
command: The argument vector to run.
|
|
109
|
+
depends: Files (or Command handles) that must be up to date
|
|
110
|
+
first.
|
|
111
|
+
script: Display name of the owning build description, used in
|
|
112
|
+
error messages.
|
|
113
|
+
|
|
114
|
+
Raises:
|
|
115
|
+
ConfigurationError: When ``command`` is empty.
|
|
116
|
+
"""
|
|
117
|
+
if not command:
|
|
118
|
+
raise ConfigurationError(
|
|
119
|
+
f"add_custom_target({name!r}) in {script} needs a non-empty command=[...] to run."
|
|
120
|
+
)
|
|
121
|
+
self._name = name
|
|
122
|
+
self._command = tuple(command)
|
|
123
|
+
self._depends_raw = tuple(depends)
|
|
124
|
+
|
|
125
|
+
@property
|
|
126
|
+
def name(self) -> str:
|
|
127
|
+
"""The CMake target name."""
|
|
128
|
+
return self._name
|
|
129
|
+
|
|
130
|
+
def __repr__(self) -> str:
|
|
131
|
+
"""Developer-facing representation.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
The name and command of this custom target.
|
|
135
|
+
"""
|
|
136
|
+
return f"CustomTarget(name={self._name!r}, command={list(self._command)!r})"
|
|
137
|
+
|
|
138
|
+
def _freeze(self) -> CustomTargetModel:
|
|
139
|
+
"""Freeze this builder into its immutable model node.
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The CustomTargetModel; validation happens on the frozen project.
|
|
143
|
+
"""
|
|
144
|
+
return CustomTargetModel(
|
|
145
|
+
name=self._name,
|
|
146
|
+
command=self._command,
|
|
147
|
+
depends=_freeze_depends(self._depends_raw),
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _freeze_depends(depends: Sequence[str | Command]) -> tuple[Path, ...]:
|
|
152
|
+
"""Flatten a depends= list: plain paths stay, Command handles expand.
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
depends: Plain path strings and/or Command handles.
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Project-root-relative paths: literal entries as given, and every
|
|
159
|
+
Command handle expanded to its declared outputs.
|
|
160
|
+
"""
|
|
161
|
+
resolved: list[Path] = []
|
|
162
|
+
for entry in depends:
|
|
163
|
+
if isinstance(entry, Command):
|
|
164
|
+
resolved.extend(Path(output) for output in entry.outputs)
|
|
165
|
+
else:
|
|
166
|
+
resolved.append(Path(entry))
|
|
167
|
+
return tuple(resolved)
|