Ctrl K
ComunidadGuías…

Makefile

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ============================================================================
# Liaison Makefile
# ============================================================================
#
# 这个 Makefile 提供了 Liaison 项目的构建、测试、打包等功能。
#
# 主要功能:
#   1. 本地开发构建(macOS)
#      - make build-local          # 构建 liaison 和 liaison-edge(本地)
#      - make build-liaison        # 仅构建 liaison
#      - make build-edge           # 仅构建 liaison-edge
#
#   2. Linux 构建(使用 Docker,支持 CGO 交叉编译)
#      - make build-linux          # 构建 Linux 版本的 liaison 和 liaison-edge
#      - make build-liaison-linux  # 仅构建 Linux 版本的 liaison
#      - make build-edge-linux     # 仅构建 Linux 版本的 liaison-edge
#
#   3. Edge 多架构构建
#      - make build-edge-all       # 构建所有架构的 edge 二进制文件
#      - make build-edge-linux-amd64    # Linux amd64
#      - make build-edge-linux-arm64    # Linux arm64
#      - make build-edge-darwin-amd64   # macOS amd64
#      - make build-edge-darwin-arm64   # macOS arm64 (Apple Silicon)
#      - make build-edge-windows-amd64   # Windows amd64
#
#   4. Edge 打包(生成 tar.gz 安装包)
#      - make package-edge-all     # 打包所有架构的 edge
#      - make package-edge-linux-amd64  # 打包 Linux amd64
#      - make package-edge-linux-arm64 # 打包 Linux arm64
#      - make package-edge-darwin-amd64 # 打包 macOS amd64
#      - make package-edge-darwin-arm64 # 打包 macOS arm64
#      - make package-edge-windows-amd64 # 打包 Windows amd64
#
#   5. 代码生成
#      - make gen                  # 生成 API 和 Swagger 文档
#      - make gen-api              # 仅生成 API 代码
#      - make gen-swagger          # 仅生成 Swagger 文档
#
#   6. 测试
#      - make test                 # 运行单元测试
#      - make test-e2e             # 运行 E2E 测试
#      - make test-all             # 运行所有测试
#
#   7. 工具构建
#      - make build-tools           # 构建所有工具(本地)
#      - make build-tools-linux     # 构建所有工具(Linux)
#
#   8. 前端构建
#      - make build-web            # 构建前端到 web/dist
#
#   9. 完整打包
#      - make package               # 打包完整的 Liaison 安装包(Linux,自动准备 guacd runtime)
#                                    包含:
#                                    - liaison (Linux amd64)
#                                    - liaison-edge (所有平台:linux-amd64, linux-arm64,
#                                                    darwin-amd64, darwin-arm64, windows-amd64)
#                                    - 前端文件 (web/dist)
#                                    - systemd 配置文件
#
#  10. Docker Compose 部署(仅服务端:liaison + frontier)
#      前置:先跑 `make package`(产出 bin/web/edge/frontier 等构件),Docker 镜像只做 COPY。
#      - make image-liaison          # 构建 liaison runtime 镜像
#      - make image-frontier         # 构建 frontier runtime 镜像
#      - make images                 # 同时构建两个镜像
#      - make compose-up             # 启动(需要 deploy/docker/.env)
#      - make compose-down           # 停止并移除容器
#      - make compose-logs           # 跟踪两个服务的日志
#      - make package-docker         # 输出默认 Linux 离线分发包(Docker)
#                                    liaison-<VERSION>-linux-amd64.tar.gz 含:
#                                    - images/{liaison,frontier,guacd}.tar(docker save)
#                                    - docker-compose.yaml(release 版,无 build: 段)
#                                    - .env.example(预钉镜像 tag)
#                                    - install.sh / README.md
#
# 注意事项:
#   - liaison 需要 CGO(SQLite),本地构建需要 CGO_ENABLED=1
#   - liaison-edge 使用纯 Go 实现,不需要 CGO,可以 CGO_ENABLED=0
#   - Linux 构建使用 Docker,自动处理依赖安装
#   - Edge 二进制文件不依赖 libpcap,可以在任何系统上运行
#
# ============================================================================

