lean-runtime 2.0.0__tar.gz → 2.0.2__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.0 → lean_runtime-2.0.2}/CHANGELOG.md +16 -0
- {lean_runtime-2.0.0/lean_runtime.egg-info → lean_runtime-2.0.2}/PKG-INFO +1 -1
- lean_runtime-2.0.2/lean_runtime/_git.py +13 -0
- lean_runtime-2.0.2/lean_runtime/_paths.py +38 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/bundles.py +6 -5
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/cli.py +14 -1
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/environments.py +14 -11
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/programs.py +4 -4
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/projects.py +2 -1
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/references.py +4 -3
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/resolver.py +4 -3
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/store.py +12 -9
- {lean_runtime-2.0.0 → lean_runtime-2.0.2/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/SOURCES.txt +4 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/pyproject.toml +1 -1
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_cli.py +22 -1
- lean_runtime-2.0.2/tests/test_git.py +12 -0
- lean_runtime-2.0.2/tests/test_paths.py +25 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_store.py +30 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/LICENSE +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/MANIFEST.in +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/README.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/compatibility/README.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/architecture.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/captures.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/case-study-v1.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/cli.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/development.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/environments.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/getting-started.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/index.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/local-projects.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/portable-copies.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/python-api.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/ready-programs.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/standalone-files.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/v1-precision.md +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/examples/mathlib.toml +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/__init__.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/__main__.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/backends.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/comparison.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/decisions.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/errors.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/events.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/facade.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/health.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/lake.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/locking.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/matrix.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/models.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/oci.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/policies.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/profiling.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/py.typed +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/run_cli.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/runtime.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/serialization.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/specs.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/timings.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/verification.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/wire.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/mkdocs.yml +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/scripts/run_compatibility.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/setup.cfg +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/conftest.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_backend.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_bundles.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_diagnostics.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_environment_integration.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_events_health.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_facade.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_frontmatter.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_interactive.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_lockfiles.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_multifile.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_programs.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_projects.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_references.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_run_cli.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_runtime.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_schema_resources.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_schemas.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_signatures.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_specs.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_toolchains.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_v1_precision.py +0 -0
- {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_verification_inventory.py +0 -0
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.0.2
|
|
4
|
+
|
|
5
|
+
- Remove staged environments safely when Git-for-Windows leaves source pack
|
|
6
|
+
files read-only or briefly locked.
|
|
7
|
+
- Preserve the primary environment-build diagnostic when staging cleanup also
|
|
8
|
+
fails.
|
|
9
|
+
- Show the failed phase, command, exit code, and captured tool output when a
|
|
10
|
+
verbose CLI materialization fails.
|
|
11
|
+
|
|
12
|
+
## 2.0.1
|
|
13
|
+
|
|
14
|
+
- Enable Git's long-path support for every Runtime-managed source operation so
|
|
15
|
+
Mathlib environments can be prepared and materialized on Windows.
|
|
16
|
+
- Preserve the primary Git diagnostic when cleanup of a failed Windows source
|
|
17
|
+
checkout also encounters locked files.
|
|
18
|
+
|
|
3
19
|
## 2.0.0
|
|
4
20
|
|
|
5
21
|
Version 2 gives the public interface the language used by Lean users rather
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Internal construction of portable Git commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def git_command(*arguments: str) -> list[str]:
|
|
7
|
+
"""Return a Git command that can materialize Lean's deep source trees.
|
|
8
|
+
|
|
9
|
+
``core.longpaths`` is meaningful on Git for Windows and harmless on other
|
|
10
|
+
platforms. Supplying it per invocation avoids mutable global Git settings.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
return ["git", "-c", "core.longpaths=true", *arguments]
|
|
@@ -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))
|
|
@@ -19,6 +19,8 @@ from datetime import datetime, timezone
|
|
|
19
19
|
from pathlib import Path, PurePosixPath
|
|
20
20
|
from typing import Any
|
|
21
21
|
|
|
22
|
+
from ._git import git_command
|
|
23
|
+
from ._paths import remove_tree
|
|
22
24
|
from .backends import Backend
|
|
23
25
|
from .errors import EnvironmentError
|
|
24
26
|
from .events import EventEmitter
|
|
@@ -281,7 +283,7 @@ def _verify_package(root: Path, package: LockedPackage) -> None:
|
|
|
281
283
|
observed: list[str] = []
|
|
282
284
|
for revision in ("HEAD", "HEAD^{tree}"):
|
|
283
285
|
result = subprocess.run(
|
|
284
|
-
|
|
286
|
+
git_command("-C", str(root), "rev-parse", revision),
|
|
285
287
|
text=True,
|
|
286
288
|
capture_output=True,
|
|
287
289
|
check=False,
|
|
@@ -292,15 +294,14 @@ def _verify_package(root: Path, package: LockedPackage) -> None:
|
|
|
292
294
|
if observed != [package.revision.lower(), package.tree_hash.lower()]:
|
|
293
295
|
raise EnvironmentError(f"bundled package source mismatch: {package.name}")
|
|
294
296
|
status = subprocess.run(
|
|
295
|
-
|
|
296
|
-
"git",
|
|
297
|
+
git_command(
|
|
297
298
|
"-C",
|
|
298
299
|
str(root),
|
|
299
300
|
"status",
|
|
300
301
|
"--porcelain=v1",
|
|
301
302
|
"--untracked-files=all",
|
|
302
303
|
"--ignored=no",
|
|
303
|
-
|
|
304
|
+
),
|
|
304
305
|
text=True,
|
|
305
306
|
capture_output=True,
|
|
306
307
|
check=False,
|
|
@@ -667,7 +668,7 @@ class EnvironmentBundles:
|
|
|
667
668
|
stage.replace(destination)
|
|
668
669
|
finally:
|
|
669
670
|
if stage.exists():
|
|
670
|
-
|
|
671
|
+
remove_tree(stage)
|
|
671
672
|
if name:
|
|
672
673
|
self.store.set_alias(name, environment_id)
|
|
673
674
|
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
|
|
@@ -20,6 +19,8 @@ from datetime import datetime, timezone
|
|
|
20
19
|
from pathlib import Path, PurePosixPath
|
|
21
20
|
from typing import Any, Generic, TextIO, TypeVar, cast
|
|
22
21
|
|
|
22
|
+
from ._git import git_command
|
|
23
|
+
from ._paths import remove_tree
|
|
23
24
|
from .backends import Backend, BackendResult, InteractiveProcess, InteractiveTextReader
|
|
24
25
|
from .diagnostics import error_diagnostic, parse_diagnostics
|
|
25
26
|
from .errors import EnvironmentError, MaterializationError, PolicyError
|
|
@@ -558,7 +559,7 @@ class Environment:
|
|
|
558
559
|
|
|
559
560
|
def cleanup() -> None:
|
|
560
561
|
if instance.exists():
|
|
561
|
-
|
|
562
|
+
remove_tree(instance)
|
|
562
563
|
with suppress(OSError):
|
|
563
564
|
job_parent.rmdir()
|
|
564
565
|
|
|
@@ -846,7 +847,7 @@ class Environment:
|
|
|
846
847
|
return result
|
|
847
848
|
finally:
|
|
848
849
|
if instance.exists():
|
|
849
|
-
|
|
850
|
+
remove_tree(instance)
|
|
850
851
|
with suppress(OSError):
|
|
851
852
|
job_parent.rmdir()
|
|
852
853
|
|
|
@@ -1106,7 +1107,10 @@ class EnvironmentManager:
|
|
|
1106
1107
|
)
|
|
1107
1108
|
finally:
|
|
1108
1109
|
if stage.exists():
|
|
1109
|
-
|
|
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)
|
|
1110
1114
|
|
|
1111
1115
|
def _ensure_sources(self, lock: EnvironmentLock) -> None:
|
|
1112
1116
|
"""Acquire exact locked sources without invoking Lake resolution."""
|
|
@@ -1138,18 +1142,17 @@ class EnvironmentManager:
|
|
|
1138
1142
|
) as raw:
|
|
1139
1143
|
checkout = Path(raw)
|
|
1140
1144
|
commands = [
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
"git",
|
|
1145
|
+
git_command("init", "-q"),
|
|
1146
|
+
git_command("remote", "add", "origin", package.url),
|
|
1147
|
+
git_command(
|
|
1145
1148
|
"fetch",
|
|
1146
1149
|
"--depth",
|
|
1147
1150
|
"1",
|
|
1148
1151
|
"--filter=blob:none",
|
|
1149
1152
|
"origin",
|
|
1150
1153
|
package.revision,
|
|
1151
|
-
|
|
1152
|
-
|
|
1154
|
+
),
|
|
1155
|
+
git_command("checkout", "--detach", "FETCH_HEAD"),
|
|
1153
1156
|
]
|
|
1154
1157
|
for command in commands:
|
|
1155
1158
|
completed = subprocess.run(
|
|
@@ -1168,7 +1171,7 @@ class EnvironmentManager:
|
|
|
1168
1171
|
output=completed.stdout + completed.stderr,
|
|
1169
1172
|
)
|
|
1170
1173
|
tree = subprocess.run(
|
|
1171
|
-
|
|
1174
|
+
git_command("rev-parse", "HEAD^{tree}"),
|
|
1172
1175
|
cwd=checkout,
|
|
1173
1176
|
text=True,
|
|
1174
1177
|
capture_output=True,
|
|
@@ -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
|
|
|
@@ -13,6 +13,7 @@ from dataclasses import dataclass
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import TYPE_CHECKING, Any
|
|
15
15
|
|
|
16
|
+
from ._git import git_command
|
|
16
17
|
from .errors import ProjectError
|
|
17
18
|
from .models import ExecutionResult, ProjectProvenance
|
|
18
19
|
from .policies import ExecutionPolicy
|
|
@@ -31,7 +32,7 @@ def _file_digest(path: Path) -> str | None:
|
|
|
31
32
|
|
|
32
33
|
def _git(root: Path, *arguments: str) -> str | None:
|
|
33
34
|
result = subprocess.run(
|
|
34
|
-
|
|
35
|
+
git_command("-C", str(root), *arguments),
|
|
35
36
|
text=True,
|
|
36
37
|
capture_output=True,
|
|
37
38
|
check=False,
|
|
@@ -17,6 +17,7 @@ if sys.version_info >= (3, 11):
|
|
|
17
17
|
else: # pragma: no cover - exercised by the Python 3.10 CI job
|
|
18
18
|
import tomli as tomllib
|
|
19
19
|
|
|
20
|
+
from ._git import git_command
|
|
20
21
|
from .errors import ResolutionError, SpecificationError
|
|
21
22
|
from .specs import GitPackage
|
|
22
23
|
from .toolchains import ToolchainManager, normalize_toolchain
|
|
@@ -126,7 +127,7 @@ class DiscoveredPackage:
|
|
|
126
127
|
def _run_git(arguments: list[str], *, cwd: Path | None = None, timeout: float = 120) -> str:
|
|
127
128
|
try:
|
|
128
129
|
process = subprocess.run(
|
|
129
|
-
|
|
130
|
+
git_command(*arguments),
|
|
130
131
|
cwd=cwd,
|
|
131
132
|
text=True,
|
|
132
133
|
capture_output=True,
|
|
@@ -137,7 +138,7 @@ def _run_git(arguments: list[str], *, cwd: Path | None = None, timeout: float =
|
|
|
137
138
|
raise ResolutionError(
|
|
138
139
|
"package-reference Git operation timed out",
|
|
139
140
|
phase="package-discovery",
|
|
140
|
-
command=(
|
|
141
|
+
command=tuple(git_command(*arguments)),
|
|
141
142
|
exit_code=124,
|
|
142
143
|
output=str(exc.stdout or "") + str(exc.stderr or ""),
|
|
143
144
|
) from exc
|
|
@@ -145,7 +146,7 @@ def _run_git(arguments: list[str], *, cwd: Path | None = None, timeout: float =
|
|
|
145
146
|
raise ResolutionError(
|
|
146
147
|
"could not acquire package-reference metadata",
|
|
147
148
|
phase="package-discovery",
|
|
148
|
-
command=(
|
|
149
|
+
command=tuple(git_command(*arguments)),
|
|
149
150
|
exit_code=process.returncode,
|
|
150
151
|
output=process.stdout + process.stderr,
|
|
151
152
|
)
|
|
@@ -11,6 +11,7 @@ from dataclasses import replace
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
from typing import Any
|
|
13
13
|
|
|
14
|
+
from ._git import git_command
|
|
14
15
|
from .backends import Backend
|
|
15
16
|
from .errors import ResolutionError
|
|
16
17
|
from .events import EventEmitter
|
|
@@ -27,7 +28,7 @@ _COMMIT = re.compile(r"[0-9a-fA-F]{40}")
|
|
|
27
28
|
|
|
28
29
|
def _git(path: Path, *arguments: str) -> str:
|
|
29
30
|
process = subprocess.run(
|
|
30
|
-
|
|
31
|
+
git_command("-C", str(path), *arguments),
|
|
31
32
|
text=True,
|
|
32
33
|
capture_output=True,
|
|
33
34
|
check=False,
|
|
@@ -36,7 +37,7 @@ def _git(path: Path, *arguments: str) -> str:
|
|
|
36
37
|
raise ResolutionError(
|
|
37
38
|
f"could not inspect resolved Git package at {path}",
|
|
38
39
|
phase="lock-validation",
|
|
39
|
-
command=("
|
|
40
|
+
command=tuple(git_command("-C", str(path), *arguments)),
|
|
40
41
|
exit_code=process.returncode,
|
|
41
42
|
output=process.stdout + process.stderr,
|
|
42
43
|
)
|
|
@@ -144,7 +145,7 @@ class EnvironmentResolver:
|
|
|
144
145
|
pinned.append(package)
|
|
145
146
|
continue
|
|
146
147
|
reference = f"refs/tags/{package.rev}"
|
|
147
|
-
command =
|
|
148
|
+
command = git_command("ls-remote", package.url, reference, f"{reference}^{{}}")
|
|
148
149
|
process = self.backend.execute(
|
|
149
150
|
command,
|
|
150
151
|
cwd=self.store.home,
|
|
@@ -17,6 +17,8 @@ from dataclasses import dataclass
|
|
|
17
17
|
from pathlib import Path
|
|
18
18
|
from typing import Any
|
|
19
19
|
|
|
20
|
+
from ._git import git_command
|
|
21
|
+
from ._paths import remove_tree
|
|
20
22
|
from .errors import EnvironmentError
|
|
21
23
|
from .lockfiles import EnvironmentLock
|
|
22
24
|
from .locking import FileLock
|
|
@@ -271,7 +273,7 @@ class EnvironmentStore:
|
|
|
271
273
|
observed = []
|
|
272
274
|
for arguments in commands:
|
|
273
275
|
process = subprocess.run(
|
|
274
|
-
|
|
276
|
+
git_command("-C", str(source), *arguments),
|
|
275
277
|
text=True,
|
|
276
278
|
capture_output=True,
|
|
277
279
|
check=False,
|
|
@@ -298,8 +300,7 @@ class EnvironmentStore:
|
|
|
298
300
|
parent.mkdir(parents=True, exist_ok=True)
|
|
299
301
|
stage = parent / f".staging-{os.getpid()}-{uuid.uuid4().hex}"
|
|
300
302
|
try:
|
|
301
|
-
command =
|
|
302
|
-
"git",
|
|
303
|
+
command = git_command(
|
|
303
304
|
"clone",
|
|
304
305
|
"--quiet",
|
|
305
306
|
"--no-local",
|
|
@@ -308,7 +309,7 @@ class EnvironmentStore:
|
|
|
308
309
|
"--no-tags",
|
|
309
310
|
checkout.resolve().as_uri(),
|
|
310
311
|
str(stage),
|
|
311
|
-
|
|
312
|
+
)
|
|
312
313
|
process = subprocess.run(
|
|
313
314
|
command,
|
|
314
315
|
text=True,
|
|
@@ -322,15 +323,14 @@ class EnvironmentStore:
|
|
|
322
323
|
+ process.stderr
|
|
323
324
|
)
|
|
324
325
|
remote = subprocess.run(
|
|
325
|
-
|
|
326
|
-
"git",
|
|
326
|
+
git_command(
|
|
327
327
|
"-C",
|
|
328
328
|
str(stage),
|
|
329
329
|
"remote",
|
|
330
330
|
"set-url",
|
|
331
331
|
"origin",
|
|
332
332
|
str(metadata["url"]),
|
|
333
|
-
|
|
333
|
+
),
|
|
334
334
|
text=True,
|
|
335
335
|
capture_output=True,
|
|
336
336
|
check=False,
|
|
@@ -346,7 +346,10 @@ class EnvironmentStore:
|
|
|
346
346
|
stage.replace(destination)
|
|
347
347
|
finally:
|
|
348
348
|
if stage.exists():
|
|
349
|
-
|
|
349
|
+
# Failed Git checkouts can leave locked pack files on
|
|
350
|
+
# Windows. Cleanup must not mask the actionable Git error.
|
|
351
|
+
with suppress(OSError):
|
|
352
|
+
remove_tree(stage)
|
|
350
353
|
return destination
|
|
351
354
|
|
|
352
355
|
def environment_path(self, environment_id: str) -> Path:
|
|
@@ -554,7 +557,7 @@ class EnvironmentStore:
|
|
|
554
557
|
):
|
|
555
558
|
retained.append(path.name)
|
|
556
559
|
continue
|
|
557
|
-
|
|
560
|
+
remove_tree(path)
|
|
558
561
|
usage.unlink(missing_ok=True)
|
|
559
562
|
removed.append(path.name)
|
|
560
563
|
return CleanupReport(tuple(candidates), tuple(removed), tuple(retained), dry_run)
|
|
@@ -25,6 +25,8 @@ docs/v1-precision.md
|
|
|
25
25
|
examples/mathlib.toml
|
|
26
26
|
lean_runtime/__init__.py
|
|
27
27
|
lean_runtime/__main__.py
|
|
28
|
+
lean_runtime/_git.py
|
|
29
|
+
lean_runtime/_paths.py
|
|
28
30
|
lean_runtime/backends.py
|
|
29
31
|
lean_runtime/bundles.py
|
|
30
32
|
lean_runtime/cli.py
|
|
@@ -86,9 +88,11 @@ tests/test_environment_integration.py
|
|
|
86
88
|
tests/test_events_health.py
|
|
87
89
|
tests/test_facade.py
|
|
88
90
|
tests/test_frontmatter.py
|
|
91
|
+
tests/test_git.py
|
|
89
92
|
tests/test_interactive.py
|
|
90
93
|
tests/test_lockfiles.py
|
|
91
94
|
tests/test_multifile.py
|
|
95
|
+
tests/test_paths.py
|
|
92
96
|
tests/test_prebuilt_policy.py
|
|
93
97
|
tests/test_programs.py
|
|
94
98
|
tests/test_projects.py
|
|
@@ -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,12 @@
|
|
|
1
|
+
from lean_runtime._git import git_command
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_git_commands_enable_windows_long_paths_without_global_configuration() -> None:
|
|
5
|
+
assert git_command("clone", "source", "destination") == [
|
|
6
|
+
"git",
|
|
7
|
+
"-c",
|
|
8
|
+
"core.longpaths=true",
|
|
9
|
+
"clone",
|
|
10
|
+
"source",
|
|
11
|
+
"destination",
|
|
12
|
+
]
|
|
@@ -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()
|
|
@@ -197,3 +197,33 @@ def test_source_snapshot_is_shallow_and_detects_content_changes(tmp_path: Path)
|
|
|
197
197
|
revision=revision,
|
|
198
198
|
tree_hash=tree_hash,
|
|
199
199
|
)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_source_snapshot_preserves_git_error_when_cleanup_also_fails(
|
|
203
|
+
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
|
204
|
+
) -> None:
|
|
205
|
+
checkout = tmp_path / "checkout"
|
|
206
|
+
checkout.mkdir()
|
|
207
|
+
store = EnvironmentStore(tmp_path / "store")
|
|
208
|
+
source_id = "source_" + "2" * 64
|
|
209
|
+
metadata = {
|
|
210
|
+
"source_id": source_id,
|
|
211
|
+
"url": "https://example.test/package",
|
|
212
|
+
"revision": "a" * 40,
|
|
213
|
+
"tree_hash": "b" * 40,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
def failed_clone(command: list[str], **_kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
217
|
+
Path(command[-1]).mkdir()
|
|
218
|
+
return subprocess.CompletedProcess(command, 1, "", "Filename too long")
|
|
219
|
+
|
|
220
|
+
def failed_cleanup(_path: Path, *, onerror) -> None:
|
|
221
|
+
assert onerror is not None
|
|
222
|
+
raise PermissionError("pack file is locked")
|
|
223
|
+
|
|
224
|
+
monkeypatch.setattr("lean_runtime.store.subprocess.run", failed_clone)
|
|
225
|
+
monkeypatch.setattr("lean_runtime._paths.shutil.rmtree", failed_cleanup)
|
|
226
|
+
monkeypatch.setattr("lean_runtime._paths.time.sleep", lambda _seconds: None)
|
|
227
|
+
|
|
228
|
+
with pytest.raises(EnvironmentError, match="Filename too long"):
|
|
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
|