#!/usr/bin/env python3 """Build all microservice images via docker buildx bake.""" import json import os import subprocess import sys from glob import glob from pathlib import Path ROOT_DIR = Path(__file__).resolve().parent.parent.parent IMAGE_PREFIX = os.environ.get("IMAGE_PREFIX", "juwan") IMAGE_TAG = sys.argv[1] if len(sys.argv) > 1 else "dev" BAKE_BATCH_SIZE = int(os.environ.get("BAKE_BATCH_SIZE", "8")) DOCKERFILE_TPL = """\ # syntax=docker/dockerfile:1.7 FROM golang:1.26-alpine AS builder RUN apk add --no-cache tzdata WORKDIR /build ARG GOPROXY="https://goproxy.cn,direct" COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod \\ --mount=type=cache,target=/root/.cache/go-build \\ go build -ldflags="-s -w" -o /app/main ./{service_dir} FROM alpine:latest COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai ENV TZ=Asia/Shanghai WORKDIR /app COPY --from=builder /app/main /app/main COPY {service_dir}/etc /app/etc CMD ["./main", "-f", "etc/{config_name}"] """ DOCKERFILE_NO_CONFIG_TPL = """\ # syntax=docker/dockerfile:1.7 FROM golang:1.26-alpine AS builder RUN apk add --no-cache tzdata WORKDIR /build ARG GOPROXY="https://goproxy.cn,direct" COPY go.mod go.sum ./ RUN --mount=type=cache,target=/go/pkg/mod go mod download COPY . . RUN --mount=type=cache,target=/go/pkg/mod \\ --mount=type=cache,target=/root/.cache/go-build \\ go build -ldflags="-s -w" -o /app/main ./{service_dir} FROM alpine:latest COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ COPY --from=builder /usr/share/zoneinfo/Asia/Shanghai /usr/share/zoneinfo/Asia/Shanghai ENV TZ=Asia/Shanghai WORKDIR /app COPY --from=builder /app/main /app/main CMD ["./main"] """ def discover_targets(): targets = {} for service_dir in sorted(glob(str(ROOT_DIR / "app" / "*" / "*"))): service_dir_rel = Path(service_dir).relative_to(ROOT_DIR).as_posix() service_type = Path(service_dir).name if service_type not in ("api", "rpc", "mq", "adapter"): continue go_files = glob(os.path.join(service_dir, "*.go")) if not go_files or not any( "package main" in Path(f).read_text(encoding="utf-8") for f in go_files ): continue yamls = glob(os.path.join(service_dir, "etc", "*.yaml")) service_name = Path(service_dir).parent.name target_name = f"{service_name}-{service_type}" if yamls: config_name = Path(yamls[0]).name dockerfile = DOCKERFILE_TPL.format( service_dir=service_dir_rel, config_name=config_name ) else: dockerfile = DOCKERFILE_NO_CONFIG_TPL.format(service_dir=service_dir_rel) targets[target_name] = { "dockerfile-inline": dockerfile, "tags": [f"{IMAGE_PREFIX}/{target_name}:{IMAGE_TAG}"], } return targets def frontend_target(): frontend_root = ROOT_DIR / "frontend" if not (frontend_root / "Dockerfile").exists(): return None return { "context": "frontend", "dockerfile": "Dockerfile", "tags": [f"{IMAGE_PREFIX}/frontend:{IMAGE_TAG}"], } def main(): os.chdir(ROOT_DIR) targets = discover_targets() ft = frontend_target() if ft is not None: targets["frontend"] = ft if not targets: print("No targets found") sys.exit(1) bake = { "group": {"default": {"targets": list(targets.keys())}}, "target": targets, } bakefile = ROOT_DIR / ".bake.json" try: bakefile.write_text(json.dumps(bake, indent=2)) print(f"Generated {len(targets)} targets -> {bakefile}") names = list(targets.keys()) for i in range(0, len(names), BAKE_BATCH_SIZE): batch = names[i : i + BAKE_BATCH_SIZE] subprocess.run( ["docker", "buildx", "bake", "--load", "-f", str(bakefile), *batch], check=True, ) finally: bakefile.unlink(missing_ok=True) main()