-
Notifications
You must be signed in to change notification settings - Fork 30
Expand file tree
/
Copy pathMakefile
More file actions
361 lines (316 loc) · 15 KB
/
Copy pathMakefile
File metadata and controls
361 lines (316 loc) · 15 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
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
SHELL := /bin/bash
# Use system default locations for Go caches (avoid polluting project tmp/)
export GOCACHE := $(shell go env GOCACHE)
export GOMODCACHE := $(shell go env GOMODCACHE)
export GOPATH := $(shell go env GOPATH)
# Binaries are discovered under cmd/*
## Discover cmd/* directories but exclude embedctl\t@echo "Host build complete"
# Build TUI binary for host platform (fast - only rebuild when needed)
BINS := $(shell for d in cmd/*; do if [ -d "$$d" ]; then bn=$$(basename $$d); if [ "$$bn" != "embedctl" ]; then echo $$bn; fi; fi; done)
# Output directory
DIST := dist
# Platforms to build for in cross (Option A: single job builds all platforms)
PLATFORMS := linux/amd64 linux/arm64 darwin/amd64 darwin/arm64 windows/amd64 windows/arm64
GOLANGCI_LINT_VERSION := v2.12.2
FORGE_BUILD_TAGS ?= forge
.PHONY: all help fmt fmt-check imports-check vet lint test test-forge-harness modernize modernize-diff ci build cross checksums tools clean build-tui frontend openapi
.PHONY: sonar sonar-up sonar-down sonar-scan
.PHONY: dev dev-backend dev-frontend
all: build
help:
@echo "Available targets:"
@echo " make tools # install dev tools (goimports, golangci-lint)"
@echo " make fmt # format code with gofmt"
@echo " make fmt-check # check formatting"
@echo " make imports-check # check imports with goimports"
@echo " make vet # run go vet"
@echo " make lint # run golangci-lint"
@echo " make test # run tests with -race and generate coverage.out"
@echo " make test-forge-harness # run deterministic Forge harness scenario tests"
@echo " make modernize-diff # preview Go modernizer changes"
@echo " make modernize # apply Go modernizer changes"
@echo " make sonar-up # start local SonarQube (http://localhost:19000)"
@echo " make sonar-scan # run SonarScanner against local SonarQube"
@echo " make sonar # alias for sonar-scan"
@echo " make sonar-down # stop local SonarQube stack"
@echo " make build # build developer host platform binaries into $(DIST)/"
@echo " make build-manifold # build standard Manifold binary with Forge backend and embedded frontend"
@echo " make build-manifold-beta # build standard Manifold binary with beta UI links"
@echo " make build-agentd # build Manifold server without refreshing embedded frontend"
@echo " make build-agent # build only the one-shot agent CLI"
@echo " make frontend # install frontend deps, then build Vue.js assets"
@echo " make openapi # generate docs/openapi/openapi.json"
@echo " make cross # build all platforms (tar/zip) into $(DIST)/"
@echo " make checksums # generate SHA256 checksums for artifacts in $(DIST)/"
@echo " make ci # run CI checks (fmt-check, imports-check, vet, lint, test)"
@echo " make clean # clean $(DIST) and coverage.out"
@echo ""
@echo "Dev mode (hot-reload frontend + live backend):"
@echo " make dev # start backend + Vite dev server together (HMR, proxied API)"
@echo " make dev-backend # start backend only (dev build, proxies frontend to Vite)"
@echo " make dev-frontend # start Vite dev server only (proxies API to backend)"
@echo ""
@echo "Note: Go caches use system defaults (GOCACHE=$(GOCACHE), GOMODCACHE=$(GOMODCACHE))"
# Install developer tools
tools:
@echo "Installing goimports and golangci-lint..."
@echo "Installing goimports"
GOFLAGS= go install golang.org/x/tools/cmd/goimports@latest
@echo "Installing golangci-lint $(GOLANGCI_LINT_VERSION)"
GOFLAGS= go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@$(GOLANGCI_LINT_VERSION)
@echo "Done"
fmt:
gofmt -w .
fmt-check:
@unformatted=$$(gofmt -l .); if [ -n "$$unformatted" ]; then echo "gofmt needs to be run on:"; echo "$$unformatted"; exit 1; fi
imports-check:
@missing=$$(goimports -l .); if [ -n "$$missing" ]; then echo "goimports needs to be run on:"; echo "$$missing"; exit 1; fi
vet:
@echo "Running go vet (excluding tmp/)..."
@go vet $$(go list ./... | grep -v '/tmp/') 2>&1 | grep -v "error generated" | grep -v "^\s*|" | grep -v "^$$" || true
@echo "go vet completed"
lint:
@if ! command -v golangci-lint >/dev/null 2>&1; then \
echo "golangci-lint not found; run 'make tools' or install it in CI"; exit 1; \
fi
golangci-lint run --timeout=5m
test:
@echo "Running tests with race detector and coverage"
go test -race -coverprofile=coverage.out ./...
test-forge-harness:
@echo "Running deterministic Forge harness scenario tests"
go test ./internal/agent -run TestForgeHarnessScenarios
modernize-diff:
go fix -diff ./...
modernize:
go fix ./...
# Local SonarQube helpers (token created & revoked per scan)
SONAR_COMPOSE_FILE := develop/sonarqube/docker-compose.yml
SONAR_COMPOSE_PROJECT := manifold-sonar
SONAR_ENV_FILE ?= .env
SONAR_PROJECT_SETTINGS := develop/sonarqube/sonar-project.properties
# Non-secret defaults
SONAR_HOST_PORT ?= 19000
SONAR_PROJECT_KEY ?= manifold
sonar-up:
docker compose -f $(SONAR_COMPOSE_FILE) -p $(SONAR_COMPOSE_PROJECT) up -d
sonar-down:
docker compose -f $(SONAR_COMPOSE_FILE) -p $(SONAR_COMPOSE_PROJECT) down
sonar-scan: sonar-up
@set -euo pipefail; \
set -a; [ -f "$(SONAR_ENV_FILE)" ] && . "./$(SONAR_ENV_FILE)"; set +a; \
: "$${SONAR_ADMIN_USER:?Set SONAR_ADMIN_USER in $(SONAR_ENV_FILE)}"; \
: "$${SONAR_ADMIN_PASSWORD:?Set SONAR_ADMIN_PASSWORD in $(SONAR_ENV_FILE)}"; \
sonar_host_port="$${SONAR_HOST_PORT:-$(SONAR_HOST_PORT)}"; \
sonar_project_key="$${SONAR_PROJECT_KEY:-$(SONAR_PROJECT_KEY)}"; \
echo "Waiting for SonarQube status=UP on http://localhost:$${sonar_host_port} ..."; \
for i in $$(seq 1 90); do \
if curl -sf "http://localhost:$${sonar_host_port}/api/system/status" | grep -q '"status":"UP"'; then break; fi; \
sleep 2; \
done; \
if ! curl -sf "http://localhost:$${sonar_host_port}/api/system/status" | grep -q '"status":"UP"'; then \
echo "SonarQube did not reach status=UP in time."; \
echo "Try: make sonar-down && make sonar-up (and check Docker logs for manifold_sonarqube)."; \
exit 1; \
fi; \
token_name="manifold-local-scan-$$(date +%s)"; \
tmp_resp="$$(mktemp)"; \
cleanup() { rm -f "$$tmp_resp"; }; trap cleanup EXIT; \
http_code=$$(curl -sS -o "$$tmp_resp" -w "%{http_code}" -u "$${SONAR_ADMIN_USER}:$${SONAR_ADMIN_PASSWORD}" -X POST "http://localhost:$${sonar_host_port}/api/user_tokens/generate" --data-urlencode "name=$$token_name" || true); \
if [ "$$http_code" != "200" ]; then \
echo "Failed to generate Sonar token (HTTP $$http_code)."; \
echo "Check SONAR_ADMIN_USER / SONAR_ADMIN_PASSWORD in $(SONAR_ENV_FILE) and that SonarQube is healthy."; \
echo "Response:"; cat "$$tmp_resp"; \
exit 1; \
fi; \
sonar_token=$$(python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print(d.get("token", ""))' "$$tmp_resp"); \
if [ -z "$$sonar_token" ]; then \
echo "Token generation succeeded but response did not include a token."; \
echo "Response:"; cat "$$tmp_resp"; \
exit 1; \
fi; \
docker run --rm --network $(SONAR_COMPOSE_PROJECT)_default -v "$$PWD:/usr/src" -w /usr/src sonarsource/sonar-scanner-cli:latest -Dproject.settings="$(SONAR_PROJECT_SETTINGS)" -Dsonar.host.url=http://sonarqube:9000 -Dsonar.login="$$sonar_token"; \
curl -sf -u "$${SONAR_ADMIN_USER}:$${SONAR_ADMIN_PASSWORD}" -X POST "http://localhost:$${sonar_host_port}/api/user_tokens/revoke" --data-urlencode "name=$$token_name" >/dev/null || true; \
echo "Scan submitted. Open http://localhost:$${sonar_host_port}/dashboard?id=$${sonar_project_key}"
sonar: sonar-scan
ci: fmt-check imports-check vet lint test
@echo "CI checks passed"
# Build binaries for host platform. Builds each cmd/* directory (if present)
build: clean | $(DIST)
@echo "Building host platform binaries into $(DIST)/"
for b in $(BINS); do \
out_name=$$b; \
if [ "$$b" = "agentd" ]; then out_name=manifold; fi; \
out=$(DIST)/$$out_name; \
echo "Building $$b -> $$out"; \
go build -o "$$out" ./cmd/$$b; \
done
@echo "Host build complete"
# Cross compile all platforms and package them into $(DIST)/
# Option A: single job builds all platforms (do not run cross in parallel across jobs)
cross: clean | $(DIST)
@echo "Cross-building for: $(PLATFORMS)"
@echo "Note: CGO-dependent binaries will be skipped in cross-compilation if any are added later"
set -e
for plat in $(PLATFORMS); do \
os=$${plat%%/*}; arch=$${plat##*/}; \
for b in $(BINS); do \
mkdir -p $(DIST)/$${os}_$${arch}; \
outname=$${b}; \
if [ "$$b" = "agentd" ]; then outname=manifold; fi; \
outfile=$${outname}; \
if [ "$${os}" = "windows" ]; then outfile=$${outname}.exe; fi; \
echo "Building $$b for $${os}/$${arch} -> $(DIST)/$${os}_$${arch}/$$outfile"; \
CGO_ENABLED=0 GOOS=$${os} GOARCH=$${arch} go build -o "$(DIST)/$${os}_$${arch}/$$outfile" ./cmd/$$b; \
done; \
# Package per-platform directory
pushd $(DIST) >/dev/null; \
if [ "$${os}" = "windows" ]; then \
zipname="$${os}-$${arch}.zip"; \
zip -r "$$zipname" "$${os}_$${arch}" >/dev/null; \
sha256sum "$$zipname" > "$${zipname}.sha256"; \
echo "Packaged $$zipname"; \
# remove per-platform dir after packaging
rm -rf "$${os}_$${arch}"; \
else \
tarname="$${os}-$${arch}.tar.gz"; \
tar -czf "$$tarname" "$${os}_$${arch}"; \
sha256sum "$$tarname" > "$${tarname}.sha256"; \
echo "Packaged $$tarname"; \
rm -rf "$${os}_$${arch}"; \
fi; \
popd >/dev/null; \
done
@echo "Cross-build complete. Artifacts are in $(DIST)/"
# Generate a combined checksums file sorted for reproducibility
checksums:
@echo "Generating combined checksums in $(DIST)/checksums.txt"
@cd $(DIST) && ls -1 | sort | grep -v "checksums.txt" | xargs -I{} sh -c 'if [ -f "{}" ]; then sha256sum "{}"; fi' > checksums.txt
@echo "Checksums written to $(DIST)/checksums.txt"
# Ensure dist exists
$(DIST):
@mkdir -p $(DIST)
clean:
rm -rf $(DIST) coverage.out || true
@echo "Cleaned"
.PHONY: build-agentd
build-agentd: | $(DIST)
@echo "Building Manifold server into $(DIST)/"
@rm -f $(DIST)/agentd
go build -o $(DIST)/manifold ./cmd/agentd
@echo "Manifold server build complete"
.PHONY: build-agent
build-agent: | $(DIST)
@echo "Building agent only into $(DIST)/"
go build -o $(DIST)/agent ./cmd/agent
@echo "agent build complete"
FRONTEND_DIR := web/agentd-ui
FRONTEND_SRC_DIST := $(FRONTEND_DIR)/dist
FRONTEND_EMBED_DIR := internal/webui/dist
PNPM := pnpm
FEATURE_GATE ?= stable
.PHONY: build-manifold build-manifold-beta
build-manifold: frontend | $(DIST)
@echo "Building standard Manifold with Forge backend and embedded frontend using tags '$(FORGE_BUILD_TAGS)' -> $(DIST)/manifold"
@rm -f $(DIST)/agentd
go build -tags "$(FORGE_BUILD_TAGS)" -o $(DIST)/manifold ./cmd/agentd
@echo "Standard Manifold build complete"
build-manifold-beta: FEATURE_GATE := beta
build-manifold-beta: build-manifold
# Build web UI server
build-webui:
@echo "Building webui into $(DIST)/"
go build -o $(DIST)/webui ./cmd/webui
.PHONY: frontend
frontend:
@if ! command -v $(PNPM) >/dev/null 2>&1; then \
echo "pnpm not found; install it from https://pnpm.io/"; \
exit 1; \
fi
@echo "Installing frontend dependencies in $(FRONTEND_DIR)"
cd $(FRONTEND_DIR) && $(PNPM) install --frozen-lockfile
@echo "Building frontend in $(FRONTEND_DIR) with feature gate '$(FEATURE_GATE)'"
cd $(FRONTEND_DIR) && VITE_MANIFOLD_FEATURE_GATE=$(FEATURE_GATE) $(PNPM) run build
@if [ ! -d "$(FRONTEND_SRC_DIST)" ]; then \
echo "Frontend build output not found at $(FRONTEND_SRC_DIST)"; \
exit 1; \
fi
rm -rf $(FRONTEND_EMBED_DIR)
mkdir -p $(FRONTEND_EMBED_DIR)
cp -R $(FRONTEND_SRC_DIST)/. $(FRONTEND_EMBED_DIR)/
@echo "Frontend assets copied into $(FRONTEND_EMBED_DIR)"
.PHONY: openapi
openapi:
go run ./cmd/openapi -out docs/openapi/openapi.json -server http://localhost:32180
# ─── Dev mode ────────────────────────────────────────────────────────────────
# The dev targets run the Go backend and the Vite dev server separately so you
# get full HMR (Hot Module Replacement) while the frontend stays connected to
# the real backend API.
#
# Architecture:
# • Backend is built with -tags dev which compiles assets_dev.go instead of
# assets_embed.go. This avoids needing a pre-built internal/webui/dist —
# the backend reads frontend files directly from the filesystem.
# • When FRONTEND_DEV_PROXY is set, the backend reverse-proxies all non-API
# requests to the Vite dev server (default http://127.0.0.1:5177).
# • When VITE_DEV_SERVER_PROXY is set, Vite proxies /api, /stt, /audio, /auth
# requests to the backend (default http://127.0.0.1:32180).
#
# Usage:
# make dev # starts both backend and frontend in one shell
# make dev-backend # backend only (use if you already have Vite running)
# make dev-frontend # frontend only (use if you already have backend running)
VITE_PORT ?= 5177
BACKEND_PORT ?= 32180
VITE_HOST ?= 127.0.0.1
FRONTEND_DEV_PROXY ?= http://$(VITE_HOST):$(VITE_PORT)
VITE_DEV_SERVER_PROXY ?= http://127.0.0.1:$(BACKEND_PORT)
# Start both backend and Vite dev server. Vite is launched first in the
# background, then the backend runs in the foreground. Ctrl-C kills both.
dev:
@set -e; \
frontend_pid=""; \
cleanup() { \
status=$$?; \
if [ -n "$$frontend_pid" ]; then \
kill "$$frontend_pid" 2>/dev/null || true; \
wait "$$frontend_pid" 2>/dev/null || true; \
fi; \
exit "$$status"; \
}; \
trap cleanup EXIT INT TERM; \
$(MAKE) --no-print-directory dev-frontend & \
frontend_pid=$$!; \
echo "Waiting for Vite dev server on $(VITE_HOST):$(VITE_PORT)..."; \
for _ in $$(seq 1 120); do \
if (echo > /dev/tcp/$(VITE_HOST)/$(VITE_PORT)) >/dev/null 2>&1; then \
break; \
fi; \
if ! kill -0 "$$frontend_pid" 2>/dev/null; then \
wait "$$frontend_pid"; \
exit $$?; \
fi; \
sleep 0.5; \
done; \
if ! (echo > /dev/tcp/$(VITE_HOST)/$(VITE_PORT)) >/dev/null 2>&1; then \
echo "Vite dev server did not become ready on $(VITE_HOST):$(VITE_PORT)"; \
exit 1; \
fi; \
$(MAKE) --no-print-directory dev-backend
# Backend: dev build + run with frontend dev proxy enabled.
dev-backend: | $(DIST)
@echo "Building Manifold backend (dev tags) -> $(DIST)/manifold"
@rm -f $(DIST)/agentd
go build -tags "dev" -o $(DIST)/manifold ./cmd/agentd
@echo "Starting backend on :$(BACKEND_PORT) (frontend proxy -> $(FRONTEND_DEV_PROXY))"
FRONTEND_DEV_PROXY=$(FRONTEND_DEV_PROXY) $(DIST)/manifold
# Frontend: run Vite dev server with API proxy pointing to backend.
dev-frontend:
@if ! command -v $(PNPM) > /dev/null 2>&1; then \
echo "pnpm not found; install it from https://pnpm.io/"; \
exit 1; \
fi
@echo "Installing frontend dependencies (if needed)..."
@cd $(FRONTEND_DIR) && $(PNPM) install --frozen-lockfile 2>/dev/null || $(PNPM) install
@echo "Starting Vite dev server on :$(VITE_PORT) (API proxy -> $(VITE_DEV_SERVER_PROXY))"
cd $(FRONTEND_DIR) && VITE_DEV_SERVER_PROXY=$(VITE_DEV_SERVER_PROXY) $(PNPM) run dev --host $(VITE_HOST) --port $(VITE_PORT) --strictPort