lean-runtime 4.26.0__tar.gz → 4.30.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.26.0 → lean_runtime-4.30.0}/CHANGELOG.md +70 -0
- {lean_runtime-4.26.0/lean_runtime.egg-info → lean_runtime-4.30.0}/PKG-INFO +1 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/python-api.md +5 -2
- lean_runtime-4.30.0/lean_runtime/_paths.py +87 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/backends.py +35 -4
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/bundles.py +13 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/cli.py +1 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/console.py +142 -11
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/declaration_index.py +5 -4
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/locking.py +33 -11
- lean_runtime-4.30.0/lean_runtime/package_ids.py +42 -0
- lean_runtime-4.30.0/lean_runtime/progress.py +95 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/project_execution.py +24 -4
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/project_sharing.py +60 -19
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/projects.py +110 -12
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/run_cli.py +6 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/runtime.py +26 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/shared_projects.py +14 -8
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/store.py +15 -10
- {lean_runtime-4.26.0 → lean_runtime-4.30.0/lean_runtime.egg-info}/PKG-INFO +1 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/SOURCES.txt +4 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/pyproject.toml +1 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/conftest.py +12 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_backend.py +23 -2
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_header_cache.py +1 -0
- lean_runtime-4.30.0/tests/test_package_ids.py +44 -0
- lean_runtime-4.30.0/tests/test_paths.py +103 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_programs.py +1 -0
- lean_runtime-4.30.0/tests/test_progress.py +251 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_projects.py +123 -9
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_runtime.py +22 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_store.py +2 -1
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_toolchain_oci.py +2 -1
- lean_runtime-4.26.0/lean_runtime/_paths.py +0 -38
- lean_runtime-4.26.0/tests/test_paths.py +0 -25
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/LICENSE +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/MANIFEST.in +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/README.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/environments.toml +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.33.1.lock.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/README.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.32.2.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.32.2.toml +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.33.0.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.33.0.toml +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/concepts/context-selection.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/index.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/commands.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/python-advanced.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/storage.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/trust.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/tutorial.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/workflows/check-files.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/workflows/lake-projects.md +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/examples/mathlib.toml +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/__init__.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/__main__.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/_git.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/capsules.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/comparison.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/context_resolution.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/decisions.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/declaration_index_build.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/declaration_index_oci.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/diagnostics.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/__init__.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/analyzer.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/api.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/candidate.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog_build.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog_cli.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/data/catalog.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/defaults.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/engine.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/errors.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/history.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/memo.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/module_inventory.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/planner.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/policy.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/probe.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/py.typed +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/result.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schema_resources.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/environments.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/errors.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/events.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/facade.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/frontmatter.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/header_cache.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/health.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/identifier_resolver.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/import_syntax.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/lake.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/lake_cache.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/lockfiles.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/matrix.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/models.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/oci.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/oci_protocol.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/packs.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/policies.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/profiling.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/programs.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/publisher_verification.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/py.typed +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/references.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/resolver.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/schema_resources.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/serialization.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/specs.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/timings.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/toolchain_oci.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/toolchain_slim.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/toolchains.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/verification.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/wire.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/entry_points.txt +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/requires.txt +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/top_level.txt +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/mkdocs.yml +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/attestation-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/check-batch-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/cleanup-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/comparison-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/execution-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/inspect-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/matrix-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/plan-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/profile-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/publication-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/status-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/verify-v1.schema.json +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/announcement_gate.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/catalog_update.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/lake_cache_gate.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/prepare_declaration_index.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/publish_declaration_index.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/registry_preflight.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/run_compatibility.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/run_v1_case_study.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/smoke_wheel.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/setup.cfg +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_analyzer.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_catalog.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_catalog_build.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_engine.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_memo.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_planner.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_policy.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_announcement_gate.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_bundles.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_capsules.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_cli.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_console.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_context_resolution.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_declaration_index.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_diagnostics.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_environment_integration.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_events_health.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_facade.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_frontmatter.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_git.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_identifier_resolver.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_interactive.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_lake_cache.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_lockfiles.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_locking.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_models.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_multifile.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_oci_protocol.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_packs.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_prebuilt_policy.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_project_bootstrap.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_project_publish.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_references.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_run_cli.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_schema_resources.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_schemas.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_signatures.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_specs.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_toolchain_slim.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_toolchains.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_v1_precision.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_verification_inventory.py +0 -0
- {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_verification_probe.py +0 -0
|
@@ -2,6 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
## Unreleased
|
|
4
4
|
|
|
5
|
+
## 4.30.0 - 2026-08-26
|
|
6
|
+
|
|
7
|
+
- Attach shared dependencies on Windows without administrator rights. Creating
|
|
8
|
+
a symbolic link needs `SeCreateSymbolicLinkPrivilege` (administrators or
|
|
9
|
+
Developer Mode), so `adopt` and `project attach` failed with
|
|
10
|
+
`WinError 1314: A required privilege is not held by the client` on a normal
|
|
11
|
+
account, and the pre-attach probe never caught it. When symlink creation is
|
|
12
|
+
denied, the package link is now created as a directory junction, which any
|
|
13
|
+
user may create and which Lake and Git traverse identically. Every place
|
|
14
|
+
that recognizes an attached package as a link (`status`, `verify`,
|
|
15
|
+
`unshare`, adoption planning, rollback, source fingerprinting) now
|
|
16
|
+
recognizes junctions too.
|
|
17
|
+
- Size local dependency checkouts during `adopt` without descending into
|
|
18
|
+
attached links, using one directory scan per level instead of `rglob`.
|
|
19
|
+
- Show progress while subprocesses run. Lake's `[n/m]` build steps now stream
|
|
20
|
+
from `check`, `build`, and artifact hydration as `process.progress` events
|
|
21
|
+
(rate-limited to ten per second), rendered on a terminal as one in-place bar
|
|
22
|
+
(`lake [████░░░░] 3300/8318 · Building Mathlib.Foo`) and in logs as 25%
|
|
23
|
+
checkpoints; every other output line becomes a throttled `process.output`
|
|
24
|
+
heartbeat shown in place on a terminal, so `lake exe cache get` is no longer
|
|
25
|
+
silent. Third-party backends that do not accept the new optional `on_output`
|
|
26
|
+
callback keep working unchanged.
|
|
27
|
+
- Announce counted phases that used to be silent: `adopt.inspect_started` and
|
|
28
|
+
`adopt.identity_started` for each project during adoption planning, and
|
|
29
|
+
`project.detach.package_started` for each package `unshare` materializes; all
|
|
30
|
+
carry `current`/`total` and render as the same counted bar.
|
|
31
|
+
- Keep the terminal alive during any silent stretch: when no event has arrived
|
|
32
|
+
for two seconds the console shows `⋯ <last activity> (Ns)` in place; plain
|
|
33
|
+
logs print `Still working: …` after ten seconds and then every thirty.
|
|
34
|
+
- Name new shared package directories `pkg_<32 hex>` instead of
|
|
35
|
+
`project_package_<64 hex>`. The 80-character name pushed Mathlib's deepest
|
|
36
|
+
build outputs past Windows' 260-character path limit, so an adopted Mathlib
|
|
37
|
+
project could hydrate its cache but never finish `build`
|
|
38
|
+
(`failed to create file '…\Isometric.olean.server'`). The digest is the same
|
|
39
|
+
identity truncated to 128 bits; existing `project_package_*` directories stay
|
|
40
|
+
valid and reusable. To move already adopted projects onto short paths,
|
|
41
|
+
`lean-runtime project unshare` each of them, delete the legacy
|
|
42
|
+
`project-packages/project_package_*` directories (nothing references them once
|
|
43
|
+
every project is unshared; `clean` does not yet reclaim them), and
|
|
44
|
+
`lean-runtime adopt` again.
|
|
45
|
+
- Windows correctness fixes surfaced by running the suite on Windows:
|
|
46
|
+
compact source snapshots stage under a shorter directory name so Git's
|
|
47
|
+
shallow-clone metadata stays below `MAX_PATH` (`'$GIT_DIR' too big`);
|
|
48
|
+
`lean-runtime run` rewrites a container backend's POSIX staged path back to
|
|
49
|
+
the user's file on Windows hosts; lock waiters learn the holding operation
|
|
50
|
+
through a `.owner` sidecar (a byte-locked file cannot be read on Windows);
|
|
51
|
+
declaration shards close their SQLite connection before publication so the
|
|
52
|
+
rename succeeds; `storage` deduplicates hard links via `os.stat()` because
|
|
53
|
+
`DirEntry.stat()` reports no inode on Windows; `publish` accepts a
|
|
54
|
+
drive-letter origin path (`C:\...`) instead of treating `C` as a URL scheme;
|
|
55
|
+
bundle import accepts CRLF working files whose LF-normalized blob is the
|
|
56
|
+
recorded object (`core.autocrlf`); and storage cleanup clamps negative file
|
|
57
|
+
ages from filesystem timestamp granularity.
|
|
58
|
+
|
|
59
|
+
## 4.27.0 - 2026-08-26
|
|
60
|
+
|
|
61
|
+
- Fingerprint a project by what Lake compiles, not by everything beside the
|
|
62
|
+
lakefile. The workspace digest that keys header snapshots and provenance now
|
|
63
|
+
hashes `lakefile.toml`/`lakefile.lean`, `lean-toolchain`, `lake-manifest.json`,
|
|
64
|
+
and the `.lean` files under each declared target's module roots (the whole
|
|
65
|
+
tree, minus `.git` and `.lake`, when the lakefile is not declarative).
|
|
66
|
+
Previously every byte under the project root was read on each `check` and
|
|
67
|
+
`build`, so a lakefile above a data directory, a virtual environment, or an
|
|
68
|
+
unrelated monorepo made the first step of a check run for minutes with no
|
|
69
|
+
output. Digests change value once because of this.
|
|
70
|
+
- Announce project fingerprinting as `project.fingerprint_started` and
|
|
71
|
+
`project.fingerprint_finished` events (phase `fingerprint`, with
|
|
72
|
+
`elapsed_ms`), so `--verbose` never goes silent between `check.started` and
|
|
73
|
+
the first Lake invocation.
|
|
74
|
+
|
|
5
75
|
## 4.26.0 - 2026-08-26
|
|
6
76
|
|
|
7
77
|
- Stop `status` claiming a download is required for core-only files. A lock
|
|
@@ -137,8 +137,11 @@ and lock identities, exact package commits and Git tree hashes, toolchain,
|
|
|
137
137
|
platform, backend, policy, source digest, and start time.
|
|
138
138
|
|
|
139
139
|
Mutable project results have no environment or lock identity. Their provenance
|
|
140
|
-
instead records the canonical root, a workspace digest
|
|
141
|
-
|
|
140
|
+
instead records the canonical root, a workspace digest of what Lake compiles
|
|
141
|
+
(the lakefile, toolchain pin, manifest, and the `.lean` files under each
|
|
142
|
+
declared target's module roots; the whole tree minus `.git` and `.lake` when the
|
|
143
|
+
lakefile is not declarative), Lake configuration and manifest digests, and Git
|
|
144
|
+
revision state.
|
|
142
145
|
|
|
143
146
|
## Work with Lake projects
|
|
144
147
|
|
|
@@ -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()
|
|
@@ -360,6 +360,18 @@ def _git_object_id(kind: str, data: bytes) -> str:
|
|
|
360
360
|
return hashlib.sha1(framed, usedforsecurity=False).hexdigest() # noqa: S324
|
|
361
361
|
|
|
362
362
|
|
|
363
|
+
def _matches_normalized(data: bytes, object_id: str) -> bool:
|
|
364
|
+
"""Accept a CRLF working file whose LF-normalized blob is the recorded one.
|
|
365
|
+
|
|
366
|
+
Git with ``core.autocrlf`` (the Git for Windows default) stores text blobs
|
|
367
|
+
with LF endings while checking them out with CRLF, so the on-disk bytes of
|
|
368
|
+
an untouched file legitimately hash differently from its recorded object.
|
|
369
|
+
"""
|
|
370
|
+
if b"\r\n" not in data:
|
|
371
|
+
return False
|
|
372
|
+
return _git_object_id("blob", data.replace(b"\r\n", b"\n")) == object_id
|
|
373
|
+
|
|
374
|
+
|
|
363
375
|
def _source_tree_inventory(root: Path, package: LockedPackage) -> bytes:
|
|
364
376
|
process = subprocess.run(
|
|
365
377
|
git_command("-C", str(root), "ls-tree", "-rz", "--full-tree", "HEAD"),
|
|
@@ -475,7 +487,7 @@ def _verify_source_tree_inventory(root: Path, package: LockedPackage, path: Path
|
|
|
475
487
|
if executable != (mode == "100755"):
|
|
476
488
|
raise EnvironmentError(f"bundled package source mode mismatch: {package.name}")
|
|
477
489
|
data = source.read_bytes()
|
|
478
|
-
if _git_object_id("blob", data) != object_id:
|
|
490
|
+
if _git_object_id("blob", data) != object_id and not _matches_normalized(data, object_id):
|
|
479
491
|
raise EnvironmentError(f"bundled package source mismatch: {package.name}")
|
|
480
492
|
entries.append(entry)
|
|
481
493
|
if _inventory_tree_id(entries) != package.tree_hash:
|
|
@@ -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":
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import threading
|
|
7
8
|
import time
|
|
8
9
|
from typing import Any, Literal, TextIO
|
|
9
10
|
|
|
@@ -16,6 +17,15 @@ _BAR_WIDTH = 20
|
|
|
16
17
|
_TTY_REDRAW_SECONDS = 0.1
|
|
17
18
|
_PLAIN_CHECKPOINT_PERCENT = 25
|
|
18
19
|
_LARGE_CLOSURE_BYTES = 1024**3
|
|
20
|
+
_HEARTBEAT_POLL_SECONDS = 0.25
|
|
21
|
+
_PLAIN_HEARTBEAT_FIRST_SECONDS = 10.0
|
|
22
|
+
_PLAIN_HEARTBEAT_REPEAT_SECONDS = 30.0
|
|
23
|
+
_DETAIL_WIDTH = 60
|
|
24
|
+
_OUTPUT_WIDTH = 100
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _truncate(text: str, width: int) -> str:
|
|
28
|
+
return text if len(text) <= width else text[: width - 1] + "…"
|
|
19
29
|
|
|
20
30
|
|
|
21
31
|
def select_mode(*, quiet: bool = False, stream: TextIO | None = None) -> RenderMode:
|
|
@@ -95,6 +105,7 @@ class ConsoleRenderer:
|
|
|
95
105
|
verbose: bool = False,
|
|
96
106
|
color: bool | None = None,
|
|
97
107
|
clock: Any = time.monotonic,
|
|
108
|
+
heartbeat_seconds: float | None = None,
|
|
98
109
|
) -> None:
|
|
99
110
|
self.stream = stream if stream is not None else sys.stderr
|
|
100
111
|
self.mode: RenderMode = mode if mode is not None else select_mode(stream=self.stream)
|
|
@@ -107,26 +118,77 @@ class ConsoleRenderer:
|
|
|
107
118
|
self._last_redraw: float | None = None
|
|
108
119
|
self._next_checkpoint = _PLAIN_CHECKPOINT_PERCENT
|
|
109
120
|
self._download_finished = False
|
|
121
|
+
self._count_key: tuple[str, int] | None = None
|
|
122
|
+
self._count_checkpoint = _PLAIN_CHECKPOINT_PERCENT
|
|
123
|
+
# Heartbeat: when events stop arriving mid-operation, keep showing the
|
|
124
|
+
# last known activity so a long silent phase never looks hung.
|
|
125
|
+
self._heartbeat_seconds = heartbeat_seconds
|
|
126
|
+
self._lock = threading.RLock()
|
|
127
|
+
self._last_event_at: float | None = None
|
|
128
|
+
self._last_message = ""
|
|
129
|
+
self._next_plain_heartbeat = _PLAIN_HEARTBEAT_FIRST_SECONDS
|
|
130
|
+
self._heartbeat_thread: threading.Thread | None = None
|
|
131
|
+
self._stop = threading.Event()
|
|
110
132
|
|
|
111
133
|
def __call__(self, event: RuntimeEvent) -> None:
|
|
112
134
|
if self.mode == "quiet":
|
|
113
135
|
return
|
|
114
|
-
|
|
115
|
-
self.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
handler(event)
|
|
136
|
+
with self._lock:
|
|
137
|
+
self._note_activity(event)
|
|
138
|
+
if self.verbose:
|
|
139
|
+
self._verbose_line(event)
|
|
140
|
+
return
|
|
141
|
+
handler = getattr(self, "_render_" + event.kind.replace(".", "_"), None)
|
|
142
|
+
if handler is not None:
|
|
143
|
+
handler(event)
|
|
120
144
|
|
|
121
145
|
def note(self, message: str) -> None:
|
|
122
146
|
"""Print one renderer-owned status line."""
|
|
123
147
|
if self.mode == "quiet":
|
|
124
148
|
return
|
|
125
|
-
self.
|
|
149
|
+
with self._lock:
|
|
150
|
+
self._print(message)
|
|
126
151
|
|
|
127
152
|
def close(self) -> None:
|
|
128
153
|
"""Terminate any in-place progress line before final output."""
|
|
129
|
-
self.
|
|
154
|
+
self._stop.set()
|
|
155
|
+
thread = self._heartbeat_thread
|
|
156
|
+
if thread is not None and thread is not threading.current_thread():
|
|
157
|
+
thread.join(timeout=1)
|
|
158
|
+
with self._lock:
|
|
159
|
+
self._end_progress_line()
|
|
160
|
+
|
|
161
|
+
# -- heartbeat --------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
def _note_activity(self, event: RuntimeEvent) -> None:
|
|
164
|
+
self._last_event_at = self._clock()
|
|
165
|
+
self._last_message = event.message
|
|
166
|
+
self._next_plain_heartbeat = _PLAIN_HEARTBEAT_FIRST_SECONDS
|
|
167
|
+
if (
|
|
168
|
+
self._heartbeat_seconds is not None
|
|
169
|
+
and self._heartbeat_thread is None
|
|
170
|
+
and not self._stop.is_set()
|
|
171
|
+
):
|
|
172
|
+
self._heartbeat_thread = threading.Thread(
|
|
173
|
+
target=self._heartbeat_loop, name="lean-runtime-heartbeat", daemon=True
|
|
174
|
+
)
|
|
175
|
+
self._heartbeat_thread.start()
|
|
176
|
+
|
|
177
|
+
def _heartbeat_loop(self) -> None:
|
|
178
|
+
assert self._heartbeat_seconds is not None
|
|
179
|
+
while not self._stop.wait(_HEARTBEAT_POLL_SECONDS):
|
|
180
|
+
with self._lock:
|
|
181
|
+
if self._last_event_at is None or not self._last_message:
|
|
182
|
+
continue
|
|
183
|
+
idle = self._clock() - self._last_event_at
|
|
184
|
+
if idle < self._heartbeat_seconds:
|
|
185
|
+
continue
|
|
186
|
+
if self.mode == "tty":
|
|
187
|
+
text = f"⋯ {_truncate(self._last_message, _OUTPUT_WIDTH)} ({int(idle)}s)"
|
|
188
|
+
self._draw_line(text, self.style.dim(text))
|
|
189
|
+
elif idle >= self._next_plain_heartbeat:
|
|
190
|
+
self._print(f"Still working: {self._last_message} ({int(idle)}s)")
|
|
191
|
+
self._next_plain_heartbeat = idle + _PLAIN_HEARTBEAT_REPEAT_SECONDS
|
|
130
192
|
|
|
131
193
|
# -- individual event renderings ------------------------------------
|
|
132
194
|
|
|
@@ -285,6 +347,67 @@ class ConsoleRenderer:
|
|
|
285
347
|
def _render_project_workspace_lock_wait(self, event: RuntimeEvent) -> None:
|
|
286
348
|
self._print(event.message)
|
|
287
349
|
|
|
350
|
+
# -- subprocess output and counted progress ---------------------------
|
|
351
|
+
|
|
352
|
+
def _render_process_progress(self, event: RuntimeEvent) -> None:
|
|
353
|
+
self._count_from(event, str(event.data.get("label") or "Working"), "detail")
|
|
354
|
+
|
|
355
|
+
def _render_process_output(self, event: RuntimeEvent) -> None:
|
|
356
|
+
if self.mode != "tty":
|
|
357
|
+
return
|
|
358
|
+
line = str(event.data.get("line") or "").strip()
|
|
359
|
+
if line:
|
|
360
|
+
text = _truncate(line, _OUTPUT_WIDTH)
|
|
361
|
+
self._draw_line(text, self.style.dim(text))
|
|
362
|
+
|
|
363
|
+
def _render_adopt_inspect_started(self, event: RuntimeEvent) -> None:
|
|
364
|
+
self._count_from(event, "Inspecting projects", "name")
|
|
365
|
+
|
|
366
|
+
def _render_adopt_identity_started(self, event: RuntimeEvent) -> None:
|
|
367
|
+
self._count_from(event, "Resolving dependency identities", "name")
|
|
368
|
+
|
|
369
|
+
def _render_project_detach_package_started(self, event: RuntimeEvent) -> None:
|
|
370
|
+
self._count_from(event, "Materializing packages", "package")
|
|
371
|
+
|
|
372
|
+
def _count_from(self, event: RuntimeEvent, label: str, detail_key: str) -> None:
|
|
373
|
+
current = event.data.get("current")
|
|
374
|
+
total = event.data.get("total")
|
|
375
|
+
if not isinstance(current, int) or not isinstance(total, int) or total <= 0:
|
|
376
|
+
return
|
|
377
|
+
self._render_count_progress(label, current, total, str(event.data.get(detail_key) or ""))
|
|
378
|
+
|
|
379
|
+
def _render_count_progress(self, label: str, current: int, total: int, detail: str) -> None:
|
|
380
|
+
current = min(current, total)
|
|
381
|
+
if self.mode == "tty":
|
|
382
|
+
now = self._clock()
|
|
383
|
+
if (
|
|
384
|
+
current < total
|
|
385
|
+
and self._last_redraw is not None
|
|
386
|
+
and now - self._last_redraw < _TTY_REDRAW_SECONDS
|
|
387
|
+
):
|
|
388
|
+
return
|
|
389
|
+
self._last_redraw = now
|
|
390
|
+
filled = _BAR_WIDTH * current // total
|
|
391
|
+
bar = "█" * filled + "░" * (_BAR_WIDTH - filled)
|
|
392
|
+
suffix = f" · {_truncate(detail, _DETAIL_WIDTH)}" if detail else ""
|
|
393
|
+
plain = f"{label} [{bar}] {current}/{total}{suffix}"
|
|
394
|
+
styled = (
|
|
395
|
+
f"{label} [{self.style.cyan(bar)}] "
|
|
396
|
+
f"{self.style.bold(f'{current}/{total}')}{self.style.dim(suffix)}"
|
|
397
|
+
)
|
|
398
|
+
self._draw_line(plain, styled)
|
|
399
|
+
if current >= total:
|
|
400
|
+
self._end_progress_line()
|
|
401
|
+
return
|
|
402
|
+
key = (label, total)
|
|
403
|
+
if self._count_key != key:
|
|
404
|
+
self._count_key = key
|
|
405
|
+
self._count_checkpoint = _PLAIN_CHECKPOINT_PERCENT
|
|
406
|
+
percent = current * 100 // total
|
|
407
|
+
while self._count_checkpoint <= percent:
|
|
408
|
+
self._print(f"{label}: {self._count_checkpoint}% ({current}/{total})")
|
|
409
|
+
self._count_checkpoint += _PLAIN_CHECKPOINT_PERCENT
|
|
410
|
+
|
|
288
411
|
# -- low-level output -----------------------------------------------
|
|
289
412
|
|
|
290
413
|
def _verbose_line(self, event: RuntimeEvent) -> None:
|
|
@@ -325,6 +448,13 @@ class ConsoleRenderer:
|
|
|
325
448
|
self.stream.flush()
|
|
326
449
|
self._line_length = len(plain)
|
|
327
450
|
|
|
451
|
+
def _draw_line(self, plain: str, styled: str | None = None) -> None:
|
|
452
|
+
"""Redraw the single in-place status line."""
|
|
453
|
+
padding = " " * max(0, self._line_length - len(plain))
|
|
454
|
+
self.stream.write("\r" + (styled if styled is not None else plain) + padding)
|
|
455
|
+
self.stream.flush()
|
|
456
|
+
self._line_length = len(plain)
|
|
457
|
+
|
|
328
458
|
def _end_progress_line(self) -> None:
|
|
329
459
|
if self._line_length:
|
|
330
460
|
self.stream.write("\n")
|
|
@@ -332,6 +462,7 @@ class ConsoleRenderer:
|
|
|
332
462
|
self._line_length = 0
|
|
333
463
|
|
|
334
464
|
def _print(self, message: str) -> None:
|
|
335
|
-
self.
|
|
336
|
-
|
|
337
|
-
|
|
465
|
+
with self._lock:
|
|
466
|
+
self._end_progress_line()
|
|
467
|
+
self.stream.write(message + "\n")
|
|
468
|
+
self.stream.flush()
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import contextlib
|
|
5
6
|
import re
|
|
6
7
|
import sqlite3
|
|
7
8
|
from dataclasses import dataclass, replace
|
|
@@ -180,7 +181,7 @@ class DeclarationIndex:
|
|
|
180
181
|
|
|
181
182
|
def _validate(self) -> None:
|
|
182
183
|
try:
|
|
183
|
-
with self._connect() as connection:
|
|
184
|
+
with contextlib.closing(self._connect()) as connection:
|
|
184
185
|
version = int(connection.execute("PRAGMA user_version").fetchone()[0])
|
|
185
186
|
metadata = dict(connection.execute("SELECT key, value FROM meta"))
|
|
186
187
|
columns = {
|
|
@@ -226,7 +227,7 @@ class DeclarationIndex:
|
|
|
226
227
|
@property
|
|
227
228
|
def declaration_count(self) -> int:
|
|
228
229
|
try:
|
|
229
|
-
with self._connect() as connection:
|
|
230
|
+
with contextlib.closing(self._connect()) as connection:
|
|
230
231
|
return int(connection.execute("SELECT count(*) FROM decl").fetchone()[0])
|
|
231
232
|
except (sqlite3.Error, TypeError, ValueError) as exc:
|
|
232
233
|
raise EnvironmentError("declaration shard count failed") from exc
|
|
@@ -235,7 +236,7 @@ class DeclarationIndex:
|
|
|
235
236
|
if not valid_declaration_name(name):
|
|
236
237
|
return None
|
|
237
238
|
try:
|
|
238
|
-
with self._connect() as connection:
|
|
239
|
+
with contextlib.closing(self._connect()) as connection:
|
|
239
240
|
row = connection.execute(
|
|
240
241
|
"SELECT name, module, kind, weight FROM decl WHERE name = ?",
|
|
241
242
|
(name,),
|
|
@@ -248,7 +249,7 @@ class DeclarationIndex:
|
|
|
248
249
|
if not valid_declaration_name(suffix) or not 1 <= limit <= 20:
|
|
249
250
|
return ()
|
|
250
251
|
try:
|
|
251
|
-
with self._connect() as connection:
|
|
252
|
+
with contextlib.closing(self._connect()) as connection:
|
|
252
253
|
rows = connection.execute(
|
|
253
254
|
"""
|
|
254
255
|
SELECT d.name, d.module, d.kind, d.weight
|
|
@@ -19,8 +19,9 @@ class FileLock:
|
|
|
19
19
|
"""One advisory cross-process lock.
|
|
20
20
|
|
|
21
21
|
``owner`` is a small JSON-serializable description of the acquiring
|
|
22
|
-
operation;
|
|
23
|
-
|
|
22
|
+
operation; it is published while the lock is held (POSIX: inside the lock
|
|
23
|
+
file, Windows: in a ``.owner`` sidecar, because a byte-locked file cannot
|
|
24
|
+
be read by waiters) so that waiters can attribute their wait. ``on_wait`` is invoked at
|
|
24
25
|
most once, on first contention, with the current holder's description (or
|
|
25
26
|
``None`` when it cannot be read).
|
|
26
27
|
"""
|
|
@@ -44,19 +45,37 @@ class FileLock:
|
|
|
44
45
|
def holder(self) -> dict[str, Any] | None:
|
|
45
46
|
"""Best-effort description of the current lock holder."""
|
|
46
47
|
try:
|
|
47
|
-
value = json.loads(self.
|
|
48
|
+
value = json.loads(self._owner_source().read_text(encoding="utf-8"))
|
|
48
49
|
except (OSError, UnicodeError, json.JSONDecodeError):
|
|
49
50
|
return None
|
|
50
51
|
return value if isinstance(value, dict) else None
|
|
51
52
|
|
|
53
|
+
def _owner_source(self) -> Path:
|
|
54
|
+
"""Where the holder's description lives.
|
|
55
|
+
|
|
56
|
+
POSIX writes it into the lock file itself. Windows mandatory byte locks
|
|
57
|
+
make a locked file unreadable to waiters, so the description lives in a
|
|
58
|
+
sidecar beside the lock instead.
|
|
59
|
+
"""
|
|
60
|
+
if os.name == "nt":
|
|
61
|
+
return self.path.with_name(self.path.name + ".owner")
|
|
62
|
+
return self.path
|
|
63
|
+
|
|
52
64
|
def _write_owner(self, handle: BinaryIO) -> None:
|
|
53
|
-
if self.owner is None
|
|
65
|
+
if self.owner is None:
|
|
54
66
|
return
|
|
55
67
|
try:
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
payload = json.dumps({"pid": os.getpid(), **self.owner}).encode("utf-8")
|
|
69
|
+
if os.name == "nt":
|
|
70
|
+
sidecar = self._owner_source()
|
|
71
|
+
temporary = sidecar.with_name(f"{sidecar.name}.{os.getpid()}.tmp")
|
|
72
|
+
temporary.write_bytes(payload)
|
|
73
|
+
temporary.replace(sidecar)
|
|
74
|
+
else:
|
|
75
|
+
handle.seek(0)
|
|
76
|
+
handle.truncate()
|
|
77
|
+
handle.write(payload)
|
|
78
|
+
handle.flush()
|
|
60
79
|
self._owner_written = True
|
|
61
80
|
except (OSError, TypeError, ValueError):
|
|
62
81
|
self._owner_written = False
|
|
@@ -119,9 +138,12 @@ class FileLock:
|
|
|
119
138
|
return
|
|
120
139
|
if self._owner_written:
|
|
121
140
|
with contextlib.suppress(OSError):
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
141
|
+
if os.name == "nt":
|
|
142
|
+
self._owner_source().unlink()
|
|
143
|
+
else:
|
|
144
|
+
handle.seek(0)
|
|
145
|
+
handle.truncate()
|
|
146
|
+
handle.flush()
|
|
125
147
|
self._owner_written = False
|
|
126
148
|
if os.name == "nt":
|
|
127
149
|
import msvcrt
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"""Names of managed package directories in the shared project store.
|
|
2
|
+
|
|
3
|
+
A package directory is content-addressed by its identity marker. The original
|
|
4
|
+
scheme named directories ``project_package_<64 hex>``; that 80-character name
|
|
5
|
+
pushed Mathlib's deepest build outputs past Windows' 260-character path limit
|
|
6
|
+
(``…\\project_package_<64>\\.lake\\build\\lib\\lean\\Mathlib\\…\\Isometric.olean.server``).
|
|
7
|
+
New directories therefore use ``pkg_<32 hex>``, the same digest truncated to
|
|
8
|
+
128 bits. Legacy directories stay valid and reusable.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from .serialization import sha256_id
|
|
18
|
+
|
|
19
|
+
PACKAGE_ID_PATTERN = re.compile(r"(?:project_package_[0-9a-f]{64}|pkg_[0-9a-f]{32})\Z")
|
|
20
|
+
_DIRECTORY_GLOBS = ("project_package_*", "pkg_*")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def package_directory_id(identity: dict[str, Any]) -> str:
|
|
24
|
+
"""Name the store directory for one package identity marker."""
|
|
25
|
+
digest = sha256_id("project_package", identity).removeprefix("project_package_")
|
|
26
|
+
return f"pkg_{digest[:32]}"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def package_id_matches(identity: dict[str, Any], package_id: str) -> bool:
|
|
30
|
+
"""Whether ``package_id`` names ``identity`` under either naming scheme."""
|
|
31
|
+
return package_id in {sha256_id("project_package", identity), package_directory_id(identity)}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def is_package_id(value: str) -> bool:
|
|
35
|
+
return PACKAGE_ID_PATTERN.fullmatch(value) is not None
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def package_directories(root: Path) -> list[Path]:
|
|
39
|
+
"""Managed package directories under ``root`` in both naming schemes, sorted."""
|
|
40
|
+
return sorted(
|
|
41
|
+
path for pattern in _DIRECTORY_GLOBS for path in root.glob(pattern) if path.is_dir()
|
|
42
|
+
)
|