lean-runtime 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,67 @@
1
+ """Content-addressed execution environments for Lean 4."""
2
+
3
+ from .environments import (
4
+ Environment,
5
+ EnvironmentInfo,
6
+ ExecutionCapture,
7
+ ExecutionJob,
8
+ InteractiveSession,
9
+ )
10
+ from .errors import (
11
+ EnvironmentError,
12
+ LeanRuntimeError,
13
+ MaterializationError,
14
+ PolicyError,
15
+ ProjectError,
16
+ ResolutionError,
17
+ SpecificationError,
18
+ ToolchainError,
19
+ )
20
+ from .events import EventCallback, RuntimeEvent
21
+ from .health import DoctorCheck, DoctorReport
22
+ from .lockfiles import EnvironmentLock, LockedPackage
23
+ from .models import Diagnostic, ExecutionProvenance, ExecutionResult, PackageProvenance
24
+ from .policies import ExecutionPolicy
25
+ from .references import DiscoveredPackage, PackageReference
26
+ from .runtime import Runtime, project_toolchain
27
+ from .specs import EnvironmentSpec, GitPackage, Package
28
+ from .store import GarbageCollectionReport, StoreStatus
29
+ from .toolchains import ToolchainManager, normalize_toolchain
30
+
31
+ __all__ = [
32
+ "Diagnostic",
33
+ "DiscoveredPackage",
34
+ "DoctorCheck",
35
+ "DoctorReport",
36
+ "Environment",
37
+ "EnvironmentError",
38
+ "EnvironmentInfo",
39
+ "EnvironmentLock",
40
+ "EnvironmentSpec",
41
+ "ExecutionCapture",
42
+ "ExecutionJob",
43
+ "InteractiveSession",
44
+ "ExecutionPolicy",
45
+ "ExecutionProvenance",
46
+ "ExecutionResult",
47
+ "EventCallback",
48
+ "GarbageCollectionReport",
49
+ "GitPackage",
50
+ "LeanRuntimeError",
51
+ "LockedPackage",
52
+ "MaterializationError",
53
+ "Package",
54
+ "PackageReference",
55
+ "PackageProvenance",
56
+ "PolicyError",
57
+ "ProjectError",
58
+ "ResolutionError",
59
+ "Runtime",
60
+ "RuntimeEvent",
61
+ "SpecificationError",
62
+ "StoreStatus",
63
+ "ToolchainError",
64
+ "ToolchainManager",
65
+ "normalize_toolchain",
66
+ "project_toolchain",
67
+ ]
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+
3
+ if __name__ == "__main__":
4
+ raise SystemExit(main())
@@ -0,0 +1,373 @@
1
+ """Execution backends and the trusted local implementation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import signal
7
+ import subprocess
8
+ import threading
9
+ import time
10
+ from collections.abc import Callable, Mapping, Sequence
11
+ from contextlib import suppress
12
+ from dataclasses import dataclass
13
+ from pathlib import Path
14
+ from typing import Any, BinaryIO, Protocol, TextIO, cast
15
+
16
+ from .errors import PolicyError
17
+ from .policies import ExecutionPolicy
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class BackendResult:
22
+ exit_code: int
23
+ stdout: str
24
+ stderr: str
25
+ elapsed_seconds: float
26
+ timed_out: bool
27
+ cancelled: bool
28
+ output_truncated: bool
29
+ enforced_policy_fields: tuple[str, ...]
30
+
31
+
32
+ class Backend(Protocol):
33
+ name: str
34
+
35
+ def execute(
36
+ self,
37
+ command: Sequence[str],
38
+ *,
39
+ cwd: Path,
40
+ environment: Mapping[str, str],
41
+ policy: ExecutionPolicy,
42
+ cancel: threading.Event | None = None,
43
+ ) -> BackendResult: ...
44
+
45
+
46
+ class InteractiveTextReader(Protocol):
47
+ @property
48
+ def closed(self) -> bool: ...
49
+
50
+ def read(self, size: int = -1) -> str: ...
51
+
52
+ def readline(self, size: int = -1) -> str: ...
53
+
54
+ def fileno(self) -> int: ...
55
+
56
+ def close(self) -> None: ...
57
+
58
+
59
+ class InteractiveProcess(Protocol):
60
+ """Live standard-I/O streams plus managed process finalization."""
61
+
62
+ stdin: TextIO
63
+ stdout: InteractiveTextReader
64
+ stderr: InteractiveTextReader
65
+
66
+ def poll(self) -> int | None: ...
67
+
68
+ def finish(self) -> BackendResult: ...
69
+
70
+
71
+ class _OutputBudget:
72
+ def __init__(self, limit: int) -> None:
73
+ self.remaining = limit
74
+ self.lock = threading.Lock()
75
+ self.truncated = False
76
+
77
+ def take(self, chunk: bytes) -> bytes:
78
+ with self.lock:
79
+ size = min(len(chunk), self.remaining)
80
+ self.remaining -= size
81
+ if size < len(chunk):
82
+ self.truncated = True
83
+ return chunk[:size]
84
+
85
+
86
+ def _drain(stream: BinaryIO, budget: _OutputBudget, chunks: list[bytes]) -> None:
87
+ while True:
88
+ chunk = stream.read(65_536)
89
+ if not chunk:
90
+ return
91
+ kept = budget.take(chunk)
92
+ if kept:
93
+ chunks.append(kept)
94
+
95
+
96
+ class _TranscriptReader:
97
+ """Mirror caller-consumed text into the bounded execution transcript."""
98
+
99
+ def __init__(self, stream: TextIO, budget: _OutputBudget, chunks: list[bytes]) -> None:
100
+ self._stream = stream
101
+ self._budget = budget
102
+ self._chunks = chunks
103
+
104
+ @property
105
+ def closed(self) -> bool:
106
+ return self._stream.closed
107
+
108
+ def fileno(self) -> int:
109
+ return self._stream.fileno()
110
+
111
+ def _record(self, value: str) -> str:
112
+ if value:
113
+ kept = self._budget.take(value.encode("utf-8"))
114
+ if kept:
115
+ self._chunks.append(kept)
116
+ return value
117
+
118
+ def read(self, size: int = -1) -> str:
119
+ return self._record(self._stream.read(size))
120
+
121
+ def readline(self, size: int = -1) -> str:
122
+ return self._record(self._stream.readline(size))
123
+
124
+ def close(self) -> None:
125
+ self._stream.close()
126
+
127
+
128
+ class _LocalInteractiveProcess:
129
+ def __init__(
130
+ self,
131
+ process: subprocess.Popen[str],
132
+ *,
133
+ policy: ExecutionPolicy,
134
+ enforced_policy_fields: tuple[str, ...],
135
+ ) -> None:
136
+ assert process.stdin is not None
137
+ assert process.stdout is not None
138
+ assert process.stderr is not None
139
+ self._process = process
140
+ self._policy = policy
141
+ self._enforced_policy_fields = enforced_policy_fields
142
+ self._started = time.monotonic()
143
+ self._timed_out = threading.Event()
144
+ self._finished = threading.Event()
145
+ self._budget = _OutputBudget(policy.max_output_bytes)
146
+ self._stdout_chunks: list[bytes] = []
147
+ self._stderr_chunks: list[bytes] = []
148
+ self.stdin = cast(TextIO, process.stdin)
149
+ self.stdout: InteractiveTextReader = _TranscriptReader(
150
+ cast(TextIO, process.stdout), self._budget, self._stdout_chunks
151
+ )
152
+ self.stderr: InteractiveTextReader = _TranscriptReader(
153
+ cast(TextIO, process.stderr), self._budget, self._stderr_chunks
154
+ )
155
+ self._monitor = threading.Thread(
156
+ target=self._enforce_timeout,
157
+ name=f"lean-runtime-process-{process.pid}",
158
+ daemon=True,
159
+ )
160
+ self._monitor.start()
161
+
162
+ def _enforce_timeout(self) -> None:
163
+ remaining = self._policy.timeout_seconds - (time.monotonic() - self._started)
164
+ if remaining > 0 and self._finished.wait(remaining):
165
+ return
166
+ if self._process.poll() is not None:
167
+ return
168
+ self._timed_out.set()
169
+ LocalBackend._stop(self._process)
170
+ try:
171
+ self._process.wait(timeout=2)
172
+ except subprocess.TimeoutExpired:
173
+ LocalBackend._kill(self._process)
174
+
175
+ def poll(self) -> int | None:
176
+ return self._process.poll()
177
+
178
+ @staticmethod
179
+ def _remaining(reader: InteractiveTextReader) -> None:
180
+ with suppress(OSError, ValueError):
181
+ reader.read()
182
+
183
+ def finish(self) -> BackendResult:
184
+ if not self.stdin.closed:
185
+ self.stdin.close()
186
+ cancelled = False
187
+ try:
188
+ self._process.wait(timeout=2)
189
+ except subprocess.TimeoutExpired:
190
+ cancelled = True
191
+ LocalBackend._stop(self._process)
192
+ try:
193
+ self._process.wait(timeout=2)
194
+ except subprocess.TimeoutExpired:
195
+ LocalBackend._kill(self._process)
196
+ self._process.wait()
197
+ self._finished.set()
198
+ self._monitor.join(timeout=3)
199
+ self._remaining(self.stdout)
200
+ self._remaining(self.stderr)
201
+ self.stdout.close()
202
+ self.stderr.close()
203
+ timed_out = self._timed_out.is_set()
204
+ return BackendResult(
205
+ exit_code=124 if timed_out else 130 if cancelled else int(self._process.returncode),
206
+ stdout=b"".join(self._stdout_chunks).decode("utf-8", errors="replace"),
207
+ stderr=b"".join(self._stderr_chunks).decode("utf-8", errors="replace"),
208
+ elapsed_seconds=time.monotonic() - self._started,
209
+ timed_out=timed_out,
210
+ cancelled=cancelled and not timed_out,
211
+ output_truncated=self._budget.truncated,
212
+ enforced_policy_fields=self._enforced_policy_fields,
213
+ )
214
+
215
+
216
+ class LocalBackend:
217
+ """Trusted local subprocess execution with bounded captured output."""
218
+
219
+ name = "local"
220
+
221
+ @staticmethod
222
+ def _process_options(
223
+ policy: ExecutionPolicy,
224
+ ) -> tuple[list[str], Callable[[], object] | None, int]:
225
+ if policy.network == "disabled":
226
+ raise PolicyError("the local backend cannot enforce network isolation")
227
+ enforced = ["timeout_seconds", "max_output_bytes"]
228
+ preexec = None
229
+ if os.name != "nt" and (policy.memory_mb or policy.cpu_seconds):
230
+ memory_mb = policy.memory_mb
231
+ cpu_seconds = policy.cpu_seconds
232
+
233
+ def apply_limits() -> None:
234
+ import resource
235
+
236
+ if memory_mb is not None:
237
+ limit = memory_mb * 1024 * 1024
238
+ getattr(resource, "setrlimit")( # noqa: B009
239
+ getattr(resource, "RLIMIT_AS"), # noqa: B009
240
+ (limit, limit),
241
+ )
242
+ if cpu_seconds is not None:
243
+ getattr(resource, "setrlimit")( # noqa: B009
244
+ getattr(resource, "RLIMIT_CPU"), # noqa: B009
245
+ (cpu_seconds, cpu_seconds),
246
+ )
247
+
248
+ preexec = apply_limits
249
+ if memory_mb is not None:
250
+ enforced.append("memory_mb")
251
+ if cpu_seconds is not None:
252
+ enforced.append("cpu_seconds")
253
+ elif os.name == "nt" and (policy.memory_mb or policy.cpu_seconds):
254
+ raise PolicyError("the local Windows backend cannot enforce memory or CPU limits")
255
+ creationflags = getattr(subprocess, "CREATE_NEW_PROCESS_GROUP", 0) if os.name == "nt" else 0
256
+ return enforced, preexec, creationflags
257
+
258
+ def spawn_interactive(
259
+ self,
260
+ command: Sequence[str],
261
+ *,
262
+ cwd: Path,
263
+ environment: Mapping[str, str],
264
+ policy: ExecutionPolicy,
265
+ ) -> InteractiveProcess:
266
+ """Spawn a trusted local process with live text pipes."""
267
+ enforced, preexec, creationflags = self._process_options(policy)
268
+ process = subprocess.Popen(
269
+ list(command),
270
+ cwd=cwd,
271
+ env=dict(environment),
272
+ stdin=subprocess.PIPE,
273
+ stdout=subprocess.PIPE,
274
+ stderr=subprocess.PIPE,
275
+ text=True,
276
+ encoding="utf-8",
277
+ errors="replace",
278
+ bufsize=1,
279
+ start_new_session=os.name != "nt",
280
+ creationflags=creationflags,
281
+ preexec_fn=preexec,
282
+ )
283
+ return _LocalInteractiveProcess(
284
+ process,
285
+ policy=policy,
286
+ enforced_policy_fields=tuple(enforced),
287
+ )
288
+
289
+ def execute(
290
+ self,
291
+ command: Sequence[str],
292
+ *,
293
+ cwd: Path,
294
+ environment: Mapping[str, str],
295
+ policy: ExecutionPolicy,
296
+ cancel: threading.Event | None = None,
297
+ ) -> BackendResult:
298
+ enforced, preexec, creationflags = self._process_options(policy)
299
+
300
+ started = time.monotonic()
301
+ process = subprocess.Popen(
302
+ list(command),
303
+ cwd=cwd,
304
+ env=dict(environment),
305
+ stdout=subprocess.PIPE,
306
+ stderr=subprocess.PIPE,
307
+ start_new_session=os.name != "nt",
308
+ creationflags=creationflags,
309
+ preexec_fn=preexec,
310
+ )
311
+ assert process.stdout is not None and process.stderr is not None
312
+ budget = _OutputBudget(policy.max_output_bytes)
313
+ stdout_chunks: list[bytes] = []
314
+ stderr_chunks: list[bytes] = []
315
+ readers = [
316
+ threading.Thread(target=_drain, args=(process.stdout, budget, stdout_chunks)),
317
+ threading.Thread(target=_drain, args=(process.stderr, budget, stderr_chunks)),
318
+ ]
319
+ for reader in readers:
320
+ reader.start()
321
+ timed_out = False
322
+ cancelled = False
323
+ while process.poll() is None:
324
+ if cancel is not None and cancel.is_set():
325
+ cancelled = True
326
+ self._stop(process)
327
+ break
328
+ if time.monotonic() - started >= policy.timeout_seconds:
329
+ timed_out = True
330
+ self._stop(process)
331
+ break
332
+ time.sleep(0.02)
333
+ try:
334
+ process.wait(timeout=2)
335
+ except subprocess.TimeoutExpired:
336
+ self._kill(process)
337
+ process.wait()
338
+ for reader in readers:
339
+ reader.join()
340
+ exit_code = 130 if cancelled else 124 if timed_out else int(process.returncode)
341
+ return BackendResult(
342
+ exit_code=exit_code,
343
+ stdout=b"".join(stdout_chunks).decode("utf-8", errors="replace"),
344
+ stderr=b"".join(stderr_chunks).decode("utf-8", errors="replace"),
345
+ elapsed_seconds=time.monotonic() - started,
346
+ timed_out=timed_out,
347
+ cancelled=cancelled,
348
+ output_truncated=budget.truncated,
349
+ enforced_policy_fields=tuple(enforced),
350
+ )
351
+
352
+ @staticmethod
353
+ def _stop(process: subprocess.Popen[Any]) -> None:
354
+ try:
355
+ if os.name == "nt":
356
+ process.terminate()
357
+ else:
358
+ getattr(os, "killpg")(process.pid, signal.SIGTERM) # noqa: B009
359
+ except ProcessLookupError:
360
+ pass
361
+
362
+ @staticmethod
363
+ def _kill(process: subprocess.Popen[Any]) -> None:
364
+ try:
365
+ if os.name == "nt":
366
+ process.kill()
367
+ else:
368
+ getattr(os, "killpg")( # noqa: B009
369
+ process.pid,
370
+ getattr(signal, "SIGKILL"), # noqa: B009
371
+ )
372
+ except ProcessLookupError:
373
+ pass
lean_runtime/cli.py ADDED
@@ -0,0 +1,254 @@
1
+ """Minimal command-line interface around the environment compiler."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import sys
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from .environments import ExecutionCapture
12
+ from .errors import LeanRuntimeError, MaterializationError, ResolutionError
13
+ from .events import RuntimeEvent
14
+ from .lockfiles import EnvironmentLock
15
+ from .models import ExecutionResult
16
+ from .policies import ExecutionPolicy
17
+ from .runtime import Runtime
18
+ from .specs import EnvironmentSpec
19
+
20
+
21
+ def _json(value: Any) -> None:
22
+ print(json.dumps(value, ensure_ascii=False, indent=2, sort_keys=True))
23
+
24
+
25
+ def _progress(event: RuntimeEvent) -> None:
26
+ package = f" [{event.data['package']}]" if "package" in event.data else ""
27
+ print(f"lean-runtime: {event.kind}{package}: {event.message}", file=sys.stderr)
28
+
29
+
30
+ def _cli_source_name(path: Path) -> str:
31
+ if path.is_absolute():
32
+ return path.name
33
+ return path.as_posix()
34
+
35
+
36
+ def _emit_result(result: ExecutionResult, as_json: bool) -> None:
37
+ if as_json:
38
+ _json(result.to_dict())
39
+ return
40
+ if result.stdout:
41
+ print(result.stdout, end="" if result.stdout.endswith("\n") else "\n")
42
+ if result.stderr:
43
+ print(result.stderr, end="" if result.stderr.endswith("\n") else "\n", file=sys.stderr)
44
+ status = "accepted" if result.ok else "rejected"
45
+ environment = f" environment={result.environment_id}" if result.environment_id else ""
46
+ print(
47
+ f"{status}:{environment} toolchain={result.toolchain} "
48
+ f"exit={result.exit_code} elapsed={result.elapsed_seconds:.3f}s"
49
+ )
50
+
51
+
52
+ def _policy(arguments: argparse.Namespace) -> ExecutionPolicy:
53
+ return ExecutionPolicy(
54
+ timeout_seconds=arguments.timeout,
55
+ max_output_bytes=arguments.max_output,
56
+ memory_mb=arguments.memory,
57
+ cpu_seconds=arguments.cpu,
58
+ network=arguments.network,
59
+ )
60
+
61
+
62
+ def _add_policy(parser: argparse.ArgumentParser, *, timeout: float = 120) -> None:
63
+ parser.add_argument("--timeout", type=float, default=timeout)
64
+ parser.add_argument("--max-output", type=int, default=1_000_000)
65
+ parser.add_argument("--memory", type=int, help="memory limit in MiB")
66
+ parser.add_argument("--cpu", type=int, help="CPU time limit in seconds")
67
+ parser.add_argument("--network", choices=("inherit", "disabled"), default="inherit")
68
+
69
+
70
+ def parser() -> argparse.ArgumentParser:
71
+ root = argparse.ArgumentParser(prog="lean-runtime")
72
+ root.add_argument("--home", help="runtime store root")
73
+ root.add_argument("--quiet", action="store_true", help="suppress progress events")
74
+ commands = root.add_subparsers(dest="command", required=True)
75
+
76
+ resolve = commands.add_parser("resolve", help="compile a TOML/JSON spec into a lock")
77
+ resolve.add_argument("spec", type=Path)
78
+ resolve.add_argument("--output", type=Path)
79
+ resolve.add_argument("--timeout", type=float, default=900)
80
+
81
+ ensure = commands.add_parser("ensure", help="build or reopen a locked environment")
82
+ ensure.add_argument("lock", type=Path)
83
+ ensure.add_argument("--name")
84
+
85
+ check = commands.add_parser(
86
+ "check", help="check with --with packages or in a published environment"
87
+ )
88
+ check.add_argument(
89
+ "inputs",
90
+ nargs="+",
91
+ help="FILE with --with, otherwise ENVIRONMENT FILE; FILE may be - for stdin",
92
+ )
93
+ check.add_argument(
94
+ "--with",
95
+ dest="package_refs",
96
+ action="append",
97
+ default=[],
98
+ metavar="REFERENCE",
99
+ help="repeatable github:owner/repository@tag-or-commit package reference",
100
+ )
101
+ check.add_argument("--toolchain", help="override the toolchain discovered from --with packages")
102
+ check.add_argument(
103
+ "--include", action="append", default=[], type=Path, help="additional Lean source file"
104
+ )
105
+ check.add_argument("--json", action="store_true")
106
+ _add_policy(check)
107
+
108
+ inspect = commands.add_parser("inspect", help="inspect a published environment")
109
+ inspect.add_argument("environment")
110
+ inspect.add_argument("--packages", action="store_true", help="include exact package locks")
111
+
112
+ commands.add_parser("env-list", help="list published environments")
113
+ commands.add_parser("cache-status", help="show cache counts and disk usage")
114
+ commands.add_parser("doctor", help="check local prerequisites and cache health")
115
+
116
+ replay = commands.add_parser("replay", help="replay a canonical execution capture")
117
+ replay.add_argument("capture", type=Path)
118
+ replay.add_argument("--json", action="store_true")
119
+
120
+ gc = commands.add_parser("gc", help="collect old unreferenced environments")
121
+ gc.add_argument("--execute", action="store_true", help="remove candidates; default is dry-run")
122
+ gc.add_argument("--minimum-age-hours", type=float, default=24 * 30)
123
+
124
+ raw = commands.add_parser("raw-check", help="check without a managed environment")
125
+ raw.add_argument("file", type=Path, help="Lean source file, or - for stdin")
126
+ raw.add_argument("--toolchain")
127
+ raw.add_argument("--project", type=Path)
128
+ raw.add_argument("--json", action="store_true")
129
+ _add_policy(raw)
130
+
131
+ build = commands.add_parser("project-build", help="build an existing Lake project")
132
+ build.add_argument("project", type=Path)
133
+ build.add_argument("targets", nargs="*")
134
+ build.add_argument("--toolchain")
135
+ build.add_argument("--timeout", type=float, default=900)
136
+ build.add_argument("--json", action="store_true")
137
+
138
+ install = commands.add_parser("install", help="install a Lean toolchain")
139
+ install.add_argument("toolchain")
140
+ return root
141
+
142
+
143
+ def main(argv: list[str] | None = None) -> int:
144
+ args = parser().parse_args(argv)
145
+ runtime = Runtime(home=args.home, on_event=None if args.quiet else _progress)
146
+ try:
147
+ if args.command == "install":
148
+ print(runtime.toolchains.ensure(args.toolchain))
149
+ return 0
150
+ if args.command == "resolve":
151
+ lock = runtime.resolve(EnvironmentSpec.load(args.spec), timeout=args.timeout)
152
+ if args.output:
153
+ lock.write(args.output)
154
+ print(lock.lock_id)
155
+ else:
156
+ _json(lock.to_dict())
157
+ return 0
158
+ if args.command == "ensure":
159
+ environment = runtime.ensure(EnvironmentLock.load(args.lock), name=args.name)
160
+ _json(environment.inspect().to_dict())
161
+ return 0
162
+ if args.command == "inspect":
163
+ environment = runtime.open(args.environment)
164
+ payload = environment.inspect().to_dict()
165
+ if args.packages:
166
+ payload["package_locks"] = [
167
+ package.to_dict() for package in environment.lock.packages
168
+ ]
169
+ _json(payload)
170
+ return 0
171
+ if args.command == "env-list":
172
+ _json(list(runtime.list_environments()))
173
+ return 0
174
+ if args.command == "cache-status":
175
+ _json(runtime.store_status().to_dict())
176
+ return 0
177
+ if args.command == "doctor":
178
+ doctor_report = runtime.doctor()
179
+ _json(doctor_report.to_dict())
180
+ return 0 if doctor_report.ok else 2
181
+ if args.command == "replay":
182
+ capture = ExecutionCapture.load(args.capture)
183
+ result = runtime.replay_capture(capture)
184
+ _emit_result(result, args.json)
185
+ if capture.expected_ok is not None and result.ok != capture.expected_ok:
186
+ return 1
187
+ return 0 if result.ok else 1
188
+ if args.command == "gc":
189
+ gc_report = runtime.gc(
190
+ dry_run=not args.execute,
191
+ minimum_age_seconds=args.minimum_age_hours * 3600,
192
+ )
193
+ _json(gc_report.to_dict())
194
+ return 0
195
+ if args.command == "check":
196
+ if args.package_refs:
197
+ if len(args.inputs) != 1:
198
+ raise ValueError("check with --with expects exactly one FILE")
199
+ environment = runtime.ensure_references(args.package_refs, toolchain=args.toolchain)
200
+ source_file = Path(args.inputs[0])
201
+ else:
202
+ if len(args.inputs) != 2:
203
+ raise ValueError("check expects ENVIRONMENT FILE, or FILE with --with")
204
+ if args.toolchain:
205
+ raise ValueError("check --toolchain is only valid with --with")
206
+ environment = runtime.open(args.inputs[0])
207
+ source_file = Path(args.inputs[1])
208
+ if str(source_file) == "-":
209
+ if args.include:
210
+ raise ValueError("stdin entrypoints cannot be combined with --include")
211
+ result = environment.check(sys.stdin.read(), policy=_policy(args))
212
+ else:
213
+ source_paths = [source_file, *args.include]
214
+ files = {_cli_source_name(path): path.read_text() for path in source_paths}
215
+ result = environment.check_files(
216
+ files,
217
+ entrypoint=_cli_source_name(source_file),
218
+ policy=_policy(args),
219
+ )
220
+ elif args.command == "raw-check":
221
+ source = sys.stdin.read() if str(args.file) == "-" else args.file.read_text()
222
+ result = runtime.check(
223
+ source,
224
+ toolchain=args.toolchain,
225
+ project=args.project,
226
+ policy=_policy(args),
227
+ )
228
+ else:
229
+ result = runtime.build(
230
+ args.project,
231
+ targets=args.targets,
232
+ toolchain=args.toolchain,
233
+ timeout=args.timeout,
234
+ )
235
+ except (ResolutionError, MaterializationError) as exc:
236
+ _json(
237
+ {
238
+ "error": str(exc),
239
+ "phase": exc.phase,
240
+ "command": list(exc.command),
241
+ "exit_code": exc.exit_code,
242
+ "output": exc.output,
243
+ }
244
+ )
245
+ return 2
246
+ except (LeanRuntimeError, OSError, UnicodeError, ValueError, json.JSONDecodeError) as exc:
247
+ print(f"lean-runtime: {exc}", file=sys.stderr)
248
+ return 2
249
+ _emit_result(result, args.json)
250
+ return 0 if result.ok else 1
251
+
252
+
253
+ if __name__ == "__main__":
254
+ raise SystemExit(main())