#!/bin/bash # Proto 文件编译脚本(自动扫描模式) # ---------------------------------------------------------------------------- # 扫描 proto/*.proto 自动编译为 Go 代码: # - 含 service 的 proto:生成 .pb.go + .triple.go # - 不含 service 的 proto(如 common.proto / event.proto):只生成 .pb.go # # protoc-gen-go(pb.go)按 --go_out + 源文件名输出; # protoc-gen-go-triple v3.0.0(triple.go)忽略 --go-triple_out, # 严格按 go_package 第一段生成目录、按源文件名生成文件。 # 因此同一个 proto 的两个产物可能落在不同目录(典型:castlove_config.proto # → pb.go 在 pkg/proto/castlove_config/,triple.go 在 pkg/proto/castlove/)。 # # 兼容 bash 3.2(macOS 自带 bash),不使用 declare -A 关联数组。 # # 新增 proto 文件时无需修改本脚本,放到 proto/ 目录下即可。 # ---------------------------------------------------------------------------- set -e SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR/.." export PATH="$PATH:$(go env GOPATH)/bin" echo "======================================" echo "开始编译 Proto 文件(自动扫描模式)" echo "======================================" # --- 检查 protoc --- if ! command -v protoc &> /dev/null; then echo "❌ protoc 未安装!" echo " macOS: brew install protobuf" echo " Ubuntu: apt-get install protobuf-compiler" exit 1 fi echo "✅ protoc 版本: $(protoc --version)" # --- 检查 Go 插件 --- MISSING_PLUGINS=false if ! command -v protoc-gen-go &> /dev/null; then echo "❌ protoc-gen-go 未安装" MISSING_PLUGINS=true fi if ! command -v protoc-gen-go-triple &> /dev/null; then echo "❌ protoc-gen-go-triple 未安装" MISSING_PLUGINS=true fi if [ "$MISSING_PLUGINS" = true ]; then echo "" echo "请安装缺失的插件:" echo " go install google.golang.org/protobuf/cmd/protoc-gen-go@latest" echo " go install github.com/dubbogo/protoc-gen-go-triple/v3@latest" exit 1 fi echo "✅ 所有必需插件已安装" echo "" # --- 工具函数:从 proto 文件解析 go_package 第一段(生成路径段),用于定位 triple.go 输出目录 --- # protoc-gen-go-triple v3.0.0 忽略 --go-triple_out,严格按 go_package 第一段生成路径, # 但文件名仍用源文件名(如 castlove_config.proto → .../castlove/castlove_config.triple.go)。 # 退化:解析失败时按源文件名推断。 resolve_triple_out_dir() { local name="$1" local proto_file="proto/${name}.proto" local go_pkg_path # 用 sed -E 替代 grep -oP(macOS BSD grep 不支持 -P;sed -E 是 POSIX ERE) # 匹配 option go_package = "PATH;NAME";,提取 PATH 段 go_pkg_path=$(sed -nE 's/.*option go_package = "([^";]+);.*/\1/p' "$proto_file" | head -1) if [ -n "$go_pkg_path" ]; then echo "pkg/proto/$(basename "$go_pkg_path")" else echo "pkg/proto/$name" fi } # --- 工具函数:从 protoc-gen-go-triple v3 默认输出位置把 triple.go 搬到目标目录 --- move_triple_files() { local name="$1" # proto 文件名(不含扩展名) local target_dir="$2" # triple.go 的目标目录(pkg/proto/) local triple_file="${name}.triple.go" # v3 生成位置:github.com//.triple.go(go_pkg 第一段 + 源文件名) local src_file="github.com/topfans/backend/pkg/proto/$(basename "$target_dir")/${triple_file}" if [ -f "$src_file" ]; then mv "$src_file" "$target_dir/" # 清掉残留的空目录(v3 在 github.com/ 下生成的路径骨架) local pkg_dir_name=$(basename "$target_dir") rm -rf "github.com/topfans/backend/pkg/proto/${pkg_dir_name}" echo " ✅ ${triple_file} 已移动到 ${target_dir}/" else # 兜底:尝试源文件名目录(兼容不同版本行为) local fallback="github.com/topfans/backend/pkg/proto/${name}/${triple_file}" if [ -f "$fallback" ]; then mv "$fallback" "$target_dir/" rm -rf "github.com/topfans/backend/pkg/proto/${name}" echo " ✅ ${triple_file} 已在兜底路径找到并移动" else echo " ⚠️ 未找到 ${src_file}(v3 插件输出位置可能已变,需检查)" fi fi } # --- 跨包类型引用后处理:protoc-gen-go-triple v3.0.0 在跨包类型作 RPC 参数时丢包前缀 --- # 已知触发:statistic.proto 引用 event.proto(event.Event / event.BatchEventRequest 作 RPC 参数)。 # 如果以后其他 proto 也出现同样症状,把 name 加到下方 case 分支即可。 patch_cross_package_refs() { local name="$1" local triple_dir triple_dir=$(resolve_triple_out_dir "$name") case "$name" in statistic) local TRIPLE_FILE="${triple_dir}/${name}.triple.go" if [ ! -f "$TRIPLE_FILE" ]; then return fi if grep -q 'github.com/topfans/backend/pkg/proto/event"' "$TRIPLE_FILE"; then return # 已修复过 fi python3 -c " import re with open('$TRIPLE_FILE', 'r') as f: src = f.read() pattern = re.compile(r'(import\s*\([^)]*\))', re.MULTILINE) blocks = list(pattern.finditer(src)) if len(blocks) >= 2: second = blocks[1] block_text = second.group(1) if 'pkg/proto/event' not in block_text: new_block = block_text[:-1].rstrip() + '\n\tevent \"github.com/topfans/backend/pkg/proto/event\"\n)' src = src[:second.start()] + new_block + src[second.end():] with open('$TRIPLE_FILE', 'w') as f: f.write(src) " sed -i '' \ -e 's/\*Event\([,)]\)/\*event.Event\1/g' \ -e 's/\*Event$/&XXX/g' \ -e 's/\*Event[^a-zA-Z._)/]/\*event.Event\&/g' \ -e 's/new(Event)/new(event.Event)/g' \ -e 's/\*BatchEventRequest\([,)]\)/\*event.BatchEventRequest\1/g' \ -e 's/new(BatchEventRequest)/new(event.BatchEventRequest)/g' \ "$TRIPLE_FILE" sed -i '' \ -e 's/(\*Event)/(\*event.Event)/g' \ -e 's/(\*BatchEventRequest)/(\*event.BatchEventRequest)/g' \ "$TRIPLE_FILE" echo " ✅ ${TRIPLE_FILE} 已修复跨包类型引用(protoc-gen-go-triple v3.0.0 bug)" ;; esac } # --- 扫描 proto/*.proto --- shopt -s nullglob PROTO_FILES=(proto/*.proto) shopt -u nullglob if [ ${#PROTO_FILES[@]} -eq 0 ]; then echo "❌ proto/ 目录下未发现 .proto 文件" exit 1 fi # 按文件名排序,保证输出稳定、可复现 IFS=$'\n' PROTO_FILES=($(printf '%s\n' "${PROTO_FILES[@]}" | sort)) unset IFS echo "🔍 发现 ${#PROTO_FILES[@]} 个 proto 文件:" for f in "${PROTO_FILES[@]}"; do echo " - $(basename "$f")" done echo "" # --- 创建 pkg/proto 子目录 --- echo "📁 创建目标目录..." for f in "${PROTO_FILES[@]}"; do name=$(basename "$f" .proto) pb_dir="pkg/proto/$name" triple_dir=$(resolve_triple_out_dir "$name") mkdir -p "$pb_dir" [ "$triple_dir" != "$pb_dir" ] && mkdir -p "$triple_dir" done echo "" # --- 逐个编译 --- echo "⚙️ 开始编译..." for f in "${PROTO_FILES[@]}"; do name=$(basename "$f" .proto) pb_dir="pkg/proto/$name" triple_dir=$(resolve_triple_out_dir "$name") has_service=false if grep -q '^service ' "$f"; then has_service=true fi if [ "$has_service" = true ]; then if [ "$pb_dir" = "$triple_dir" ]; then echo "📦 编译 ${name}.proto (含 service → pb + triple 同目录)" else echo "📦 编译 ${name}.proto (含 service → pb@${pb_dir}/, triple@${triple_dir}/)" fi protoc \ --proto_path=proto \ --proto_path=. \ --go_out="$pb_dir" \ --go_opt=paths=source_relative \ --go-triple_out="$triple_dir" \ --go-triple_opt=paths=source_relative \ "$name.proto" move_triple_files "$name" "$triple_dir" else # 不含 service:不生成 triple.go(避免未使用 import 编译失败) echo "📦 编译 ${name}.proto (无 service → 仅生成 pb@${pb_dir}/)" protoc \ --proto_path=proto \ --proto_path=. \ --go_out="$pb_dir" \ --go_opt=paths=source_relative \ "$name.proto" fi patch_cross_package_refs "$name" done echo "" echo "✅ 编译阶段完成" echo "" # --- 清理 protoc 临时产物 --- echo "🔄 清理冗余文件..." if [ -d "github.com" ]; then rm -rf github.com echo " ✅ github.com/ 目录已清理" fi for f in "${PROTO_FILES[@]}"; do name=$(basename "$f" .proto) pb_dir="pkg/proto/$name" triple_dir=$(resolve_triple_out_dir "$name") if [ -f "proto/${name}.pb.go" ]; then rm "proto/${name}.pb.go" echo " ✅ proto/${name}.pb.go 已清理" fi if [ -f "proto/${name}.triple.go" ]; then rm "proto/${name}.triple.go" echo " ✅ proto/${name}.triple.go 已清理" fi for d in "$pb_dir" "$triple_dir"; do [ "$d" = "$pb_dir" ] && [ "$d" = "$triple_dir" ] && continue if [ -d "$d/proto" ]; then rm -rf "$d/proto" echo " ✅ ${d}/proto/ 子目录已清理" fi done done echo "" # --- 汇总输出 --- echo "📂 生成的文件结构:" echo " pkg/proto/" # 收集所有出现过的目录(pb_dir + triple_dir 去重),按目录名排序 TMP_DIRS=$(mktemp) trap 'rm -f "$TMP_DIRS"' EXIT for f in "${PROTO_FILES[@]}"; do name=$(basename "$f" .proto) pb_dir="pkg/proto/$name" triple_dir=$(resolve_triple_out_dir "$name") echo "$pb_dir" >> "$TMP_DIRS" echo "$triple_dir" >> "$TMP_DIRS" done # 按目录排序后去重,逐个打印 sort -u "$TMP_DIRS" | while read -r d; do dir_name=$(basename "$d") pb_files=$(find "$d" -maxdepth 1 -name "*.pb.go" 2>/dev/null | sort) triple_files=$(find "$d" -maxdepth 1 -name "*.triple.go" 2>/dev/null | sort) echo " ├── ${dir_name}/" for pf in $pb_files; do echo " │ ├── $(basename "$pf")" done for tf in $triple_files; do echo " │ └── $(basename "$tf")" done done GENERATED_FILES=$(find pkg/proto -name "*.pb.go" -o -name "*.triple.go" 2>/dev/null | wc -l | tr -d ' ') echo "" echo "======================================" echo "✅ 所有 Proto 文件编译完成!" echo "======================================" echo "生成文件总数: ${GENERATED_FILES}" echo "" echo "提示:以后新增 proto 只需放到 proto/ 目录,无需修改本脚本。"