rosetta-build 0.1.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.
@@ -0,0 +1,3 @@
1
+ from rosetta_build.cli import main
2
+
3
+ __all__ = ["main"]
@@ -0,0 +1,4 @@
1
+ from rosetta_build.cli import main
2
+
3
+ if __name__ == "__main__":
4
+ main()
@@ -0,0 +1,182 @@
1
+ """PEP 517 build-backend hooks for rosetta-build."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import shutil
7
+ import tempfile
8
+ from collections.abc import Mapping
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from rosetta_build.collect import Collection, CollectionError, collect
13
+ from rosetta_build.execute import BuildError, run_build
14
+ from rosetta_build.language import CompilerFamily
15
+ from rosetta_build.metadata import (
16
+ MetadataError,
17
+ ProjectMetadata,
18
+ load_project_metadata,
19
+ names_match,
20
+ )
21
+ from rosetta_build.plan import BuildPlan, PlanError, plan_build
22
+ from rosetta_build.target import WheelTarget
23
+ from rosetta_build.wheel import write_dist_info
24
+
25
+ __all__ = [
26
+ "BackendError",
27
+ "build_sdist",
28
+ "build_wheel",
29
+ "get_requires_for_build_sdist",
30
+ "get_requires_for_build_wheel",
31
+ "prepare_metadata_for_build_wheel",
32
+ ]
33
+
34
+
35
+ class BackendError(Exception):
36
+ """Raised when a PEP 517 hook cannot complete."""
37
+
38
+
39
+ def get_requires_for_build_wheel(
40
+ config_settings: dict[str, Any] | None = None,
41
+ ) -> list[str]:
42
+ del config_settings
43
+ return []
44
+
45
+
46
+ def get_requires_for_build_sdist(
47
+ config_settings: dict[str, Any] | None = None,
48
+ ) -> list[str]:
49
+ del config_settings
50
+ return []
51
+
52
+
53
+ def prepare_metadata_for_build_wheel(
54
+ metadata_directory: str,
55
+ config_settings: dict[str, Any] | None = None,
56
+ ) -> str:
57
+ del config_settings
58
+ try:
59
+ metadata = _distribution_metadata(Path.cwd())
60
+ return write_dist_info(metadata, Path(metadata_directory))
61
+ except (CollectionError, MetadataError, PlanError, BackendError) as exc:
62
+ raise BackendError(str(exc)) from exc
63
+
64
+
65
+ def build_wheel(
66
+ wheel_directory: str,
67
+ config_settings: dict[str, Any] | None = None,
68
+ metadata_directory: str | None = None,
69
+ ) -> str:
70
+ del metadata_directory
71
+ try:
72
+ plan = _plan_for_cwd(config_settings)
73
+ asyncio.run(run_build(plan))
74
+ target = _primary_wheel_target_name(plan)
75
+ built = plan.wheel_artifact_by_target[target]
76
+ return _copy_artifact(built, Path(wheel_directory))
77
+ except (CollectionError, MetadataError, PlanError, BuildError, BackendError) as exc:
78
+ raise BackendError(str(exc)) from exc
79
+
80
+
81
+ def build_sdist(
82
+ sdist_directory: str,
83
+ config_settings: dict[str, Any] | None = None,
84
+ ) -> str:
85
+ try:
86
+ plan = _plan_for_cwd(config_settings)
87
+ asyncio.run(run_build(plan))
88
+ target = _primary_wheel_target_name(plan)
89
+ built = plan.sdist_artifact_by_target[target]
90
+ return _copy_artifact(built, Path(sdist_directory))
91
+ except (CollectionError, MetadataError, PlanError, BuildError, BackendError) as exc:
92
+ raise BackendError(str(exc)) from exc
93
+
94
+
95
+ def _plan_for_cwd(config_settings: dict[str, Any] | None) -> BuildPlan:
96
+ source_tree = Path.cwd()
97
+ collection = collect(source_tree)
98
+ _primary_wheel_target(collection, load_project_metadata(source_tree))
99
+ build_dir = _build_dir(source_tree, config_settings)
100
+ family = _family(config_settings)
101
+ return plan_build(collection, build_dir=build_dir, family=family)
102
+
103
+
104
+ def _distribution_metadata(source_tree: Path) -> ProjectMetadata:
105
+ collection = collect(source_tree)
106
+ metadata = load_project_metadata(source_tree)
107
+ target = _primary_wheel_target(collection, metadata)
108
+ if names_match(target.name, metadata.name):
109
+ return metadata
110
+ return metadata.with_name(target.name)
111
+
112
+
113
+ def _primary_wheel_target(
114
+ collection: Collection, metadata: ProjectMetadata
115
+ ) -> WheelTarget:
116
+ wheels = [
117
+ target
118
+ for target in collection.targets.values()
119
+ if isinstance(target, WheelTarget)
120
+ ]
121
+ if not wheels:
122
+ raise BackendError(
123
+ 'PEP 517 backend requires at least one type = "wheel" target'
124
+ )
125
+ matching = [target for target in wheels if names_match(target.name, metadata.name)]
126
+ if len(matching) == 1:
127
+ return matching[0]
128
+ if len(wheels) == 1:
129
+ return wheels[0]
130
+ names = ", ".join(sorted(target.name for target in wheels))
131
+ raise BackendError(
132
+ f"ambiguous wheel targets for distribution {metadata.name!r}: {names}"
133
+ )
134
+
135
+
136
+ def _primary_wheel_target_name(plan: BuildPlan) -> str:
137
+ if len(plan.wheel_steps) == 1:
138
+ return plan.wheel_steps[0].target
139
+ if not plan.wheel_steps:
140
+ raise BackendError("planned build has no wheel targets")
141
+ names = ", ".join(step.target for step in plan.wheel_steps)
142
+ raise BackendError(f"planned build has multiple wheel targets: {names}")
143
+
144
+
145
+ def _build_dir(source_tree: Path, config_settings: dict[str, Any] | None) -> Path:
146
+ configured = _config_value(config_settings, "build-dir", "build_dir")
147
+ if configured is not None:
148
+ path = Path(configured)
149
+ if not path.is_absolute():
150
+ path = source_tree / path
151
+ return path
152
+ return Path(tempfile.mkdtemp(prefix="rosetta-build-"))
153
+
154
+
155
+ def _family(config_settings: dict[str, Any] | None) -> CompilerFamily:
156
+ configured = _config_value(config_settings, "family") or CompilerFamily.GCC.value
157
+ try:
158
+ return CompilerFamily(configured)
159
+ except ValueError as exc:
160
+ raise BackendError(f"unknown compiler family: {configured!r}") from exc
161
+
162
+
163
+ def _config_value(config_settings: Mapping[str, Any] | None, *keys: str) -> str | None:
164
+ if not config_settings:
165
+ return None
166
+ for key in keys:
167
+ if key not in config_settings:
168
+ continue
169
+ value = config_settings[key]
170
+ if isinstance(value, list):
171
+ if not value:
172
+ return None
173
+ return str(value[-1])
174
+ return str(value)
175
+ return None
176
+
177
+
178
+ def _copy_artifact(built: Path, destination_dir: Path) -> str:
179
+ destination_dir.mkdir(parents=True, exist_ok=True)
180
+ dest = destination_dir / built.name
181
+ shutil.copy2(built, dest)
182
+ return dest.name
rosetta_build/cli.py ADDED
@@ -0,0 +1,217 @@
1
+ """Command-line interface for rosetta-build."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import asyncio
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ from rosetta_build.collect import Collection, CollectionError, collect
11
+ from rosetta_build.collect_graphviz import write_collection_dot
12
+ from rosetta_build.execute import BuildError, run_build, run_link
13
+ from rosetta_build.language import CompilerFamily
14
+ from rosetta_build.plan import BuildPlan, PlanError, plan_build
15
+ from rosetta_build.target import (
16
+ DynamicLibraryTarget,
17
+ ExecutableTarget,
18
+ ModuleImplementationTarget,
19
+ ModuleInterfaceTarget,
20
+ ModulePartitionTarget,
21
+ StaticLibraryTarget,
22
+ Target,
23
+ WheelTarget,
24
+ )
25
+
26
+
27
+ def main(argv: list[str] | None = None) -> None:
28
+ parser = _build_parser()
29
+ args = parser.parse_args(argv)
30
+ try:
31
+ raise SystemExit(args.handler(args))
32
+ except CollectionError as exc:
33
+ print(f"error: {exc}", file=sys.stderr)
34
+ raise SystemExit(1) from exc
35
+ except PlanError as exc:
36
+ print(f"error: {exc}", file=sys.stderr)
37
+ raise SystemExit(1) from exc
38
+ except BuildError as exc:
39
+ print(f"error: {exc}", file=sys.stderr)
40
+ if exc.stderr:
41
+ print(exc.stderr, file=sys.stderr, end="")
42
+ raise SystemExit(exc.returncode or 1) from exc
43
+
44
+
45
+ def _build_parser() -> argparse.ArgumentParser:
46
+ parser = argparse.ArgumentParser(
47
+ prog="rosetta-build",
48
+ description="Experimental multilanguage build tool.",
49
+ )
50
+ subparsers = parser.add_subparsers(dest="command", required=True)
51
+
52
+ collect_parser = subparsers.add_parser(
53
+ "collect",
54
+ help=(
55
+ "Load and validate targets, then build source, usage, link, "
56
+ "and module graphs."
57
+ ),
58
+ )
59
+ collect_parser.add_argument(
60
+ "source_tree",
61
+ type=Path,
62
+ help="Source tree root containing pyproject.toml.",
63
+ )
64
+ collect_parser.add_argument(
65
+ "--graphviz",
66
+ type=Path,
67
+ metavar="OUT",
68
+ help="Write Graphviz DOT for the collected graphs to OUT.",
69
+ )
70
+ collect_parser.set_defaults(handler=_cmd_collect)
71
+
72
+ build_parser = subparsers.add_parser(
73
+ "build",
74
+ help=(
75
+ "Compile native/module targets and build wheel/sdist targets "
76
+ "(GCC/g++ for native for now)."
77
+ ),
78
+ )
79
+ _add_build_args(build_parser)
80
+ build_parser.set_defaults(handler=_cmd_build)
81
+
82
+ link_parser = subparsers.add_parser(
83
+ "link",
84
+ help="Link compiled objects into libraries and executables.",
85
+ )
86
+ _add_build_args(link_parser)
87
+ link_parser.set_defaults(handler=_cmd_link)
88
+
89
+ install_parser = subparsers.add_parser(
90
+ "install",
91
+ help="Install build artifacts (not yet implemented).",
92
+ )
93
+ install_parser.set_defaults(handler=_cmd_not_implemented, step_name="install")
94
+
95
+ return parser
96
+
97
+
98
+ def _add_build_args(parser: argparse.ArgumentParser) -> None:
99
+ parser.add_argument(
100
+ "source_tree",
101
+ type=Path,
102
+ help="Source tree root containing pyproject.toml.",
103
+ )
104
+ parser.add_argument(
105
+ "--build-dir",
106
+ type=Path,
107
+ default=None,
108
+ help="Build output directory (default: <source_tree>/build).",
109
+ )
110
+ parser.add_argument(
111
+ "--family",
112
+ type=str,
113
+ default=CompilerFamily.GCC.value,
114
+ choices=[family.value for family in CompilerFamily],
115
+ help="Compiler family (default: gcc).",
116
+ )
117
+
118
+
119
+ def _cmd_collect(args: argparse.Namespace) -> int:
120
+ collection = collect(args.source_tree)
121
+ _print_collection(collection)
122
+ if args.graphviz is not None:
123
+ write_collection_dot(collection, args.graphviz)
124
+ print(f"wrote graphviz: {args.graphviz}")
125
+ return 0
126
+
127
+
128
+ def _cmd_build(args: argparse.Namespace) -> int:
129
+ plan = _plan_from_args(args)
130
+ asyncio.run(run_build(plan))
131
+ print(f"compiled {len(plan.compile_steps)} translation unit(s)")
132
+ print(f"built {len(plan.wheel_steps)} wheel/sdist target(s)")
133
+ for name, path in sorted(plan.wheel_artifact_by_target.items()):
134
+ print(f" wheel {name}: {path}")
135
+ for name, path in sorted(plan.sdist_artifact_by_target.items()):
136
+ print(f" sdist {name}: {path}")
137
+ return 0
138
+
139
+
140
+ def _cmd_link(args: argparse.Namespace) -> int:
141
+ plan = _plan_from_args(args)
142
+ asyncio.run(run_link(plan))
143
+ print(f"linked {len(plan.link_steps)} artifact(s)")
144
+ for name, path in sorted(plan.link_artifact_by_target.items()):
145
+ print(f" {name}: {path}")
146
+ return 0
147
+
148
+
149
+ def _plan_from_args(args: argparse.Namespace) -> BuildPlan:
150
+ collection = collect(args.source_tree)
151
+ build_dir = args.build_dir
152
+ if build_dir is None:
153
+ build_dir = collection.source_tree / "build"
154
+ return plan_build(
155
+ collection,
156
+ build_dir=build_dir,
157
+ family=CompilerFamily(args.family),
158
+ )
159
+
160
+
161
+ def _cmd_not_implemented(args: argparse.Namespace) -> int:
162
+ print(f"error: '{args.step_name}' is not implemented yet", file=sys.stderr)
163
+ return 1
164
+
165
+
166
+ def _print_collection(collection: Collection) -> None:
167
+ print(f"source tree: {collection.source_tree}")
168
+ print(f"targets: {len(collection.targets)}")
169
+ for name in sorted(collection.targets):
170
+ target = collection.targets[name]
171
+ sources = collection.source_graph.sources_for(name)
172
+ usage = collection.usage_graph.dependencies_of(name)
173
+ links = collection.link_graph.dependencies_of(name)
174
+ module_imports = collection.module_graph.dependencies_of(name)
175
+ print(f" {name} ({_target_kind(target)})")
176
+ print(f" config: {target.config_path}")
177
+ print(f" sources ({len(sources)}):")
178
+ for source in sorted(sources):
179
+ print(f" {source.relative_to(collection.source_tree)}")
180
+ print(f" usage ({len(usage)}):")
181
+ for dep in sorted(usage):
182
+ print(f" {dep}")
183
+ print(f" dynamic link libraries ({len(links)}):")
184
+ for lib in sorted(links):
185
+ print(f" {lib}")
186
+ print(f" module deps ({len(module_imports)}):")
187
+ for dep in sorted(module_imports):
188
+ print(f" {dep}")
189
+ exports = collection.module_exports.get(name, frozenset())
190
+ if exports:
191
+ print(f" module exports ({len(exports)}):")
192
+ for exported in sorted(exports):
193
+ print(f" {exported}")
194
+ print("dynamic link order:")
195
+ for name in collection.link_graph.topological_order(kind="dynamic link"):
196
+ print(f" {name}")
197
+ print("module order:")
198
+ for name in collection.module_graph.topological_order(kind="module"):
199
+ print(f" {name}")
200
+
201
+
202
+ def _target_kind(target: Target) -> str:
203
+ if isinstance(target, ExecutableTarget):
204
+ return "executable"
205
+ if isinstance(target, StaticLibraryTarget):
206
+ return "static_library"
207
+ if isinstance(target, DynamicLibraryTarget):
208
+ return "dynamic_library"
209
+ if isinstance(target, ModuleInterfaceTarget):
210
+ return "module_interface"
211
+ if isinstance(target, ModulePartitionTarget):
212
+ return "module_partition"
213
+ if isinstance(target, ModuleImplementationTarget):
214
+ return "module_implementation"
215
+ if isinstance(target, WheelTarget):
216
+ return "wheel"
217
+ return type(target).__name__