lean-runtime 0.6.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.
- lean_runtime/__init__.py +67 -0
- lean_runtime/__main__.py +4 -0
- lean_runtime/backends.py +373 -0
- lean_runtime/cli.py +254 -0
- lean_runtime/diagnostics.py +52 -0
- lean_runtime/environments.py +1170 -0
- lean_runtime/errors.py +63 -0
- lean_runtime/events.py +35 -0
- lean_runtime/health.py +86 -0
- lean_runtime/lake.py +43 -0
- lean_runtime/lockfiles.py +192 -0
- lean_runtime/locking.py +82 -0
- lean_runtime/models.py +78 -0
- lean_runtime/policies.py +36 -0
- lean_runtime/py.typed +1 -0
- lean_runtime/references.py +246 -0
- lean_runtime/resolver.py +282 -0
- lean_runtime/runtime.py +430 -0
- lean_runtime/serialization.py +51 -0
- lean_runtime/specs.py +210 -0
- lean_runtime/store.py +462 -0
- lean_runtime/toolchains.py +164 -0
- lean_runtime-0.6.0.dist-info/METADATA +332 -0
- lean_runtime-0.6.0.dist-info/RECORD +28 -0
- lean_runtime-0.6.0.dist-info/WHEEL +5 -0
- lean_runtime-0.6.0.dist-info/entry_points.txt +2 -0
- lean_runtime-0.6.0.dist-info/licenses/LICENSE +202 -0
- lean_runtime-0.6.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
"""Ergonomic package references compiled into exact Git package specifications."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from collections.abc import Sequence
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Literal
|
|
13
|
+
|
|
14
|
+
if sys.version_info >= (3, 11):
|
|
15
|
+
import tomllib
|
|
16
|
+
else: # pragma: no cover - exercised by the Python 3.10 CI job
|
|
17
|
+
import tomli as tomllib
|
|
18
|
+
|
|
19
|
+
from .errors import ResolutionError, SpecificationError
|
|
20
|
+
from .specs import GitPackage
|
|
21
|
+
from .toolchains import ToolchainManager, normalize_toolchain
|
|
22
|
+
|
|
23
|
+
_GITHUB = re.compile(
|
|
24
|
+
r"github:(?P<owner>[A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))/"
|
|
25
|
+
r"(?P<repo>[A-Za-z0-9_.-]+)@(?P<revision>[A-Za-z0-9][A-Za-z0-9._/+\-]{0,199})"
|
|
26
|
+
)
|
|
27
|
+
_COMMIT = re.compile(r"[0-9a-fA-F]{40}")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True, slots=True)
|
|
31
|
+
class PackageReference:
|
|
32
|
+
"""A human-friendly reference to one Git-hosted Lean package."""
|
|
33
|
+
|
|
34
|
+
url: str
|
|
35
|
+
revision: str
|
|
36
|
+
display: str
|
|
37
|
+
revision_kind: Literal["commit", "tag"]
|
|
38
|
+
|
|
39
|
+
@classmethod
|
|
40
|
+
def parse(cls, value: str) -> PackageReference:
|
|
41
|
+
"""Parse ``github:owner/repository@tag-or-commit``."""
|
|
42
|
+
match = _GITHUB.fullmatch(value)
|
|
43
|
+
if match is None:
|
|
44
|
+
raise SpecificationError(
|
|
45
|
+
"invalid package reference; expected github:owner/repository@tag-or-commit"
|
|
46
|
+
)
|
|
47
|
+
owner = match.group("owner")
|
|
48
|
+
repository = match.group("repo")
|
|
49
|
+
if repository.endswith(".git"):
|
|
50
|
+
repository = repository[:-4]
|
|
51
|
+
if not repository:
|
|
52
|
+
raise SpecificationError("GitHub package reference has an empty repository name")
|
|
53
|
+
revision = match.group("revision")
|
|
54
|
+
kind: Literal["commit", "tag"] = (
|
|
55
|
+
"commit" if _COMMIT.fullmatch(revision) is not None else "tag"
|
|
56
|
+
)
|
|
57
|
+
return cls(
|
|
58
|
+
url=f"https://github.com/{owner}/{repository}.git",
|
|
59
|
+
revision=revision,
|
|
60
|
+
display=f"github:{owner}/{repository}@{revision}",
|
|
61
|
+
revision_kind=kind,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
@classmethod
|
|
65
|
+
def git(cls, url: str, revision: str) -> PackageReference:
|
|
66
|
+
"""Construct a reference for a generic Git URL, primarily for Python callers."""
|
|
67
|
+
if not url or "\n" in url or "\r" in url:
|
|
68
|
+
raise SpecificationError("invalid Git package-reference URL")
|
|
69
|
+
kind: Literal["commit", "tag"] = (
|
|
70
|
+
"commit" if _COMMIT.fullmatch(revision) is not None else "tag"
|
|
71
|
+
)
|
|
72
|
+
# Reuse GitPackage's strict revision validation with a temporary valid name.
|
|
73
|
+
if kind == "commit":
|
|
74
|
+
GitPackage.git("package_reference", url, revision)
|
|
75
|
+
else:
|
|
76
|
+
GitPackage.tag("package_reference", url, revision)
|
|
77
|
+
return cls(url=url, revision=revision, display=f"git:{url}@{revision}", revision_kind=kind)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass(frozen=True, slots=True)
|
|
81
|
+
class DiscoveredPackage:
|
|
82
|
+
"""Exact package metadata discovered from a referenced Lake project."""
|
|
83
|
+
|
|
84
|
+
reference: PackageReference
|
|
85
|
+
toolchain: str
|
|
86
|
+
package: GitPackage
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _run_git(arguments: list[str], *, cwd: Path | None = None, timeout: float = 120) -> str:
|
|
90
|
+
try:
|
|
91
|
+
process = subprocess.run(
|
|
92
|
+
["git", *arguments],
|
|
93
|
+
cwd=cwd,
|
|
94
|
+
text=True,
|
|
95
|
+
capture_output=True,
|
|
96
|
+
timeout=timeout,
|
|
97
|
+
check=False,
|
|
98
|
+
)
|
|
99
|
+
except subprocess.TimeoutExpired as exc:
|
|
100
|
+
raise ResolutionError(
|
|
101
|
+
"package-reference Git operation timed out",
|
|
102
|
+
phase="package-discovery",
|
|
103
|
+
command=("git", *arguments),
|
|
104
|
+
exit_code=124,
|
|
105
|
+
output=str(exc.stdout or "") + str(exc.stderr or ""),
|
|
106
|
+
) from exc
|
|
107
|
+
if process.returncode:
|
|
108
|
+
raise ResolutionError(
|
|
109
|
+
"could not acquire package-reference metadata",
|
|
110
|
+
phase="package-discovery",
|
|
111
|
+
command=("git", *arguments),
|
|
112
|
+
exit_code=process.returncode,
|
|
113
|
+
output=process.stdout + process.stderr,
|
|
114
|
+
)
|
|
115
|
+
return process.stdout.strip()
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _read_lake_metadata(lakefile: Path) -> tuple[str, str]:
|
|
119
|
+
"""Read the declarative Lake metadata needed by environment resolution."""
|
|
120
|
+
try:
|
|
121
|
+
value = tomllib.loads(lakefile.read_text(encoding="utf-8"))
|
|
122
|
+
except (OSError, tomllib.TOMLDecodeError) as exc:
|
|
123
|
+
raise SpecificationError(f"could not read package metadata {lakefile.name}: {exc}") from exc
|
|
124
|
+
if not isinstance(value, dict):
|
|
125
|
+
raise SpecificationError(f"package metadata {lakefile.name} is not a TOML object")
|
|
126
|
+
name = value.get("name")
|
|
127
|
+
if not isinstance(name, str) or not name:
|
|
128
|
+
raise SpecificationError(f"package metadata {lakefile.name} has no package name")
|
|
129
|
+
libraries = value.get("lean_lib")
|
|
130
|
+
if not isinstance(libraries, list) or not libraries:
|
|
131
|
+
raise SpecificationError(f"package metadata {lakefile.name} declares no [[lean_lib]]")
|
|
132
|
+
first = libraries[0]
|
|
133
|
+
if not isinstance(first, dict):
|
|
134
|
+
raise SpecificationError("package's first [[lean_lib]] is malformed")
|
|
135
|
+
roots = first.get("roots")
|
|
136
|
+
root_module: Any = roots[0] if isinstance(roots, list) and roots else first.get("name")
|
|
137
|
+
if not isinstance(root_module, str) or not root_module:
|
|
138
|
+
raise SpecificationError("package's first [[lean_lib]] has no importable root")
|
|
139
|
+
return name, root_module
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _lake_metadata(
|
|
143
|
+
checkout: Path,
|
|
144
|
+
*,
|
|
145
|
+
toolchain: str,
|
|
146
|
+
toolchains: ToolchainManager,
|
|
147
|
+
) -> tuple[str, str]:
|
|
148
|
+
lakefile = checkout / "lakefile.toml"
|
|
149
|
+
if lakefile.is_file():
|
|
150
|
+
return _read_lake_metadata(lakefile)
|
|
151
|
+
|
|
152
|
+
lakefile_lean = checkout / "lakefile.lean"
|
|
153
|
+
if not lakefile_lean.is_file():
|
|
154
|
+
raise SpecificationError(
|
|
155
|
+
"referenced package has neither a root lakefile.toml nor lakefile.lean"
|
|
156
|
+
)
|
|
157
|
+
translated = checkout / ".lean-runtime-lakefile.toml"
|
|
158
|
+
try:
|
|
159
|
+
command = toolchains.command(
|
|
160
|
+
toolchain,
|
|
161
|
+
"lake",
|
|
162
|
+
"translate-config",
|
|
163
|
+
"toml",
|
|
164
|
+
str(translated),
|
|
165
|
+
)
|
|
166
|
+
process = subprocess.run(
|
|
167
|
+
command,
|
|
168
|
+
cwd=checkout,
|
|
169
|
+
env=toolchains.environment,
|
|
170
|
+
text=True,
|
|
171
|
+
stdout=subprocess.PIPE,
|
|
172
|
+
stderr=subprocess.STDOUT,
|
|
173
|
+
timeout=120,
|
|
174
|
+
check=False,
|
|
175
|
+
)
|
|
176
|
+
except subprocess.TimeoutExpired as exc:
|
|
177
|
+
raise ResolutionError(
|
|
178
|
+
"Lake package-metadata translation timed out",
|
|
179
|
+
phase="package-discovery",
|
|
180
|
+
command=tuple(command),
|
|
181
|
+
exit_code=124,
|
|
182
|
+
output=str(exc.stdout or "") + str(exc.stderr or ""),
|
|
183
|
+
) from exc
|
|
184
|
+
if process.returncode:
|
|
185
|
+
raise ResolutionError(
|
|
186
|
+
"Lake could not translate package metadata",
|
|
187
|
+
phase="package-discovery",
|
|
188
|
+
command=tuple(command),
|
|
189
|
+
exit_code=process.returncode,
|
|
190
|
+
output=process.stdout,
|
|
191
|
+
)
|
|
192
|
+
if not translated.is_file():
|
|
193
|
+
raise ResolutionError(
|
|
194
|
+
"Lake completed without producing translated package metadata",
|
|
195
|
+
phase="package-discovery",
|
|
196
|
+
command=tuple(command),
|
|
197
|
+
output=process.stdout,
|
|
198
|
+
)
|
|
199
|
+
return _read_lake_metadata(translated)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def discover_package(
|
|
203
|
+
reference: PackageReference,
|
|
204
|
+
*,
|
|
205
|
+
directory: Path,
|
|
206
|
+
toolchains: ToolchainManager | None = None,
|
|
207
|
+
) -> DiscoveredPackage:
|
|
208
|
+
"""Acquire a shallow checkout and compile its metadata into an exact package."""
|
|
209
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
210
|
+
with tempfile.TemporaryDirectory(prefix="package-", dir=directory) as raw:
|
|
211
|
+
checkout = Path(raw)
|
|
212
|
+
_run_git(["init", "--quiet"], cwd=checkout)
|
|
213
|
+
_run_git(["remote", "add", "origin", reference.url], cwd=checkout)
|
|
214
|
+
target = (
|
|
215
|
+
reference.revision
|
|
216
|
+
if reference.revision_kind == "commit"
|
|
217
|
+
else f"refs/tags/{reference.revision}"
|
|
218
|
+
)
|
|
219
|
+
_run_git(["fetch", "--quiet", "--depth", "1", "origin", target], cwd=checkout)
|
|
220
|
+
_run_git(["checkout", "--quiet", "--detach", "FETCH_HEAD"], cwd=checkout)
|
|
221
|
+
revision = _run_git(["rev-parse", "HEAD"], cwd=checkout).lower()
|
|
222
|
+
if _COMMIT.fullmatch(revision) is None:
|
|
223
|
+
raise ResolutionError(
|
|
224
|
+
"package reference did not resolve to a full Git commit",
|
|
225
|
+
phase="package-discovery",
|
|
226
|
+
)
|
|
227
|
+
toolchain_path = checkout / "lean-toolchain"
|
|
228
|
+
if not toolchain_path.is_file():
|
|
229
|
+
raise SpecificationError("referenced package has no lean-toolchain file")
|
|
230
|
+
toolchain = normalize_toolchain(toolchain_path.read_text(encoding="utf-8"))
|
|
231
|
+
manager = toolchains or ToolchainManager(directory.parent)
|
|
232
|
+
name, root_module = _lake_metadata(
|
|
233
|
+
checkout,
|
|
234
|
+
toolchain=toolchain,
|
|
235
|
+
toolchains=manager,
|
|
236
|
+
)
|
|
237
|
+
package = GitPackage.git(name, reference.url, revision, root_module=root_module)
|
|
238
|
+
return DiscoveredPackage(reference=reference, toolchain=toolchain, package=package)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def normalize_references(
|
|
242
|
+
values: Sequence[str | PackageReference],
|
|
243
|
+
) -> tuple[PackageReference, ...]:
|
|
244
|
+
return tuple(
|
|
245
|
+
PackageReference.parse(value) if isinstance(value, str) else value for value in values
|
|
246
|
+
)
|
lean_runtime/resolver.py
ADDED
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
"""Compile exact-Git specifications into portable Lake-backed locks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import re
|
|
7
|
+
import subprocess
|
|
8
|
+
import tempfile
|
|
9
|
+
from dataclasses import replace
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from .errors import ResolutionError
|
|
14
|
+
from .events import EventEmitter
|
|
15
|
+
from .lake import ROOT_MODULE, generate_lakefile, generate_root_module
|
|
16
|
+
from .lockfiles import EnvironmentLock, LockedPackage
|
|
17
|
+
from .serialization import sha256_id
|
|
18
|
+
from .specs import EnvironmentSpec, GitPackage
|
|
19
|
+
from .store import EnvironmentStore
|
|
20
|
+
from .toolchains import ToolchainManager, normalize_toolchain
|
|
21
|
+
|
|
22
|
+
_COMMIT = re.compile(r"[0-9a-fA-F]{40}")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _git(path: Path, *arguments: str) -> str:
|
|
26
|
+
process = subprocess.run(
|
|
27
|
+
["git", "-C", str(path), *arguments],
|
|
28
|
+
text=True,
|
|
29
|
+
capture_output=True,
|
|
30
|
+
check=False,
|
|
31
|
+
)
|
|
32
|
+
if process.returncode:
|
|
33
|
+
raise ResolutionError(
|
|
34
|
+
f"could not inspect resolved Git package at {path}",
|
|
35
|
+
phase="lock-validation",
|
|
36
|
+
command=("git", "-C", str(path), *arguments),
|
|
37
|
+
exit_code=process.returncode,
|
|
38
|
+
output=process.stdout + process.stderr,
|
|
39
|
+
)
|
|
40
|
+
return process.stdout.strip()
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class EnvironmentResolver:
|
|
44
|
+
def __init__(
|
|
45
|
+
self,
|
|
46
|
+
toolchains: ToolchainManager,
|
|
47
|
+
store: EnvironmentStore,
|
|
48
|
+
events: EventEmitter | None = None,
|
|
49
|
+
) -> None:
|
|
50
|
+
self.toolchains = toolchains
|
|
51
|
+
self.store = store
|
|
52
|
+
self.events = events or EventEmitter()
|
|
53
|
+
|
|
54
|
+
def resolve(self, spec: EnvironmentSpec, *, timeout: float = 900) -> EnvironmentLock:
|
|
55
|
+
self.events.emit(
|
|
56
|
+
"resolution.started",
|
|
57
|
+
"Resolving environment",
|
|
58
|
+
toolchain=spec.toolchain,
|
|
59
|
+
packages=len(spec.packages),
|
|
60
|
+
)
|
|
61
|
+
toolchain = self.toolchains.ensure(spec.toolchain)
|
|
62
|
+
self.events.emit("toolchain.ready", "Lean toolchain is ready", toolchain=toolchain)
|
|
63
|
+
pinned_spec = self._pin_tags(spec)
|
|
64
|
+
root_lakefile = generate_lakefile(pinned_spec)
|
|
65
|
+
root_module = generate_root_module(spec)
|
|
66
|
+
resolution_root = self.store.home / "resolution"
|
|
67
|
+
resolution_root.mkdir(parents=True, exist_ok=True)
|
|
68
|
+
with tempfile.TemporaryDirectory(prefix="resolve-", dir=resolution_root) as raw:
|
|
69
|
+
workspace = Path(raw)
|
|
70
|
+
(workspace / "lean-toolchain").write_text(toolchain + "\n", encoding="utf-8")
|
|
71
|
+
(workspace / "lakefile.toml").write_text(root_lakefile, encoding="utf-8")
|
|
72
|
+
(workspace / f"{ROOT_MODULE}.lean").write_text(root_module, encoding="utf-8")
|
|
73
|
+
command = self.toolchains.command(toolchain, "lake", "update")
|
|
74
|
+
self.events.emit("resolution.lake_started", "Lake dependency resolution started")
|
|
75
|
+
try:
|
|
76
|
+
process = subprocess.run(
|
|
77
|
+
command,
|
|
78
|
+
cwd=workspace,
|
|
79
|
+
env=self.toolchains.environment,
|
|
80
|
+
text=True,
|
|
81
|
+
capture_output=True,
|
|
82
|
+
timeout=timeout,
|
|
83
|
+
check=False,
|
|
84
|
+
)
|
|
85
|
+
except subprocess.TimeoutExpired as exc:
|
|
86
|
+
raise ResolutionError(
|
|
87
|
+
f"Lake resolution exceeded {timeout:g} seconds",
|
|
88
|
+
command=tuple(command),
|
|
89
|
+
exit_code=124,
|
|
90
|
+
output=str(exc.stdout or "") + str(exc.stderr or ""),
|
|
91
|
+
) from exc
|
|
92
|
+
if process.returncode:
|
|
93
|
+
raise ResolutionError(
|
|
94
|
+
"Lake could not resolve the environment specification",
|
|
95
|
+
command=tuple(command),
|
|
96
|
+
exit_code=process.returncode,
|
|
97
|
+
output=process.stdout + process.stderr,
|
|
98
|
+
)
|
|
99
|
+
manifest_path = workspace / "lake-manifest.json"
|
|
100
|
+
if not manifest_path.is_file():
|
|
101
|
+
raise ResolutionError("Lake resolution did not produce lake-manifest.json")
|
|
102
|
+
manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
103
|
+
if not isinstance(manifest, dict):
|
|
104
|
+
raise ResolutionError("Lake manifest is not a JSON object")
|
|
105
|
+
packages = self._lock_packages(workspace, manifest, spec, toolchain)
|
|
106
|
+
lock = EnvironmentLock(
|
|
107
|
+
toolchain=toolchain,
|
|
108
|
+
spec_digest=spec.spec_digest,
|
|
109
|
+
root_lakefile=root_lakefile,
|
|
110
|
+
root_module=root_module,
|
|
111
|
+
manifest=manifest,
|
|
112
|
+
packages=packages,
|
|
113
|
+
)
|
|
114
|
+
self.store.publish_lock(lock)
|
|
115
|
+
self.events.emit(
|
|
116
|
+
"resolution.completed",
|
|
117
|
+
"Environment lock published",
|
|
118
|
+
lock_id=lock.lock_id,
|
|
119
|
+
packages=len(packages),
|
|
120
|
+
)
|
|
121
|
+
return lock
|
|
122
|
+
|
|
123
|
+
def _pin_tags(self, spec: EnvironmentSpec) -> EnvironmentSpec:
|
|
124
|
+
pinned: list[GitPackage] = []
|
|
125
|
+
for package in spec.packages:
|
|
126
|
+
if package.revision_kind == "commit":
|
|
127
|
+
pinned.append(package)
|
|
128
|
+
continue
|
|
129
|
+
reference = f"refs/tags/{package.rev}"
|
|
130
|
+
command = ["git", "ls-remote", package.url, reference, f"{reference}^{{}}"]
|
|
131
|
+
try:
|
|
132
|
+
process = subprocess.run(
|
|
133
|
+
command,
|
|
134
|
+
text=True,
|
|
135
|
+
capture_output=True,
|
|
136
|
+
timeout=60,
|
|
137
|
+
check=False,
|
|
138
|
+
)
|
|
139
|
+
except subprocess.TimeoutExpired as exc:
|
|
140
|
+
raise ResolutionError(
|
|
141
|
+
f"tag lookup timed out for package {package.name!r}",
|
|
142
|
+
phase="tag-resolution",
|
|
143
|
+
command=tuple(command),
|
|
144
|
+
exit_code=124,
|
|
145
|
+
output=str(exc.stdout or "") + str(exc.stderr or ""),
|
|
146
|
+
) from exc
|
|
147
|
+
if process.returncode:
|
|
148
|
+
raise ResolutionError(
|
|
149
|
+
f"could not resolve tag {package.rev!r} for package {package.name!r}",
|
|
150
|
+
phase="tag-resolution",
|
|
151
|
+
command=tuple(command),
|
|
152
|
+
exit_code=process.returncode,
|
|
153
|
+
output=process.stdout + process.stderr,
|
|
154
|
+
)
|
|
155
|
+
candidates: dict[str, str] = {}
|
|
156
|
+
for line in process.stdout.splitlines():
|
|
157
|
+
fields = line.split(maxsplit=1)
|
|
158
|
+
if len(fields) == 2:
|
|
159
|
+
candidates[fields[1]] = fields[0].lower()
|
|
160
|
+
revision = candidates.get(f"{reference}^{{}}", candidates.get(reference, ""))
|
|
161
|
+
if _COMMIT.fullmatch(revision) is None:
|
|
162
|
+
raise ResolutionError(
|
|
163
|
+
f"Git tag {package.rev!r} was not found for package {package.name!r}",
|
|
164
|
+
phase="tag-resolution",
|
|
165
|
+
command=tuple(command),
|
|
166
|
+
exit_code=1,
|
|
167
|
+
output=process.stdout + process.stderr,
|
|
168
|
+
)
|
|
169
|
+
pinned.append(replace(package, rev=revision, revision_kind="commit"))
|
|
170
|
+
self.events.emit(
|
|
171
|
+
"tag.resolved",
|
|
172
|
+
f"Resolved {package.rev} for {package.name}",
|
|
173
|
+
package=package.name,
|
|
174
|
+
tag=package.rev,
|
|
175
|
+
revision=revision,
|
|
176
|
+
)
|
|
177
|
+
return EnvironmentSpec(spec.toolchain, tuple(pinned))
|
|
178
|
+
|
|
179
|
+
def _lock_packages(
|
|
180
|
+
self,
|
|
181
|
+
workspace: Path,
|
|
182
|
+
manifest: dict[str, Any],
|
|
183
|
+
spec: EnvironmentSpec,
|
|
184
|
+
toolchain: str,
|
|
185
|
+
) -> tuple[LockedPackage, ...]:
|
|
186
|
+
direct: dict[str, GitPackage] = {package.name: package for package in spec.packages}
|
|
187
|
+
packages_dir = workspace / str(manifest.get("packagesDir", ".lake/packages"))
|
|
188
|
+
raw_packages = manifest.get("packages", [])
|
|
189
|
+
if not isinstance(raw_packages, list):
|
|
190
|
+
raise ResolutionError("Lake manifest packages field is not an array")
|
|
191
|
+
locked: list[LockedPackage] = []
|
|
192
|
+
for value in raw_packages:
|
|
193
|
+
if not isinstance(value, dict) or value.get("type") != "git":
|
|
194
|
+
raise ResolutionError(
|
|
195
|
+
"the initial lock schema supports only Git transitive dependencies",
|
|
196
|
+
phase="lock-validation",
|
|
197
|
+
)
|
|
198
|
+
name = str(value.get("name", ""))
|
|
199
|
+
revision = str(value.get("rev", "")).lower()
|
|
200
|
+
if _COMMIT.fullmatch(revision) is None:
|
|
201
|
+
raise ResolutionError(
|
|
202
|
+
f"Lake did not resolve package {name!r} to a full Git commit",
|
|
203
|
+
phase="lock-validation",
|
|
204
|
+
)
|
|
205
|
+
checkout = packages_dir / name
|
|
206
|
+
if not checkout.is_dir():
|
|
207
|
+
raise ResolutionError(
|
|
208
|
+
f"resolved package checkout is missing: {name}", phase="acquisition"
|
|
209
|
+
)
|
|
210
|
+
head = _git(checkout, "rev-parse", "HEAD").lower()
|
|
211
|
+
if head != revision:
|
|
212
|
+
raise ResolutionError(
|
|
213
|
+
f"package {name!r} checkout does not match its manifest revision",
|
|
214
|
+
phase="lock-validation",
|
|
215
|
+
)
|
|
216
|
+
tree = _git(checkout, "rev-parse", "HEAD^{tree}").lower()
|
|
217
|
+
package_root = checkout
|
|
218
|
+
subdir = value.get("subDir")
|
|
219
|
+
if isinstance(subdir, str) and subdir:
|
|
220
|
+
package_root /= subdir
|
|
221
|
+
requested = direct.get(name)
|
|
222
|
+
declared_toolchain = package_root / "lean-toolchain"
|
|
223
|
+
if declared_toolchain.is_file():
|
|
224
|
+
declared = declared_toolchain.read_text(encoding="utf-8").strip()
|
|
225
|
+
if declared and normalize_toolchain(declared) != toolchain:
|
|
226
|
+
self.events.emit(
|
|
227
|
+
"compatibility.toolchain_difference",
|
|
228
|
+
f"{name} declares {declared}; compatibility will be tested by the build",
|
|
229
|
+
package=name,
|
|
230
|
+
declared_toolchain=declared,
|
|
231
|
+
environment_toolchain=toolchain,
|
|
232
|
+
direct=requested is not None,
|
|
233
|
+
)
|
|
234
|
+
url = str(value.get("url", ""))
|
|
235
|
+
source_id = sha256_id(
|
|
236
|
+
"source", {"source": "git", "url": url, "revision": revision, "tree": tree}
|
|
237
|
+
)
|
|
238
|
+
metadata = {
|
|
239
|
+
"schema": "lean-runtime-git-source/1",
|
|
240
|
+
"source_id": source_id,
|
|
241
|
+
"name": name,
|
|
242
|
+
"url": url,
|
|
243
|
+
"revision": revision,
|
|
244
|
+
"tree_hash": tree,
|
|
245
|
+
}
|
|
246
|
+
self.store.publish_source(checkout, source_id, metadata)
|
|
247
|
+
self.events.emit(
|
|
248
|
+
"source.locked",
|
|
249
|
+
f"Locked {name}",
|
|
250
|
+
package=name,
|
|
251
|
+
revision=revision,
|
|
252
|
+
source_id=source_id,
|
|
253
|
+
)
|
|
254
|
+
locked.append(
|
|
255
|
+
LockedPackage(
|
|
256
|
+
name=name,
|
|
257
|
+
url=url,
|
|
258
|
+
requested_revision=(
|
|
259
|
+
(
|
|
260
|
+
requested.rev.lower()
|
|
261
|
+
if requested.revision_kind == "commit"
|
|
262
|
+
else requested.rev
|
|
263
|
+
)
|
|
264
|
+
if requested
|
|
265
|
+
else value.get("inputRev")
|
|
266
|
+
),
|
|
267
|
+
revision=revision,
|
|
268
|
+
tree_hash=tree,
|
|
269
|
+
source_id=source_id,
|
|
270
|
+
inherited=bool(value.get("inherited", False)),
|
|
271
|
+
subdir=subdir if isinstance(subdir, str) else None,
|
|
272
|
+
root_module=requested.module if requested else None,
|
|
273
|
+
artifact_command=requested.artifact_command if requested else (),
|
|
274
|
+
)
|
|
275
|
+
)
|
|
276
|
+
missing = sorted(set(direct) - {package.name for package in locked})
|
|
277
|
+
if missing:
|
|
278
|
+
raise ResolutionError(
|
|
279
|
+
"direct packages missing from Lake manifest: " + ", ".join(missing),
|
|
280
|
+
phase="lock-validation",
|
|
281
|
+
)
|
|
282
|
+
return tuple(sorted(locked, key=lambda package: package.name))
|