pg-maker 0.1.0__py3-none-any.whl

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 (181) hide show
  1. pg_maker/__init__.py +3 -0
  2. pg_maker/__main__.py +3 -0
  3. pg_maker/api.py +594 -0
  4. pg_maker/artifacts/__init__.py +4 -0
  5. pg_maker/artifacts/catalog.py +67 -0
  6. pg_maker/artifacts/manifest.py +301 -0
  7. pg_maker/assets.py +93 -0
  8. pg_maker/builders/__init__.py +4 -0
  9. pg_maker/builders/autotools.py +430 -0
  10. pg_maker/builders/components.py +359 -0
  11. pg_maker/bundled/PROJECT.md +57 -0
  12. pg_maker/bundled/build.yaml +29 -0
  13. pg_maker/bundled/components/pg-stat-kcache.yaml +30 -0
  14. pg_maker/bundled/components/pg-stat-statements.yaml +24 -0
  15. pg_maker/bundled/components/pg-wait-sampling.yaml +30 -0
  16. pg_maker/bundled/config/defaults.yaml +80 -0
  17. pg_maker/bundled/config/documentation.yaml +24 -0
  18. pg_maker/bundled/config/features/my-feature.yaml +17 -0
  19. pg_maker/bundled/config/vendor.yaml +13 -0
  20. pg_maker/bundled/documentation/README.md +20 -0
  21. pg_maker/bundled/documentation/pg-stat-kcache.sgml +99 -0
  22. pg_maker/bundled/documentation/pg-stat-kcache.yaml +15 -0
  23. pg_maker/bundled/documentation/pg-wait-sampling.sgml +122 -0
  24. pg_maker/bundled/documentation/pg-wait-sampling.yaml +15 -0
  25. pg_maker/bundled/patches/unix-socket-directory.patch +5 -0
  26. pg_maker/changes/__init__.py +1 -0
  27. pg_maker/changes/apply.py +298 -0
  28. pg_maker/changes/loader.py +135 -0
  29. pg_maker/changes/workspace.py +221 -0
  30. pg_maker/cli.py +546 -0
  31. pg_maker/components/__init__.py +4 -0
  32. pg_maker/config.py +514 -0
  33. pg_maker/development/__init__.py +1 -0
  34. pg_maker/development/cli.py +104 -0
  35. pg_maker/development/git.py +362 -0
  36. pg_maker/development/session.py +735 -0
  37. pg_maker/documentation/__init__.py +4 -0
  38. pg_maker/documentation/builder.py +467 -0
  39. pg_maker/documentation/cli.py +24 -0
  40. pg_maker/documentation/config.py +127 -0
  41. pg_maker/documentation/install.py +79 -0
  42. pg_maker/documentation/site.py +166 -0
  43. pg_maker/documentation/sphinx_ext.py +150 -0
  44. pg_maker/documentation/theme/_static/pg-maker.css +26 -0
  45. pg_maker/documentation/theme/_static/pg-maker.js +35 -0
  46. pg_maker/documentation/theme/_templates/footer_en.html +5 -0
  47. pg_maker/documentation/theme/_templates/footer_metadata.html +13 -0
  48. pg_maker/documentation/theme/_templates/footer_ru.html +5 -0
  49. pg_maker/documentation/translation.py +341 -0
  50. pg_maker/documentation/validation.py +132 -0
  51. pg_maker/documentation/worker.py +62 -0
  52. pg_maker/documentation/writer.py +324 -0
  53. pg_maker/documentation/xml.py +210 -0
  54. pg_maker/documents.py +118 -0
  55. pg_maker/errors.py +50 -0
  56. pg_maker/execution/__init__.py +1 -0
  57. pg_maker/execution/build.py +502 -0
  58. pg_maker/execution/cache.py +287 -0
  59. pg_maker/execution/cancellation.py +60 -0
  60. pg_maker/execution/checkpoints.py +218 -0
  61. pg_maker/execution/cleanup.py +122 -0
  62. pg_maker/execution/component_cache.py +164 -0
  63. pg_maker/execution/development.py +109 -0
  64. pg_maker/execution/engine.py +171 -0
  65. pg_maker/execution/events.py +65 -0
  66. pg_maker/execution/incremental.py +178 -0
  67. pg_maker/execution/preparation.py +130 -0
  68. pg_maker/execution/process.py +155 -0
  69. pg_maker/execution/resources.py +41 -0
  70. pg_maker/execution/state.py +82 -0
  71. pg_maker/execution/worker.py +45 -0
  72. pg_maker/identity.py +116 -0
  73. pg_maker/integrations/__init__.py +1 -0
  74. pg_maker/integrations/machine.py +163 -0
  75. pg_maker/integrations/runtime.py +387 -0
  76. pg_maker/models.py +245 -0
  77. pg_maker/packaging/__init__.py +4 -0
  78. pg_maker/packaging/deb.py +337 -0
  79. pg_maker/packaging/service.py +165 -0
  80. pg_maker/packaging/spec.py +64 -0
  81. pg_maker/packaging/templates/deb-control.in +8 -0
  82. pg_maker/packaging/tree.py +239 -0
  83. pg_maker/planning/__init__.py +1 -0
  84. pg_maker/planning/documents.py +65 -0
  85. pg_maker/planning/environments.py +318 -0
  86. pg_maker/planning/graph.py +23 -0
  87. pg_maker/planning/plan.py +418 -0
  88. pg_maker/planning/resolve.py +117 -0
  89. pg_maker/planning/reuse.py +80 -0
  90. pg_maker/planning/runtime_repository.py +90 -0
  91. pg_maker/postgresql.py +59 -0
  92. pg_maker/progress.py +64 -0
  93. pg_maker/publishing/__init__.py +4 -0
  94. pg_maker/py.typed +0 -0
  95. pg_maker/recipes/README.md +13 -0
  96. pg_maker/recipes/platforms/debian-13.yaml +125 -0
  97. pg_maker/recipes/postgresql.yaml +114 -0
  98. pg_maker/runners/__init__.py +1 -0
  99. pg_maker/runners/base.py +29 -0
  100. pg_maker/runners/docker.py +312 -0
  101. pg_maker/schemas/build.schema.json +5 -0
  102. pg_maker/schemas/change-set.schema.json +5 -0
  103. pg_maker/schemas/component-envelope.schema.json +69 -0
  104. pg_maker/schemas/component.schema.json +5 -0
  105. pg_maker/schemas/defaults.schema.json +5 -0
  106. pg_maker/schemas/doc-rules.schema.json +5 -0
  107. pg_maker/schemas/documentation.schema.json +5 -0
  108. pg_maker/schemas/environment.schema.json +228 -0
  109. pg_maker/schemas/feature.schema.json +5 -0
  110. pg_maker/schemas/insertions.schema.json +5 -0
  111. pg_maker/schemas/lock.schema.json +3963 -0
  112. pg_maker/schemas/operation-input.schema.json +306 -0
  113. pg_maker/schemas/operation-state.schema.json +127 -0
  114. pg_maker/schemas/plan.schema.json +284 -0
  115. pg_maker/schemas/planning-common.schema.json +8192 -0
  116. pg_maker/schemas/platform.schema.json +5 -0
  117. pg_maker/schemas/postgresql-catalog.schema.json +5 -0
  118. pg_maker/schemas/run-event.schema.json +72 -0
  119. pg_maker/schemas/translations.schema.json +5 -0
  120. pg_maker/schemas/vendor.schema.json +5 -0
  121. pg_maker/security.py +73 -0
  122. pg_maker/sources/__init__.py +1 -0
  123. pg_maker/sources/resolver.py +238 -0
  124. pg_maker/sources/store.py +279 -0
  125. pg_maker/storage.py +127 -0
  126. pg_maker/validation/__init__.py +4 -0
  127. pg_maker/validation/installed.py +667 -0
  128. pg_maker/validation/packages.py +309 -0
  129. pg_maker/validation/systemctl.py +185 -0
  130. pg_maker/yamlio.py +54 -0
  131. pg_maker-0.1.0.data/data/share/doc/pg-maker/CHANGELOG.md +22 -0
  132. pg_maker-0.1.0.data/data/share/doc/pg-maker/CONTRIBUTING.md +63 -0
  133. pg_maker-0.1.0.data/data/share/doc/pg-maker/LICENSE +21 -0
  134. pg_maker-0.1.0.data/data/share/doc/pg-maker/README.md +163 -0
  135. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/architecture.md +55 -0
  136. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development-storage.md +72 -0
  137. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development.md +86 -0
  138. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/README.md +32 -0
  139. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/api.md +75 -0
  140. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/build.md +112 -0
  141. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/changes.md +58 -0
  142. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/cleanup.md +17 -0
  143. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/components.md +39 -0
  144. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/configuration.md +83 -0
  145. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/development.md +88 -0
  146. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documentation.md +241 -0
  147. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documents.md +42 -0
  148. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/environments.md +40 -0
  149. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/execution.md +49 -0
  150. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/machine.md +31 -0
  151. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/observability.md +28 -0
  152. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/packaging.md +107 -0
  153. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/planning.md +63 -0
  154. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/postgresql.md +79 -0
  155. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/publication.md +20 -0
  156. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/recovery.md +83 -0
  157. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/runtime.md +31 -0
  158. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/sources.md +48 -0
  159. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/state.md +49 -0
  160. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/storage.md +24 -0
  161. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/releasing.md +98 -0
  162. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/reuse.md +24 -0
  163. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/roadmap.md +54 -0
  164. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/default-components.md +56 -0
  165. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/development-release.md +49 -0
  166. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/documentation-theme.md +45 -0
  167. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/integration.md +49 -0
  168. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/postgresql-majors.md +107 -0
  169. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/review-fixes.md +68 -0
  170. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage4.md +65 -0
  171. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage56.md +66 -0
  172. pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage7.md +72 -0
  173. pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/guc/README.md +11 -0
  174. pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage4/README.md +31 -0
  175. pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage7/README.md +16 -0
  176. pg_maker-0.1.0.dist-info/METADATA +200 -0
  177. pg_maker-0.1.0.dist-info/RECORD +181 -0
  178. pg_maker-0.1.0.dist-info/WHEEL +5 -0
  179. pg_maker-0.1.0.dist-info/entry_points.txt +4 -0
  180. pg_maker-0.1.0.dist-info/licenses/LICENSE +21 -0
  181. pg_maker-0.1.0.dist-info/top_level.txt +1 -0
