mojox-core 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.
- mojox_core/__init__.py +63 -0
- mojox_core/_errors.py +16 -0
- mojox_core/_types.py +235 -0
- mojox_core/environment.py +134 -0
- mojox_core/io/__init__.py +1 -0
- mojox_core/io/environment.py +114 -0
- mojox_core/io/manifest.py +29 -0
- mojox_core/io/toolchain.py +98 -0
- mojox_core/manifest.py +354 -0
- mojox_core/metadata.py +118 -0
- mojox_core/plan.py +333 -0
- mojox_core/policy.py +160 -0
- mojox_core/py.typed +0 -0
- mojox_core/settings.py +90 -0
- mojox_core/targets.py +195 -0
- mojox_core-0.5.0.dist-info/METADATA +178 -0
- mojox_core-0.5.0.dist-info/RECORD +18 -0
- mojox_core-0.5.0.dist-info/WHEEL +4 -0
mojox_core/__init__.py
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""mojox-core: pure model layer for Mojo build tooling.
|
|
2
|
+
|
|
3
|
+
Public API:
|
|
4
|
+
- Types: Manifest, Command, Policy, ResolvedEnv, TargetGraph, etc.
|
|
5
|
+
- Transformers: parse_manifest, build_env, discover, resolve, plan, serialize
|
|
6
|
+
- Errors: ConfigError
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from ._errors import ConfigError
|
|
10
|
+
from ._types import (
|
|
11
|
+
BinaryEntry,
|
|
12
|
+
Command,
|
|
13
|
+
CommandKind,
|
|
14
|
+
Diagnostic,
|
|
15
|
+
DistEntry,
|
|
16
|
+
DistKind,
|
|
17
|
+
HostFacts,
|
|
18
|
+
LintConfig,
|
|
19
|
+
LocalSettings,
|
|
20
|
+
Manifest,
|
|
21
|
+
Policy,
|
|
22
|
+
Profile,
|
|
23
|
+
ResolvedEnv,
|
|
24
|
+
Target,
|
|
25
|
+
TargetGraph,
|
|
26
|
+
TargetKind,
|
|
27
|
+
Toolchain,
|
|
28
|
+
)
|
|
29
|
+
from .environment import build_env
|
|
30
|
+
from .manifest import parse_manifest
|
|
31
|
+
from .metadata import serialize
|
|
32
|
+
from .plan import plan
|
|
33
|
+
from .policy import resolve
|
|
34
|
+
from .settings import parse_settings
|
|
35
|
+
from .targets import discover
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"BinaryEntry",
|
|
39
|
+
"Command",
|
|
40
|
+
"CommandKind",
|
|
41
|
+
"ConfigError",
|
|
42
|
+
"Diagnostic",
|
|
43
|
+
"DistEntry",
|
|
44
|
+
"DistKind",
|
|
45
|
+
"HostFacts",
|
|
46
|
+
"LintConfig",
|
|
47
|
+
"LocalSettings",
|
|
48
|
+
"Manifest",
|
|
49
|
+
"Policy",
|
|
50
|
+
"Profile",
|
|
51
|
+
"ResolvedEnv",
|
|
52
|
+
"Target",
|
|
53
|
+
"TargetGraph",
|
|
54
|
+
"TargetKind",
|
|
55
|
+
"Toolchain",
|
|
56
|
+
"build_env",
|
|
57
|
+
"discover",
|
|
58
|
+
"parse_manifest",
|
|
59
|
+
"parse_settings",
|
|
60
|
+
"plan",
|
|
61
|
+
"resolve",
|
|
62
|
+
"serialize",
|
|
63
|
+
]
|
mojox_core/_errors.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""User-facing configuration errors with structured key paths."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class ConfigError(Exception):
|
|
7
|
+
"""A configuration error carrying the offending key path and a remediation.
|
|
8
|
+
|
|
9
|
+
No KeyError, no TypeError, no stack trace reaches the user — every
|
|
10
|
+
rejection produces one of these with a human-readable message.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, key_path: str, message: str) -> None:
|
|
14
|
+
self.key_path = key_path
|
|
15
|
+
self.message = message
|
|
16
|
+
super().__init__(f"[{key_path}] {message}")
|
mojox_core/_types.py
ADDED
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
"""All frozen dataclasses for the mojox-core model layer.
|
|
2
|
+
|
|
3
|
+
Every type here is immutable (frozen=True). No type in this module imports
|
|
4
|
+
subprocess or performs I/O. Path-valued fields use PurePosixPath for lexical
|
|
5
|
+
paths (manifest-relative) and str for resolved absolute paths (from readers).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from collections.abc import Mapping
|
|
11
|
+
from dataclasses import dataclass, field
|
|
12
|
+
from enum import Enum
|
|
13
|
+
from pathlib import PurePosixPath
|
|
14
|
+
from typing import ClassVar, Literal
|
|
15
|
+
|
|
16
|
+
# -- Enums -----------------------------------------------------------------
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class CommandKind(Enum):
|
|
20
|
+
"""The kind of mojo invocation a Command represents."""
|
|
21
|
+
|
|
22
|
+
COMPILE_PACKAGE = "compile-package"
|
|
23
|
+
COMPILE_BINARY = "compile-binary"
|
|
24
|
+
RUN_TEST = "run-test"
|
|
25
|
+
CHECK_EXAMPLE = "check-example"
|
|
26
|
+
RUN = "run"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class TargetKind(Enum):
|
|
30
|
+
"""Discovered target kinds."""
|
|
31
|
+
|
|
32
|
+
LIB = "lib"
|
|
33
|
+
BIN = "bin"
|
|
34
|
+
TEST = "test"
|
|
35
|
+
EXAMPLE = "example"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class DistKind(Enum):
|
|
39
|
+
"""How a distribution provides its Mojo packages."""
|
|
40
|
+
|
|
41
|
+
SOURCE = "source"
|
|
42
|
+
PRECOMPILED = "precompiled"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# -- Leaf types ------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@dataclass(frozen=True)
|
|
49
|
+
class BinaryEntry:
|
|
50
|
+
"""One executable produced by ``mojo build``."""
|
|
51
|
+
|
|
52
|
+
source: str
|
|
53
|
+
name: str
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class LintConfig:
|
|
58
|
+
"""Lint settings that translate to compiler flags."""
|
|
59
|
+
|
|
60
|
+
warnings_as_errors: bool = False
|
|
61
|
+
check_doc_strings: bool = False
|
|
62
|
+
missing_doc_strings: bool = False
|
|
63
|
+
unstable_apis: bool = False
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@dataclass(frozen=True)
|
|
67
|
+
class Diagnostic:
|
|
68
|
+
"""A compiler or mojox diagnostic."""
|
|
69
|
+
|
|
70
|
+
kind: Literal["error", "warning", "note"]
|
|
71
|
+
message: str
|
|
72
|
+
file: str | None = None
|
|
73
|
+
line: int | None = None
|
|
74
|
+
column: int | None = None
|
|
75
|
+
source_text: str | None = None
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
@dataclass(frozen=True)
|
|
79
|
+
class Target:
|
|
80
|
+
"""A discovered build target."""
|
|
81
|
+
|
|
82
|
+
kind: TargetKind
|
|
83
|
+
path: str
|
|
84
|
+
target_id: str
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
@dataclass(frozen=True)
|
|
88
|
+
class DistEntry:
|
|
89
|
+
"""One installed distribution contributing to the include sequence."""
|
|
90
|
+
|
|
91
|
+
name: str
|
|
92
|
+
include_dir: str
|
|
93
|
+
kind: DistKind
|
|
94
|
+
packages: tuple[str, ...]
|
|
95
|
+
provenance: str
|
|
96
|
+
native_lib_dirs: tuple[str, ...] = ()
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# -- Compound types --------------------------------------------------------
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@dataclass(frozen=True)
|
|
103
|
+
class Profile:
|
|
104
|
+
"""A named compilation profile (dev, release, or user-declared)."""
|
|
105
|
+
|
|
106
|
+
optimize: int | None = None
|
|
107
|
+
debug_level: str | None = None
|
|
108
|
+
defines: dict[str, str] = field(default_factory=dict)
|
|
109
|
+
flags: tuple[str, ...] = ()
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@dataclass(frozen=True)
|
|
113
|
+
class Manifest:
|
|
114
|
+
"""Parsed pyproject.toml -- portable, published, versioned with the code."""
|
|
115
|
+
|
|
116
|
+
# [project]
|
|
117
|
+
name: str
|
|
118
|
+
version: str
|
|
119
|
+
description: str | None
|
|
120
|
+
readme: str | None
|
|
121
|
+
license_expr: str | None
|
|
122
|
+
license_files: tuple[str, ...]
|
|
123
|
+
requires_python: str | None
|
|
124
|
+
dependencies: tuple[str, ...]
|
|
125
|
+
optional_dependencies: dict[str, tuple[str, ...]]
|
|
126
|
+
keywords: tuple[str, ...]
|
|
127
|
+
authors: tuple[dict[str, str], ...]
|
|
128
|
+
maintainers: tuple[dict[str, str], ...]
|
|
129
|
+
urls: dict[str, str]
|
|
130
|
+
classifiers: tuple[str, ...]
|
|
131
|
+
|
|
132
|
+
# [tool.mojox]
|
|
133
|
+
packages: tuple[str, ...] | None
|
|
134
|
+
package_root: str
|
|
135
|
+
binaries: tuple[BinaryEntry, ...]
|
|
136
|
+
test_roots: tuple[str, ...]
|
|
137
|
+
test_parallel: bool
|
|
138
|
+
defines: dict[str, str]
|
|
139
|
+
flags: tuple[str, ...]
|
|
140
|
+
lints: LintConfig
|
|
141
|
+
optimize: int | None
|
|
142
|
+
debug_level: str | None
|
|
143
|
+
pre_build: tuple[tuple[str, ...], ...]
|
|
144
|
+
native_libs: tuple[str, ...]
|
|
145
|
+
source_include: tuple[str, ...] | None
|
|
146
|
+
source_exclude: tuple[str, ...]
|
|
147
|
+
wheel_exclude: tuple[str, ...]
|
|
148
|
+
profiles: dict[str, Profile]
|
|
149
|
+
build_profile: str
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
@dataclass(frozen=True)
|
|
153
|
+
class TargetGraph:
|
|
154
|
+
"""Discovered targets in topological order (lib targets first)."""
|
|
155
|
+
|
|
156
|
+
targets: tuple[Target, ...]
|
|
157
|
+
edges: tuple[tuple[str, str], ...]
|
|
158
|
+
unsearched_test_dirs: tuple[tuple[str, int], ...] = ()
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@dataclass(frozen=True)
|
|
162
|
+
class ResolvedEnv:
|
|
163
|
+
"""The resolved build environment -- include sequence, mojo binary, provenance."""
|
|
164
|
+
|
|
165
|
+
include_sequence: tuple[DistEntry, ...]
|
|
166
|
+
mojo_path: str
|
|
167
|
+
mojo_version: str
|
|
168
|
+
path_mojo: str | None
|
|
169
|
+
lock_version: int | None
|
|
170
|
+
diagnostics: tuple[Diagnostic, ...] = ()
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
@dataclass(frozen=True)
|
|
174
|
+
class Toolchain:
|
|
175
|
+
"""Resolved Mojo toolchain identity."""
|
|
176
|
+
|
|
177
|
+
mojo_path: str
|
|
178
|
+
version: str
|
|
179
|
+
subcommand: str
|
|
180
|
+
extension: str
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
@dataclass(frozen=True)
|
|
184
|
+
class HostFacts:
|
|
185
|
+
"""Machine-specific facts injected into the planner for determinism."""
|
|
186
|
+
|
|
187
|
+
cpu_count: int
|
|
188
|
+
available_memory_mb: int
|
|
189
|
+
manifest_dir: PurePosixPath
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
@dataclass(frozen=True)
|
|
193
|
+
class LocalSettings:
|
|
194
|
+
"""Machine-local settings (from .mojox/config.toml + env vars)."""
|
|
195
|
+
|
|
196
|
+
jobs: int | None = None
|
|
197
|
+
timeout_s: int | None = None
|
|
198
|
+
config_paths: tuple[str, ...] = ()
|
|
199
|
+
env: dict[str, str] = field(default_factory=dict)
|
|
200
|
+
|
|
201
|
+
EMPTY: ClassVar[LocalSettings] # set below
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
# Bootstrap the EMPTY sentinel after the class is defined.
|
|
205
|
+
LocalSettings.EMPTY = LocalSettings()
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@dataclass(frozen=True)
|
|
209
|
+
class Policy:
|
|
210
|
+
"""The fully resolved flag set the planner consumes."""
|
|
211
|
+
|
|
212
|
+
optimize: int
|
|
213
|
+
debug_level: str
|
|
214
|
+
defines: dict[str, str]
|
|
215
|
+
flags: tuple[str, ...]
|
|
216
|
+
include_paths: tuple[str, ...]
|
|
217
|
+
lints: LintConfig
|
|
218
|
+
jobs: int
|
|
219
|
+
jobs_compile: int
|
|
220
|
+
jobs_tests: int
|
|
221
|
+
timeout_s: int
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
@dataclass(frozen=True)
|
|
225
|
+
class Command:
|
|
226
|
+
"""One mojo invocation the planner produces."""
|
|
227
|
+
|
|
228
|
+
argv: tuple[str, ...]
|
|
229
|
+
cwd: PurePosixPath
|
|
230
|
+
env: Mapping[str, str]
|
|
231
|
+
kind: CommandKind
|
|
232
|
+
target_id: str
|
|
233
|
+
timeout_s: int | None
|
|
234
|
+
outputs: tuple[str, ...]
|
|
235
|
+
depends_on: tuple[str, ...]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""Build the resolved environment from distribution data.
|
|
2
|
+
|
|
3
|
+
Pure transformer -- receives pre-read distribution info and returns ResolvedEnv.
|
|
4
|
+
The IO reader that enumerates distributions lives in mojox_core.io.environment.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from ._errors import ConfigError
|
|
12
|
+
from ._types import Diagnostic, DistEntry, DistKind, ResolvedEnv
|
|
13
|
+
|
|
14
|
+
_MAX_KNOWN_LOCK_VERSION = 1
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def build_env(
|
|
18
|
+
dists: list[dict[str, Any]],
|
|
19
|
+
lock_data: dict[str, Any] | None,
|
|
20
|
+
mojo_path: str,
|
|
21
|
+
mojo_version: str,
|
|
22
|
+
*,
|
|
23
|
+
path_mojo: str | None = None,
|
|
24
|
+
) -> ResolvedEnv:
|
|
25
|
+
"""Build a ResolvedEnv from distribution data and optional lockfile.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
dists: List of distribution dicts, each containing 'name',
|
|
29
|
+
'include_dir', 'kind', 'packages', 'provenance', and
|
|
30
|
+
'native_lib_dirs' keys.
|
|
31
|
+
lock_data: Parsed lockfile data (with 'version' key), or None.
|
|
32
|
+
mojo_path: Path to the Mojo compiler in the virtual environment.
|
|
33
|
+
mojo_version: Version string of the Mojo compiler.
|
|
34
|
+
path_mojo: Path to Mojo found on PATH, if different from mojo_path.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
A fully resolved ResolvedEnv instance.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
ConfigError: If a package appears as both source and precompiled
|
|
41
|
+
in the same include directory.
|
|
42
|
+
"""
|
|
43
|
+
diagnostics: list[Diagnostic] = []
|
|
44
|
+
entries: list[DistEntry] = []
|
|
45
|
+
|
|
46
|
+
_check_source_shadows_precompiled(dists)
|
|
47
|
+
|
|
48
|
+
for d in dists:
|
|
49
|
+
entries.append(
|
|
50
|
+
DistEntry(
|
|
51
|
+
name=d["name"],
|
|
52
|
+
include_dir=d["include_dir"],
|
|
53
|
+
kind=d["kind"],
|
|
54
|
+
packages=tuple(d["packages"]),
|
|
55
|
+
provenance=d.get("provenance", "unknown"),
|
|
56
|
+
native_lib_dirs=tuple(d.get("native_lib_dirs", ())),
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
lock_version = _extract_lock_version(lock_data, diagnostics)
|
|
61
|
+
|
|
62
|
+
if path_mojo is not None and path_mojo != mojo_path:
|
|
63
|
+
diagnostics.append(
|
|
64
|
+
Diagnostic(
|
|
65
|
+
kind="warning",
|
|
66
|
+
message=f"PATH mojo ({path_mojo}) differs from environment mojo "
|
|
67
|
+
f"({mojo_path}). The environment mojo will be used.",
|
|
68
|
+
)
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
return ResolvedEnv(
|
|
72
|
+
include_sequence=tuple(entries),
|
|
73
|
+
mojo_path=mojo_path,
|
|
74
|
+
mojo_version=mojo_version,
|
|
75
|
+
path_mojo=path_mojo,
|
|
76
|
+
lock_version=lock_version,
|
|
77
|
+
diagnostics=tuple(diagnostics),
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _check_source_shadows_precompiled(dists: list[dict[str, Any]]) -> None:
|
|
82
|
+
"""Raise ConfigError if any package is both source and precompiled in the same dir.
|
|
83
|
+
|
|
84
|
+
Args:
|
|
85
|
+
dists: List of distribution dicts to validate.
|
|
86
|
+
|
|
87
|
+
Raises:
|
|
88
|
+
ConfigError: If a source-shadows-precompiled conflict is detected.
|
|
89
|
+
"""
|
|
90
|
+
dir_packages: dict[str, dict[str, list[DistKind]]] = {}
|
|
91
|
+
for d in dists:
|
|
92
|
+
include_dir = d["include_dir"]
|
|
93
|
+
by_name = dir_packages.setdefault(include_dir, {})
|
|
94
|
+
for pkg in d["packages"]:
|
|
95
|
+
by_name.setdefault(pkg, []).append(d["kind"])
|
|
96
|
+
|
|
97
|
+
for include_dir, pkgs_by_name in dir_packages.items():
|
|
98
|
+
for pkg_name, kinds in pkgs_by_name.items():
|
|
99
|
+
if DistKind.SOURCE in kinds and DistKind.PRECOMPILED in kinds:
|
|
100
|
+
raise ConfigError(
|
|
101
|
+
f"environment.{include_dir}",
|
|
102
|
+
f"source-shadows-precompiled: package {pkg_name!r} exists "
|
|
103
|
+
f"as both source and precompiled in {include_dir!r}. The "
|
|
104
|
+
"source silently wins and the consumer is on the slow "
|
|
105
|
+
"path. Remove one.",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _extract_lock_version(
|
|
110
|
+
lock_data: dict[str, Any] | None,
|
|
111
|
+
diagnostics: list[Diagnostic],
|
|
112
|
+
) -> int | None:
|
|
113
|
+
"""Extract and validate the lockfile schema version.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
lock_data: Parsed lockfile data, or None.
|
|
117
|
+
diagnostics: Mutable list to append warnings to.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
The lock version integer, or None if no lockfile was provided.
|
|
121
|
+
"""
|
|
122
|
+
if lock_data is None:
|
|
123
|
+
return None
|
|
124
|
+
lock_version = lock_data.get("version")
|
|
125
|
+
if lock_version is not None and lock_version > _MAX_KNOWN_LOCK_VERSION:
|
|
126
|
+
diagnostics.append(
|
|
127
|
+
Diagnostic(
|
|
128
|
+
kind="warning",
|
|
129
|
+
message=f"uv.lock schema version {lock_version} is above the "
|
|
130
|
+
f"known maximum ({_MAX_KNOWN_LOCK_VERSION}). Provenance data "
|
|
131
|
+
"may be incomplete.",
|
|
132
|
+
)
|
|
133
|
+
)
|
|
134
|
+
return lock_version
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Effectful IO readers — separated from pure transformers."""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Read the installed environment — the effectful half of environment building.
|
|
2
|
+
|
|
3
|
+
Enumerates installed distributions via importlib.metadata, checks which
|
|
4
|
+
ship mojo_packages/, reads uv.lock when present, and gathers HostFacts.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.metadata
|
|
10
|
+
import os
|
|
11
|
+
import sysconfig
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
from .._types import DistKind, HostFacts
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def read_distributions(platlib: str | None = None) -> list[dict[str, Any]]:
|
|
19
|
+
"""Enumerate installed distributions that ship Mojo packages.
|
|
20
|
+
|
|
21
|
+
Returns a list of dicts suitable for passing to environment.build_env().
|
|
22
|
+
"""
|
|
23
|
+
if platlib is None:
|
|
24
|
+
platlib = sysconfig.get_path("platlib")
|
|
25
|
+
assert platlib is not None
|
|
26
|
+
|
|
27
|
+
mojo_packages_dir = Path(platlib) / "mojo_packages"
|
|
28
|
+
if not mojo_packages_dir.is_dir():
|
|
29
|
+
return []
|
|
30
|
+
|
|
31
|
+
dists: list[dict[str, Any]] = []
|
|
32
|
+
for dist in importlib.metadata.distributions():
|
|
33
|
+
if dist.files is None:
|
|
34
|
+
continue
|
|
35
|
+
has_mojo = False
|
|
36
|
+
packages: list[str] = []
|
|
37
|
+
|
|
38
|
+
for f in dist.files:
|
|
39
|
+
parts = str(f).split("/")
|
|
40
|
+
if len(parts) >= 2 and parts[0] == "mojo_packages":
|
|
41
|
+
has_mojo = True
|
|
42
|
+
pkg_name = parts[1]
|
|
43
|
+
if pkg_name.endswith((".mojoc", ".mojopkg")):
|
|
44
|
+
pkg_name = pkg_name.rsplit(".", 1)[0]
|
|
45
|
+
kind = DistKind.PRECOMPILED
|
|
46
|
+
elif not pkg_name.startswith("_") and pkg_name != "lib":
|
|
47
|
+
kind = DistKind.SOURCE
|
|
48
|
+
else:
|
|
49
|
+
continue
|
|
50
|
+
if pkg_name not in packages:
|
|
51
|
+
packages.append(pkg_name)
|
|
52
|
+
|
|
53
|
+
if has_mojo and packages:
|
|
54
|
+
dists.append(
|
|
55
|
+
{
|
|
56
|
+
"name": dist.metadata["Name"],
|
|
57
|
+
"include_dir": str(mojo_packages_dir),
|
|
58
|
+
"kind": kind,
|
|
59
|
+
"packages": packages,
|
|
60
|
+
"provenance": dist.metadata.get("Version", "unknown"),
|
|
61
|
+
"native_lib_dirs": (),
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return dists
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def read_lockfile(project_root: Path) -> dict[str, Any] | None:
|
|
69
|
+
"""Read uv.lock if it exists, returning parsed TOML or None."""
|
|
70
|
+
import sys
|
|
71
|
+
|
|
72
|
+
if sys.version_info >= (3, 11):
|
|
73
|
+
import tomllib
|
|
74
|
+
else:
|
|
75
|
+
import tomli as tomllib # type: ignore[import-not-found, no-redef]
|
|
76
|
+
|
|
77
|
+
lock_path = project_root / "uv.lock"
|
|
78
|
+
if not lock_path.is_file():
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
with open(lock_path, "rb") as f:
|
|
83
|
+
return tomllib.load(f)
|
|
84
|
+
except Exception:
|
|
85
|
+
return None
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def read_host_facts(manifest_dir: Path) -> HostFacts:
|
|
89
|
+
"""Gather machine-specific facts for the planner."""
|
|
90
|
+
from pathlib import PurePosixPath
|
|
91
|
+
|
|
92
|
+
cpu_count = os.cpu_count() or 1
|
|
93
|
+
mem_mb = _available_memory_mb()
|
|
94
|
+
|
|
95
|
+
return HostFacts(
|
|
96
|
+
cpu_count=cpu_count,
|
|
97
|
+
available_memory_mb=mem_mb,
|
|
98
|
+
manifest_dir=PurePosixPath(str(manifest_dir.resolve())),
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _available_memory_mb() -> int:
|
|
103
|
+
"""Best-effort available memory in MB."""
|
|
104
|
+
try:
|
|
105
|
+
import resource # noqa: F401
|
|
106
|
+
|
|
107
|
+
# Try /proc/meminfo on Linux
|
|
108
|
+
with open("/proc/meminfo") as f:
|
|
109
|
+
for line in f:
|
|
110
|
+
if line.startswith("MemAvailable:"):
|
|
111
|
+
return int(line.split()[1]) // 1024
|
|
112
|
+
except (ImportError, OSError):
|
|
113
|
+
pass
|
|
114
|
+
return 16384
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Read pyproject.toml from disk — the effectful half of manifest parsing."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
from .._errors import ConfigError
|
|
10
|
+
|
|
11
|
+
if sys.version_info >= (3, 11):
|
|
12
|
+
import tomllib
|
|
13
|
+
else:
|
|
14
|
+
import tomli as tomllib # type: ignore[import-not-found, no-redef]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def read(path: Path) -> dict[str, Any]:
|
|
18
|
+
"""Read and parse a pyproject.toml file.
|
|
19
|
+
|
|
20
|
+
Returns the parsed dict. Raises ConfigError if the file is missing or
|
|
21
|
+
contains invalid TOML.
|
|
22
|
+
"""
|
|
23
|
+
if not path.is_file():
|
|
24
|
+
raise ConfigError("pyproject.toml", f"{path} not found")
|
|
25
|
+
try:
|
|
26
|
+
with open(path, "rb") as f:
|
|
27
|
+
return tomllib.load(f)
|
|
28
|
+
except Exception as e:
|
|
29
|
+
raise ConfigError("pyproject.toml", f"failed to parse: {e}") from e
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Resolve the Mojo toolchain from installed distributions.
|
|
2
|
+
|
|
3
|
+
Uses importlib.metadata to find the mojo-compiler distribution, its
|
|
4
|
+
console script entry point, and version. Never invokes mojo --version.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import importlib.metadata
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .._errors import ConfigError
|
|
13
|
+
from .._types import Toolchain
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def resolve() -> Toolchain:
|
|
17
|
+
"""Resolve the installed Mojo toolchain.
|
|
18
|
+
|
|
19
|
+
Reads the mojo-compiler distribution metadata to find the binary path
|
|
20
|
+
and version. Never runs a subprocess.
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
A ``Toolchain`` with the mojo binary path, version string,
|
|
24
|
+
subcommand (``"precompile"``), and extension (``".mojoc"``).
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ConfigError: If ``mojo-compiler`` is not installed or its ``mojo``
|
|
28
|
+
binary cannot be located in the distribution's file list.
|
|
29
|
+
"""
|
|
30
|
+
try:
|
|
31
|
+
dist = importlib.metadata.distribution("mojo-compiler")
|
|
32
|
+
except importlib.metadata.PackageNotFoundError:
|
|
33
|
+
raise ConfigError(
|
|
34
|
+
"toolchain",
|
|
35
|
+
"mojo-compiler is not installed. Install it with: uv add mojo-compiler",
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
version = dist.metadata["Version"]
|
|
39
|
+
|
|
40
|
+
# Find the mojo console script
|
|
41
|
+
mojo_path = _find_console_script(dist)
|
|
42
|
+
|
|
43
|
+
# Determine subcommand and extension based on version
|
|
44
|
+
subcommand = "precompile"
|
|
45
|
+
extension = ".mojoc"
|
|
46
|
+
|
|
47
|
+
return Toolchain(
|
|
48
|
+
mojo_path=mojo_path,
|
|
49
|
+
version=version,
|
|
50
|
+
subcommand=subcommand,
|
|
51
|
+
extension=extension,
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _find_console_script(dist: importlib.metadata.Distribution) -> str:
|
|
56
|
+
"""Find the mojo console script path from the distribution.
|
|
57
|
+
|
|
58
|
+
Searches the distribution's recorded file list for a file named ``mojo``
|
|
59
|
+
(either at the top of a scripts directory or under ``bin/``). Falls back
|
|
60
|
+
to inspecting entry_points if the file list doesn't yield a match.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
dist: The ``mojo-compiler`` distribution object.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The absolute path to the ``mojo`` binary as a string.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
ConfigError: If the binary cannot be located.
|
|
70
|
+
"""
|
|
71
|
+
# Look in the scripts directory
|
|
72
|
+
if dist.files:
|
|
73
|
+
for f in dist.files:
|
|
74
|
+
parts = str(f).split("/")
|
|
75
|
+
if len(parts) >= 1 and parts[-1] == "mojo":
|
|
76
|
+
located = dist.locate_file(f)
|
|
77
|
+
if located and Path(str(located)).exists():
|
|
78
|
+
return str(Path(str(located)).resolve())
|
|
79
|
+
|
|
80
|
+
# Fallback: try the entry_points
|
|
81
|
+
eps = dist.entry_points
|
|
82
|
+
for ep in eps:
|
|
83
|
+
if ep.name == "mojo":
|
|
84
|
+
# entry_points don't directly give us a path; use the scripts dir
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# Last resort: look in the same directory as the package
|
|
88
|
+
pkg_files = dist.files or []
|
|
89
|
+
for f in pkg_files:
|
|
90
|
+
if str(f).endswith("/bin/mojo") or str(f) == "bin/mojo":
|
|
91
|
+
located = dist.locate_file(f)
|
|
92
|
+
if located:
|
|
93
|
+
return str(Path(str(located)).resolve())
|
|
94
|
+
|
|
95
|
+
raise ConfigError(
|
|
96
|
+
"toolchain",
|
|
97
|
+
"mojo-compiler is installed but the `mojo` binary was not found in its file list",
|
|
98
|
+
)
|