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,309 @@
|
|
|
1
|
+
"""Run installed-product checks in fresh, network-isolated test environments."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import asdict
|
|
6
|
+
|
|
7
|
+
from pg_maker.config import resource_path
|
|
8
|
+
from pg_maker.errors import CancelledError, CommandError, PreconditionError
|
|
9
|
+
from pg_maker.models import CleanupPolicy, CommandSpec, EnvironmentSpec
|
|
10
|
+
from pg_maker.packaging.deb import inspect_deb
|
|
11
|
+
from pg_maker.packaging.service import settings
|
|
12
|
+
from pg_maker.planning.runtime_repository import verify_repository
|
|
13
|
+
from pg_maker.runners.docker import DockerRunner
|
|
14
|
+
from pg_maker.storage import content_hash, ensure_directory, file_hash, read_json, write_json
|
|
15
|
+
from pg_maker.yamlio import load_yaml
|
|
16
|
+
|
|
17
|
+
INSTALL_CAPABILITIES = ("CHOWN", "DAC_OVERRIDE", "FOWNER", "FSETID", "SETUID", "SETGID", "KILL")
|
|
18
|
+
SERVER_CHECKS = {
|
|
19
|
+
"missing-internal-dependency-rejected",
|
|
20
|
+
"installed-inventory-and-loader",
|
|
21
|
+
"install-does-not-initdb",
|
|
22
|
+
"empty-pgdata-start-rejected",
|
|
23
|
+
"service-start-status-reload",
|
|
24
|
+
"socket-and-tcp",
|
|
25
|
+
"compiled-socket-default",
|
|
26
|
+
"installed-server-version",
|
|
27
|
+
"restart-preserves-data",
|
|
28
|
+
"dump-restore",
|
|
29
|
+
"crash-recovery",
|
|
30
|
+
"checksums",
|
|
31
|
+
"invalid-config-permissions-port-socket-rejected",
|
|
32
|
+
"reinstall-preserves-config-data",
|
|
33
|
+
"config-and-file-logs-in-pgdata",
|
|
34
|
+
"remove-preserves-data",
|
|
35
|
+
"install-after-remove",
|
|
36
|
+
"purge-preserves-data",
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def runtime_repository(branch):
|
|
41
|
+
image = next(
|
|
42
|
+
i
|
|
43
|
+
for i in branch.request.lock["spec"]["environments"]["spec"]["images"]
|
|
44
|
+
if i["id"] == branch.variant["target"] + ".build"
|
|
45
|
+
)
|
|
46
|
+
repository = image.get("runtime_repository")
|
|
47
|
+
if not repository or repository["repository_hash"] != content_hash(repository["files"]):
|
|
48
|
+
raise PreconditionError(
|
|
49
|
+
"prepared build image has no pinned runtime repository; run env prepare"
|
|
50
|
+
)
|
|
51
|
+
destination = branch.output / "runtime-repository"
|
|
52
|
+
if not destination.exists():
|
|
53
|
+
branch.prepare()
|
|
54
|
+
branch.runner._control(
|
|
55
|
+
["cp", branch.handle.container_id + ":" + repository["path"], str(destination)]
|
|
56
|
+
)
|
|
57
|
+
# APT's empty scratch entries are not part of its package inventory.
|
|
58
|
+
for name in ("partial", "lock"):
|
|
59
|
+
path = destination / name
|
|
60
|
+
if path.is_symlink():
|
|
61
|
+
raise PreconditionError("runtime repository contains a symlink")
|
|
62
|
+
if path.is_dir() and not any(path.iterdir()):
|
|
63
|
+
path.rmdir()
|
|
64
|
+
elif path.is_file() and not path.stat().st_size:
|
|
65
|
+
path.unlink()
|
|
66
|
+
verify_repository(destination, repository)
|
|
67
|
+
write_json(branch.output / "runtime-repository.json", repository)
|
|
68
|
+
return destination
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate_packages(branch, *, run_database=True):
|
|
72
|
+
repository = runtime_repository(branch)
|
|
73
|
+
package_list = read_json(branch.output / "packages.json")
|
|
74
|
+
for package in package_list:
|
|
75
|
+
if file_hash(branch.output / package["path"]) != package["sha256"]:
|
|
76
|
+
raise PreconditionError("package changed before installation")
|
|
77
|
+
tree = read_json(branch.output / "install-tree.json")
|
|
78
|
+
previous = next(
|
|
79
|
+
(
|
|
80
|
+
r
|
|
81
|
+
for r in branch.config.get("previous_releases", [])
|
|
82
|
+
if r["variant"] == branch.variant["id"] and branch.branch == "candidate"
|
|
83
|
+
),
|
|
84
|
+
None,
|
|
85
|
+
)
|
|
86
|
+
previous_packages = []
|
|
87
|
+
if previous:
|
|
88
|
+
for item in previous["packages"]:
|
|
89
|
+
path = branch.inputs.blob(item["sha256"])
|
|
90
|
+
metadata = inspect_deb(path)["control"]
|
|
91
|
+
current = next(p for p in package_list if p["role"] == item["role"])
|
|
92
|
+
if (
|
|
93
|
+
metadata.get("Package") != current["name"]
|
|
94
|
+
or metadata.get("Architecture") != current["native_arch"]
|
|
95
|
+
):
|
|
96
|
+
raise PreconditionError("previous package identity differs from the upgrade target")
|
|
97
|
+
previous_packages.append(
|
|
98
|
+
{**item, "version": metadata["Version"], "filename": item["role"] + ".deb"}
|
|
99
|
+
)
|
|
100
|
+
branch.release()
|
|
101
|
+
acceptance, seen = [], set()
|
|
102
|
+
for change in branch.feature["spec"]["changes"]:
|
|
103
|
+
for test in change["spec"]["acceptance"]:
|
|
104
|
+
if branch.branch not in test["expectations"]:
|
|
105
|
+
continue
|
|
106
|
+
if test["id"] in seen:
|
|
107
|
+
continue
|
|
108
|
+
seen.add(test["id"])
|
|
109
|
+
blob = branch.inputs.blob(test["script"]["sha256"])
|
|
110
|
+
item = dict(test)
|
|
111
|
+
if test["kind"] == "command":
|
|
112
|
+
document = load_yaml(blob)
|
|
113
|
+
if set(document) != {"argv"} or not isinstance(document["argv"], list):
|
|
114
|
+
raise PreconditionError("invalid command acceptance script")
|
|
115
|
+
item["argv"] = document["argv"]
|
|
116
|
+
else:
|
|
117
|
+
item["sql"] = blob.read_text()
|
|
118
|
+
acceptance.append(item)
|
|
119
|
+
outcomes = []
|
|
120
|
+
scenarios = [
|
|
121
|
+
("libpq", ("libpq",)),
|
|
122
|
+
("client", ("libpq", "client")),
|
|
123
|
+
("server", ("libpq", "client", "server")),
|
|
124
|
+
]
|
|
125
|
+
documentation = branch.config.get("documentation", {}).get("enabled", False)
|
|
126
|
+
if documentation:
|
|
127
|
+
scenarios[-1] = ("server", ("libpq", "client", "server", "docs"))
|
|
128
|
+
scenarios.append(("docs", ("docs",)))
|
|
129
|
+
if previous and run_database:
|
|
130
|
+
scenarios.append(("upgrade", ("libpq", "client", "server")))
|
|
131
|
+
for label, roles in scenarios:
|
|
132
|
+
branch.context.check()
|
|
133
|
+
workspace = (
|
|
134
|
+
branch.context.directory
|
|
135
|
+
/ "validation"
|
|
136
|
+
/ branch.id
|
|
137
|
+
/ str(branch.context.state["attempt"])
|
|
138
|
+
/ label
|
|
139
|
+
)
|
|
140
|
+
ensure_directory(workspace)
|
|
141
|
+
runner = DockerRunner(
|
|
142
|
+
branch.context.directory,
|
|
143
|
+
operation_id=branch.context.directory.name,
|
|
144
|
+
owner_token=branch.context.state["owner_token"],
|
|
145
|
+
cancelled=branch.context.token.requested,
|
|
146
|
+
on_process=branch.context.on_process,
|
|
147
|
+
attempt=branch.context.state["attempt"],
|
|
148
|
+
workspace=workspace,
|
|
149
|
+
)
|
|
150
|
+
source = workspace / "source"
|
|
151
|
+
ensure_directory(source)
|
|
152
|
+
shutil.copytree(branch.output / "packages", source / "packages")
|
|
153
|
+
verify_repository(repository, read_json(branch.output / "runtime-repository.json"))
|
|
154
|
+
shutil.copytree(repository, source / "repository", symlinks=True)
|
|
155
|
+
if label == "upgrade":
|
|
156
|
+
ensure_directory(source / "previous")
|
|
157
|
+
for item in previous_packages:
|
|
158
|
+
shutil.copyfile(
|
|
159
|
+
branch.inputs.blob(item["sha256"]), source / "previous" / item["filename"]
|
|
160
|
+
)
|
|
161
|
+
for name in ("systemctl.py", "installed.py"):
|
|
162
|
+
shutil.copyfile(resource_path("validation", name), source / name)
|
|
163
|
+
if documentation:
|
|
164
|
+
shutil.copytree(
|
|
165
|
+
resource_path(),
|
|
166
|
+
source / "doc-tools/pg_maker",
|
|
167
|
+
ignore=shutil.ignore_patterns("__pycache__", "*.pyc"),
|
|
168
|
+
)
|
|
169
|
+
config = {
|
|
170
|
+
"service": settings(branch.config, branch.variant),
|
|
171
|
+
"roles": roles,
|
|
172
|
+
"packages": package_list,
|
|
173
|
+
"inventory": tree["entries"],
|
|
174
|
+
"branch": branch.branch,
|
|
175
|
+
"postgresql_version": branch.config["postgresql"]["version"],
|
|
176
|
+
"components": [
|
|
177
|
+
{"id": c["metadata"]["id"], **c["spec"]}
|
|
178
|
+
for c in branch.feature["spec"]["components"]
|
|
179
|
+
],
|
|
180
|
+
"acceptance": acceptance,
|
|
181
|
+
"run_database": run_database,
|
|
182
|
+
"previous_declared": previous is not None,
|
|
183
|
+
"previous": {**previous, "packages": previous_packages} if label == "upgrade" else None,
|
|
184
|
+
"documentation": read_json(branch.output / "installed-docs.json")
|
|
185
|
+
if documentation
|
|
186
|
+
else None,
|
|
187
|
+
"documentation_root": branch.variant["identity"]["paths"]["documentation_directory"],
|
|
188
|
+
"documentation_branding": branch.config["documentation"]["branding"],
|
|
189
|
+
}
|
|
190
|
+
write_json(source / "config.json", config)
|
|
191
|
+
environment = EnvironmentSpec(
|
|
192
|
+
branch.step["image_id"], "0:0", branch.id + ".packages." + label
|
|
193
|
+
)
|
|
194
|
+
handle = runner.intended_handle(environment)
|
|
195
|
+
|
|
196
|
+
def record(handle):
|
|
197
|
+
branch.context.event(
|
|
198
|
+
"environment_recorded",
|
|
199
|
+
resources=[
|
|
200
|
+
{
|
|
201
|
+
"kind": "docker",
|
|
202
|
+
"name": handle.name,
|
|
203
|
+
"labels": handle.labels,
|
|
204
|
+
"container_id": handle.container_id,
|
|
205
|
+
"environment": asdict(handle.environment),
|
|
206
|
+
}
|
|
207
|
+
],
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
record(handle)
|
|
211
|
+
try:
|
|
212
|
+
handle = runner.prepare(
|
|
213
|
+
environment,
|
|
214
|
+
memory_bytes=branch.step["resources"]["memory_bytes"],
|
|
215
|
+
cpu_jobs=branch.step["resources"]["cpu_jobs"],
|
|
216
|
+
capabilities=INSTALL_CAPABILITIES,
|
|
217
|
+
)
|
|
218
|
+
record(handle)
|
|
219
|
+
command = CommandSpec(
|
|
220
|
+
branch.id + ".packages." + label,
|
|
221
|
+
(
|
|
222
|
+
"python3",
|
|
223
|
+
"/work/source/installed.py",
|
|
224
|
+
"/work/source/config.json",
|
|
225
|
+
"/tmp/pg-maker-results",
|
|
226
|
+
),
|
|
227
|
+
cwd="/",
|
|
228
|
+
timeout_seconds=branch.step["timeout_seconds"],
|
|
229
|
+
)
|
|
230
|
+
result = runner.execute(handle, command)
|
|
231
|
+
output = branch.output / "package-tests" / label
|
|
232
|
+
ensure_directory(output.parent)
|
|
233
|
+
copied = runner._control(
|
|
234
|
+
["cp", handle.container_id + ":/tmp/pg-maker-results", str(output)],
|
|
235
|
+
check=False,
|
|
236
|
+
)
|
|
237
|
+
if result.cancelled:
|
|
238
|
+
raise CancelledError("package validation cancelled")
|
|
239
|
+
if (
|
|
240
|
+
not result.succeeded
|
|
241
|
+
or not copied.succeeded
|
|
242
|
+
or not (output / "result.json").is_file()
|
|
243
|
+
):
|
|
244
|
+
raise CommandError(
|
|
245
|
+
"installed package checks failed",
|
|
246
|
+
details={
|
|
247
|
+
"set": label,
|
|
248
|
+
"stderr": result.stderr_tail,
|
|
249
|
+
"timed_out": result.timed_out,
|
|
250
|
+
},
|
|
251
|
+
)
|
|
252
|
+
report = read_json(output / "result.json")
|
|
253
|
+
required = (
|
|
254
|
+
{"previous-release-upgrade"}
|
|
255
|
+
if label == "upgrade"
|
|
256
|
+
else SERVER_CHECKS
|
|
257
|
+
if label == "server" and run_database
|
|
258
|
+
else {
|
|
259
|
+
"installed-inventory-and-loader",
|
|
260
|
+
"install-does-not-initdb",
|
|
261
|
+
"empty-pgdata-start-rejected",
|
|
262
|
+
"remove-preserves-empty-pgdata",
|
|
263
|
+
}
|
|
264
|
+
if label == "server"
|
|
265
|
+
else {
|
|
266
|
+
"installed-inventory-and-loader",
|
|
267
|
+
"no-cluster-or-service",
|
|
268
|
+
"standalone-remove",
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
passed = {c["id"] for c in report["checks"] if c["status"] == "passed"}
|
|
272
|
+
if documentation and label != "upgrade":
|
|
273
|
+
required = required | {"installed-documentation"}
|
|
274
|
+
if not report["passed"] or not required <= passed:
|
|
275
|
+
raise CommandError("installed check evidence is incomplete")
|
|
276
|
+
if (
|
|
277
|
+
label == "server"
|
|
278
|
+
and run_database
|
|
279
|
+
and not {"acceptance:" + t["id"] for t in acceptance} <= passed
|
|
280
|
+
):
|
|
281
|
+
raise CommandError("installed acceptance evidence is incomplete")
|
|
282
|
+
outcomes.append(
|
|
283
|
+
{
|
|
284
|
+
"set": label,
|
|
285
|
+
"passed": True,
|
|
286
|
+
"checks": sorted(passed),
|
|
287
|
+
"path": "package-tests/" + label + "/result.json",
|
|
288
|
+
"sha256": file_hash(output / "result.json"),
|
|
289
|
+
}
|
|
290
|
+
)
|
|
291
|
+
finally:
|
|
292
|
+
original_error = sys.exc_info()[1]
|
|
293
|
+
try:
|
|
294
|
+
runner.release(handle, CleanupPolicy.REMOVE)
|
|
295
|
+
branch.context.event("environment_released", resources=[])
|
|
296
|
+
except Exception:
|
|
297
|
+
if original_error is None:
|
|
298
|
+
raise
|
|
299
|
+
# Keep the exact refs for cancel/resume without hiding the original test error.
|
|
300
|
+
branch.context.event("environment_cleanup_failed")
|
|
301
|
+
result = {
|
|
302
|
+
"passed": True,
|
|
303
|
+
"sets": outcomes,
|
|
304
|
+
"run_database": run_database,
|
|
305
|
+
"service_manager": "emulated",
|
|
306
|
+
}
|
|
307
|
+
write_json(branch.output / "package-tests.json", result)
|
|
308
|
+
branch.report["package_tests"] = result
|
|
309
|
+
return result
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/python3
|
|
2
|
+
"""Strict test-only systemctl subset for pg_maker's generated units.
|
|
3
|
+
|
|
4
|
+
This is an independent, small harness, not the legacy EUPL systemctl implementation.
|
|
5
|
+
It never claims native systemd/cgroup behavior and rejects unsupported directives.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import configparser
|
|
11
|
+
import os
|
|
12
|
+
import pwd
|
|
13
|
+
import shlex
|
|
14
|
+
import subprocess
|
|
15
|
+
import sys
|
|
16
|
+
import time
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
SUPPORTED = {
|
|
20
|
+
"Unit": {"Description", "After"},
|
|
21
|
+
"Service": {
|
|
22
|
+
"Type",
|
|
23
|
+
"User",
|
|
24
|
+
"Group",
|
|
25
|
+
"WorkingDirectory",
|
|
26
|
+
"Environment",
|
|
27
|
+
"PIDFile",
|
|
28
|
+
"ExecStartPre",
|
|
29
|
+
"ExecStart",
|
|
30
|
+
"ExecStop",
|
|
31
|
+
"ExecReload",
|
|
32
|
+
"TimeoutStartSec",
|
|
33
|
+
"TimeoutStopSec",
|
|
34
|
+
},
|
|
35
|
+
"Install": {"WantedBy"},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_unit(name):
|
|
40
|
+
if "/" in name or not name.endswith(".service"):
|
|
41
|
+
raise ValueError("an explicit service name is required")
|
|
42
|
+
path = Path("/usr/lib/systemd/system") / name
|
|
43
|
+
parser = configparser.ConfigParser(interpolation=None, strict=True)
|
|
44
|
+
parser.optionxform = str
|
|
45
|
+
with path.open() as stream:
|
|
46
|
+
parser.read_file(stream)
|
|
47
|
+
for section in parser.sections():
|
|
48
|
+
if section not in SUPPORTED or set(parser[section]) - SUPPORTED[section]:
|
|
49
|
+
raise ValueError("unsupported unit directive")
|
|
50
|
+
service = dict(parser["Service"])
|
|
51
|
+
if service["Type"] != "forking":
|
|
52
|
+
raise ValueError("only forking units are supported")
|
|
53
|
+
if parser["Install"]["WantedBy"] != "multi-user.target":
|
|
54
|
+
raise ValueError("unsupported install target")
|
|
55
|
+
if any("%" in value for section in parser.sections() for value in parser[section].values()):
|
|
56
|
+
raise ValueError("unit specifiers are unsupported by this harness")
|
|
57
|
+
return service
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def pid(service):
|
|
61
|
+
try:
|
|
62
|
+
lines = Path(service["PIDFile"]).read_text().splitlines()
|
|
63
|
+
number = int(lines[0])
|
|
64
|
+
data = str(Path(service["PIDFile"]).parent)
|
|
65
|
+
if len(lines) < 2 or lines[1] != data:
|
|
66
|
+
return None
|
|
67
|
+
if number <= 1:
|
|
68
|
+
return None
|
|
69
|
+
process = Path("/proc") / str(number)
|
|
70
|
+
if process.stat().st_uid != pwd.getpwnam(service["User"]).pw_uid:
|
|
71
|
+
return None
|
|
72
|
+
if process.joinpath("stat").read_text().rsplit(")", 1)[1].split()[0] == "Z":
|
|
73
|
+
return None
|
|
74
|
+
command = process.joinpath("cmdline").read_bytes().split(b"\0")[0].decode()
|
|
75
|
+
expected = str(Path(shlex.split(service["ExecStart"])[0]).with_name("postgres"))
|
|
76
|
+
if command != expected and not command.startswith(expected + " "):
|
|
77
|
+
return None
|
|
78
|
+
arguments = shlex.split(
|
|
79
|
+
process.joinpath("cmdline").read_bytes().replace(b"\0", b" ").decode()
|
|
80
|
+
)
|
|
81
|
+
if "-D" not in arguments or arguments[arguments.index("-D") + 1] != data:
|
|
82
|
+
return None
|
|
83
|
+
os.kill(number, 0)
|
|
84
|
+
return number
|
|
85
|
+
except (OSError, ValueError, KeyError, IndexError):
|
|
86
|
+
return None
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def execute(service, key):
|
|
90
|
+
command = service[key]
|
|
91
|
+
privileged = command.startswith("+")
|
|
92
|
+
argv = shlex.split(command[1:] if privileged else command)
|
|
93
|
+
if not argv or not argv[0].startswith("/"):
|
|
94
|
+
raise ValueError("unit executable must be absolute")
|
|
95
|
+
environment = {"PATH": "/usr/sbin:/usr/bin:/sbin:/bin", "LC_ALL": "C.UTF-8"}
|
|
96
|
+
for item in shlex.split(service.get("Environment", "")):
|
|
97
|
+
name, value = item.split("=", 1)
|
|
98
|
+
environment[name] = value
|
|
99
|
+
options = (
|
|
100
|
+
{}
|
|
101
|
+
if privileged
|
|
102
|
+
else {
|
|
103
|
+
"user": service["User"],
|
|
104
|
+
"group": service["Group"],
|
|
105
|
+
"extra_groups": [],
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
result = subprocess.run(
|
|
109
|
+
argv,
|
|
110
|
+
cwd="/" if privileged else service["WorkingDirectory"],
|
|
111
|
+
env=environment,
|
|
112
|
+
timeout=int(service["TimeoutStopSec" if key == "ExecStop" else "TimeoutStartSec"]),
|
|
113
|
+
check=False,
|
|
114
|
+
**options,
|
|
115
|
+
)
|
|
116
|
+
if result.returncode:
|
|
117
|
+
raise RuntimeError(key + " failed with status " + str(result.returncode))
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def main(argv=None):
|
|
121
|
+
arguments = list(sys.argv[1:] if argv is None else argv)
|
|
122
|
+
quiet = "--quiet" in arguments
|
|
123
|
+
arguments = [a for a in arguments if a != "--quiet"]
|
|
124
|
+
if arguments == ["--version"]:
|
|
125
|
+
print("pg_maker systemctl test harness v1; service_manager=emulated")
|
|
126
|
+
return 0
|
|
127
|
+
if arguments == ["daemon-reload"]:
|
|
128
|
+
return 0
|
|
129
|
+
if len(arguments) != 2:
|
|
130
|
+
raise ValueError("unsupported systemctl invocation")
|
|
131
|
+
command, name = arguments
|
|
132
|
+
if command not in {
|
|
133
|
+
"start",
|
|
134
|
+
"stop",
|
|
135
|
+
"restart",
|
|
136
|
+
"reload",
|
|
137
|
+
"status",
|
|
138
|
+
"is-active",
|
|
139
|
+
"enable",
|
|
140
|
+
"disable",
|
|
141
|
+
}:
|
|
142
|
+
raise ValueError("unsupported systemctl command")
|
|
143
|
+
service = load_unit(name)
|
|
144
|
+
if command in {"enable", "disable"}:
|
|
145
|
+
link = Path("/etc/systemd/system/multi-user.target.wants") / name
|
|
146
|
+
if command == "enable":
|
|
147
|
+
link.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
+
if link.is_symlink():
|
|
149
|
+
if os.readlink(link) != "/usr/lib/systemd/system/" + name:
|
|
150
|
+
raise ValueError("conflicting service link")
|
|
151
|
+
else:
|
|
152
|
+
link.symlink_to("/usr/lib/systemd/system/" + name)
|
|
153
|
+
else:
|
|
154
|
+
link.unlink(missing_ok=True)
|
|
155
|
+
return 0
|
|
156
|
+
if command in {"status", "is-active"}:
|
|
157
|
+
active = pid(service) is not None
|
|
158
|
+
if not quiet:
|
|
159
|
+
print("active" if active else "inactive")
|
|
160
|
+
return 0 if active else 3
|
|
161
|
+
if command in {"stop", "restart"} and pid(service):
|
|
162
|
+
execute(service, "ExecStop")
|
|
163
|
+
deadline = time.monotonic() + int(service["TimeoutStopSec"])
|
|
164
|
+
while pid(service) and time.monotonic() < deadline:
|
|
165
|
+
time.sleep(0.05)
|
|
166
|
+
if pid(service):
|
|
167
|
+
raise RuntimeError("service did not stop")
|
|
168
|
+
if command in {"start", "restart"} and not pid(service):
|
|
169
|
+
execute(service, "ExecStartPre")
|
|
170
|
+
execute(service, "ExecStart")
|
|
171
|
+
if not pid(service):
|
|
172
|
+
raise RuntimeError("service command returned success without a running server")
|
|
173
|
+
if command == "reload":
|
|
174
|
+
if not pid(service):
|
|
175
|
+
raise RuntimeError("cannot reload an inactive service")
|
|
176
|
+
execute(service, "ExecReload")
|
|
177
|
+
return 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
if __name__ == "__main__":
|
|
181
|
+
try:
|
|
182
|
+
raise SystemExit(main())
|
|
183
|
+
except Exception as exc:
|
|
184
|
+
print("pg-maker systemctl: " + str(exc), file=sys.stderr)
|
|
185
|
+
raise SystemExit(1) from exc
|
pg_maker/yamlio.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""JSON-compatible YAML with explicit duplicate-key, alias and tag rejection."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import yaml
|
|
7
|
+
from yaml.events import AliasEvent
|
|
8
|
+
|
|
9
|
+
from pg_maker.documents import validate_document
|
|
10
|
+
from pg_maker.errors import ValidationError
|
|
11
|
+
from pg_maker.storage import canonical_json
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StrictLoader(yaml.SafeLoader):
|
|
15
|
+
def compose_node(self, parent, index):
|
|
16
|
+
if self.check_event(AliasEvent):
|
|
17
|
+
raise ValidationError("YAML aliases are not supported")
|
|
18
|
+
return super().compose_node(parent, index)
|
|
19
|
+
|
|
20
|
+
def construct_mapping(self, node, deep=False):
|
|
21
|
+
result = {}
|
|
22
|
+
for key_node, value_node in node.value:
|
|
23
|
+
key = self.construct_object(key_node, deep=deep)
|
|
24
|
+
if not isinstance(key, str):
|
|
25
|
+
raise ValidationError("YAML mapping keys must be strings")
|
|
26
|
+
if key in result:
|
|
27
|
+
raise ValidationError(
|
|
28
|
+
"duplicate YAML key", details={"key": key, "line": key_node.start_mark.line + 1}
|
|
29
|
+
)
|
|
30
|
+
result[key] = self.construct_object(value_node, deep=deep)
|
|
31
|
+
return result
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def load_yaml(path: Path, schema_name: str | None = None) -> dict[str, Any]:
|
|
35
|
+
try:
|
|
36
|
+
value = yaml.load(path.read_text(encoding="utf-8"), Loader=StrictLoader)
|
|
37
|
+
except OSError as exc:
|
|
38
|
+
raise ValidationError("cannot read YAML document", details={"path": str(path)}) from exc
|
|
39
|
+
except yaml.YAMLError as exc:
|
|
40
|
+
# PyYAML's exception text can echo secrets from the offending source line.
|
|
41
|
+
mark = getattr(exc, "problem_mark", None)
|
|
42
|
+
raise ValidationError(
|
|
43
|
+
"invalid or unsupported YAML",
|
|
44
|
+
details={
|
|
45
|
+
"path": str(path),
|
|
46
|
+
"line": mark.line + 1 if mark else None,
|
|
47
|
+
},
|
|
48
|
+
) from exc
|
|
49
|
+
if not isinstance(value, dict):
|
|
50
|
+
raise ValidationError("YAML document must be a mapping")
|
|
51
|
+
canonical_json(value)
|
|
52
|
+
if schema_name:
|
|
53
|
+
validate_document(value, schema_name)
|
|
54
|
+
return value
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.0 — 2026-10-18
|
|
4
|
+
|
|
5
|
+
Initial alpha of the PostgreSQL distribution controller:
|
|
6
|
+
|
|
7
|
+
- Strict YAML configuration, pinned sources/environments, immutable locks and plans.
|
|
8
|
+
- Isolated Docker builds, durable operations, events, cancellation, recovery and caches.
|
|
9
|
+
- PostgreSQL 14–19 catalog; initial Debian 13 / x86_64 / DEB platform.
|
|
10
|
+
- Default pg_stat_statements, pg_stat_kcache and pg_wait_sampling recipes with manuals.
|
|
11
|
+
- Documentation-only and combined builds using DocBook/Sphinx, local assets, HTML/man/PDF
|
|
12
|
+
and explicit translation bundles.
|
|
13
|
+
- Server/client/libpq/docs packaging, manifests, offline installation checks and exports.
|
|
14
|
+
- Owned Git development sessions, patch import/refresh/pop/push/rebase, conflict recovery,
|
|
15
|
+
selected component rebuilds, debug commands and iteration retention.
|
|
16
|
+
- Optional pg_play CLI/API/MCP integration and runtime image handoff.
|
|
17
|
+
- English project documentation, distribution-content checks and a tag-triggered PyPI
|
|
18
|
+
Trusted Publishing workflow.
|
|
19
|
+
|
|
20
|
+
Database suites are opt-in; quick/development builds remain unverified. RPM, additional
|
|
21
|
+
platforms and PostgreSQL distribution repository publication remain future work.
|
|
22
|
+
See the [roadmap](docs/roadmap.md) and dated validation reports for precise boundaries.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
Use Linux and Python 3.10 or newer. Install the checkout in a virtual environment:
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
python3 -m venv .venv
|
|
7
|
+
.venv/bin/python -m pip install -e '.[dev]'
|
|
8
|
+
.venv/bin/ruff check .
|
|
9
|
+
.venv/bin/ruff format --check .
|
|
10
|
+
.venv/bin/python tools/generate_planning_schemas.py
|
|
11
|
+
.venv/bin/python tools/check_release.py
|
|
12
|
+
.venv/bin/pytest -q tests/unit tests/contracts
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Review generated schema changes with the matching interface contract. Keep every
|
|
16
|
+
project Markdown file in English, including examples and fixture descriptions.
|
|
17
|
+
PostgreSQL manuals produced by the utility can use configured languages. Preserve
|
|
18
|
+
upstream copyright/license notices in fixtures and generated artifacts.
|
|
19
|
+
|
|
20
|
+
Public changes need an updated MD contract and, where applicable, JSON Schema.
|
|
21
|
+
Use MakerService for CLI/API behavior, typed argv for commands, the shared logger,
|
|
22
|
+
durable state/events and owned-resource cleanup. Prefer small concrete adapters;
|
|
23
|
+
add protocols only at real replacement boundaries. See [architecture](docs/architecture.md)
|
|
24
|
+
and [interfaces](docs/interfaces/README.md).
|
|
25
|
+
|
|
26
|
+
## Tests and storage
|
|
27
|
+
|
|
28
|
+
Unit/contract tests do not require PostgreSQL builds. Small Docker tests are opt-in;
|
|
29
|
+
variables select already available images. A tiny development pipeline fixture needs
|
|
30
|
+
an image with Make and the current non-root build user:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
PG_MAKER_DEVELOPMENT_IMAGE=sha256:FULL_IMAGE_ID .venv/bin/pytest -q tests/integration/test_development_pipeline.py
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
This checks failed extension builds, Git fixes, core/component reuse, debug execution
|
|
37
|
+
and retention without starting a database. Other fixtures use PG_MAKER_DOCKER_IMAGE,
|
|
38
|
+
PG_MAKER_BUILD_IMAGE, PG_MAKER_PREPARATION_IMAGE, PG_MAKER_PACKAGE_ENVIRONMENTS and
|
|
39
|
+
PG_MAKER_DOC_ENVIRONMENTS. CI prepares its own fixture environments through
|
|
40
|
+
`tools/ci_pipeline.py --fixtures-only`.
|
|
41
|
+
|
|
42
|
+
Database suites remain off by default. The expensive PG_MAKER_BUILD_LOCK integration
|
|
43
|
+
and manual --run-db-tests path are separate checks, not routine prerequisites for
|
|
44
|
+
editing the Python utility. Never treat a successful quick build as database qualification.
|
|
45
|
+
Check free disk before work; use [explicit cleanup](docs/development-storage.md) and
|
|
46
|
+
retain useful evidence. Do not put runs, inputs, environments, credentials or build
|
|
47
|
+
artifacts into Git.
|
|
48
|
+
|
|
49
|
+
## Distribution checks
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
.venv/bin/python -m build
|
|
53
|
+
.venv/bin/python -m twine check --strict dist/*
|
|
54
|
+
.venv/bin/python tools/check_release.py --dist dist
|
|
55
|
+
.venv/bin/python tools/smoke_wheel.py dist/*.whl
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Use a distribution directory containing exactly one version. Release checks compare
|
|
59
|
+
all Markdown and runtime package files byte-for-byte between source, wheel and sdist,
|
|
60
|
+
and ensure test fixtures/tooling are present in sdist. Wheel smoke creates and removes
|
|
61
|
+
a temporary isolated environment, installs dependencies, checks entry points and
|
|
62
|
+
initializes/validates a project outside checkout. It does not contact Docker.
|
|
63
|
+
Publication is described in [releasing](docs/releasing.md).
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Oleg Gurov
|
|
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.
|