include ./Makefile.defs

# When running `make version=1.2.7` without explicit goals,
# use version update as the default action.
ifeq ($(strip $(MAKECMDGOALS)),)
ifneq ($(strip $(version)),)
.DEFAULT_GOAL := update-version
endif
endif

# ============================================================================
# Docker build variables and functions
# ============================================================================
DOCKER_IMAGE = golang:1.26.9
DOCKER_VOLUME = -v "$(shell pwd):/build"
DOCKER_WORKDIR = -w /build
DOCKER_BASE = docker run --rm $(DOCKER_VOLUME) $(DOCKER_WORKDIR)
DOCKER_HOST_UID := $(shell id -u 2>/dev/null || echo 0)
DOCKER_HOST_GID := $(shell id -g 2>/dev/null || echo 0)
GO_BUILD_FLAGS = -trimpath -buildvcs=false -ldflags '-s -w'
GUACD_IMAGE ?= guacamole/guacd:1.5.5
GUACD_CONTAINER_BINARY ?= /opt/guacamole/sbin/guacd
GUACD_RUNTIME_DIR ?= ./bin/guacd-rootfs

# Ensure Docker image exists (pull only if not present locally)
.PHONY: docker-image
docker-image:
	@if docker image inspect $(DOCKER_IMAGE) >/dev/null 2>&1; then \
		echo "✅ Docker image $(DOCKER_IMAGE) already exists locally"; \
	else \
		echo "📥 Pulling Docker image $(DOCKER_IMAGE)..."; \
		docker pull $(DOCKER_IMAGE) || exit 1; \
		echo "✅ Docker image $(DOCKER_IMAGE) pulled successfully"; \
	fi

# Function to build with CGO (for liaison and tools)
# Usage: $(call docker-build-cgo,platform,goos,goarch,output,source)
define docker-build-cgo
	@echo "Building $(4) for $(2)-$(3) using Docker..."
	@mkdir -p ./bin
	@$(DOCKER_BASE) \
		--platform $(1) \
		-e CGO_ENABLED=1 \
		-e GOOS=$(2) \
		-e GOARCH=$(3) \
		-e GOTOOLCHAIN=auto \
		$(DOCKER_IMAGE) sh -c "\
			apt-get update -qq && \
			DEBIAN_FRONTEND=noninteractive apt-get install -y -qq gcc libc6-dev libsqlite3-dev >/dev/null 2>&1 && \
			go env -w GOTOOLCHAIN=auto && \
			go mod download && \
			CC=gcc CGO_ENABLED=1 go build $(GO_BUILD_FLAGS) -o ./bin/$(4) $(5) && \
			chmod +x ./bin/$(4) && \
			chown $(DOCKER_HOST_UID):$(DOCKER_HOST_GID) ./bin/$(4)"
	@chmod +x ./bin/$(4)
	@echo "✅ Built: ./bin/$(4)"
endef

# Function to build without CGO (for edge)
# Usage: $(call docker-build-no-cgo,platform,goos,goarch,output,source)
define docker-build-no-cgo
	@echo "Building $(4) for $(2)-$(3) using Docker..."
	@mkdir -p ./bin
	@$(DOCKER_BASE) \
		--platform $(1) \
		-e GOOS=$(2) \
		-e GOARCH=$(3) \
		-e GOTOOLCHAIN=auto \
		$(DOCKER_IMAGE) sh -c "\
			go env -w GOTOOLCHAIN=auto && \
			go mod download && \
			CGO_ENABLED=0 go build $(GO_BUILD_FLAGS) -o ./bin/$(4) $(5) && \
			chmod +x ./bin/$(4) && \
			chown $(DOCKER_HOST_UID):$(DOCKER_HOST_GID) ./bin/$(4)"
	@chmod +x ./bin/$(4)
	@echo "✅ Built: ./bin/$(4)"
endef

