diff --git a/README.md b/README.md index 05503253..62059d12 100644 --- a/README.md +++ b/README.md @@ -329,10 +329,39 @@ packages: **Docker builds:** -For Docker packages, leeway automatically enables BuildKit (`DOCKER_BUILDKIT=1`) and exports `SOURCE_DATE_EPOCH`. For deterministic Docker images, see PR #285 which passes the value as a build arg (requires `ARG SOURCE_DATE_EPOCH` in Dockerfiles). +For Docker packages, leeway automatically enables BuildKit (`DOCKER_BUILDKIT=1`) and exports `SOURCE_DATE_EPOCH`. Additionally, leeway passes `SOURCE_DATE_EPOCH` as a build arg to enable deterministic image timestamps. BuildKit is the default builder since Docker Engine v23.0 and is always used in Docker Desktop. +**Dockerfile requirements for deterministic images:** + +Dockerfiles MUST declare the build arg for BuildKit to use the timestamp for image metadata: + +```dockerfile +FROM alpine:3.18 +ARG SOURCE_DATE_EPOCH +COPY app /usr/local/bin/app +``` + +With the `ARG SOURCE_DATE_EPOCH` declaration, BuildKit (>= v0.13) automatically uses the timestamp for: +- Layer creation timestamps +- Image config `created` timestamp +- History timestamps +- OCI annotations + +Without the ARG declaration, images will have non-deterministic timestamps even though leeway sets the environment variable. + +For multi-stage builds, declare the ARG in each stage: + +```dockerfile +FROM golang:1.21 AS builder +ARG SOURCE_DATE_EPOCH +RUN go build -o app + +FROM alpine:3.18 +ARG SOURCE_DATE_EPOCH +COPY --from=builder /app /app +``` ## Package Variants Leeway supports build-time variance through "package variants". Those variants are defined on the workspace level and can modify the list of sources, environment variables and config of packages. For example consider a `WORKSPACE.YAML` with this variants section: diff --git a/pkg/leeway/build.go b/pkg/leeway/build.go index 1af3e351..f11d71b4 100644 --- a/pkg/leeway/build.go +++ b/pkg/leeway/build.go @@ -1992,6 +1992,8 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("DEP_%s=%s", arg, val)) } buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("__GIT_COMMIT=%s", p.C.Git().Commit)) + // Pass SOURCE_DATE_EPOCH for deterministic Docker image timestamps + buildcmd = append(buildcmd, "--build-arg", fmt.Sprintf("SOURCE_DATE_EPOCH=%d", mtime)) if cfg.Squash { buildcmd = append(buildcmd, "--squash") } @@ -2219,7 +2221,11 @@ func (p *Package) buildDocker(buildctx *buildContext, wd, result string) (res *p // Add PostProcess to create structured metadata file res.PostProcess = func(buildCtx *buildContext, pkg *Package, buildDir string) error { - return createDockerExportMetadata(buildDir, version, cfg) + mtime, err := pkg.getDeterministicMtime() + if err != nil { + return fmt.Errorf("failed to get deterministic mtime: %w", err) + } + return createDockerExportMetadata(buildDir, version, cfg, mtime) } // Add subjects function for provenance generation @@ -2365,11 +2371,11 @@ type DockerImageMetadata struct { } // createDockerExportMetadata creates metadata file for exported Docker images -func createDockerExportMetadata(wd, version string, cfg DockerPkgConfig) error { +func createDockerExportMetadata(wd, version string, cfg DockerPkgConfig, mtime int64) error { metadata := DockerImageMetadata{ ImageNames: cfg.Image, BuiltVersion: version, - BuildTime: time.Now(), + BuildTime: time.Unix(mtime, 0), CustomMeta: cfg.Metadata, }