功能:使用go写的小工具,批量生成目录(包含子目录)下的proto文件自动新增注册文件
目录说明 protos 根目录 src proto源文件 gen pb生成目录
目录如下:
🗂️ └─protos
├─gen
│ ├─cpp
│ │ ├─common
│ │ │ base.auto.cpp
│ │ │ base.pb.cc
│ │ │ base.pb.h
│ │ │ net_msg.auto.cpp
│ │ │ net_msg.pb.cc
│ │ │ net_msg.pb.h
│ │ │
│ │ └─msg
│ │ user.auto.cpp
│ │ user.pb.cc
│ │ user.pb.h
│ │
│ └─lua
│ ├─common
│ │ base.pb
│ │ net_msg.pb
│ │
│ └─msg
│ user.pb
│
└─src
├─common
│ base.proto
│ net_msg.proto
│
└─msg
user.proto
go代码
package main
import (
"bytes"
"fmt"
"io/fs"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"sync"
"time"
)
type Options struct {
Clean bool
Watch bool
Only string // "", "cpp", "lua"
Jobs int
DryRun bool
ProtoRoot string
}
func main() {
exePath, _ := os.Executable()
exeDir := filepath.Dir(exePath)
opts := parseArgs()
if opts.ProtoRoot == "" {
opts.ProtoRoot = filepath.Join(exeDir, "protos")
}
srcDir := filepath.Join(opts.ProtoRoot, "src")
genDir := filepath.Join(opts.ProtoRoot, "gen")
cppOut := filepath.Join(genDir, "cpp")
luaOut := filepath.Join(genDir, "lua")
if !exists(srcDir) {
fmt.Println("❌ proto 目录不存在:", srcDir)
os.Exit(1)
}
protoc := findProtoc(exeDir)
if protoc == "" {
fmt.Println("❌ 找不到 protoc(PATH 或 ../protoc/protoc)")
os.Exit(1)
}
// -------------------- clean 逻辑 --------------------
if opts.Clean {
fmt.Println("🧹 清理旧生成目录:", genDir)
if !opts.DryRun {
os.RemoveAll(genDir)
}
return // clean 后不生成任何文件
}
// -------------------- 生成逻辑 --------------------
runOnce := func() {
var protoFiles []string
filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() && strings.HasSuffix(path, ".proto") {
protoFiles = append(protoFiles, path)
}
return nil
})
if len(protoFiles) == 0 {
fmt.Println("❌ 没有找到 proto 文件")
return
}
jobs := opts.Jobs
if jobs <= 0 {
jobs = 1
}
sem := make(chan struct{}, jobs)
var wg sync.WaitGroup
for _, proto := range protoFiles {
p := proto
wg.Add(1)
go func() {
defer wg.Done()
sem <- struct{}{}
defer func() { <-sem }()
rel, _ := filepath.Rel(srcDir, p)
// 获取 proto 文件的包名(不含扩展名),用于生成 auto.cpp
protoName := strings.TrimSuffix(filepath.Base(p), ".proto")
// 计算 proto 文件相对于 srcDir 的子目录(不含文件名)
subDir := filepath.Dir(rel)
if subDir == "." {
subDir = ""
}
fmt.Println("🚀 生成:", rel)
// -------------------- 生成 C++ --------------------
if opts.Only == "" || opts.Only == "cpp" {
// 修复:直接使用 cppOut 作为根目录,让 protoc 自动创建子目录
// protoc 会根据 --proto_path 和 proto 文件的相对路径自动创建目录结构
args := []string{
"--proto_path=" + srcDir,
"--cpp_out=" + cppOut,
p,
}
runOrPrint(protoc, args, opts.DryRun)
if !opts.DryRun {
// 计算 pb 文件实际生成的目录(protoc 会自动创建与 src 相同的目录结构)
actualOutDir := filepath.Join(cppOut, subDir)
// 去掉 final
fixCppFinal(actualOutDir)
// 生成 auto.cpp,与 pb 文件在同一目录
autoCpp := filepath.Join(actualOutDir, protoName+".auto.cpp")
if err := genAutoCpp(p, autoCpp, protoName); err != nil {
fmt.Println("⚠️ 生成 auto.cpp 失败:", err)
}
}
}
// -------------------- 生成 Lua --------------------
if opts.Only == "" || opts.Only == "lua" {
// Lua 生成同样修复
pbName := protoName + ".pb"
actualLuaDir := filepath.Join(luaOut, subDir)
if !opts.DryRun {
os.MkdirAll(actualLuaDir, 0755)
}
pbOut := filepath.Join(actualLuaDir, pbName)
args := []string{
"--proto_path=" + srcDir,
"--descriptor_set_out=" + pbOut,
"--include_imports",
p,
}
runOrPrint(protoc, args, opts.DryRun)
}
}()
}
wg.Wait()
fmt.Println("✅ 本轮生成完成")
}
runOnce()
if opts.Watch {
fmt.Println("👀 监听 proto 变化中... (Ctrl+C 退出)")
last := time.Now()
for {
time.Sleep(time.Second)
changed := false
filepath.WalkDir(srcDir, func(path string, d fs.DirEntry, err error) error {
if !d.IsDir() && strings.HasSuffix(path, ".proto") {
info, _ := d.Info()
if info.ModTime().After(last) {
changed = true
}
}
return nil
})
if changed {
fmt.Println("🔄 检测到变更,重新生成...")
runOnce()
last = time.Now()
}
}
}
}
// -------------------- 工具函数 --------------------
func parseArgs() Options {
var a Options
args := os.Args[1:]
for i := 0; i < len(args); i++ {
switch args[i] {
case "--clean":
a.Clean = true
case "--watch":
a.Watch = true
case "--dry-run":
a.DryRun = true
case "--only":
if i+1 < len(args) {
a.Only = args[i+1]
i++
}
case "-j":
if i+1 < len(args) {
fmt.Sscan(args[i+1], &a.Jobs)
i++
}
default:
if !strings.HasPrefix(args[i], "-") {
a.ProtoRoot = args[i]
}
}
}
return a
}
func runOrPrint(protoc string, args []string, dry bool) {
if dry {
fmt.Println("DRY:", protoc, strings.Join(args, " "))
return
}
cmd := exec.Command(protoc, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
fmt.Println("❌ 执行失败:", err)
os.Exit(1)
}
}
func findProtoc(exeDir string) string {
if path, err := exec.LookPath("protoc"); err == nil {
return path
}
local := filepath.Join(exeDir, "..", "protoc", "protoc")
if exists(local) {
return local
}
return ""
}
func exists(p string) bool {
_, err := os.Stat(p)
return err == nil
}
// 去掉 C++ final
func fixCppFinal(dir string) {
filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
if strings.HasSuffix(path, ".h") || strings.HasSuffix(path, ".hpp") {
data, _ := os.ReadFile(path)
newData := bytes.ReplaceAll(data, []byte(" final"), []byte(""))
os.WriteFile(path, newData, 0644)
}
return nil
})
}
// -------------------- 自动生成 C++ auto.cpp --------------------
func parseProtoMessages(protoFile string) ([]string, error) {
data, err := os.ReadFile(protoFile)
if err != nil {
return nil, err
}
re := regexp.MustCompile(`(?m)^message\s+(\w+)`)
matches := re.FindAllStringSubmatch(string(data), -1)
var msgs []string
for _, m := range matches {
if len(m) > 1 {
msgs = append(msgs, m[1])
}
}
return msgs, nil
}
// 修复:增加 protoName 参数,确保生成的文件名和路径正确对应
func genAutoCpp(protoPath, autoCpp string, protoName string) error {
msgs, err := parseProtoMessages(protoPath)
if err != nil {
return err
}
var b strings.Builder
b.WriteString("#include \"net_msg.h\"\n")
b.WriteString("\n")
// b.WriteString(fmt.Sprintf("#include \"%s.pb.h\"\n\n", protoName))
b.WriteString(fmt.Sprintf("namespace %s {\n", protoName))
b.WriteString("\n")
b.WriteString(" int reg_msg()\n")
b.WriteString(" {\n")
b.WriteString(" // 开始注册消息\n")
for _, m := range msgs {
b.WriteString(fmt.Sprintf(" msg_reg_normal(%s);\n", m))
}
b.WriteString(" return 0;\n")
b.WriteString(" }\n\n")
b.WriteString(" auto _ = reg_msg();\n")
b.WriteString("}\n")
return os.WriteFile(autoCpp, []byte(b.String()), 0644)
}
使用示例
编译
go build -o proto_gen proto_gen.go
生成 可以指定目录 否则默认当前目录的protos
./proto_gen
清理
./proto_gen --clean
无人值守 监听改变自动编译pb 多线程
.\proto_gen.exe --watch -j 8
Enjoy ~。~