aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
Diffstat
-rw-r--r--.github/workflows/build.yml6+4 −2
-rw-r--r--Taskfile.yml102+78 −24
-rw-r--r--cmd/mirum-server/database.go3+2 −1
-rw-r--r--cmd/mirum-server/id.go3+2 −1
-rw-r--r--cmd/mirum-server/main.go7+3 −4
-rw-r--r--cmd/mirum-server/proto/buf.gen.yaml4+2 −2
-rw-r--r--cmd/mirum-server/web/lib/utils.ts1+1 −0
-rw-r--r--go.mod7+6 −1
-rw-r--r--internal/protocol/proto/buf.gen.yaml4+2 −2
-rw-r--r--internal/supervisor/supervisor.go22+20 −2
-rw-r--r--internal/supervisor/systemd.go22+18 −4
-rw-r--r--nfpm.yaml10+5 −5
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, unknown> | 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 <me@govorov.online>
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