# Function for local darwin builds
# Usage: $(call local-build-darwin,goarch,output,source)
define local-build-darwin
	@echo "Building $(2) for darwin-$(1)..."
	@mkdir -p ./bin
	@if [ "$$(uname)" != "Darwin" ]; then \
		echo "❌ Error: darwin CGO builds must run on macOS"; \
		exit 1; \
	fi
	@ARCH_FLAG="$$(if [ "$(1)" = "amd64" ]; then echo "x86_64"; else echo "arm64"; fi)" && \
		CGO_ENABLED=1 GOOS=darwin GOARCH=$(1) \
		CC="clang -arch $$ARCH_FLAG" \
		CXX="clang++ -arch $$ARCH_FLAG" \
		go build $(GO_BUILD_FLAGS) -o ./bin/$(2) $(3)
	@chmod +x ./bin/$(2)
	@echo "✅ Built: ./bin/$(2)"
endef

# ============================================================================
# Default targets
# ============================================================================
.PHONY: all
all: build-local test-deps

.PHONY: linux
linux: build-linux

# ============================================================================
# Docker images for code generation
# ============================================================================
.PHONY: image-gen-swagger
image-gen-swagger:
	docker buildx build -t liaison-gen-swagger:${VERSION} -f images/Dockerfile.liaison-swagger .

.PHONY: image-gen-api
image-gen-api:
	docker buildx build -t image-gen-api:${VERSION} -f images/Dockerfile.liaison-api .

# ============================================================================
# Local development builds (for macOS)
# ============================================================================
.PHONY: build-local
build-local: build-liaison build-edge

.PHONY: build-liaison
LIAISON_BINARY ?= ./bin/liaison
build-liaison:
	CGO_ENABLED=1 go build -trimpath -ldflags "-s -w" -o $(LIAISON_BINARY) cmd/manager/main.go

.PHONY: image-overlay deploy-overlay
image-overlay: ## Build a test overlay from an explicitly supplied context and image tag
	test -n "$(OVERLAY_CONTEXT)" && test -n "$(OVERLAY_IMAGE)"
	docker build -t "$(OVERLAY_IMAGE)" "$(OVERLAY_CONTEXT)"

deploy-overlay: ## Recreate only the manager using explicitly supplied Compose files
	test -n "$(COMPOSE_BASE)" && test -n "$(COMPOSE_OVERRIDE)" && test -n "$(COMPOSE_PROJECT)"
	docker compose -p "$(COMPOSE_PROJECT)" -f "$(COMPOSE_BASE)" -f "$(COMPOSE_OVERRIDE)" up -d --no-deps liaison

.PHONY: build-edge
build-edge:
	go build -trimpath -ldflags "-s -w" -o ./bin/liaison-edge cmd/edge/main.go

# Legacy aliases
.PHONY: liaison liaison-edge
liaison: build-liaison
liaison-edge: build-edge

# ============================================================================
# Linux builds using Docker (required for CGO cross-compilation from macOS)
# ============================================================================
.PHONY: build-linux
build-linux: build-liaison-linux build-edge-linux build-frontier-linux

.PHONY: build-liaison-linux
build-liaison-linux: docker-image
	$(call docker-build-cgo,linux/amd64,linux,amd64,liaison,cmd/manager/main.go)

.PHONY: build-edge-linux
build-edge-linux: docker-image
	$(call docker-build-no-cgo,linux/amd64,linux,amd64,liaison-edge,cmd/edge/main.go)

.PHONY: build-frontier-linux
build-frontier-linux:
	@mkdir -p ./bin
	@if [ -f "./bin/frontier" ] && command -v file >/dev/null 2>&1 && file ./bin/frontier | grep -Eq 'ELF 64-bit.*(x86-64|x86_64)'; then \
		echo "✅ frontier linux-amd64 binary already exists, skipping download"; \
	else \
		echo "Downloading frontier-linux-amd64 from GitHub releases..."; \
		rm -f ./bin/frontier; \
		curl -L -o ./bin/frontier https://github.com/singchia/frontier/releases/download/v1.2.4/frontier-linux-amd64 && \
		chmod +x ./bin/frontier && \
		echo "✅ Downloaded: ./bin/frontier"; \
	fi

