# Lint phase, built alone by script/lint. The tools are invoked directly # rather than through `make lint`, which runs docker itself and so cannot # run inside a build step. # golangci/golangci-lint:v2.14.0, 2026-10-07 FROM golangci/golangci-lint@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # The gofmt half of `make fmt-check`. gofmt's output is assigned to a # variable first so that its own exit status, as when it cannot parse a # file, still fails the step. RUN files="$(gofmt -s -l .)" && \ if [ -n "$files" ]; then \ echo "gofmt: files not formatted:" >&2; echo "$files" >&2; exit 1; \ fi # Validates .golangci.yml against the schema the pinned binary embeds. RUN golangci-lint config verify --config .golangci.yml RUN golangci-lint run --config .golangci.yml ./... # Test phase, built alone by script/test. -race needs cgo and so a C # compiler, which the Debian Go image ships and the alpine one does not. # # The tests run as nobody: several of them make a file unreadable and # expect reading it to fail, and root reads it anyway. nobody has no home # directory, so HOME is /tmp, where Go puts its build cache. # golang:1.25-trixie, 2026-10-04 FROM golang@sha256:2c4c60ef415fbfa5e90300722293bef36c5e63fae17570ce18f580af933dbd73 AS test USER nobody ENV HOME=/tmp WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go test -timeout 90s -race -cover ./... || \ { echo "--- Rerunning with -v for details ---"; \ go test -timeout 90s -race -v ./...; exit 1; } # Build stage. Nothing is wanted from either phase above; the copies are # what make BuildKit build them first, so this stage cannot run unless # lint and test passed. # golang:1.25-alpine, 2026-07-23 FROM golang@sha256:56961d79ea8129efddcc0b8643fd8a5416b4e6228cfd477e3fd61deb2672c587 AS builder COPY --from=lint /src/go.sum /dev/null COPY --from=test /src/go.sum /dev/null RUN apk add --no-cache git make # A tar-stream context keeps the sender's file owners, which git refuses. RUN git config --system --add safe.directory /src WORKDIR /src COPY go.mod go.sum ./ RUN go mod download COPY . . # The version stamped into the binary: the VERSION build argument when # one is given, otherwise `git describe --tags --always` of the .git in # the build context. A context that carries .git and still yields no # version fails the build; with neither, as from a source tarball, it is # "dev". `make build` rather than `go build`, so the image and a host # build share one compile recipe, cgo disabled included. ARG VERSION RUN version="${VERSION:-$(git describe --tags --always || echo dev)}"; \ if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \ [ "$version" = unknown ]; }; then \ echo "no version could be derived although the build context carries .git" >&2; \ exit 1; \ fi; \ make build VERSION="$version" # Runtime stage, and the last one: a plain `docker build .` builds this # stage's chain and nothing else. # alpine:3.22, 2026-07-23 FROM alpine@sha256:14358309a308569c32bdc37e2e0e9694be33a9d99e68afb0f5ff33cc1f695dce COPY --from=builder /src/sfdupes /usr/local/bin/sfdupes ENTRYPOINT ["sfdupes"]