From 146da8552e6e3c7dcad6a014b5f42ce5670eff64 Mon Sep 17 00:00:00 2001 From: Nikolay Govorov Date: Sun, 12 Apr 2026 16:33:16 +0100 Subject: Configures reproducible builds for full matrix --- .github/workflows/build.yml | 6 +- Taskfile.yml | 102 ++++++++++++++++++++------- cmd/mirum-server/database.go | 3 +- cmd/mirum-server/id.go | 3 +- cmd/mirum-server/main.go | 7 +- cmd/mirum-server/proto/buf.gen.yaml | 4 +- cmd/mirum-server/web/lib/utils.ts | 1 + go.mod | 7 +- internal/protocol/proto/buf.gen.yaml | 4 +- internal/supervisor/supervisor.go | 22 +++++- internal/supervisor/systemd.go | 22 ++++-- nfpm.yaml | 10 +-- 12 files changed, 143 insertions(+), 48 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ddc44a6..305b0ad 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,6 +1,10 @@ # Copyright (c) 2026 Nikolay Govorov # SPDX-License-Identifier: AGPL-3.0-or-later +# NOTE: Don't extend it; keep the logic in the Taskfile. +# We're using GitHub Actions as a temporary solution +# until Mirum can handle its own maintenance. + name: Build on: @@ -31,8 +35,6 @@ jobs: sudo apt update && sudo apt install nfpm apt-utils rclone createrepo-c sh -c "$(curl --location https://taskfile.dev/install.sh)" -- -d -b /usr/local/bin curl -sSL "https://github.com/bufbuild/buf/releases/download/v1.67.0/buf-$(uname -s)-$(uname -m)" -o /usr/local/bin/buf && chmod +x /usr/local/bin/buf - go install google.golang.org/protobuf/cmd/protoc-gen-go@latest - go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest pipx install reuse - name: Build and package diff --git a/Taskfile.yml b/Taskfile.yml index 936e885..56e2d43 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -5,12 +5,24 @@ version: "3" output: prefixed +env: + CGO_ENABLED: "0" # cgo forbidden in mirum, do not change this behavior. + SOURCE_DATE_EPOCH: + sh: 'if [ -z "$(git status --porcelain)" ]; then git log -1 --format=%ct; else date +%s; fi' + vars: - BUILD_DIR: build - DIST_DIR: build/dist - ARCHES: amd64 arm64 riscv64 ppc64le - DEV_DB: mirum-local-dev DOCKER: "false" + DEV_DB: mirum-local-dev + DIST_DIR: build/dist + BUILD_DIR: build + TARGETS: >- + linux-amd64 linux-arm64 linux-riscv64 linux-ppc64le linux-loong64 linux-s390x + darwin-amd64 darwin-arm64 + windows-amd64 windows-arm64 + freebsd-amd64 freebsd-arm64 freebsd-riscv64 + openbsd-amd64 openbsd-arm64 + netbsd-amd64 netbsd-arm64 + LINUX_PKG_TARGETS: amd64 arm64 riscv64 ppc64le loong64 s390x tasks: ci: @@ -231,8 +243,6 @@ tasks: desc: Build mirum-server with -tags dev and run it against the dev environment deps: [proto, devenv] prefix: mirum-server - env: - CGO_ENABLED: "0" cmds: - mkdir -p {{.BUILD_DIR}} - go build -tags dev -o {{.BUILD_DIR}}/mirum-server ./cmd/mirum-server @@ -248,24 +258,55 @@ tasks: vars: GOOS: { sh: "echo ${GOOS:-$(go env GOOS)}" } GOARCH: { sh: "echo ${GOARCH:-$(go env GOARCH)}" } + LDFLAGS: "-s -w" env: - CGO_ENABLED: "0" GOOS: "{{.GOOS}}" GOARCH: "{{.GOARCH}}" + GOFLAGS: "-trimpath" cmds: - mkdir -p {{.BUILD_DIR}} - - go build -o {{.BUILD_DIR}}/mirum-server-{{.GOOS}}-{{.GOARCH}} ./cmd/mirum-server - - go build -o {{.BUILD_DIR}}/mirum-worker-{{.GOOS}}-{{.GOARCH}} ./cmd/mirum-worker - - go build -o {{.BUILD_DIR}}/mirum-{{.GOOS}}-{{.GOARCH}} ./cmd/mirum - - cp {{.BUILD_DIR}}/mirum-server-{{.GOOS}}-{{.GOARCH}} {{.BUILD_DIR}}/mirum-server - - cp {{.BUILD_DIR}}/mirum-worker-{{.GOOS}}-{{.GOARCH}} {{.BUILD_DIR}}/mirum-worker - - cp {{.BUILD_DIR}}/mirum-{{.GOOS}}-{{.GOARCH}} {{.BUILD_DIR}}/mirum + - go build -ldflags="{{.LDFLAGS}}" -o {{.BUILD_DIR}}/mirum-server-{{.GOOS}}-{{.GOARCH}} ./cmd/mirum-server + - go build -ldflags="{{.LDFLAGS}}" -o {{.BUILD_DIR}}/mirum-worker-{{.GOOS}}-{{.GOARCH}} ./cmd/mirum-worker + - go build -ldflags="{{.LDFLAGS}}" -o {{.BUILD_DIR}}/mirum-{{.GOOS}}-{{.GOARCH}} ./cmd/mirum + + cross: + desc: Cross-compile and archive binaries for all supported platforms + cmds: + - | + mkdir -p {{.DIST_DIR}} + for target in {{.TARGETS}}; do + os="${target%%-*}" + arch="${target#*-}" + task build GOOS="$os" GOARCH="$arch" + + ext="" + [ "$os" = "windows" ] && ext=".exe" + + staging=$(mktemp -d) + cp README.md "$staging/" + cp -r LICENSES "$staging/" + for cmd in cmd/*/; do + bin=$(basename "$cmd") + cp "{{.BUILD_DIR}}/${bin}-${os}-${arch}" "${staging}/${bin}${ext}" + done + + if [ "$os" = "windows" ]; then + (cd "$staging" && zip -qr "$OLDPWD/{{.DIST_DIR}}/mirum-${target}.zip" .) + else + tar -czf "{{.DIST_DIR}}/mirum-${target}.tar.gz" -C "$staging" . + fi + rm -rf "$staging" + done + - cd {{.DIST_DIR}} && sha256sum *.tar.gz *.zip > SHA256SUMS run: desc: Build and run mirum-server against the dev environment deps: [build, devenv] + vars: + GOOS: { sh: go env GOOS } + GOARCH: { sh: go env GOARCH } cmds: - - "{{.BUILD_DIR}}/mirum-server daemon --config dev/mirum-server.yaml" + - "{{.BUILD_DIR}}/mirum-server-{{.GOOS}}-{{.GOARCH}} daemon --config dev/mirum-server.yaml" test: desc: Run all tests @@ -274,7 +315,8 @@ tasks: - go test -race -count=1 ./... package: - desc: Build deb/rpm packages for all architectures + desc: Cross-compile all platforms and build deb/rpm packages for Linux + deps: [cross] vars: VERSION: sh: | @@ -308,12 +350,15 @@ tasks: export NFPM_PASSPHRASE="$GPG_PASSPHRASE" fi - mkdir -p {{.DIST_DIR}} - for arch in {{.ARCHES}}; do - task build GOOS=linux GOARCH=$arch + for arch in {{.LINUX_PKG_TARGETS}}; do + mkdir -p {{.BUILD_DIR}}/tmp + for cmd in cmd/*/; do + bin=$(basename "$cmd") + cp "{{.BUILD_DIR}}/${bin}-linux-${arch}" "{{.BUILD_DIR}}/tmp/${bin}" + done ARCH=$arch nfpm package --packager deb --target {{.DIST_DIR}}/ ARCH=$arch nfpm package --packager rpm --target {{.DIST_DIR}}/ - rm {{.BUILD_DIR}}/mirum-server {{.BUILD_DIR}}/mirum-worker + rm -rf {{.BUILD_DIR}}/tmp done printf 'VERSION={{.VERSION}}\nCHANNEL={{.CHANNEL}}\n' > {{.DIST_DIR}}/build.env @@ -337,16 +382,25 @@ tasks: done fi - gh release create "$TAG" {{.DIST_DIR}}/*.deb {{.DIST_DIR}}/*.rpm \ + gh release create "$TAG" {{.DIST_DIR}}/*.deb {{.DIST_DIR}}/*.rpm {{.DIST_DIR}}/*.tar.gz {{.DIST_DIR}}/*.zip {{.DIST_DIR}}/SHA256SUMS \ --title "$NAME" \ $( [ "{{.CHANNEL}}" = "nightly" ] && echo "--prerelease" ) \ --notes "**Version**: {{.VERSION}}" \ - || gh release upload "$TAG" {{.DIST_DIR}}/*.deb {{.DIST_DIR}}/*.rpm --clobber + || gh release upload "$TAG" {{.DIST_DIR}}/*.deb {{.DIST_DIR}}/*.rpm {{.DIST_DIR}}/*.tar.gz {{.DIST_DIR}}/*.zip {{.DIST_DIR}}/SHA256SUMS --clobber publish:repos: desc: Publish packages to APT and RPM repositories on S3 requires: - vars: [CHANNEL, S3_BUCKET, S3_PUBLIC_URL, S3_PROVIDER, S3_REGION, S3_ENDPOINT, GPG_KEY_ID] + vars: + [ + CHANNEL, + S3_BUCKET, + S3_PUBLIC_URL, + S3_PROVIDER, + S3_REGION, + S3_ENDPOINT, + GPG_KEY_ID, + ] cmds: - | set -e @@ -383,7 +437,7 @@ tasks: for ch in stable nightly; do mkdir -p apt-repo/pool/$ch - for arch in {{.ARCHES}}; do + for arch in {{.LINUX_PKG_TARGETS}}; do mkdir -p apt-repo/dists/$ch/main/binary-$arch done done @@ -394,7 +448,7 @@ tasks: apt-ftparchive \ -o APT::FTPArchive::Release::Codename=$CHANNEL \ -o APT::FTPArchive::Release::Components=main \ - -o APT::FTPArchive::Release::Architectures="{{.ARCHES}}" \ + -o APT::FTPArchive::Release::Architectures="{{.LINUX_PKG_TARGETS}}" \ release dists/$CHANNEL/ > dists/$CHANNEL/Release gpg --default-key="{{.GPG_KEY_ID}}" --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" -abs -o dists/$CHANNEL/Release.gpg dists/$CHANNEL/Release gpg --default-key="{{.GPG_KEY_ID}}" --batch --yes --pinentry-mode loopback --passphrase "$GPG_PASSPHRASE" --clearsign -o dists/$CHANNEL/InRelease dists/$CHANNEL/Release diff --git a/cmd/mirum-server/database.go b/cmd/mirum-server/database.go index 8655dbc..3289832 100644 --- a/cmd/mirum-server/database.go +++ b/cmd/mirum-server/database.go @@ -10,6 +10,7 @@ import ( "crypto/sha256" "crypto/subtle" "encoding/base64" + "encoding/hex" "errors" "fmt" "log/slog" @@ -1348,7 +1349,7 @@ func (db *DB) WorkerLookup(ctx context.Context, actor Actor, publicKey []byte) ( // hashToken returns the hex-encoded SHA-256 of a session token. func hashToken(token string) string { h := sha256.Sum256([]byte(token)) - return fmt.Sprintf("%x", h) + return hex.EncodeToString(h[:]) } // verifyHash parses a PHC-format argon2id string and compares. diff --git a/cmd/mirum-server/id.go b/cmd/mirum-server/id.go index 09ec141..9d50142 100644 --- a/cmd/mirum-server/id.go +++ b/cmd/mirum-server/id.go @@ -5,6 +5,7 @@ package main import ( "database/sql/driver" + "encoding/hex" "errors" "fmt" "log/slog" @@ -173,7 +174,7 @@ func FormatAnyID(b []byte) string { if len(b) == 16 { return encodeBase58(uuid.UUID(b)) } - return fmt.Sprintf("%x", b) + return hex.EncodeToString(b) } func parseID(prefix, s string) (uuid.UUID, error) { diff --git a/cmd/mirum-server/main.go b/cmd/mirum-server/main.go index c592f71..549373d 100644 --- a/cmd/mirum-server/main.go +++ b/cmd/mirum-server/main.go @@ -18,7 +18,6 @@ import ( "dimidiumlabs/mirum/internal/protocol/wirepb" "dimidiumlabs/mirum/internal/supervisor" - "github.com/coreos/go-systemd/v22/activation" "github.com/spf13/cobra" ) @@ -121,7 +120,7 @@ func daemon(configFile, socketFlag string) error { }, }) - grpcLn, webLn, adminLn, err := listeners(cfg) + grpcLn, webLn, adminLn, err := listeners(cfg, sup) if err != nil { slog.Error("listeners failed", "err", err) return err @@ -192,8 +191,8 @@ func daemon(configFile, socketFlag string) error { // listeners returns gRPC, web, and admin listeners. // With systemd socket activation it expects two named fds: "grpc" and "web". // Without socket activation it falls back to configured addresses. -func listeners(cfg *appConfig) (grpcLn, webLn, adminLn net.Listener, err error) { - named, err := activation.ListenersWithNames() +func listeners(cfg *appConfig, sup supervisor.Supervisor) (grpcLn, webLn, adminLn net.Listener, err error) { + named, err := sup.ActivationListeners() if err != nil { return nil, nil, nil, fmt.Errorf("socket activation: %w", err) } diff --git a/cmd/mirum-server/proto/buf.gen.yaml b/cmd/mirum-server/proto/buf.gen.yaml index a9a94e2..676d20f 100644 --- a/cmd/mirum-server/proto/buf.gen.yaml +++ b/cmd/mirum-server/proto/buf.gen.yaml @@ -3,10 +3,10 @@ version: v2 plugins: - - local: protoc-gen-go + - local: ["go", "tool", "google.golang.org/protobuf/cmd/protoc-gen-go"] out: ../apipb opt: paths=source_relative - - local: protoc-gen-connect-go + - local: ["go", "tool", "connectrpc.com/connect/cmd/protoc-gen-connect-go"] out: ../apipb opt: paths=source_relative - remote: buf.build/bufbuild/es:v2.11.0 diff --git a/cmd/mirum-server/web/lib/utils.ts b/cmd/mirum-server/web/lib/utils.ts index 5d8fa3d..ce51c0f 100644 --- a/cmd/mirum-server/web/lib/utils.ts +++ b/cmd/mirum-server/web/lib/utils.ts @@ -6,6 +6,7 @@ import { twMerge } from "tailwind-merge" export type ClassValue = ClassArray | Record | string | number | bigint | null | boolean | undefined export type ClassArray = ClassValue[] +// avoid dependency for 10 lines and this version is stricter in TS function classList(value: ClassValue): string { if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") { return String(value) diff --git a/go.mod b/go.mod index 4bb59e7..3186af3 100644 --- a/go.mod +++ b/go.mod @@ -15,6 +15,7 @@ require ( github.com/jackc/pgx/v5 v5.9.1 github.com/jackc/tern/v2 v2.3.6 github.com/spf13/cobra v1.8.0 + github.com/spf13/pflag v1.0.5 go.starlark.net v0.0.0-20260326113308-fadfc96def35 golang.org/x/crypto v0.49.0 google.golang.org/protobuf v1.36.11 @@ -41,7 +42,6 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/shopspring/decimal v1.4.0 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect golang.org/x/sync v0.20.0 // indirect @@ -50,3 +50,8 @@ require ( google.golang.org/genproto/googleapis/api v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect ) + +tool ( + connectrpc.com/connect/cmd/protoc-gen-connect-go + google.golang.org/protobuf/cmd/protoc-gen-go +) diff --git a/internal/protocol/proto/buf.gen.yaml b/internal/protocol/proto/buf.gen.yaml index 8e9a8e9..8db3d92 100644 --- a/internal/protocol/proto/buf.gen.yaml +++ b/internal/protocol/proto/buf.gen.yaml @@ -3,9 +3,9 @@ version: v2 plugins: - - local: protoc-gen-go + - local: ["go", "tool", "google.golang.org/protobuf/cmd/protoc-gen-go"] out: ../wirepb opt: paths=source_relative - - local: protoc-gen-connect-go + - local: ["go", "tool", "connectrpc.com/connect/cmd/protoc-gen-connect-go"] out: ../wirepb opt: paths=source_relative diff --git a/internal/supervisor/supervisor.go b/internal/supervisor/supervisor.go index 202644f..7e32cee 100644 --- a/internal/supervisor/supervisor.go +++ b/internal/supervisor/supervisor.go @@ -9,6 +9,7 @@ package supervisor import ( "context" + "net" "os/signal" "syscall" ) @@ -29,12 +30,25 @@ type Supervisor interface { // WaitForStop blocks until the supervisor or OS requests shutdown. WaitForStop(ctx context.Context) context.Context + + // ActivationListeners returns named listeners passed by the service + // manager (e.g. systemd socket activation). Returns nil when the + // platform does not support listener inheritance. + ActivationListeners() (map[string][]net.Listener, error) +} + +var detectors []func() Supervisor + +func register(fn func() Supervisor) { + detectors = append(detectors, fn) } // Detect returns a Supervisor for the current platform. func Detect() Supervisor { - if detectSystemd() { - return &systemd{} + for _, fn := range detectors { + if s := fn(); s != nil { + return s + } } return &noop{} } @@ -52,3 +66,7 @@ func (*noop) WaitForStop(ctx context.Context) context.Context { _ = stop return ctx } + +func (*noop) ActivationListeners() (map[string][]net.Listener, error) { + return nil, nil +} diff --git a/internal/supervisor/systemd.go b/internal/supervisor/systemd.go index 63c1c0d..ac57659 100644 --- a/internal/supervisor/systemd.go +++ b/internal/supervisor/systemd.go @@ -1,6 +1,8 @@ // Copyright (c) 2026 Nikolay Govorov // SPDX-License-Identifier: AGPL-3.0-or-later +//go:build linux + package supervisor import ( @@ -12,15 +14,23 @@ import ( "syscall" "time" + "net" + + "github.com/coreos/go-systemd/v22/activation" "github.com/coreos/go-systemd/v22/daemon" ) -type systemd struct{} - -func detectSystemd() bool { - return os.Getenv("NOTIFY_SOCKET") != "" +func init() { + register(func() Supervisor { + if os.Getenv("NOTIFY_SOCKET") != "" { + return &systemd{} + } + return nil + }) } +type systemd struct{} + func (*systemd) WaitForStop(ctx context.Context) context.Context { ctx, stop := signal.NotifyContext(ctx, syscall.SIGTERM, syscall.SIGINT) _ = stop // stop allows you to cancel the observer, but it's not particularly useful @@ -67,3 +77,7 @@ func (*systemd) StartWatchdog(ctx context.Context) { } } } + +func (*systemd) ActivationListeners() (map[string][]net.Listener, error) { + return activation.ListenersWithNames() +} diff --git a/nfpm.yaml b/nfpm.yaml index 0b070cb..fa12a4a 100644 --- a/nfpm.yaml +++ b/nfpm.yaml @@ -13,22 +13,22 @@ maintainer: Nikolay Govorov description: Modern CI platform contents: - - src: ./LICENSE - dst: /usr/share/doc/mirum/LICENSE + - src: ./LICENSES/* + dst: /usr/share/doc/mirum/LICENSES/ - src: ./README.md dst: /usr/share/doc/mirum/README.md - - src: build/mirum-server + - src: build/tmp/mirum-server dst: /usr/local/bin/mirum-server file_info: mode: 0755 - - src: build/mirum-worker + - src: build/tmp/mirum-worker dst: /usr/local/bin/mirum-worker file_info: mode: 0755 - - src: build/mirum + - src: build/tmp/mirum dst: /usr/local/bin/mirum file_info: mode: 0755 -- Gilti