.PHONY: prepare-guacd-linux
prepare-guacd-linux:
	@set -e; \
	mkdir -p ./bin; \
	if [ -x "./bin/guacd" ] && [ -s "$(GUACD_RUNTIME_DIR)$(GUACD_CONTAINER_BINARY)" ] && [ -s "$(GUACD_RUNTIME_DIR)/lib/ld-musl-x86_64.so.1" ]; then \
		echo "✅ guacd runtime already exists, skipping download"; \
		exit 0; \
	fi; \
	if ! command -v docker >/dev/null 2>&1; then \
		echo "❌ Error: Docker is required to prepare bundled guacd. Install Docker or place ./bin/guacd and $(GUACD_RUNTIME_DIR) manually."; \
		exit 1; \
	fi; \
	echo "Preparing bundled guacd linux/amd64 runtime from $(GUACD_IMAGE)..."; \
	rm -rf "$(GUACD_RUNTIME_DIR)" "./bin/guacd"; \
	mkdir -p "$(GUACD_RUNTIME_DIR)/opt" "$(GUACD_RUNTIME_DIR)/usr"; \
	image_arch=$$(docker image inspect --format '{{.Architecture}}' "$(GUACD_IMAGE)" 2>/dev/null || true); \
	if [ "$$image_arch" = "amd64" ]; then \
		echo "✅ Docker image $(GUACD_IMAGE) already exists locally for amd64"; \
	else \
		echo "📥 Pulling Docker image $(GUACD_IMAGE) for linux/amd64..."; \
		docker pull --platform linux/amd64 "$(GUACD_IMAGE)"; \
	fi; \
	tmp_tar="./bin/guacd-rootfs.tar"; \
	rm -f "$$tmp_tar"; \
	docker run --rm --user 0 --platform linux/amd64 --entrypoint sh "$(GUACD_IMAGE)" -c 'cd / && tar -cf - opt/guacamole lib usr/lib' > "$$tmp_tar"; \
	tar -C "$(GUACD_RUNTIME_DIR)" -xf "$$tmp_tar"; \
	rm -f "$$tmp_tar"; \
	perl -0pi -e 's|\Q/opt/guacamole/lib\E|/opt/liaison/guacd|g' "$(GUACD_RUNTIME_DIR)$(GUACD_CONTAINER_BINARY)"; \
	printf '%s\n' \
		'#!/bin/sh' \
		'set -e' \
		'ROOT="$${LIAISON_GUACD_ROOT:-/opt/liaison/guacd-rootfs}"' \
		'PLUGIN_DIR="$${LIAISON_GUACD_PLUGIN_DIR:-/opt/liaison/guacd}"' \
		'export LD_LIBRARY_PATH="$${PLUGIN_DIR}:$${PLUGIN_DIR}/freerdp2:$${ROOT}/opt/guacamole/lib:$${ROOT}/lib:$${ROOT}/usr/lib:$${ROOT}/usr/lib/pulseaudio$${LD_LIBRARY_PATH:+:$${LD_LIBRARY_PATH}}"' \
		'export FREERDP2_PLUGIN_PATH="$${PLUGIN_DIR}/freerdp2"' \
		'exec "$${ROOT}/lib/ld-musl-x86_64.so.1" --library-path "$${LD_LIBRARY_PATH}" "$${ROOT}/opt/guacamole/sbin/guacd" "$$@"' \
		> ./bin/guacd; \
	chmod +x ./bin/guacd "$(GUACD_RUNTIME_DIR)$(GUACD_CONTAINER_BINARY)"; \
	find "$(GUACD_RUNTIME_DIR)" -name '._*' -delete 2>/dev/null || true; \
	test -s ./bin/guacd; \
	test -s "$(GUACD_RUNTIME_DIR)$(GUACD_CONTAINER_BINARY)"; \
	test -s "$(GUACD_RUNTIME_DIR)/lib/ld-musl-x86_64.so.1"; \
	echo "✅ Prepared: ./bin/guacd + $(GUACD_RUNTIME_DIR)"

# Legacy aliases
.PHONY: liaison-linux liaison-edge-linux
liaison-linux: build-liaison-linux
liaison-edge-linux: build-edge-linux

