-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathMakefile
More file actions
261 lines (221 loc) · 11.1 KB
/
Copy pathMakefile
File metadata and controls
261 lines (221 loc) · 11.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
# Infer PROJECT_NAME and PROJECT_ORG from git remote or directory path (NEVER hardcode)
PROJECT_NAME := $(shell git remote get-url origin 2>/dev/null | sed -E 's|.*/([^/]+)(\.git)?$$|\1|' || basename "$$(pwd)")
PROJECT_ORG := $(shell git remote get-url origin 2>/dev/null | sed -E 's|.*/([^/]+)/[^/]+(\.git)?$$|\1|' || basename "$$(dirname "$$(pwd)")")
# Frozen forever, even across a project rename — sourced from IDEA.md, falls back to PROJECT_NAME on first-time setup
INTERNAL_NAME := $(shell grep -E '^internal_name:[[:space:]]*.+$$' IDEA.md 2>/dev/null | sed -E 's/^internal_name:[[:space:]]*//' || echo "$(PROJECT_NAME)")
# Version precedence: release.txt (wins if it exists) > VERSION env var > "devel" fallback
VERSION := $(shell cat release.txt 2>/dev/null || echo "$${VERSION:-devel}")
# Build info - BUILD_EPOCH is the single captured time source
# Unix build timestamp (seconds, UTC) - used by the updater's daily-channel check
BUILD_EPOCH := $(shell date -u +%s)
# Derived from BUILD_EPOCH - ISO 8601 UTC, e.g. "2025-12-04T13:05:13Z"
# Used only for Docker OCI labels (org.opencontainers.image.created); not an ldflag
BUILD_DATE := $(shell date -u -d @$(BUILD_EPOCH) +"%Y-%m-%dT%H:%M:%SZ")
COMMIT_ID := $(shell git rev-parse --short=7 HEAD 2>/dev/null || echo "N/A")
# COMMIT_ID used directly - no VCS_REF alias
# Official site URL (OPTIONAL - never guess or assume)
# Sources (in order of precedence):
# 1. File: site.txt in project root (single line, URL only)
# 2. Environment variable: OFFICIAL_SITE=https://example.com
# 3. Empty (self-hosted projects - users must use --server flag)
# NEVER infer from project name, domain, or any other source
OFFICIAL_SITE := $(shell [ -f site.txt ] && cat site.txt || echo "${OFFICIAL_SITE:-}")
# Linker flags to embed build info
# BuildDate is NOT embedded - it is derived from BuildEpoch at process start
LDFLAGS := -s -w \
-X 'main.Version=$(VERSION)' \
-X 'main.CommitID=$(COMMIT_ID)' \
-X 'main.BuildEpoch=$(BUILD_EPOCH)' \
-X 'main.OfficialSite=$(OFFICIAL_SITE)'
# Directories
BINDIR := binaries
RELDIR := releases
# Go directories (persistent across builds)
# GO_CACHE: module download cache — safe to share across concurrent builds (Go file-locks writes)
# GO_BUILD: compile cache — scoped per project to prevent corruption when multiple projects build concurrently
GO_CACHE ?= $(HOME)/go/pkg/mod
GO_BUILD ?= $(HOME)/.cache/go-build/$(PROJECT_NAME)
# Build targets - all 8 platforms
PLATFORMS ?= linux/amd64,linux/arm64,darwin/amd64,darwin/arm64,windows/amd64,windows/arm64,freebsd/amd64,freebsd/arm64
# Docker - Set REGISTRY based on your platform (ghcr.io, registry.gitlab.com, git.example.com)
REGISTRY ?= ghcr.io/$(PROJECT_ORG)/$(PROJECT_NAME)
# Resource limits for build containers
DOCKER_MEM ?= 4g
DOCKER_CPUS ?= 2
# GO_DOCKER_RUN: shared docker run prefix (no image) so targets can add mounts before the image
GO_DOCKER_RUN := docker run --rm \
--name $(PROJECT_NAME)-$$(tr -dc 'a-z0-9' </dev/urandom | head -c8) \
--memory=$(DOCKER_MEM) --cpus=$(DOCKER_CPUS) \
-v $(PWD):/app \
-v $(GO_CACHE):/usr/local/share/go/pkg/mod \
-v $(GO_BUILD):/usr/local/share/go/cache \
-w /app \
-e CGO_ENABLED=0 \
-e GOFLAGS=-buildvcs=false
GO_DOCKER := $(GO_DOCKER_RUN) casjaysdev/go:latest
# CGO_ENABLED=0 and GOFLAGS=-buildvcs=false are casjaysdev/go:latest defaults; set explicitly for clarity
.PHONY: build local release docker test dev clean
# =============================================================================
# BUILD - Build all platforms + local binary (via Docker with cached modules)
# =============================================================================
build: clean
@mkdir -p $(BINDIR) $(GO_CACHE) $(GO_BUILD)
@echo "Building version $(VERSION)..."
# Tidy and download modules
@echo "Tidying and downloading Go modules..."
@$(GO_DOCKER) go mod tidy
@$(GO_DOCKER) go mod download
# Build for local OS/ARCH
@echo "Building local binary..."
@$(GO_DOCKER) sh -c "GOOS=$$(go env GOOS) GOARCH=$$(go env GOARCH) \
go build -buildvcs=false -trimpath -ldflags \"$(LDFLAGS)\" -o $(BINDIR)/$(PROJECT_NAME) ./src"
# Build server for all platforms (PLATFORMS is comma-separated; split for the loop)
@for platform in $$(echo "$(PLATFORMS)" | tr ',' ' '); do \
OS=$${platform%/*}; \
ARCH=$${platform#*/}; \
OUTPUT=$(BINDIR)/$(PROJECT_NAME)-$$OS-$$ARCH; \
[ "$$OS" = "windows" ] && OUTPUT=$$OUTPUT.exe; \
echo "Building server $$OS/$$ARCH..."; \
$(GO_DOCKER) sh -c "GOOS=$$OS GOARCH=$$ARCH \
go build -buildvcs=false -trimpath -ldflags \"$(LDFLAGS)\" \
-o $$OUTPUT ./src" || exit 1; \
done
# Build CLI for all platforms (if exists)
@if [ -d "src/client" ]; then \
for platform in $$(echo "$(PLATFORMS)" | tr ',' ' '); do \
OS=$${platform%/*}; \
ARCH=$${platform#*/}; \
OUTPUT=$(BINDIR)/$(PROJECT_NAME)-cli-$$OS-$$ARCH; \
[ "$$OS" = "windows" ] && OUTPUT=$$OUTPUT.exe; \
echo "Building CLI $$OS/$$ARCH..."; \
$(GO_DOCKER) sh -c "GOOS=$$OS GOARCH=$$ARCH \
go build -buildvcs=false -trimpath -ldflags \"$(LDFLAGS)\" \
-o $$OUTPUT ./src/client" || exit 1; \
done; \
fi
@echo "Build complete: $(BINDIR)/"
# =============================================================================
# LOCAL - Build local binaries only (fast development builds)
# =============================================================================
local: clean
@mkdir -p $(BINDIR) $(GO_CACHE) $(GO_BUILD)
@echo "Building local binaries version $(VERSION)..."
# Tidy and download modules
@echo "Tidying and downloading Go modules..."
@$(GO_DOCKER) go mod tidy
@$(GO_DOCKER) go mod download
# Build server binary
@echo "Building $(PROJECT_NAME)..."
@$(GO_DOCKER) sh -c "GOOS=$$(go env GOOS) GOARCH=$$(go env GOARCH) \
go build -buildvcs=false -trimpath -ldflags \"$(LDFLAGS)\" -o $(BINDIR)/$(PROJECT_NAME) ./src"
# Build CLI binary (if exists)
@if [ -d "src/client" ]; then \
echo "Building $(PROJECT_NAME)-cli..."; \
$(GO_DOCKER) sh -c "GOOS=$$(go env GOOS) GOARCH=$$(go env GOARCH) \
go build -buildvcs=false -trimpath -ldflags \"$(LDFLAGS)\" -o $(BINDIR)/$(PROJECT_NAME)-cli ./src/client"; \
fi
@echo "Local build complete: $(BINDIR)/"
# =============================================================================
# RELEASE - Manual local release (stable only)
# =============================================================================
release: build
@mkdir -p $(RELDIR)
@echo "Preparing release $(VERSION)..."
# Create version.txt
@echo "$(VERSION)" > $(RELDIR)/version.txt
# Copy binaries to releases (strip if needed)
@for f in $(BINDIR)/$(PROJECT_NAME)-*; do \
[ -f "$$f" ] || continue; \
strip "$$f" 2>/dev/null || true; \
cp "$$f" $(RELDIR)/; \
done
# Create source archive (exclude VCS and build artifacts)
@tar --exclude='.git' --exclude='.github' --exclude='.gitea' \
--exclude='binaries' --exclude='releases' --exclude='*.tar.gz' \
-czf $(RELDIR)/$(PROJECT_NAME)-$(VERSION)-source.tar.gz .
# Delete existing release/tag if exists
@gh release delete $(VERSION) --yes 2>/dev/null || true
@git tag -d $(VERSION) 2>/dev/null || true
@git push origin :refs/tags/$(VERSION) 2>/dev/null || true
# Create new release (stable)
@gh release create $(VERSION) $(RELDIR)/* \
--title "$(PROJECT_NAME) $(VERSION)" \
--notes "Release $(VERSION)" \
--latest
@echo "Release complete: $(VERSION)"
# =============================================================================
# DOCKER - Build and push container to registry (set REGISTRY env var)
# =============================================================================
# Uses multi-stage Dockerfile - Go compilation happens inside Docker
# No pre-built binaries needed
docker:
@echo "Building Docker image $(VERSION)..."
# Ensure buildx is available
@docker buildx version > /dev/null 2>&1 || (echo "docker buildx required" && exit 1)
# Create/use builder
@docker buildx create --name $(PROJECT_NAME)-builder --use 2>/dev/null || \
docker buildx use $(PROJECT_NAME)-builder
# Build multi-arch and push (buildx multi-arch images must be pushed; set REGISTRY first)
@docker buildx build \
-f docker/Dockerfile \
--platform linux/amd64,linux/arm64 \
--push \
--build-arg VERSION="$(VERSION)" \
--build-arg BUILD_DATE="$(BUILD_DATE)" \
--build-arg BUILD_EPOCH="$(BUILD_EPOCH)" \
--build-arg COMMIT_ID="$(COMMIT_ID)" \
-t $(REGISTRY):$(VERSION) \
-t $(REGISTRY):latest \
.
@echo "Docker build complete: $(REGISTRY):$(VERSION)"
# =============================================================================
# TEST - Run all tests with coverage enforcement (via Docker)
# =============================================================================
test:
@mkdir -p $(GO_CACHE) $(GO_BUILD)
@echo "Running tests with coverage..."
@$(GO_DOCKER) sh -c " \
mkdir -p \"\$${TMPDIR:-/tmp}/$(PROJECT_ORG)\" && \
COVDIR=\$$(mktemp -d \"\$${TMPDIR:-/tmp}/$(PROJECT_ORG)/$(INTERNAL_NAME)-XXXXXX\") && \
go mod download && \
go test -v -cover -coverprofile=\$$COVDIR/coverage.out ./... && \
COVERAGE=\$$(go tool cover -func=\$$COVDIR/coverage.out | grep total | awk '{print \$$3}' | sed 's/%//') && \
echo \"Coverage: \$$COVERAGE%\" && \
if [ \$$(echo \"\$$COVERAGE < 60\" | bc -l) -eq 1 ]; then \
echo \"ERROR: Coverage is \$$COVERAGE%, must be >= 60%\"; exit 1; \
fi && \
echo \"Tests complete - Coverage: \$$COVERAGE% (>= 60% required) ✓\""
# =============================================================================
# Coverage gates:
# - All Go projects: 60% minimum (go test -cover must report >= 60.0%)
# - Override upward in IDEA.md (## Project variables -> coverage_minimum: 80)
# when higher coverage is appropriate. Never override downward.
# - CLI tools and libraries: same 60% floor.
# Coverage runs in CI on every push and in `make test` locally.
# Never skip with -short or -count=0. No //go:coverage ignore workarounds.
# =============================================================================
# =============================================================================
# DEV - Quick build for local development/testing (to random temp dir)
# =============================================================================
# Fast: local platform only, no ldflags, random temp dir for isolation
# Builds server + CLI (if they exist)
dev:
@mkdir -p $(GO_CACHE) $(GO_BUILD)
@$(GO_DOCKER) go mod tidy
@mkdir -p "$${TMPDIR:-/tmp}/$(PROJECT_ORG)" && \
BUILD_DIR=$$(mktemp -d "$${TMPDIR:-/tmp}/$(PROJECT_ORG)/$(INTERNAL_NAME)-XXXXXX") && \
echo "Quick dev build to $$BUILD_DIR..." && \
$(GO_DOCKER_RUN) -v $$BUILD_DIR:/build casjaysdev/go:latest \
go build -buildvcs=false -o /build/$(PROJECT_NAME) ./src && \
echo "Built: $$BUILD_DIR/$(PROJECT_NAME)" && \
if [ -d "src/client" ]; then \
$(GO_DOCKER_RUN) -v $$BUILD_DIR:/build casjaysdev/go:latest \
go build -buildvcs=false -o /build/$(PROJECT_NAME)-cli ./src/client && \
echo "Built: $$BUILD_DIR/$(PROJECT_NAME)-cli"; \
fi && \
echo "Test: docker run --rm --name $(PROJECT_NAME)-test -v $$BUILD_DIR:/app alpine:latest /app/$(PROJECT_NAME) --help"
# =============================================================================
# CLEAN - Remove build artifacts
# =============================================================================
clean:
@rm -rf $(BINDIR) $(RELDIR)