seampact-ir 1.0.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 (131) hide show
  1. seampact_ir-1.0.0/.gitignore +6 -0
  2. seampact_ir-1.0.0/LICENSE +21 -0
  3. seampact_ir-1.0.0/PKG-INFO +17 -0
  4. seampact_ir-1.0.0/README.md +1 -0
  5. seampact_ir-1.0.0/pyproject.toml +36 -0
  6. seampact_ir-1.0.0/src/seampact/ir/__init__.py +1 -0
  7. seampact_ir-1.0.0/src/seampact/ir/binary/__init__.py +23 -0
  8. seampact_ir-1.0.0/src/seampact/ir/binary/absolute_path.py +43 -0
  9. seampact_ir-1.0.0/src/seampact/ir/binary/binaries.json +10 -0
  10. seampact_ir-1.0.0/src/seampact/ir/binary/cache_root.py +52 -0
  11. seampact_ir-1.0.0/src/seampact/ir/binary/cache_safety.py +25 -0
  12. seampact_ir-1.0.0/src/seampact/ir/binary/child_environment.py +15 -0
  13. seampact_ir-1.0.0/src/seampact/ir/binary/compiler_binary.py +115 -0
  14. seampact_ir-1.0.0/src/seampact/ir/binary/dependency_overrides.py +92 -0
  15. seampact_ir-1.0.0/src/seampact/ir/binary/documents_command.py +14 -0
  16. seampact_ir-1.0.0/src/seampact/ir/binary/download.py +73 -0
  17. seampact_ir-1.0.0/src/seampact/ir/binary/failure.py +38 -0
  18. seampact_ir-1.0.0/src/seampact/ir/binary/host_libc.py +39 -0
  19. seampact_ir-1.0.0/src/seampact/ir/binary/host_platform.py +55 -0
  20. seampact_ir-1.0.0/src/seampact/ir/binary/installed_binary.py +38 -0
  21. seampact_ir-1.0.0/src/seampact/ir/binary/installer.py +208 -0
  22. seampact_ir-1.0.0/src/seampact/ir/binary/override.py +112 -0
  23. seampact_ir-1.0.0/src/seampact/ir/binary/pins.py +55 -0
  24. seampact_ir-1.0.0/src/seampact/ir/binary/posix_cache_safety.py +173 -0
  25. seampact_ir-1.0.0/src/seampact/ir/binary/release_assets.py +20 -0
  26. seampact_ir-1.0.0/src/seampact/ir/binary/root_project.py +76 -0
  27. seampact_ir-1.0.0/src/seampact/ir/binary/runner.py +251 -0
  28. seampact_ir-1.0.0/src/seampact/ir/binary/wheel.py +22 -0
  29. seampact_ir-1.0.0/src/seampact/ir/binary/windows_cache_safety.py +121 -0
  30. seampact_ir-1.0.0/src/seampact/ir/binary/windows_path_facts.py +139 -0
  31. seampact_ir-1.0.0/src/seampact/ir/bundle/__init__.py +20 -0
  32. seampact_ir-1.0.0/src/seampact/ir/bundle/archive.py +84 -0
  33. seampact_ir-1.0.0/src/seampact/ir/bundle/cache.py +68 -0
  34. seampact_ir-1.0.0/src/seampact/ir/bundle/channel.py +9 -0
  35. seampact_ir-1.0.0/src/seampact/ir/bundle/directory.py +64 -0
  36. seampact_ir-1.0.0/src/seampact/ir/bundle/discriminator_rewrite.py +80 -0
  37. seampact_ir-1.0.0/src/seampact/ir/bundle/failure.py +56 -0
  38. seampact_ir-1.0.0/src/seampact/ir/bundle/fetched.py +11 -0
  39. seampact_ir-1.0.0/src/seampact/ir/bundle/fetcher.py +188 -0
  40. seampact_ir-1.0.0/src/seampact/ir/bundle/files.py +12 -0
  41. seampact_ir-1.0.0/src/seampact/ir/bundle/git_fetch.py +372 -0
  42. seampact_ir-1.0.0/src/seampact/ir/bundle/integrity.py +20 -0
  43. seampact_ir-1.0.0/src/seampact/ir/bundle/ir_check.py +119 -0
  44. seampact_ir-1.0.0/src/seampact/ir/bundle/layout.py +22 -0
  45. seampact_ir-1.0.0/src/seampact/ir/bundle/manifest.py +100 -0
  46. seampact_ir-1.0.0/src/seampact/ir/bundle/manifest_reader.py +58 -0
  47. seampact_ir-1.0.0/src/seampact/ir/bundle/manifest_rules.py +94 -0
  48. seampact_ir-1.0.0/src/seampact/ir/bundle/path.py +81 -0
  49. seampact_ir-1.0.0/src/seampact/ir/bundle/release_fetch.py +60 -0
  50. seampact_ir-1.0.0/src/seampact/ir/bundle/source.py +55 -0
  51. seampact_ir-1.0.0/src/seampact/ir/bundle/spec_schema.py +76 -0
  52. seampact_ir-1.0.0/src/seampact/ir/bundle/text.py +17 -0
  53. seampact_ir-1.0.0/src/seampact/ir/bundle/untrusted_json.py +95 -0
  54. seampact_ir-1.0.0/src/seampact/ir/bundle/ustar.py +346 -0
  55. seampact_ir-1.0.0/src/seampact/ir/bundle/verification.py +29 -0
  56. seampact_ir-1.0.0/src/seampact/ir/bundle/verifier.py +195 -0
  57. seampact_ir-1.0.0/src/seampact/ir/compatibility/python-targets.json +71 -0
  58. seampact_ir-1.0.0/src/seampact/ir/config/__init__.py +19 -0
  59. seampact_ir-1.0.0/src/seampact/ir/config/app_config.py +101 -0
  60. seampact_ir-1.0.0/src/seampact/ir/config/config_kind.py +16 -0
  61. seampact_ir-1.0.0/src/seampact/ir/config/config_path.py +44 -0
  62. seampact_ir-1.0.0/src/seampact/ir/config/config_tree.py +17 -0
  63. seampact_ir-1.0.0/src/seampact/ir/config/entry_view.py +261 -0
  64. seampact_ir-1.0.0/src/seampact/ir/config/javascript_number.py +55 -0
  65. seampact_ir-1.0.0/src/seampact/ir/config/json_value.py +66 -0
  66. seampact_ir-1.0.0/src/seampact/ir/config/key_path.py +31 -0
  67. seampact_ir-1.0.0/src/seampact/ir/config/local_source_policy.py +30 -0
  68. seampact_ir-1.0.0/src/seampact/ir/config/problem.py +27 -0
  69. seampact_ir-1.0.0/src/seampact/ir/config/reader.py +344 -0
  70. seampact_ir-1.0.0/src/seampact/ir/config/schema_check.py +279 -0
  71. seampact_ir-1.0.0/src/seampact/ir/config/target_ecosystem.py +27 -0
  72. seampact_ir-1.0.0/src/seampact/ir/config/waits.py +59 -0
  73. seampact_ir-1.0.0/src/seampact/ir/config/workspace_api_names.py +52 -0
  74. seampact_ir-1.0.0/src/seampact/ir/config/yaml_reader.py +329 -0
  75. seampact_ir-1.0.0/src/seampact/ir/https/__init__.py +6 -0
  76. seampact_ir-1.0.0/src/seampact/ir/https/download.py +269 -0
  77. seampact_ir-1.0.0/src/seampact/ir/https/printable_url.py +58 -0
  78. seampact_ir-1.0.0/src/seampact/ir/https/untrusted_text.py +16 -0
  79. seampact_ir-1.0.0/src/seampact/ir/hydration.py +295 -0
  80. seampact_ir-1.0.0/src/seampact/ir/model/__init__.py +7544 -0
  81. seampact_ir-1.0.0/src/seampact/ir/model/shapes.py +1005 -0
  82. seampact_ir-1.0.0/src/seampact/ir/model/version.py +9 -0
  83. seampact_ir-1.0.0/src/seampact/ir/naming/__init__.py +17 -0
  84. seampact_ir-1.0.0/src/seampact/ir/naming/name.py +134 -0
  85. seampact_ir-1.0.0/src/seampact/ir/naming/python_identifier.py +42 -0
  86. seampact_ir-1.0.0/src/seampact/ir/naming/python_reserved.py +58 -0
  87. seampact_ir-1.0.0/src/seampact/ir/naming/reserved-words.json +780 -0
  88. seampact_ir-1.0.0/src/seampact/ir/naming/split.py +89 -0
  89. seampact_ir-1.0.0/src/seampact/ir/py.typed +0 -0
  90. seampact_ir-1.0.0/src/seampact/ir/read/__init__.py +15 -0
  91. seampact_ir-1.0.0/src/seampact/ir/read/must_act.py +129 -0
  92. seampact_ir-1.0.0/src/seampact/ir/read/nesting.py +233 -0
  93. seampact_ir-1.0.0/src/seampact/ir/read/parse.py +29 -0
  94. seampact_ir-1.0.0/src/seampact/ir/read/reader.py +70 -0
  95. seampact_ir-1.0.0/src/seampact/ir/read/refusal.py +144 -0
  96. seampact_ir-1.0.0/src/seampact/ir/read/support.py +179 -0
  97. seampact_ir-1.0.0/src/seampact/ir/run/__init__.py +26 -0
  98. seampact_ir-1.0.0/src/seampact/ir/run/command.py +109 -0
  99. seampact_ir-1.0.0/src/seampact/ir/run/contract_apis.py +202 -0
  100. seampact_ir-1.0.0/src/seampact/ir/run/discovery.py +121 -0
  101. seampact_ir-1.0.0/src/seampact/ir/run/documents.py +103 -0
  102. seampact_ir-1.0.0/src/seampact/ir/run/entry_plan.py +23 -0
  103. seampact_ir-1.0.0/src/seampact/ir/run/framework_docs.py +31 -0
  104. seampact_ir-1.0.0/src/seampact/ir/run/generator.py +101 -0
  105. seampact_ir-1.0.0/src/seampact/ir/run/matrix_check.py +172 -0
  106. seampact_ir-1.0.0/src/seampact/ir/run/matrix_framework.py +126 -0
  107. seampact_ir-1.0.0/src/seampact/ir/run/native_docs_document.py +243 -0
  108. seampact_ir-1.0.0/src/seampact/ir/run/problems.py +40 -0
  109. seampact_ir-1.0.0/src/seampact/ir/run/report.py +19 -0
  110. seampact_ir-1.0.0/src/seampact/ir/run/runner.py +475 -0
  111. seampact_ir-1.0.0/src/seampact/ir/schemas/bundle/manifest.schema.json +103 -0
  112. seampact_ir-1.0.0/src/seampact/ir/schemas/config/app.schema.json +552 -0
  113. seampact_ir-1.0.0/src/seampact/ir/schemas/config/root.schema.json +288 -0
  114. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/common.schema.json +250 -0
  115. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/declarations.schema.json +578 -0
  116. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/formats.schema.json +263 -0
  117. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/ir.schema.json +375 -0
  118. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/operation.schema.json +747 -0
  119. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/schemas.schema.json +450 -0
  120. seampact_ir-1.0.0/src/seampact/ir/schemas/ir/source-map.schema.json +41 -0
  121. seampact_ir-1.0.0/src/seampact/ir/shape.py +46 -0
  122. seampact_ir-1.0.0/src/seampact/ir/write/__init__.py +19 -0
  123. seampact_ir-1.0.0/src/seampact/ir/write/code_unit_order.py +22 -0
  124. seampact_ir-1.0.0/src/seampact/ir/write/generated_file.py +9 -0
  125. seampact_ir-1.0.0/src/seampact/ir/write/header.py +77 -0
  126. seampact_ir-1.0.0/src/seampact/ir/write/manifest.py +91 -0
  127. seampact_ir-1.0.0/src/seampact/ir/write/paths.py +129 -0
  128. seampact_ir-1.0.0/src/seampact/ir/write/policy.py +9 -0
  129. seampact_ir-1.0.0/src/seampact/ir/write/refusal.py +10 -0
  130. seampact_ir-1.0.0/src/seampact/ir/write/safe_path.py +26 -0
  131. seampact_ir-1.0.0/src/seampact/ir/write/writer.py +272 -0
