lean-runtime 2.9.2__tar.gz → 2.10.0__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.9.2 → lean_runtime-2.10.0}/CHANGELOG.md +16 -0
- {lean_runtime-2.9.2/lean_runtime.egg-info → lean_runtime-2.10.0}/PKG-INFO +1 -1
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/local-projects.md +13 -4
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/cli.py +165 -20
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/console.py +7 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/header_cache.py +109 -26
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/locking.py +50 -1
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/models.py +4 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/project_execution.py +99 -30
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/runtime.py +1 -1
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/shared_projects.py +33 -2
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/wire.py +18 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime.egg-info/SOURCES.txt +2 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/pyproject.toml +1 -1
- lean_runtime-2.10.0/schemas/check-batch-v1.schema.json +36 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/execution-v1.schema.json +1 -1
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_cli.py +173 -0
- lean_runtime-2.10.0/tests/test_header_cache.py +329 -0
- lean_runtime-2.10.0/tests/test_locking.py +59 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_schemas.py +6 -0
- lean_runtime-2.9.2/tests/test_header_cache.py +0 -44
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/LICENSE +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/MANIFEST.in +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/README.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/environments.toml +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/compatibility/README.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/compatibility/mathlib-4.33.0.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/compatibility/mathlib-4.33.0.toml +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/architecture.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/captures.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/case-study-v1.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/cli.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/development.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/environments.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/getting-started.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/index.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/lake-artifact-cache-design.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/portable-copies.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/project-publishing.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/python-api.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/ready-programs.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/standalone-files.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/trust-and-limitations.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/docs/v1-precision.md +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/examples/mathlib.toml +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/__init__.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/__main__.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/_git.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/_paths.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/backends.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/bundles.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/capsules.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/comparison.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/decisions.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/engine.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/probe.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/result.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/environments.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/errors.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/events.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/facade.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/health.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/identifier_resolver.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/lake.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/lake_cache.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/matrix.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/oci.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/oci_protocol.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/packs.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/policies.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/profiling.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/programs.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/project_sharing.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/projects.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/py.typed +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/references.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/resolver.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/run_cli.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/serialization.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/specs.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/store.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/timings.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/toolchain_oci.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/toolchain_slim.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime/verification.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/mkdocs.yml +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/publication-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/scripts/announcement_gate.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/scripts/lake_cache_gate.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/scripts/registry_preflight.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/scripts/run_compatibility.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/setup.cfg +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/conftest.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_engine.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_backend.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_bundles.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_capsules.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_console.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_diagnostics.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_environment_integration.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_events_health.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_facade.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_frontmatter.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_git.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_identifier_resolver.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_interactive.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_lake_cache.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_lockfiles.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_models.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_multifile.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_oci_protocol.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_packs.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_paths.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_programs.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_project_publish.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_projects.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_references.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_run_cli.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_runtime.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_schema_resources.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_signatures.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_specs.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_store.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_toolchain_oci.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_toolchain_slim.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_toolchains.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_v1_precision.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_verification_inventory.py +0 -0
- {lean_runtime-2.9.2 → lean_runtime-2.10.0}/tests/test_verification_probe.py +0 -0
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 2.10.0 - 2026-08-16
|
|
6
|
+
|
|
7
|
+
- Make header snapshots opt-in (`LEAN_RUNTIME_HEADER_SNAPSHOTS=1`; `--watch` and
|
|
8
|
+
`--repeat` enable them automatically), key them by module identity, load
|
|
9
|
+
existing snapshots without holding the creation lock, cancel lock waits
|
|
10
|
+
promptly, and retry once without a snapshot—quarantining it—when a snapshot
|
|
11
|
+
check times out or reports snapshot errors.
|
|
12
|
+
- Support `check FILE...` and `check DIRECTORY...` with independent per-file
|
|
13
|
+
results, `--concurrency`, a `lean-runtime.check-batch/v1` JSON envelope, and
|
|
14
|
+
an explicit `--environment NAME`; the legacy positional `ENVIRONMENT FILE`
|
|
15
|
+
form now applies only when the first argument is not an existing path.
|
|
16
|
+
- Attribute shared workspace lock waits to their holder (PID and operation),
|
|
17
|
+
announce header snapshot waits, and record `workspace_lock` and
|
|
18
|
+
`header_snapshot` phase timings so coordination cost is visible next to the
|
|
19
|
+
actual Lean check time.
|
|
20
|
+
|
|
5
21
|
## 2.9.2 - 2026-08-16
|
|
6
22
|
|
|
7
23
|
- Keep one fail-closed publication authentication session from push preflight through
|
|
@@ -68,10 +68,19 @@ stays silent. Only root-project outputs enter this cache: locked dependencies
|
|
|
68
68
|
continue to use the verified shared workspace, avoiding a second multi-gigabyte
|
|
69
69
|
copy. `build` retains Lake's complete target semantics.
|
|
70
70
|
|
|
71
|
-
Repeated file
|
|
72
|
-
toolchain, project identity, and import block. Body-only
|
|
73
|
-
reloading the unchanged import environment.
|
|
74
|
-
|
|
71
|
+
Repeated checks of one file can reuse Lean's native header snapshots, keyed by
|
|
72
|
+
the exact toolchain, project identity, module, and import block. Body-only
|
|
73
|
+
edits therefore avoid reloading the unchanged import environment. Snapshots are
|
|
74
|
+
used automatically by `--watch` and `--repeat`; set
|
|
75
|
+
`LEAN_RUNTIME_HEADER_SNAPSHOTS=1` to enable them for one-shot checks (or `=0`
|
|
76
|
+
to disable them everywhere). A check that fails or times out under a snapshot
|
|
77
|
+
is retried once without it, and the snapshot is discarded. `--watch` is a thin
|
|
78
|
+
save loop over the same ordinary check/result contract. `check` also accepts
|
|
79
|
+
several files or directories and checks each independently, in parallel with
|
|
80
|
+
`--concurrency`. When one invocation must wait on another, the console names
|
|
81
|
+
the holder (for example `Waiting for shared workspace lock held by PID 512
|
|
82
|
+
(shared build of mathlib)`), and `--timings` reports `workspace_lock` and
|
|
83
|
+
`header_snapshot` phases alongside the actual Lean execution time. Failed checks may add exact-graph
|
|
75
84
|
identifier hints derived from pinned `.ilean` indexes; raw Lean streams remain
|
|
76
85
|
unchanged.
|
|
77
86
|
|
|
@@ -8,6 +8,7 @@ import os
|
|
|
8
8
|
import re
|
|
9
9
|
import sys
|
|
10
10
|
import time
|
|
11
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
11
12
|
from dataclasses import replace
|
|
12
13
|
from datetime import datetime
|
|
13
14
|
from importlib.metadata import version as distribution_version
|
|
@@ -24,6 +25,7 @@ from .errors import (
|
|
|
24
25
|
ResolutionError,
|
|
25
26
|
)
|
|
26
27
|
from .events import RuntimeEvent
|
|
28
|
+
from .header_cache import ENABLE_VARIABLE as _HEADER_SNAPSHOTS_VARIABLE
|
|
27
29
|
from .health import DoctorReport
|
|
28
30
|
from .lake import ROOT_MODULE
|
|
29
31
|
from .lockfiles import EnvironmentLock
|
|
@@ -40,6 +42,7 @@ from .timings import render_timings
|
|
|
40
42
|
from .wire import (
|
|
41
43
|
envelope,
|
|
42
44
|
error,
|
|
45
|
+
serialize_check_batch_v1,
|
|
43
46
|
serialize_comparison_v1,
|
|
44
47
|
serialize_execution_v1,
|
|
45
48
|
serialize_matrix_v1,
|
|
@@ -343,6 +346,82 @@ def _cli_source_name(path: Path) -> str:
|
|
|
343
346
|
return path.as_posix()
|
|
344
347
|
|
|
345
348
|
|
|
349
|
+
def _expand_check_inputs(inputs: list[str]) -> list[Path]:
|
|
350
|
+
"""Expand FILE and DIRECTORY inputs into an ordered, deduplicated file list."""
|
|
351
|
+
files: list[Path] = []
|
|
352
|
+
for raw in inputs:
|
|
353
|
+
path = Path(raw)
|
|
354
|
+
if path.is_dir():
|
|
355
|
+
found: list[Path] = []
|
|
356
|
+
for current, directories, filenames in os.walk(path):
|
|
357
|
+
directories[:] = sorted(name for name in directories if not name.startswith("."))
|
|
358
|
+
found.extend(
|
|
359
|
+
Path(current) / name for name in sorted(filenames) if name.endswith(".lean")
|
|
360
|
+
)
|
|
361
|
+
if not found:
|
|
362
|
+
raise ValueError(f"no Lean files found under directory: {path}")
|
|
363
|
+
files.extend(found)
|
|
364
|
+
elif path.is_file():
|
|
365
|
+
files.append(path)
|
|
366
|
+
else:
|
|
367
|
+
raise ValueError(f"check input does not exist: {path}")
|
|
368
|
+
return list(dict.fromkeys(files))
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _run_check_batch(runtime: Runtime, files: list[Path], args: argparse.Namespace) -> int:
|
|
372
|
+
"""Check each file independently, optionally in parallel, and report per file."""
|
|
373
|
+
if args.concurrency < 1 or args.concurrency > 32:
|
|
374
|
+
raise ValueError("check concurrency must be between 1 and 32")
|
|
375
|
+
environment = runtime.environment(args.environment) if args.environment else None
|
|
376
|
+
|
|
377
|
+
def execute(path: Path) -> ExecutionResult:
|
|
378
|
+
if environment is not None:
|
|
379
|
+
name = _cli_source_name(path)
|
|
380
|
+
return environment.check_files(
|
|
381
|
+
{name: path.read_text(encoding="utf-8")}, entrypoint=name, policy=_policy(args)
|
|
382
|
+
)
|
|
383
|
+
return runtime.check_file(
|
|
384
|
+
path, toolchain=args.toolchain, project=args.project, policy=_policy(args)
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
started = time.monotonic()
|
|
388
|
+
results: dict[Path, ExecutionResult] = {}
|
|
389
|
+
if args.concurrency == 1:
|
|
390
|
+
for path in files:
|
|
391
|
+
results[path] = execute(path)
|
|
392
|
+
else:
|
|
393
|
+
with ThreadPoolExecutor(
|
|
394
|
+
max_workers=args.concurrency, thread_name_prefix="lean-check"
|
|
395
|
+
) as pool:
|
|
396
|
+
futures = {pool.submit(execute, path): path for path in files}
|
|
397
|
+
try:
|
|
398
|
+
for future in as_completed(futures):
|
|
399
|
+
results[futures[future]] = future.result()
|
|
400
|
+
except BaseException:
|
|
401
|
+
for future in futures:
|
|
402
|
+
future.cancel()
|
|
403
|
+
raise
|
|
404
|
+
entries = [(str(path), results[path]) for path in files]
|
|
405
|
+
runtime.events.emit(
|
|
406
|
+
"check.completed",
|
|
407
|
+
"Lean check completed",
|
|
408
|
+
ok=all(result.ok for _, result in entries),
|
|
409
|
+
)
|
|
410
|
+
if args.json:
|
|
411
|
+
_json(serialize_check_batch_v1(entries, time.monotonic() - started))
|
|
412
|
+
else:
|
|
413
|
+
for display, result in entries:
|
|
414
|
+
status = "accepted" if result.ok else "rejected"
|
|
415
|
+
print(f"{display}\t{status}\t{result.elapsed_seconds:.2f}s")
|
|
416
|
+
if not result.ok:
|
|
417
|
+
output = "\n".join(part for part in (result.stdout, result.stderr) if part).strip()
|
|
418
|
+
if output:
|
|
419
|
+
print(output, file=sys.stderr)
|
|
420
|
+
accepted = sum(1 for _, result in entries if result.ok)
|
|
421
|
+
print(f"{accepted}/{len(entries)} accepted")
|
|
422
|
+
return 0 if all(result.ok for _, result in entries) else 1
|
|
423
|
+
|
|
424
|
+
|
|
346
425
|
def _display_result_text(text: str, result: ExecutionResult, display_path: str | None) -> str:
|
|
347
426
|
"""Rewrite only the staged entrypoint when the CLI has a logical input name."""
|
|
348
427
|
if display_path is None or not result.command:
|
|
@@ -657,11 +736,36 @@ Run `lean-runtime COMMAND --help` for examples and options.""",
|
|
|
657
736
|
program_index.add_argument("--tag", action="append", default=[])
|
|
658
737
|
program_index.add_argument("--sign", action="store_true")
|
|
659
738
|
|
|
660
|
-
check = commands.add_parser(
|
|
739
|
+
check = commands.add_parser(
|
|
740
|
+
"check",
|
|
741
|
+
help="check Lean files or all local libraries",
|
|
742
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
743
|
+
epilog="""examples:
|
|
744
|
+
lean-runtime check check every local library of this project
|
|
745
|
+
lean-runtime check Foo.lean check one file in its discovered project
|
|
746
|
+
lean-runtime check Foo.lean Bar.lean check several files, one result per file
|
|
747
|
+
lean-runtime check src --concurrency 4 check every *.lean under src in parallel
|
|
748
|
+
lean-runtime check Foo.lean --project ~/p check a file against an explicit Lake project
|
|
749
|
+
lean-runtime check --environment NAME Foo.lean
|
|
750
|
+
check inside a published environment
|
|
751
|
+
lean-runtime check Foo.lean --with github:owner/repo@v1.0
|
|
752
|
+
check against exact package references
|
|
753
|
+
lean-runtime check Foo.lean --timeout 60 bound one Lean invocation to 60 seconds
|
|
754
|
+
lean-runtime --timings check Foo.lean append stable phase timings to the result
|
|
755
|
+
|
|
756
|
+
Multi-file checks exit nonzero if any file is rejected. Header snapshots for
|
|
757
|
+
warm re-checks are used by --watch and --repeat automatically; set
|
|
758
|
+
LEAN_RUNTIME_HEADER_SNAPSHOTS=1 to enable them for one-shot checks.""",
|
|
759
|
+
)
|
|
661
760
|
check.add_argument(
|
|
662
761
|
"inputs",
|
|
663
762
|
nargs="*",
|
|
664
|
-
help="FILE
|
|
763
|
+
help="FILE..., DIRECTORY..., legacy ENVIRONMENT FILE, or omit inside a Lake "
|
|
764
|
+
"project; FILE may be -",
|
|
765
|
+
)
|
|
766
|
+
check.add_argument(
|
|
767
|
+
"--environment",
|
|
768
|
+
help="check the given FILEs inside this published environment",
|
|
665
769
|
)
|
|
666
770
|
check.add_argument(
|
|
667
771
|
"--with",
|
|
@@ -686,7 +790,9 @@ Run `lean-runtime COMMAND --help` for examples and options.""",
|
|
|
686
790
|
check.add_argument("--repeat", type=int, help="measure N repeated checks of one project file")
|
|
687
791
|
check.add_argument("--warmup", type=int, default=1, help="warmups before --repeat samples")
|
|
688
792
|
check.add_argument("--across", type=Path, help="check FILE across contexts in CONFIGURATION")
|
|
689
|
-
check.add_argument(
|
|
793
|
+
check.add_argument(
|
|
794
|
+
"--concurrency", type=int, default=1, help="workers for --across and multi-file checks"
|
|
795
|
+
)
|
|
690
796
|
_add_policy(check)
|
|
691
797
|
|
|
692
798
|
inspect = commands.add_parser("inspect", help="inspect a published environment")
|
|
@@ -1554,7 +1660,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1554
1660
|
return 0
|
|
1555
1661
|
if args.command == "check":
|
|
1556
1662
|
if args.across is not None:
|
|
1557
|
-
if
|
|
1663
|
+
if (
|
|
1664
|
+
len(args.inputs) != 1
|
|
1665
|
+
or args.package_refs
|
|
1666
|
+
or args.include
|
|
1667
|
+
or args.project
|
|
1668
|
+
or args.environment
|
|
1669
|
+
):
|
|
1558
1670
|
raise ValueError("check --across requires exactly one FILE")
|
|
1559
1671
|
across_file = Path(args.inputs[0])
|
|
1560
1672
|
contexts = load_matrix(args.across)
|
|
@@ -1589,6 +1701,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1589
1701
|
"check --repeat requires positive samples and nonnegative warmups"
|
|
1590
1702
|
)
|
|
1591
1703
|
repeated_file = Path(args.inputs[0])
|
|
1704
|
+
if os.environ.get(_HEADER_SNAPSHOTS_VARIABLE) is None:
|
|
1705
|
+
runtime.header_cache.enabled = True
|
|
1592
1706
|
for _ in range(args.warmup):
|
|
1593
1707
|
warm = runtime.check_file(
|
|
1594
1708
|
repeated_file, project=args.project, policy=_policy(args)
|
|
@@ -1626,6 +1740,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1626
1740
|
if not watched.is_file():
|
|
1627
1741
|
raise ValueError(f"watched Lean file does not exist: {watched}")
|
|
1628
1742
|
print(f"Watching {watched} · Ctrl-C to stop")
|
|
1743
|
+
if os.environ.get(_HEADER_SNAPSHOTS_VARIABLE) is None:
|
|
1744
|
+
runtime.header_cache.enabled = True
|
|
1629
1745
|
previous: tuple[int, int] | None = None
|
|
1630
1746
|
while True:
|
|
1631
1747
|
stat = watched.stat()
|
|
@@ -1650,9 +1766,24 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1650
1766
|
runtime.events.emit("check.started", "Checking Lean input", subject=check_subject)
|
|
1651
1767
|
if args.project is not None and args.package_refs:
|
|
1652
1768
|
raise ValueError("check cannot combine --project with --with")
|
|
1769
|
+
if args.environment is not None and (
|
|
1770
|
+
args.package_refs or args.project or args.toolchain
|
|
1771
|
+
):
|
|
1772
|
+
raise ValueError(
|
|
1773
|
+
"check --environment cannot be combined with --with, --project, or --toolchain"
|
|
1774
|
+
)
|
|
1775
|
+
legacy_environment = (
|
|
1776
|
+
args.environment is None
|
|
1777
|
+
and not args.package_refs
|
|
1778
|
+
and len(args.inputs) == 2
|
|
1779
|
+
and args.inputs[0] != "-"
|
|
1780
|
+
and not Path(args.inputs[0]).exists()
|
|
1781
|
+
)
|
|
1653
1782
|
if not args.inputs:
|
|
1654
1783
|
if args.package_refs or args.include:
|
|
1655
1784
|
raise ValueError("project-wide check does not accept --with or --include")
|
|
1785
|
+
if args.environment is not None:
|
|
1786
|
+
raise ValueError("check --environment requires at least one FILE")
|
|
1656
1787
|
result = runtime.check_project(
|
|
1657
1788
|
args.project or Path("."), toolchain=args.toolchain, policy=_policy(args)
|
|
1658
1789
|
)
|
|
@@ -1662,11 +1793,22 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1662
1793
|
raise ValueError("check with --with expects exactly one FILE")
|
|
1663
1794
|
environment = runtime.open_references(args.package_refs, toolchain=args.toolchain)
|
|
1664
1795
|
source_file = Path(args.inputs[0])
|
|
1665
|
-
elif
|
|
1666
|
-
if args.
|
|
1667
|
-
raise ValueError("
|
|
1668
|
-
|
|
1669
|
-
|
|
1796
|
+
elif legacy_environment:
|
|
1797
|
+
if args.toolchain:
|
|
1798
|
+
raise ValueError("check --toolchain is only valid with --with")
|
|
1799
|
+
if args.project:
|
|
1800
|
+
raise ValueError("check --project is only valid with a single FILE")
|
|
1801
|
+
environment = runtime.environment(args.inputs[0])
|
|
1802
|
+
source_file = Path(args.inputs[1])
|
|
1803
|
+
elif "-" in args.inputs:
|
|
1804
|
+
if len(args.inputs) != 1:
|
|
1805
|
+
raise ValueError("stdin (-) must be the only check input")
|
|
1806
|
+
if args.environment is not None:
|
|
1807
|
+
environment = runtime.environment(args.environment)
|
|
1808
|
+
source_file = Path("-")
|
|
1809
|
+
else:
|
|
1810
|
+
if args.include:
|
|
1811
|
+
raise ValueError("local project checks do not accept --include")
|
|
1670
1812
|
display_path = "<stdin>"
|
|
1671
1813
|
result = runtime.check(
|
|
1672
1814
|
sys.stdin.read(),
|
|
@@ -1674,23 +1816,26 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1674
1816
|
project=args.project,
|
|
1675
1817
|
policy=_policy(args),
|
|
1676
1818
|
)
|
|
1819
|
+
source_file = None
|
|
1820
|
+
else:
|
|
1821
|
+
selected_files = _expand_check_inputs(args.inputs)
|
|
1822
|
+
if len(selected_files) > 1:
|
|
1823
|
+
if args.include:
|
|
1824
|
+
raise ValueError("multi-file checks do not accept --include")
|
|
1825
|
+
return _run_check_batch(runtime, selected_files, args)
|
|
1826
|
+
if args.environment is not None:
|
|
1827
|
+
environment = runtime.environment(args.environment)
|
|
1828
|
+
source_file = selected_files[0]
|
|
1677
1829
|
else:
|
|
1830
|
+
if args.include:
|
|
1831
|
+
raise ValueError("local project checks do not accept --include")
|
|
1678
1832
|
result = runtime.check_file(
|
|
1679
|
-
|
|
1833
|
+
selected_files[0],
|
|
1680
1834
|
toolchain=args.toolchain,
|
|
1681
1835
|
project=args.project,
|
|
1682
1836
|
policy=_policy(args),
|
|
1683
1837
|
)
|
|
1684
|
-
|
|
1685
|
-
else:
|
|
1686
|
-
if len(args.inputs) != 2:
|
|
1687
|
-
raise ValueError("check expects FILE, ENVIRONMENT FILE, or FILE with --with")
|
|
1688
|
-
if args.toolchain:
|
|
1689
|
-
raise ValueError("check --toolchain is only valid with --with")
|
|
1690
|
-
if args.project:
|
|
1691
|
-
raise ValueError("check --project is only valid with a single FILE")
|
|
1692
|
-
environment = runtime.environment(args.inputs[0])
|
|
1693
|
-
source_file = Path(args.inputs[1])
|
|
1838
|
+
source_file = None
|
|
1694
1839
|
if source_file is None:
|
|
1695
1840
|
pass
|
|
1696
1841
|
elif str(source_file) == "-":
|
|
@@ -227,6 +227,13 @@ class ConsoleRenderer:
|
|
|
227
227
|
def _render_check_completed(self, _event: RuntimeEvent) -> None:
|
|
228
228
|
self._end_progress_line()
|
|
229
229
|
|
|
230
|
+
def _render_check_header_wait(self, event: RuntimeEvent) -> None:
|
|
231
|
+
module = str(event.data.get("module") or "Lean input")
|
|
232
|
+
self._print(f"Waiting for header snapshot initialization: {module}")
|
|
233
|
+
|
|
234
|
+
def _render_project_workspace_lock_wait(self, event: RuntimeEvent) -> None:
|
|
235
|
+
self._print(event.message)
|
|
236
|
+
|
|
230
237
|
# -- low-level output -----------------------------------------------
|
|
231
238
|
|
|
232
239
|
def _verbose_line(self, event: RuntimeEvent) -> None:
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
5
6
|
import hashlib
|
|
6
7
|
import json
|
|
7
8
|
import os
|
|
@@ -10,15 +11,26 @@ import tempfile
|
|
|
10
11
|
from collections.abc import Iterator, Sequence
|
|
11
12
|
from contextlib import contextmanager
|
|
12
13
|
from pathlib import Path
|
|
13
|
-
from typing import TYPE_CHECKING
|
|
14
|
+
from typing import TYPE_CHECKING, NamedTuple
|
|
14
15
|
|
|
16
|
+
from .errors import EnvironmentError
|
|
15
17
|
from .locking import FileLock
|
|
16
18
|
from .serialization import sha256_id
|
|
17
19
|
from .store import platform_compatibility
|
|
18
20
|
|
|
19
21
|
if TYPE_CHECKING:
|
|
22
|
+
import threading
|
|
23
|
+
|
|
24
|
+
from .events import EventEmitter
|
|
20
25
|
from .toolchains import ToolchainManager
|
|
21
26
|
|
|
27
|
+
ENABLE_VARIABLE = "LEAN_RUNTIME_HEADER_SNAPSHOTS"
|
|
28
|
+
_TRUTHY = {"1", "true", "on", "yes"}
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _configured_enabled() -> bool:
|
|
32
|
+
return os.environ.get(ENABLE_VARIABLE, "").strip().lower() in _TRUTHY
|
|
33
|
+
|
|
22
34
|
|
|
23
35
|
def _header_identity(source: str) -> str:
|
|
24
36
|
"""Return a conservative identity for the module header/import block."""
|
|
@@ -43,12 +55,34 @@ def _header_identity(source: str) -> str:
|
|
|
43
55
|
return "".join(lines)
|
|
44
56
|
|
|
45
57
|
|
|
58
|
+
class _SnapshotPaths(NamedTuple):
|
|
59
|
+
snapshot: Path
|
|
60
|
+
deps: Path
|
|
61
|
+
lock: Path
|
|
62
|
+
|
|
63
|
+
def valid(self) -> bool:
|
|
64
|
+
return self.snapshot.is_file() and self.deps.is_file()
|
|
65
|
+
|
|
66
|
+
|
|
46
67
|
class LeanHeaderCache:
|
|
47
|
-
"""Reuse Lean's native experimental import snapshots when the binary supports them.
|
|
68
|
+
"""Reuse Lean's native experimental import snapshots when the binary supports them.
|
|
69
|
+
|
|
70
|
+
Snapshots are keyed by exact toolchain, workspace identity, logical module
|
|
71
|
+
name, and import-block content, so distinct modules never share a snapshot.
|
|
72
|
+
Existing snapshots are loaded without holding any lock; only first-time
|
|
73
|
+
snapshot creation serializes behind a per-key file lock.
|
|
74
|
+
"""
|
|
48
75
|
|
|
49
|
-
def __init__(
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
home: Path,
|
|
79
|
+
toolchains: ToolchainManager,
|
|
80
|
+
events: EventEmitter | None = None,
|
|
81
|
+
) -> None:
|
|
50
82
|
self.home = home / "header-snapshots"
|
|
51
83
|
self.toolchains = toolchains
|
|
84
|
+
self.events = events
|
|
85
|
+
self.enabled = _configured_enabled()
|
|
52
86
|
self._support: dict[str, bool] = {}
|
|
53
87
|
self._toolchain_keys: dict[str, str] = {}
|
|
54
88
|
|
|
@@ -95,37 +129,86 @@ class LeanHeaderCache:
|
|
|
95
129
|
self._support[key] = supported
|
|
96
130
|
return supported
|
|
97
131
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
toolchain: str,
|
|
102
|
-
workspace_identity: str,
|
|
103
|
-
source: str,
|
|
104
|
-
command: Sequence[str],
|
|
105
|
-
) -> Iterator[list[str]]:
|
|
106
|
-
if not self.supported(toolchain):
|
|
107
|
-
yield list(command)
|
|
108
|
-
return
|
|
132
|
+
def _paths(
|
|
133
|
+
self, toolchain: str, workspace_identity: str, module: str, source: str
|
|
134
|
+
) -> _SnapshotPaths:
|
|
109
135
|
key = sha256_id(
|
|
110
136
|
"header",
|
|
111
137
|
{
|
|
112
138
|
"toolchain": self._toolchain_key(toolchain),
|
|
113
139
|
"workspace": workspace_identity,
|
|
140
|
+
"module": module,
|
|
114
141
|
"header": hashlib.sha256(_header_identity(source).encode()).hexdigest(),
|
|
115
142
|
},
|
|
116
143
|
).removeprefix("header-")
|
|
117
144
|
root = self.home / self._toolchain_key(toolchain)
|
|
118
145
|
snapshot = root / f"{key}.snap"
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
146
|
+
return _SnapshotPaths(snapshot, Path(str(snapshot) + ".deps"), root / f"{key}.lock")
|
|
147
|
+
|
|
148
|
+
def discard(self, toolchain: str, workspace_identity: str, module: str, source: str) -> None:
|
|
149
|
+
"""Quarantine a snapshot that produced timeout or staleness symptoms."""
|
|
150
|
+
paths = self._paths(toolchain, workspace_identity, module, source)
|
|
151
|
+
for path in (paths.snapshot, paths.deps):
|
|
152
|
+
with contextlib.suppress(OSError):
|
|
153
|
+
path.unlink(missing_ok=True)
|
|
154
|
+
|
|
155
|
+
@contextmanager
|
|
156
|
+
def _creation_lock(
|
|
157
|
+
self, paths: _SnapshotPaths, module: str, cancel: threading.Event | None
|
|
158
|
+
) -> Iterator[None]:
|
|
159
|
+
"""Acquire the per-key creation lock, announcing contention once."""
|
|
160
|
+
lock = FileLock(paths.lock, timeout=0)
|
|
161
|
+
try:
|
|
162
|
+
lock.__enter__()
|
|
163
|
+
except EnvironmentError:
|
|
164
|
+
if self.events is not None:
|
|
165
|
+
self.events.emit(
|
|
166
|
+
"check.header_wait",
|
|
167
|
+
f"Waiting for header snapshot initialization: {module}",
|
|
168
|
+
phase="check",
|
|
169
|
+
module=module,
|
|
170
|
+
)
|
|
171
|
+
lock = FileLock(paths.lock, cancel=cancel)
|
|
172
|
+
lock.__enter__()
|
|
173
|
+
try:
|
|
174
|
+
yield
|
|
175
|
+
finally:
|
|
176
|
+
lock.__exit__(None, None, None)
|
|
177
|
+
|
|
178
|
+
@contextmanager
|
|
179
|
+
def command(
|
|
180
|
+
self,
|
|
181
|
+
toolchain: str,
|
|
182
|
+
workspace_identity: str,
|
|
183
|
+
module: str,
|
|
184
|
+
source: str,
|
|
185
|
+
command: Sequence[str],
|
|
186
|
+
*,
|
|
187
|
+
cancel: threading.Event | None = None,
|
|
188
|
+
) -> Iterator[list[str]]:
|
|
189
|
+
if not self.enabled or not self.supported(toolchain):
|
|
190
|
+
yield list(command)
|
|
191
|
+
return
|
|
192
|
+
paths = self._paths(toolchain, workspace_identity, module, source)
|
|
193
|
+
if paths.valid():
|
|
194
|
+
yield [*command[:-1], f"--incr-load={paths.snapshot}", command[-1]]
|
|
195
|
+
return
|
|
196
|
+
paths.snapshot.parent.mkdir(parents=True, exist_ok=True)
|
|
197
|
+
hit_after_wait = False
|
|
198
|
+
with self._creation_lock(paths, module, cancel):
|
|
199
|
+
if paths.valid():
|
|
200
|
+
hit_after_wait = True
|
|
201
|
+
else:
|
|
202
|
+
root = str(paths.snapshot.parent)
|
|
203
|
+
with tempfile.TemporaryDirectory(
|
|
204
|
+
prefix=f".{paths.snapshot.stem}.", dir=root
|
|
205
|
+
) as temporary_root:
|
|
206
|
+
temporary = Path(temporary_root) / "header.snap"
|
|
207
|
+
temporary_deps = Path(str(temporary) + ".deps")
|
|
208
|
+
yield [*command[:-1], f"--incr-header-save={temporary}", command[-1]]
|
|
209
|
+
if temporary.is_file() and temporary_deps.is_file():
|
|
210
|
+
os.replace(temporary, paths.snapshot)
|
|
211
|
+
os.replace(temporary_deps, paths.deps)
|
|
124
212
|
return
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
temporary_deps = Path(str(temporary) + ".deps")
|
|
128
|
-
yield [*command[:-1], f"--incr-header-save={temporary}", command[-1]]
|
|
129
|
-
if temporary.is_file() and temporary_deps.is_file():
|
|
130
|
-
os.replace(temporary, snapshot)
|
|
131
|
-
os.replace(temporary_deps, deps)
|
|
213
|
+
if hit_after_wait:
|
|
214
|
+
yield [*command[:-1], f"--incr-load={paths.snapshot}", command[-1]]
|
|
@@ -2,32 +2,70 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
6
|
+
import json
|
|
5
7
|
import os
|
|
6
8
|
import threading
|
|
7
9
|
import time
|
|
10
|
+
from collections.abc import Callable
|
|
8
11
|
from pathlib import Path
|
|
9
12
|
from types import TracebackType
|
|
10
|
-
from typing import BinaryIO
|
|
13
|
+
from typing import Any, BinaryIO
|
|
11
14
|
|
|
12
15
|
from .errors import EnvironmentError
|
|
13
16
|
|
|
14
17
|
|
|
15
18
|
class FileLock:
|
|
19
|
+
"""One advisory cross-process lock.
|
|
20
|
+
|
|
21
|
+
``owner`` is a small JSON-serializable description of the acquiring
|
|
22
|
+
operation; on POSIX it is written into the lock file while the lock is
|
|
23
|
+
held so that waiters can attribute their wait. ``on_wait`` is invoked at
|
|
24
|
+
most once, on first contention, with the current holder's description (or
|
|
25
|
+
``None`` when it cannot be read).
|
|
26
|
+
"""
|
|
27
|
+
|
|
16
28
|
def __init__(
|
|
17
29
|
self,
|
|
18
30
|
path: Path,
|
|
19
31
|
timeout: float = 300,
|
|
20
32
|
cancel: threading.Event | None = None,
|
|
33
|
+
owner: dict[str, Any] | None = None,
|
|
34
|
+
on_wait: Callable[[dict[str, Any] | None], None] | None = None,
|
|
21
35
|
) -> None:
|
|
22
36
|
self.path = path
|
|
23
37
|
self.timeout = timeout
|
|
24
38
|
self.cancel = cancel
|
|
39
|
+
self.owner = owner
|
|
40
|
+
self.on_wait = on_wait
|
|
25
41
|
self._handle: BinaryIO | None = None
|
|
42
|
+
self._owner_written = False
|
|
43
|
+
|
|
44
|
+
def holder(self) -> dict[str, Any] | None:
|
|
45
|
+
"""Best-effort description of the current lock holder."""
|
|
46
|
+
try:
|
|
47
|
+
value = json.loads(self.path.read_text(encoding="utf-8"))
|
|
48
|
+
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
49
|
+
return None
|
|
50
|
+
return value if isinstance(value, dict) else None
|
|
51
|
+
|
|
52
|
+
def _write_owner(self, handle: BinaryIO) -> None:
|
|
53
|
+
if self.owner is None or os.name == "nt":
|
|
54
|
+
return
|
|
55
|
+
try:
|
|
56
|
+
handle.seek(0)
|
|
57
|
+
handle.truncate()
|
|
58
|
+
handle.write(json.dumps({"pid": os.getpid(), **self.owner}).encode("utf-8"))
|
|
59
|
+
handle.flush()
|
|
60
|
+
self._owner_written = True
|
|
61
|
+
except (OSError, TypeError, ValueError):
|
|
62
|
+
self._owner_written = False
|
|
26
63
|
|
|
27
64
|
def __enter__(self) -> FileLock:
|
|
28
65
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
|
29
66
|
handle = self.path.open("a+b")
|
|
30
67
|
started = time.monotonic()
|
|
68
|
+
announced = False
|
|
31
69
|
while True:
|
|
32
70
|
try:
|
|
33
71
|
if os.name == "nt":
|
|
@@ -51,8 +89,13 @@ class FileLock:
|
|
|
51
89
|
getattr(fcntl, "LOCK_EX") | getattr(fcntl, "LOCK_NB"), # noqa: B009
|
|
52
90
|
)
|
|
53
91
|
self._handle = handle
|
|
92
|
+
self._write_owner(handle)
|
|
54
93
|
return self
|
|
55
94
|
except OSError as error:
|
|
95
|
+
if not announced:
|
|
96
|
+
announced = True
|
|
97
|
+
if self.on_wait is not None:
|
|
98
|
+
self.on_wait(self.holder())
|
|
56
99
|
if self.cancel is not None and self.cancel.is_set():
|
|
57
100
|
handle.close()
|
|
58
101
|
raise EnvironmentError(
|
|
@@ -74,6 +117,12 @@ class FileLock:
|
|
|
74
117
|
handle = self._handle
|
|
75
118
|
if handle is None:
|
|
76
119
|
return
|
|
120
|
+
if self._owner_written:
|
|
121
|
+
with contextlib.suppress(OSError):
|
|
122
|
+
handle.seek(0)
|
|
123
|
+
handle.truncate()
|
|
124
|
+
handle.flush()
|
|
125
|
+
self._owner_written = False
|
|
77
126
|
if os.name == "nt":
|
|
78
127
|
import msvcrt
|
|
79
128
|
|
|
@@ -20,6 +20,8 @@ TimingPhase = Literal[
|
|
|
20
20
|
"instance_creation",
|
|
21
21
|
"input_staging",
|
|
22
22
|
"command_preparation",
|
|
23
|
+
"workspace_lock",
|
|
24
|
+
"header_snapshot",
|
|
23
25
|
"execution",
|
|
24
26
|
"result_collection",
|
|
25
27
|
"result_publication",
|
|
@@ -40,6 +42,8 @@ TIMING_PHASES = frozenset(
|
|
|
40
42
|
"instance_creation",
|
|
41
43
|
"input_staging",
|
|
42
44
|
"command_preparation",
|
|
45
|
+
"workspace_lock",
|
|
46
|
+
"header_snapshot",
|
|
43
47
|
"execution",
|
|
44
48
|
"result_collection",
|
|
45
49
|
"result_publication",
|