# ============================================================================
# Code generation
# ============================================================================
.PHONY: gen-api
gen-api:
	docker run --rm -v ${PWD}/api/v1:/api/v1 image-gen-api:${VERSION}

.PHONY: gen-swagger
gen-swagger:
	docker run --rm -v ${PWD}:/liaison liaison-gen-swagger:${VERSION}

.PHONY: gen
gen: gen-api gen-swagger

# ============================================================================
# Testing
# ============================================================================
.PHONY: test
test:
	go test -v ./...

.PHONY: test-deps
test-deps:
	go get github.com/stretchr/testify/assert
	go get github.com/stretchr/testify/require

.PHONY: test-e2e
test-e2e:
	@echo "Running E2E tests..."
	@./test/e2e/run_simple_test.sh

.PHONY: test-e2e-full
test-e2e-full:
	@echo "Running full E2E tests..."
	@./test/e2e/run_e2e_test.sh

.PHONY: test-all
test-all: test test-e2e

# ============================================================================
# Tools
# ============================================================================
.PHONY: build-tools
build-tools: build-password-verifier build-password-generator

.PHONY: build-password-verifier
build-password-verifier:
	CGO_ENABLED=1 go build $(GO_BUILD_FLAGS) -o ./bin/password-verifier ./tools/password-verifier

.PHONY: build-password-generator
build-password-generator:
	CGO_ENABLED=1 go build $(GO_BUILD_FLAGS) -o ./bin/password-generator ./tools/password-generator

.PHONY: build-tools-linux
build-tools-linux: build-password-verifier-linux build-password-generator-linux

.PHONY: build-password-verifier-linux
build-password-verifier-linux: docker-image
	$(call docker-build-cgo,linux/amd64,linux,amd64,password-verifier,./tools/password-verifier)

.PHONY: build-password-generator-linux
build-password-generator-linux: docker-image
	$(call docker-build-cgo,linux/amd64,linux,amd64,password-generator,./tools/password-generator)

# Legacy aliases
.PHONY: tools tools-linux password-verifier password-generator password-verifier-linux password-generator-linux
tools: build-tools
tools-linux: build-tools-linux
password-verifier: build-password-verifier
password-generator: build-password-generator
password-verifier-linux: build-password-verifier-linux
password-generator-linux: build-password-generator-linux

# ============================================================================
# Edge binary builds for different architectures
# ============================================================================
.PHONY: build-edge-all
build-edge-all: build-edge-linux-amd64 build-edge-linux-arm64 build-edge-darwin-amd64 build-edge-darwin-arm64 build-edge-windows-amd64
	@echo "✅ All edge binaries built in ./bin/"

.PHONY: build-edge-linux-amd64
build-edge-linux-amd64: docker-image
	$(call docker-build-no-cgo,linux/amd64,linux,amd64,liaison-edge-linux-amd64,cmd/edge/main.go)

.PHONY: build-edge-linux-arm64
build-edge-linux-arm64: docker-image
	$(call docker-build-no-cgo,linux/arm64,linux,arm64,liaison-edge-linux-arm64,cmd/edge/main.go)

.PHONY: build-edge-darwin-amd64
build-edge-darwin-amd64:
	$(call local-build-darwin,amd64,liaison-edge-darwin-amd64,cmd/edge/main.go)

.PHONY: build-edge-darwin-arm64
build-edge-darwin-arm64:
	$(call local-build-darwin,arm64,liaison-edge-darwin-arm64,cmd/edge/main.go)

.PHONY: build-edge-windows-amd64
build-edge-windows-amd64: docker-image
	$(call docker-build-no-cgo,linux/amd64,windows,amd64,liaison-edge-windows-amd64.exe,cmd/edge/main.go)

# ============================================================================
# Tar command detection: prefer gtar (GNU tar) if available
# ============================================================================
TAR_CMD := $(shell command -v gtar 2>/dev/null || command -v tar 2>/dev/null || echo tar)

# ============================================================================
# Edge package creation (tar.gz with binary and config template)
# ============================================================================
.PHONY: package-edge-all
package-edge-all: package-edge-linux-amd64 package-edge-linux-arm64 package-edge-darwin-amd64 package-edge-darwin-arm64 package-edge-windows-amd64
	@echo "✅ All edge packages created in ./packages/edge/"