@@ -0,0 +1,6 @@
1
+ .venv/
2
+ __pycache__/
3
+ .mypy_cache/
4
+ .pytest_cache/
5
+ .ruff_cache/
6
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Julien Metral
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.5
2
+ Name: seampact-ir
3
+ Version: 1.0.0
4
+ Summary: The seampact IR model, naming and reader for Python generators.
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: Programming Language :: Python :: 3 :: Only
9
+ Classifier: Typing :: Typed
10
+ Requires-Python: >=3.12
11
+ Requires-Dist: jsonschema==4.26.0
12
+ Requires-Dist: ruamel-yaml==0.19.1
13
+ Provides-Extra: compiler
14
+ Requires-Dist: seampact-compiler==1.0.0; extra == 'compiler'
15
+ Description-Content-Type: text/markdown
16
+
17
+ The seampact IR model generated from the IR JSON Schema, the native naming, the IR reader, the bundle verifier and the compiler binary runner, for Python generators.
@@ -0,0 +1 @@
1
+ The seampact IR model generated from the IR JSON Schema, the native naming, the IR reader, the bundle verifier and the compiler binary runner, for Python generators.
@@ -0,0 +1,36 @@
1
+ [build-system]
2
+ requires = ["hatchling==1.33.0"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "seampact-ir"
7
+ version = "1.0.0"
8
+ description = "The seampact IR model, naming and reader for Python generators."
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.12"
13
+ dependencies = ["jsonschema==4.26.0", "ruamel.yaml==0.19.1"]
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "Programming Language :: Python :: 3 :: Only",
17
+ "Typing :: Typed",
18
+ ]
19
+
20
+ [project.optional-dependencies]
21
+ # The compiler binary of this platform, for a `path` source or the OpenAPI documents of an app config; a bundle-only
22
+ # app with `openapi: none` never needs it.
23
+ compiler = ["seampact-compiler==1.0.0"]
24
+
25
+ # PEP 420: `seampact` is a namespace shared by every seampact distribution, so it has no `__init__.py`.
26
+ [project.scripts]
27
+ seampact = "seampact.ir.run.command:main"
28
+
29
+ [tool.hatch.build.targets.wheel]
30
+ packages = ["src/seampact"]
31
+
32
+ [tool.hatch.build.targets.sdist]
33
+ include = ["src/seampact", "LICENSE"]
34
+
35
+ [tool.uv.sources]
36
+ seampact-compiler = { workspace = true }
@@ -0,0 +1 @@
1
+ """The seampact IR for Python generators (ADR 0010): the generated model, the native naming and the reader."""
@@ -0,0 +1,23 @@
1
+ """The compiler binary of a Python build (ADR 0010): wheel lookup, pin, platform, private cache and runner."""
2
+
3
+ from seampact.ir.binary.compiler_binary import install_compiler_binary
4
+ from seampact.ir.binary.documents_command import DocumentsCommand
5
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
6
+ from seampact.ir.binary.host_libc import HostLibc
7
+ from seampact.ir.binary.host_platform import HostPlatform
8
+ from seampact.ir.binary.installed_binary import InstalledBinary
9
+ from seampact.ir.binary.runner import CompileOutput, DocumentsOutput, compile_with_binary, documents_with_binary
10
+
11
+ __all__ = [
12
+ "BinaryFailureCode",
13
+ "BinaryRefusedError",
14
+ "CompileOutput",
15
+ "DocumentsCommand",
16
+ "DocumentsOutput",
17
+ "HostLibc",
18
+ "HostPlatform",
19
+ "InstalledBinary",
20
+ "compile_with_binary",
21
+ "documents_with_binary",
22
+ "install_compiler_binary",
23
+ ]
@@ -0,0 +1,43 @@
1
+ """Absolute paths for the binary channel: a path is made absolute once, before any check, and then used as is, so the
2
+ file checked and the file started are one file whatever directory the child starts in."""
3
+
4
+ import os
5
+ import re
6
+ import sys
7
+ from typing import Final
8
+
9
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
10
+
11
+ _WINDOWS_DRIVE_PATH: Final = re.compile(r"[A-Za-z]:[\\/]")
12
+
13
+
14
+ def is_absolute(path: str) -> bool:
15
+ """Whether `path` is absolute on this host: `is_absolute_on_windows` on Windows, else `is_absolute_on_posix`."""
16
+ return is_absolute_on_windows(path) if sys.platform == "win32" else is_absolute_on_posix(path)
17
+
18
+
19
+ def is_absolute_on_posix(path: str) -> bool:
20
+ """A path from the filesystem root; `\\x` and `C:/x` are relative names on POSIX."""
21
+ return path.startswith("/")
22
+
23
+
24
+ def is_absolute_on_windows(path: str) -> bool:
25
+ """A drive path, a UNC path or a path from the root of the current drive."""
26
+ return path.startswith(("/", "\\")) or _WINDOWS_DRIVE_PATH.match(path) is not None
27
+
28
+
29
+ def resolved(path: str) -> str:
30
+ """`path` itself when absolute, else `path` below the working directory of this process."""
31
+ if is_absolute(path):
32
+ return path
33
+
34
+ try:
35
+ working_directory = os.getcwd()
36
+ except OSError as error:
37
+ raise BinaryRefusedError(
38
+ BinaryFailureCode.COMPILE_FAILED,
39
+ f"The relative path '{path}' cannot be made absolute: the working directory cannot be read ({error}).",
40
+ "Run seampact from a directory your user can read, or pass absolute paths.",
41
+ ) from error
42
+
43
+ return os.path.join(working_directory, path)
@@ -0,0 +1,10 @@
1
+ {
2
+ "version": "1.0.0",
3
+ "sha256": {
4
+ "seampact-linux-x64": "e3a6e749206f27ed2883e4cd8ea0f3c736bb629c91134eaafef64c147ed7ee2f",
5
+ "seampact-linux-arm64": "19b32653f54578dd8d3c0bc1ef929ea7211b55350308800da38b139712241c6b",
6
+ "seampact-darwin-x64": "4c6e11153456339667dc35b1b8c3ef7b059151fbade0c78d98e1af07cc2af3c7",
7
+ "seampact-darwin-arm64": "87e004d1b5e9ee4afe322b87e04bd0982c326adc01c8f206227d4f8215305775",
8
+ "seampact-win32-x64.exe": "3f56467f4de7b5a2583011b85594592a0c37af88c4ff493482d3cff5d7ad8b09"
9
+ }
10
+ }
@@ -0,0 +1,52 @@
1
+ """The binary cache root: `$XDG_CACHE_HOME/seampact`, else `$HOME/.cache/seampact`; on Windows
2
+ `%LOCALAPPDATA%\\seampact`, whose ACL is per user. A relative `XDG_CACHE_HOME` is ignored, as the XDG specification
3
+ says; any other relative variable is refused by name, so the cache never depends on the directory a command
4
+ starts in."""
5
+
6
+ from collections.abc import Mapping
7
+ from typing import Final
8
+
9
+ from seampact.ir.binary.absolute_path import is_absolute_on_posix, is_absolute_on_windows
10
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
11
+
12
+ _DIRECTORY: Final = "seampact"
13
+ # A relative value of this variable is ignored (XDG Base Directory specification); others are refused.
14
+ _IGNORED_WHEN_RELATIVE: Final = "XDG_CACHE_HOME"
15
+
16
+
17
+ def locate_cache_root(environment: Mapping[str, str], *, windows: bool) -> str:
18
+ """An absolute path. Raises `binary.cache-unavailable` when none of the variables is set or the one read is
19
+ relative."""
20
+ candidates = [("LOCALAPPDATA", "")] if windows else [(_IGNORED_WHEN_RELATIVE, ""), ("HOME", "/.cache")]
21
+ separator = "\\" if windows else "/"
22
+
23
+ for variable, below in candidates:
24
+ base = environment.get(variable, "")
25
+ is_absolute = is_absolute_on_windows(base) if windows else is_absolute_on_posix(base)
26
+
27
+ if base == "" or (not is_absolute and variable == _IGNORED_WHEN_RELATIVE):
28
+ continue
29
+
30
+ if not is_absolute:
31
+ raise _relative(variable, base)
32
+
33
+ return f"{base.rstrip('/\\')}{below}{separator}{_DIRECTORY}"
34
+
35
+ names = ", ".join(variable for variable, _ in candidates)
36
+
37
+ raise BinaryRefusedError(
38
+ BinaryFailureCode.CACHE_UNAVAILABLE,
39
+ f"No cache directory for the compiler binary: none of {names} is set.",
40
+ "Run seampact as a Windows user with a profile (LOCALAPPDATA set)."
41
+ if windows
42
+ else "Set XDG_CACHE_HOME or HOME to a directory private to your user.",
43
+ )
44
+
45
+
46
+ def _relative(variable: str, value: str) -> BinaryRefusedError:
47
+ return BinaryRefusedError(
48
+ BinaryFailureCode.CACHE_UNAVAILABLE,
49
+ f"{variable} is the relative path '{value}', so the binary cache would depend on the directory seampact "
50
+ "starts in.",
51
+ f'Set {variable} to an absolute path, such as "$PWD/{value}" in your CI configuration.',
52
+ )
@@ -0,0 +1,25 @@
1
+ """The cache checks run on the private copy of the binary before each start (`binary.cache-unsafe`)."""
2
+
3
+ from typing import Protocol
4
+
5
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
6
+
7
+
8
+ class CacheSafety(Protocol):
9
+ def create_private_directory(self, directory: str) -> None:
10
+ """Creates the cache root, each missing directory on its way and `directory` owner-only, whatever the
11
+ umask."""
12
+ ...
13
+
14
+ def refuse_unsafe(self, copy_path: str, cache_root: str) -> None:
15
+ """`cache_root` is the real path of the cache root, which `copy_path` is below. Raises `binary.cache-unsafe`,
16
+ naming the path."""
17
+ ...
18
+
19
+
20
+ def unsafe_cache_path(path: str, problem: str, fix: str) -> BinaryRefusedError:
21
+ return BinaryRefusedError(
22
+ BinaryFailureCode.CACHE_UNSAFE,
23
+ f"The binary cache path '{path}' {problem}; seampact runs only a binary no other user can change.",
24
+ fix,
25
+ )
@@ -0,0 +1,15 @@
1
+ """The environment the binary starts with: only the variables of its contract, `TMPDIR`, `TEMP`, `TMP` and, on
2
+ Windows, `SystemRoot`, so no code the environment names (an OpenSSL configuration, a `NODE_OPTIONS` require) runs
3
+ inside the pinned binary. Windows names are case-insensitive."""
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Final
7
+
8
+ _CONTRACT_VARIABLES: Final = ("TMPDIR", "TEMP", "TMP")
9
+ _WINDOWS_VARIABLES: Final = ("SYSTEMROOT",)
10
+
11
+
12
+ def child_environment(parent: Mapping[str, str], *, windows: bool) -> dict[str, str]:
13
+ allowed = (*_CONTRACT_VARIABLES, *_WINDOWS_VARIABLES) if windows else _CONTRACT_VARIABLES
14
+
15
+ return {name: value for name, value in parent.items() if (name.upper() if windows else name) in allowed}
@@ -0,0 +1,115 @@
1
+ """The compiler binary of a Python build: pinned by the root project's `[tool.seampact.binary]` (a dependency's is
2
+ refused) or the pins `seampact-ir` embeds, copied or downloaded into the private content-addressed cache, and run only
3
+ from there. A `path` override is copied; a `version` override downloads the release asset of this host, and a `url`
4
+ override the asset of its mirror. With the embedded pins, the binary is the file of the `seampact-compiler` wheel for
5
+ this host. Each is checked against its pin. A development build embeds no pin: without an override it is refused by
6
+ name (`binary.unpublished`)."""
7
+
8
+ from collections.abc import Callable, Mapping
9
+ from typing import Final
10
+
11
+ from seampact.ir.binary.cache_root import locate_cache_root
12
+ from seampact.ir.binary.cache_safety import CacheSafety
13
+ from seampact.ir.binary.dependency_overrides import refuse_dependency_overrides
14
+ from seampact.ir.binary.download import BinaryDownload
15
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
16
+ from seampact.ir.binary.host_platform import HostPlatform
17
+ from seampact.ir.binary.installed_binary import InstalledBinary
18
+ from seampact.ir.binary.installer import install_binary, install_downloaded_binary
19
+ from seampact.ir.binary.override import BinaryOverride, BinaryOverrideSource
20
+ from seampact.ir.binary.pins import embedded_pins
21
+ from seampact.ir.binary.posix_cache_safety import PosixCacheSafety
22
+ from seampact.ir.binary.root_project import RootProject
23
+ from seampact.ir.binary.wheel import COMPILER_DISTRIBUTION, wheel_binary_path
24
+ from seampact.ir.binary.windows_cache_safety import WindowsCacheSafety, locate_user_profile
25
+
26
+ _FILE_NAME: Final = "seampact"
27
+ _WINDOWS_FILE_NAME: Final = "seampact.exe"
28
+ # Installs the pinned binary into a cache root, under a file name and a cache rule.
29
+ _Install = Callable[[str, str, CacheSafety], InstalledBinary]
30
+
31
+ _UNPUBLISHED_FIX: Final = (
32
+ 'Set [tool.seampact.binary] in your root pyproject.toml to path = "<your build of the compiler>" and '
33
+ 'sha256 = "<its SHA-256>", or use a bundle source, which needs no binary.'
34
+ )
35
+
36
+
37
+ def install_compiler_binary(
38
+ project_directory: str,
39
+ host: HostPlatform,
40
+ environment: Mapping[str, str],
41
+ safety: CacheSafety | None = None,
42
+ download: BinaryDownload | None = None,
43
+ ) -> InstalledBinary:
44
+ """`environment` holds the variables of this process. `safety` is the host's cache rule and `download` the
45
+ GitHub releases unless a test injects its own. Raises `BinaryRefusedError`."""
46
+ download = download if download is not None else BinaryDownload()
47
+ root = RootProject.locate(project_directory)
48
+ refuse_dependency_overrides(root)
49
+ override = root.binary_override()
50
+
51
+ install = _pinned_install(override, host, download)
52
+ cache_root = locate_cache_root(environment, windows=host.is_windows)
53
+ cache_safety = safety if safety is not None else _host_cache_safety(host, environment)
54
+
55
+ return install(cache_root, _WINDOWS_FILE_NAME if host.is_windows else _FILE_NAME, cache_safety)
56
+
57
+
58
+ def _pinned_install(override: BinaryOverride | None, host: HostPlatform, download: BinaryDownload) -> _Install:
59
+ """How the pinned binary goes into a cache root: copied from the wheel or a `path`, or downloaded. The pins are
60
+ read before the cache is located."""
61
+ if override is None:
62
+ wheel_file, wheel_sha256 = _wheel_source(host)
63
+
64
+ return lambda cache_root, file_name, safety: install_binary(
65
+ wheel_file, wheel_sha256, cache_root, file_name, safety
66
+ )
67
+
68
+ # Only a `path` override carries a single hash.
69
+ if override.sha256 is not None:
70
+ path, path_sha256 = override.value, override.sha256
71
+
72
+ return lambda cache_root, file_name, safety: install_binary(path, path_sha256, cache_root, file_name, safety)
73
+
74
+ asset = host.asset_name()
75
+ url = (
76
+ BinaryDownload.mirror_url(override.value, asset)
77
+ if override.source is BinaryOverrideSource.URL
78
+ else download.release_url(override.value, asset)
79
+ )
80
+ asset_sha256 = override.sha256_by_asset[asset]
81
+
82
+ return lambda cache_root, file_name, safety: install_downloaded_binary(
83
+ url, asset_sha256, cache_root, file_name, safety, download
84
+ )
85
+
86
+
87
+ def _host_cache_safety(host: HostPlatform, environment: Mapping[str, str]) -> CacheSafety:
88
+ return WindowsCacheSafety(locate_user_profile(environment)) if host.is_windows else PosixCacheSafety()
89
+
90
+
91
+ def _wheel_source(host: HostPlatform) -> tuple[str, str]:
92
+ """The wheel's file for this host and the pinned hash it must have."""
93
+ pins = embedded_pins()
94
+
95
+ if pins is None:
96
+ raise BinaryRefusedError(
97
+ BinaryFailureCode.UNPUBLISHED,
98
+ "This seampact-ir is a development build: it embeds no binary pin, so it has no binary to run.",
99
+ _UNPUBLISHED_FIX,
100
+ )
101
+
102
+ # With pins, the host decides the asset first, so an unsupported host learns its own fixes.
103
+ asset = host.asset_name()
104
+ installed = wheel_binary_path(asset)
105
+
106
+ if installed is None:
107
+ raise BinaryRefusedError(
108
+ BinaryFailureCode.UNPUBLISHED,
109
+ f"This seampact-ir pins the compiler {pins.version}, but no installed {COMPILER_DISTRIBUTION} wheel holds "
110
+ f"'{asset}', the binary of this platform.",
111
+ f"Install seampact-ir[compiler] ({COMPILER_DISTRIBUTION} {pins.version}), or set [tool.seampact.binary] "
112
+ 'in your root pyproject.toml to url = "<a mirror of the release assets>" with the sha256 of each asset.',
113
+ )
114
+
115
+ return installed, pins.sha256_by_asset[asset]
@@ -0,0 +1,92 @@
1
+ """Refuses a `[tool.seampact.binary]` table set outside the root project (`binary.override-not-root`), so a
2
+ dependency never chooses the binary the build runs. The sources it reads are the pyproject files a Python build can
3
+ reach: the members of the root's uv workspace and the local directories installed distributions record in
4
+ `direct_url.json` (path dependencies, editable installs)."""
5
+
6
+ import glob
7
+ import json
8
+ import os
9
+ from collections.abc import Iterator
10
+ from importlib import metadata
11
+ from typing import Final
12
+ from urllib.parse import unquote, urlparse
13
+ from urllib.request import url2pathname
14
+
15
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
16
+ from seampact.ir.binary.root_project import PYPROJECT_FILE, RootProject, tool_table
17
+ from seampact.ir.https.untrusted_text import quoted
18
+
19
+ _FILE_SCHEME: Final = "file"
20
+ _DIRECT_URL_FILE: Final = "direct_url.json"
21
+
22
+
23
+ def refuse_dependency_overrides(root: RootProject) -> None:
24
+ root_file = os.path.realpath(root.pyproject_file)
25
+
26
+ for name, pyproject_file in _reachable_pyproject_files(root):
27
+ if os.path.realpath(pyproject_file) == root_file:
28
+ continue
29
+
30
+ tool = tool_table(pyproject_file)
31
+ seampact = tool.get("seampact")
32
+
33
+ if isinstance(seampact, dict) and "binary" in seampact:
34
+ raise BinaryRefusedError(
35
+ BinaryFailureCode.OVERRIDE_NOT_ROOT,
36
+ f"'{pyproject_file}' of {name} sets `tool.seampact.binary`; only the root project's "
37
+ f"'{root.pyproject_file}' chooses the compiler binary.",
38
+ f"Remove the table from '{pyproject_file}', or set the override in your own {PYPROJECT_FILE}.",
39
+ )
40
+
41
+
42
+ def _reachable_pyproject_files(root: RootProject) -> Iterator[tuple[str, str]]:
43
+ yield from _workspace_members(root)
44
+ yield from _local_installs()
45
+
46
+
47
+ def _workspace_members(root: RootProject) -> Iterator[tuple[str, str]]:
48
+ if not os.path.isfile(root.pyproject_file):
49
+ return
50
+
51
+ workspace = _table(_table(tool_table(root.pyproject_file).get("uv")).get("workspace"))
52
+ members = workspace.get("members", [])
53
+
54
+ for pattern in members if isinstance(members, list) else []:
55
+ for directory in sorted(glob.glob(os.path.join(root.directory, str(pattern)))):
56
+ pyproject_file = os.path.join(directory, PYPROJECT_FILE)
57
+
58
+ if os.path.isfile(pyproject_file):
59
+ yield f"the workspace member {quoted(os.path.basename(directory))}", pyproject_file
60
+
61
+
62
+ def _local_installs() -> Iterator[tuple[str, str]]:
63
+ for distribution in metadata.distributions():
64
+ recorded = distribution.read_text(_DIRECT_URL_FILE)
65
+ url = _local_directory_url(recorded)
66
+
67
+ if url is None:
68
+ continue
69
+
70
+ pyproject_file = os.path.join(url2pathname(unquote(urlparse(url).path)), PYPROJECT_FILE)
71
+
72
+ if os.path.isfile(pyproject_file):
73
+ yield f"the installed package {quoted(distribution.metadata['Name'])}", pyproject_file
74
+
75
+
76
+ def _local_directory_url(recorded: str | None) -> str | None:
77
+ if recorded is None:
78
+ return None
79
+
80
+ try:
81
+ direct_url = json.loads(recorded)
82
+ except ValueError:
83
+ # A malformed record belongs to the installer that wrote it; it names no local directory.
84
+ return None
85
+
86
+ url = direct_url.get("url") if isinstance(direct_url, dict) else None
87
+
88
+ return url if isinstance(url, str) and url.startswith(f"{_FILE_SCHEME}:") and "dir_info" in direct_url else None
89
+
90
+
91
+ def _table(value: object) -> dict[str, object]:
92
+ return value if isinstance(value, dict) else {}
@@ -0,0 +1,14 @@
1
+ """The binary's commands on an app config's OpenAPI documents (ADR 0010): `generate`, `generate --force`, which
2
+ overwrites a document edited by hand, and `check`."""
3
+
4
+ from enum import Enum
5
+
6
+
7
+ class DocumentsCommand(Enum):
8
+ GENERATE = ("generate",)
9
+ GENERATE_OVERWRITING_HAND_EDITS = ("generate", "--force")
10
+ CHECK = ("check",)
11
+
12
+ @property
13
+ def arguments(self) -> list[str]:
14
+ return list(self.value)
@@ -0,0 +1,73 @@
1
+ """Where the compiler binary is downloaded from, and how (ADR 0010, "Fetch"): a release asset of the seampact GitHub
2
+ releases, or a mirror base URL to which `/<asset>` is appended, so a base with a query or a fragment is refused. The
3
+ download is `HttpsDownload`'s, with the system's certificate authorities and no proxy, as the bundle channel's, a
4
+ deadline sized for a binary of about 100 MB on a slow link, and a cap well above any release asset."""
5
+
6
+ from collections.abc import Callable
7
+ from typing import Final
8
+ from urllib.parse import quote
9
+
10
+ from seampact.ir.binary.failure import BinaryFailureCode, BinaryRefusedError
11
+ from seampact.ir.https import HttpsDownload, untried_reason
12
+ from seampact.ir.https.printable_url import printable_url
13
+
14
+ RELEASE_BASE_URL: Final = "https://github.com/jubasse/seampact/releases/download"
15
+ DEADLINE_SECONDS: Final = 600.0
16
+ BYTES_MAX: Final = 512 * 1024 * 1024
17
+
18
+ _FETCH_FIX: Final = (
19
+ "Check that the URL serves the release asset over HTTPS, or set [tool.seampact.binary] in your root "
20
+ "pyproject.toml to a mirror `url` or a `path`, with their hashes."
21
+ )
22
+
23
+ _MIRROR_QUERY_OR_FRAGMENT: Final = (
24
+ "was not tried: the mirror base URL has a query or a fragment, so the asset name cannot be appended to its path"
25
+ )
26
+
27
+
28
+ class BinaryDownload:
29
+ def __init__(
30
+ self,
31
+ release_base_url: str = RELEASE_BASE_URL,
32
+ deadline_seconds: float = DEADLINE_SECONDS,
33
+ ca_file: str | None = None,
34
+ ) -> None:
35
+ """`ca_file` replaces the system's certificate authorities; tests serve the assets themselves."""
36
+ self._release_base_url = release_base_url
37
+ self._https = HttpsDownload(deadline_seconds, ca_file)
38
+
39
+ def release_url(self, version: str, asset: str) -> str:
40
+ """The URL of `asset` in the release of `version`, tagged `v<version>`."""
41
+ return f"{self._release_base_url}/v{quote(version, safe='')}/{asset}"
42
+
43
+ @staticmethod
44
+ def mirror_url(base_url: str, asset: str) -> str:
45
+ """Raises `binary.fetch-failed` for a base with a query or a fragment, which the asset would land in."""
46
+ if "?" in base_url or "#" in base_url:
47
+ raise _fetch_failed(base_url, _MIRROR_QUERY_OR_FRAGMENT)
48
+
49
+ return f"{base_url.rstrip('/')}/{asset}"
50
+
51
+ @staticmethod
52
+ def refuse_unless_https(url: str) -> None:
53
+ """Refuses a URL that is not `https:`, or holds a control character or a space, before the cache is touched.
54
+ Raises `binary.fetch-failed`."""
55
+ untried = untried_reason(url)
56
+
57
+ if untried is not None:
58
+ raise _fetch_failed(url, f"was not tried: {untried}")
59
+
60
+ def stream(self, url: str, sink: Callable[[bytes], None]) -> None:
61
+ """Streams the asset at `url` into `sink`. Raises `binary.fetch-failed`, or what the sink raises."""
62
+ failure = self._https.stream(url, BYTES_MAX, sink)
63
+
64
+ if failure is not None:
65
+ raise _fetch_failed(failure.url, failure.reason)
66
+
67
+
68
+ def _fetch_failed(url: str, reason: str) -> BinaryRefusedError:
69
+ return BinaryRefusedError(
70
+ BinaryFailureCode.FETCH_FAILED,
71
+ f"Downloading the compiler binary from '{printable_url(url)}' {reason}; nothing is installed.",
72
+ _FETCH_FIX,
73
+ )
@@ -0,0 +1,38 @@
1
+ """The refusals of the compiler binary channel of a Python build (ADR 0010, "Amendment 2026-11-01: the Python
2
+ channels"): the codes are those of the PHP and JVM channels."""
3
+
4
+ from dataclasses import dataclass
5
+ from enum import StrEnum
6
+
7
+
8
+ class BinaryFailureCode(StrEnum):
9
+ UNPUBLISHED = "binary.unpublished"
10
+ PIN_MISSING = "binary.pin-missing"
11
+ PIN_INVALID = "binary.pin-invalid"
12
+ HASH_MISMATCH = "binary.hash-mismatch"
13
+ FETCH_FAILED = "binary.fetch-failed"
14
+ OVERRIDE_NOT_ROOT = "binary.override-not-root"
15
+ PLATFORM_UNSUPPORTED = "binary.platform-unsupported"
16
+ CACHE_UNAVAILABLE = "binary.cache-unavailable"
17
+ CACHE_UNSAFE = "binary.cache-unsafe"
18
+ COMPILE_REFUSED = "binary.compile-refused"
19
+ COMPILE_TIMEOUT = "binary.compile-timeout"
20
+ COMPILE_FAILED = "binary.compile-failed"
21
+ DOCUMENTS_REFUSED = "binary.documents-refused"
22
+
23
+
24
+ @dataclass(frozen=True, slots=True)
25
+ class BinaryFailure:
26
+ """Why the compiler binary is not run: the code, what was found, and the fix."""
27
+
28
+ code: BinaryFailureCode
29
+ message: str
30
+ fix: str
31
+
32
+
33
+ class BinaryRefusedError(Exception):
34
+ """The compiler binary is refused before or while it runs; `failure` names the code and the fix."""
35
+
36
+ def __init__(self, code: BinaryFailureCode, message: str, fix: str) -> None:
37
+ super().__init__(f"{code.value}: {message} {fix}")
38
+ self.failure = BinaryFailure(code, message, fix)
@@ -0,0 +1,39 @@
1
+ """The C library of a Linux host; other systems have none to tell apart."""
2
+
3
+ import os
4
+ from enum import StrEnum
5
+ from typing import Final
6
+
7
+ _MUSL_LOADER_PREFIX: Final = "ld-musl-"
8
+ _LIBRARY_DIRECTORY: Final = "/lib"
9
+
10
+
11
+ class HostLibc(StrEnum):
12
+ GLIBC = "glibc"
13
+ MUSL = "musl"
14
+ UNKNOWN = "unknown"
15
+ NONE = "none"
16
+
17
+
18
+ def linux_libc(glibc_version: str | None, library_entries: list[str]) -> HostLibc:
19
+ """glibc when `os.confstr("CS_GNU_LIBC_VERSION")` answered (`glibc_version`); otherwise the loaders under `/lib`
20
+ decide: `ld-musl-*` is musl, anything else unknown."""
21
+ if glibc_version is not None:
22
+ return HostLibc.GLIBC
23
+
24
+ if any(entry.startswith(_MUSL_LOADER_PREFIX) for entry in library_entries):
25
+ return HostLibc.MUSL
26
+
27
+ return HostLibc.UNKNOWN
28
+
29
+
30
+ def current_linux_libc() -> HostLibc:
31
+ return linux_libc(_glibc_version(), os.listdir(_LIBRARY_DIRECTORY) if os.path.isdir(_LIBRARY_DIRECTORY) else [])
32
+
33
+
34
+ def _glibc_version() -> str | None:
35
+ try:
36
+ return os.confstr("CS_GNU_LIBC_VERSION")
37
+ except (ValueError, OSError):
38
+ # musl has no such name: the question has no answer there, and the loader decides.
39
+ return None