2026-05-15 16:00:25 +08:00
SHELL := /bin/bash
DEV_RUST_LOG := info,executor::candidate_loop= debug,stream::execution= debug
ifeq ( $( origin RUST_LOG ) , command line)
DEV_RUST_LOG := $( RUST_LOG)
endif
export DEV_RUST_LOG
2026-09-03 11:05:59 +08:00
.PHONY : dev dev -backend dev -frontend db -status db -prepare migration backfill
2026-05-15 16:00:25 +08:00
define DEV_BACKEND_SCRIPT
set -euo pipefail
if [ ! -f .env ]; then
echo "=> 未找到 .env,请先执行 : cp .env .example .env "
exit 1
fi
set -a
source .env
set +a
2026-09-03 11:05:59 +08:00
if [[ -n "$${ADMIN_EMAIL : -}" || -n "$${ADMIN_USERNAME :-}" || -n "$${ADMIN_PASSWORD :-}" ]]; then
if [[ -z " $$ {ADMIN_USERNAME:-}" || -z " $$ {ADMIN_PASSWORD:-}" ]] ; then
echo "=> 管理员自举配置不完整,请在 .env 中设置 ADMIN_USERNAME 和 ADMIN_PASSWORD"
exit 1
fi
fi
2026-05-15 16:00:25 +08:00
dotenv_has_key() {
local key = " $$ 1"
grep -Eq "^[[:space:]]* $$ {key}=" .env
}
lowercase() {
printf '%s' "$$1" | tr '[ : upper :]' '[:lower :]'
}
dev_uses_sqlite_database() {
local driver
local url
driver = " $$ (lowercase " $$ { AETHER_DATABASE_DRIVER:-} ")"
url = " $$ {AETHER_DATABASE_URL:- $$ {DATABASE_URL:-}}"
[[ " $$ {driver}" == "sqlite" || " $$ {url}" == sqlite:* ]]
}
dev_uses_postgres_database() {
local driver
local url
driver = " $$ (lowercase " $$ { AETHER_DATABASE_DRIVER:-} ")"
url = " $$ {AETHER_DATABASE_URL:- $$ {DATABASE_URL:-}}"
if [[ -z " $$ {driver}" && -z " $$ {url}" ]] ; then
return 0
fi
[[ " $$ {driver}" == "postgres" || " $$ {driver}" == "postgresql" || " $$ {url}" == postgres:* || " $$ {url}" == postgresql:* ]]
}
dev_uses_redis_runtime() {
local backend
backend = " $$ (lowercase " $$ { AETHER_RUNTIME_BACKEND:-} ")"
if [[ " $$ {backend}" == "memory" ]] ; then
return 1
fi
if [[ " $$ {backend}" == "redis" ]] ; then
return 0
fi
if dev_uses_sqlite_database; then
return 1
fi
return 0
}
print_dev_infra_hint() {
echo "=> 本地开发依赖未就绪。"
echo "=> 可手动启动 Postgres / Redis : "
echo "=> docker compose up -d postgres redis"
}
check_postgres_ready() {
local host = " $$ 1"
local port = " $$ 2"
if command -v pg_isready >/dev/null 2>& 1; then
pg_isready -h " $$ {host}" -p " $$ {port}" >/dev/null 2>& 1
return $$ ?
fi
if command -v nc >/dev/null 2>& 1; then
nc -z " $$ {host}" " $$ {port}" >/dev/null 2>& 1
return $$ ?
fi
return 0
}
check_redis_ready() {
local host = " $$ 1"
local port = " $$ 2"
local password = " $$ 3"
if command -v redis-cli >/dev/null 2>& 1; then
REDISCLI_AUTH = " $$ {password}" redis-cli -h " $$ {host}" -p " $$ {port}" ping >/dev/null 2>& 1
return $$ ?
fi
if command -v nc >/dev/null 2>& 1; then
nc -z " $$ {host}" " $$ {port}" >/dev/null 2>& 1
return $$ ?
fi
return 0
}
is_local_host() {
case "$$1" in
localhost|127.0.0.1| :: 1)
return 0
;;
esac
return 1
}
ensure_dev_infra() {
local postgres_host="$${DB_HOST : -localhost }"
local postgres_port = " $$ {DB_PORT:-5432}"
local redis_host = " $$ {REDIS_HOST:-localhost}"
local redis_port = " $$ {REDIS_PORT:-6379}"
local redis_password = " $$ {REDIS_PASSWORD:-}"
local need_postgres = false
local need_redis = false
local services =()
if dev_uses_postgres_database; then
if ! check_postgres_ready " $$ {postgres_host}" " $$ {postgres_port}" ; then
if is_local_host " $$ {postgres_host}" ; then
need_postgres = true
services +=( postgres)
else
echo "=> PostgreSQL 不可用: $$ {postgres_host}: $$ {postgres_port}"
print_dev_infra_hint
return 1
fi
fi
fi
if dev_uses_redis_runtime; then
if ! check_redis_ready " $$ {redis_host}" " $$ {redis_port}" " $$ {redis_password}" ; then
if is_local_host " $$ {redis_host}" ; then
need_redis = true
services +=( redis)
else
echo "=> Redis 不可用: $$ {redis_host}: $$ {redis_port}"
print_dev_infra_hint
return 1
fi
fi
fi
if [ " $$ {#services[@]}" -eq 0 ] ; then
return 0
fi
if ! command -v docker >/dev/null 2>& 1; then
echo "=> 未找到 docker,无法自动启动本地开发依赖。"
print_dev_infra_hint
return 1
fi
echo "=> 本地开发依赖未就绪,正在启动: docker compose up -d $$ {services[*]}"
if ! docker compose up -d " $$ {services[@]}" ; then
echo "=> docker compose 启动本地开发依赖失败。"
print_dev_infra_hint
return 1
fi
for _ in { 1..100} ; do
local ready = true
if [ " $$ {need_postgres}" = "true" ] && ! check_postgres_ready " $$ {postgres_host}" " $$ {postgres_port}" ; then
ready = false
fi
if [ " $$ {need_redis}" = "true" ] && ! check_redis_ready " $$ {redis_host}" " $$ {redis_port}" " $$ {redis_password}" ; then
ready = false
fi
if [ " $$ {ready}" = "true" ] ; then
return 0
fi
sleep 0.2
done
if [ " $$ {need_postgres}" = "true" ] && ! check_postgres_ready " $$ {postgres_host}" " $$ {postgres_port}" ; then
echo "=> PostgreSQL 不可用: $$ {postgres_host}: $$ {postgres_port}"
fi
if [ " $$ {need_redis}" = "true" ] && ! check_redis_ready " $$ {redis_host}" " $$ {redis_port}" " $$ {redis_password}" ; then
echo "=> Redis 不可用: $$ {redis_host}: $$ {redis_port}"
fi
print_dev_infra_hint
return 1
}
print_startup_failure_hint() {
local log_file = " $$ 1"
if [ -n " $$ {log_file}" ] && [ -f " $$ {log_file}" ] ; then
if grep -Eq "database schema is behind" " $$ {log_file}" ; then
2026-09-03 11:05:59 +08:00
echo "=> 检测到数据库尚未准备完成,请执行: make db-prepare"
2026-05-15 16:00:25 +08:00
return
fi
if grep -Eq "database backfills are behind" " $$ {log_file}" ; then
2026-09-03 11:05:59 +08:00
echo "=> 检测到数据库尚未准备完成,请执行: make db-prepare"
return
fi
if grep -Eq "bootstrap admin env is partially configured.*ADMIN_PASSWORD" " $$ {log_file}" ; then
echo "=> 首次启动需要管理员密码,请在 .env 中设置 ADMIN_PASSWORD"
2026-05-15 16:00:25 +08:00
return
fi
fi
echo "=> 未识别到明确的修复动作,请根据上面的日志继续排查。"
}
wait_for_startup() {
local pid = " $$ 1"
local timeout_seconds = " $$ 2"
local service_name = " $$ 3"
shift 3
STARTUP_WAIT_EARLY_EXIT = false
local attempts = $$ (( timeout_seconds * 10))
if [ " $$ {attempts}" -lt 1 ] ; then
attempts = 1
fi
for (( i = 0; i < attempts; i++)) ; do
if " $$ @" >/dev/null 2>& 1; then
return 0
fi
if ! kill -0 " $$ {pid}" >/dev/null 2>& 1; then
STARTUP_WAIT_EARLY_EXIT = true
echo "=> $$ {service_name} 启动进程已提前退出,请检查上面的日志。"
print_startup_failure_hint " $$ {GATEWAY_LOG_FILE}"
return 1
fi
sleep 0.1
done
if " $$ @" >/dev/null 2>& 1; then
return 0
fi
if ! kill -0 " $$ {pid}" >/dev/null 2>& 1; then
STARTUP_WAIT_EARLY_EXIT = true
echo "=> $$ {service_name} 启动进程已提前退出,请检查上面的日志。"
print_startup_failure_hint " $$ {GATEWAY_LOG_FILE}"
return 1
fi
echo "=> $$ {service_name} 在 $$ {timeout_seconds}s 内未通过启动检查。"
echo "=> 如果这是冷编译或存在并发 cargo 构建,可调大启动超时后重试。"
return 1
}
create_gateway_log_file() {
local tmp_root="$${TMPDIR : -/tmp }"
tmp_root = " $$ {tmp_root%/}"
GATEWAY_LOG_DIR = " $$ (mktemp -d " $$ { tmp_root} /aether-dev-startup.XXXXXX")"
GATEWAY_LOG_FILE = " $$ {GATEWAY_LOG_DIR}/gateway.log"
: > " $$ {GATEWAY_LOG_FILE}"
}
cleanup() {
local status="$${1 : -0}"
trap - INT TERM EXIT
if [ -n " $$ {GATEWAY_PID:-}" ] ; then
echo ""
echo "=> 停止 aether-gateway..."
kill " $$ {GATEWAY_PID}" >/dev/null 2>& 1 || true
wait " $$ {GATEWAY_PID}" >/dev/null 2>& 1 || true
fi
if [ -n " $$ {GATEWAY_LOG_FILE:-}" ] && [ -f " $$ {GATEWAY_LOG_FILE}" ] ; then
rm -f " $$ {GATEWAY_LOG_FILE}"
fi
if [ -n " $$ {GATEWAY_LOG_DIR:-}" ] && [ -d " $$ {GATEWAY_LOG_DIR}" ] ; then
rmdir " $$ {GATEWAY_LOG_DIR}" >/dev/null 2>& 1 || true
fi
exit " $$ {status}"
}
trap 'cleanup 130' INT
trap 'cleanup 143' TERM
trap 'cleanup $$?' EXIT
export APP_PORT = " $$ {APP_PORT:-8084}"
export RUST_LOG = " $$ {DEV_RUST_LOG}"
RUST_SERVICE_STARTUP_TIMEOUT_SECONDS = " $$ {RUST_SERVICE_STARTUP_TIMEOUT_SECONDS:-180}"
GATEWAY_STARTUP_TIMEOUT_SECONDS = " $$ {GATEWAY_STARTUP_TIMEOUT_SECONDS:- $$ {RUST_SERVICE_STARTUP_TIMEOUT_SECONDS}}"
export AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE = " $$ {AETHER_GATEWAY_VIDEO_TASK_TRUTH_SOURCE_MODE:-rust-authoritative}"
if dev_uses_postgres_database; then
export DATABASE_URL = "postgresql:// $$ {DB_USER:-postgres}: $$ {DB_PASSWORD:-}@ $$ {DB_HOST:-localhost}: $$ {DB_PORT:-5432}/ $$ {DB_NAME:-aether}"
if ! dotenv_has_key "AETHER_GATEWAY_DATA_POSTGRES_URL" ; then
export AETHER_GATEWAY_DATA_POSTGRES_URL = " $$ {DATABASE_URL}"
fi
fi
if dev_uses_redis_runtime; then
export REDIS_URL = "redis://: $$ {REDIS_PASSWORD:-}@ $$ {REDIS_HOST:-localhost}: $$ {REDIS_PORT:-6379}/0"
if ! dotenv_has_key "AETHER_GATEWAY_DATA_REDIS_URL" ; then
export AETHER_GATEWAY_DATA_REDIS_URL = " $$ {REDIS_URL}"
fi
else
unset REDIS_URL
unset AETHER_GATEWAY_DATA_REDIS_URL
fi
if ! dotenv_has_key "AETHER_GATEWAY_DATA_ENCRYPTION_KEY" ; then
export AETHER_GATEWAY_DATA_ENCRYPTION_KEY = " $$ {ENCRYPTION_KEY:-}"
fi
export DB_POOL_SIZE = " $$ {DB_POOL_SIZE:-5}"
export DB_MAX_OVERFLOW = " $$ {DB_MAX_OVERFLOW:-5}"
export HTTP_MAX_CONNECTIONS = " $$ {HTTP_MAX_CONNECTIONS:-20}"
export HTTP_KEEPALIVE_CONNECTIONS = " $$ {HTTP_KEEPALIVE_CONNECTIONS:-5}"
if ! command -v cargo >/dev/null 2>&1; then
echo "=> 未找到 cargo,无法启动 aether-gateway。请先安装 Rust toolchain。"
exit 1
fi
if ! command -v curl >/dev/null 2>&1; then
echo "=> 未找到 curl,无法检查 aether-gateway 健康状态。请先安装 curl。"
exit 1
fi
2026-05-20 23:42:12 +08:00
if [ -z "$${RUSTC_WRAPPER : -}" ] && command -v sccache >/dev /null 2>&1; then
export RUSTC_WRAPPER = " $$ (command -v sccache)"
echo "=> 启用 Rust 编译缓存: $$ {RUSTC_WRAPPER}"
fi
2026-05-15 16:00:25 +08:00
if ! ensure_dev_infra; then
exit 1
fi
GATEWAY_PID = ""
GATEWAY_LOG_DIR = ""
GATEWAY_LOG_FILE = ""
STARTUP_WAIT_EARLY_EXIT = false
create_gateway_log_file
echo "=> 启动 aether-gateway (Rust frontdoor : 0.0.0.0:$${APP_PORT })..."
echo "=> 日志过滤 : $${RUST_LOG }"
2026-09-03 11:05:59 +08:00
echo "=> 执行命令 : cargo run -p aether -gateway --bin aether -gateway -- --app -port $${APP_PORT }"
cargo run -p aether-gateway --bin aether-gateway -- --app-port "$${APP_PORT}" > >(
2026-05-15 16:00:25 +08:00
tee -a "$${GATEWAY_LOG_FILE}"
) 2>&1 &
GATEWAY_PID = $$ !
if ! wait_for_startup "$${GATEWAY_PID}" "$${GATEWAY_STARTUP_TIMEOUT_SECONDS}" "aether-gateway" curl -sf "http : //127.0.0.1:$${APP_PORT }/_gateway /health "; then
if [ " $$ {STARTUP_WAIT_EARLY_EXIT}" = "true" ] ; then
GATEWAY_PID = ""
fi
exit 1
fi
if wait "$${GATEWAY_PID}" ; then
gateway_exit_code = 0
else
gateway_exit_code = $$ ?
fi
GATEWAY_PID = ""
if [ "$${gateway_exit_code}" -ne 130 ] && [ "$${gateway_exit_code}" -ne 143 ]; then
echo "=> aether-gateway 运行失败并已退出,请检查上面的日志。"
print_startup_failure_hint "$${GATEWAY_LOG_FILE}"
fi
exit "$${gateway_exit_code}"
endef
export DEV_BACKEND_SCRIPT
define DEV_SCRIPT
set -euo pipefail
backend_pid = ""
frontend_pid = ""
cleanup() {
local status="$${1 : -0}"
trap - INT TERM EXIT
if [ -n " $$ {backend_pid}" ] || [ -n " $$ {frontend_pid}" ] ; then
echo ""
echo "=> 停止本地开发服务..."
if [ -n " $$ {backend_pid}" ] ; then
kill " $$ {backend_pid}" >/dev/null 2>& 1 || true
wait " $$ {backend_pid}" >/dev/null 2>& 1 || true
fi
if [ -n " $$ {frontend_pid}" ] ; then
kill " $$ {frontend_pid}" >/dev/null 2>& 1 || true
wait " $$ {frontend_pid}" >/dev/null 2>& 1 || true
fi
fi
exit " $$ {status}"
}
wait_for_backend_ready() {
while : ; do
if curl -sf "http://127.0.0.1: $$ {APP_PORT}/_gateway/health" >/dev/null 2>& 1; then
return 0
fi
if ! kill -0 " $$ {backend_pid}" >/dev/null 2>& 1; then
if wait " $$ {backend_pid}" ; then
status = 0
else
status = $$ ?
fi
if [ " $$ {status}" -ne 0 ] ; then
echo "=> 后端进程已退出 (status $$ {status})"
else
echo "=> 后端进程已退出"
fi
backend_pid = ""
cleanup " $$ {status}"
fi
sleep 0.2
done
}
trap 'cleanup 130' INT
trap 'cleanup 143' TERM
trap 'cleanup $$?' EXIT
if [ -f .env ]; then
set -a
source .env
set +a
fi
export APP_PORT = " $$ {APP_PORT:-8084}"
2026-09-03 11:05:59 +08:00
echo "=> 启动后端 : RUST_LOG =$${DEV_RUST_LOG } cargo run -p aether -gateway --bin aether -gateway -- --app -port $${APP_PORT :-8084}"
2026-05-15 16:00:25 +08:00
/bin/bash -euo pipefail -c "$$DEV_BACKEND_SCRIPT" &
backend_pid = $$ !
echo "=> 等待后端健康检查 : http ://127.0.0.1:$${APP_PORT }/_gateway /health "
wait_for_backend_ready
echo "=> 启动前端 : cd frontend && npm run dev "
( cd frontend && exec npm run dev ) &
frontend_pid = $$ !
while : ; do
if ! kill -0 " $$ {backend_pid}" >/dev/null 2>& 1; then
if wait " $$ {backend_pid}" ; then
status = 0
else
status = $$ ?
fi
if [ " $$ {status}" -ne 0 ] ; then
echo "=> 后端进程已退出 (status $$ {status})"
else
echo "=> 后端进程已退出"
fi
backend_pid = ""
cleanup " $$ {status}"
fi
if ! kill -0 " $$ {frontend_pid}" >/dev/null 2>& 1; then
if wait " $$ {frontend_pid}" ; then
status = 0
else
status = $$ ?
fi
if [ " $$ {status}" -ne 0 ] ; then
echo "=> 前端进程已退出 (status $$ {status})"
else
echo "=> 前端进程已退出"
fi
frontend_pid = ""
cleanup " $$ {status}"
fi
sleep 1
done
endef
export DEV_SCRIPT
2026-05-15 18:10:49 +08:00
define DB_TASK_SCRIPT
set -euo pipefail
2026-09-03 11:05:59 +08:00
if [ -z "$${DB_TASK_COMMAND : -}" ] || [ -z "$${DB_TASK_LABEL :-}" ]; then
echo "=> 内部错误: DB_TASK_COMMAND / DB_TASK_LABEL 未设置"
exit 1
fi
read -r -a db_task_args <<< "$${DB_TASK_COMMAND}"
if [ "$${#db_task_args[@]}" -eq 0 ]; then
echo "=> 内部错误 : DB_TASK_COMMAND 为空"
2026-05-15 18:10:49 +08:00
exit 1
fi
if [ ! -f .env ]; then
echo "=> 未找到 .env,请先执行 : cp .env .example .env "
exit 1
fi
set -a
source .env
set +a
dotenv_has_key() {
local key = " $$ 1"
grep -Eq "^[[:space:]]* $$ {key}=" .env
}
lowercase() {
printf '%s' "$$1" | tr '[ : upper :]' '[:lower :]'
}
uses_postgres_database() {
local driver
local url
driver = " $$ (lowercase " $$ { AETHER_DATABASE_DRIVER:-} ")"
url = " $$ {AETHER_DATABASE_URL:- $$ {DATABASE_URL:-}}"
if [[ -z " $$ {driver}" && -z " $$ {url}" ]] ; then
return 0
fi
[[ " $$ {driver}" == "postgres" || " $$ {driver}" == "postgresql" || " $$ {url}" == postgres:* || " $$ {url}" == postgresql:* ]]
}
if uses_postgres_database; then
export DATABASE_URL = "postgresql:// $$ {DB_USER:-postgres}: $$ {DB_PASSWORD:-}@ $$ {DB_HOST:-localhost}: $$ {DB_PORT:-5432}/ $$ {DB_NAME:-aether}"
if ! dotenv_has_key "AETHER_GATEWAY_DATA_POSTGRES_URL" ; then
export AETHER_GATEWAY_DATA_POSTGRES_URL = " $$ {DATABASE_URL}"
fi
fi
if ! dotenv_has_key "AETHER_GATEWAY_DATA_ENCRYPTION_KEY" ; then
export AETHER_GATEWAY_DATA_ENCRYPTION_KEY = " $$ {ENCRYPTION_KEY:-}"
fi
if ! command -v cargo >/dev/null 2>&1; then
echo "=> 未找到 cargo,无法执行 $${DB_TASK_LABEL}。请先安装 Rust toolchain。"
exit 1
fi
2026-09-03 11:05:59 +08:00
echo "=> 执行 $${DB_TASK_LABEL} : cargo run -p aether -gateway --bin aether -gateway -- $${db_task_args [*]}"
exec cargo run -p aether-gateway --bin aether-gateway -- "$${db_task_args[@]}"
2026-05-15 18:10:49 +08:00
endef
export DB_TASK_SCRIPT
2026-05-15 16:00:25 +08:00
dev :
@$( SHELL) -euo pipefail -c " $$ DEV_SCRIPT"
dev-backend :
@$( SHELL) -euo pipefail -c " $$ DEV_BACKEND_SCRIPT"
dev-frontend :
@cd frontend && npm run dev
2026-05-15 18:10:49 +08:00
2026-09-03 11:05:59 +08:00
db-status :
@DB_TASK_COMMAND= "db status" DB_TASK_LABEL = "数据库状态检查" $( SHELL) -euo pipefail -c " $$ DB_TASK_SCRIPT"
db-prepare :
@DB_TASK_COMMAND= "db prepare" DB_TASK_LABEL = "数据库准备" $( SHELL) -euo pipefail -c " $$ DB_TASK_SCRIPT"
2026-05-15 18:10:49 +08:00
migration :
2026-09-03 11:05:59 +08:00
@DB_TASK_COMMAND= "--migrate" DB_TASK_LABEL = "数据库迁移" $( SHELL) -euo pipefail -c " $$ DB_TASK_SCRIPT"
2026-05-15 18:10:49 +08:00
backfill :
2026-09-03 11:05:59 +08:00
@DB_TASK_COMMAND= "--apply-backfills" DB_TASK_LABEL = "数据库 backfill" $( SHELL) -euo pipefail -c " $$ DB_TASK_SCRIPT"