make build now builds with CGO_ENABLED=0, and the image builds through it. The binary's only cgo code was mattn/go-sqlite3, which gorm.io/driver/sqlite imports and webhooker never opens: every SQLite file is opened with modernc.org/sqlite and the connection handed to gorm. Without cgo, mattn's package compiles to a stub nothing calls. The Dockerfile builds the binary once. The static relink and its netgo, osusergo and sqlite_omit_load_extension tags are gone, and with them the Makefile's GO_LDFLAGS and GO_TAGS, which only the relink used. The test phase keeps cgo for -race. No README or Dockerfile sentence says the binary needs cgo any more. Model: opus-5-5
This commit was merged in pull request #550.
This commit is contained in:
+4
-23
@@ -76,8 +76,6 @@ RUN --network=none node_modules/.bin/prettier --check '**/*.md'
|
|||||||
# depends on it.
|
# depends on it.
|
||||||
#
|
#
|
||||||
# golangci/golangci-lint:v2.14.0 (Debian-based), 2026-09-24
|
# golangci/golangci-lint:v2.14.0 (Debian-based), 2026-09-24
|
||||||
# Using Debian-based image because mattn/go-sqlite3 (CGO) does not
|
|
||||||
# compile on Alpine musl (off64_t is a glibc type).
|
|
||||||
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
FROM golangci/golangci-lint:v2.14.0@sha256:ad862ba6b3798cbe0fd9fd7408d498fd74fbd2623a92406b2fd3898faf0bf98f AS lint
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
@@ -167,9 +165,7 @@ RUN --mount=type=tmpfs,target=/tmp/tests,size=512m \
|
|||||||
|
|
||||||
# Build stage
|
# Build stage
|
||||||
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
|
# golang:1.26.1-bookworm (Debian-based), 2026-03-17
|
||||||
# Using Debian-based image because gorm.io/driver/sqlite pulls in
|
# The image ships git and make, which the version step below uses.
|
||||||
# mattn/go-sqlite3 (CGO), which does not compile on Alpine musl. The image
|
|
||||||
# ships git and make, which the version step below uses.
|
|
||||||
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
|
FROM golang:1.26.1-bookworm@sha256:4465644228bc2857a954b092167e12aa59c006a3492282a6c820bf4755fd64a4 AS builder
|
||||||
|
|
||||||
# Nothing is wanted from the lint and test phases or from the stylesheet and
|
# Nothing is wanted from the lint and test phases or from the stylesheet and
|
||||||
@@ -211,26 +207,11 @@ RUN version="$(make version VERSION="$VERSION")"; \
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
# Builds through the Makefile's build target, which runs script/assets
|
# Builds through the Makefile's build target, which runs script/assets
|
||||||
# (Alpine.js, extracted from its tarball in 3p/) first.
|
# (Alpine.js, extracted from its tarball in 3p/) first. The target builds
|
||||||
|
# with cgo off, so the binary is statically linked and runs on the Alpine
|
||||||
|
# runtime image below as it is.
|
||||||
RUN make build VERSION="$VERSION"
|
RUN make build VERSION="$VERSION"
|
||||||
|
|
||||||
# Rebuild with static linking for the Alpine runtime. The cgo binary the
|
|
||||||
# step above builds is dynamically linked against glibc, which Alpine
|
|
||||||
# (musl) does not have.
|
|
||||||
#
|
|
||||||
# A statically linked glibc still loads shared libraries at run time for
|
|
||||||
# getaddrinfo and dlopen, and Alpine has none to load, so the build tags
|
|
||||||
# keep both out of the binary: netgo and osusergo select Go's own DNS
|
|
||||||
# resolver and user lookup instead of glibc's, and
|
|
||||||
# sqlite_omit_load_extension leaves out SQLite extension loading, which
|
|
||||||
# webhooker never uses.
|
|
||||||
#
|
|
||||||
# The static flags and tags go in through GO_LDFLAGS and GO_TAGS rather
|
|
||||||
# than a -ldflags of their own: the build target composes them with the
|
|
||||||
# -X that stamps the version, so this relink cannot silently drop the
|
|
||||||
# stamp.
|
|
||||||
RUN CGO_ENABLED=1 make build VERSION="$VERSION" GO_TAGS=netgo,osusergo,sqlite_omit_load_extension GO_LDFLAGS='-extldflags "-static"'
|
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
# alpine:3.21, 2026-03-17
|
# alpine:3.21, 2026-03-17
|
||||||
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
FROM alpine:3.21@sha256:c3f8e73fdb79deaebaa2037150150191b9dcbfba68b4a46d70103204c53f4709
|
||||||
|
|||||||
@@ -16,17 +16,6 @@ VERSION ?= $(shell script/version)
|
|||||||
# exists to correct.
|
# exists to correct.
|
||||||
override VERSION := $(or $(strip $(VERSION)),$(shell script/version))
|
override VERSION := $(or $(strip $(VERSION)),$(shell script/version))
|
||||||
|
|
||||||
# Extra linker flags and build tags for the build target. The static
|
|
||||||
# relink in the Dockerfile passes -extldflags and its build tags through
|
|
||||||
# these rather than a -ldflags of its own, so composing flags cannot drop
|
|
||||||
# the version stamp.
|
|
||||||
#
|
|
||||||
# The build target itself always passes -trimpath and -s -w, as the Go
|
|
||||||
# Dockerfile in REPO_POLICIES.md does: no build paths, symbol table or
|
|
||||||
# debug information in the binary.
|
|
||||||
GO_LDFLAGS ?=
|
|
||||||
GO_TAGS ?=
|
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
@script/bootstrap
|
@script/bootstrap
|
||||||
|
|
||||||
@@ -54,8 +43,12 @@ fmt-check:
|
|||||||
check:
|
check:
|
||||||
@script/check
|
@script/check
|
||||||
|
|
||||||
|
# Built with cgo off, -trimpath and -s -w, as the Go Dockerfile in
|
||||||
|
# REPO_POLICIES.md does: the binary carries no build paths, symbol table or
|
||||||
|
# debug information, and on Linux, the image's included, it is statically
|
||||||
|
# linked.
|
||||||
build: assets
|
build: assets
|
||||||
go build -trimpath -tags '$(GO_TAGS)' -ldflags '$(strip -s -w -X main.version=$(VERSION) $(GO_LDFLAGS))' -o bin/webhooker ./cmd/webhooker
|
CGO_ENABLED=0 go build -trimpath -ldflags '-s -w -X main.version=$(VERSION)' -o bin/webhooker ./cmd/webhooker
|
||||||
|
|
||||||
run: build
|
run: build
|
||||||
./bin/webhooker
|
./bin/webhooker
|
||||||
|
|||||||
@@ -1393,10 +1393,10 @@ It uses:
|
|||||||
management
|
management
|
||||||
- **[go-chi](https://github.com/go-chi/chi)** for HTTP routing
|
- **[go-chi](https://github.com/go-chi/chi)** for HTTP routing
|
||||||
- **[GORM](https://gorm.io)** for database access with
|
- **[GORM](https://gorm.io)** for database access with
|
||||||
**[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite)** as the runtime
|
**[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite)**, which is pure
|
||||||
SQLite driver. Note: `gorm.io/driver/sqlite` transitively depends on
|
Go, as the SQLite driver. `gorm.io/driver/sqlite` also imports
|
||||||
`mattn/go-sqlite3`, which requires CGO at build time (see [Docker](#docker)
|
`mattn/go-sqlite3`, whose stub for builds without cgo is linked into the
|
||||||
section)
|
binary but never used
|
||||||
- **[slog](https://pkg.go.dev/log/slog)** (stdlib) for structured logging with
|
- **[slog](https://pkg.go.dev/log/slog)** (stdlib) for structured logging with
|
||||||
TTY detection (text for dev, JSON for prod)
|
TTY detection (text for dev, JSON for prod)
|
||||||
- **[gorilla/sessions](https://github.com/gorilla/sessions)** for encrypted
|
- **[gorilla/sessions](https://github.com/gorilla/sessions)** for encrypted
|
||||||
@@ -2135,9 +2135,7 @@ endpoint. That is the JSON key; the error text for a missing one reads
|
|||||||
`webhook_url is required`, which is the message, not the key.
|
`webhook_url is required`, which is the message, not the key.
|
||||||
|
|
||||||
The database uses the
|
The database uses the
|
||||||
[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) driver at runtime,
|
[modernc.org/sqlite](https://pkg.go.dev/modernc.org/sqlite) driver.
|
||||||
though CGO is required at build time due to the transitive `mattn/go-sqlite3`
|
|
||||||
dependency from `gorm.io/driver/sqlite`.
|
|
||||||
|
|
||||||
#### Schema
|
#### Schema
|
||||||
|
|
||||||
@@ -3468,16 +3466,12 @@ fixed independently of the compiler's:
|
|||||||
a step's log, and then fails. `make test` builds this stage alone.
|
a step's log, and then fails. `make test` builds this stage alone.
|
||||||
6. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint and test
|
6. **Builder stage** (`golang:1.26.1-bookworm`) — depends on the lint and test
|
||||||
phases and the `css-check` and `markdown-check` stages passing (it copies a
|
phases and the `css-check` and `markdown-check` stages passing (it copies a
|
||||||
file from each), runs `make build` (which extracts Alpine.js from `3p/`
|
file from each), and runs `make build` (which extracts Alpine.js from `3p/`
|
||||||
first), and then rebuilds the binary with `CGO_ENABLED=1` and static linking
|
first). `make build` compiles with cgo off, so the binary is statically
|
||||||
so it runs on musl, with the build tags `netgo`, `osusergo` and
|
linked and runs on the Alpine runtime image. The version is the `VERSION`
|
||||||
`sqlite_omit_load_extension` so it needs none of glibc's shared libraries at
|
build arg if one is given, otherwise derived from the `.git` in the context,
|
||||||
run time. Both builds go through `make build`, the relink adding its
|
and the stage fails if a context with `.git` would stamp an empty version,
|
||||||
`-extldflags` via `GO_LDFLAGS` and its tags via `GO_TAGS`, so neither can
|
`dev` or `unknown` (see [Version stamping](#version-stamping)).
|
||||||
drop the `-X` that stamps the version. The version is the `VERSION` build arg
|
|
||||||
if one is given, otherwise derived from the `.git` in the context, and the
|
|
||||||
stage fails if a context with `.git` would stamp an empty version, `dev` or
|
|
||||||
`unknown` (see [Version stamping](#version-stamping)).
|
|
||||||
7. **Runtime stage** (`alpine:3.21`) — copies the static binary and
|
7. **Runtime stage** (`alpine:3.21`) — copies the static binary and
|
||||||
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker` directory for
|
`deploy/docker-entrypoint.sh`, creates the `/var/lib/webhooker` directory for
|
||||||
all SQLite databases, exposes port 8080, and includes a health check against
|
all SQLite databases, exposes port 8080, and includes a health check against
|
||||||
@@ -3489,11 +3483,6 @@ The lint and test phases invoke `gofmt`, `golangci-lint` and `go test` directly
|
|||||||
rather than `make fmt-check`, `make lint` and `make test`: those targets build
|
rather than `make fmt-check`, `make lint` and `make test`: those targets build
|
||||||
docker stages, which would need a docker daemon inside this build.
|
docker stages, which would need a docker daemon inside this build.
|
||||||
|
|
||||||
The lint phase and the Go stages use Debian rather than Alpine because
|
|
||||||
`gorm.io/driver/sqlite` pulls in `mattn/go-sqlite3`, which needs CGO and does
|
|
||||||
not compile against musl. Only the final binary is statically linked, which is
|
|
||||||
what lets it run on the Alpine runtime image.
|
|
||||||
|
|
||||||
`script/cibuild` is the CI gate: it runs `script/bootstrap`, then
|
`script/cibuild` is the CI gate: it runs `script/bootstrap`, then
|
||||||
`script/check`, then builds the image, whose build runs the lint and test phases
|
`script/check`, then builds the image, whose build runs the lint and test phases
|
||||||
and the stylesheet and Markdown checks again. A build that succeeds is a repo
|
and the stylesheet and Markdown checks again. A build that succeeds is a repo
|
||||||
|
|||||||
@@ -153,19 +153,15 @@ func TestVersion_IsStableAcrossInvocations(t *testing.T) {
|
|||||||
require.Equal(t, first, second)
|
require.Equal(t, first, second)
|
||||||
}
|
}
|
||||||
|
|
||||||
// The build target is the only place composing linker flags. If a
|
// The build target is the only place that stamps the version. If a
|
||||||
// future edit drops either half, the binary silently reports "dev"
|
// future edit drops the -X flag, the binary silently reports "dev"
|
||||||
// again (the defect this package exists for) or fails to link on
|
// again, the defect this package exists for.
|
||||||
// Alpine. If it stops passing GO_TAGS, the image's static binary links
|
func TestMakefile_BuildStampsTheVersion(t *testing.T) {
|
||||||
// glibc's getaddrinfo and dlopen again.
|
|
||||||
func TestMakefile_BuildComposesVersionAndExtraFlags(t *testing.T) {
|
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
makefile := read(t, makefilePath)
|
makefile := read(t, makefilePath)
|
||||||
|
|
||||||
require.Contains(t, makefile, "-X main.version=$(VERSION)")
|
require.Contains(t, makefile, "-X main.version=$(VERSION)")
|
||||||
require.Contains(t, makefile, "$(GO_LDFLAGS)")
|
|
||||||
require.Contains(t, makefile, "-tags '$(GO_TAGS)'")
|
|
||||||
require.Contains(t, makefile, "VERSION ?= $(shell script/version)")
|
require.Contains(t, makefile, "VERSION ?= $(shell script/version)")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -224,8 +220,8 @@ func requireMake(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Every compile in the image goes through the build target, so the
|
// The image compiles through the build target, so it carries the
|
||||||
// static relink cannot replace the flags that carry the stamp.
|
// target's version stamp.
|
||||||
func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -234,10 +230,7 @@ func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
|||||||
require.NotContains(t, dockerfile, "go build",
|
require.NotContains(t, dockerfile, "go build",
|
||||||
"a raw go build bypasses the Makefile's -X flag")
|
"a raw go build bypasses the Makefile's -X flag")
|
||||||
require.Contains(t, dockerfile, "ARG VERSION")
|
require.Contains(t, dockerfile, "ARG VERSION")
|
||||||
require.Contains(t, dockerfile,
|
require.Contains(t, dockerfile, `RUN make build VERSION="$VERSION"`)
|
||||||
`make build VERSION="$VERSION"`+
|
|
||||||
` GO_TAGS=netgo,osusergo,sqlite_omit_load_extension`+
|
|
||||||
` GO_LDFLAGS='-extldflags "-static"'`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func read(t *testing.T, path string) string {
|
func read(t *testing.T, path string) string {
|
||||||
|
|||||||
Reference in New Issue
Block a user