.PHONY: package-edge-linux-amd64
package-edge-linux-amd64: build-edge-linux-amd64
	@echo "Packaging liaison-edge-linux-amd64..."
	@mkdir -p ./packages/edge
	@TMP_DIR=$$(mktemp -d) && \
		PACKAGE_PATH="$$(pwd)/packages/edge/liaison-edge-linux-amd64.tar.gz" && \
		COPYFILE_DISABLE=1 cp ./bin/liaison-edge-linux-amd64 "$$TMP_DIR/liaison-edge" && \
		COPYFILE_DISABLE=1 cp ./dist/edge/liaison-edge.yaml.template "$$TMP_DIR/liaison-edge.yaml.template" && \
		cd "$$TMP_DIR" && \
		COPYFILE_DISABLE=1 $(TAR_CMD) --exclude='._*' --exclude='.DS_Store' -czf "$$PACKAGE_PATH" liaison-edge liaison-edge.yaml.template && \
		rm -rf "$$TMP_DIR" && \
		echo "✅ Package created: ./packages/edge/liaison-edge-linux-amd64.tar.gz"

.PHONY: package-edge-linux-arm64
package-edge-linux-arm64: build-edge-linux-arm64
	@echo "Packaging liaison-edge-linux-arm64..."
	@mkdir -p ./packages/edge
	@TMP_DIR=$$(mktemp -d) && \
		PACKAGE_PATH="$$(pwd)/packages/edge/liaison-edge-linux-arm64.tar.gz" && \
		COPYFILE_DISABLE=1 cp ./bin/liaison-edge-linux-arm64 "$$TMP_DIR/liaison-edge" && \
		COPYFILE_DISABLE=1 cp ./dist/edge/liaison-edge.yaml.template "$$TMP_DIR/liaison-edge.yaml.template" && \
		cd "$$TMP_DIR" && \
		COPYFILE_DISABLE=1 $(TAR_CMD) --exclude='._*' --exclude='.DS_Store' -czf "$$PACKAGE_PATH" liaison-edge liaison-edge.yaml.template && \
		rm -rf "$$TMP_DIR" && \
		echo "✅ Package created: ./packages/edge/liaison-edge-linux-arm64.tar.gz"

.PHONY: package-edge-darwin-amd64
package-edge-darwin-amd64: build-edge-darwin-amd64
	@echo "Packaging liaison-edge-darwin-amd64..."
	@mkdir -p ./packages/edge
	@TMP_DIR=$$(mktemp -d) && \
		PACKAGE_PATH="$$(pwd)/packages/edge/liaison-edge-darwin-amd64.tar.gz" && \
		COPYFILE_DISABLE=1 cp -X ./bin/liaison-edge-darwin-amd64 "$$TMP_DIR/liaison-edge" && \
		COPYFILE_DISABLE=1 cp -X ./dist/edge/liaison-edge.yaml.template "$$TMP_DIR/liaison-edge.yaml.template" && \
		cd "$$TMP_DIR" && \
		COPYFILE_DISABLE=1 $(TAR_CMD) --exclude='._*' --exclude='.DS_Store' -czf "$$PACKAGE_PATH" liaison-edge liaison-edge.yaml.template && \
		rm -rf "$$TMP_DIR" && \
		echo "✅ Package created: ./packages/edge/liaison-edge-darwin-amd64.tar.gz"

.PHONY: package-edge-darwin-arm64
package-edge-darwin-arm64: build-edge-darwin-arm64
	@echo "Packaging liaison-edge-darwin-arm64..."
	@mkdir -p ./packages/edge
	@TMP_DIR=$$(mktemp -d) && \
		PACKAGE_PATH="$$(pwd)/packages/edge/liaison-edge-darwin-arm64.tar.gz" && \
		COPYFILE_DISABLE=1 cp -X ./bin/liaison-edge-darwin-arm64 "$$TMP_DIR/liaison-edge" && \
		COPYFILE_DISABLE=1 cp -X ./dist/edge/liaison-edge.yaml.template "$$TMP_DIR/liaison-edge.yaml.template" && \
		cd "$$TMP_DIR" && \
		COPYFILE_DISABLE=1 $(TAR_CMD) --exclude='._*' --exclude='.DS_Store' -czf "$$PACKAGE_PATH" liaison-edge liaison-edge.yaml.template && \
		rm -rf "$$TMP_DIR" && \
		echo "✅ Package created: ./packages/edge/liaison-edge-darwin-arm64.tar.gz"

