2026-08-10 18:14:49 -04:00
|
|
|
.PHONY: frontend build dev-frontend dev-backend dev-proxy dev-transcode lint test test-go test-web embed-stub clean jellyfin-web migrate-continuum-check verify-local-paths install-hooks migrate-create migrate-validate migrate-status migrate-up migrate-down-to settings-bindings verify-settings-bindings verify-settings-bindings-web verify-settings-bindings-all playback-fixtures verify-playback-fixtures
|
2026-05-22 20:26:11 -04:00
|
|
|
|
|
|
|
|
GIT_COMMON_DIR := $(strip $(shell git rev-parse --git-common-dir 2>/dev/null))
|
|
|
|
|
MAIN_CHECKOUT_ROOT := $(if $(GIT_COMMON_DIR),$(abspath $(GIT_COMMON_DIR)/..))
|
|
|
|
|
SHARED_MAKEFILE_LOCAL := $(if $(GIT_COMMON_DIR),$(abspath $(GIT_COMMON_DIR)/../Makefile.local))
|
|
|
|
|
DEFAULT_PLUGIN_SDK_DIR := $(abspath ../silo-plugin-sdk)
|
|
|
|
|
SHARED_PLUGIN_SDK_DIR := $(if $(MAIN_CHECKOUT_ROOT),$(abspath $(MAIN_CHECKOUT_ROOT)/../silo-plugin-sdk))
|
2026-06-06 23:51:30 -04:00
|
|
|
GOOSE := go run github.com/pressly/goose/v3/cmd/goose@v3.27.1
|
|
|
|
|
GOOSE_DIR := migrations/sql
|
|
|
|
|
ENV_FILE ?= .env
|
2026-05-22 20:26:11 -04:00
|
|
|
|
|
|
|
|
ifneq ($(wildcard $(DEFAULT_PLUGIN_SDK_DIR)),)
|
|
|
|
|
DEV_PLUGIN_SDK_DIR ?= $(DEFAULT_PLUGIN_SDK_DIR)
|
|
|
|
|
else ifneq ($(wildcard $(SHARED_PLUGIN_SDK_DIR)),)
|
|
|
|
|
DEV_PLUGIN_SDK_DIR ?= $(SHARED_PLUGIN_SDK_DIR)
|
|
|
|
|
endif
|
|
|
|
|
|
2026-06-15 06:34:08 -07:00
|
|
|
JELLYFIN_WEB_INSTALL_DIR ?= .local/compat/jellyfin-web
|
|
|
|
|
JELLYFIN_WEB_VERSION ?= 10.11.6
|
2026-05-22 20:26:11 -04:00
|
|
|
|
2026-05-28 18:46:56 -04:00
|
|
|
# Build version stamping: inject the git revision so the admin Build panel shows a
|
|
|
|
|
# version even when Go's VCS metadata isn't embedded (mirrors the Dockerfile ldflags).
|
|
|
|
|
BUILDINFO_PKG := github.com/Silo-Server/silo-server/internal/buildinfo
|
|
|
|
|
BUILD_REVISION ?= $(shell git rev-parse HEAD 2>/dev/null)
|
|
|
|
|
BUILD_DIRTY ?= $(shell test -n "$$(git status --porcelain 2>/dev/null)" && echo true || echo false)
|
|
|
|
|
GO_LDFLAGS := -X $(BUILDINFO_PKG).revisionOverride=$(BUILD_REVISION) -X $(BUILDINFO_PKG).dirtyOverride=$(BUILD_DIRTY)
|
|
|
|
|
|
2026-05-22 20:26:11 -04:00
|
|
|
# Build the frontend (requires pnpm)
|
|
|
|
|
frontend:
|
|
|
|
|
cd web && pnpm install --frozen-lockfile && pnpm run build
|
|
|
|
|
|
|
|
|
|
# Build the Go binary (depends on frontend)
|
|
|
|
|
build: frontend
|
2026-05-28 18:46:56 -04:00
|
|
|
go build -ldflags "$(GO_LDFLAGS)" -o silo ./cmd/silo/
|
2026-05-22 20:26:11 -04:00
|
|
|
|
|
|
|
|
# Run frontend dev server (proxies API to localhost:8080)
|
|
|
|
|
dev-frontend:
|
|
|
|
|
cd web && pnpm run dev
|
|
|
|
|
|
|
|
|
|
# Run the Go backend (integrated mode)
|
|
|
|
|
dev-backend:
|
|
|
|
|
go run ./cmd/silo/
|
|
|
|
|
|
|
|
|
|
# Run a proxy node (stateless stream proxy, no DB required)
|
|
|
|
|
dev-proxy:
|
|
|
|
|
go run ./cmd/silo/ --mode=proxy
|
|
|
|
|
|
|
|
|
|
# Run a transcode node (HLS transcode worker, no DB required)
|
|
|
|
|
dev-transcode:
|
|
|
|
|
go run ./cmd/silo/ --mode=transcode
|
|
|
|
|
|
|
|
|
|
# Lint Go and frontend code
|
|
|
|
|
lint:
|
|
|
|
|
golangci-lint run
|
|
|
|
|
cd web && pnpm run lint
|
|
|
|
|
|
2026-07-30 10:52:41 -04:00
|
|
|
# Frontend test files that fail on main today. This list is shrink-only: delete
|
|
|
|
|
# an entry along with its fix, and never extend it to land a change. The Go
|
|
|
|
|
# suite has no equivalent — a Go test that cannot pass yet carries a t.Skip and
|
|
|
|
|
# its reason in the source, where whoever reads the test finds it.
|
|
|
|
|
WEBTEST_KNOWN_FAILURES := \
|
|
|
|
|
--exclude src/pages/Catalog.test.tsx \
|
|
|
|
|
--exclude src/pages/ItemDetail/SeasonContent.test.tsx \
|
|
|
|
|
--exclude src/pages/LibraryRecommended.test.tsx \
|
|
|
|
|
--exclude src/pages/setup-wizard/steps/ServerStorageStep.test.tsx \
|
|
|
|
|
--exclude src/player/hooks/useASSSubtitles.test.tsx
|
|
|
|
|
|
|
|
|
|
# The Go binary embeds the built frontend, so every Go build and test needs
|
|
|
|
|
# web/dist to exist. Tests never serve it, so a placeholder is enough; `make
|
|
|
|
|
# build` still builds the real bundle.
|
|
|
|
|
embed-stub:
|
|
|
|
|
@mkdir -p web/dist
|
|
|
|
|
@[ -e web/dist/index.html ] || printf '<!doctype html>\n' > web/dist/index.html
|
|
|
|
|
|
|
|
|
|
# Run the Go and frontend test suites.
|
|
|
|
|
test: test-go test-web
|
|
|
|
|
|
|
|
|
|
test-go: embed-stub
|
|
|
|
|
go test ./...
|
|
|
|
|
|
|
|
|
|
test-web:
|
|
|
|
|
cd web && pnpm exec vitest run $(WEBTEST_KNOWN_FAILURES)
|
|
|
|
|
|
|
|
|
|
# Regenerate the settings-contract bindings for every language.
|
|
|
|
|
#
|
|
|
|
|
# The client repos are siblings of this one (see CLAUDE.md); a missing checkout
|
|
|
|
|
# is skipped rather than failing, so a server-only developer can still run this.
|
|
|
|
|
#
|
|
|
|
|
# The conformance fixture (contracts/settings/v1/conformance.json) travels with
|
|
|
|
|
# the bindings: the vendored copy in web/src/lib is what the web runner reads.
|
|
|
|
|
# The Kotlin and Swift copies land together with their runners in the client
|
|
|
|
|
# repos, which will pick their own test-resource paths.
|
|
|
|
|
SILO_ANDROID_DIR ?= $(abspath ../silo-android)
|
|
|
|
|
SILO_APPLE_DIR ?= $(abspath ../silo-apple)
|
|
|
|
|
|
|
|
|
|
settings-bindings:
|
|
|
|
|
@mkdir -p internal/settingskeys
|
|
|
|
|
go run ./cmd/settingsgen -lang go -out internal/settingskeys/keys.go
|
|
|
|
|
gofmt -w internal/settingskeys/keys.go
|
|
|
|
|
go run ./cmd/settingsgen -lang ts -out web/src/lib/settingsContract.ts
|
|
|
|
|
@cd web && pnpm exec prettier --write src/lib/settingsContract.ts >/dev/null
|
|
|
|
|
cp contracts/settings/v1/conformance.json web/src/lib/settingsConformance.json
|
|
|
|
|
@if [ -d "$(SILO_ANDROID_DIR)" ]; then \
|
|
|
|
|
go run ./cmd/settingsgen -lang kotlin \
|
|
|
|
|
-out "$(SILO_ANDROID_DIR)/shared/src/commonMain/kotlin/org/siloserver/silo/model/settings/SettingKeys.kt"; \
|
|
|
|
|
echo "wrote Kotlin bindings to $(SILO_ANDROID_DIR)"; \
|
|
|
|
|
else \
|
|
|
|
|
echo "skipping Kotlin: $(SILO_ANDROID_DIR) not checked out"; \
|
|
|
|
|
fi
|
|
|
|
|
@if [ -d "$(SILO_APPLE_DIR)" ]; then \
|
|
|
|
|
go run ./cmd/settingsgen -lang swift \
|
|
|
|
|
-out "$(SILO_APPLE_DIR)/iosApp/iosApp/Networking/SettingKeys.generated.swift"; \
|
|
|
|
|
echo "wrote Swift bindings to $(SILO_APPLE_DIR)"; \
|
|
|
|
|
else \
|
|
|
|
|
echo "skipping Swift: $(SILO_APPLE_DIR) not checked out"; \
|
|
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
# Fail when the committed bindings disagree with the manifest, so a manifest
|
|
|
|
|
# change cannot merge without regenerating what every client reads.
|
|
|
|
|
#
|
|
|
|
|
# Split in two because the generated TypeScript is compared after prettier, and
|
|
|
|
|
# only the Web CI job has pnpm: the Go job runs this target, the Web job runs
|
|
|
|
|
# verify-settings-bindings-web. Locally, `verify-settings-bindings-all` is both.
|
|
|
|
|
verify-settings-bindings:
|
|
|
|
|
@CHECK_DIR=$$(mktemp -d) && trap 'rm -rf "$$CHECK_DIR"' EXIT && \
|
|
|
|
|
go run ./cmd/settingsgen -lang go | gofmt > "$$CHECK_DIR/keys.go" && \
|
|
|
|
|
diff -u internal/settingskeys/keys.go "$$CHECK_DIR/keys.go" \
|
|
|
|
|
|| { echo "::error::internal/settingskeys/keys.go is stale; run make settings-bindings"; exit 1; }
|
|
|
|
|
@diff -u web/src/lib/settingsConformance.json contracts/settings/v1/conformance.json \
|
|
|
|
|
|| { echo "::error::web/src/lib/settingsConformance.json is stale; run make settings-bindings"; exit 1; }
|
|
|
|
|
@echo "settings bindings are current"
|
|
|
|
|
|
|
|
|
|
# The half that needs pnpm: regenerate the web binding, format it the way the
|
|
|
|
|
# bindings target does, and compare. Without this a manifest change could merge
|
|
|
|
|
# with a stale settingsContract.ts, which is what every web control renders from.
|
|
|
|
|
verify-settings-bindings-web:
|
|
|
|
|
@CHECK_DIR=$$(mktemp -d) && trap 'rm -rf "$$CHECK_DIR"' EXIT && \
|
|
|
|
|
go run ./cmd/settingsgen -lang ts -out "$$CHECK_DIR/settingsContract.ts" && \
|
|
|
|
|
cd web && pnpm exec prettier --log-level silent --config .prettierrc \
|
|
|
|
|
--write "$$CHECK_DIR/settingsContract.ts" && cd .. && \
|
|
|
|
|
diff -u web/src/lib/settingsContract.ts "$$CHECK_DIR/settingsContract.ts" \
|
|
|
|
|
|| { echo "::error::web/src/lib/settingsContract.ts is stale; run make settings-bindings"; exit 1; }
|
|
|
|
|
@echo "web settings binding is current"
|
|
|
|
|
|
|
|
|
|
verify-settings-bindings-all: verify-settings-bindings verify-settings-bindings-web
|
|
|
|
|
|
2026-08-10 18:14:49 -04:00
|
|
|
# Regenerate the protocol-v3 golden contract fixtures from the live types and planner.
|
|
|
|
|
#
|
|
|
|
|
# The server owns the playback contract and the clients prove conformance
|
|
|
|
|
# against these bodies, so they are only trustworthy while the code that emits
|
|
|
|
|
# them is the code that serves traffic. Editing one by hand instead of running
|
|
|
|
|
# this would let the contract and the implementation drift apart in silence.
|
|
|
|
|
PLAYBACK_FIXTURE_DIR := internal/playback/testdata/protocol_v3
|
|
|
|
|
PLAYBACK_SCHEMA_FIXTURE_DIR := docs/design/schemas/playback-v3/v3/fixtures/valid
|
|
|
|
|
PLAYBACK_WIRE_FIXTURES := start_request.json replan_request.json decision_response.json capability_response.json error_response.json route_event.json
|
|
|
|
|
|
|
|
|
|
playback-fixtures:
|
|
|
|
|
go run ./cmd/playbackfixtures -out $(PLAYBACK_FIXTURE_DIR)
|
|
|
|
|
@set -e; for fixture in $(PLAYBACK_WIRE_FIXTURES); do \
|
|
|
|
|
cp "$(PLAYBACK_FIXTURE_DIR)/$$fixture" "$(PLAYBACK_SCHEMA_FIXTURE_DIR)/$$fixture"; \
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
# Fail when the committed fixtures disagree with the contract types. A change
|
|
|
|
|
# that does not regenerate leaves every client testing against a body the server
|
|
|
|
|
# no longer produces, which is exactly the drift the fixtures exist to catch.
|
|
|
|
|
verify-playback-fixtures:
|
|
|
|
|
@CHECK_DIR=$$(mktemp -d) && trap 'rm -rf "$$CHECK_DIR"' EXIT && \
|
|
|
|
|
go run ./cmd/playbackfixtures -out "$$CHECK_DIR" && \
|
|
|
|
|
diff -ur $(PLAYBACK_FIXTURE_DIR) "$$CHECK_DIR" \
|
|
|
|
|
|| { echo "::error::$(PLAYBACK_FIXTURE_DIR) is stale; run make playback-fixtures"; exit 1; }; \
|
|
|
|
|
for fixture in $(PLAYBACK_WIRE_FIXTURES); do \
|
|
|
|
|
cmp -s "$$CHECK_DIR/$$fixture" "$(PLAYBACK_SCHEMA_FIXTURE_DIR)/$$fixture" \
|
|
|
|
|
|| { echo "::error::$(PLAYBACK_SCHEMA_FIXTURE_DIR)/$$fixture is stale; run make playback-fixtures"; exit 1; }; \
|
|
|
|
|
done
|
|
|
|
|
@echo "playback fixtures are current"
|
|
|
|
|
|
2026-05-24 19:58:22 -04:00
|
|
|
# Check committed content for local machine path leaks.
|
|
|
|
|
verify-local-paths:
|
|
|
|
|
scripts/check-local-path-leaks.sh
|
|
|
|
|
|
2026-06-06 23:51:30 -04:00
|
|
|
# Create a timestamped Goose SQL migration. Usage: make migrate-create NAME=add_thing
|
|
|
|
|
migrate-create:
|
|
|
|
|
@if [ -z "$(NAME)" ]; then echo "usage: make migrate-create NAME=add_thing"; exit 1; fi
|
|
|
|
|
$(GOOSE) -dir $(GOOSE_DIR) create "$(NAME)" sql
|
|
|
|
|
|
|
|
|
|
# Validate Goose migration annotations and SQL parsing without touching a database.
|
|
|
|
|
migrate-validate:
|
|
|
|
|
$(GOOSE) -dir $(GOOSE_DIR) validate
|
|
|
|
|
|
|
|
|
|
# Show Goose migration status through Silo's bootstrapping runner.
|
|
|
|
|
migrate-status:
|
|
|
|
|
go run ./cmd/silo/ --env "$(ENV_FILE)" --migrate-status
|
|
|
|
|
|
2026-07-30 10:52:41 -04:00
|
|
|
# Roll back every migration newer than VERSION (the version to KEEP).
|
|
|
|
|
#
|
|
|
|
|
# Not a routine operation: it discards data. It exists because some migrations
|
|
|
|
|
# are Go rather than SQL — the settings backfill and the jellycompat
|
|
|
|
|
# DisplayPreferences move — and those are registered in-process, so the goose
|
|
|
|
|
# CLI above cannot see or reverse them.
|
|
|
|
|
#
|
|
|
|
|
# This is a RANGE, not a list: everything newer than VERSION comes off, including
|
|
|
|
|
# migrations belonging to other features that happen to sort in between. Check
|
|
|
|
|
# `make migrate-status` and read the down of each one you are about to revert.
|
|
|
|
|
# Take a backup first regardless; the per-user SQLite stores have no down path.
|
|
|
|
|
#
|
|
|
|
|
# Usage: make migrate-down-to VERSION=<timestamp from migrate-status>
|
|
|
|
|
migrate-down-to:
|
|
|
|
|
@if [ -z "$(VERSION)" ]; then echo "usage: make migrate-down-to VERSION=<timestamp from make migrate-status>"; exit 1; fi
|
|
|
|
|
go run ./cmd/silo/ --env "$(ENV_FILE)" --migrate-down-to "$(VERSION)"
|
|
|
|
|
|
2026-06-06 23:51:30 -04:00
|
|
|
# Apply pending Goose migrations through Silo's bootstrapping runner.
|
|
|
|
|
migrate-up:
|
|
|
|
|
go run ./cmd/silo/ --env "$(ENV_FILE)" --migrate-only
|
|
|
|
|
|
2026-05-24 19:58:22 -04:00
|
|
|
# Install repo-local git hooks for this checkout/worktree.
|
|
|
|
|
install-hooks:
|
2026-05-25 00:27:29 -04:00
|
|
|
@existing="$$(git config --local core.hooksPath 2>/dev/null || true)"; \
|
|
|
|
|
if [ -n "$$existing" ] && [ "$$existing" != ".githooks" ]; then \
|
|
|
|
|
echo "warning: overwriting existing local core.hooksPath ($$existing) with .githooks"; \
|
|
|
|
|
fi
|
2026-05-24 19:58:22 -04:00
|
|
|
git config core.hooksPath .githooks
|
|
|
|
|
|
2026-06-15 06:34:08 -07:00
|
|
|
# Fetch and build the pinned Jellyfin Web component into a gitignored local cache.
|
|
|
|
|
jellyfin-web:
|
|
|
|
|
go run ./cmd/silo/ compat-web install --dir "$(JELLYFIN_WEB_INSTALL_DIR)" --version "$(JELLYFIN_WEB_VERSION)"
|
2026-05-22 20:26:11 -04:00
|
|
|
|
|
|
|
|
# Read-only preflight for Continuum Docker installs moving to Silo.
|
|
|
|
|
migrate-continuum-check:
|
|
|
|
|
scripts/migrate-continuum-docker.sh check
|
|
|
|
|
|
|
|
|
|
# Clean build artifacts
|
|
|
|
|
clean:
|
|
|
|
|
rm -rf web/dist web/node_modules silo
|
|
|
|
|
|
|
|
|
|
# Include developer-specific targets (gitignored, optional).
|
|
|
|
|
# In Git worktrees, fall back to the main checkout's Makefile.local so custom
|
|
|
|
|
# targets like dev-deploy work without per-worktree symlinks or copies.
|
|
|
|
|
ifneq ($(wildcard Makefile.local),)
|
|
|
|
|
include Makefile.local
|
|
|
|
|
else ifneq ($(wildcard $(SHARED_MAKEFILE_LOCAL)),)
|
|
|
|
|
include $(SHARED_MAKEFILE_LOCAL)
|
|
|
|
|
endif
|