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.
Files changed (47) hide show
  1. cmakeless/__init__.py +70 -0
  2. cmakeless/__main__.py +10 -0
  3. cmakeless/_constants.py +7 -0
  4. cmakeless/_parallel.py +48 -0
  5. cmakeless/_version.py +7 -0
  6. cmakeless/api/__init__.py +52 -0
  7. cmakeless/api/_context.py +246 -0
  8. cmakeless/api/commands.py +167 -0
  9. cmakeless/api/dependencies.py +268 -0
  10. cmakeless/api/options.py +102 -0
  11. cmakeless/api/presets.py +101 -0
  12. cmakeless/api/project.py +1143 -0
  13. cmakeless/api/targets.py +740 -0
  14. cmakeless/api/toolchains.py +99 -0
  15. cmakeless/api/when.py +234 -0
  16. cmakeless/cli.py +266 -0
  17. cmakeless/deps/__init__.py +30 -0
  18. cmakeless/deps/conan.py +153 -0
  19. cmakeless/deps/fetchcontent.py +177 -0
  20. cmakeless/deps/find_package.py +144 -0
  21. cmakeless/deps/lockfile.py +156 -0
  22. cmakeless/deps/provider.py +137 -0
  23. cmakeless/deps/registry.py +210 -0
  24. cmakeless/deps/resolver.py +160 -0
  25. cmakeless/deps/vcpkg.py +210 -0
  26. cmakeless/driver/__init__.py +14 -0
  27. cmakeless/driver/cmake_driver.py +289 -0
  28. cmakeless/driver/error_translation.py +148 -0
  29. cmakeless/driver/file_api.py +121 -0
  30. cmakeless/driver/generators.py +159 -0
  31. cmakeless/emitter/__init__.py +14 -0
  32. cmakeless/emitter/cmake_emitter.py +1270 -0
  33. cmakeless/emitter/presets_emitter.py +132 -0
  34. cmakeless/emitter/sanitizers.py +102 -0
  35. cmakeless/emitter/toolchain_emitter.py +50 -0
  36. cmakeless/emitter/when_emitter.py +75 -0
  37. cmakeless/errors.py +96 -0
  38. cmakeless/model/__init__.py +29 -0
  39. cmakeless/model/nodes.py +659 -0
  40. cmakeless/model/validate.py +1166 -0
  41. cmakeless/observer.py +114 -0
  42. cmakeless/py.typed +0 -0
  43. cmakeless-0.5.0.dist-info/METADATA +339 -0
  44. cmakeless-0.5.0.dist-info/RECORD +47 -0
  45. cmakeless-0.5.0.dist-info/WHEEL +4 -0
  46. cmakeless-0.5.0.dist-info/entry_points.txt +2 -0
  47. 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())
@@ -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)