lean-runtime 2.1.0__tar.gz → 2.1.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.1.0 → lean_runtime-2.1.2}/CHANGELOG.md +30 -0
- {lean_runtime-2.1.0/lean_runtime.egg-info → lean_runtime-2.1.2}/PKG-INFO +1 -1
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/development.md +23 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/cli.py +8 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/engine.py +5 -4
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/environments.py +13 -3
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/oci.py +27 -5
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/references.py +14 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/run_cli.py +11 -2
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/runtime.py +2 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/SOURCES.txt +2 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/pyproject.toml +1 -1
- lean_runtime-2.1.2/scripts/announcement_gate.py +188 -0
- lean_runtime-2.1.2/scripts/registry_preflight.py +156 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_bundles.py +51 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_references.py +16 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/LICENSE +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/MANIFEST.in +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/README.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/environments.toml +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/compatibility/README.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/architecture.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/captures.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/case-study-v1.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/cli.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/environments.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/getting-started.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/index.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/local-projects.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/portable-copies.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/python-api.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/ready-programs.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/standalone-files.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/docs/v1-precision.md +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/examples/mathlib.toml +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/__init__.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/__main__.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/_git.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/_paths.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/backends.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/bundles.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/comparison.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/decisions.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/probe.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/result.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/errors.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/events.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/facade.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/health.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/lake.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/locking.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/matrix.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/models.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/policies.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/profiling.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/programs.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/projects.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/py.typed +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/resolver.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/serialization.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/specs.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/store.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/timings.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/toolchain_slim.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/verification.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime/wire.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/mkdocs.yml +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/scripts/run_compatibility.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/setup.cfg +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/conftest.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_engine.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_backend.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_cli.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_diagnostics.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_environment_integration.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_events_health.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_facade.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_frontmatter.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_git.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_interactive.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_lockfiles.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_models.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_multifile.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_paths.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_programs.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_projects.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_run_cli.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_runtime.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_schema_resources.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_schemas.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_signatures.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_specs.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_store.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_toolchain_slim.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_toolchains.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_v1_precision.py +0 -0
- {lean_runtime-2.1.0 → lean_runtime-2.1.2}/tests/test_verification_inventory.py +0 -0
|
@@ -1,5 +1,35 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.1.2 - 2026-08-12
|
|
4
|
+
|
|
5
|
+
- Raise `lean-run`'s default `--check-timeout` from 120 to 300 seconds: a
|
|
6
|
+
first Mathlib check legitimately takes 2-5 minutes on slower machines
|
|
7
|
+
today (instance staging plus Lake's full trace scan), and the old default
|
|
8
|
+
timed out authoritative checks that would have succeeded.
|
|
9
|
+
|
|
10
|
+
## 2.1.1 - 2026-08-12
|
|
11
|
+
|
|
12
|
+
- The discovery search budget now gates starting additional candidates and
|
|
13
|
+
no longer kills an in-flight authoritative check, which is bounded by its
|
|
14
|
+
own per-candidate timeout. A slow machine's first Mathlib check can outlive
|
|
15
|
+
the remaining search budget legitimately.
|
|
16
|
+
- OCI blob downloads keep partials on truncated transfers and resume with a
|
|
17
|
+
Range request, retry transport errors mid-stream, and attempt four times.
|
|
18
|
+
- Add `build-and-publish --accelerate`: hydrate artifacts from known package
|
|
19
|
+
caches (Mathlib's `lake exe cache get`) for locked packages that carry no
|
|
20
|
+
artifact command of their own. Acceleration is keyed by exact canonical
|
|
21
|
+
package URL, never changes the lock identity, is recorded in the hydration
|
|
22
|
+
report, and the built environment is still probe-verified before
|
|
23
|
+
publication. The public-cache workflow uses it, turning multi-hour Mathlib
|
|
24
|
+
source builds into minutes.
|
|
25
|
+
- Add a fail-closed release toolkit: `scripts/registry_preflight.py` verifies
|
|
26
|
+
every bundled catalog entry is anonymously downloadable, and the
|
|
27
|
+
announcement-gate workflow runs the advertised PyPI journeys on clean
|
|
28
|
+
unauthenticated Linux and macOS runners (dispatch + daily canary).
|
|
29
|
+
- `lean-run` now warns loudly when a downloadable environment is unavailable
|
|
30
|
+
and states that source builds can take 30+ minutes, with a
|
|
31
|
+
`--no-source-build` hint, instead of falling back silently.
|
|
32
|
+
|
|
3
33
|
## 2.1.0 - 2026-08-11
|
|
4
34
|
|
|
5
35
|
- Add `lean-runtime toolchain-slim` and
|
|
@@ -78,3 +78,26 @@ a normal publisher and creates the PyPI project.
|
|
|
78
78
|
PyPI does not permit replacing files for an already published version. If a
|
|
79
79
|
release needs a correction, increment the version rather than recreating the
|
|
80
80
|
tag.
|
|
81
|
+
|
|
82
|
+
## Release and announcement protocol
|
|
83
|
+
|
|
84
|
+
Catalog entries are public claims that a prebuilt environment is
|
|
85
|
+
downloadable. The protocol below makes those claims fail-closed:
|
|
86
|
+
|
|
87
|
+
1. Publish downloadable environments for every catalog entry and platform
|
|
88
|
+
(`public-cache.yml`) **before** shipping a wheel that references them, and
|
|
89
|
+
make the GHCR package publicly pullable.
|
|
90
|
+
2. Run `python scripts/registry_preflight.py` — it verifies, anonymously,
|
|
91
|
+
that every catalog lock resolves to an index, platform manifests, and
|
|
92
|
+
readable blobs. CI-cheap; run it on every release.
|
|
93
|
+
3. After publishing to PyPI, dispatch the **Announcement gate** workflow
|
|
94
|
+
(`announcement-gate.yml`). It installs from PyPI on clean unauthenticated
|
|
95
|
+
Linux and macOS runners and runs the advertised journeys from
|
|
96
|
+
`acceptance/`: a cold check with `--no-source-build` (fail-closed: a
|
|
97
|
+
broken registry fails in seconds instead of hiding behind a source
|
|
98
|
+
build), a warm re-check, a failing proof with logical diagnostic paths, a
|
|
99
|
+
lock round-trip, and the Python batch API. Release titles use `vX.Y.Z`,
|
|
100
|
+
matching the tag.
|
|
101
|
+
4. Announce only after the gate is green. The same workflow runs daily as a
|
|
102
|
+
canary: registry permissions and docs deployments can break without any
|
|
103
|
+
code change.
|
|
@@ -153,6 +153,13 @@ def parser() -> argparse.ArgumentParser:
|
|
|
153
153
|
action="store_true",
|
|
154
154
|
help="publish blobs and platform manifest without updating the lock index",
|
|
155
155
|
)
|
|
156
|
+
push.add_argument(
|
|
157
|
+
"--accelerate",
|
|
158
|
+
action="store_true",
|
|
159
|
+
help="hydrate artifacts from known package caches (e.g. Mathlib's) for locked "
|
|
160
|
+
"packages without an artifact command; the lock identity is unchanged and the "
|
|
161
|
+
"built environment is still probe-verified before publication",
|
|
162
|
+
)
|
|
156
163
|
push.add_argument("--sign", action="store_true", help="sign the published lock index")
|
|
157
164
|
push.add_argument(
|
|
158
165
|
"--attest", action="store_true", help="publish a signed source/probe attestation"
|
|
@@ -391,6 +398,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
391
398
|
EnvironmentLock.load(args.lock),
|
|
392
399
|
name=args.name,
|
|
393
400
|
build_timeout=args.timeout,
|
|
401
|
+
accelerate=args.accelerate,
|
|
394
402
|
)
|
|
395
403
|
_json(
|
|
396
404
|
runtime.publish_environment(
|
|
@@ -223,10 +223,11 @@ def discover(
|
|
|
223
223
|
else attempt.diagnostics,
|
|
224
224
|
)
|
|
225
225
|
remaining = deadline - time.monotonic()
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
226
|
+
# The search budget gates STARTING candidates (checked at the top of
|
|
227
|
+
# the loop); it must not kill an in-flight authoritative check, which
|
|
228
|
+
# is bounded by its own per-candidate timeout. A slow machine's first
|
|
229
|
+
# check may legitimately outlive the remaining search budget.
|
|
230
|
+
timeout = plan.policy.candidate_timeout_seconds or remaining
|
|
230
231
|
with _AttemptCancellation(timeout, cancel) as attempt_cancel:
|
|
231
232
|
try:
|
|
232
233
|
outcome = probe.check(
|
|
@@ -35,6 +35,7 @@ from .models import (
|
|
|
35
35
|
PhaseTiming,
|
|
36
36
|
)
|
|
37
37
|
from .policies import ExecutionPolicy
|
|
38
|
+
from .references import artifact_accelerators
|
|
38
39
|
from .serialization import sha256_id, write_json_atomic
|
|
39
40
|
from .store import (
|
|
40
41
|
EnvironmentStore,
|
|
@@ -1011,6 +1012,7 @@ class EnvironmentManager:
|
|
|
1011
1012
|
name: str | None = None,
|
|
1012
1013
|
build_profile: str = "release",
|
|
1013
1014
|
build_timeout: float = 1800,
|
|
1015
|
+
accelerate: bool = False,
|
|
1014
1016
|
cancel: threading.Event | None = None,
|
|
1015
1017
|
) -> Environment:
|
|
1016
1018
|
self.store.publish_lock(lock)
|
|
@@ -1030,6 +1032,7 @@ class EnvironmentManager:
|
|
|
1030
1032
|
destination,
|
|
1031
1033
|
build_profile,
|
|
1032
1034
|
build_timeout=build_timeout,
|
|
1035
|
+
accelerate=accelerate,
|
|
1033
1036
|
cancel=cancel,
|
|
1034
1037
|
)
|
|
1035
1038
|
else:
|
|
@@ -1075,6 +1078,7 @@ class EnvironmentManager:
|
|
|
1075
1078
|
build_profile: str,
|
|
1076
1079
|
*,
|
|
1077
1080
|
build_timeout: float,
|
|
1081
|
+
accelerate: bool = False,
|
|
1078
1082
|
cancel: threading.Event | None,
|
|
1079
1083
|
) -> None:
|
|
1080
1084
|
stage = self.store.environments / f".staging-{os.getpid()}-{uuid.uuid4().hex}"
|
|
@@ -1105,15 +1109,20 @@ class EnvironmentManager:
|
|
|
1105
1109
|
max_output_bytes=10_000_000,
|
|
1106
1110
|
)
|
|
1107
1111
|
hydration: list[dict[str, Any]] = []
|
|
1112
|
+
accelerators = artifact_accelerators() if accelerate else {}
|
|
1108
1113
|
for package in lock.packages:
|
|
1109
|
-
|
|
1114
|
+
requested_command = package.artifact_command or accelerators.get(package.url, ())
|
|
1115
|
+
if not requested_command:
|
|
1110
1116
|
continue
|
|
1117
|
+
accelerated = not package.artifact_command
|
|
1111
1118
|
self.events.emit(
|
|
1112
1119
|
"artifact.hydration_started",
|
|
1113
|
-
f"Hydrating artifacts for {package.name}"
|
|
1120
|
+
f"Hydrating artifacts for {package.name}"
|
|
1121
|
+
+ (" (accelerated)" if accelerated else ""),
|
|
1114
1122
|
package=package.name,
|
|
1123
|
+
accelerated=accelerated,
|
|
1115
1124
|
)
|
|
1116
|
-
command = list(
|
|
1125
|
+
command = list(requested_command)
|
|
1117
1126
|
if command[0] in {"lake", "lean"}:
|
|
1118
1127
|
command = self.toolchains.command(lock.toolchain, command[0], *command[1:])
|
|
1119
1128
|
result = self.backend.execute(
|
|
@@ -1127,6 +1136,7 @@ class EnvironmentManager:
|
|
|
1127
1136
|
{
|
|
1128
1137
|
"package": package.name,
|
|
1129
1138
|
"command": command,
|
|
1139
|
+
"accelerated": accelerated,
|
|
1130
1140
|
"exit_code": result.exit_code,
|
|
1131
1141
|
"output": result.stdout + result.stderr,
|
|
1132
1142
|
}
|
|
@@ -32,7 +32,7 @@ _BEARER_PARAMETER = re.compile(r'([a-zA-Z]+)="([^"]*)"')
|
|
|
32
32
|
_DIGEST = re.compile(r"sha256:([0-9a-f]{64})")
|
|
33
33
|
_ACCEPT = ", ".join((INDEX_MEDIA_TYPE, MANIFEST_MEDIA_TYPE))
|
|
34
34
|
DEFAULT_ENVIRONMENT_LIBRARIES = ("oci://ghcr.io/alerad/lean-runtime-cache",)
|
|
35
|
-
_BLOB_INTEGRITY_ATTEMPTS =
|
|
35
|
+
_BLOB_INTEGRITY_ATTEMPTS = 4
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
class SignatureVerifier(Protocol):
|
|
@@ -263,18 +263,30 @@ class OCIRegistryClient:
|
|
|
263
263
|
if written == size and observed_digest == digest:
|
|
264
264
|
temporary.replace(destination)
|
|
265
265
|
break
|
|
266
|
-
|
|
266
|
+
# A short read is a truncated transfer (registry or proxy
|
|
267
|
+
# closed the connection early): keep the partial so the
|
|
268
|
+
# next attempt resumes with a Range request instead of
|
|
269
|
+
# restarting a multi-gigabyte download from zero. Only a
|
|
270
|
+
# complete-but-mismatched blob is real corruption.
|
|
271
|
+
truncated = written < size
|
|
272
|
+
if not truncated:
|
|
273
|
+
temporary.unlink(missing_ok=True)
|
|
267
274
|
if attempt == _BLOB_INTEGRITY_ATTEMPTS:
|
|
275
|
+
temporary.unlink(missing_ok=True)
|
|
276
|
+
reason = "was truncated" if truncated else "failed digest verification"
|
|
268
277
|
raise EnvironmentError(
|
|
269
|
-
"downloaded OCI blob
|
|
278
|
+
f"downloaded OCI blob {reason} "
|
|
270
279
|
f"after {attempt} attempts (expected {digest}, got "
|
|
271
280
|
f"{observed_digest}, bytes {written}/{size})"
|
|
272
281
|
)
|
|
273
282
|
events.emit(
|
|
274
283
|
"library.layer_download_retry",
|
|
275
|
-
"
|
|
284
|
+
"Resuming truncated OCI blob"
|
|
285
|
+
if truncated
|
|
286
|
+
else "Retrying OCI blob after integrity verification failed",
|
|
276
287
|
digest=digest,
|
|
277
288
|
attempt=attempt + 1,
|
|
289
|
+
truncated=truncated,
|
|
278
290
|
observed_digest=observed_digest,
|
|
279
291
|
downloaded_bytes=written,
|
|
280
292
|
expected_bytes=size,
|
|
@@ -282,7 +294,17 @@ class OCIRegistryClient:
|
|
|
282
294
|
except urllib.error.HTTPError as exc:
|
|
283
295
|
raise DownloadUnavailable(f"OCI blob download failed: HTTP {exc.code}") from exc
|
|
284
296
|
except OSError as exc:
|
|
285
|
-
|
|
297
|
+
# Connection resets and read timeouts mid-transfer are
|
|
298
|
+
# retryable; the kept partial resumes on the next attempt.
|
|
299
|
+
if attempt == _BLOB_INTEGRITY_ATTEMPTS:
|
|
300
|
+
raise DownloadUnavailable(f"OCI blob download failed: {exc}") from exc
|
|
301
|
+
events.emit(
|
|
302
|
+
"library.layer_download_retry",
|
|
303
|
+
"Retrying OCI blob after a transport error",
|
|
304
|
+
digest=digest,
|
|
305
|
+
attempt=attempt + 1,
|
|
306
|
+
error=str(exc),
|
|
307
|
+
)
|
|
286
308
|
return destination
|
|
287
309
|
|
|
288
310
|
def blob_exists(self, digest: str) -> bool:
|
|
@@ -41,6 +41,20 @@ PACKAGE_ALIASES: dict[str, tuple[str, str, tuple[str, ...]]] = {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def artifact_accelerators() -> dict[str, tuple[str, ...]]:
|
|
45
|
+
"""Map canonical package URLs to their known artifact-cache commands.
|
|
46
|
+
|
|
47
|
+
Used by opt-in build acceleration for locks whose packages carry no
|
|
48
|
+
``artifact_command`` of their own. Keyed by exact URL so a fork that
|
|
49
|
+
merely shares a package name is never accelerated.
|
|
50
|
+
"""
|
|
51
|
+
return {
|
|
52
|
+
f"https://github.com/{owner}/{repository}.git": command
|
|
53
|
+
for owner, repository, command in PACKAGE_ALIASES.values()
|
|
54
|
+
if command
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
44
58
|
@dataclass(frozen=True, slots=True)
|
|
45
59
|
class PackageReference:
|
|
46
60
|
"""A human-friendly reference to one Git-hosted Lean package."""
|
|
@@ -36,9 +36,18 @@ def _progress(event: RuntimeEvent) -> None:
|
|
|
36
36
|
"library.layer_download_started": "Downloading cached environment",
|
|
37
37
|
"library.layer_download_retry": "Retrying cached-environment download",
|
|
38
38
|
"toolchain.install_started": "Installing Lean toolchain (a first run can take minutes)",
|
|
39
|
-
"environment.build_started": "Building environment"
|
|
39
|
+
"environment.build_started": "Building environment from source (large dependencies "
|
|
40
|
+
"such as Mathlib can take 30+ minutes; pass --no-source-build to fail fast instead)",
|
|
40
41
|
"environment.cache_hit": "Using local environment",
|
|
41
42
|
}
|
|
43
|
+
if event.kind == "availability.fallback":
|
|
44
|
+
library = event.data.get("library", "environment library")
|
|
45
|
+
reason = event.data.get("reason_code") or event.data.get("reason") or "unavailable"
|
|
46
|
+
print(
|
|
47
|
+
f"lean-run: WARNING: downloadable environment unavailable from {library} ({reason})",
|
|
48
|
+
file=sys.stderr,
|
|
49
|
+
)
|
|
50
|
+
return
|
|
42
51
|
message = messages.get(event.kind)
|
|
43
52
|
if message:
|
|
44
53
|
package = f" {event.data['reference']}" if "reference" in event.data else ""
|
|
@@ -97,7 +106,7 @@ def parser() -> argparse.ArgumentParser:
|
|
|
97
106
|
"--timeout",
|
|
98
107
|
dest="check_timeout",
|
|
99
108
|
type=float,
|
|
100
|
-
default=
|
|
109
|
+
default=300,
|
|
101
110
|
help="budget for one Lean invocation (--timeout is a deprecated alias)",
|
|
102
111
|
)
|
|
103
112
|
return root
|
|
@@ -171,6 +171,7 @@ class Runtime:
|
|
|
171
171
|
name: str | None = None,
|
|
172
172
|
build_profile: str = "release",
|
|
173
173
|
build_timeout: float = 1800,
|
|
174
|
+
accelerate: bool = False,
|
|
174
175
|
cancel: threading.Event | None = None,
|
|
175
176
|
) -> Environment:
|
|
176
177
|
environment_id = environment_identity(lock, build_profile)
|
|
@@ -209,6 +210,7 @@ class Runtime:
|
|
|
209
210
|
name=name,
|
|
210
211
|
build_profile=build_profile,
|
|
211
212
|
build_timeout=build_timeout,
|
|
213
|
+
accelerate=accelerate,
|
|
212
214
|
cancel=cancel,
|
|
213
215
|
)
|
|
214
216
|
|
|
@@ -104,6 +104,8 @@ schemas/inspect-v1.schema.json
|
|
|
104
104
|
schemas/matrix-v1.schema.json
|
|
105
105
|
schemas/profile-v1.schema.json
|
|
106
106
|
schemas/verify-v1.schema.json
|
|
107
|
+
scripts/announcement_gate.py
|
|
108
|
+
scripts/registry_preflight.py
|
|
107
109
|
scripts/run_compatibility.py
|
|
108
110
|
scripts/run_v1_case_study.py
|
|
109
111
|
scripts/smoke_wheel.py
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fail-closed announcement gate: the advertised user journeys, from clean state.
|
|
3
|
+
|
|
4
|
+
Runs the exact journeys a new user follows after ``pip install lean-runtime``
|
|
5
|
+
against an empty runtime home, with hard wall-clock timeouts, and writes one
|
|
6
|
+
JSON report. The critical property is fail-closed distribution: the first cold
|
|
7
|
+
check runs with ``--no-source-build``, so a broken or private registry fails
|
|
8
|
+
in seconds instead of degrading into a silent source build.
|
|
9
|
+
|
|
10
|
+
Steps:
|
|
11
|
+
1. cold check of acceptance/Main.lean with --no-source-build and --lock-out
|
|
12
|
+
2. warm re-check (must be fast)
|
|
13
|
+
3. acceptance/Fail.lean must be rejected with a diagnostic naming Fail.lean
|
|
14
|
+
4. re-check via the emitted lock (reproducibility)
|
|
15
|
+
5. the documented Python batch API against the same lock
|
|
16
|
+
|
|
17
|
+
Run it on a machine WITHOUT registry credentials; the point is proving the
|
|
18
|
+
anonymous path. The runtime home is created fresh; pass --keep-home to retain
|
|
19
|
+
it for inspection.
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
python scripts/announcement_gate.py --report gate-report.json
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import argparse
|
|
28
|
+
import json
|
|
29
|
+
import shutil
|
|
30
|
+
import subprocess
|
|
31
|
+
import sys
|
|
32
|
+
import time
|
|
33
|
+
from pathlib import Path
|
|
34
|
+
|
|
35
|
+
ACCEPTANCE = Path(__file__).resolve().parent.parent / "acceptance"
|
|
36
|
+
|
|
37
|
+
COLD_TIMEOUT = 2700 # toolchain download + environment download, no builds
|
|
38
|
+
WARM_TIMEOUT = 300
|
|
39
|
+
WARM_LIMIT = 240 # warm Mathlib checks include a full Lake trace scan today
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def run(
|
|
43
|
+
command: list[str],
|
|
44
|
+
*,
|
|
45
|
+
timeout: float,
|
|
46
|
+
env: dict[str, str],
|
|
47
|
+
) -> tuple[int, str, float]:
|
|
48
|
+
started = time.monotonic()
|
|
49
|
+
try:
|
|
50
|
+
completed = subprocess.run(
|
|
51
|
+
command,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
timeout=timeout,
|
|
55
|
+
env=env,
|
|
56
|
+
check=False,
|
|
57
|
+
)
|
|
58
|
+
output = completed.stdout + completed.stderr
|
|
59
|
+
return completed.returncode, output, time.monotonic() - started
|
|
60
|
+
except subprocess.TimeoutExpired as error:
|
|
61
|
+
output = (error.stdout or "") + (error.stderr or "")
|
|
62
|
+
if isinstance(output, bytes): # pragma: no cover - platform dependent
|
|
63
|
+
output = output.decode(errors="replace")
|
|
64
|
+
return -1, f"TIMEOUT after {timeout}s\n{output}", time.monotonic() - started
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def main() -> int:
|
|
68
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
69
|
+
parser.add_argument("--home", default=None, help="runtime home (default: fresh temp dir)")
|
|
70
|
+
parser.add_argument("--report", default="announcement-gate-report.json")
|
|
71
|
+
parser.add_argument("--keep-home", action="store_true")
|
|
72
|
+
parser.add_argument("--cold-timeout", type=float, default=COLD_TIMEOUT)
|
|
73
|
+
arguments = parser.parse_args()
|
|
74
|
+
|
|
75
|
+
import os
|
|
76
|
+
import tempfile
|
|
77
|
+
|
|
78
|
+
home = Path(arguments.home) if arguments.home else Path(tempfile.mkdtemp(prefix="gate-home-"))
|
|
79
|
+
home.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
if any(home.iterdir()):
|
|
81
|
+
print(f"refusing to run: home is not empty: {home}")
|
|
82
|
+
return 2
|
|
83
|
+
env = {**os.environ, "LEAN_RUNTIME_HOME": str(home)}
|
|
84
|
+
lock_path = home / "gate.lock.json"
|
|
85
|
+
main_lean = str(ACCEPTANCE / "Main.lean")
|
|
86
|
+
fail_lean = str(ACCEPTANCE / "Fail.lean")
|
|
87
|
+
|
|
88
|
+
report: dict[str, object] = {"home": str(home), "steps": [], "ok": False}
|
|
89
|
+
steps: list[dict[str, object]] = report["steps"] # type: ignore[assignment]
|
|
90
|
+
|
|
91
|
+
def step(
|
|
92
|
+
name: str,
|
|
93
|
+
command: list[str],
|
|
94
|
+
*,
|
|
95
|
+
timeout: float,
|
|
96
|
+
expect_exit: int = 0,
|
|
97
|
+
require: str | None = None,
|
|
98
|
+
forbid: str | None = None,
|
|
99
|
+
limit: float | None = None,
|
|
100
|
+
) -> bool:
|
|
101
|
+
code, output, elapsed = run(command, timeout=timeout, env=env)
|
|
102
|
+
ok = code == expect_exit
|
|
103
|
+
detail = ""
|
|
104
|
+
if ok and require is not None and require not in output:
|
|
105
|
+
ok, detail = False, f"missing expected text: {require!r}"
|
|
106
|
+
if ok and forbid is not None and forbid in output:
|
|
107
|
+
ok, detail = False, f"contains forbidden text: {forbid!r}"
|
|
108
|
+
if ok and limit is not None and elapsed > limit:
|
|
109
|
+
ok, detail = False, f"took {elapsed:.1f}s, limit {limit}s"
|
|
110
|
+
steps.append(
|
|
111
|
+
{
|
|
112
|
+
"name": name,
|
|
113
|
+
"command": command,
|
|
114
|
+
"exit_code": code,
|
|
115
|
+
"elapsed_seconds": round(elapsed, 2),
|
|
116
|
+
"ok": ok,
|
|
117
|
+
"detail": detail or None,
|
|
118
|
+
"tail": output[-2000:],
|
|
119
|
+
}
|
|
120
|
+
)
|
|
121
|
+
suffix = f": {detail}" if detail else ""
|
|
122
|
+
print(f"{'ok ' if ok else 'FAIL'} {name} ({elapsed:.1f}s){suffix}")
|
|
123
|
+
return ok
|
|
124
|
+
|
|
125
|
+
version = subprocess.run(
|
|
126
|
+
[
|
|
127
|
+
sys.executable,
|
|
128
|
+
"-c",
|
|
129
|
+
"import lean_runtime, importlib.metadata as m; print(m.version('lean-runtime'))",
|
|
130
|
+
],
|
|
131
|
+
capture_output=True,
|
|
132
|
+
text=True,
|
|
133
|
+
check=False,
|
|
134
|
+
).stdout.strip()
|
|
135
|
+
report["lean_runtime_version"] = version
|
|
136
|
+
print(f"gate: lean-runtime {version}, home {home}")
|
|
137
|
+
|
|
138
|
+
ok = step(
|
|
139
|
+
"cold-no-source-build",
|
|
140
|
+
[
|
|
141
|
+
"lean-run",
|
|
142
|
+
main_lean,
|
|
143
|
+
"--no-source-build",
|
|
144
|
+
"--lock-out",
|
|
145
|
+
str(lock_path),
|
|
146
|
+
],
|
|
147
|
+
timeout=arguments.cold_timeout,
|
|
148
|
+
)
|
|
149
|
+
ok = ok and step(
|
|
150
|
+
"warm",
|
|
151
|
+
["lean-run", main_lean],
|
|
152
|
+
timeout=WARM_TIMEOUT,
|
|
153
|
+
limit=WARM_LIMIT,
|
|
154
|
+
)
|
|
155
|
+
ok = ok and step(
|
|
156
|
+
"failing-proof-diagnostics",
|
|
157
|
+
["lean-run", fail_lean, "--lock", str(lock_path)],
|
|
158
|
+
timeout=WARM_TIMEOUT,
|
|
159
|
+
expect_exit=1,
|
|
160
|
+
require="Fail.lean",
|
|
161
|
+
forbid="instance-",
|
|
162
|
+
)
|
|
163
|
+
ok = ok and step(
|
|
164
|
+
"lock-reproducibility",
|
|
165
|
+
["lean-run", main_lean, "--lock", str(lock_path)],
|
|
166
|
+
timeout=WARM_TIMEOUT,
|
|
167
|
+
limit=WARM_LIMIT,
|
|
168
|
+
)
|
|
169
|
+
ok = ok and step(
|
|
170
|
+
"python-batch-api",
|
|
171
|
+
[sys.executable, str(ACCEPTANCE / "python_api.py"), str(lock_path)],
|
|
172
|
+
timeout=900,
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
usage = shutil.disk_usage(home)
|
|
176
|
+
report["ok"] = ok
|
|
177
|
+
report["home_bytes"] = sum(f.stat().st_size for f in home.rglob("*") if f.is_file())
|
|
178
|
+
report["disk_free_bytes"] = usage.free
|
|
179
|
+
Path(arguments.report).write_text(json.dumps(report, indent=2), encoding="utf-8")
|
|
180
|
+
print(f"report: {arguments.report}")
|
|
181
|
+
if not arguments.keep_home and arguments.home is None:
|
|
182
|
+
shutil.rmtree(home, ignore_errors=True)
|
|
183
|
+
print("GATE PASSED" if ok else "GATE FAILED")
|
|
184
|
+
return 0 if ok else 1
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
if __name__ == "__main__":
|
|
188
|
+
sys.exit(main())
|