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.
- pg_maker/__init__.py +3 -0
- pg_maker/__main__.py +3 -0
- pg_maker/api.py +594 -0
- pg_maker/artifacts/__init__.py +4 -0
- pg_maker/artifacts/catalog.py +67 -0
- pg_maker/artifacts/manifest.py +301 -0
- pg_maker/assets.py +93 -0
- pg_maker/builders/__init__.py +4 -0
- pg_maker/builders/autotools.py +430 -0
- pg_maker/builders/components.py +359 -0
- pg_maker/bundled/PROJECT.md +57 -0
- pg_maker/bundled/build.yaml +29 -0
- pg_maker/bundled/components/pg-stat-kcache.yaml +30 -0
- pg_maker/bundled/components/pg-stat-statements.yaml +24 -0
- pg_maker/bundled/components/pg-wait-sampling.yaml +30 -0
- pg_maker/bundled/config/defaults.yaml +80 -0
- pg_maker/bundled/config/documentation.yaml +24 -0
- pg_maker/bundled/config/features/my-feature.yaml +17 -0
- pg_maker/bundled/config/vendor.yaml +13 -0
- pg_maker/bundled/documentation/README.md +20 -0
- pg_maker/bundled/documentation/pg-stat-kcache.sgml +99 -0
- pg_maker/bundled/documentation/pg-stat-kcache.yaml +15 -0
- pg_maker/bundled/documentation/pg-wait-sampling.sgml +122 -0
- pg_maker/bundled/documentation/pg-wait-sampling.yaml +15 -0
- pg_maker/bundled/patches/unix-socket-directory.patch +5 -0
- pg_maker/changes/__init__.py +1 -0
- pg_maker/changes/apply.py +298 -0
- pg_maker/changes/loader.py +135 -0
- pg_maker/changes/workspace.py +221 -0
- pg_maker/cli.py +546 -0
- pg_maker/components/__init__.py +4 -0
- pg_maker/config.py +514 -0
- pg_maker/development/__init__.py +1 -0
- pg_maker/development/cli.py +104 -0
- pg_maker/development/git.py +362 -0
- pg_maker/development/session.py +735 -0
- pg_maker/documentation/__init__.py +4 -0
- pg_maker/documentation/builder.py +467 -0
- pg_maker/documentation/cli.py +24 -0
- pg_maker/documentation/config.py +127 -0
- pg_maker/documentation/install.py +79 -0
- pg_maker/documentation/site.py +166 -0
- pg_maker/documentation/sphinx_ext.py +150 -0
- pg_maker/documentation/theme/_static/pg-maker.css +26 -0
- pg_maker/documentation/theme/_static/pg-maker.js +35 -0
- pg_maker/documentation/theme/_templates/footer_en.html +5 -0
- pg_maker/documentation/theme/_templates/footer_metadata.html +13 -0
- pg_maker/documentation/theme/_templates/footer_ru.html +5 -0
- pg_maker/documentation/translation.py +341 -0
- pg_maker/documentation/validation.py +132 -0
- pg_maker/documentation/worker.py +62 -0
- pg_maker/documentation/writer.py +324 -0
- pg_maker/documentation/xml.py +210 -0
- pg_maker/documents.py +118 -0
- pg_maker/errors.py +50 -0
- pg_maker/execution/__init__.py +1 -0
- pg_maker/execution/build.py +502 -0
- pg_maker/execution/cache.py +287 -0
- pg_maker/execution/cancellation.py +60 -0
- pg_maker/execution/checkpoints.py +218 -0
- pg_maker/execution/cleanup.py +122 -0
- pg_maker/execution/component_cache.py +164 -0
- pg_maker/execution/development.py +109 -0
- pg_maker/execution/engine.py +171 -0
- pg_maker/execution/events.py +65 -0
- pg_maker/execution/incremental.py +178 -0
- pg_maker/execution/preparation.py +130 -0
- pg_maker/execution/process.py +155 -0
- pg_maker/execution/resources.py +41 -0
- pg_maker/execution/state.py +82 -0
- pg_maker/execution/worker.py +45 -0
- pg_maker/identity.py +116 -0
- pg_maker/integrations/__init__.py +1 -0
- pg_maker/integrations/machine.py +163 -0
- pg_maker/integrations/runtime.py +387 -0
- pg_maker/models.py +245 -0
- pg_maker/packaging/__init__.py +4 -0
- pg_maker/packaging/deb.py +337 -0
- pg_maker/packaging/service.py +165 -0
- pg_maker/packaging/spec.py +64 -0
- pg_maker/packaging/templates/deb-control.in +8 -0
- pg_maker/packaging/tree.py +239 -0
- pg_maker/planning/__init__.py +1 -0
- pg_maker/planning/documents.py +65 -0
- pg_maker/planning/environments.py +318 -0
- pg_maker/planning/graph.py +23 -0
- pg_maker/planning/plan.py +418 -0
- pg_maker/planning/resolve.py +117 -0
- pg_maker/planning/reuse.py +80 -0
- pg_maker/planning/runtime_repository.py +90 -0
- pg_maker/postgresql.py +59 -0
- pg_maker/progress.py +64 -0
- pg_maker/publishing/__init__.py +4 -0
- pg_maker/py.typed +0 -0
- pg_maker/recipes/README.md +13 -0
- pg_maker/recipes/platforms/debian-13.yaml +125 -0
- pg_maker/recipes/postgresql.yaml +114 -0
- pg_maker/runners/__init__.py +1 -0
- pg_maker/runners/base.py +29 -0
- pg_maker/runners/docker.py +312 -0
- pg_maker/schemas/build.schema.json +5 -0
- pg_maker/schemas/change-set.schema.json +5 -0
- pg_maker/schemas/component-envelope.schema.json +69 -0
- pg_maker/schemas/component.schema.json +5 -0
- pg_maker/schemas/defaults.schema.json +5 -0
- pg_maker/schemas/doc-rules.schema.json +5 -0
- pg_maker/schemas/documentation.schema.json +5 -0
- pg_maker/schemas/environment.schema.json +228 -0
- pg_maker/schemas/feature.schema.json +5 -0
- pg_maker/schemas/insertions.schema.json +5 -0
- pg_maker/schemas/lock.schema.json +3963 -0
- pg_maker/schemas/operation-input.schema.json +306 -0
- pg_maker/schemas/operation-state.schema.json +127 -0
- pg_maker/schemas/plan.schema.json +284 -0
- pg_maker/schemas/planning-common.schema.json +8192 -0
- pg_maker/schemas/platform.schema.json +5 -0
- pg_maker/schemas/postgresql-catalog.schema.json +5 -0
- pg_maker/schemas/run-event.schema.json +72 -0
- pg_maker/schemas/translations.schema.json +5 -0
- pg_maker/schemas/vendor.schema.json +5 -0
- pg_maker/security.py +73 -0
- pg_maker/sources/__init__.py +1 -0
- pg_maker/sources/resolver.py +238 -0
- pg_maker/sources/store.py +279 -0
- pg_maker/storage.py +127 -0
- pg_maker/validation/__init__.py +4 -0
- pg_maker/validation/installed.py +667 -0
- pg_maker/validation/packages.py +309 -0
- pg_maker/validation/systemctl.py +185 -0
- pg_maker/yamlio.py +54 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/CHANGELOG.md +22 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/CONTRIBUTING.md +63 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/LICENSE +21 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/README.md +163 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/architecture.md +55 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development-storage.md +72 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/development.md +86 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/README.md +32 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/api.md +75 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/build.md +112 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/changes.md +58 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/cleanup.md +17 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/components.md +39 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/configuration.md +83 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/development.md +88 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documentation.md +241 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/documents.md +42 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/environments.md +40 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/execution.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/machine.md +31 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/observability.md +28 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/packaging.md +107 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/planning.md +63 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/postgresql.md +79 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/publication.md +20 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/recovery.md +83 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/runtime.md +31 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/sources.md +48 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/state.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/interfaces/storage.md +24 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/releasing.md +98 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/reuse.md +24 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/roadmap.md +54 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/default-components.md +56 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/development-release.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/documentation-theme.md +45 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/integration.md +49 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/postgresql-majors.md +107 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/review-fixes.md +68 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage4.md +65 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage56.md +66 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/docs/validation/stage7.md +72 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/guc/README.md +11 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage4/README.md +31 -0
- pg_maker-0.1.0.data/data/share/doc/pg-maker/tests/fixtures/stage7/README.md +16 -0
- pg_maker-0.1.0.dist-info/METADATA +200 -0
- pg_maker-0.1.0.dist-info/RECORD +181 -0
- pg_maker-0.1.0.dist-info/WHEEL +5 -0
- pg_maker-0.1.0.dist-info/entry_points.txt +4 -0
- pg_maker-0.1.0.dist-info/licenses/LICENSE +21 -0
- pg_maker-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Portable artifact manifests and immutable verification records."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import copy
|
|
6
|
+
import html
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import shutil
|
|
10
|
+
import tempfile
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from types import SimpleNamespace
|
|
13
|
+
|
|
14
|
+
from pg_maker.documents import relative_output
|
|
15
|
+
from pg_maker.errors import PreconditionError
|
|
16
|
+
from pg_maker.packaging.service import settings
|
|
17
|
+
from pg_maker.sources.store import InputStore, inventory
|
|
18
|
+
from pg_maker.storage import (
|
|
19
|
+
content_hash,
|
|
20
|
+
ensure_directory,
|
|
21
|
+
file_hash,
|
|
22
|
+
read_json,
|
|
23
|
+
sync_directory,
|
|
24
|
+
write_json,
|
|
25
|
+
write_text,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def finish(branch):
|
|
30
|
+
qualified = branch.report["qualification"]
|
|
31
|
+
if branch.request.plan["spec"]["goal"] == "verified":
|
|
32
|
+
results = branch.report.get("package_tests", {})
|
|
33
|
+
if (
|
|
34
|
+
qualified != "source-verified"
|
|
35
|
+
or not results.get("passed")
|
|
36
|
+
or not results.get("run_database")
|
|
37
|
+
):
|
|
38
|
+
raise PreconditionError("verified goal lacks source/package evidence")
|
|
39
|
+
qualified = "package-verified"
|
|
40
|
+
# Eligibility is granted only after the entire matrix (including baseline) succeeds.
|
|
41
|
+
eligible = False
|
|
42
|
+
evidence = []
|
|
43
|
+
for path in sorted(branch.output.rglob("*.json")):
|
|
44
|
+
if path.name in {"report.json", "artifact-manifest.json", "verification.json"}:
|
|
45
|
+
continue
|
|
46
|
+
if "commands" not in path.parts:
|
|
47
|
+
evidence.append(
|
|
48
|
+
{
|
|
49
|
+
"path": path.relative_to(branch.output).as_posix(),
|
|
50
|
+
"sha256": file_hash(path),
|
|
51
|
+
"size_bytes": path.stat().st_size,
|
|
52
|
+
}
|
|
53
|
+
)
|
|
54
|
+
verification = {
|
|
55
|
+
"api_version": "pg_maker/verification/v1",
|
|
56
|
+
"qualification": qualified,
|
|
57
|
+
"run_id": branch.context.directory.name,
|
|
58
|
+
"attempt": branch.context.state["attempt"],
|
|
59
|
+
"plan_hash": branch.request.plan["plan_hash"],
|
|
60
|
+
"lock_hash": branch.request.lock["lock_hash"],
|
|
61
|
+
"db_tests": branch.report["db_tests"],
|
|
62
|
+
"evidence": evidence,
|
|
63
|
+
"service_manager": "emulated" if qualified == "package-verified" else "not-tested",
|
|
64
|
+
}
|
|
65
|
+
verification["verification_hash"] = content_hash(verification)
|
|
66
|
+
write_json(branch.output / "verification.json", verification)
|
|
67
|
+
target = next(t for t in branch.config["targets"] if t["id"] == branch.variant["target"])
|
|
68
|
+
common = {
|
|
69
|
+
"target_id": target["id"],
|
|
70
|
+
"os": target["platform"]["spec"]["os"],
|
|
71
|
+
"architecture": target["architecture"],
|
|
72
|
+
"feature": branch.feature["metadata"]["id"],
|
|
73
|
+
"vendor": branch.config["vendor"]["slug"],
|
|
74
|
+
"build_mode": branch.variant["build"]["mode"],
|
|
75
|
+
"postgresql_version": branch.config["postgresql"]["version"],
|
|
76
|
+
"product_version": branch.variant["identity"]["product_version"],
|
|
77
|
+
"source_hash": branch.report["source_hash"],
|
|
78
|
+
"install_tree_hash": branch.report["install_tree"]["tree_hash"],
|
|
79
|
+
"producer": {
|
|
80
|
+
"run_id": branch.context.directory.name,
|
|
81
|
+
"lock_hash": branch.request.lock["lock_hash"],
|
|
82
|
+
"plan_hash": branch.request.plan["plan_hash"],
|
|
83
|
+
},
|
|
84
|
+
"build_producer": branch.report.get("cache_producer"),
|
|
85
|
+
"build_producer_scope": "core-and-bundled-contrib",
|
|
86
|
+
"component_producers": {
|
|
87
|
+
c["id"]: c.get(
|
|
88
|
+
"cache_producer",
|
|
89
|
+
{
|
|
90
|
+
"run_id": branch.context.directory.name,
|
|
91
|
+
"lock_hash": branch.request.lock["lock_hash"],
|
|
92
|
+
},
|
|
93
|
+
)
|
|
94
|
+
for c in branch.report.get("components", [])
|
|
95
|
+
},
|
|
96
|
+
"qualification": qualified,
|
|
97
|
+
"verification": {
|
|
98
|
+
"path": "verification.json",
|
|
99
|
+
"sha256": file_hash(branch.output / "verification.json"),
|
|
100
|
+
},
|
|
101
|
+
}
|
|
102
|
+
artifacts = [
|
|
103
|
+
{
|
|
104
|
+
**common,
|
|
105
|
+
**package,
|
|
106
|
+
"id": package["name"],
|
|
107
|
+
"media_type": "application/vnd.debian.binary-package",
|
|
108
|
+
"package_role": package["role"],
|
|
109
|
+
"role": "baseline-package" if branch.branch == "baseline" else package["role"],
|
|
110
|
+
}
|
|
111
|
+
for package in branch.report["packages"]
|
|
112
|
+
]
|
|
113
|
+
if branch.config.get("documentation", {}).get("enabled", False):
|
|
114
|
+
docs = read_json(branch.output / "docs-manifest.json")
|
|
115
|
+
if branch.request.plan["spec"]["goal"] == "verified" and any(
|
|
116
|
+
not value["quality"]["complete"] for value in docs["languages"].values()
|
|
117
|
+
):
|
|
118
|
+
raise PreconditionError("incomplete documentation cannot qualify a package")
|
|
119
|
+
for artifact in artifacts:
|
|
120
|
+
artifact["documentation"] = {
|
|
121
|
+
"path": "docs-manifest.json",
|
|
122
|
+
"sha256": file_hash(branch.output / "docs-manifest.json"),
|
|
123
|
+
"languages": docs["profile"]["languages"],
|
|
124
|
+
"formats": docs["profile"]["formats"],
|
|
125
|
+
"backend": docs["profile"]["html_backend"],
|
|
126
|
+
"manifest_hash": docs["manifest_hash"],
|
|
127
|
+
}
|
|
128
|
+
manifest = {
|
|
129
|
+
"api_version": "pg_maker/artifact-manifest/v1",
|
|
130
|
+
"artifacts": artifacts,
|
|
131
|
+
"publication_eligible": eligible,
|
|
132
|
+
}
|
|
133
|
+
manifest["manifest_hash"] = content_hash(manifest)
|
|
134
|
+
verify_manifest(branch.output, manifest)
|
|
135
|
+
write_json(branch.output / "artifact-manifest.json", manifest)
|
|
136
|
+
for name, value in (("lock", branch.request.lock), ("plan", branch.request.plan)):
|
|
137
|
+
document = copy.deepcopy(value)
|
|
138
|
+
document["transport"] = {}
|
|
139
|
+
write_json(branch.output / (name + ".json"), document)
|
|
140
|
+
s = settings(branch.config, branch.variant)
|
|
141
|
+
commands = " ".join("./" + p["path"] for p in branch.report["packages"])
|
|
142
|
+
write_text(
|
|
143
|
+
branch.output / "INSTALL.txt",
|
|
144
|
+
"Install with the distribution package manager:\n"
|
|
145
|
+
"apt-get install " + commands + "\n\n"
|
|
146
|
+
"Installation does not initialize or start a database.\n"
|
|
147
|
+
f"runuser -u {s['user']} -- {s['prefix']}/bin/initdb -D {s['data']}\n"
|
|
148
|
+
f"systemctl start {s['name']}.service\n"
|
|
149
|
+
"Configuration and file logs belong inside PGDATA. Remove/purge preserves PGDATA.\n"
|
|
150
|
+
f"Client programs: {s['prefix']}/bin\n"
|
|
151
|
+
f"Qualification: {qualified}; DB tests: {branch.report['db_tests']}.\n",
|
|
152
|
+
)
|
|
153
|
+
branch.report.update(
|
|
154
|
+
qualification=qualified,
|
|
155
|
+
publication_eligible=eligible,
|
|
156
|
+
artifact_manifest="artifact-manifest.json",
|
|
157
|
+
verification="verification.json",
|
|
158
|
+
)
|
|
159
|
+
render_report(branch)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def render_report(branch):
|
|
163
|
+
write_text(
|
|
164
|
+
branch.output / "report.html",
|
|
165
|
+
'<!doctype html><meta charset="utf-8"><title>pg_maker report</title>'
|
|
166
|
+
"<h1>pg_maker</h1><pre>"
|
|
167
|
+
+ html.escape(json.dumps(branch.report, indent=2, ensure_ascii=False))
|
|
168
|
+
+ "</pre>",
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def complete_matrix(reports: list[Path]) -> None:
|
|
173
|
+
"""Preflight all branches before granting publication eligibility to any candidate."""
|
|
174
|
+
checked = []
|
|
175
|
+
for path in reports:
|
|
176
|
+
report = read_json(path)
|
|
177
|
+
if report.get("status") != "succeeded" or report.get("qualification") != "package-verified":
|
|
178
|
+
raise PreconditionError("publication eligibility requires every verified branch")
|
|
179
|
+
manifest = read_json(path.parent / "artifact-manifest.json")
|
|
180
|
+
verify_manifest(path.parent, manifest)
|
|
181
|
+
checked.append((path, report, manifest))
|
|
182
|
+
for path, report, manifest in checked:
|
|
183
|
+
if report["branch"] != "candidate":
|
|
184
|
+
continue
|
|
185
|
+
manifest["publication_eligible"] = True
|
|
186
|
+
manifest["manifest_hash"] = content_hash(
|
|
187
|
+
{k: v for k, v in manifest.items() if k != "manifest_hash"}
|
|
188
|
+
)
|
|
189
|
+
write_json(path.parent / "artifact-manifest.json", manifest)
|
|
190
|
+
report["publication_eligible"] = True
|
|
191
|
+
write_json(path, report)
|
|
192
|
+
render_report(SimpleNamespace(output=path.parent, report=report))
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def verify_manifest(root: Path, manifest: dict):
|
|
196
|
+
core = {k: v for k, v in manifest.items() if k != "manifest_hash"}
|
|
197
|
+
if manifest.get("manifest_hash") != content_hash(core):
|
|
198
|
+
raise PreconditionError("artifact manifest hash changed")
|
|
199
|
+
for artifact in manifest["artifacts"]:
|
|
200
|
+
for reference in (artifact, artifact["verification"]):
|
|
201
|
+
path = reference["path"]
|
|
202
|
+
if not relative_output(path):
|
|
203
|
+
raise PreconditionError("artifact reference must be relative")
|
|
204
|
+
file = root / path
|
|
205
|
+
if any(p.is_symlink() for p in (file, *file.parents)) or not file.is_file():
|
|
206
|
+
raise PreconditionError("artifact is missing or traverses a symlink")
|
|
207
|
+
if file_hash(file) != reference["sha256"]:
|
|
208
|
+
raise PreconditionError("artifact hash changed", details={"path": path})
|
|
209
|
+
if "size_bytes" in reference and file.stat().st_size != reference["size_bytes"]:
|
|
210
|
+
raise PreconditionError("artifact size changed")
|
|
211
|
+
verification_path = root / "verification.json"
|
|
212
|
+
if verification_path.is_file():
|
|
213
|
+
value = read_json(verification_path)
|
|
214
|
+
if value["verification_hash"] != content_hash(
|
|
215
|
+
{k: v for k, v in value.items() if k != "verification_hash"}
|
|
216
|
+
):
|
|
217
|
+
raise PreconditionError("verification record hash changed")
|
|
218
|
+
for evidence in value["evidence"]:
|
|
219
|
+
file = root / evidence["path"]
|
|
220
|
+
if (
|
|
221
|
+
not relative_output(evidence["path"])
|
|
222
|
+
or any(p.is_symlink() for p in (file, *file.parents))
|
|
223
|
+
or not file.is_file()
|
|
224
|
+
or file_hash(file) != evidence["sha256"]
|
|
225
|
+
):
|
|
226
|
+
raise PreconditionError("verification evidence changed")
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def export_operation(directory: Path, destination: Path, *, include_inputs=False) -> dict:
|
|
230
|
+
if destination.exists() or destination.is_symlink():
|
|
231
|
+
raise PreconditionError("export destination already exists")
|
|
232
|
+
ensure_directory(destination.parent)
|
|
233
|
+
output = directory / "workspace/output"
|
|
234
|
+
for path in output.glob("*/artifact-manifest.json"):
|
|
235
|
+
verify_manifest(path.parent, read_json(path))
|
|
236
|
+
with tempfile.TemporaryDirectory(dir=destination.parent, prefix=".pg-maker-export-") as name:
|
|
237
|
+
staging = Path(name) / "result"
|
|
238
|
+
staging.mkdir()
|
|
239
|
+
if output.is_dir():
|
|
240
|
+
shutil.copytree(output, staging / "output", symlinks=True)
|
|
241
|
+
for name in ("attempts",):
|
|
242
|
+
if (directory / name).is_dir():
|
|
243
|
+
shutil.copytree(
|
|
244
|
+
directory / name,
|
|
245
|
+
staging / name,
|
|
246
|
+
symlinks=True,
|
|
247
|
+
ignore=shutil.ignore_patterns("workspace", "previous"),
|
|
248
|
+
)
|
|
249
|
+
for name in ("input.json", "state.json", "events.jsonl", "worker.log"):
|
|
250
|
+
if (directory / name).is_file():
|
|
251
|
+
shutil.copy2(directory / name, staging / name)
|
|
252
|
+
request = read_json(directory / "input.json")
|
|
253
|
+
state = read_json(directory / "state.json")
|
|
254
|
+
if request["operation_kind"] in {
|
|
255
|
+
"prepare_environments",
|
|
256
|
+
"resolve_inputs",
|
|
257
|
+
"runtime_image",
|
|
258
|
+
} and state.get("result"):
|
|
259
|
+
produced = Path(state["result"]["document"])
|
|
260
|
+
if file_hash(produced) != state["result"]["sha256"]:
|
|
261
|
+
raise PreconditionError("prepared output changed before export")
|
|
262
|
+
shutil.copyfile(produced, staging / "prepared-document.json")
|
|
263
|
+
if include_inputs and request["operation_kind"] == "build":
|
|
264
|
+
store = InputStore(Path(request["input_store"]))
|
|
265
|
+
config = request["lock"]["spec"]["config"]
|
|
266
|
+
trees = {config["source"]["source_hash"]}
|
|
267
|
+
trees.update(
|
|
268
|
+
c["spec"]["source"]["source_hash"]
|
|
269
|
+
for f in config["features"]
|
|
270
|
+
for c in f["spec"]["components"]
|
|
271
|
+
if "source" in c["spec"]
|
|
272
|
+
)
|
|
273
|
+
destination_store = InputStore(staging / "inputs")
|
|
274
|
+
for digest in trees:
|
|
275
|
+
destination_store.put_tree(store.tree(digest))
|
|
276
|
+
for digest in request["lock"]["spec"]["blobs"].values():
|
|
277
|
+
destination_store.put_blob(store.blob(digest), digest)
|
|
278
|
+
for path in (staging / "output").glob("*/artifact-manifest.json"):
|
|
279
|
+
verify_manifest(path.parent, read_json(path))
|
|
280
|
+
contents = inventory(staging)
|
|
281
|
+
write_json(
|
|
282
|
+
staging / "export-manifest.json",
|
|
283
|
+
{
|
|
284
|
+
"api_version": "pg_maker/export/v1",
|
|
285
|
+
"operation_id": directory.name,
|
|
286
|
+
"includes_inputs": include_inputs,
|
|
287
|
+
"inventory": contents,
|
|
288
|
+
"tree_hash": content_hash(contents),
|
|
289
|
+
},
|
|
290
|
+
)
|
|
291
|
+
for entry in contents["entries"]:
|
|
292
|
+
if entry["type"] == "file":
|
|
293
|
+
with (staging / entry["path"]).open("rb") as stream:
|
|
294
|
+
os.fsync(stream.fileno())
|
|
295
|
+
directories = [staging / e["path"] for e in contents["entries"] if e["type"] == "directory"]
|
|
296
|
+
for path in sorted(directories, key=lambda p: len(p.parts), reverse=True):
|
|
297
|
+
sync_directory(path)
|
|
298
|
+
sync_directory(staging)
|
|
299
|
+
os.replace(staging, destination)
|
|
300
|
+
sync_directory(destination.parent)
|
|
301
|
+
return {"directory": str(destination), "tree_hash": content_hash(contents)}
|
pg_maker/assets.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Idempotent project initialization from installed package resources."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from pg_maker.errors import OwnershipError, ValidationError
|
|
7
|
+
from pg_maker.storage import ensure_directory, write_text
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def initialize_project(
|
|
11
|
+
directory: Path, *, with_doc=None, doc_languages=None, features=None, build_mode=None
|
|
12
|
+
) -> dict:
|
|
13
|
+
ensure_directory(directory)
|
|
14
|
+
assets = {"README.md": files("pg_maker").joinpath("bundled", "PROJECT.md").read_text()}
|
|
15
|
+
bundled = files("pg_maker").joinpath("bundled")
|
|
16
|
+
assets["build.yaml"] = bundled.joinpath("build.yaml").read_text()
|
|
17
|
+
for item in bundled.joinpath("config").iterdir():
|
|
18
|
+
if item.name.endswith(".yaml"):
|
|
19
|
+
assets[f"config/{item.name}"] = item.read_text()
|
|
20
|
+
for item in bundled.joinpath("config", "features").iterdir():
|
|
21
|
+
if item.name.endswith(".yaml"):
|
|
22
|
+
assets[f"config/features/{item.name}"] = item.read_text()
|
|
23
|
+
for item in bundled.joinpath("patches").iterdir():
|
|
24
|
+
assets[f"patches/{item.name}"] = item.read_text()
|
|
25
|
+
|
|
26
|
+
# Component recipes and their documentation remain editable project inputs.
|
|
27
|
+
def collect(resource, prefix):
|
|
28
|
+
for item in sorted(resource.iterdir(), key=lambda item: item.name):
|
|
29
|
+
name = prefix + "/" + item.name
|
|
30
|
+
if item.is_dir():
|
|
31
|
+
collect(item, name)
|
|
32
|
+
else:
|
|
33
|
+
assets[name] = item.read_text()
|
|
34
|
+
|
|
35
|
+
for directory_name in ("components", "documentation"):
|
|
36
|
+
collect(bundled.joinpath(directory_name), directory_name)
|
|
37
|
+
for item in files("pg_maker").joinpath("schemas").iterdir():
|
|
38
|
+
if item.name.endswith(".json"):
|
|
39
|
+
assets[f"schemas/{item.name}"] = item.read_text()
|
|
40
|
+
if any(value is not None for value in (with_doc, doc_languages, features, build_mode)):
|
|
41
|
+
import yaml
|
|
42
|
+
|
|
43
|
+
manifest = yaml.safe_load(assets["build.yaml"])
|
|
44
|
+
docs = yaml.safe_load(assets["config/documentation.yaml"])
|
|
45
|
+
defaults = yaml.safe_load(assets["config/defaults.yaml"])
|
|
46
|
+
if with_doc is not None:
|
|
47
|
+
docs["spec"]["enabled"] = with_doc
|
|
48
|
+
if doc_languages is not None:
|
|
49
|
+
if not docs["spec"]["enabled"]:
|
|
50
|
+
raise ValidationError("doc-language requires enabled documentation")
|
|
51
|
+
aliases = docs["spec"].get("language_aliases", {"en": "en", "ru": "ru"})
|
|
52
|
+
if (
|
|
53
|
+
not doc_languages
|
|
54
|
+
or len(set(doc_languages)) != len(doc_languages)
|
|
55
|
+
or any(lang not in aliases for lang in doc_languages)
|
|
56
|
+
):
|
|
57
|
+
raise ValidationError("unknown or duplicate documentation language")
|
|
58
|
+
docs["spec"]["languages"] = doc_languages
|
|
59
|
+
if features is not None:
|
|
60
|
+
if not features or any(
|
|
61
|
+
"config/features/" + name + ".yaml" not in assets for name in features
|
|
62
|
+
):
|
|
63
|
+
raise ValidationError("init requires an available feature")
|
|
64
|
+
manifest["spec"]["features"] = features
|
|
65
|
+
if build_mode is not None:
|
|
66
|
+
if build_mode not in {"release", "debug"}:
|
|
67
|
+
raise ValidationError("unknown build mode")
|
|
68
|
+
defaults["spec"]["build"]["mode"] = build_mode
|
|
69
|
+
for name, value in (
|
|
70
|
+
("build.yaml", manifest),
|
|
71
|
+
("config/documentation.yaml", docs),
|
|
72
|
+
("config/defaults.yaml", defaults),
|
|
73
|
+
):
|
|
74
|
+
assets[name] = yaml.safe_dump(value, sort_keys=False)
|
|
75
|
+
# Preflight every target first: a conflict must not leave a half-updated project.
|
|
76
|
+
for name, text in assets.items():
|
|
77
|
+
target = directory / name
|
|
78
|
+
for item in (target, *target.parents):
|
|
79
|
+
if item.is_symlink():
|
|
80
|
+
raise OwnershipError(f"initialization target traverses a symlink: {item}")
|
|
81
|
+
if any(item.exists() and not item.is_dir() for item in target.parents):
|
|
82
|
+
raise OwnershipError(f"project parent path is not a directory: {target.parent}")
|
|
83
|
+
if target.exists() and (not target.is_file() or target.read_text() != text):
|
|
84
|
+
raise OwnershipError(f"project file already exists with different content: {target}")
|
|
85
|
+
created, unchanged = [], []
|
|
86
|
+
for name, text in assets.items():
|
|
87
|
+
target = directory / name
|
|
88
|
+
if target.exists():
|
|
89
|
+
unchanged.append(name)
|
|
90
|
+
else:
|
|
91
|
+
write_text(target, text)
|
|
92
|
+
created.append(name)
|
|
93
|
+
return {"directory": str(directory), "created": created, "unchanged": unchanged}
|