docker-devkit 0.2.1.dev37490360141__py3-none-any.whl → 0.3.0.dev113__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.
- docker_devkit/build_docker.py +51 -53
- docker_devkit/matrix.py +31 -14
- {docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/METADATA +2 -2
- {docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/RECORD +8 -8
- {docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/WHEEL +0 -0
- {docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/entry_points.txt +0 -0
- {docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/licenses/LICENSE +0 -0
- {docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/licenses/NOTICE +0 -0
docker_devkit/build_docker.py
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import hashlib
|
|
3
4
|
import json
|
|
4
5
|
import os
|
|
5
6
|
import platform
|
|
@@ -7,12 +8,12 @@ import re
|
|
|
7
8
|
from pathlib import Path
|
|
8
9
|
from subprocess import CalledProcessError
|
|
9
10
|
from tempfile import TemporaryDirectory
|
|
10
|
-
from typing import Annotated, Any,
|
|
11
|
+
from typing import Annotated, Any, TypedDict
|
|
11
12
|
|
|
12
13
|
import typer
|
|
13
14
|
from bashrun.bash import bash, bash_output
|
|
14
|
-
from
|
|
15
|
-
from
|
|
15
|
+
from build_artifact_registry.builds import push_build
|
|
16
|
+
from build_artifact_registry.setup_oras import install_oras
|
|
16
17
|
from .detect_gpu import GPU_TYPES, Gpu, detect_gpu
|
|
17
18
|
from .documents import BakeDocument, digest_pinned, parse_bake
|
|
18
19
|
from pydantic_settings import BaseSettings
|
|
@@ -32,8 +33,6 @@ settings = Settings.model_validate({})
|
|
|
32
33
|
|
|
33
34
|
METADATA_PATH = Path("metadata.json")
|
|
34
35
|
|
|
35
|
-
Mode = Literal["local", "ci"]
|
|
36
|
-
|
|
37
36
|
DIGEST_PROJECT = "images-digests"
|
|
38
37
|
DIGEST_PLATFORM = "all"
|
|
39
38
|
DIGEST_FILE_NAME = "images-digests.json"
|
|
@@ -50,55 +49,43 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
50
49
|
|
|
51
50
|
@app.command()
|
|
52
51
|
def build(
|
|
52
|
+
push: Annotated[
|
|
53
|
+
bool, typer.Option("--push", help="Push images (+ registry caches) and shelve the digest manifest.")
|
|
54
|
+
] = False,
|
|
55
|
+
targets_opt: Annotated[
|
|
56
|
+
list[str] | None,
|
|
57
|
+
typer.Option("--targets", "-t", help="Build only these services (from the image manifest)."),
|
|
58
|
+
] = None,
|
|
59
|
+
gpu: Annotated[Gpu, typer.Option("--gpu", help="auto|cuda|rocm|none")] = "auto",
|
|
53
60
|
upgrade: bool = typer.Option(False, "--upgrade", "-u", help="Re-resolve and rewrite base digests."),
|
|
54
61
|
lock_only: bool = typer.Option(False, "--lock-only", help="Update lock file without building images."),
|
|
55
|
-
mode: Annotated[
|
|
56
|
-
Mode, typer.Option("--mode", help="local: --load images; ci: --push images + registry caches.")
|
|
57
|
-
] = "local",
|
|
58
|
-
gpu: Annotated[Gpu, typer.Option("--gpu", help="auto|cuda|rocm|none")] = "auto",
|
|
59
62
|
gpu_only: bool = typer.Option(
|
|
60
63
|
False, "--gpu-only", help="Build only the services suffixed for this gpu (requires a concrete --gpu)."
|
|
61
64
|
),
|
|
62
65
|
no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild by disabling cache usage."),
|
|
63
|
-
targets_opt: Annotated[
|
|
64
|
-
list[str] | None,
|
|
65
|
-
typer.Option("--targets", "-t", help="Build only these services (from the image manifest)."),
|
|
66
|
-
] = None,
|
|
67
|
-
builds_registry: Annotated[
|
|
68
|
-
str | None,
|
|
69
|
-
typer.Option(
|
|
70
|
-
"--builds-registry", help="OCI registry path to push the digest manifest to (CI build artifacts)."
|
|
71
|
-
),
|
|
72
|
-
] = None,
|
|
73
|
-
run_number: Annotated[
|
|
74
|
-
int, typer.Option("--run-number", help="CI run number baked into the digest manifest's builds-shelf tag.")
|
|
75
|
-
] = 0,
|
|
76
66
|
) -> None:
|
|
67
|
+
targets = [service for value in targets_opt or [] for service in value.split()] or None
|
|
77
68
|
run_build(
|
|
69
|
+
push=push,
|
|
70
|
+
targets_opt=targets,
|
|
71
|
+
gpu=gpu,
|
|
78
72
|
upgrade=upgrade,
|
|
79
73
|
lock_only=lock_only,
|
|
80
|
-
mode=mode,
|
|
81
|
-
gpu=gpu,
|
|
82
74
|
gpu_only=gpu_only,
|
|
83
75
|
no_cache=no_cache,
|
|
84
|
-
targets_opt=targets_opt,
|
|
85
|
-
builds_registry=builds_registry,
|
|
86
|
-
run_number=run_number,
|
|
87
76
|
)
|
|
88
77
|
|
|
89
78
|
|
|
90
79
|
def run_build(
|
|
91
80
|
*,
|
|
81
|
+
push: bool = False,
|
|
82
|
+
targets_opt: list[str] | None = None,
|
|
83
|
+
gpu: Gpu = "auto",
|
|
92
84
|
upgrade: bool = False,
|
|
93
85
|
lock_only: bool = False,
|
|
94
|
-
mode: Mode = "local",
|
|
95
|
-
gpu: Gpu = "auto",
|
|
96
86
|
gpu_only: bool = False,
|
|
97
87
|
no_cache: bool = False,
|
|
98
|
-
targets_opt: list[str] | None = None,
|
|
99
88
|
env_overlay: dict[str, str] | None = None,
|
|
100
|
-
builds_registry: str | None = None,
|
|
101
|
-
run_number: int = 0,
|
|
102
89
|
) -> None:
|
|
103
90
|
root = Path.cwd()
|
|
104
91
|
manifest = require_manifest(root)
|
|
@@ -113,18 +100,17 @@ def run_build(
|
|
|
113
100
|
# Load any existing lock
|
|
114
101
|
lock_data = parse_env_file(lock_file) if lock_file.exists() else {}
|
|
115
102
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
raise typer.BadParameter("In CI mode, --gpu cannot be 'auto'; specify 'cuda' or 'rocm'.")
|
|
103
|
+
if push and gpu == "auto":
|
|
104
|
+
raise typer.BadParameter("With --push, --gpu cannot be 'auto'; specify 'cuda' or 'rocm'.")
|
|
119
105
|
|
|
120
|
-
if
|
|
121
|
-
raise typer.BadParameter("
|
|
106
|
+
if push and bake.registry_cache is None:
|
|
107
|
+
raise typer.BadParameter("With --push, the bake file must declare x-registry-cache.")
|
|
122
108
|
|
|
123
109
|
if gpu_only and gpu not in GPU_TYPES:
|
|
124
110
|
raise typer.BadParameter("--gpu-only requires a concrete gpu (cuda or rocm), not 'auto' or 'none'.")
|
|
125
111
|
|
|
126
112
|
# For local builds, ensure Docker GC limits are high enough that GPU builds don't cause cache evictions
|
|
127
|
-
if
|
|
113
|
+
if not push and not lock_only and not targets_opt:
|
|
128
114
|
_check_gc_limits(min_gb=60)
|
|
129
115
|
|
|
130
116
|
# Resolve gpu
|
|
@@ -176,8 +162,8 @@ def run_build(
|
|
|
176
162
|
else:
|
|
177
163
|
targets = compute_default_targets(bake, gpu, gpu_only)
|
|
178
164
|
|
|
179
|
-
# Configure registry caches
|
|
180
|
-
if
|
|
165
|
+
# Configure registry caches for push builds
|
|
166
|
+
if push:
|
|
181
167
|
for target in targets:
|
|
182
168
|
target_cache = f"{bake.registry_cache}:{target}"
|
|
183
169
|
command_arguments.append(
|
|
@@ -187,9 +173,9 @@ def run_build(
|
|
|
187
173
|
|
|
188
174
|
# `docker buildx --load` only writes a single arch to the host image store,
|
|
189
175
|
# so bake targets declared multi-platform get overridden to host arch in
|
|
190
|
-
# local builds.
|
|
176
|
+
# local builds. --push produces the full manifest list.
|
|
191
177
|
# Override key is `platform` (singular) even though the bake field is plural.
|
|
192
|
-
if
|
|
178
|
+
if not push:
|
|
193
179
|
machine = platform.machine().lower()
|
|
194
180
|
if machine in ("x86_64", "amd64"):
|
|
195
181
|
host_platform = "linux/amd64"
|
|
@@ -201,8 +187,8 @@ def run_build(
|
|
|
201
187
|
if len(bake.services[target].build.platforms) > 1:
|
|
202
188
|
command_arguments.append(f"--set {target}.platform={host_platform}")
|
|
203
189
|
|
|
204
|
-
# Load or push images
|
|
205
|
-
command_arguments.append("--
|
|
190
|
+
# Load or push images
|
|
191
|
+
command_arguments.append("--push" if push else "--load")
|
|
206
192
|
|
|
207
193
|
# Handle no-cache option
|
|
208
194
|
if no_cache:
|
|
@@ -230,29 +216,39 @@ def run_build(
|
|
|
230
216
|
if not set(targets) <= baked_images.keys():
|
|
231
217
|
raise RuntimeError("Bake output does not cover the requested targets")
|
|
232
218
|
|
|
233
|
-
|
|
219
|
+
# Push the digest manifest to the repo's builds shelf (registry supplied explicitly, SHA from the checkout)
|
|
220
|
+
if push:
|
|
221
|
+
registry = os.environ.get("REGISTRY", "")
|
|
222
|
+
if not registry:
|
|
223
|
+
raise SystemExit("REGISTRY not set — --push shelves the digest manifest under it")
|
|
224
|
+
shelf = f"{registry}/builds"
|
|
225
|
+
sha = bash_output("git rev-parse HEAD").strip()
|
|
226
|
+
push_image_digests(shelf, sha, baked_images, targets)
|
|
227
|
+
|
|
234
228
|
|
|
229
|
+
def leg_key(targets: list[str]) -> str:
|
|
230
|
+
return hashlib.sha256(",".join(sorted(targets)).encode("utf-8")).hexdigest()[:8]
|
|
235
231
|
|
|
232
|
+
|
|
233
|
+
# Each matrix leg pushes its own manifest shard under sha-{sha}-{key}: legs run in
|
|
234
|
+
# parallel, so a shared tag would be last-writer-wins. The pull side enumerates a
|
|
235
|
+
# SHA's shards and merges — consumption happens only after every leg finished.
|
|
236
236
|
def push_image_digests(
|
|
237
|
-
|
|
238
|
-
|
|
237
|
+
shelf: str,
|
|
238
|
+
sha: str,
|
|
239
239
|
baked_images: dict[str, Any],
|
|
240
240
|
targets: list[str],
|
|
241
241
|
) -> None:
|
|
242
|
-
if builds_registry is None:
|
|
243
|
-
return
|
|
244
|
-
if run_number <= 0:
|
|
245
|
-
raise typer.BadParameter("--builds-registry requires a positive --run-number")
|
|
246
242
|
install_oras()
|
|
247
243
|
manifest = distill_digest_manifest(baked_images, targets)
|
|
248
244
|
with TemporaryDirectory(prefix="digest-manifest-") as staging:
|
|
249
245
|
manifest_path = Path(staging) / DIGEST_FILE_NAME
|
|
250
246
|
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
251
247
|
push_build(
|
|
252
|
-
|
|
248
|
+
shelf,
|
|
253
249
|
DIGEST_PROJECT,
|
|
254
250
|
DIGEST_PLATFORM,
|
|
255
|
-
f"
|
|
251
|
+
f"sha-{sha}-{leg_key(targets)}",
|
|
256
252
|
Path(staging),
|
|
257
253
|
[DIGEST_FILE_NAME],
|
|
258
254
|
)
|
|
@@ -273,6 +269,8 @@ def distill_digest_manifest(baked_images: dict[str, Any], targets: list[str]) ->
|
|
|
273
269
|
if separator:
|
|
274
270
|
image_ref = repository
|
|
275
271
|
tags.append(tag)
|
|
272
|
+
if not tags:
|
|
273
|
+
raise ValueError(f"target '{target}' produced no tagged image references from '{image_name}'")
|
|
276
274
|
manifest[target] = {"ref": image_ref, "digest": digest, "tags": tags}
|
|
277
275
|
return manifest
|
|
278
276
|
|
docker_devkit/matrix.py
CHANGED
|
@@ -18,32 +18,49 @@ PLAIN_VARIANT = "common"
|
|
|
18
18
|
@app.command()
|
|
19
19
|
def matrix_main() -> None:
|
|
20
20
|
bake = parse_bake(require_manifest(Path.cwd()))
|
|
21
|
-
print(
|
|
21
|
+
print(json.dumps(compute_matrix_entries(bake)))
|
|
22
22
|
|
|
23
23
|
|
|
24
|
-
# One entry per variant: targets are the dependency-closure groups
|
|
25
|
-
# explicitly-buildable services, coalesced into a single leg — the
|
|
26
|
-
# gpu-resolved build invocation would produce, with dependents
|
|
27
|
-
# their additional_contexts `target:` references pull in.
|
|
24
|
+
# One entry per variant and cohort cell: targets are the dependency-closure groups
|
|
25
|
+
# of that cell's explicitly-buildable services, coalesced into a single leg — the
|
|
26
|
+
# shape a single gpu-resolved build invocation would produce, with dependents
|
|
27
|
+
# grouped with the base their additional_contexts `target:` references pull in.
|
|
28
|
+
# The cross-compile cohort gets its own legs: per-arch builds are the QEMU-slow
|
|
29
|
+
# ones, so they parallelize away from native builds instead of serializing them
|
|
30
|
+
# into one invocation.
|
|
28
31
|
def compute_matrix_entries(bake: BakeDocument) -> list[dict[str, str]]:
|
|
29
|
-
|
|
32
|
+
cohort = cross_compile_cohort(bake)
|
|
33
|
+
entries_by_cell: dict[tuple[str, bool], list[str]] = {}
|
|
30
34
|
for group in dependency_groups(bake):
|
|
31
|
-
explicit = sorted(
|
|
32
|
-
service
|
|
33
|
-
for service in group
|
|
34
|
-
if bake.services[service].build.tags and service not in set(bake.cross_compile_targets)
|
|
35
|
-
)
|
|
35
|
+
explicit = sorted(service for service in group if bake.services[service].build.tags)
|
|
36
36
|
if not explicit:
|
|
37
37
|
continue
|
|
38
38
|
variant = group_variant(bake, explicit)
|
|
39
|
-
|
|
39
|
+
memberships = {service in cohort for service in explicit}
|
|
40
|
+
if len(memberships) > 1:
|
|
41
|
+
raise RuntimeError(
|
|
42
|
+
f"dependency group spans cross-compile cohort membership ({', '.join(explicit)}) — "
|
|
43
|
+
"additional_contexts must stay within one cohort"
|
|
44
|
+
)
|
|
45
|
+
entries_by_cell.setdefault((variant, memberships.pop()), []).extend(explicit)
|
|
40
46
|
|
|
41
47
|
return [
|
|
42
|
-
{"targets": " ".join(sorted(
|
|
43
|
-
for variant in sorted(
|
|
48
|
+
{"targets": " ".join(sorted(targets)), "variant": variant}
|
|
49
|
+
for (variant, _), targets in sorted(entries_by_cell.items())
|
|
44
50
|
]
|
|
45
51
|
|
|
46
52
|
|
|
53
|
+
# The cross-compile cohort: every service declaring `platforms:` (per-arch
|
|
54
|
+
# treatment is a manifest fact, not an operator judgment) plus the hand-listed
|
|
55
|
+
# x-cross-compile-targets members.
|
|
56
|
+
def cross_compile_cohort(bake: BakeDocument) -> set[str]:
|
|
57
|
+
unknown = set(bake.cross_compile_targets) - set(bake.services)
|
|
58
|
+
if unknown:
|
|
59
|
+
raise RuntimeError(f"x-cross-compile-targets names unknown services: {', '.join(sorted(unknown))}")
|
|
60
|
+
pinned = {service for service, config in bake.services.items() if config.build.platforms}
|
|
61
|
+
return pinned | set(bake.cross_compile_targets)
|
|
62
|
+
|
|
63
|
+
|
|
47
64
|
def dependency_groups(bake: BakeDocument) -> list[list[str]]:
|
|
48
65
|
parent = {service: service for service in bake.services}
|
|
49
66
|
|
{docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/METADATA
RENAMED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: docker-devkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0.dev113
|
|
4
4
|
Summary: Docker-stack lifecycle commands (up/down/build) driven by a pyproject-declared compose assembly
|
|
5
5
|
License-File: LICENSE
|
|
6
6
|
License-File: NOTICE
|
|
7
7
|
Requires-Python: >=3.13
|
|
8
8
|
Requires-Dist: bashrun>=0.1.0
|
|
9
|
-
Requires-Dist:
|
|
9
|
+
Requires-Dist: build-artifact-registry>=0.2.0.dev0
|
|
10
10
|
Requires-Dist: packaging>=24.1
|
|
11
11
|
Requires-Dist: pathspec>=1.0.4
|
|
12
12
|
Requires-Dist: pydantic-settings>=2.9.1
|
{docker_devkit-0.2.1.dev37490360141.dist-info → docker_devkit-0.3.0.dev113.dist-info}/RECORD
RENAMED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
docker_devkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
2
|
-
docker_devkit/build_docker.py,sha256=
|
|
2
|
+
docker_devkit/build_docker.py,sha256=DtvTUTGHdQAkpBVelGObWyYD5Ldc05iPWXkNpCMQt-E,12927
|
|
3
3
|
docker_devkit/config.py,sha256=UMAS6yF1E9PIbsRs2TVig9XwSNCvQe5w9eTLUHOPKq0,3112
|
|
4
4
|
docker_devkit/context_sha.py,sha256=lhCxUcYM98xCdDum3hsFRFw5WYrjvZUSMbOu_7ZmQjE,3041
|
|
5
5
|
docker_devkit/detect_gpu.py,sha256=C4e3vixvSSJS6xQiMVWp2IdWSAgMr551KrBTuIrkTk4,456
|
|
@@ -7,15 +7,15 @@ docker_devkit/documents.py,sha256=c6jqbBNUKyJOltex-8u536_niWhEL7ScOab_MwZmM9k,21
|
|
|
7
7
|
docker_devkit/down.py,sha256=9OMXPZQ3SIdaSELc46hzkqNpLSwOJ1athCD8Bmlz2BI,2189
|
|
8
8
|
docker_devkit/image_refs.py,sha256=6pHYPWh4lL6qMqHtQBHBbeWijVsT0PhsMS6CMDho5nY,5294
|
|
9
9
|
docker_devkit/lifecycle.py,sha256=FuwaCSRK7qVx_qj-HmIFqMm8Ak4cpAQEqqXRPDNCLmo,2259
|
|
10
|
-
docker_devkit/matrix.py,sha256=
|
|
10
|
+
docker_devkit/matrix.py,sha256=6NyGL4Ol849dYlcMRzAL2yjFoN6sGkGwVaaxcDm3bG4,4530
|
|
11
11
|
docker_devkit/mirror.py,sha256=tissqnoqMwRA6s0ykwXZOEgy6F1d3AAebPKGEkSpJRg,3224
|
|
12
12
|
docker_devkit/modes.py,sha256=Lwhblzu5Kn7qhfy_QMqE28XuflSIJtCRBYNX7Iq8Qfo,1510
|
|
13
13
|
docker_devkit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
14
14
|
docker_devkit/score_generate.py,sha256=psbQm-MWf9A5YP0ov0rLPUiuVAQbUWn6yfUmB-CXurU,9434
|
|
15
15
|
docker_devkit/up.py,sha256=32A2_6mxAF00XILecxYTzCQw0QvSjevBWCnayF5X1Xs,4284
|
|
16
|
-
docker_devkit-0.
|
|
17
|
-
docker_devkit-0.
|
|
18
|
-
docker_devkit-0.
|
|
19
|
-
docker_devkit-0.
|
|
20
|
-
docker_devkit-0.
|
|
21
|
-
docker_devkit-0.
|
|
16
|
+
docker_devkit-0.3.0.dev113.dist-info/METADATA,sha256=0NT3OLGPZKW3hL5cvP-avLSORfSbo57cbDvwrZV2aOs,540
|
|
17
|
+
docker_devkit-0.3.0.dev113.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
18
|
+
docker_devkit-0.3.0.dev113.dist-info/entry_points.txt,sha256=6cAHqlMvuXI00uTJLf73MTqWnLd1-m5sDgJrSyHWv4U,244
|
|
19
|
+
docker_devkit-0.3.0.dev113.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
|
|
20
|
+
docker_devkit-0.3.0.dev113.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
|
|
21
|
+
docker_devkit-0.3.0.dev113.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|