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.
@@ -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, Literal, TypedDict
11
+ from typing import Annotated, Any, TypedDict
11
12
 
12
13
  import typer
13
14
  from bashrun.bash import bash, bash_output
14
- from ci_devkit.builds import push_build
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
- # TOOD: Create separate commands for ci and local modes so typer can do this validation instead of us
116
- if mode == "ci" and gpu == "auto":
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 mode == "ci" and bake.registry_cache is None:
120
- raise typer.BadParameter("In CI mode, the bake file must declare x-registry-cache.")
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 mode == "local" and not lock_only and not targets_opt:
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 in CI mode
179
- if mode == "ci":
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. CI uses `--push` and produces the full manifest list.
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 mode == "local":
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 based on mode
204
- command_arguments.append("--load" if mode == "local" else "--push")
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
- push_image_digests(builds_registry, run_number, baked_images, targets)
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
- builds_registry: str | None,
237
- run_number: int,
237
+ shelf: str,
238
+ sha: str,
238
239
  baked_images: dict[str, Any],
239
240
  targets: list[str],
240
241
  ) -> None:
241
- if builds_registry is None:
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
- builds_registry,
248
+ shelf,
251
249
  DIGEST_PROJECT,
252
250
  DIGEST_PLATFORM,
253
- f"run-{run_number}",
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(f"matrix={json.dumps({'include': compute_matrix_entries(bake)})}")
21
+ print(json.dumps(compute_matrix_entries(bake)))
22
22
 
23
23
 
24
- # One entry per variant: targets are the dependency-closure groups of that variant's
25
- # explicitly-buildable services, coalesced into a single leg — the shape a single
26
- # gpu-resolved build invocation would produce, with dependents grouped with the base
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
- entries_by_variant: dict[str, list[str]] = {}
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
- entries_by_variant.setdefault(variant, []).extend(explicit)
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(entries_by_variant[variant])), "variant": variant}
43
- for variant in sorted(entries_by_variant)
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
 
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: docker-devkit
3
- Version: 0.2.1.dev37485576216
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: ci-devkit>=0.1.15
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
@@ -1,5 +1,5 @@
1
1
  docker_devkit/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
- docker_devkit/build_docker.py,sha256=wyIvfh5v-WarKa7XCanWxhMWQUPiBBPrQJxzALtnTQ0,12748
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=__qZhGdCOdYqjeexDYHsm55ehMyyD1Nw67m2T-DTvFg,3489
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.2.1.dev37485576216.dist-info/METADATA,sha256=Czxw-O5SIfhkg2ST2JPcC6a-7pfKbj3n682oywKLtJ4,530
17
- docker_devkit-0.2.1.dev37485576216.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
18
- docker_devkit-0.2.1.dev37485576216.dist-info/entry_points.txt,sha256=6cAHqlMvuXI00uTJLf73MTqWnLd1-m5sDgJrSyHWv4U,244
19
- docker_devkit-0.2.1.dev37485576216.dist-info/licenses/LICENSE,sha256=RFhQPdSOiMTguUX7JSoIuTxA7HVzCbj_p8WU36HjUQQ,10947
20
- docker_devkit-0.2.1.dev37485576216.dist-info/licenses/NOTICE,sha256=jvglZza3GCdN7v4_TLuXUqX9C_uABlwAKUkcIeKwk_8,26
21
- docker_devkit-0.2.1.dev37485576216.dist-info/RECORD,,
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,,