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.
Files changed (111) hide show
  1. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/CHANGELOG.md +16 -0
  2. {lean_runtime-2.0.0/lean_runtime.egg-info → lean_runtime-2.0.2}/PKG-INFO +1 -1
  3. lean_runtime-2.0.2/lean_runtime/_git.py +13 -0
  4. lean_runtime-2.0.2/lean_runtime/_paths.py +38 -0
  5. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/bundles.py +6 -5
  6. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/cli.py +14 -1
  7. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/environments.py +14 -11
  8. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/programs.py +4 -4
  9. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/projects.py +2 -1
  10. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/references.py +4 -3
  11. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/resolver.py +4 -3
  12. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/store.py +12 -9
  13. {lean_runtime-2.0.0 → lean_runtime-2.0.2/lean_runtime.egg-info}/PKG-INFO +1 -1
  14. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/SOURCES.txt +4 -0
  15. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/pyproject.toml +1 -1
  16. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_cli.py +22 -1
  17. lean_runtime-2.0.2/tests/test_git.py +12 -0
  18. lean_runtime-2.0.2/tests/test_paths.py +25 -0
  19. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_store.py +30 -0
  20. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/LICENSE +0 -0
  21. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/MANIFEST.in +0 -0
  22. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/README.md +0 -0
  23. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/compatibility/README.md +0 -0
  24. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/compatibility/mathlib-4.32.2.json +0 -0
  25. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/compatibility/mathlib-4.32.2.toml +0 -0
  26. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/architecture.md +0 -0
  27. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/captures.md +0 -0
  28. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/case-study-v1.md +0 -0
  29. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/cli.md +0 -0
  30. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/development.md +0 -0
  31. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/environments.md +0 -0
  32. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/getting-started.md +0 -0
  33. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/index.md +0 -0
  34. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/local-projects.md +0 -0
  35. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/portable-copies.md +0 -0
  36. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/python-api.md +0 -0
  37. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/ready-programs.md +0 -0
  38. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/standalone-files.md +0 -0
  39. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/trust-and-limitations.md +0 -0
  40. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/docs/v1-precision.md +0 -0
  41. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/examples/mathlib.toml +0 -0
  42. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/__init__.py +0 -0
  43. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/__main__.py +0 -0
  44. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/backends.py +0 -0
  45. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/comparison.py +0 -0
  46. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/decisions.py +0 -0
  47. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/diagnostics.py +0 -0
  48. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/errors.py +0 -0
  49. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/events.py +0 -0
  50. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/facade.py +0 -0
  51. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/frontmatter.py +0 -0
  52. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/health.py +0 -0
  53. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/lake.py +0 -0
  54. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/lockfiles.py +0 -0
  55. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/locking.py +0 -0
  56. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/matrix.py +0 -0
  57. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/models.py +0 -0
  58. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/oci.py +0 -0
  59. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/policies.py +0 -0
  60. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/profiling.py +0 -0
  61. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/publisher_verification.py +0 -0
  62. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/py.typed +0 -0
  63. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/run_cli.py +0 -0
  64. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/runtime.py +0 -0
  65. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/schema_resources.py +0 -0
  66. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/serialization.py +0 -0
  67. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/specs.py +0 -0
  68. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/timings.py +0 -0
  69. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/toolchains.py +0 -0
  70. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/verification.py +0 -0
  71. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime/wire.py +0 -0
  72. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/dependency_links.txt +0 -0
  73. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/entry_points.txt +0 -0
  74. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/requires.txt +0 -0
  75. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/lean_runtime.egg-info/top_level.txt +0 -0
  76. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/mkdocs.yml +0 -0
  77. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/cleanup-v1.schema.json +0 -0
  78. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/comparison-v1.schema.json +0 -0
  79. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/execution-v1.schema.json +0 -0
  80. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/inspect-v1.schema.json +0 -0
  81. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/matrix-v1.schema.json +0 -0
  82. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/profile-v1.schema.json +0 -0
  83. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/schemas/verify-v1.schema.json +0 -0
  84. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/scripts/run_compatibility.py +0 -0
  85. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/scripts/run_v1_case_study.py +0 -0
  86. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/scripts/smoke_wheel.py +0 -0
  87. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/setup.cfg +0 -0
  88. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/conftest.py +0 -0
  89. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_backend.py +0 -0
  90. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_bundles.py +0 -0
  91. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_diagnostics.py +0 -0
  92. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_environment_integration.py +0 -0
  93. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_events_health.py +0 -0
  94. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_facade.py +0 -0
  95. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_frontmatter.py +0 -0
  96. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_interactive.py +0 -0
  97. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_lockfiles.py +0 -0
  98. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_multifile.py +0 -0
  99. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_prebuilt_policy.py +0 -0
  100. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_programs.py +0 -0
  101. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_projects.py +0 -0
  102. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_references.py +0 -0
  103. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_run_cli.py +0 -0
  104. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_runtime.py +0 -0
  105. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_schema_resources.py +0 -0
  106. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_schemas.py +0 -0
  107. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_signatures.py +0 -0
  108. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_specs.py +0 -0
  109. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_toolchains.py +0 -0
  110. {lean_runtime-2.0.0 → lean_runtime-2.0.2}/tests/test_v1_precision.py +0 -0
  111. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -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
- ["git", "-C", str(root), "rev-parse", revision],
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
- shutil.rmtree(stage)
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
- print(f"lean-runtime: {exc}", file=sys.stderr)
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
- shutil.rmtree(instance)
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
- shutil.rmtree(instance)
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
- shutil.rmtree(stage)
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
- ["git", "init", "-q"],
1142
- ["git", "remote", "add", "origin", package.url],
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
- ["git", "checkout", "--detach", "FETCH_HEAD"],
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
- ["git", "rev-parse", "HEAD^{tree}"],
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
- shutil.rmtree(instance)
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
- shutil.rmtree(stage)
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
- shutil.rmtree(stage)
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
- ["git", "-C", str(root), *arguments],
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
- ["git", *arguments],
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=("git", *arguments),
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=("git", *arguments),
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
- ["git", "-C", str(path), *arguments],
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=("git", "-C", str(path), *arguments),
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 = ["git", "ls-remote", package.url, reference, f"{reference}^{{}}"]
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
- ["git", "-C", str(source), *arguments],
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
- shutil.rmtree(stage)
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
- shutil.rmtree(path)
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)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 2.0.0
3
+ Version: 2.0.2
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -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,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "lean-runtime"
7
- version = "2.0.0"
7
+ version = "2.0.2"
8
8
  description = "Run Lean 4 proofs from Python or standalone files"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -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