docker-devkit 0.2.1.dev37485576216__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 +52 -52
- docker_devkit/matrix.py +31 -14
- {docker_devkit-0.2.1.dev37485576216.dist-info → docker_devkit-0.3.0.dev113.dist-info}/METADATA +2 -2
- {docker_devkit-0.2.1.dev37485576216.dist-info → docker_devkit-0.3.0.dev113.dist-info}/RECORD +8 -8
- {docker_devkit-0.2.1.dev37485576216.dist-info → docker_devkit-0.3.0.dev113.dist-info}/WHEEL +0 -0
- {docker_devkit-0.2.1.dev37485576216.dist-info → docker_devkit-0.3.0.dev113.dist-info}/entry_points.txt +0 -0
- {docker_devkit-0.2.1.dev37485576216.dist-info → docker_devkit-0.3.0.dev113.dist-info}/licenses/LICENSE +0 -0
- {docker_devkit-0.2.1.dev37485576216.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,11 +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 build_artifact_registry.builds import push_build
|
|
16
|
+
from build_artifact_registry.setup_oras import install_oras
|
|
15
17
|
from .detect_gpu import GPU_TYPES, Gpu, detect_gpu
|
|
16
18
|
from .documents import BakeDocument, digest_pinned, parse_bake
|
|
17
19
|
from pydantic_settings import BaseSettings
|
|
@@ -31,8 +33,6 @@ settings = Settings.model_validate({})
|
|
|
31
33
|
|
|
32
34
|
METADATA_PATH = Path("metadata.json")
|
|
33
35
|
|
|
34
|
-
Mode = Literal["local", "ci"]
|
|
35
|
-
|
|
36
36
|
DIGEST_PROJECT = "images-digests"
|
|
37
37
|
DIGEST_PLATFORM = "all"
|
|
38
38
|
DIGEST_FILE_NAME = "images-digests.json"
|
|
@@ -49,55 +49,43 @@ app = typer.Typer(add_completion=False, pretty_exceptions_show_locals=False)
|
|
|
49
49
|
|
|
50
50
|
@app.command()
|
|
51
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",
|
|
52
60
|
upgrade: bool = typer.Option(False, "--upgrade", "-u", help="Re-resolve and rewrite base digests."),
|
|
53
61
|
lock_only: bool = typer.Option(False, "--lock-only", help="Update lock file without building images."),
|
|
54
|
-
mode: Annotated[
|
|
55
|
-
Mode, typer.Option("--mode", help="local: --load images; ci: --push images + registry caches.")
|
|
56
|
-
] = "local",
|
|
57
|
-
gpu: Annotated[Gpu, typer.Option("--gpu", help="auto|cuda|rocm|none")] = "auto",
|
|
58
62
|
gpu_only: bool = typer.Option(
|
|
59
63
|
False, "--gpu-only", help="Build only the services suffixed for this gpu (requires a concrete --gpu)."
|
|
60
64
|
),
|
|
61
65
|
no_cache: bool = typer.Option(False, "--no-cache", help="Force rebuild by disabling cache usage."),
|
|
62
|
-
targets_opt: Annotated[
|
|
63
|
-
list[str] | None,
|
|
64
|
-
typer.Option("--targets", "-t", help="Build only these services (from the image manifest)."),
|
|
65
|
-
] = None,
|
|
66
|
-
builds_registry: Annotated[
|
|
67
|
-
str | None,
|
|
68
|
-
typer.Option(
|
|
69
|
-
"--builds-registry", help="OCI registry path to push the digest manifest to (CI build artifacts)."
|
|
70
|
-
),
|
|
71
|
-
] = None,
|
|
72
|
-
run_number: Annotated[
|
|
73
|
-
int, typer.Option("--run-number", help="CI run number baked into the digest manifest's builds-shelf tag.")
|
|
74
|
-
] = 0,
|
|
75
66
|
) -> None:
|
|
67
|
+
targets = [service for value in targets_opt or [] for service in value.split()] or None
|
|
76
68
|
run_build(
|
|
69
|
+
push=push,
|
|
70
|
+
targets_opt=targets,
|
|
71
|
+
gpu=gpu,
|
|
77
72
|
upgrade=upgrade,
|
|
78
73
|
lock_only=lock_only,
|
|
79
|
-
mode=mode,
|
|
80
|
-
gpu=gpu,
|
|
81
74
|
gpu_only=gpu_only,
|
|
82
75
|
no_cache=no_cache,
|
|
83
|
-
targets_opt=targets_opt,
|
|
84
|
-
builds_registry=builds_registry,
|
|
85
|
-
run_number=run_number,
|
|
86
76
|
)
|
|
87
77
|
|
|
88
78
|
|
|
89
79
|
def run_build(
|
|
90
80
|
*,
|
|
81
|
+
push: bool = False,
|
|
82
|
+
targets_opt: list[str] | None = None,
|
|
83
|
+
gpu: Gpu = "auto",
|
|
91
84
|
upgrade: bool = False,
|
|
92
85
|
lock_only: bool = False,
|
|
93
|
-
mode: Mode = "local",
|
|
94
|
-
gpu: Gpu = "auto",
|
|
95
86
|
gpu_only: bool = False,
|
|
96
87
|
no_cache: bool = False,
|
|
97
|
-
targets_opt: list[str] | None = None,
|
|
98
88
|
env_overlay: dict[str, str] | None = None,
|
|
99
|
-
builds_registry: str | None = None,
|
|
100
|
-
run_number: int = 0,
|
|
101
89
|
) -> None:
|
|
102
90
|
root = Path.cwd()
|
|
103
91
|
manifest = require_manifest(root)
|
|
@@ -112,18 +100,17 @@ def run_build(
|
|
|
112
100
|
# Load any existing lock
|
|
113
101
|
lock_data = parse_env_file(lock_file) if lock_file.exists() else {}
|
|
114
102
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
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'.")
|
|
118
105
|
|
|
119
|
-
if
|
|
120
|
-
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.")
|
|
121
108
|
|
|
122
109
|
if gpu_only and gpu not in GPU_TYPES:
|
|
123
110
|
raise typer.BadParameter("--gpu-only requires a concrete gpu (cuda or rocm), not 'auto' or 'none'.")
|
|
124
111
|
|
|
125
112
|
# For local builds, ensure Docker GC limits are high enough that GPU builds don't cause cache evictions
|
|
126
|
-
if
|
|
113
|
+
if not push and not lock_only and not targets_opt:
|
|
127
114
|
_check_gc_limits(min_gb=60)
|
|
128
115
|
|
|
129
116
|
# Resolve gpu
|
|
@@ -175,8 +162,8 @@ def run_build(
|
|
|
175
162
|
else:
|
|
176
163
|
targets = compute_default_targets(bake, gpu, gpu_only)
|
|
177
164
|
|
|
178
|
-
# Configure registry caches
|
|
179
|
-
if
|
|
165
|
+
# Configure registry caches for push builds
|
|
166
|
+
if push:
|
|
180
167
|
for target in targets:
|
|
181
168
|
target_cache = f"{bake.registry_cache}:{target}"
|
|
182
169
|
command_arguments.append(
|
|
@@ -186,9 +173,9 @@ def run_build(
|
|
|
186
173
|
|
|
187
174
|
# `docker buildx --load` only writes a single arch to the host image store,
|
|
188
175
|
# so bake targets declared multi-platform get overridden to host arch in
|
|
189
|
-
# local builds.
|
|
176
|
+
# local builds. --push produces the full manifest list.
|
|
190
177
|
# Override key is `platform` (singular) even though the bake field is plural.
|
|
191
|
-
if
|
|
178
|
+
if not push:
|
|
192
179
|
machine = platform.machine().lower()
|
|
193
180
|
if machine in ("x86_64", "amd64"):
|
|
194
181
|
host_platform = "linux/amd64"
|
|
@@ -200,8 +187,8 @@ def run_build(
|
|
|
200
187
|
if len(bake.services[target].build.platforms) > 1:
|
|
201
188
|
command_arguments.append(f"--set {target}.platform={host_platform}")
|
|
202
189
|
|
|
203
|
-
# Load or push images
|
|
204
|
-
command_arguments.append("--
|
|
190
|
+
# Load or push images
|
|
191
|
+
command_arguments.append("--push" if push else "--load")
|
|
205
192
|
|
|
206
193
|
# Handle no-cache option
|
|
207
194
|
if no_cache:
|
|
@@ -229,28 +216,39 @@ def run_build(
|
|
|
229
216
|
if not set(targets) <= baked_images.keys():
|
|
230
217
|
raise RuntimeError("Bake output does not cover the requested targets")
|
|
231
218
|
|
|
232
|
-
|
|
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
|
+
|
|
233
228
|
|
|
229
|
+
def leg_key(targets: list[str]) -> str:
|
|
230
|
+
return hashlib.sha256(",".join(sorted(targets)).encode("utf-8")).hexdigest()[:8]
|
|
234
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.
|
|
235
236
|
def push_image_digests(
|
|
236
|
-
|
|
237
|
-
|
|
237
|
+
shelf: str,
|
|
238
|
+
sha: str,
|
|
238
239
|
baked_images: dict[str, Any],
|
|
239
240
|
targets: list[str],
|
|
240
241
|
) -> None:
|
|
241
|
-
|
|
242
|
-
return
|
|
243
|
-
if run_number <= 0:
|
|
244
|
-
raise typer.BadParameter("--builds-registry requires a positive --run-number")
|
|
242
|
+
install_oras()
|
|
245
243
|
manifest = distill_digest_manifest(baked_images, targets)
|
|
246
244
|
with TemporaryDirectory(prefix="digest-manifest-") as staging:
|
|
247
245
|
manifest_path = Path(staging) / DIGEST_FILE_NAME
|
|
248
246
|
manifest_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8")
|
|
249
247
|
push_build(
|
|
250
|
-
|
|
248
|
+
shelf,
|
|
251
249
|
DIGEST_PROJECT,
|
|
252
250
|
DIGEST_PLATFORM,
|
|
253
|
-
f"
|
|
251
|
+
f"sha-{sha}-{leg_key(targets)}",
|
|
254
252
|
Path(staging),
|
|
255
253
|
[DIGEST_FILE_NAME],
|
|
256
254
|
)
|
|
@@ -271,6 +269,8 @@ def distill_digest_manifest(baked_images: dict[str, Any], targets: list[str]) ->
|
|
|
271
269
|
if separator:
|
|
272
270
|
image_ref = repository
|
|
273
271
|
tags.append(tag)
|
|
272
|
+
if not tags:
|
|
273
|
+
raise ValueError(f"target '{target}' produced no tagged image references from '{image_name}'")
|
|
274
274
|
manifest[target] = {"ref": image_ref, "digest": digest, "tags": tags}
|
|
275
275
|
return manifest
|
|
276
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.dev37485576216.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.dev37485576216.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
|