go 批量生成 c++与lua的proto文件

功能:使用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 ~。~

相关推荐
万能的小裴同学1 小时前
饥荒Mod
java·开发语言·junit
foxsen_xia1 小时前
Kamailio通过Lua写路由
开发语言·lua·信息与通信
燃于AC之乐1 小时前
深入解剖STL set/multiset:接口使用与核心特性详解
开发语言·c++·stl·面试题·set·multiset
REDcker1 小时前
Paho MQTT C 开发者快速入门
c语言·开发语言·mqtt
破烂pan1 小时前
Python 实现 HTTP Client 的常见方式
开发语言·python·http
J_liaty1 小时前
SpringBoot缓存预热:ApplicationRunner与CommandLineRunner深度对比与实战
spring boot·后端·缓存
宁酱醇1 小时前
ORACLE 练习1
java·开发语言
HAPPY酷1 小时前
现代 C++ 并发服务器的核心模式
服务器·开发语言·c++
139的世界真奇妙2 小时前
工作事宜思考点
经验分享·笔记·golang·go