.PHONY: package-edge-windows-amd64
package-edge-windows-amd64: build-edge-windows-amd64
	@echo "Packaging liaison-edge-windows-amd64..."
	@mkdir -p ./packages/edge
	@TMP_DIR=$$(mktemp -d) && \
		PACKAGE_PATH="$$(pwd)/packages/edge/liaison-edge-windows-amd64.tar.gz" && \
		COPYFILE_DISABLE=1 cp ./bin/liaison-edge-windows-amd64.exe "$$TMP_DIR/liaison-edge.exe" && \
		COPYFILE_DISABLE=1 cp ./dist/edge/liaison-edge.yaml.template "$$TMP_DIR/liaison-edge.yaml.template" && \
		cd "$$TMP_DIR" && \
		COPYFILE_DISABLE=1 $(TAR_CMD) --exclude='._*' --exclude='.DS_Store' -czf "$$PACKAGE_PATH" liaison-edge.exe liaison-edge.yaml.template && \
		rm -rf "$$TMP_DIR" && \
		echo "✅ Package created: ./packages/edge/liaison-edge-windows-amd64.tar.gz"

# Legacy aliases
.PHONY: edge-packages edge-package-linux-amd64 edge-package-linux-arm64 edge-package-darwin-amd64 edge-package-darwin-arm64 edge-package-windows-amd64
edge-packages: package-edge-all
edge-package-linux-amd64: package-edge-linux-amd64
edge-package-linux-arm64: package-edge-linux-arm64
edge-package-darwin-amd64: package-edge-darwin-amd64
edge-package-darwin-arm64: package-edge-darwin-arm64
edge-package-windows-amd64: package-edge-windows-amd64

# ============================================================================
# Frontend build
# ============================================================================
.PHONY: build-web
build-web:
	@echo "Building frontend..."
	@if [ ! -d "web" ]; then \
		echo "❌ Error: web directory not found"; \
		exit 1; \
	fi
	@if [ ! -f "web/package.json" ]; then \
		echo "❌ Error: web/package.json not found"; \
		exit 1; \
	fi
	@echo "Cleaning old build artifacts..."
	@rm -rf web/dist
	@( \
		cd web && \
		export PNPM_HOME="$$HOME/.local/share/pnpm" 2>/dev/null || true && \
		export PATH="$$PNPM_HOME:$$PATH" 2>/dev/null || true && \
		echo "Installing dependencies..." && \
		pnpm install || (echo "❌ Error: pnpm install failed" && exit 1) && \
		echo "Building frontend..." && \
		pnpm run build || (echo "❌ Error: pnpm run build failed" && exit 1) && \
		echo "Build completed, checking output..." && \
		if [ -d "dist" ] && [ "$$(ls -A dist 2>/dev/null)" ]; then \
			echo "✅ Build output found in dist/"; \
			ls -1 dist | head -5; \
		else \
			echo "❌ Error: dist directory not found or empty in web/"; \
			ls -la . | head -10; \
			exit 1; \
		fi \
	) || (echo "❌ Error: Frontend build failed" && exit 1)
	@if [ -d "web/dist" ] && [ "$$(ls -A web/dist 2>/dev/null)" ]; then \
		echo "✅ Frontend built successfully in web/dist/"; \
		echo "   Build output: $$(ls -1 web/dist | head -5 | tr '\n' ' ')"; \
	else \
		echo "❌ Error: web/dist not found or empty after build"; \
		echo "   Checking web directory contents..."; \
		ls -la web/ | head -10 || true; \
		exit 1; \
	fi

