lean-runtime 2.0.1__tar.gz → 2.0.3__tar.gz
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-2.0.1 → lean_runtime-2.0.3}/CHANGELOG.md +16 -0
- {lean_runtime-2.0.1/lean_runtime.egg-info → lean_runtime-2.0.3}/PKG-INFO +1 -1
- lean_runtime-2.0.3/lean_runtime/_paths.py +38 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/bundles.py +56 -9
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/cli.py +14 -1
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/environments.py +7 -4
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/programs.py +4 -4
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/store.py +3 -2
- {lean_runtime-2.0.1 → lean_runtime-2.0.3/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime.egg-info/SOURCES.txt +2 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/pyproject.toml +1 -1
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_bundles.py +53 -1
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_cli.py +22 -1
- lean_runtime-2.0.3/tests/test_paths.py +25 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_store.py +4 -2
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/LICENSE +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/MANIFEST.in +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/README.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/compatibility/README.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/architecture.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/captures.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/case-study-v1.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/cli.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/development.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/environments.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/getting-started.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/index.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/local-projects.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/portable-copies.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/python-api.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/ready-programs.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/standalone-files.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/docs/v1-precision.md +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/examples/mathlib.toml +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/__init__.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/__main__.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/_git.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/backends.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/comparison.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/decisions.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/errors.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/events.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/facade.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/health.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/lake.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/locking.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/matrix.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/models.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/oci.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/policies.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/profiling.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/projects.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/py.typed +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/references.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/resolver.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/run_cli.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/runtime.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/serialization.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/specs.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/timings.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/verification.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime/wire.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/mkdocs.yml +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/scripts/run_compatibility.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/setup.cfg +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/conftest.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_backend.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_diagnostics.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_environment_integration.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_events_health.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_facade.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_frontmatter.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_git.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_interactive.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_lockfiles.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_multifile.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_programs.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_projects.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_references.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_run_cli.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_runtime.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_schema_resources.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_schemas.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_signatures.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_specs.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_toolchains.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_v1_precision.py +0 -0
- {lean_runtime-2.0.1 → lean_runtime-2.0.3}/tests/test_verification_inventory.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.3
|
|
4
|
+
|
|
5
|
+
- Accept portable-copy symlinks whose normalized targets remain inside the
|
|
6
|
+
extracted copy, including common links such as `docs/README.md -> ../README.md`.
|
|
7
|
+
- Continue rejecting absolute or escaping symlink targets, duplicate archive
|
|
8
|
+
members, and later archive writes that traverse an extracted symlink.
|
|
9
|
+
|
|
10
|
+
## 2.0.2
|
|
11
|
+
|
|
12
|
+
- Remove staged environments safely when Git-for-Windows leaves source pack
|
|
13
|
+
files read-only or briefly locked.
|
|
14
|
+
- Preserve the primary environment-build diagnostic when staging cleanup also
|
|
15
|
+
fails.
|
|
16
|
+
- Show the failed phase, command, exit code, and captured tool output when a
|
|
17
|
+
verbose CLI materialization fails.
|
|
18
|
+
|
|
3
19
|
## 2.0.1
|
|
4
20
|
|
|
5
21
|
- Enable Git's long-path support for every Runtime-managed source operation so
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Internal cross-platform filesystem operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import stat
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _retry_writable(
|
|
16
|
+
function: Callable[..., Any],
|
|
17
|
+
path: str,
|
|
18
|
+
_error: tuple[type[BaseException], BaseException, TracebackType | None],
|
|
19
|
+
) -> None:
|
|
20
|
+
"""Clear Git-for-Windows read-only attributes and retry one removal."""
|
|
21
|
+
|
|
22
|
+
os.chmod(path, stat.S_IWRITE)
|
|
23
|
+
function(path)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def remove_tree(path: Path) -> None:
|
|
27
|
+
"""Remove a tree containing Git packs, tolerating transient Windows locks."""
|
|
28
|
+
|
|
29
|
+
for attempt in range(4):
|
|
30
|
+
try:
|
|
31
|
+
shutil.rmtree(path, onerror=_retry_writable)
|
|
32
|
+
return
|
|
33
|
+
except PermissionError:
|
|
34
|
+
if not path.exists():
|
|
35
|
+
return
|
|
36
|
+
if attempt == 3:
|
|
37
|
+
raise
|
|
38
|
+
time.sleep(0.05 * (2**attempt))
|
|
@@ -20,6 +20,7 @@ from pathlib import Path, PurePosixPath
|
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
22
|
from ._git import git_command
|
|
23
|
+
from ._paths import remove_tree
|
|
23
24
|
from .backends import Backend
|
|
24
25
|
from .errors import EnvironmentError
|
|
25
26
|
from .events import EventEmitter
|
|
@@ -202,11 +203,54 @@ def _require_media_type(descriptor: dict[str, Any], expected: str, label: str) -
|
|
|
202
203
|
|
|
203
204
|
def _safe_name(name: str) -> PurePosixPath:
|
|
204
205
|
path = PurePosixPath(name)
|
|
205
|
-
if
|
|
206
|
+
if (
|
|
207
|
+
not name
|
|
208
|
+
or path == PurePosixPath(".")
|
|
209
|
+
or path.is_absolute()
|
|
210
|
+
or ".." in path.parts
|
|
211
|
+
or "\\" in name
|
|
212
|
+
or "\x00" in name
|
|
213
|
+
):
|
|
206
214
|
raise EnvironmentError(f"unsafe bundle member path: {name!r}")
|
|
207
215
|
return path
|
|
208
216
|
|
|
209
217
|
|
|
218
|
+
def _internal_link_target(member: PurePosixPath, linkname: str) -> PurePosixPath:
|
|
219
|
+
link = PurePosixPath(linkname)
|
|
220
|
+
if not linkname or link.is_absolute() or "\\" in linkname or "\x00" in linkname:
|
|
221
|
+
raise EnvironmentError(f"unsafe bundle symlink: {member.as_posix()!r}")
|
|
222
|
+
parts: list[str] = []
|
|
223
|
+
for part in member.parent.joinpath(link).parts:
|
|
224
|
+
if part in {"", "."}:
|
|
225
|
+
continue
|
|
226
|
+
if part == "..":
|
|
227
|
+
if not parts:
|
|
228
|
+
raise EnvironmentError(f"unsafe bundle symlink: {member.as_posix()!r}")
|
|
229
|
+
parts.pop()
|
|
230
|
+
else:
|
|
231
|
+
parts.append(part)
|
|
232
|
+
return PurePosixPath(*parts) if parts else PurePosixPath(".")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _ensure_directories(destination: Path, relative: PurePosixPath) -> None:
|
|
236
|
+
current = destination
|
|
237
|
+
for part in relative.parts:
|
|
238
|
+
if part == ".":
|
|
239
|
+
continue
|
|
240
|
+
current = current / part
|
|
241
|
+
if current.is_symlink():
|
|
242
|
+
raise EnvironmentError(
|
|
243
|
+
f"bundle member traverses an extracted symlink: {relative.as_posix()!r}"
|
|
244
|
+
)
|
|
245
|
+
if current.exists():
|
|
246
|
+
if not current.is_dir():
|
|
247
|
+
raise EnvironmentError(
|
|
248
|
+
f"bundle member parent is not a directory: {relative.as_posix()!r}"
|
|
249
|
+
)
|
|
250
|
+
else:
|
|
251
|
+
current.mkdir()
|
|
252
|
+
|
|
253
|
+
|
|
210
254
|
def _packages_directory(lock: EnvironmentLock) -> PurePosixPath:
|
|
211
255
|
value = lock.manifest.get("packagesDir", ".lake/packages")
|
|
212
256
|
if not isinstance(value, str):
|
|
@@ -220,6 +264,8 @@ def _packages_directory(lock: EnvironmentLock) -> PurePosixPath:
|
|
|
220
264
|
def _extract_layer(data: bytes | Path, destination: Path) -> None:
|
|
221
265
|
total = 0
|
|
222
266
|
count = 0
|
|
267
|
+
if destination.is_symlink():
|
|
268
|
+
raise EnvironmentError("bundle extraction destination must not be a symlink")
|
|
223
269
|
destination.mkdir(parents=True, exist_ok=True)
|
|
224
270
|
try:
|
|
225
271
|
if isinstance(data, Path):
|
|
@@ -239,10 +285,12 @@ def _extract_layer(data: bytes | Path, destination: Path) -> None:
|
|
|
239
285
|
relative = _safe_name(member.name)
|
|
240
286
|
target = destination.joinpath(*relative.parts)
|
|
241
287
|
if member.isdir():
|
|
242
|
-
|
|
288
|
+
_ensure_directories(destination, relative)
|
|
243
289
|
target.chmod(member.mode & 0o777)
|
|
244
290
|
elif member.isfile():
|
|
245
|
-
|
|
291
|
+
_ensure_directories(destination, relative.parent)
|
|
292
|
+
if target.exists() or target.is_symlink():
|
|
293
|
+
raise EnvironmentError(f"duplicate bundle member: {member.name!r}")
|
|
246
294
|
source = archive.extractfile(member)
|
|
247
295
|
if source is None:
|
|
248
296
|
raise EnvironmentError(f"bundle member has no content: {member.name}")
|
|
@@ -250,11 +298,10 @@ def _extract_layer(data: bytes | Path, destination: Path) -> None:
|
|
|
250
298
|
shutil.copyfileobj(source, handle)
|
|
251
299
|
target.chmod(member.mode & 0o777)
|
|
252
300
|
elif member.issym():
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
if
|
|
256
|
-
raise EnvironmentError(f"
|
|
257
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
301
|
+
_internal_link_target(relative, member.linkname)
|
|
302
|
+
_ensure_directories(destination, relative.parent)
|
|
303
|
+
if target.exists() or target.is_symlink():
|
|
304
|
+
raise EnvironmentError(f"duplicate bundle member: {member.name!r}")
|
|
258
305
|
target.symlink_to(member.linkname)
|
|
259
306
|
else:
|
|
260
307
|
raise EnvironmentError(f"unsupported bundle member: {member.name!r}")
|
|
@@ -667,7 +714,7 @@ class EnvironmentBundles:
|
|
|
667
714
|
stage.replace(destination)
|
|
668
715
|
finally:
|
|
669
716
|
if stage.exists():
|
|
670
|
-
|
|
717
|
+
remove_tree(stage)
|
|
671
718
|
if name:
|
|
672
719
|
self.store.set_alias(name, environment_id)
|
|
673
720
|
self.events.emit(
|
|
@@ -50,6 +50,19 @@ def _progress(event: RuntimeEvent) -> None:
|
|
|
50
50
|
print(f"lean-runtime: {event.kind}{package}: {event.message}", file=sys.stderr)
|
|
51
51
|
|
|
52
52
|
|
|
53
|
+
def _print_operation_failure(exc: ResolutionError | MaterializationError, *, verbose: bool) -> None:
|
|
54
|
+
print(f"lean-runtime: {exc}", file=sys.stderr)
|
|
55
|
+
if not verbose:
|
|
56
|
+
return
|
|
57
|
+
print(f"phase: {exc.phase}", file=sys.stderr)
|
|
58
|
+
if exc.command:
|
|
59
|
+
print("command: " + " ".join(exc.command), file=sys.stderr)
|
|
60
|
+
if exc.exit_code is not None:
|
|
61
|
+
print(f"exit code: {exc.exit_code}", file=sys.stderr)
|
|
62
|
+
if exc.output:
|
|
63
|
+
print(exc.output.rstrip(), file=sys.stderr)
|
|
64
|
+
|
|
65
|
+
|
|
53
66
|
def _cli_source_name(path: Path) -> str:
|
|
54
67
|
if path.is_absolute():
|
|
55
68
|
return path.name
|
|
@@ -643,7 +656,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
643
656
|
)
|
|
644
657
|
)
|
|
645
658
|
else:
|
|
646
|
-
|
|
659
|
+
_print_operation_failure(exc, verbose=args.verbose)
|
|
647
660
|
return 2
|
|
648
661
|
except (LeanRuntimeError, OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
|
|
649
662
|
if getattr(args, "json", False):
|
|
@@ -6,7 +6,6 @@ import asyncio
|
|
|
6
6
|
import json
|
|
7
7
|
import os
|
|
8
8
|
import re
|
|
9
|
-
import shutil
|
|
10
9
|
import subprocess
|
|
11
10
|
import tempfile
|
|
12
11
|
import threading
|
|
@@ -21,6 +20,7 @@ from pathlib import Path, PurePosixPath
|
|
|
21
20
|
from typing import Any, Generic, TextIO, TypeVar, cast
|
|
22
21
|
|
|
23
22
|
from ._git import git_command
|
|
23
|
+
from ._paths import remove_tree
|
|
24
24
|
from .backends import Backend, BackendResult, InteractiveProcess, InteractiveTextReader
|
|
25
25
|
from .diagnostics import error_diagnostic, parse_diagnostics
|
|
26
26
|
from .errors import EnvironmentError, MaterializationError, PolicyError
|
|
@@ -559,7 +559,7 @@ class Environment:
|
|
|
559
559
|
|
|
560
560
|
def cleanup() -> None:
|
|
561
561
|
if instance.exists():
|
|
562
|
-
|
|
562
|
+
remove_tree(instance)
|
|
563
563
|
with suppress(OSError):
|
|
564
564
|
job_parent.rmdir()
|
|
565
565
|
|
|
@@ -847,7 +847,7 @@ class Environment:
|
|
|
847
847
|
return result
|
|
848
848
|
finally:
|
|
849
849
|
if instance.exists():
|
|
850
|
-
|
|
850
|
+
remove_tree(instance)
|
|
851
851
|
with suppress(OSError):
|
|
852
852
|
job_parent.rmdir()
|
|
853
853
|
|
|
@@ -1107,7 +1107,10 @@ class EnvironmentManager:
|
|
|
1107
1107
|
)
|
|
1108
1108
|
finally:
|
|
1109
1109
|
if stage.exists():
|
|
1110
|
-
|
|
1110
|
+
# Preserve a build/materialization diagnostic if cleanup of
|
|
1111
|
+
# Git's read-only pack files also fails on Windows.
|
|
1112
|
+
with suppress(OSError):
|
|
1113
|
+
remove_tree(stage)
|
|
1111
1114
|
|
|
1112
1115
|
def _ensure_sources(self, lock: EnvironmentLock) -> None:
|
|
1113
1116
|
"""Acquire exact locked sources without invoking Lake resolution."""
|
|
@@ -10,7 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
import hashlib
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
|
-
import shutil
|
|
14
13
|
import tempfile
|
|
15
14
|
import uuid
|
|
16
15
|
from collections.abc import Mapping, Sequence
|
|
@@ -20,6 +19,7 @@ from datetime import datetime, timezone
|
|
|
20
19
|
from pathlib import Path, PurePosixPath
|
|
21
20
|
from typing import Any, cast
|
|
22
21
|
|
|
22
|
+
from ._paths import remove_tree
|
|
23
23
|
from .backends import Backend, BackendResult, InteractiveProcess
|
|
24
24
|
from .bundles import (
|
|
25
25
|
INDEX_MEDIA_TYPE,
|
|
@@ -238,7 +238,7 @@ class ReadyProgram:
|
|
|
238
238
|
|
|
239
239
|
def cleanup() -> None:
|
|
240
240
|
if instance.exists():
|
|
241
|
-
|
|
241
|
+
remove_tree(instance)
|
|
242
242
|
with suppress(OSError):
|
|
243
243
|
job_parent.rmdir()
|
|
244
244
|
|
|
@@ -347,7 +347,7 @@ class ProgramManager:
|
|
|
347
347
|
stage.replace(destination)
|
|
348
348
|
finally:
|
|
349
349
|
if stage.exists():
|
|
350
|
-
|
|
350
|
+
remove_tree(stage)
|
|
351
351
|
self.events.emit(
|
|
352
352
|
"program.created", "Created executable program", program_id=manifest.program_id
|
|
353
353
|
)
|
|
@@ -534,7 +534,7 @@ class ProgramManager:
|
|
|
534
534
|
stage.replace(destination)
|
|
535
535
|
finally:
|
|
536
536
|
if stage.exists():
|
|
537
|
-
|
|
537
|
+
remove_tree(stage)
|
|
538
538
|
return self.open(manifest.program_id)
|
|
539
539
|
|
|
540
540
|
|
|
@@ -18,6 +18,7 @@ from pathlib import Path
|
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
20
|
from ._git import git_command
|
|
21
|
+
from ._paths import remove_tree
|
|
21
22
|
from .errors import EnvironmentError
|
|
22
23
|
from .lockfiles import EnvironmentLock
|
|
23
24
|
from .locking import FileLock
|
|
@@ -348,7 +349,7 @@ class EnvironmentStore:
|
|
|
348
349
|
# Failed Git checkouts can leave locked pack files on
|
|
349
350
|
# Windows. Cleanup must not mask the actionable Git error.
|
|
350
351
|
with suppress(OSError):
|
|
351
|
-
|
|
352
|
+
remove_tree(stage)
|
|
352
353
|
return destination
|
|
353
354
|
|
|
354
355
|
def environment_path(self, environment_id: str) -> Path:
|
|
@@ -556,7 +557,7 @@ class EnvironmentStore:
|
|
|
556
557
|
):
|
|
557
558
|
retained.append(path.name)
|
|
558
559
|
continue
|
|
559
|
-
|
|
560
|
+
remove_tree(path)
|
|
560
561
|
usage.unlink(missing_ok=True)
|
|
561
562
|
removed.append(path.name)
|
|
562
563
|
return CleanupReport(tuple(candidates), tuple(removed), tuple(retained), dry_run)
|
|
@@ -26,6 +26,7 @@ examples/mathlib.toml
|
|
|
26
26
|
lean_runtime/__init__.py
|
|
27
27
|
lean_runtime/__main__.py
|
|
28
28
|
lean_runtime/_git.py
|
|
29
|
+
lean_runtime/_paths.py
|
|
29
30
|
lean_runtime/backends.py
|
|
30
31
|
lean_runtime/bundles.py
|
|
31
32
|
lean_runtime/cli.py
|
|
@@ -91,6 +92,7 @@ tests/test_git.py
|
|
|
91
92
|
tests/test_interactive.py
|
|
92
93
|
tests/test_lockfiles.py
|
|
93
94
|
tests/test_multifile.py
|
|
95
|
+
tests/test_paths.py
|
|
94
96
|
tests/test_prebuilt_policy.py
|
|
95
97
|
tests/test_programs.py
|
|
96
98
|
tests/test_projects.py
|
|
@@ -2,6 +2,7 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import hashlib
|
|
4
4
|
import http.server
|
|
5
|
+
import io
|
|
5
6
|
import json
|
|
6
7
|
import os
|
|
7
8
|
import shutil
|
|
@@ -15,7 +16,7 @@ from pathlib import Path
|
|
|
15
16
|
import pytest
|
|
16
17
|
|
|
17
18
|
from lean_runtime import EnvironmentError, EnvironmentLock, LockedPackage, Runtime
|
|
18
|
-
from lean_runtime.bundles import _oci_archive
|
|
19
|
+
from lean_runtime.bundles import _extract_layer, _oci_archive
|
|
19
20
|
from lean_runtime.environments import ENVIRONMENT_SCHEMA
|
|
20
21
|
from lean_runtime.oci import OCIEnvironmentPublisher, OCIRepository
|
|
21
22
|
from lean_runtime.store import environment_identity, platform_record, source_snapshot_digest
|
|
@@ -119,6 +120,57 @@ def _archive_entries(path: Path) -> dict[str, bytes]:
|
|
|
119
120
|
return result
|
|
120
121
|
|
|
121
122
|
|
|
123
|
+
def _layer(*members: tuple[str, str, bytes | str]) -> bytes:
|
|
124
|
+
output = io.BytesIO()
|
|
125
|
+
with tarfile.open(fileobj=output, mode="w:gz", format=tarfile.PAX_FORMAT) as archive:
|
|
126
|
+
for kind, name, value in members:
|
|
127
|
+
info = tarfile.TarInfo(name)
|
|
128
|
+
info.mode = 0o644
|
|
129
|
+
if kind == "file":
|
|
130
|
+
assert isinstance(value, bytes)
|
|
131
|
+
info.size = len(value)
|
|
132
|
+
archive.addfile(info, io.BytesIO(value))
|
|
133
|
+
else:
|
|
134
|
+
assert kind == "symlink" and isinstance(value, str)
|
|
135
|
+
info.type = tarfile.SYMTYPE
|
|
136
|
+
info.linkname = value
|
|
137
|
+
archive.addfile(info)
|
|
138
|
+
return output.getvalue()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
|
|
142
|
+
def test_layer_extract_accepts_internal_parent_symlink(tmp_path: Path) -> None:
|
|
143
|
+
layer = _layer(
|
|
144
|
+
("file", "README.md", b"Batteries\n"),
|
|
145
|
+
("symlink", "docs/README.md", "../README.md"),
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
_extract_layer(layer, tmp_path / "output")
|
|
149
|
+
|
|
150
|
+
link = tmp_path / "output" / "docs" / "README.md"
|
|
151
|
+
assert link.is_symlink()
|
|
152
|
+
assert link.read_text() == "Batteries\n"
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
|
|
156
|
+
def test_layer_extract_rejects_symlink_outside_destination(tmp_path: Path) -> None:
|
|
157
|
+
layer = _layer(("symlink", "docs/README.md", "../../outside"))
|
|
158
|
+
|
|
159
|
+
with pytest.raises(EnvironmentError, match="unsafe bundle symlink"):
|
|
160
|
+
_extract_layer(layer, tmp_path / "output")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.skipif(os.name == "nt", reason="Windows runners may not permit symlink creation")
|
|
164
|
+
def test_layer_extract_rejects_later_write_through_symlink(tmp_path: Path) -> None:
|
|
165
|
+
layer = _layer(
|
|
166
|
+
("symlink", "redirect", "inside"),
|
|
167
|
+
("file", "redirect/payload", b"must not be written"),
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
with pytest.raises(EnvironmentError, match="traverses an extracted symlink"):
|
|
171
|
+
_extract_layer(layer, tmp_path / "output")
|
|
172
|
+
|
|
173
|
+
|
|
122
174
|
def test_bundle_export_is_deterministic_and_imports_into_fresh_store(tmp_path: Path) -> None:
|
|
123
175
|
producer, environment_id, lock = _published_runtime(tmp_path / "producer")
|
|
124
176
|
first = tmp_path / "first.oci.tar.gz"
|
|
@@ -4,7 +4,8 @@ import json
|
|
|
4
4
|
from pathlib import Path
|
|
5
5
|
|
|
6
6
|
from lean_runtime.bundles import PortableCopyInfo
|
|
7
|
-
from lean_runtime.cli import main, parser
|
|
7
|
+
from lean_runtime.cli import _print_operation_failure, main, parser
|
|
8
|
+
from lean_runtime.errors import MaterializationError
|
|
8
9
|
from lean_runtime.models import ExecutionResult
|
|
9
10
|
from lean_runtime.verification import VerificationCheck, VerificationReport
|
|
10
11
|
|
|
@@ -34,6 +35,26 @@ def test_removed_v1_commands_are_absent() -> None:
|
|
|
34
35
|
assert commands.isdisjoint(removed)
|
|
35
36
|
|
|
36
37
|
|
|
38
|
+
def test_verbose_materialization_failure_includes_tool_output(capsys) -> None:
|
|
39
|
+
failure = MaterializationError(
|
|
40
|
+
"environment build failed",
|
|
41
|
+
phase="build",
|
|
42
|
+
command=("lake", "build"),
|
|
43
|
+
exit_code=1,
|
|
44
|
+
output="specific compiler failure\n",
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
_print_operation_failure(failure, verbose=True)
|
|
48
|
+
|
|
49
|
+
assert capsys.readouterr().err == (
|
|
50
|
+
"lean-runtime: environment build failed\n"
|
|
51
|
+
"phase: build\n"
|
|
52
|
+
"command: lake build\n"
|
|
53
|
+
"exit code: 1\n"
|
|
54
|
+
"specific compiler failure\n"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
37
58
|
def test_check_file_cli_json_result(monkeypatch, tmp_path: Path, capsys) -> None:
|
|
38
59
|
source = tmp_path / "Main.lean"
|
|
39
60
|
source.write_text("example : True := by trivial")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
|
|
3
|
+
from lean_runtime._paths import remove_tree
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_remove_tree_retries_transient_permission_errors(tmp_path: Path, monkeypatch) -> None:
|
|
7
|
+
target = tmp_path / "tree"
|
|
8
|
+
target.mkdir()
|
|
9
|
+
calls = 0
|
|
10
|
+
|
|
11
|
+
def flaky_rmtree(path: Path, *, onerror) -> None:
|
|
12
|
+
nonlocal calls
|
|
13
|
+
calls += 1
|
|
14
|
+
if calls == 1:
|
|
15
|
+
raise PermissionError("Git pack is briefly locked")
|
|
16
|
+
assert onerror is not None
|
|
17
|
+
path.rmdir()
|
|
18
|
+
|
|
19
|
+
monkeypatch.setattr("lean_runtime._paths.shutil.rmtree", flaky_rmtree)
|
|
20
|
+
monkeypatch.setattr("lean_runtime._paths.time.sleep", lambda _seconds: None)
|
|
21
|
+
|
|
22
|
+
remove_tree(target)
|
|
23
|
+
|
|
24
|
+
assert calls == 2
|
|
25
|
+
assert not target.exists()
|
|
@@ -217,11 +217,13 @@ def test_source_snapshot_preserves_git_error_when_cleanup_also_fails(
|
|
|
217
217
|
Path(command[-1]).mkdir()
|
|
218
218
|
return subprocess.CompletedProcess(command, 1, "", "Filename too long")
|
|
219
219
|
|
|
220
|
-
def failed_cleanup(_path: Path) -> None:
|
|
220
|
+
def failed_cleanup(_path: Path, *, onerror) -> None:
|
|
221
|
+
assert onerror is not None
|
|
221
222
|
raise PermissionError("pack file is locked")
|
|
222
223
|
|
|
223
224
|
monkeypatch.setattr("lean_runtime.store.subprocess.run", failed_clone)
|
|
224
|
-
monkeypatch.setattr("lean_runtime.
|
|
225
|
+
monkeypatch.setattr("lean_runtime._paths.shutil.rmtree", failed_cleanup)
|
|
226
|
+
monkeypatch.setattr("lean_runtime._paths.time.sleep", lambda _seconds: None)
|
|
225
227
|
|
|
226
228
|
with pytest.raises(EnvironmentError, match="Filename too long"):
|
|
227
229
|
store.publish_source(checkout, source_id, metadata)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|