pg_maker/config.py ADDED
@@ -0,0 +1,514 @@
1
+ """Resolve YAML and recipes without fetching sources or touching Docker."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ import os
7
+ import re
8
+ import shlex
9
+ from dataclasses import dataclass
10
+ from importlib.resources import files
11
+ from pathlib import Path
12
+
13
+ from packaging.specifiers import InvalidSpecifier, SpecifierSet
14
+ from packaging.version import InvalidVersion, Version
15
+
16
+ from pg_maker.documents import validate_document
17
+ from pg_maker.errors import UnsupportedError, ValidationError
18
+ from pg_maker.identity import identities
19
+ from pg_maker.postgresql import recipe as postgresql_recipe
20
+ from pg_maker.postgresql import version_info
21
+ from pg_maker.storage import content_hash
22
+ from pg_maker.yamlio import load_yaml
23
+
24
+
25
+ def resource_path(*parts: str) -> Path:
26
+ # Wheels are installed unpacked; retaining real paths also supports detached workers.
27
+ return Path(str(files("pg_maker").joinpath(*parts)))
28
+
29
+
30
+ def merge(base: dict, update: dict) -> dict:
31
+ result = copy.deepcopy(base)
32
+ for key, value in update.items():
33
+ result[key] = (
34
+ merge(result[key], value)
35
+ if isinstance(value, dict) and isinstance(result.get(key), dict)
36
+ else copy.deepcopy(value)
37
+ )
38
+ return result
39
+
40
+
41
+ def compatible(version: str, requirement: str, subject: str) -> None:
42
+ try:
43
+ accepted = SpecifierSet(requirement).contains(Version(version), prereleases=True)
44
+ except (InvalidSpecifier, InvalidVersion) as exc:
45
+ raise ValidationError("invalid PostgreSQL compatibility declaration") from exc
46
+ if not accepted:
47
+ raise UnsupportedError(
48
+ "PostgreSQL version is outside recipe compatibility",
49
+ details={
50
+ "subject": subject,
51
+ "version": version,
52
+ "compatible_postgresql": requirement,
53
+ },
54
+ )
55
+
56
+
57
+ def mark_leaves(value, origin: str, path: str, provenance: dict) -> None:
58
+ if isinstance(value, dict) and value:
59
+ for key, item in value.items():
60
+ mark_leaves(item, origin, path + "/" + key, provenance)
61
+ elif isinstance(value, list) and value:
62
+ for index, item in enumerate(value):
63
+ mark_leaves(item, origin, path + f"/{index}", provenance)
64
+ else:
65
+ provenance[path] = origin
66
+
67
+
68
+ @dataclass(frozen=True)
69
+ class EffectiveConfig:
70
+ data: dict
71
+ provenance: dict[str, str]
72
+ bindings: dict[str, str]
73
+
74
+ def to_document(self) -> dict:
75
+ return {"data": self.data, "provenance": self.provenance, "bindings": self.bindings}
76
+
77
+
78
+ def platform_recipe(reference: str, base: Path, config_dir: Path | None) -> tuple[dict, Path]:
79
+ explicit = "/" in reference or reference.endswith((".yaml", ".yml"))
80
+ if explicit:
81
+ path = (base / reference).resolve()
82
+ else:
83
+ if not re.fullmatch(r"[a-z][a-z0-9-]{0,63}", reference):
84
+ raise ValidationError("invalid platform id")
85
+ builtin = resource_path("recipes", "platforms", reference + ".yaml")
86
+ custom = config_dir / "platforms" / (reference + ".yaml") if config_dir else None
87
+ path = custom if custom and custom.is_file() else builtin
88
+ if path == custom and builtin.is_file():
89
+ if not load_yaml(path, "platform")["spec"].get("replace_builtin", False):
90
+ raise ValidationError("platform override needs replace_builtin or an explicit path")
91
+ if not path.is_file():
92
+ raise UnsupportedError("platform recipe is unavailable", details={"platform": reference})
93
+ recipe = load_yaml(path, "platform")
94
+ if not explicit and recipe["metadata"]["id"] != reference:
95
+ raise ValidationError("platform id differs from its filename")
96
+ return recipe, path
97
+
98
+
99
+ def _unmanaged(arguments: list[str], pointer: str) -> None:
100
+ managed = re.compile(
101
+ r"^(--(?:prefix|exec-prefix|[a-z]*dir|with(?:out)?-(?:llvm|ssl|openssl|icu|lz4|zstd|"
102
+ r"libxml|libxslt|pam|systemd|uuid|perl|python|tcl|ldap|gssapi)|"
103
+ r"(?:enable|disable)-(?:debug|cassert|nls|tap-tests)))(?:=|$)|"
104
+ r"^(?:-[Oog]|-m(?:arch|cpu|tune)|-flto|-D_FORTIFY_SOURCE|(?:C|CPP|CXX|LD)FLAGS=)"
105
+ )
106
+ for value in arguments:
107
+ if not value or any(c in value for c in "\x00\n\r"):
108
+ raise ValidationError("invalid build argument", details={"pointer": pointer})
109
+ try:
110
+ tokens = shlex.split(value)
111
+ except ValueError as exc:
112
+ raise ValidationError("malformed build argument", details={"pointer": pointer}) from exc
113
+ if any(managed.search(token) for token in tokens):
114
+ raise ValidationError(
115
+ "argument overrides a managed build setting",
116
+ details={
117
+ "pointer": pointer,
118
+ "argument": value,
119
+ },
120
+ )
121
+
122
+
123
+ def build_flags(defaults: dict, feature: dict, target: dict, recipe: dict, extra: list) -> dict:
124
+ build = copy.deepcopy(defaults["build"])
125
+ additions = feature["spec"]["build_options"]
126
+ build["options"].update(additions.get("options", {}))
127
+ if build["backend"] not in recipe["backends"]:
128
+ raise UnsupportedError("build backend is not supported by platform")
129
+ if build["backend"] != "autotools" or build["flags"]["lto"]:
130
+ raise UnsupportedError("this planner supports autotools without LTO")
131
+ for name, value in build["options"].items():
132
+ if name not in recipe["options"] or value not in recipe["options"][name]:
133
+ raise UnsupportedError("unsupported build option", details={"option": name})
134
+ if name not in recipe["option_configure"] or name not in recipe["option_dependencies"]:
135
+ raise ValidationError("platform option lacks configure or dependency policy")
136
+ mode = defaults["build_modes"][build["mode"]]
137
+ flags = {
138
+ "cflags": list(mode["cflags"]),
139
+ "cxxflags": list(mode["cflags"]),
140
+ "cppflags": [*build["flags"]["cppflags"], *mode["cppflags"]],
141
+ "ldflags": list(build["flags"]["ldflags"]),
142
+ }
143
+ for key in flags:
144
+ _unmanaged(additions.get(key, []), "/feature/build_options/" + key)
145
+ flags[key] += additions.get(key, [])
146
+ cpu = recipe["cpu_targets"][target["architecture"]][target["cpu_target"]]
147
+ flags["cflags"] += cpu
148
+ flags["cxxflags"] += cpu
149
+ for name, value in additions.get("defines", {}).items():
150
+ if not re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", name) or name == "_FORTIFY_SOURCE":
151
+ raise ValidationError("invalid or managed feature define")
152
+ if value is not None and not re.fullmatch(r"[A-Za-z0-9_.+-]+", str(value)):
153
+ raise ValidationError("define values must be single preprocessor tokens")
154
+ flags["cppflags"].append("-D" + name + ("=" + str(value) if value is not None else ""))
155
+ _unmanaged(extra, "/spec/extra_build_args")
156
+ _unmanaged(additions.get("configure_args", []), "/feature/build_options/configure_args")
157
+ build["resolved_flags"] = flags
158
+ build["configure_args"] = [
159
+ *mode["configure_options"],
160
+ *[
161
+ recipe["option_configure"][name]["enabled" if value else "disabled"]
162
+ for name, value in sorted(build["options"].items())
163
+ ],
164
+ *additions.get("configure_args", []),
165
+ *extra,
166
+ ]
167
+ return build
168
+
169
+
170
+ def load_config(
171
+ manifest_path: Path,
172
+ *,
173
+ config_dir: Path | None = None,
174
+ features: list[str] | None = None,
175
+ build_mode: str | None = None,
176
+ with_doc: bool | None = None,
177
+ doc_languages: list[str] | None = None,
178
+ ) -> EffectiveConfig:
179
+ from pg_maker.changes.loader import load_changes, load_components, required_tests
180
+
181
+ manifest_path = manifest_path.resolve()
182
+ manifest = load_yaml(manifest_path, "build")
183
+ spec = manifest["spec"]
184
+ version = spec["postgresql"]["version"]
185
+ pg_recipe = postgresql_recipe(version)
186
+ if config_dir is not None:
187
+ config_dir = config_dir.resolve()
188
+ elif "config_dir" in spec:
189
+ config_dir = (manifest_path.parent / spec["config_dir"]).resolve()
190
+ elif os.environ.get("PG_MAKER_CONFIG_DIR"):
191
+ config_dir = Path(os.environ["PG_MAKER_CONFIG_DIR"]).resolve()
192
+ if config_dir is not None and not config_dir.is_dir():
193
+ raise ValidationError("selected config directory does not exist")
194
+ provenance, bindings = {}, {"source": str(manifest_path.parent)}
195
+ docs_base = resource_path("bundled", "config")
196
+ settings = {}
197
+ for name in ("vendor", "defaults", "documentation"):
198
+ builtin = resource_path("bundled", "config", name + ".yaml")
199
+ value = load_yaml(builtin, name)
200
+ mark_leaves(value["spec"], "builtin:" + name, "/" + name, provenance)
201
+ path = config_dir / (name + ".yaml") if config_dir else None
202
+ if name == "documentation" and spec.get("documentation", {}).get("profile"):
203
+ path = manifest_path.parent / spec["documentation"]["profile"]
204
+ if not path.is_file():
205
+ raise ValidationError("documentation profile does not exist")
206
+ if path is not None and path.is_file():
207
+ override = load_yaml(path)
208
+ value = merge(value, override)
209
+ validate_document(value, name)
210
+ mark_leaves(override.get("spec", {}), str(path), "/" + name, provenance)
211
+ if name == "documentation":
212
+ docs_base = path.parent
213
+ settings[name] = value["spec"]
214
+ defaults = settings["defaults"]
215
+ for section in ("build", "validation", "execution"):
216
+ defaults[section] = merge(defaults[section], spec.get(section, {}))
217
+ mark_leaves(spec.get(section, {}), str(manifest_path), "/defaults/" + section, provenance)
218
+ if build_mode is not None:
219
+ if build_mode not in {"release", "debug"}:
220
+ raise ValidationError("unknown build mode")
221
+ defaults["build"]["mode"] = build_mode
222
+ provenance["/defaults/build/mode"] = "cli:build-mode"
223
+ docs = settings["documentation"]
224
+ docs.update(
225
+ {key: val for key, val in spec.get("documentation", {}).items() if key != "profile"}
226
+ )
227
+ if with_doc is not None:
228
+ docs["enabled"] = with_doc
229
+ provenance["/documentation/enabled"] = "cli:with-doc"
230
+ if doc_languages is not None:
231
+ docs["languages"] = doc_languages
232
+ for i in range(len(doc_languages)):
233
+ provenance[f"/documentation/languages/{i}"] = "cli:doc-language"
234
+ if docs["enabled"]:
235
+ from pg_maker.documentation.config import normalize
236
+ from pg_maker.postgresql import documentation_fixes
237
+
238
+ docs["rules"] = [*documentation_fixes(version, pg_recipe), *docs["rules"]]
239
+ normalize(docs, docs_base, bindings, version=version, validation=defaults["validation"])
240
+ elif doc_languages is not None:
241
+ raise ValidationError("doc-language requires enabled documentation")
242
+ selected = features if features is not None else spec["features"]
243
+ if not selected or len(selected) != len(set(selected)):
244
+ raise ValidationError("feature selection must be nonempty and unique")
245
+ feature_docs, used_ids, numeric_ids = [], set(), set()
246
+ for reference in selected:
247
+ explicit = "/" in reference or reference.endswith((".yaml", ".yml"))
248
+ if explicit:
249
+ path = (manifest_path.parent / reference).resolve()
250
+ else:
251
+ if not re.fullmatch(r"[a-z][a-z0-9-]{0,63}", reference):
252
+ raise ValidationError("invalid feature id")
253
+ path = (
254
+ (config_dir or resource_path("bundled", "config"))
255
+ / "features"
256
+ / (reference + ".yaml")
257
+ )
258
+ feature = load_yaml(path, "feature")
259
+ fid, fs = feature["metadata"]["id"], feature["spec"]
260
+ if not explicit and fid != reference:
261
+ raise ValidationError("feature id differs from its filename")
262
+ if fid in used_ids or (fs["numeric_id"] is not None and fs["numeric_id"] in numeric_ids):
263
+ raise ValidationError("duplicate feature id or numeric_id")
264
+ used_ids.add(fid)
265
+ if fs["numeric_id"] is not None:
266
+ numeric_ids.add(fs["numeric_id"])
267
+ compatible(version, fs.get("compatible_postgresql", ">=14"), fid)
268
+ changes, file_bindings = load_changes(fs["changes"], path.parent, version)
269
+ components, component_bindings = load_components(fs["components"], path.parent, version)
270
+ bindings.update({fid + "/" + key: val for key, val in file_bindings.items()})
271
+ bindings.update({fid + "/" + key: val for key, val in component_bindings.items()})
272
+ feature["spec"]["changes"], feature["spec"]["components"] = changes, components
273
+ if docs["enabled"]:
274
+ from pg_maker.documentation.config import load_rules
275
+
276
+ fs["documentation"]["rules"] = load_rules(
277
+ fs["documentation"]["rules"], path.parent, fid, bindings
278
+ )
279
+ for component in components:
280
+ if "documentation" in component["spec"]:
281
+ cid = component["metadata"]["id"]
282
+ component["spec"]["documentation"]["rules"] = load_rules(
283
+ component["spec"]["documentation"]["rules"],
284
+ Path(component_bindings["components/" + cid]),
285
+ fid + "/" + cid,
286
+ bindings,
287
+ )
288
+ required_tests(feature)
289
+ mark_leaves(feature, str(path), f"/features/{len(feature_docs)}", provenance)
290
+ feature_docs.append(feature)
291
+ targets, target_ids = [], set()
292
+ for target in spec["targets"]:
293
+ if target["id"] in target_ids:
294
+ raise ValidationError("duplicate target id")
295
+ target_ids.add(target["id"])
296
+ recipe, path = platform_recipe(target["platform"], manifest_path.parent, config_dir)
297
+ rs = recipe["spec"]
298
+ if docs["enabled"]:
299
+ if "documentation" not in rs or "docs" not in rs["roles"]:
300
+ raise UnsupportedError("platform lacks documentation recipe")
301
+ if set(docs["languages"]) - set(rs["documentation"]["languages"]):
302
+ raise UnsupportedError("platform lacks selected documentation language/fonts")
303
+ compatible(version, rs["compatible_postgresql"], recipe["metadata"]["id"])
304
+ arch = target["architecture"]
305
+ if arch not in rs["architectures"] or target["cpu_target"] not in rs["cpu_targets"].get(
306
+ arch, {}
307
+ ):
308
+ raise UnsupportedError("architecture or CPU target is unsupported")
309
+ if (
310
+ arch not in rs["architecture_names"]
311
+ or not {"server", "client", "libpq"} <= rs["roles"].keys()
312
+ ):
313
+ raise ValidationError("platform lacks architecture or package roles")
314
+ if not target["artifacts"] or len(set(target["artifacts"])) != len(target["artifacts"]):
315
+ raise ValidationError("artifacts must be nonempty and unique")
316
+ if set(target["artifacts"]) - set(rs["artifacts"]) or target["artifacts"] != ["deb"]:
317
+ raise UnsupportedError("this planner supports the DEB artifact family")
318
+ if rs["package_format"] != "deb" or rs["package_manager"] != "apt":
319
+ raise UnsupportedError("this planner supports the apt/DEB recipe adapter")
320
+ if defaults["validation"]["profile"] not in rs["test_profiles"]:
321
+ raise UnsupportedError("test profile is unsupported by platform")
322
+ mark_leaves(target, str(manifest_path), f"/targets/{len(targets)}", provenance)
323
+ mark_leaves(recipe, str(path), f"/targets/{len(targets)}/platform", provenance)
324
+ targets.append({**target, "platform": recipe})
325
+ variants, collisions, filenames = [], {}, set()
326
+ for feature in feature_docs:
327
+ fs, fid = feature["spec"], feature["metadata"]["id"]
328
+ sockets = {
329
+ change["spec"].get("postconditions", {}).get("socket_directory")
330
+ for change in fs["changes"]
331
+ }
332
+ sockets.discard(None)
333
+ if defaults["common_changes"]:
334
+ sockets.add("/var/run/postgresql")
335
+ if len(sockets) > 1:
336
+ raise ValidationError("conflicting socket_directory postconditions")
337
+ socket = next(iter(sockets), "/tmp")
338
+ if not socket.startswith("/") or ".." in Path(socket).parts:
339
+ raise ValidationError("socket_directory must be absolute and normalized")
340
+ baseline = defaults["validation"].get("baseline", "auto")
341
+ expectations = any(
342
+ "baseline" in t["expectations"] for c in fs["changes"] for t in c["spec"]["acceptance"]
343
+ )
344
+ if baseline is False and expectations:
345
+ raise ValidationError("baseline is required by acceptance expectations")
346
+ baseline = bool(fs["changes"]) if baseline == "auto" else baseline is True
347
+ for target in targets:
348
+ rs = target["platform"]["spec"]
349
+ build = build_flags(defaults, feature, target, rs, spec.get("extra_build_args", []))
350
+ if build["backend"] not in pg_recipe["execution_backends"]:
351
+ raise UnsupportedError("PostgreSQL recipe does not implement this backend")
352
+ unavailable = set(build["options"]) & set(pg_recipe["unavailable_options"])
353
+ if unavailable:
354
+ raise UnsupportedError(
355
+ "build option is unavailable in this PostgreSQL major",
356
+ details={"options": sorted(unavailable), "version": version},
357
+ )
358
+ identity = identities(
359
+ settings["vendor"],
360
+ defaults,
361
+ feature,
362
+ target,
363
+ {**rs, "id": target["platform"]["metadata"]["id"]},
364
+ version,
365
+ spec["product"],
366
+ build["mode"],
367
+ docs["enabled"],
368
+ )
369
+ for package in identity["packages"]:
370
+ if package["filename"] in filenames:
371
+ raise ValidationError("package filename collision across targets")
372
+ filenames.add(package["filename"])
373
+ # Alternative OS/architecture targets may intentionally use the same runtime path.
374
+ for name in [
375
+ identity["service"],
376
+ *identity["paths"].values(),
377
+ *[p["name"] for p in identity["packages"]],
378
+ ]:
379
+ key = (target["id"], name)
380
+ if key in collisions and collisions[key] != fid:
381
+ raise ValidationError("feature identity collision", details={"value": name})
382
+ collisions[key] = fid
383
+ variants.append(
384
+ {
385
+ "id": fid + "." + target["id"],
386
+ "feature": fid,
387
+ "target": target["id"],
388
+ "build": build,
389
+ "identity": identity,
390
+ "socket_directory": socket,
391
+ "baseline": baseline,
392
+ }
393
+ )
394
+ if spec.get("coinstall") and len(feature_docs) > 1:
395
+ raise ValidationError("coinstall features require distinct service ports")
396
+ previous_releases = []
397
+ previous_variants = set()
398
+ for release in spec.get("previous_releases", []):
399
+ identifier = release["variant"]
400
+ if identifier in previous_variants or identifier not in {v["id"] for v in variants}:
401
+ raise ValidationError("previous release must reference one unique selected variant")
402
+ previous_variants.add(identifier)
403
+ if {p["role"] for p in release["packages"]} != {"server", "client", "libpq"}:
404
+ raise ValidationError("previous release requires server, client and libpq packages")
405
+ if not re.fullmatch(r"[0-9]+\.[0-9]+", release["postgresql_version"]):
406
+ raise ValidationError("previous PostgreSQL version must be M.m")
407
+ if version_info(release["postgresql_version"])["major"] != version_info(version)["major"]:
408
+ raise UnsupportedError("major-version upgrade requires a separate future adapter")
409
+ packages = []
410
+ for package in release["packages"]:
411
+ key = "previous/" + identifier + "/" + package["role"]
412
+ bindings[key] = str((manifest_path.parent / package["path"]).resolve())
413
+ packages.append({"role": package["role"], "input": key, "sha256": package["sha256"]})
414
+ previous_releases.append({**release, "packages": packages})
415
+ data = {
416
+ "name": manifest["metadata"]["name"],
417
+ "source": spec["source"],
418
+ "postgresql": spec["postgresql"],
419
+ "postgresql_recipe": pg_recipe,
420
+ "product": spec["product"],
421
+ **settings,
422
+ "features": feature_docs,
423
+ "targets": targets,
424
+ "variants": variants,
425
+ "previous_releases": previous_releases,
426
+ }
427
+ # Every resolved leaf has an origin; derived fields explicitly identify their producer.
428
+ all_leaves = {}
429
+ for key in ("source", "postgresql", "product"):
430
+ mark_leaves(data[key], str(manifest_path), "/" + key, provenance)
431
+ mark_leaves(data, "derived:config/v1", "", all_leaves)
432
+ provenance = {key: provenance.get(key, origin) for key, origin in all_leaves.items()}
433
+ return EffectiveConfig(data, provenance, bindings)
434
+
435
+
436
+ def environment_requirements(config: dict) -> dict:
437
+ result = []
438
+ for target in config["targets"]:
439
+ recipe = target["platform"]
440
+ rs = recipe["spec"]
441
+ variants = [v for v in config["variants"] if v["target"] == target["id"]]
442
+ components = [c for f in config["features"] for c in f["spec"]["components"]]
443
+ for role in ("build", "test"):
444
+ definition = rs["environments"][role]
445
+ packages = set(definition["packages"])
446
+ tools = dict(definition["tools"])
447
+ minimums = {}
448
+ if role == "build":
449
+ pg_recipe = config["postgresql_recipe"]
450
+ minimums = {key: value for key, value in pg_recipe["tools"].items() if value}
451
+ minimums["make"] = str(
452
+ max(Version(minimums["make"]), Version(pg_recipe["maker_make_minimum"]))
453
+ )
454
+ if any(v["build"]["options"].get("llvm") for v in variants):
455
+ tools.setdefault("llvm", ["llvm-config", "--version"])
456
+ tools.setdefault("clang", ["clang", "--version"])
457
+ minimums.update(llvm=pg_recipe["llvm_minimum"], clang=pg_recipe["llvm_minimum"])
458
+ docs = config["documentation"]
459
+ if docs["enabled"]:
460
+ recipe_docs = rs["documentation"]
461
+ if role == "build":
462
+ packages.update(recipe_docs["packages"])
463
+ tools.update(recipe_docs["tools"])
464
+ for language in docs["languages"]:
465
+ packages.update(recipe_docs["languages"][language])
466
+ for fmt in docs["formats"]:
467
+ packages.update(recipe_docs["format_packages"].get(fmt, []))
468
+ packages.update(recipe_docs["backend_packages"].get(docs["html_backend"], []))
469
+ else:
470
+ packages.update(["python3-lxml", "poppler-utils", "groff-base"])
471
+ if role == "build":
472
+ for variant in variants:
473
+ for option, enabled in variant["build"]["options"].items():
474
+ if enabled:
475
+ packages.update(rs["option_dependencies"][option])
476
+ for component in components:
477
+ packages.update(component["spec"]["build_dependencies"])
478
+ runtime_packages = set()
479
+ if role == "build":
480
+ for dependencies in rs["runtime_dependencies"].values():
481
+ runtime_packages.update(dependencies)
482
+ for dependencies in rs.get("service_dependencies", {}).values():
483
+ runtime_packages.update(dependencies)
484
+ for variant in variants:
485
+ for option, enabled in variant["build"]["options"].items():
486
+ if enabled:
487
+ runtime_packages.update(
488
+ rs.get("option_runtime_dependencies", {}).get(option, [])
489
+ )
490
+ for component in components:
491
+ runtime_packages.update(component["spec"]["runtime_dependencies"])
492
+ if any(not re.fullmatch(r"[a-z0-9][a-z0-9+.:=-]*", p) for p in packages):
493
+ raise ValidationError("invalid package dependency")
494
+ result.append(
495
+ {
496
+ "id": target["id"] + "." + role,
497
+ "target": target["id"],
498
+ "role": role,
499
+ "recipe_hash": content_hash(recipe),
500
+ "os": rs["os"],
501
+ "architecture": target["architecture"],
502
+ "image": definition["image"],
503
+ "packages": sorted(packages),
504
+ "runtime_packages": sorted(runtime_packages),
505
+ "tools": tools,
506
+ "tool_minimums": minimums,
507
+ "locales": config["defaults"]["validation"]["database_locales"],
508
+ }
509
+ )
510
+ return {
511
+ "adapter": "pg_maker/apt-environment/v3",
512
+ "environments": result,
513
+ "builder_user": config["defaults"]["execution"]["build_user"],
514
+ }
@@ -0,0 +1 @@
1
+ """Owned Git workspaces and immutable development iterations."""
@@ -0,0 +1,104 @@
1
+ """Argument definitions shared by the maker CLI's development adapter."""
2
+
3
+ from pathlib import Path
4
+
5
+ from pg_maker.storage import read_json, write_json
6
+
7
+
8
+ def add_commands(sub):
9
+ dev = sub.add_parser("dev", help="owned Git trees, patch stacks and incremental builds")
10
+ commands = dev.add_subparsers(dest="dev_command", required=True)
11
+ command = commands.add_parser("open")
12
+ command.add_argument("directory", type=Path)
13
+ source = command.add_mutually_exclusive_group(required=True)
14
+ source.add_argument("--lock", type=Path)
15
+ source.add_argument("--from-run", type=Path)
16
+ command.add_argument("--feature")
17
+ command.add_argument("--input-store", type=Path)
18
+ command.add_argument("--repository", type=Path)
19
+ commands.add_parser("status").add_argument("directory", type=Path)
20
+ command = commands.add_parser("patch")
21
+ command.add_argument("directory", type=Path)
22
+ command.add_argument(
23
+ "action",
24
+ choices=[
25
+ "import",
26
+ "commit",
27
+ "refresh",
28
+ "pop",
29
+ "push",
30
+ "rebase",
31
+ "continue",
32
+ "abort",
33
+ "export",
34
+ "adopt",
35
+ ],
36
+ )
37
+ command.add_argument("--scope", default="postgresql")
38
+ command.add_argument("--id", dest="identifier")
39
+ command.add_argument("--patch", type=Path)
40
+ command.add_argument("--count", type=int, default=1)
41
+ command.add_argument("--message")
42
+ command.add_argument("--onto-source")
43
+ command.add_argument("--onto-lock", type=Path)
44
+ command.add_argument("--out", dest="output", type=Path)
45
+ for name in ("build", "explain"):
46
+ command = commands.add_parser(name)
47
+ command.add_argument("directory", type=Path)
48
+ command.add_argument(
49
+ "--goal", choices=["source", "build", "package", "docs"], default="build"
50
+ )
51
+ command.add_argument("--component", action="append", dest="components")
52
+ if name == "build":
53
+ command.add_argument("--detach", action="store_true")
54
+ command = commands.add_parser("exec")
55
+ command.add_argument("directory", type=Path)
56
+ command.add_argument("--iteration")
57
+ command.add_argument("--component")
58
+ command.add_argument("--target")
59
+ command.add_argument("--timeout-seconds", type=int, default=3600)
60
+ command.add_argument("--detach", action="store_true")
61
+ command.add_argument("argv", nargs="+")
62
+ command = commands.add_parser("pin")
63
+ command.add_argument("directory", type=Path)
64
+ command.add_argument("iteration")
65
+ command.add_argument("--unpin", action="store_true")
66
+ command = commands.add_parser("prune")
67
+ command.add_argument("directory", type=Path)
68
+ command.add_argument("--keep-last", type=int, default=2)
69
+ command.add_argument("--out", type=Path)
70
+ command.add_argument("--apply", type=Path)
71
+
72
+
73
+ def dispatch(service, args):
74
+ name = args.dev_command
75
+ options = {
76
+ key: value
77
+ for key, value in vars(args).items()
78
+ if key
79
+ not in {
80
+ "command",
81
+ "dev_command",
82
+ "machine",
83
+ "request_id",
84
+ "component_capabilities",
85
+ "detach",
86
+ }
87
+ }
88
+ options["directory"] = options["directory"].absolute()
89
+ if name == "pin":
90
+ options["pinned"] = not options.pop("unpin")
91
+ if name == "prune":
92
+ output = options.pop("out")
93
+ if options["apply"]:
94
+ options["apply"] = read_json(options["apply"])
95
+ if name == "exec":
96
+ name = "execute"
97
+ if options["argv"][:1] == ["--"]:
98
+ options["argv"] = options["argv"][1:]
99
+ result = getattr(service, "dev_" + name)(**options)
100
+ if args.dev_command == "prune" and output:
101
+ write_json(output, result)
102
+ if args.dev_command in {"build", "exec"} and not args.detach:
103
+ result = service.wait(Path(result["operation_directory"]))
104
+ return result