# ============================================================================
# Full package (liaison + edge binaries for all platforms + frontend + systemd files)
# ============================================================================
.PHONY: package
package: build-linux build-edge-all package-edge-all build-web build-tools-linux prepare-guacd-linux
	@chmod +x dist/package.sh
	@./dist/package.sh

# Legacy alias
.PHONY: pack
pack: package

# ============================================================================
# Docker images (server-side: liaison + frontier)
# Images are "thin": they COPY pre-built artifacts produced by `make package`.
# Run `make package` first (or at least build-linux + build-web + build-tools-linux
# + build-edge-all + package-edge-all + build-frontier-linux).
# ============================================================================
DOCKER_COMPOSE_DIR = deploy/docker
LIAISON_IMAGE_REGISTRY ?= liaison
LIAISON_IMAGE_TAG ?= $(shell cat VERSION 2>/dev/null | tr -d 'v' || echo latest)
DOCKER_IMAGE_PLATFORM ?= linux/amd64

.PHONY: image-liaison
image-liaison:
	@if [ ! -f bin/liaison ] || [ ! -f bin/password-generator ] || [ ! -d web/dist ]; then \
		echo "❌ Missing artifacts. Run 'make package' first."; \
		exit 1; \
	fi
	@if command -v file >/dev/null 2>&1; then \
		for binary in bin/liaison bin/password-generator; do \
			file "$$binary" | grep -Eq 'ELF 64-bit.*(x86-64|x86_64)' || { \
				echo "❌ $$binary must be a Linux amd64 binary. Run 'make package' first."; \
				exit 1; \
			}; \
		done; \
	fi
	docker build --platform $(DOCKER_IMAGE_PLATFORM) \
		-t $(LIAISON_IMAGE_REGISTRY)/liaison:$(LIAISON_IMAGE_TAG) \
		-f $(DOCKER_COMPOSE_DIR)/Dockerfile.liaison .
	@echo "✅ Built $(LIAISON_IMAGE_REGISTRY)/liaison:$(LIAISON_IMAGE_TAG)"

.PHONY: image-frontier
image-frontier: build-frontier-linux
	@if command -v file >/dev/null 2>&1; then \
		file bin/frontier | grep -Eq 'ELF 64-bit.*(x86-64|x86_64)' || { \
			echo "❌ bin/frontier must be a Linux amd64 binary. Run 'make build-frontier-linux' first."; \
			exit 1; \
		}; \
	fi
	docker build --platform $(DOCKER_IMAGE_PLATFORM) \
		-t $(LIAISON_IMAGE_REGISTRY)/frontier:$(LIAISON_IMAGE_TAG) \
		-f $(DOCKER_COMPOSE_DIR)/Dockerfile.frontier .
	@echo "✅ Built $(LIAISON_IMAGE_REGISTRY)/frontier:$(LIAISON_IMAGE_TAG)"

.PHONY: images
images: image-liaison image-frontier

.PHONY: compose-up
compose-up:
	@if [ ! -f $(DOCKER_COMPOSE_DIR)/.env ]; then \
		echo "❌ $(DOCKER_COMPOSE_DIR)/.env not found. Copy .env.example and edit first."; \
		exit 1; \
	fi
	cd $(DOCKER_COMPOSE_DIR) && docker compose up -d

.PHONY: compose-down
compose-down:
	cd $(DOCKER_COMPOSE_DIR) && docker compose down

.PHONY: compose-logs
compose-logs:
	cd $(DOCKER_COMPOSE_DIR) && docker compose logs -f

.PHONY: package-docker
package-docker: images
	@chmod +x $(DOCKER_COMPOSE_DIR)/package-docker.sh
	@$(DOCKER_COMPOSE_DIR)/package-docker.sh

# ============================================================================
# Version sync
# Usage:
#   make update-version version=1.2.7
#   make version version=1.2.7
#   make version=1.2.7
# ============================================================================
.PHONY: update-version version
update-version version:
	@if [ -z "$(version)" ]; then \
		echo "❌ Error: missing version. Usage: make version=1.2.7"; \
		exit 1; \
	fi
	@chmod +x scripts/sync-release-version.sh
	@scripts/sync-release-version.sh "$(version)"