fdai-deployment-cli 0.1.2__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.
- fdai_deployment_cli/__about__.py +3 -0
- fdai_deployment_cli/__init__.py +5 -0
- fdai_deployment_cli/__main__.py +5 -0
- fdai_deployment_cli/acr_publish.py +585 -0
- fdai_deployment_cli/aks_catalog_review.py +137 -0
- fdai_deployment_cli/aks_historical_reconciliation.py +732 -0
- fdai_deployment_cli/aks_job_environment.py +59 -0
- fdai_deployment_cli/aks_job_execution.py +794 -0
- fdai_deployment_cli/aks_kubernetes_defaults.py +66 -0
- fdai_deployment_cli/aks_preflight.py +422 -0
- fdai_deployment_cli/aks_readiness.py +220 -0
- fdai_deployment_cli/aks_service_update.py +180 -0
- fdai_deployment_cli/aks_workload_jobs.py +281 -0
- fdai_deployment_cli/application_state_adoption.py +347 -0
- fdai_deployment_cli/azure_actor.py +61 -0
- fdai_deployment_cli/azure_naming.py +252 -0
- fdai_deployment_cli/azure_retail_prices.py +173 -0
- fdai_deployment_cli/bootstrap_approval.py +99 -0
- fdai_deployment_cli/bootstrap_channel.py +321 -0
- fdai_deployment_cli/bootstrap_executor.py +311 -0
- fdai_deployment_cli/bootstrap_plan.py +237 -0
- fdai_deployment_cli/bootstrap_reconcile.py +402 -0
- fdai_deployment_cli/bootstrap_support.py +138 -0
- fdai_deployment_cli/bootstrap_worker.py +448 -0
- fdai_deployment_cli/bundle.py +352 -0
- fdai_deployment_cli/capacity.py +143 -0
- fdai_deployment_cli/catalog_review_profile.py +306 -0
- fdai_deployment_cli/cli.py +798 -0
- fdai_deployment_cli/cli_help.py +214 -0
- fdai_deployment_cli/cli_parser.py +791 -0
- fdai_deployment_cli/cli_plan.py +450 -0
- fdai_deployment_cli/compiler.py +99 -0
- fdai_deployment_cli/console_artifact.py +218 -0
- fdai_deployment_cli/console_config.py +214 -0
- fdai_deployment_cli/console_update.py +832 -0
- fdai_deployment_cli/contracts.py +307 -0
- fdai_deployment_cli/control_package.py +140 -0
- fdai_deployment_cli/deadline_transport.py +70 -0
- fdai_deployment_cli/deployment_cost.py +131 -0
- fdai_deployment_cli/deployment_deadline.py +28 -0
- fdai_deployment_cli/deployment_kit.py +790 -0
- fdai_deployment_cli/deployment_kit_cache.py +191 -0
- fdai_deployment_cli/deployment_progress.py +648 -0
- fdai_deployment_cli/development_authority_cli.py +56 -0
- fdai_deployment_cli/development_authority_profile.py +319 -0
- fdai_deployment_cli/doctor.py +136 -0
- fdai_deployment_cli/entitlement_preflight.py +68 -0
- fdai_deployment_cli/entitlement_transport.py +49 -0
- fdai_deployment_cli/entra_bootstrap.py +579 -0
- fdai_deployment_cli/entra_control_profile.py +214 -0
- fdai_deployment_cli/entra_graph.py +349 -0
- fdai_deployment_cli/entra_profiles.py +215 -0
- fdai_deployment_cli/entra_source.py +244 -0
- fdai_deployment_cli/execution_bundle.py +478 -0
- fdai_deployment_cli/execution_copies.py +63 -0
- fdai_deployment_cli/foundation_adoption.py +438 -0
- fdai_deployment_cli/foundation_adoption_evidence.py +245 -0
- fdai_deployment_cli/foundation_adoption_host.py +157 -0
- fdai_deployment_cli/foundation_adoption_transport.py +51 -0
- fdai_deployment_cli/foundation_failure.py +72 -0
- fdai_deployment_cli/foundation_image.py +168 -0
- fdai_deployment_cli/foundation_input.py +235 -0
- fdai_deployment_cli/foundation_output.py +154 -0
- fdai_deployment_cli/foundation_plan.py +381 -0
- fdai_deployment_cli/foundation_process.py +64 -0
- fdai_deployment_cli/foundation_progress.py +231 -0
- fdai_deployment_cli/github_actions.py +171 -0
- fdai_deployment_cli/github_apply_artifact.py +259 -0
- fdai_deployment_cli/github_artifact_io.py +48 -0
- fdai_deployment_cli/github_dispatch.py +248 -0
- fdai_deployment_cli/github_plan_artifact.py +181 -0
- fdai_deployment_cli/github_provider_artifact.py +170 -0
- fdai_deployment_cli/github_transport.py +46 -0
- fdai_deployment_cli/github_workflow_values.py +312 -0
- fdai_deployment_cli/hub_image_deploy.py +354 -0
- fdai_deployment_cli/hub_image_stage.py +162 -0
- fdai_deployment_cli/hub_image_trust.py +281 -0
- fdai_deployment_cli/hub_ownership.py +161 -0
- fdai_deployment_cli/hub_workload_handoff.py +264 -0
- fdai_deployment_cli/hub_workload_handoff_inputs.py +227 -0
- fdai_deployment_cli/identity_profile.py +227 -0
- fdai_deployment_cli/image_targets.py +197 -0
- fdai_deployment_cli/installation_scope.py +280 -0
- fdai_deployment_cli/license.py +311 -0
- fdai_deployment_cli/license_issue.py +239 -0
- fdai_deployment_cli/lifecycle_authorization.py +252 -0
- fdai_deployment_cli/lifecycle_commands.py +173 -0
- fdai_deployment_cli/lifecycle_configuration.py +722 -0
- fdai_deployment_cli/lifecycle_configuration_artifact.py +45 -0
- fdai_deployment_cli/lifecycle_configuration_cli.py +324 -0
- fdai_deployment_cli/lifecycle_configuration_document.py +74 -0
- fdai_deployment_cli/lifecycle_configuration_entities.py +146 -0
- fdai_deployment_cli/lifecycle_configuration_package.py +492 -0
- fdai_deployment_cli/lifecycle_configuration_publication.py +277 -0
- fdai_deployment_cli/lifecycle_configuration_publication_cli.py +191 -0
- fdai_deployment_cli/lifecycle_configuration_sealing.py +127 -0
- fdai_deployment_cli/lifecycle_enrollment.py +45 -0
- fdai_deployment_cli/lifecycle_infrastructure_agent.py +174 -0
- fdai_deployment_cli/lifecycle_installation_binding.py +65 -0
- fdai_deployment_cli/lifecycle_native.py +247 -0
- fdai_deployment_cli/lifecycle_native_reconciliation.py +568 -0
- fdai_deployment_cli/lifecycle_native_transition.py +339 -0
- fdai_deployment_cli/lifecycle_ownership_exclusion.py +78 -0
- fdai_deployment_cli/lifecycle_plan.py +800 -0
- fdai_deployment_cli/lifecycle_promotion.py +185 -0
- fdai_deployment_cli/lifecycle_release.py +308 -0
- fdai_deployment_cli/lifecycle_self_upgrade.py +202 -0
- fdai_deployment_cli/lifecycle_upgrade_bundle.py +176 -0
- fdai_deployment_cli/notification_recipients_input.py +222 -0
- fdai_deployment_cli/observer_artifact.py +106 -0
- fdai_deployment_cli/oci_archive.py +353 -0
- fdai_deployment_cli/offline_kit.py +797 -0
- fdai_deployment_cli/offline_prepare.py +135 -0
- fdai_deployment_cli/operational_evidence_verifier_input.py +575 -0
- fdai_deployment_cli/plan_input.py +101 -0
- fdai_deployment_cli/private_output.py +192 -0
- fdai_deployment_cli/profile.py +70 -0
- fdai_deployment_cli/progress.py +183 -0
- fdai_deployment_cli/readiness.py +286 -0
- fdai_deployment_cli/run_command_receiver.py +418 -0
- fdai_deployment_cli/runtime_build.py +386 -0
- fdai_deployment_cli/runtime_profile.py +287 -0
- fdai_deployment_cli/runtime_release.py +745 -0
- fdai_deployment_cli/runtime_stage.py +82 -0
- fdai_deployment_cli/runtime_support_installation.py +215 -0
- fdai_deployment_cli/simulation.py +80 -0
- fdai_deployment_cli/source_application_inputs.py +397 -0
- fdai_deployment_cli/source_azure.py +586 -0
- fdai_deployment_cli/source_change_cli.py +107 -0
- fdai_deployment_cli/source_change_parser.py +75 -0
- fdai_deployment_cli/source_change_plan.py +181 -0
- fdai_deployment_cli/source_changed_update.py +196 -0
- fdai_deployment_cli/source_deploy.py +170 -0
- fdai_deployment_cli/source_foundation.py +174 -0
- fdai_deployment_cli/source_host_tools.py +163 -0
- fdai_deployment_cli/source_image_stage.py +400 -0
- fdai_deployment_cli/source_input.py +173 -0
- fdai_deployment_cli/source_projection.py +164 -0
- fdai_deployment_cli/source_receiver.py +76 -0
- fdai_deployment_cli/source_recovery.py +264 -0
- fdai_deployment_cli/source_runtime.py +81 -0
- fdai_deployment_cli/source_runtime_support.py +368 -0
- fdai_deployment_cli/source_service_update.py +726 -0
- fdai_deployment_cli/source_service_update_cli.py +47 -0
- fdai_deployment_cli/source_snapshot.py +178 -0
- fdai_deployment_cli/source_teardown.py +574 -0
- fdai_deployment_cli/source_terraform.py +85 -0
- fdai_deployment_cli/source_transport.py +399 -0
- fdai_deployment_cli/standalone_aks_inventory.py +558 -0
- fdai_deployment_cli/standalone_aks_job_execution.py +769 -0
- fdai_deployment_cli/standalone_aks_nodepool_guard.py +141 -0
- fdai_deployment_cli/standalone_application.py +977 -0
- fdai_deployment_cli/standalone_application_completion.py +151 -0
- fdai_deployment_cli/standalone_application_input_recovery.py +107 -0
- fdai_deployment_cli/standalone_application_recovery.py +116 -0
- fdai_deployment_cli/standalone_approval_input.py +24 -0
- fdai_deployment_cli/standalone_catalog_checkpoint.py +122 -0
- fdai_deployment_cli/standalone_catalog_review.py +251 -0
- fdai_deployment_cli/standalone_checkpoint_failure.py +127 -0
- fdai_deployment_cli/standalone_console_publish.py +174 -0
- fdai_deployment_cli/standalone_database_principals.py +41 -0
- fdai_deployment_cli/standalone_deploy.py +638 -0
- fdai_deployment_cli/standalone_development_authority.py +166 -0
- fdai_deployment_cli/standalone_foundation_adoption.py +87 -0
- fdai_deployment_cli/standalone_foundation_transition.py +767 -0
- fdai_deployment_cli/standalone_host.py +4608 -0
- fdai_deployment_cli/standalone_host_rebind.py +226 -0
- fdai_deployment_cli/standalone_host_residual.py +104 -0
- fdai_deployment_cli/standalone_host_state.py +145 -0
- fdai_deployment_cli/standalone_host_values.py +563 -0
- fdai_deployment_cli/standalone_kubernetes_tools.py +100 -0
- fdai_deployment_cli/standalone_license_installation.py +177 -0
- fdai_deployment_cli/standalone_management_egress.py +83 -0
- fdai_deployment_cli/standalone_migration_evidence.py +38 -0
- fdai_deployment_cli/standalone_operational_evidence.py +356 -0
- fdai_deployment_cli/standalone_planned_outputs.py +147 -0
- fdai_deployment_cli/standalone_private_access_probe.py +100 -0
- fdai_deployment_cli/standalone_product_profile.py +110 -0
- fdai_deployment_cli/standalone_remote_prepare.py +442 -0
- fdai_deployment_cli/standalone_residual_apply.py +540 -0
- fdai_deployment_cli/standalone_residual_successor.py +431 -0
- fdai_deployment_cli/standalone_review.py +266 -0
- fdai_deployment_cli/standalone_service_update_finalization.py +116 -0
- fdai_deployment_cli/standalone_service_update_recovery.py +277 -0
- fdai_deployment_cli/standalone_stage_targets.py +325 -0
- fdai_deployment_cli/standalone_status.py +67 -0
- fdai_deployment_cli/standalone_terraform_environment.py +237 -0
- fdai_deployment_cli/standalone_transfer_cleanup.py +237 -0
- fdai_deployment_cli/standalone_trial_activation.py +193 -0
- fdai_deployment_cli/state.py +434 -0
- fdai_deployment_cli/state_handoff.py +253 -0
- fdai_deployment_cli/status_projection.py +97 -0
- fdai_deployment_cli/support_install.py +257 -0
- fdai_deployment_cli/target.py +46 -0
- fdai_deployment_cli/trust/capabilities.json +62 -0
- fdai_deployment_cli/trust/deployment-bundle-root.pub +3 -0
- fdai_deployment_cli/trust/deployment-release-root.pub +3 -0
- fdai_deployment_cli/trust/upstream-signing-key.pub +3 -0
- fdai_deployment_cli/trust_roots.py +50 -0
- fdai_deployment_cli-0.1.2.dist-info/METADATA +112 -0
- fdai_deployment_cli-0.1.2.dist-info/RECORD +203 -0
- fdai_deployment_cli-0.1.2.dist-info/WHEEL +4 -0
- fdai_deployment_cli-0.1.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"""Install source-deployment runtime support from the committed workspace lock.
|
|
2
|
+
|
|
3
|
+
A signed kit carries a prebuilt runtime wheelhouse. A source deployment has no such artifact, so
|
|
4
|
+
the workstation builds the snapshot's pure-Python workspace wheels, exports the committed
|
|
5
|
+
``uv.lock`` closure as hashed requirements, and transfers both as one digest-bound archive. The
|
|
6
|
+
managed host installs the third-party closure by hash and then the workspace wheels by hash
|
|
7
|
+
without any index or build step, so no unpinned build backend ever runs on the host.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import hashlib
|
|
13
|
+
import io
|
|
14
|
+
import json
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import shutil
|
|
18
|
+
import subprocess
|
|
19
|
+
import sys
|
|
20
|
+
import tarfile
|
|
21
|
+
import tempfile
|
|
22
|
+
import tomllib
|
|
23
|
+
from pathlib import Path, PurePosixPath
|
|
24
|
+
|
|
25
|
+
from fdai_deployment_cli.contracts import canonical_digest
|
|
26
|
+
from fdai_deployment_cli.private_output import (
|
|
27
|
+
read_private_bytes,
|
|
28
|
+
write_private_bytes,
|
|
29
|
+
write_private_output,
|
|
30
|
+
)
|
|
31
|
+
from fdai_deployment_cli.standalone_host_state import private_json as _private_json
|
|
32
|
+
|
|
33
|
+
# Kept equal to the runtime and support package sets of
|
|
34
|
+
# scripts/deployment/release/stage-runtime-wheelhouse.py by a consistency test.
|
|
35
|
+
WORKSPACE_PACKAGES = {
|
|
36
|
+
"fdai-service-contracts": "packages/service-contracts",
|
|
37
|
+
"fdai-core-control-plane": "services/core-control-plane",
|
|
38
|
+
"fdai-operator-service": "services/operator-service",
|
|
39
|
+
"fdai-document-ingestion-api": "services/document-ingestion-api",
|
|
40
|
+
"fdai-document-processing-worker": "services/document-processing-worker",
|
|
41
|
+
"fdai-isolated-executor-service": "services/isolated-executor",
|
|
42
|
+
"fdai-github-app-auth": "packages/github-app-auth",
|
|
43
|
+
"fdai-runtime-diagnostics": "packages/runtime-diagnostics",
|
|
44
|
+
}
|
|
45
|
+
INPUT_NAME = "source-runtime-support.tar"
|
|
46
|
+
_RECEIPT = "runtime-support-installation.json"
|
|
47
|
+
_SCHEMA = "fdai.source-runtime-support-installation.v2"
|
|
48
|
+
_DIGEST = re.compile(r"[0-9a-f]{64}")
|
|
49
|
+
_MAX_REQUIREMENTS = 4 * 1024 * 1024
|
|
50
|
+
_MAX_ARCHIVE = 256 * 1024 * 1024
|
|
51
|
+
_WHEEL = re.compile(r"(?P<name>[A-Za-z0-9_.]+)-(?P<version>[^-]+)-py3-none-any\.whl")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def prepare_runtime_support_input(source_tree: Path, prepared_root: Path) -> str:
|
|
55
|
+
"""Write the digest-bound runtime support archive and return its SHA-256 digest."""
|
|
56
|
+
uv = os.environ.get("UV") or shutil.which("uv")
|
|
57
|
+
if not uv:
|
|
58
|
+
raise ValueError("source deployment requires uv to prepare runtime support")
|
|
59
|
+
versions = _workspace_versions(source_tree)
|
|
60
|
+
with tempfile.TemporaryDirectory(prefix=".runtime-support-", dir=prepared_root) as scratch:
|
|
61
|
+
root = Path(scratch)
|
|
62
|
+
requirements = _export_requirements(uv, source_tree, root / "uv-cache")
|
|
63
|
+
# Build from a private copy so the verified snapshot is never written.
|
|
64
|
+
tree = root / "tree"
|
|
65
|
+
shutil.copytree(source_tree, tree, symlinks=True)
|
|
66
|
+
wheels = root / "wheels"
|
|
67
|
+
wheels.mkdir(mode=0o700)
|
|
68
|
+
members: dict[str, bytes] = {"requirements.txt": requirements}
|
|
69
|
+
pins = []
|
|
70
|
+
for name in WORKSPACE_PACKAGES:
|
|
71
|
+
_run(
|
|
72
|
+
(uv, "build", "--wheel", "--package", name, "--out-dir", str(wheels), "--quiet"),
|
|
73
|
+
tree,
|
|
74
|
+
600,
|
|
75
|
+
"workspace wheel build",
|
|
76
|
+
)
|
|
77
|
+
for name, version in versions.items():
|
|
78
|
+
wheel = _single_wheel(wheels, name, version)
|
|
79
|
+
content = wheel.read_bytes()
|
|
80
|
+
members[f"wheels/{wheel.name}"] = content
|
|
81
|
+
pins.append(f"{name}=={version} --hash=sha256:{hashlib.sha256(content).hexdigest()}")
|
|
82
|
+
members["workspace.txt"] = ("\n".join(pins) + "\n").encode()
|
|
83
|
+
archive = _deterministic_tar(members)
|
|
84
|
+
destination = prepared_root / INPUT_NAME
|
|
85
|
+
destination.unlink(missing_ok=True)
|
|
86
|
+
write_private_bytes(destination, archive)
|
|
87
|
+
return hashlib.sha256(archive).hexdigest()
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _export_requirements(uv: str, source_tree: Path, cache: Path) -> bytes:
|
|
91
|
+
packages = tuple(argument for name in WORKSPACE_PACKAGES for argument in ("--package", name))
|
|
92
|
+
try:
|
|
93
|
+
result = subprocess.run(
|
|
94
|
+
(
|
|
95
|
+
uv,
|
|
96
|
+
"export",
|
|
97
|
+
"--directory",
|
|
98
|
+
str(source_tree),
|
|
99
|
+
"--locked",
|
|
100
|
+
"--offline",
|
|
101
|
+
*packages,
|
|
102
|
+
"--no-dev",
|
|
103
|
+
"--no-default-groups",
|
|
104
|
+
"--no-emit-workspace",
|
|
105
|
+
"--format",
|
|
106
|
+
"requirements-txt",
|
|
107
|
+
"--no-header",
|
|
108
|
+
"--no-annotate",
|
|
109
|
+
"--quiet",
|
|
110
|
+
),
|
|
111
|
+
check=False,
|
|
112
|
+
capture_output=True,
|
|
113
|
+
timeout=120,
|
|
114
|
+
env={**os.environ, "UV_CACHE_DIR": str(cache), "UV_NO_PROGRESS": "1"},
|
|
115
|
+
)
|
|
116
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
117
|
+
raise ValueError("locked runtime closure export failed") from exc
|
|
118
|
+
content = result.stdout
|
|
119
|
+
if result.returncode != 0 or not content or len(content) > _MAX_REQUIREMENTS:
|
|
120
|
+
raise ValueError("locked runtime closure export failed")
|
|
121
|
+
_validate_requirements(content)
|
|
122
|
+
return content
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _single_wheel(directory: Path, name: str, version: str) -> Path:
|
|
126
|
+
normalized = name.replace("-", "_")
|
|
127
|
+
matches = [
|
|
128
|
+
path
|
|
129
|
+
for path in directory.iterdir()
|
|
130
|
+
if (match := _WHEEL.fullmatch(path.name)) is not None
|
|
131
|
+
and match["name"] == normalized
|
|
132
|
+
and match["version"] == version
|
|
133
|
+
]
|
|
134
|
+
if len(matches) != 1 or not matches[0].is_file() or matches[0].is_symlink():
|
|
135
|
+
raise ValueError(f"workspace package {name} did not build one pure-Python wheel")
|
|
136
|
+
return matches[0]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _deterministic_tar(members: dict[str, bytes]) -> bytes:
|
|
140
|
+
buffer = io.BytesIO()
|
|
141
|
+
with tarfile.open(fileobj=buffer, mode="w", format=tarfile.PAX_FORMAT) as archive:
|
|
142
|
+
for name in sorted(members):
|
|
143
|
+
info = tarfile.TarInfo(name)
|
|
144
|
+
info.size = len(members[name])
|
|
145
|
+
info.mode = 0o600
|
|
146
|
+
archive.addfile(info, io.BytesIO(members[name]))
|
|
147
|
+
return buffer.getvalue()
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _read_input(archive: Path, digest: str) -> dict[str, bytes]:
|
|
151
|
+
content = read_private_bytes(archive, max_bytes=_MAX_ARCHIVE)
|
|
152
|
+
if hashlib.sha256(content).hexdigest() != digest:
|
|
153
|
+
raise ValueError("source runtime support input differs from the transferred digest")
|
|
154
|
+
members: dict[str, bytes] = {}
|
|
155
|
+
try:
|
|
156
|
+
with tarfile.open(fileobj=io.BytesIO(content), mode="r:") as bundle:
|
|
157
|
+
for member in bundle.getmembers():
|
|
158
|
+
path = PurePosixPath(member.name)
|
|
159
|
+
allowed = path.as_posix() in {"requirements.txt", "workspace.txt"} or (
|
|
160
|
+
len(path.parts) == 2
|
|
161
|
+
and path.parts[0] == "wheels"
|
|
162
|
+
and _WHEEL.fullmatch(path.parts[1]) is not None
|
|
163
|
+
)
|
|
164
|
+
if not member.isreg() or not allowed or member.name in members:
|
|
165
|
+
raise ValueError("source runtime support input member is invalid")
|
|
166
|
+
extracted = bundle.extractfile(member)
|
|
167
|
+
if extracted is None:
|
|
168
|
+
raise ValueError("source runtime support input member is invalid")
|
|
169
|
+
members[member.name] = extracted.read()
|
|
170
|
+
except tarfile.TarError as exc:
|
|
171
|
+
raise ValueError("source runtime support input is invalid") from exc
|
|
172
|
+
if "requirements.txt" not in members or "workspace.txt" not in members:
|
|
173
|
+
raise ValueError("source runtime support input is incomplete")
|
|
174
|
+
return members
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _validate_pins(content: bytes, members: dict[str, bytes], expected: dict[str, str]) -> None:
|
|
178
|
+
lines = content.decode("utf-8").splitlines()
|
|
179
|
+
pins = {}
|
|
180
|
+
for line in lines:
|
|
181
|
+
match = re.fullmatch(r"([a-z0-9-]+)==(\S+) --hash=sha256:([0-9a-f]{64})", line)
|
|
182
|
+
if match is None or match[1] in pins:
|
|
183
|
+
raise ValueError("source runtime workspace pins are invalid")
|
|
184
|
+
pins[match[1]] = (match[2], match[3])
|
|
185
|
+
wheel_digests = {
|
|
186
|
+
hashlib.sha256(value).hexdigest()
|
|
187
|
+
for name, value in members.items()
|
|
188
|
+
if name != "workspace.txt"
|
|
189
|
+
}
|
|
190
|
+
if {name: version for name, (version, _) in pins.items()} != expected or any(
|
|
191
|
+
digest not in wheel_digests for _, digest in pins.values()
|
|
192
|
+
):
|
|
193
|
+
raise ValueError("source runtime workspace pins differ from the snapshot")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def install_source_runtime_support(
|
|
197
|
+
work_dir: Path,
|
|
198
|
+
*,
|
|
199
|
+
source_root: Path,
|
|
200
|
+
archive: Path,
|
|
201
|
+
archive_digest: str,
|
|
202
|
+
snapshot_digest: str,
|
|
203
|
+
) -> None:
|
|
204
|
+
"""Install the hashed lock closure, then the hashed workspace wheels, and verify them."""
|
|
205
|
+
if _DIGEST.fullmatch(archive_digest) is None or _DIGEST.fullmatch(snapshot_digest) is None:
|
|
206
|
+
raise ValueError("source runtime support binding is invalid")
|
|
207
|
+
members = _read_input(archive, archive_digest)
|
|
208
|
+
_validate_requirements(members["requirements.txt"])
|
|
209
|
+
expected = _workspace_versions(source_root)
|
|
210
|
+
_validate_pins(members["workspace.txt"], members, expected)
|
|
211
|
+
environment = work_dir / "runtime-venv"
|
|
212
|
+
receipt_path = work_dir / _RECEIPT
|
|
213
|
+
if _retained_matches(
|
|
214
|
+
environment,
|
|
215
|
+
receipt_path,
|
|
216
|
+
snapshot_digest=snapshot_digest,
|
|
217
|
+
input_digest=archive_digest,
|
|
218
|
+
expected=expected,
|
|
219
|
+
work_dir=work_dir,
|
|
220
|
+
):
|
|
221
|
+
return
|
|
222
|
+
# The environment is disposable tooling; any partial or stale one is rebuilt.
|
|
223
|
+
receipt_path.unlink(missing_ok=True)
|
|
224
|
+
for stale in (environment, work_dir / "runtime-support-input"):
|
|
225
|
+
if stale.is_symlink() or stale.is_file():
|
|
226
|
+
stale.unlink()
|
|
227
|
+
elif stale.exists():
|
|
228
|
+
shutil.rmtree(stale)
|
|
229
|
+
unpacked = work_dir / "runtime-support-input"
|
|
230
|
+
(unpacked / "wheels").mkdir(mode=0o700, parents=True)
|
|
231
|
+
unpacked.chmod(0o700)
|
|
232
|
+
for name, content in members.items():
|
|
233
|
+
write_private_bytes(unpacked / name, content)
|
|
234
|
+
_run((sys.executable, "-m", "venv", str(environment)), work_dir, 120, "environment creation")
|
|
235
|
+
pip = str(environment / "bin/pip")
|
|
236
|
+
base = (pip, "install", "--no-cache-dir", "--disable-pip-version-check", "--require-hashes")
|
|
237
|
+
_run(
|
|
238
|
+
(*base, "--only-binary", ":all:", "--requirement", str(unpacked / "requirements.txt")),
|
|
239
|
+
work_dir,
|
|
240
|
+
1200,
|
|
241
|
+
"locked dependency installation",
|
|
242
|
+
)
|
|
243
|
+
_run(
|
|
244
|
+
(
|
|
245
|
+
*base,
|
|
246
|
+
"--no-index",
|
|
247
|
+
"--no-deps",
|
|
248
|
+
"--only-binary",
|
|
249
|
+
":all:",
|
|
250
|
+
"--find-links",
|
|
251
|
+
str(unpacked / "wheels"),
|
|
252
|
+
"--requirement",
|
|
253
|
+
str(unpacked / "workspace.txt"),
|
|
254
|
+
),
|
|
255
|
+
work_dir,
|
|
256
|
+
600,
|
|
257
|
+
"workspace package installation",
|
|
258
|
+
)
|
|
259
|
+
write_private_output(
|
|
260
|
+
receipt_path,
|
|
261
|
+
json.dumps(
|
|
262
|
+
{
|
|
263
|
+
"schema_version": _SCHEMA,
|
|
264
|
+
"snapshot_digest": snapshot_digest,
|
|
265
|
+
"input_digest": archive_digest,
|
|
266
|
+
"package_readback_digest": _readback(
|
|
267
|
+
environment, expected=expected, work_dir=work_dir
|
|
268
|
+
),
|
|
269
|
+
"dependencies_verified": True,
|
|
270
|
+
},
|
|
271
|
+
sort_keys=True,
|
|
272
|
+
separators=(",", ":"),
|
|
273
|
+
)
|
|
274
|
+
+ "\n",
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _retained_matches(
|
|
279
|
+
environment: Path,
|
|
280
|
+
receipt_path: Path,
|
|
281
|
+
*,
|
|
282
|
+
snapshot_digest: str,
|
|
283
|
+
input_digest: str,
|
|
284
|
+
expected: dict[str, str],
|
|
285
|
+
work_dir: Path,
|
|
286
|
+
) -> bool:
|
|
287
|
+
if not receipt_path.is_file() or environment.is_symlink() or not environment.is_dir():
|
|
288
|
+
return False
|
|
289
|
+
try:
|
|
290
|
+
receipt = _private_json(receipt_path, "source runtime support receipt")
|
|
291
|
+
return (
|
|
292
|
+
receipt.get("schema_version") == _SCHEMA
|
|
293
|
+
and receipt.get("snapshot_digest") == snapshot_digest
|
|
294
|
+
and receipt.get("input_digest") == input_digest
|
|
295
|
+
and receipt.get("dependencies_verified") is True
|
|
296
|
+
and receipt.get("package_readback_digest")
|
|
297
|
+
== _readback(environment, expected=expected, work_dir=work_dir)
|
|
298
|
+
)
|
|
299
|
+
except (OSError, ValueError):
|
|
300
|
+
return False
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def _validate_requirements(content: bytes) -> None:
|
|
304
|
+
try:
|
|
305
|
+
text = content.decode("utf-8")
|
|
306
|
+
except UnicodeDecodeError as exc:
|
|
307
|
+
raise ValueError("source runtime requirements are not UTF-8") from exc
|
|
308
|
+
entries = [line for line in text.splitlines() if line and not line.startswith((" ", "#"))]
|
|
309
|
+
if not entries or any(
|
|
310
|
+
line.startswith(("-", ".", "/")) or "://" in line or "==" not in line for line in entries
|
|
311
|
+
):
|
|
312
|
+
raise ValueError("source runtime requirements must pin index packages only")
|
|
313
|
+
if "--hash=sha256:" not in text:
|
|
314
|
+
raise ValueError("source runtime requirements must carry hashes")
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _workspace_versions(source_root: Path) -> dict[str, str]:
|
|
318
|
+
versions = {}
|
|
319
|
+
for name, path in WORKSPACE_PACKAGES.items():
|
|
320
|
+
try:
|
|
321
|
+
project = tomllib.loads((source_root / path / "pyproject.toml").read_text("utf-8"))[
|
|
322
|
+
"project"
|
|
323
|
+
]
|
|
324
|
+
except (OSError, KeyError, tomllib.TOMLDecodeError) as exc:
|
|
325
|
+
raise ValueError("source runtime workspace package is unavailable") from exc
|
|
326
|
+
if project.get("name") != name or not isinstance(project.get("version"), str):
|
|
327
|
+
raise ValueError("source runtime workspace package metadata is invalid")
|
|
328
|
+
versions[name] = project["version"]
|
|
329
|
+
return versions
|
|
330
|
+
|
|
331
|
+
|
|
332
|
+
def _readback(environment: Path, *, expected: dict[str, str], work_dir: Path) -> str:
|
|
333
|
+
pip = str(environment / "bin/pip")
|
|
334
|
+
_run((pip, "check"), work_dir, 120, "dependency check")
|
|
335
|
+
try:
|
|
336
|
+
result = subprocess.run(
|
|
337
|
+
(pip, "list", "--format=json", "--disable-pip-version-check"),
|
|
338
|
+
cwd=work_dir,
|
|
339
|
+
check=False,
|
|
340
|
+
capture_output=True,
|
|
341
|
+
timeout=120,
|
|
342
|
+
)
|
|
343
|
+
entries = json.loads(result.stdout) if result.returncode == 0 else None
|
|
344
|
+
except (OSError, subprocess.SubprocessError, json.JSONDecodeError):
|
|
345
|
+
entries = None
|
|
346
|
+
if not isinstance(entries, list):
|
|
347
|
+
raise ValueError("source runtime support package readback failed")
|
|
348
|
+
observed: dict[str, str] = {}
|
|
349
|
+
for entry in entries:
|
|
350
|
+
name = entry.get("name") if isinstance(entry, dict) else None
|
|
351
|
+
version = entry.get("version") if isinstance(entry, dict) else None
|
|
352
|
+
if not isinstance(name, str) or not isinstance(version, str):
|
|
353
|
+
raise ValueError("source runtime support package readback is invalid")
|
|
354
|
+
observed[re.sub(r"[-_.]+", "-", name.casefold())] = version
|
|
355
|
+
if any(observed.get(name) != version for name, version in expected.items()):
|
|
356
|
+
raise ValueError("source runtime workspace packages differ from the snapshot")
|
|
357
|
+
return canonical_digest(observed)
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def _run(command: tuple[str, ...], work_dir: Path, timeout: int, step: str) -> None:
|
|
361
|
+
try:
|
|
362
|
+
result = subprocess.run(
|
|
363
|
+
command, cwd=work_dir, check=False, capture_output=True, timeout=timeout
|
|
364
|
+
)
|
|
365
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
366
|
+
raise ValueError(f"source runtime support {step} failed") from exc
|
|
367
|
+
if result.returncode != 0:
|
|
368
|
+
raise ValueError(f"source runtime support {step} failed")
|