lean-runtime 4.27.0__tar.gz → 4.33.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-4.27.0 → lean_runtime-4.33.0}/CHANGELOG.md +77 -0
- {lean_runtime-4.27.0/lean_runtime.egg-info → lean_runtime-4.33.0}/PKG-INFO +1 -1
- lean_runtime-4.33.0/lean_runtime/_paths.py +87 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/backends.py +35 -4
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/bundles.py +140 -10
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/capsules.py +50 -9
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/cli.py +4 -3
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/console.py +173 -11
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/declaration_index.py +5 -4
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/declaration_index_build.py +32 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/declaration_index_oci.py +38 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/events.py +22 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/locking.py +33 -11
- lean_runtime-4.33.0/lean_runtime/package_ids.py +42 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/packs.py +57 -42
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/programs.py +33 -3
- lean_runtime-4.33.0/lean_runtime/progress.py +165 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/project_execution.py +6 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/project_sharing.py +60 -19
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/projects.py +15 -5
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/run_cli.py +6 -1
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/runtime.py +102 -41
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/shared_projects.py +14 -8
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/store.py +153 -35
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/toolchain_oci.py +28 -2
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/toolchain_slim.py +49 -20
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/toolchains.py +18 -9
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/verification.py +67 -37
- {lean_runtime-4.27.0 → lean_runtime-4.33.0/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime.egg-info/SOURCES.txt +5 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/pyproject.toml +1 -1
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/conftest.py +12 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_backend.py +23 -2
- lean_runtime-4.33.0/tests/test_package_ids.py +44 -0
- lean_runtime-4.33.0/tests/test_paths.py +103 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_programs.py +1 -0
- lean_runtime-4.33.0/tests/test_progress.py +257 -0
- lean_runtime-4.33.0/tests/test_progress_counted.py +155 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_projects.py +9 -9
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_runtime.py +22 -1
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_store.py +2 -1
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_toolchain_oci.py +2 -1
- lean_runtime-4.27.0/lean_runtime/_paths.py +0 -38
- lean_runtime-4.27.0/tests/test_paths.py +0 -25
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/LICENSE +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/MANIFEST.in +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/README.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/environments.toml +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/catalog/locks/mathlib-v4.33.1.lock.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/compatibility/README.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/compatibility/mathlib-4.33.0.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/compatibility/mathlib-4.33.0.toml +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/concepts/context-selection.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/index.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/reference/commands.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/reference/python-advanced.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/reference/python-api.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/reference/storage.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/trust.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/tutorial.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/workflows/check-files.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/docs/workflows/lake-projects.md +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/examples/mathlib.toml +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/__init__.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/__main__.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/_git.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/comparison.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/context_resolution.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/decisions.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/engine.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/history.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/memo.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/probe.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/result.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/environments.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/errors.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/facade.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/header_cache.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/health.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/identifier_resolver.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/import_syntax.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/lake.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/lake_cache.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/matrix.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/models.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/oci.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/oci_protocol.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/policies.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/profiling.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/py.typed +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/references.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/resolver.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/serialization.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/specs.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/timings.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime/wire.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/mkdocs.yml +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/attestation-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/check-batch-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/publication-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/status-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/announcement_gate.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/catalog_update.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/lake_cache_gate.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/prepare_declaration_index.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/publish_declaration_index.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/registry_preflight.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/run_compatibility.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/setup.cfg +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_engine.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_memo.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_announcement_gate.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_bundles.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_capsules.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_cli.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_console.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_context_resolution.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_declaration_index.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_diagnostics.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_environment_integration.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_events_health.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_facade.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_frontmatter.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_git.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_header_cache.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_identifier_resolver.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_interactive.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_lake_cache.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_lockfiles.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_locking.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_models.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_multifile.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_oci_protocol.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_packs.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_project_bootstrap.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_project_publish.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_references.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_run_cli.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_schema_resources.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_schemas.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_signatures.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_specs.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_toolchain_slim.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_toolchains.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_v1_precision.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_verification_inventory.py +0 -0
- {lean_runtime-4.27.0 → lean_runtime-4.33.0}/tests/test_verification_probe.py +0 -0
|
@@ -2,6 +2,83 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.33.0 - 2026-08-27
|
|
6
|
+
|
|
7
|
+
- Cover the remaining silent phases with progress. Elan bootstrap and
|
|
8
|
+
`elan toolchain install`, and the `lake init`/`lake update` behind `new`, now
|
|
9
|
+
stream their output as `process.progress`/`process.output` like every other
|
|
10
|
+
subprocess. Long pure-Python loops report counted progress that renders as a
|
|
11
|
+
bar: `verification.inventory` (hashing build artifacts in `verify`),
|
|
12
|
+
`bundle.tree_inventory` (`env export`/`import`), `source.snapshot_digest`
|
|
13
|
+
(source fingerprints, including path dependencies during `adopt`), and
|
|
14
|
+
`capsule.import_parse`. The publish preflight fetch
|
|
15
|
+
(`publish.remote_probe_started`) and the Lake configuration read behind
|
|
16
|
+
project-wide `check` (`project.config_translate_started`) announce
|
|
17
|
+
themselves. Any event carrying `current`/`total` and a `label` now renders as
|
|
18
|
+
a counted bar without a dedicated console handler, and deep helpers reach
|
|
19
|
+
the active runtime's emitter through `lean_runtime.events.current()`.
|
|
20
|
+
- Re-running `adopt` over a tree no longer re-probes every already attached
|
|
21
|
+
project (a Lake graph load each, up to two minutes for Mathlib); the plan
|
|
22
|
+
already reports them as attached and `verify` re-checks links. Attachment
|
|
23
|
+
now announces `adopt.attach_started` per project, rendered as
|
|
24
|
+
`Attaching projects [bar] i/n`, and the pre-swap Lake graph probe streams its
|
|
25
|
+
output like every other Lake invocation instead of running silently.
|
|
26
|
+
|
|
27
|
+
## 4.30.0 - 2026-08-26
|
|
28
|
+
|
|
29
|
+
- Attach shared dependencies on Windows without administrator rights. Creating
|
|
30
|
+
a symbolic link needs `SeCreateSymbolicLinkPrivilege` (administrators or
|
|
31
|
+
Developer Mode), so `adopt` and `project attach` failed with
|
|
32
|
+
`WinError 1314: A required privilege is not held by the client` on a normal
|
|
33
|
+
account, and the pre-attach probe never caught it. When symlink creation is
|
|
34
|
+
denied, the package link is now created as a directory junction, which any
|
|
35
|
+
user may create and which Lake and Git traverse identically. Every place
|
|
36
|
+
that recognizes an attached package as a link (`status`, `verify`,
|
|
37
|
+
`unshare`, adoption planning, rollback, source fingerprinting) now
|
|
38
|
+
recognizes junctions too.
|
|
39
|
+
- Size local dependency checkouts during `adopt` without descending into
|
|
40
|
+
attached links, using one directory scan per level instead of `rglob`.
|
|
41
|
+
- Show progress while subprocesses run. Lake's `[n/m]` build steps now stream
|
|
42
|
+
from `check`, `build`, and artifact hydration as `process.progress` events
|
|
43
|
+
(rate-limited to ten per second), rendered on a terminal as one in-place bar
|
|
44
|
+
(`lake [████░░░░] 3300/8318 · Building Mathlib.Foo`) and in logs as 25%
|
|
45
|
+
checkpoints; every other output line becomes a throttled `process.output`
|
|
46
|
+
heartbeat shown in place on a terminal, so `lake exe cache get` is no longer
|
|
47
|
+
silent. Third-party backends that do not accept the new optional `on_output`
|
|
48
|
+
callback keep working unchanged.
|
|
49
|
+
- Announce counted phases that used to be silent: `adopt.inspect_started` and
|
|
50
|
+
`adopt.identity_started` for each project during adoption planning, and
|
|
51
|
+
`project.detach.package_started` for each package `unshare` materializes; all
|
|
52
|
+
carry `current`/`total` and render as the same counted bar.
|
|
53
|
+
- Keep the terminal alive during any silent stretch: when no event has arrived
|
|
54
|
+
for two seconds the console shows `⋯ no new events for Ns · last: <activity>`
|
|
55
|
+
in place; plain logs print `Still working (…)` after ten seconds and then
|
|
56
|
+
every thirty.
|
|
57
|
+
- Name new shared package directories `pkg_<32 hex>` instead of
|
|
58
|
+
`project_package_<64 hex>`. The 80-character name pushed Mathlib's deepest
|
|
59
|
+
build outputs past Windows' 260-character path limit, so an adopted Mathlib
|
|
60
|
+
project could hydrate its cache but never finish `build`
|
|
61
|
+
(`failed to create file '…\Isometric.olean.server'`). The digest is the same
|
|
62
|
+
identity truncated to 128 bits; existing `project_package_*` directories stay
|
|
63
|
+
valid and reusable. To move already adopted projects onto short paths,
|
|
64
|
+
`lean-runtime project unshare` each of them, delete the legacy
|
|
65
|
+
`project-packages/project_package_*` directories (nothing references them once
|
|
66
|
+
every project is unshared; `clean` does not yet reclaim them), and
|
|
67
|
+
`lean-runtime adopt` again.
|
|
68
|
+
- Windows correctness fixes surfaced by running the suite on Windows:
|
|
69
|
+
compact source snapshots stage under a shorter directory name so Git's
|
|
70
|
+
shallow-clone metadata stays below `MAX_PATH` (`'$GIT_DIR' too big`);
|
|
71
|
+
`lean-runtime run` rewrites a container backend's POSIX staged path back to
|
|
72
|
+
the user's file on Windows hosts; lock waiters learn the holding operation
|
|
73
|
+
through a `.owner` sidecar (a byte-locked file cannot be read on Windows);
|
|
74
|
+
declaration shards close their SQLite connection before publication so the
|
|
75
|
+
rename succeeds; `storage` deduplicates hard links via `os.stat()` because
|
|
76
|
+
`DirEntry.stat()` reports no inode on Windows; `publish` accepts a
|
|
77
|
+
drive-letter origin path (`C:\...`) instead of treating `C` as a URL scheme;
|
|
78
|
+
bundle import accepts CRLF working files whose LF-normalized blob is the
|
|
79
|
+
recorded object (`core.autocrlf`); and storage cleanup clamps negative file
|
|
80
|
+
ages from filesystem timestamp granularity.
|
|
81
|
+
|
|
5
82
|
## 4.27.0 - 2026-08-26
|
|
6
83
|
|
|
7
84
|
- Fingerprint a project by what Lake compiles, not by everything beside the
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Internal cross-platform filesystem operations."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import stat
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from types import TracebackType
|
|
12
|
+
from typing import Any, cast
|
|
13
|
+
|
|
14
|
+
_WINDOWS_MOUNT_POINT_REPARSE_TAG = 0xA0000003
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _retry_writable(
|
|
18
|
+
function: Callable[..., Any],
|
|
19
|
+
path: str,
|
|
20
|
+
_error: tuple[type[BaseException], BaseException, TracebackType | None],
|
|
21
|
+
) -> None:
|
|
22
|
+
"""Clear Git-for-Windows read-only attributes and retry one removal."""
|
|
23
|
+
|
|
24
|
+
os.chmod(path, stat.S_IWRITE)
|
|
25
|
+
function(path)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def is_link(path: Path) -> bool:
|
|
29
|
+
"""Whether ``path`` is a symbolic link or a Windows directory junction.
|
|
30
|
+
|
|
31
|
+
Shared package directories are attached as links. Python reports a
|
|
32
|
+
junction as a plain directory (``is_symlink()`` is false), so every place
|
|
33
|
+
that asks "is this an attached link?" must go through this helper.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
if path.is_symlink():
|
|
37
|
+
return True
|
|
38
|
+
if os.name != "nt":
|
|
39
|
+
return False
|
|
40
|
+
try:
|
|
41
|
+
status = os.lstat(path)
|
|
42
|
+
except OSError:
|
|
43
|
+
return False
|
|
44
|
+
attributes = getattr(status, "st_file_attributes", 0)
|
|
45
|
+
reparse_tag = getattr(status, "st_reparse_tag", 0)
|
|
46
|
+
return bool(attributes & stat.FILE_ATTRIBUTE_REPARSE_POINT) and (
|
|
47
|
+
reparse_tag == _WINDOWS_MOUNT_POINT_REPARSE_TAG
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def link_directory(target: Path, link: Path) -> None:
|
|
52
|
+
"""Attach ``link`` to the directory ``target`` without requiring privileges.
|
|
53
|
+
|
|
54
|
+
A symbolic link is preferred everywhere. Windows only grants symlink
|
|
55
|
+
creation to administrators or Developer Mode users
|
|
56
|
+
(``ERROR_PRIVILEGE_NOT_HELD``, 1314); in that case a directory junction,
|
|
57
|
+
which any user may create and which Lake and Git traverse identically, is
|
|
58
|
+
used instead.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
os.symlink(target, link, target_is_directory=True)
|
|
63
|
+
except OSError as exc:
|
|
64
|
+
if os.name != "nt" or getattr(exc, "winerror", None) != 1314:
|
|
65
|
+
raise
|
|
66
|
+
import _winapi
|
|
67
|
+
|
|
68
|
+
# These members exist only in the Windows runtime/typeshed surface.
|
|
69
|
+
# Resolve the API dynamically so strict checking on macOS/Linux still
|
|
70
|
+
# validates this module instead of rejecting the platform-only member.
|
|
71
|
+
create_junction = cast(Callable[[str, str], None], vars(_winapi)["CreateJunction"])
|
|
72
|
+
create_junction(str(target.resolve()), str(link))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def remove_tree(path: Path) -> None:
|
|
76
|
+
"""Remove a tree containing Git packs, tolerating transient Windows locks."""
|
|
77
|
+
|
|
78
|
+
for attempt in range(4):
|
|
79
|
+
try:
|
|
80
|
+
shutil.rmtree(path, onerror=_retry_writable)
|
|
81
|
+
return
|
|
82
|
+
except PermissionError:
|
|
83
|
+
if not path.exists():
|
|
84
|
+
return
|
|
85
|
+
if attempt == 3:
|
|
86
|
+
raise
|
|
87
|
+
time.sleep(0.05 * (2**attempt))
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
|
+
import re
|
|
6
7
|
import signal
|
|
7
8
|
import subprocess
|
|
8
9
|
import threading
|
|
@@ -83,14 +84,39 @@ class _OutputBudget:
|
|
|
83
84
|
return chunk[:size]
|
|
84
85
|
|
|
85
86
|
|
|
86
|
-
|
|
87
|
+
_LINE_BREAK = re.compile(rb"\r\n|\r|\n")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _drain(
|
|
91
|
+
stream: BinaryIO,
|
|
92
|
+
budget: _OutputBudget,
|
|
93
|
+
chunks: list[bytes],
|
|
94
|
+
on_output: Callable[[str], None] | None = None,
|
|
95
|
+
) -> None:
|
|
96
|
+
pending = b""
|
|
87
97
|
while True:
|
|
88
98
|
chunk = stream.read(65_536)
|
|
89
99
|
if not chunk:
|
|
90
|
-
|
|
100
|
+
break
|
|
91
101
|
kept = budget.take(chunk)
|
|
92
102
|
if kept:
|
|
93
103
|
chunks.append(kept)
|
|
104
|
+
if on_output is None:
|
|
105
|
+
continue
|
|
106
|
+
# Progress bars redraw with bare carriage returns, so treat those as lines too.
|
|
107
|
+
parts = _LINE_BREAK.split(pending + chunk)
|
|
108
|
+
pending = parts.pop()
|
|
109
|
+
for part in parts:
|
|
110
|
+
_observe(on_output, part)
|
|
111
|
+
if on_output is not None and pending:
|
|
112
|
+
_observe(on_output, pending)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _observe(on_output: Callable[[str], None], line: bytes) -> None:
|
|
116
|
+
try:
|
|
117
|
+
on_output(line.decode("utf-8", errors="replace"))
|
|
118
|
+
except Exception: # noqa: BLE001 - an observer must never break execution
|
|
119
|
+
return
|
|
94
120
|
|
|
95
121
|
|
|
96
122
|
class _TranscriptReader:
|
|
@@ -294,6 +320,7 @@ class LocalBackend:
|
|
|
294
320
|
environment: Mapping[str, str],
|
|
295
321
|
policy: ExecutionPolicy,
|
|
296
322
|
cancel: threading.Event | None = None,
|
|
323
|
+
on_output: Callable[[str], None] | None = None,
|
|
297
324
|
) -> BackendResult:
|
|
298
325
|
enforced, preexec, creationflags = self._process_options(policy)
|
|
299
326
|
|
|
@@ -313,8 +340,12 @@ class LocalBackend:
|
|
|
313
340
|
stdout_chunks: list[bytes] = []
|
|
314
341
|
stderr_chunks: list[bytes] = []
|
|
315
342
|
readers = [
|
|
316
|
-
threading.Thread(
|
|
317
|
-
|
|
343
|
+
threading.Thread(
|
|
344
|
+
target=_drain, args=(process.stdout, budget, stdout_chunks, on_output)
|
|
345
|
+
),
|
|
346
|
+
threading.Thread(
|
|
347
|
+
target=_drain, args=(process.stderr, budget, stderr_chunks, on_output)
|
|
348
|
+
),
|
|
318
349
|
]
|
|
319
350
|
for reader in readers:
|
|
320
351
|
reader.start()
|
|
@@ -13,10 +13,11 @@ import tarfile
|
|
|
13
13
|
import tempfile
|
|
14
14
|
import uuid
|
|
15
15
|
from collections.abc import Iterable, Mapping, Sequence
|
|
16
|
+
from contextlib import nullcontext
|
|
16
17
|
from dataclasses import dataclass
|
|
17
18
|
from datetime import datetime, timezone
|
|
18
19
|
from pathlib import Path, PurePosixPath
|
|
19
|
-
from typing import Any
|
|
20
|
+
from typing import Any, Protocol
|
|
20
21
|
|
|
21
22
|
import zstandard
|
|
22
23
|
|
|
@@ -33,7 +34,7 @@ from .capsules import (
|
|
|
33
34
|
setup_artifact_groups,
|
|
34
35
|
)
|
|
35
36
|
from .errors import EnvironmentError
|
|
36
|
-
from .events import EventEmitter
|
|
37
|
+
from .events import EventEmitter, current
|
|
37
38
|
from .lake import ROOT_MODULE
|
|
38
39
|
from .lockfiles import EnvironmentLock, LockedPackage
|
|
39
40
|
from .locking import FileLock
|
|
@@ -61,6 +62,7 @@ from .packs import (
|
|
|
61
62
|
unpack_frame,
|
|
62
63
|
)
|
|
63
64
|
from .policies import ExecutionPolicy
|
|
65
|
+
from .progress import CountedProgress
|
|
64
66
|
from .serialization import canonical_json_bytes, write_json_atomic
|
|
65
67
|
from .store import (
|
|
66
68
|
EnvironmentStore,
|
|
@@ -81,6 +83,53 @@ SOURCE_TREE_INVENTORY = ".lean-runtime-source-tree.json"
|
|
|
81
83
|
MAX_CAPSULE_CONFIG_BYTES = 64 * 1024**2
|
|
82
84
|
|
|
83
85
|
|
|
86
|
+
class _BinaryReader(Protocol):
|
|
87
|
+
def read(self, size: int = -1) -> bytes: ...
|
|
88
|
+
|
|
89
|
+
def readinto(self, buffer: Any) -> int | None: ...
|
|
90
|
+
|
|
91
|
+
def seek(self, offset: int, whence: int = 0) -> int: ...
|
|
92
|
+
|
|
93
|
+
def tell(self) -> int: ...
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
class _ProgressReader:
|
|
97
|
+
"""Report bytes after a consumer has actually read them."""
|
|
98
|
+
|
|
99
|
+
def __init__(
|
|
100
|
+
self, handle: _BinaryReader, progress: CountedProgress, *, offset: int = 0
|
|
101
|
+
) -> None:
|
|
102
|
+
self._handle = handle
|
|
103
|
+
self._progress = progress
|
|
104
|
+
self._offset = offset
|
|
105
|
+
self._furthest = offset
|
|
106
|
+
|
|
107
|
+
def _report(self) -> None:
|
|
108
|
+
position = self._offset + self._handle.tell()
|
|
109
|
+
if position > self._furthest:
|
|
110
|
+
self._furthest = position
|
|
111
|
+
self._progress.advance(to=position)
|
|
112
|
+
|
|
113
|
+
def read(self, size: int = -1) -> bytes:
|
|
114
|
+
data = self._handle.read(size)
|
|
115
|
+
self._report()
|
|
116
|
+
return data
|
|
117
|
+
|
|
118
|
+
def readinto(self, buffer: Any) -> int:
|
|
119
|
+
count = self._handle.readinto(buffer)
|
|
120
|
+
self._report()
|
|
121
|
+
return count if count is not None else 0
|
|
122
|
+
|
|
123
|
+
def seek(self, offset: int, whence: int = 0) -> int:
|
|
124
|
+
return self._handle.seek(offset, whence)
|
|
125
|
+
|
|
126
|
+
def tell(self) -> int:
|
|
127
|
+
return self._handle.tell()
|
|
128
|
+
|
|
129
|
+
def __getattr__(self, name: str) -> Any:
|
|
130
|
+
return getattr(self._handle, name)
|
|
131
|
+
|
|
132
|
+
|
|
84
133
|
@dataclass(frozen=True, slots=True)
|
|
85
134
|
class PortableCopyInfo:
|
|
86
135
|
environment_id: str
|
|
@@ -130,8 +179,27 @@ def _tree_entries(
|
|
|
130
179
|
excluded_names: frozenset[str] = frozenset(),
|
|
131
180
|
omit_volatile_build_metadata: bool = False,
|
|
132
181
|
) -> Iterable[tuple[Path, str]]:
|
|
133
|
-
|
|
182
|
+
scan = CountedProgress(
|
|
183
|
+
current().emit,
|
|
184
|
+
"bundle.tree_scan",
|
|
185
|
+
f"Scanning {root.name}",
|
|
186
|
+
1,
|
|
187
|
+
phase="bundle",
|
|
188
|
+
)
|
|
189
|
+
scan.start()
|
|
190
|
+
paths = sorted(root.rglob("*"), key=lambda value: value.relative_to(root).as_posix())
|
|
191
|
+
scan.advance(f"{len(paths)} entries")
|
|
192
|
+
progress = CountedProgress(
|
|
193
|
+
current().emit,
|
|
194
|
+
"bundle.tree_inventory",
|
|
195
|
+
f"Inventorying {root.name}",
|
|
196
|
+
len(paths),
|
|
197
|
+
phase="bundle",
|
|
198
|
+
)
|
|
199
|
+
progress.start()
|
|
200
|
+
for path in paths:
|
|
134
201
|
if excluded is not None and (path == excluded or excluded in path.parents):
|
|
202
|
+
progress.advance(path.relative_to(root).as_posix())
|
|
135
203
|
continue
|
|
136
204
|
relative = path.relative_to(root)
|
|
137
205
|
if relative.parts and relative.parts[0] in excluded_names:
|
|
@@ -145,8 +213,10 @@ def _tree_entries(
|
|
|
145
213
|
or relative.name.endswith(".rsp")
|
|
146
214
|
)
|
|
147
215
|
):
|
|
216
|
+
progress.advance(relative.as_posix())
|
|
148
217
|
continue
|
|
149
218
|
yield path, relative.as_posix()
|
|
219
|
+
progress.advance(relative.as_posix())
|
|
150
220
|
|
|
151
221
|
|
|
152
222
|
def _write_tar_gzip(
|
|
@@ -219,6 +289,18 @@ def _oci_archive(entries: dict[str, bytes]) -> bytes:
|
|
|
219
289
|
|
|
220
290
|
|
|
221
291
|
def _write_oci_archive(entries: dict[str, Path], output: Path) -> None:
|
|
292
|
+
ordered = sorted(entries.items())
|
|
293
|
+
total = sum(path.stat().st_size for _name, path in ordered)
|
|
294
|
+
progress = CountedProgress(
|
|
295
|
+
current().emit,
|
|
296
|
+
"bundle.archive_write",
|
|
297
|
+
"Writing portable archive",
|
|
298
|
+
total,
|
|
299
|
+
phase="bundle",
|
|
300
|
+
unit="bytes",
|
|
301
|
+
)
|
|
302
|
+
progress.start()
|
|
303
|
+
written = 0
|
|
222
304
|
with (
|
|
223
305
|
output.open("wb") as raw_output,
|
|
224
306
|
gzip.GzipFile(
|
|
@@ -226,11 +308,13 @@ def _write_oci_archive(entries: dict[str, Path], output: Path) -> None:
|
|
|
226
308
|
) as compressed,
|
|
227
309
|
tarfile.open(fileobj=compressed, mode="w|", format=tarfile.PAX_FORMAT) as archive,
|
|
228
310
|
):
|
|
229
|
-
for name, path in
|
|
311
|
+
for name, path in ordered:
|
|
230
312
|
info = _normalized_info(name, mode=0o644)
|
|
231
313
|
info.size = path.stat().st_size
|
|
232
314
|
with path.open("rb") as handle:
|
|
233
|
-
archive.addfile(info, handle)
|
|
315
|
+
archive.addfile(info, _ProgressReader(handle, progress, offset=written))
|
|
316
|
+
written += info.size
|
|
317
|
+
progress.advance(name, to=written)
|
|
234
318
|
|
|
235
319
|
|
|
236
320
|
def _safe_name(name: str) -> PurePosixPath:
|
|
@@ -315,16 +399,32 @@ def _extract_layer(data: bytes | Path, destination: Path) -> None:
|
|
|
315
399
|
if destination.is_symlink():
|
|
316
400
|
raise EnvironmentError("bundle extraction destination must not be a symlink")
|
|
317
401
|
destination.mkdir(parents=True, exist_ok=True)
|
|
402
|
+
progress: CountedProgress | None = None
|
|
403
|
+
raw_handle: Any = None
|
|
318
404
|
try:
|
|
319
405
|
if isinstance(data, Path):
|
|
320
|
-
|
|
406
|
+
raw_handle = data.open("rb")
|
|
407
|
+
progress = CountedProgress(
|
|
408
|
+
current().emit,
|
|
409
|
+
"bundle.layer_extract",
|
|
410
|
+
f"Extracting {destination.name}",
|
|
411
|
+
data.stat().st_size,
|
|
412
|
+
phase="bundle",
|
|
413
|
+
unit="bytes",
|
|
414
|
+
)
|
|
415
|
+
progress.start()
|
|
416
|
+
archive = tarfile.open( # noqa: SIM115
|
|
417
|
+
fileobj=_ProgressReader(raw_handle, progress), mode="r:gz"
|
|
418
|
+
)
|
|
321
419
|
else:
|
|
322
420
|
archive = tarfile.open( # noqa: SIM115
|
|
323
421
|
fileobj=io.BytesIO(data), mode="r:gz"
|
|
324
422
|
)
|
|
325
423
|
except (tarfile.TarError, OSError) as exc:
|
|
424
|
+
if raw_handle is not None:
|
|
425
|
+
raw_handle.close()
|
|
326
426
|
raise EnvironmentError("bundle layer is not a valid gzip tar archive") from exc
|
|
327
|
-
with archive:
|
|
427
|
+
with archive, raw_handle if raw_handle is not None else nullcontext():
|
|
328
428
|
for member in archive:
|
|
329
429
|
count += 1
|
|
330
430
|
total += member.size
|
|
@@ -353,6 +453,8 @@ def _extract_layer(data: bytes | Path, destination: Path) -> None:
|
|
|
353
453
|
target.symlink_to(member.linkname)
|
|
354
454
|
else:
|
|
355
455
|
raise EnvironmentError(f"unsupported bundle member: {member.name!r}")
|
|
456
|
+
if progress is not None:
|
|
457
|
+
progress.advance(to=progress.total)
|
|
356
458
|
|
|
357
459
|
|
|
358
460
|
def _git_object_id(kind: str, data: bytes) -> str:
|
|
@@ -360,6 +462,18 @@ def _git_object_id(kind: str, data: bytes) -> str:
|
|
|
360
462
|
return hashlib.sha1(framed, usedforsecurity=False).hexdigest() # noqa: S324
|
|
361
463
|
|
|
362
464
|
|
|
465
|
+
def _matches_normalized(data: bytes, object_id: str) -> bool:
|
|
466
|
+
"""Accept a CRLF working file whose LF-normalized blob is the recorded one.
|
|
467
|
+
|
|
468
|
+
Git with ``core.autocrlf`` (the Git for Windows default) stores text blobs
|
|
469
|
+
with LF endings while checking them out with CRLF, so the on-disk bytes of
|
|
470
|
+
an untouched file legitimately hash differently from its recorded object.
|
|
471
|
+
"""
|
|
472
|
+
if b"\r\n" not in data:
|
|
473
|
+
return False
|
|
474
|
+
return _git_object_id("blob", data.replace(b"\r\n", b"\n")) == object_id
|
|
475
|
+
|
|
476
|
+
|
|
363
477
|
def _source_tree_inventory(root: Path, package: LockedPackage) -> bytes:
|
|
364
478
|
process = subprocess.run(
|
|
365
479
|
git_command("-C", str(root), "ls-tree", "-rz", "--full-tree", "HEAD"),
|
|
@@ -475,7 +589,7 @@ def _verify_source_tree_inventory(root: Path, package: LockedPackage, path: Path
|
|
|
475
589
|
if executable != (mode == "100755"):
|
|
476
590
|
raise EnvironmentError(f"bundled package source mode mismatch: {package.name}")
|
|
477
591
|
data = source.read_bytes()
|
|
478
|
-
if _git_object_id("blob", data) != object_id:
|
|
592
|
+
if _git_object_id("blob", data) != object_id and not _matches_normalized(data, object_id):
|
|
479
593
|
raise EnvironmentError(f"bundled package source mismatch: {package.name}")
|
|
480
594
|
entries.append(entry)
|
|
481
595
|
if _inventory_tree_id(entries) != package.tree_hash:
|
|
@@ -1100,11 +1214,26 @@ class EnvironmentBundles:
|
|
|
1100
1214
|
entries: dict[str, Path] = {}
|
|
1101
1215
|
total = 0
|
|
1102
1216
|
count = 0
|
|
1217
|
+
byte_progress = CountedProgress(
|
|
1218
|
+
current().emit,
|
|
1219
|
+
"bundle.archive_extract",
|
|
1220
|
+
f"Reading {bundle.name}",
|
|
1221
|
+
bundle.stat().st_size,
|
|
1222
|
+
phase="bundle",
|
|
1223
|
+
unit="bytes",
|
|
1224
|
+
)
|
|
1225
|
+
byte_progress.start()
|
|
1226
|
+
raw_handle: Any = None
|
|
1103
1227
|
try:
|
|
1104
|
-
|
|
1228
|
+
raw_handle = bundle.open("rb")
|
|
1229
|
+
archive = tarfile.open( # noqa: SIM115
|
|
1230
|
+
fileobj=_ProgressReader(raw_handle, byte_progress), mode="r:gz"
|
|
1231
|
+
)
|
|
1105
1232
|
except (tarfile.TarError, OSError) as exc:
|
|
1233
|
+
if raw_handle is not None:
|
|
1234
|
+
raw_handle.close()
|
|
1106
1235
|
raise EnvironmentError(f"could not read OCI bundle: {bundle}") from exc
|
|
1107
|
-
with archive:
|
|
1236
|
+
with archive, raw_handle:
|
|
1108
1237
|
for member in archive:
|
|
1109
1238
|
count += 1
|
|
1110
1239
|
if count > MAX_FILES:
|
|
@@ -1124,6 +1253,7 @@ class EnvironmentBundles:
|
|
|
1124
1253
|
with path.open("wb") as output:
|
|
1125
1254
|
shutil.copyfileobj(source, output)
|
|
1126
1255
|
entries[member.name] = path
|
|
1256
|
+
byte_progress.advance(to=byte_progress.total)
|
|
1127
1257
|
return entries
|
|
1128
1258
|
|
|
1129
1259
|
def _import_capsule_layout(
|
|
@@ -21,8 +21,10 @@ from pathlib import Path, PurePosixPath
|
|
|
21
21
|
from typing import Any, Literal
|
|
22
22
|
|
|
23
23
|
from .errors import EnvironmentError
|
|
24
|
+
from .events import current
|
|
24
25
|
from .import_syntax import IMPORT_STATEMENT
|
|
25
26
|
from .lockfiles import EnvironmentLock
|
|
27
|
+
from .progress import CountedProgress
|
|
26
28
|
from .serialization import canonical_json_bytes, write_json_atomic
|
|
27
29
|
|
|
28
30
|
CAPSULE_SCHEMA = "lean-runtime-check-capsule/1"
|
|
@@ -253,6 +255,14 @@ def parse_import_headers(
|
|
|
253
255
|
with tempfile.TemporaryDirectory(prefix="lean-runtime-imports-") as raw:
|
|
254
256
|
helper = Path(raw) / "ParseImports.lean"
|
|
255
257
|
helper.write_text(IMPORT_PARSER_SOURCE, encoding="utf-8")
|
|
258
|
+
progress = CountedProgress(
|
|
259
|
+
current().emit,
|
|
260
|
+
"capsule.import_parse",
|
|
261
|
+
"Parsing imports",
|
|
262
|
+
len(source_paths),
|
|
263
|
+
phase="capsule",
|
|
264
|
+
)
|
|
265
|
+
progress.start()
|
|
256
266
|
for offset in range(0, len(source_paths), batch_size):
|
|
257
267
|
batch = list(source_paths[offset : offset + batch_size])
|
|
258
268
|
process = subprocess.run(
|
|
@@ -286,6 +296,7 @@ def parse_import_headers(
|
|
|
286
296
|
if isinstance(item, dict) and isinstance(item.get("module"), str)
|
|
287
297
|
}
|
|
288
298
|
result[path] = tuple(sorted(names))
|
|
299
|
+
progress.advance(to=min(offset + len(batch), len(source_paths)))
|
|
289
300
|
return result
|
|
290
301
|
|
|
291
302
|
|
|
@@ -369,16 +380,34 @@ def build_manifest(
|
|
|
369
380
|
complete_modules: frozenset[str] | None = None,
|
|
370
381
|
) -> CapsuleManifest:
|
|
371
382
|
"""Inventory retained module artifacts beneath package build roots."""
|
|
383
|
+
scan = CountedProgress(
|
|
384
|
+
current().emit,
|
|
385
|
+
"capsule.artifact_scan",
|
|
386
|
+
"Scanning capsule artifacts",
|
|
387
|
+
1,
|
|
388
|
+
phase="capsule",
|
|
389
|
+
)
|
|
390
|
+
scan.start()
|
|
391
|
+
paths = [
|
|
392
|
+
(package, root, path)
|
|
393
|
+
for package, root in sorted(build_roots.items())
|
|
394
|
+
if root.is_dir()
|
|
395
|
+
for path in sorted(root.rglob("*"))
|
|
396
|
+
if path.is_file()
|
|
397
|
+
]
|
|
398
|
+
scan.advance(f"{len(paths)} files")
|
|
399
|
+
progress = CountedProgress(
|
|
400
|
+
current().emit,
|
|
401
|
+
"capsule.artifact_inventory",
|
|
402
|
+
"Hashing capsule artifacts",
|
|
403
|
+
len(paths),
|
|
404
|
+
phase="capsule",
|
|
405
|
+
)
|
|
406
|
+
progress.start()
|
|
372
407
|
modules: dict[str, tuple[str, list[CapsuleArtifact]]] = {}
|
|
373
|
-
for package, root in
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
for path in sorted(root.rglob("*")):
|
|
377
|
-
if not path.is_file():
|
|
378
|
-
continue
|
|
379
|
-
module = module_from_artifact(path.relative_to(root))
|
|
380
|
-
if module is None:
|
|
381
|
-
continue
|
|
408
|
+
for package, root, path in paths:
|
|
409
|
+
module = module_from_artifact(path.relative_to(root))
|
|
410
|
+
if module is not None:
|
|
382
411
|
relative = path.relative_to(workspace).as_posix()
|
|
383
412
|
capability = artifact_capability(path)
|
|
384
413
|
artifact = CapsuleArtifact(
|
|
@@ -390,6 +419,7 @@ def build_manifest(
|
|
|
390
419
|
f"capsule module {module} is provided by both {existing[0]} and {package}"
|
|
391
420
|
)
|
|
392
421
|
modules.setdefault(module, (package, []))[1].append(artifact)
|
|
422
|
+
progress.advance(path.relative_to(workspace).as_posix())
|
|
393
423
|
known_complete = frozenset(imports) if complete_modules is None else complete_modules
|
|
394
424
|
records = tuple(
|
|
395
425
|
CapsuleModule(
|
|
@@ -436,6 +466,14 @@ def inventory_workspace(
|
|
|
436
466
|
) -> CapsuleManifest:
|
|
437
467
|
"""Build an exact capsule inventory from a verified full workspace."""
|
|
438
468
|
source_roots, build_roots = _package_directories(workspace, lock)
|
|
469
|
+
discovery = CountedProgress(
|
|
470
|
+
current().emit,
|
|
471
|
+
"capsule.module_discovery",
|
|
472
|
+
"Discovering capsule modules",
|
|
473
|
+
len(build_roots) + len(source_roots),
|
|
474
|
+
phase="capsule",
|
|
475
|
+
)
|
|
476
|
+
discovery.start()
|
|
439
477
|
modules_by_package: dict[str, set[str]] = {}
|
|
440
478
|
for package, root in build_roots.items():
|
|
441
479
|
modules_by_package[package] = {
|
|
@@ -443,11 +481,13 @@ def inventory_workspace(
|
|
|
443
481
|
for path in root.rglob("*.olean")
|
|
444
482
|
if (module := module_from_artifact(path.relative_to(root))) is not None
|
|
445
483
|
}
|
|
484
|
+
discovery.advance(package)
|
|
446
485
|
|
|
447
486
|
source_modules: dict[Path, str] = {}
|
|
448
487
|
claimed: dict[str, tuple[int, Path]] = {}
|
|
449
488
|
for package, root in source_roots.items():
|
|
450
489
|
if not root.is_dir():
|
|
490
|
+
discovery.advance(package)
|
|
451
491
|
continue
|
|
452
492
|
available = modules_by_package[package]
|
|
453
493
|
for path in sorted(root.rglob("*.lean")):
|
|
@@ -473,6 +513,7 @@ def inventory_workspace(
|
|
|
473
513
|
source_modules.pop(previous[1], None)
|
|
474
514
|
claimed[module] = (stripped, path)
|
|
475
515
|
source_modules[path] = module
|
|
516
|
+
discovery.advance(package)
|
|
476
517
|
|
|
477
518
|
parsed = parse_import_headers(lean_command, tuple(source_modules))
|
|
478
519
|
imports = {source_modules[path]: names for path, names in parsed.items()}
|
|
@@ -1924,6 +1924,7 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1924
1924
|
renderer = ConsoleRenderer(
|
|
1925
1925
|
mode="quiet" if args.quiet or getattr(args, "json", False) else None,
|
|
1926
1926
|
verbose=args.verbose,
|
|
1927
|
+
heartbeat_seconds=2.0,
|
|
1927
1928
|
)
|
|
1928
1929
|
try:
|
|
1929
1930
|
if args.command == "completion":
|
|
@@ -1969,9 +1970,9 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
1969
1970
|
)
|
|
1970
1971
|
if declined is not None:
|
|
1971
1972
|
return declined
|
|
1972
|
-
di_publication = OCIDeclarationIndexPublisher(
|
|
1973
|
-
|
|
1974
|
-
)
|
|
1973
|
+
di_publication = OCIDeclarationIndexPublisher(
|
|
1974
|
+
di_repository, events=runtime.events
|
|
1975
|
+
).publish(tuple(di_item.source for di_item in built.shards), lock_id=lock.lock_id)
|
|
1975
1976
|
if args.sign:
|
|
1976
1977
|
CosignVerifier(executable=runtime.verification_executable).sign(
|
|
1977
1978
|
di_repository, di_publication.manifest_digest
|