SHELL := /usr/bin/env bash .DEFAULT_GOAL := help APP := glchat MODULE := glchat # Версия релиза — единственный источник правды: файл VERSION в корне (его же # читает установщик, а tauri.conf.json/Cargo.toml держат то же значение). # Без файла (например, в архиве без него) работает прежнее правило по git. VERSION ?= $(shell cat $(CURDIR)/VERSION 2>/dev/null || git describe --tags --always --dirty 2>/dev/null || echo dev) COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo none) BUILD_DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ) ROOT := $(CURDIR) CACHE_DIR := $(ROOT)/.cache BIN_DIR := $(CACHE_DIR)/bin BUILD_DIR := $(ROOT)/build export GOCACHE := $(CACHE_DIR)/go-build export GOMODCACHE:= $(CACHE_DIR)/go-mod export GOPATH := $(CACHE_DIR)/gopath export GOTOOLCHAIN := local export GOLANGCI_LINT_CACHE := $(CACHE_DIR)/golangci-lint # Сети без доступа к storage.googleapis.com (например, домашний сервер) не могут # скачать модули через proxy.golang.org; goproxy.io работает как fallback. export GOPROXY ?= https://proxy.golang.org,direct export GOSUMDB ?= sum.golang.org export CGO_ENABLED ?= 1 # sqlite_fts5 включает полнотекстовый поиск в mattn/go-sqlite3 (AGENT.md 7.15): # без тега поиск по сообщениям не работает, поэтому он обязателен для всех сборок. GO_TAGS ?= sqlite_fts5 GOFLAGS_LOCAL := -trimpath -tags $(GO_TAGS) GO_LDFLAGS := -s -w \ -X main.version=$(VERSION) \ -X main.commit=$(COMMIT) \ -X main.buildDate=$(BUILD_DATE) # Инструмент берётся из .cache/bin; если он уже установлен в системе # (PATH), повторная установка не нужна — это важно для CI и серверов, # где сборка инструментов из исходников недоступна. SYSTEM_TOOLS := golangci-lint gofumpt sqlc goose govulncheck gitleaks export PATH := $(BIN_DIR):$(PATH) GO_PACKAGES := ./cmd/... ./internal/... GO_LINT_TIMEOUT ?= 5m WEB_DIR := $(ROOT)/web DOCKER_IMAGE ?= glchat DOCKER_TAG ?= $(VERSION) SKIP_TLS ?= 0 .PHONY: help help: ## Показать список целей @printf 'glchat — доступные цели:\n\n' @grep -hE '^[a-zA-Z0-9_.-]+:.*?## ' $(MAKEFILE_LIST) | \ awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' @printf '\nПрофиль: %s | Кэш: %s\n' "$${PROFILE:-auto}" "$(CACHE_DIR)" .PHONY: tools tools: ## Установить dev-инструменты в .cache/bin (уже установленные в системе пропускаются) @mkdir -p $(CACHE_DIR) $(BIN_DIR) @for tool in $(SYSTEM_TOOLS); do \ if command -v $$tool >/dev/null 2>&1; then \ printf ' ok %-16s %s\n' "$$tool" "$$(command -v $$tool)"; \ else \ $(MAKE) --no-print-directory $(BIN_DIR)/$$tool || exit 1; \ fi; \ done $(BIN_DIR)/golangci-lint: | $(CACHE_DIR) @echo "==> install golangci-lint v2.13.2" @GOBIN=$(BIN_DIR) go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.13.2 $(BIN_DIR)/gofumpt: | $(CACHE_DIR) @echo "==> install gofumpt" @GOBIN=$(BIN_DIR) go install mvdan.cc/gofumpt@v0.12.0 $(BIN_DIR)/sqlc: | $(CACHE_DIR) @echo "==> install sqlc" @GOBIN=$(BIN_DIR) go install github.com/sqlc-dev/sqlc/cmd/sqlc@v1.31.1 $(BIN_DIR)/goose: | $(CACHE_DIR) @echo "==> install goose" @GOBIN=$(BIN_DIR) go install github.com/pressly/goose/v3/cmd/goose@v3.28.0 $(BIN_DIR)/govulncheck: | $(CACHE_DIR) @echo "==> install govulncheck" @GOBIN=$(BIN_DIR) go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 $(BIN_DIR)/gitleaks: | $(CACHE_DIR) @echo "==> install gitleaks" @GOBIN=$(BIN_DIR) go install github.com/zricethezav/gitleaks/v8@v8.30.0 .PHONY: dev dev: ## Запустить сервер локально (данные в ./build/dev-data) @mkdir -p $(BUILD_DIR)/dev-data/db DATA_DIR=$(BUILD_DIR)/dev-data \ LISTEN_ADDR=127.0.0.1:8080 \ DOMAIN=localhost \ LOG_FORMAT=text \ LOG_LEVEL=debug \ go run ./cmd/glchat .PHONY: build build: ## Собрать бинарник сервера в ./build/glchat @mkdir -p $(BUILD_DIR) go build $(GOFLAGS_LOCAL) -ldflags '$(GO_LDFLAGS)' -o $(BUILD_DIR)/$(APP) ./cmd/glchat @ls -lh $(BUILD_DIR)/$(APP) .PHONY: test test: ## Тесты Go с race-детектором go test -tags $(GO_TAGS) -race -count=1 $(GO_PACKAGES) .PHONY: test-cover test-cover: ## Тесты с отчётом покрытия (./build/coverage.out) @mkdir -p $(BUILD_DIR) go test -tags $(GO_TAGS) -race -count=1 -coverprofile=$(BUILD_DIR)/coverage.out $(GO_PACKAGES) go tool cover -func=$(BUILD_DIR)/coverage.out | tail -1 .PHONY: lint lint: tools go-lint web-lint ## Полный линт (Go + web) .PHONY: go-lint go-lint: tools ## Линт Go: формат, vet, golangci-lint @echo "==> формат (gofumpt + golangci-lint fmt)" @out=$$(gofumpt -l ./cmd ./internal ./scripts 2>/dev/null || true); \ if [ -n "$$out" ]; then printf '%s\n' "$$out"; echo "run: make fmt"; exit 1; fi @out=$$(golangci-lint fmt --diff $(GO_PACKAGES)); \ if [ -n "$$out" ]; then printf '%s\n' "$$out"; echo "run: make fmt"; exit 1; fi @echo "==> go vet" @go vet $(GO_PACKAGES) @echo "==> golangci-lint" @golangci-lint run --timeout $(GO_LINT_TIMEOUT) $(GO_PACKAGES) .PHONY: fmt fmt: tools ## Форматировать Go-код gofumpt -w ./cmd ./internal golangci-lint fmt $(GO_PACKAGES) .PHONY: generate generate: tools ## Проверить сгенерированные артефакты (OpenAPI, sqlc — по мере появления) @echo "==> openapi: обслуживается из internal/server/openapi.go (/api/v1/openapi.json)" @test -s internal/server/openapi.go || { echo "openapi.go отсутствует"; exit 1; } @if [ -d queries ] && [ -n "$$(ls -A queries 2>/dev/null | grep -v '^.gitkeep$$')" ]; then \ echo "==> sqlc generate"; sqlc generate; \ else echo "==> sqlc: запросов пока нет, пропускаем"; fi .PHONY: security security: tools ## AppSec: govulncheck, gitleaks, npm audit, trivy (по возможности) @mkdir -p $(BUILD_DIR) @echo "==> govulncheck" @govulncheck ./cmd/... ./internal/... || { echo "govulncheck нашёл уязвимости"; exit 1; } @echo "==> gitleaks" @$(ROOT)/scripts/gitleaks-scan.sh @echo "==> npm audit (web, только production-зависимости)" @cd $(WEB_DIR) && npm audit --omit=dev --audit-level=high || { echo "npm audit нашёл уязвимости"; exit 1; } @echo "==> trivy filesystem scan" @if command -v trivy >/dev/null 2>&1; then \ trivy fs --quiet --scanners vuln,secret,misconfig --exit-code 1 \ --skip-dirs .cache --skip-dirs .git --skip-dirs web/node_modules .; \ else echo "trivy not installed — skipped"; fi .PHONY: sbom sbom: ## SBOM зависимостей в build/sbom.cdx.json (trivy, если установлен) @mkdir -p $(BUILD_DIR) @if command -v trivy >/dev/null 2>&1; then \ trivy fs --quiet --format cyclonedx --output $(BUILD_DIR)/sbom.cdx.json \ --skip-dirs .cache --skip-dirs web/node_modules . && \ echo "SBOM: $(BUILD_DIR)/sbom.cdx.json"; \ else echo "trivy не установлен — SBOM не собран (запустите на сервере сборки)"; fi .PHONY: secrets-scan secrets-scan: tools ## Только проверка на утечки секретов @$(ROOT)/scripts/gitleaks-scan.sh .PHONY: web-install web-install: ## Установить зависимости web-клиента cd $(WEB_DIR) && npm ci .PHONY: web-lint web-lint: ## Линт и типизация web-клиента cd $(WEB_DIR) && npm run lint cd $(WEB_DIR) && npm run check .PHONY: web-test web-test: ## Тесты web-клиента (vitest) cd $(WEB_DIR) && npm test .PHONY: web-build web-build: ## Сборка web-клиента в web/dist cd $(WEB_DIR) && npm run build # Desktop-обёртка (Фаза 6, docs/client-tauri.md). Кэши инструментов — в .cache, # адрес инстанса по умолчанию задаёт сама обёртка (https://gl.mhspx.su). DESKTOP_DIR := $(ROOT)/desktop DESKTOP_TAURI_DIR := $(DESKTOP_DIR)/src-tauri DESKTOP_KEY ?= $(BUILD_DIR)/desktop-keys/glchat-updater.key # Бандлы Tauri: `all` — обычная сборка (значение по умолчанию из tauri.conf.json), # `app` — только .app (и артефакты обновления) без .dmg: сборка .dmg монтирует # образ и требует Finder. Tauri CLI не принимает `all` в --bundles: при `all` # флаг не передаётся вовсе, и список бандлов берётся из bundle.targets. DESKTOP_BUNDLES ?= all DESKTOP_BUNDLES_ARG := $(if $(filter-out all,$(DESKTOP_BUNDLES)),--bundles $(DESKTOP_BUNDLES),) TAURI_CLI ?= npx --yes @tauri-apps/cli@^2 export CARGO_HOME ?= $(CACHE_DIR)/cargo export NPM_CONFIG_CACHE ?= $(CACHE_DIR)/npm .PHONY: desktop-check desktop-check: ## Проверки Rust-обёртки: fmt, clippy, тесты cd $(DESKTOP_TAURI_DIR) && cargo fmt --check cd $(DESKTOP_TAURI_DIR) && cargo clippy --release --all-targets cd $(DESKTOP_TAURI_DIR) && cargo test --release .PHONY: desktop-build desktop-build: ## Собрать desktop-приложение (бандлы по умолчанию для текущей ОС) @if [ -f "$(DESKTOP_KEY)" ]; then \ echo " ключ подписи обновлений: $(DESKTOP_KEY)"; \ cd $(DESKTOP_TAURI_DIR) && \ TAURI_SIGNING_PRIVATE_KEY="$$(cat $(DESKTOP_KEY))" \ TAURI_SIGNING_PRIVATE_KEY_PASSWORD="$${TAURI_SIGNING_PRIVATE_KEY_PASSWORD:-glchat-local-dev}" \ $(TAURI_CLI) build $(DESKTOP_BUNDLES_ARG); \ else \ echo " ключа $(DESKTOP_KEY) нет: сборка без артефактов автообновления"; \ cd $(DESKTOP_TAURI_DIR) && \ $(TAURI_CLI) build $(DESKTOP_BUNDLES_ARG) \ --config '{"bundle":{"createUpdaterArtifacts":false}}'; \ fi .PHONY: desktop-run desktop-run: ## Запустить собранное приложение (macOS); доп. аргументы — ARGS=… open -a "$(DESKTOP_TAURI_DIR)/target/release/bundle/macos/glchat.app" $(ARGS) .PHONY: desktop-build-app desktop-build-app: ## Собрать .app и артефакты обновления без .dmg (dmg требует Finder/AppleScript) @$(MAKE) --no-print-directory desktop-build DESKTOP_BUNDLES=app # Каталог обновлений инстанса и его публичный адрес (docs/client-tauri.md §4). # Раскладку делает scripts/desktop-release.sh: манифест версии + latest.json, # артефакт и подпись; приложение отдаёт их по /updates/{target}/{arch}/{version}. DESKTOP_UPDATES_DIR ?= /opt/glchat/updates DESKTOP_BASE_URL ?= https://gl.mhspx.su .PHONY: desktop-release desktop-release: ## Разложить артефакты обновления в DESKTOP_UPDATES_DIR (нужен sudo) bash scripts/desktop-release.sh --dir "$(DESKTOP_UPDATES_DIR)" \ --base-url "$(DESKTOP_BASE_URL)" $(ARGS) .PHONY: desktop-release-upload desktop-release-upload: ## Загрузить обновления на инстанс: make desktop-release-upload HOST=user@server bash scripts/desktop-release.sh --remote "$(HOST)" \ --base-url "$(DESKTOP_BASE_URL)" $(ARGS) .PHONY: docker-build docker-build: ## Собрать образ приложения docker build \ --build-arg VERSION=$(VERSION) \ --build-arg COMMIT=$(COMMIT) \ --build-arg BUILD_DATE=$(BUILD_DATE) \ --build-arg GOPROXY=$(GOPROXY) \ --build-arg GOSUMDB=$(GOSUMDB) \ -t $(DOCKER_IMAGE):$(DOCKER_TAG) . .PHONY: install-dry-run install-dry-run: ## Проверить установщик без изменений системы bash deploy/install.sh --dry-run --domain gl.mhspx.su --email admin@gl.mhspx.su --yes .PHONY: install-test install-test: ## Прогон install.sh в чистом контейнере Ubuntu 24.04 bash scripts/test-install.sh .PHONY: profiles-test profiles-test: ## Проверить таблицу профилей производительности (AGENT.md 10.10) bash scripts/test-profiles.sh .PHONY: backup-test backup-test: ## Проверить бэкап и offsite-выгрузку на синтетическом инстансе bash scripts/test-backup.sh .PHONY: profiles-env profiles-env: ## Сверить .env установленного инстанса с таблицей профилей sudo bash scripts/test-profiles.sh --env "$${GLCHAT_ETC_DIR:-/opt/glchat}/.env" .PHONY: ops-test ops-test: profiles-test backup-test ## Проверки эксплуатационных скриптов .PHONY: clean clean: ## Удалить артефакты сборки rm -rf $(BUILD_DIR)