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.
Files changed (204) hide show
  1. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/CHANGELOG.md +70 -0
  2. {lean_runtime-4.26.0/lean_runtime.egg-info → lean_runtime-4.30.0}/PKG-INFO +1 -1
  3. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/python-api.md +5 -2
  4. lean_runtime-4.30.0/lean_runtime/_paths.py +87 -0
  5. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/backends.py +35 -4
  6. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/bundles.py +13 -1
  7. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/cli.py +1 -0
  8. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/console.py +142 -11
  9. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/declaration_index.py +5 -4
  10. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/locking.py +33 -11
  11. lean_runtime-4.30.0/lean_runtime/package_ids.py +42 -0
  12. lean_runtime-4.30.0/lean_runtime/progress.py +95 -0
  13. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/project_execution.py +24 -4
  14. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/project_sharing.py +60 -19
  15. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/projects.py +110 -12
  16. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/run_cli.py +6 -1
  17. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/runtime.py +26 -0
  18. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/shared_projects.py +14 -8
  19. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/store.py +15 -10
  20. {lean_runtime-4.26.0 → lean_runtime-4.30.0/lean_runtime.egg-info}/PKG-INFO +1 -1
  21. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/SOURCES.txt +4 -0
  22. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/pyproject.toml +1 -1
  23. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/conftest.py +12 -0
  24. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_backend.py +23 -2
  25. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_header_cache.py +1 -0
  26. lean_runtime-4.30.0/tests/test_package_ids.py +44 -0
  27. lean_runtime-4.30.0/tests/test_paths.py +103 -0
  28. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_programs.py +1 -0
  29. lean_runtime-4.30.0/tests/test_progress.py +251 -0
  30. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_projects.py +123 -9
  31. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_runtime.py +22 -1
  32. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_store.py +2 -1
  33. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_toolchain_oci.py +2 -1
  34. lean_runtime-4.26.0/lean_runtime/_paths.py +0 -38
  35. lean_runtime-4.26.0/tests/test_paths.py +0 -25
  36. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/LICENSE +0 -0
  37. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/MANIFEST.in +0 -0
  38. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/README.md +0 -0
  39. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/environments.toml +0 -0
  40. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/core-v4.32.2.lock.json +0 -0
  41. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.30.0.5.lock.json +0 -0
  42. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.31.0.lock.json +0 -0
  43. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.32.2.4.lock.json +0 -0
  44. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/leancert-v4.33.0.lock.json +0 -0
  45. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.30.0.lock.json +0 -0
  46. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.31.0.lock.json +0 -0
  47. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.32.2.lock.json +0 -0
  48. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.33.0.lock.json +0 -0
  49. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/catalog/locks/mathlib-v4.33.1.lock.json +0 -0
  50. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/README.md +0 -0
  51. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/lake-artifact-cache-4.33.0.json +0 -0
  52. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.32.2.json +0 -0
  53. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.32.2.toml +0 -0
  54. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.33.0.json +0 -0
  55. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/compatibility/mathlib-4.33.0.toml +0 -0
  56. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/concepts/context-selection.md +0 -0
  57. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/index.md +0 -0
  58. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/commands.md +0 -0
  59. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/python-advanced.md +0 -0
  60. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/reference/storage.md +0 -0
  61. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/trust.md +0 -0
  62. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/tutorial.md +0 -0
  63. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/workflows/check-files.md +0 -0
  64. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/docs/workflows/lake-projects.md +0 -0
  65. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/examples/mathlib.toml +0 -0
  66. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/__init__.py +0 -0
  67. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/__main__.py +0 -0
  68. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/_git.py +0 -0
  69. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/capsules.py +0 -0
  70. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/comparison.py +0 -0
  71. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/context_resolution.py +0 -0
  72. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/decisions.py +0 -0
  73. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/declaration_index_build.py +0 -0
  74. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/declaration_index_oci.py +0 -0
  75. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/diagnostics.py +0 -0
  76. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/__init__.py +0 -0
  77. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/analyzer.py +0 -0
  78. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/api.py +0 -0
  79. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/candidate.py +0 -0
  80. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog.py +0 -0
  81. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog_build.py +0 -0
  82. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog_cli.py +0 -0
  83. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/catalog_manifest.py +0 -0
  84. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/data/catalog.json +0 -0
  85. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/defaults.py +0 -0
  86. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/engine.py +0 -0
  87. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/errors.py +0 -0
  88. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/history.py +0 -0
  89. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/memo.py +0 -0
  90. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/module_inventory.py +0 -0
  91. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/planner.py +0 -0
  92. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/policy.py +0 -0
  93. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/probe.py +0 -0
  94. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/py.typed +0 -0
  95. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/result.py +0 -0
  96. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schema_resources.py +0 -0
  97. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schemas/catalog-v1.schema.json +0 -0
  98. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schemas/plan-v1.schema.json +0 -0
  99. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/discovery/schemas/result-v1.schema.json +0 -0
  100. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/environments.py +0 -0
  101. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/errors.py +0 -0
  102. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/events.py +0 -0
  103. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/facade.py +0 -0
  104. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/frontmatter.py +0 -0
  105. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/header_cache.py +0 -0
  106. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/health.py +0 -0
  107. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/identifier_resolver.py +0 -0
  108. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/import_syntax.py +0 -0
  109. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/lake.py +0 -0
  110. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/lake_cache.py +0 -0
  111. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/lockfiles.py +0 -0
  112. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/matrix.py +0 -0
  113. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/models.py +0 -0
  114. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/oci.py +0 -0
  115. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/oci_protocol.py +0 -0
  116. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/packs.py +0 -0
  117. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/policies.py +0 -0
  118. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/profiling.py +0 -0
  119. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/programs.py +0 -0
  120. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/publisher_verification.py +0 -0
  121. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/py.typed +0 -0
  122. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/references.py +0 -0
  123. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/resolver.py +0 -0
  124. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/schema_resources.py +0 -0
  125. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/serialization.py +0 -0
  126. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/specs.py +0 -0
  127. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/timings.py +0 -0
  128. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/toolchain_oci.py +0 -0
  129. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/toolchain_slim.py +0 -0
  130. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/toolchains.py +0 -0
  131. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/verification.py +0 -0
  132. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime/wire.py +0 -0
  133. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/dependency_links.txt +0 -0
  134. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/entry_points.txt +0 -0
  135. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/requires.txt +0 -0
  136. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/lean_runtime.egg-info/top_level.txt +0 -0
  137. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/mkdocs.yml +0 -0
  138. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/attestation-v1.schema.json +0 -0
  139. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/check-batch-v1.schema.json +0 -0
  140. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/cleanup-v1.schema.json +0 -0
  141. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/comparison-v1.schema.json +0 -0
  142. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/execution-v1.schema.json +0 -0
  143. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/inspect-v1.schema.json +0 -0
  144. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/matrix-v1.schema.json +0 -0
  145. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/plan-v1.schema.json +0 -0
  146. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/profile-v1.schema.json +0 -0
  147. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/publication-v1.schema.json +0 -0
  148. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/status-v1.schema.json +0 -0
  149. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/schemas/verify-v1.schema.json +0 -0
  150. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/announcement_gate.py +0 -0
  151. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/catalog_update.py +0 -0
  152. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/lake_cache_gate.py +0 -0
  153. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/prepare_declaration_index.py +0 -0
  154. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/publish_declaration_index.py +0 -0
  155. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/registry_preflight.py +0 -0
  156. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/run_compatibility.py +0 -0
  157. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/run_v1_case_study.py +0 -0
  158. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/scripts/smoke_wheel.py +0 -0
  159. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/setup.cfg +0 -0
  160. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/e2e/test_real_discovery.py +0 -0
  161. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_analyzer.py +0 -0
  162. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_catalog.py +0 -0
  163. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_catalog_build.py +0 -0
  164. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_discovery_schema_resources.py +0 -0
  165. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_engine.py +0 -0
  166. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_memo.py +0 -0
  167. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_planner.py +0 -0
  168. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/discovery/test_policy.py +0 -0
  169. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_announcement_gate.py +0 -0
  170. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_bundles.py +0 -0
  171. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_capsules.py +0 -0
  172. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_cli.py +0 -0
  173. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_console.py +0 -0
  174. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_context_resolution.py +0 -0
  175. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_declaration_index.py +0 -0
  176. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_diagnostics.py +0 -0
  177. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_environment_integration.py +0 -0
  178. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_events_health.py +0 -0
  179. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_facade.py +0 -0
  180. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_frontmatter.py +0 -0
  181. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_git.py +0 -0
  182. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_identifier_resolver.py +0 -0
  183. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_interactive.py +0 -0
  184. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_lake_cache.py +0 -0
  185. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_lockfiles.py +0 -0
  186. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_locking.py +0 -0
  187. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_models.py +0 -0
  188. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_multifile.py +0 -0
  189. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_oci_protocol.py +0 -0
  190. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_packs.py +0 -0
  191. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_prebuilt_policy.py +0 -0
  192. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_project_bootstrap.py +0 -0
  193. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_project_publish.py +0 -0
  194. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_references.py +0 -0
  195. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_run_cli.py +0 -0
  196. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_schema_resources.py +0 -0
  197. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_schemas.py +0 -0
  198. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_signatures.py +0 -0
  199. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_specs.py +0 -0
  200. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_toolchain_slim.py +0 -0
  201. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_toolchains.py +0 -0
  202. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_v1_precision.py +0 -0
  203. {lean_runtime-4.26.0 → lean_runtime-4.30.0}/tests/test_verification_inventory.py +0 -0
  204. {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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lean-runtime
3
- Version: 4.26.0
3
+ Version: 4.30.0
4
4
  Summary: Run Lean 4 proofs from Python or standalone files
5
5
  Author: Alejandro Radisic
6
6
  License-Expression: Apache-2.0
@@ -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 excluding `.git` and
141
- `.lake`, Lake configuration and manifest digests, and Git revision state.
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
- def _drain(stream: BinaryIO, budget: _OutputBudget, chunks: list[bytes]) -> None:
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
- return
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(target=_drain, args=(process.stdout, budget, stdout_chunks)),
317
- threading.Thread(target=_drain, args=(process.stderr, budget, stderr_chunks)),
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
- if self.verbose:
115
- self._verbose_line(event)
116
- return
117
- handler = getattr(self, "_render_" + event.kind.replace(".", "_"), None)
118
- if handler is not None:
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._print(message)
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._end_progress_line()
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._end_progress_line()
336
- self.stream.write(message + "\n")
337
- self.stream.flush()
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; on POSIX it is written into the lock file while the lock is
23
- held so that waiters can attribute their wait. ``on_wait`` is invoked at
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.path.read_text(encoding="utf-8"))
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 or os.name == "nt":
65
+ if self.owner is None:
54
66
  return
55
67
  try:
56
- handle.seek(0)
57
- handle.truncate()
58
- handle.write(json.dumps({"pid": os.getpid(), **self.owner}).encode("utf-8"))
59
- handle.flush()
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
- handle.seek(0)
123
- handle.truncate()
124
- handle.flush()
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
+ )