Files
temporal/cmd/tools/protogen/main.go
Roey Berman 4c59cd15d7 Add support for protos in chasm libs (#8182)
## What changed?

Added support for defining protos in chasm libs.

## Why?

Keep everything local to the library.

## How did you test it?
- [x] built
- [x] run locally and tested manually
2025-08-15 18:09:58 +00:00

444 lines
14 KiB
Go

package main
import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/fatih/color"
)
var cyan = color.New(color.FgHiCyan, color.Bold)
func info(format string, args ...interface{}) {
log.Println(cyan.Sprintf(format, args...))
}
// runCommand executes a shell command and returns an error if it fails
func runCommand(ctx context.Context, name string, args ...string) error {
cmd := exec.CommandContext(ctx, name, args...)
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
return cmd.Run()
}
func copyRecursive(src, dst string) error {
srcInfo, err := os.Stat(src)
if err != nil {
return fmt.Errorf("error stating source %s: %w", src, err)
}
if !srcInfo.IsDir() {
return fmt.Errorf("source %s is not a directory", src)
}
err = os.MkdirAll(dst, srcInfo.Mode())
if err != nil {
return fmt.Errorf("error creating destination directory %s: %w", dst, err)
}
entries, err := os.ReadDir(src)
if err != nil {
return fmt.Errorf("error reading source directory %s: %w", src, err)
}
for _, entry := range entries {
srcPath := filepath.Join(src, entry.Name())
dstPath := filepath.Join(dst, entry.Name())
srcInfo, err := entry.Info()
if err != nil {
return fmt.Errorf("error getting info for %s: %w", srcPath, err)
}
if entry.IsDir() {
if err := copyRecursive(srcPath, dstPath); err != nil {
return err
}
} else {
// Fix imports in the generated mock file
content, err := os.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("error reading file %s: %w", srcPath, err)
}
if err := os.WriteFile(dstPath, []byte(content), srcInfo.Mode()); err != nil {
return fmt.Errorf("error writing file %s: %w", dstPath, err)
}
}
}
return nil
}
// exists checks if a path exists
func exists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
func existsDir(path string) bool {
stat, err := os.Stat(path)
return err == nil && stat.IsDir()
}
// replaceInFile replaces text in a file using sed-like patterns
func replaceInFile(filePath, oldPattern, newPattern string) error {
content, err := os.ReadFile(filePath)
if err != nil {
return err
}
// Convert sed pattern to Go regex
re, err := regexp.Compile(oldPattern)
if err != nil {
return err
}
newContent := re.ReplaceAllString(string(content), newPattern)
return os.WriteFile(filePath, []byte(newContent), 0644)
}
type generator struct {
rootDir string
tempOut string
tempProtoRoot string
protoOut string
protoBackup string
protogen string
apiBinpb string
protoRoot string
goimports string
mockgen string
chasmLibDirs []string
}
func newGenerator() (*generator, error) {
var gen generator
flag.StringVar(&gen.protoOut, "proto-out", "", "Proto output directory (required)")
flag.StringVar(&gen.protoRoot, "proto-root", "", "Proto root directory (required)")
flag.StringVar(&gen.rootDir, "root", "", "Root directory (required)")
flag.StringVar(&gen.protogen, "protogen", "", "Path to protogen binary (required)")
flag.StringVar(&gen.apiBinpb, "api-binpb", "", "Path to API binpb file (required)")
flag.StringVar(&gen.goimports, "goimports", "", "Path to goimports binary (required)")
flag.StringVar(&gen.mockgen, "mockgen", "", "Path to mockgen binary (required)")
flag.Parse()
// Validate required flags
if gen.protoOut == "" || gen.protoRoot == "" || gen.rootDir == "" || gen.protogen == "" ||
gen.apiBinpb == "" || gen.goimports == "" || gen.mockgen == "" {
flag.Usage()
return nil, errors.New("all flags are required")
}
chasmDir := filepath.Join(gen.rootDir, "chasm", "lib")
ls, err := os.ReadDir(chasmDir)
if err != nil {
return nil, fmt.Errorf("error reading chasm/lib directory %s: %w", chasmDir, err)
}
gen.chasmLibDirs = make([]string, 0, len(ls))
for _, entry := range ls {
if !entry.IsDir() {
continue
}
gen.chasmLibDirs = append(gen.chasmLibDirs, filepath.Join(chasmDir, entry.Name()))
}
gen.tempOut = gen.protoOut + ".new"
gen.tempProtoRoot = gen.protoRoot + ".tmp"
gen.protoBackup = gen.protoOut + ".old"
return &gen, nil
}
func (g *generator) removeExistingGenDirs() error {
for _, dir := range g.chasmLibDirs {
genDir := filepath.Join(dir, "gen")
if err := os.RemoveAll(genDir); err != nil {
return fmt.Errorf("error removing directory %s: %w", genDir, err)
}
}
return nil
}
func (g *generator) backupProtos() error {
if exists(g.protoOut) {
if err := os.Rename(g.protoOut, g.protoBackup); err != nil {
return fmt.Errorf("error backing up proto output directory %s to %s: %w", g.protoOut, g.protoBackup, err)
}
}
return nil
}
func (g *generator) prepareTempDirs() error {
// Remove and create new directory
if err := os.RemoveAll(g.tempOut); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error removing directory %s: %w", g.tempOut, err)
}
if err := os.MkdirAll(g.tempOut, 0755); err != nil {
return fmt.Errorf("error creating directory %s: %w", g.tempOut, err)
}
if err := os.RemoveAll(g.tempProtoRoot); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error removing directory %s: %w", g.tempOut, err)
}
if err := copyRecursive(g.protoRoot, g.tempProtoRoot); err != nil {
return fmt.Errorf("error copying proto root %s to %s: %w", g.protoRoot, g.tempProtoRoot, err)
}
return nil
}
func (g *generator) copyChasmLibProtos() error {
for _, dir := range g.chasmLibDirs {
protoDir := filepath.Join(dir, "proto")
if !existsDir(protoDir) {
continue
}
destDir := filepath.Join(g.tempProtoRoot, "internal", "temporal", "server", "chasm", "lib", filepath.Base(dir))
if err := os.MkdirAll(destDir, 0755); err != nil {
return fmt.Errorf("error creating directory %s: %w", destDir, err)
}
if err := copyRecursive(protoDir, filepath.Join(destDir, "proto")); err != nil {
return fmt.Errorf("error copying proto files from %s to %s: %w", protoDir, destDir, err)
}
protos, err := filepath.Glob(filepath.Join(destDir, "proto", "**", "*.proto"))
if err != nil {
return fmt.Errorf("error finding proto files in %s: %w", destDir, err)
}
for _, proto := range protos {
if err := replaceInFile(proto, `import "chasm`, `import "temporal/server/chasm`); err != nil {
return fmt.Errorf("error updating import path in %s: %w", proto, err)
}
}
}
return nil
}
func (g *generator) runProtogen(ctx context.Context) error {
// Run protogen
protoArgs := []string{
"--descriptor_set_in=" + g.apiBinpb,
"--root=" + filepath.Join(g.tempProtoRoot, "internal"),
"--rewrite-enum=BuildId_State:BuildId",
"--output=" + g.tempOut,
"-p", "go-grpc_out=paths=source_relative:" + g.tempOut,
"-p", "go-helpers_out=paths=source_relative:" + g.tempOut,
}
if err := runCommand(ctx, g.protogen, protoArgs...); err != nil {
return fmt.Errorf("error running protogen: %w", err)
}
return nil
}
func (g *generator) runGoImports(ctx context.Context) error {
if err := runCommand(ctx, g.goimports, "-w", g.tempOut); err != nil {
return fmt.Errorf("error running goimports: %w", err)
}
return nil
}
func (g *generator) generateProtoMocks(ctx context.Context) error {
// Find service.pb.go and service_grpc.pb.go files
return filepath.Walk(g.tempOut, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if strings.HasSuffix(path, "service.pb.go") || strings.HasSuffix(path, "service_grpc.pb.go") {
// Convert service/ to servicemock/ and .go to .mock.go
dst := strings.ReplaceAll(path, "service/", "servicemock/")
dst = strings.ReplaceAll(dst, ".go", ".mock.go")
// Get package name
dstDir := filepath.Dir(dst)
pkg := filepath.Base(filepath.Dir(dstDir))
// Create destination directory
if err := os.MkdirAll(dstDir, 0755); err != nil {
return fmt.Errorf("error creating directory %s: %v", dstDir, err)
}
// Run mockgen
mockgenArgs := []string{
"-package", pkg,
"-source", path,
"-destination", dst,
}
if err := runCommand(ctx, g.mockgen, mockgenArgs...); err != nil {
return fmt.Errorf("error running mockgen: %v", err)
}
// Fix imports in the generated mock file
content, err := os.ReadFile(dst)
if err != nil {
return fmt.Errorf("error reading file %s: %v", dst, err)
}
// Replace the incorrect import path
newContent := strings.ReplaceAll(string(content), g.tempOut+"/temporal/server/", "")
if err := os.WriteFile(dst, []byte(newContent), 0644); err != nil {
return fmt.Errorf("error writing file %s: %v", dst, err)
}
}
return nil
})
}
func (g *generator) modifyHistoryServiceFile() error {
historyServiceFile := filepath.Join(g.tempOut, "temporal", "server", "api", "historyservice", "v1", "service_grpc.pb.go")
// Replace GetWorkflowExecutionHistory method signature
err := replaceInFile(historyServiceFile,
`GetWorkflowExecutionHistory\(context\.Context, \*GetWorkflowExecutionHistoryRequest\) \(\*GetWorkflowExecutionHistoryResponse, error\)`,
`GetWorkflowExecutionHistory(context.Context, *GetWorkflowExecutionHistoryRequest) (*GetWorkflowExecutionHistoryResponseWithRaw, error)`)
if err != nil {
return fmt.Errorf("error modifying GetWorkflowExecutionHistory: %w", err)
}
// Replace RecordWorkflowTaskStarted method signature
err = replaceInFile(historyServiceFile,
`RecordWorkflowTaskStarted\(context\.Context, \*RecordWorkflowTaskStartedRequest\) \(\*RecordWorkflowTaskStartedResponse, error\)`,
`RecordWorkflowTaskStarted(context.Context, *RecordWorkflowTaskStartedRequest) (*RecordWorkflowTaskStartedResponseWithRawHistory, error)`)
if err != nil {
return fmt.Errorf("error modifying RecordWorkflowTaskStarted: %w", err)
}
return nil
}
func (g *generator) moveProtoFiles() error {
sourceApiDir := filepath.Join(g.tempOut, "temporal", "server", "api")
return os.Rename(sourceApiDir, g.protoOut)
}
func (g *generator) moveGeneratedChasmFiles() error {
sourceChasmDir := filepath.Join(g.tempOut, "temporal", "server", "chasm")
return filepath.Walk(sourceChasmDir, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
// Calculate relative path from sourceChasmDir
relPath, err := filepath.Rel(sourceChasmDir, path)
if err != nil {
return err
}
// Transform relPath to match the destination structure
// e.g., "lib/<name>/proto/v1/*" -> "lib/<name>/gen/<name>pb/*"
re := regexp.MustCompile(`^lib/([^/]+)/proto/(v\d+)/(.*)$`)
parts := re.FindStringSubmatch(relPath)
if len(parts) < 4 {
return nil
}
dstPath := filepath.Join("chasm", "lib", parts[1], "gen", parts[1]+"pb", parts[2], parts[3])
dirName := filepath.Dir(dstPath) // Ensure the destination path is a base path
if err := os.MkdirAll(dirName, 0755); err != nil {
return fmt.Errorf("error creating directory %s: %w", dirName, err)
}
if err := os.Rename(path, dstPath); err != nil {
return fmt.Errorf("error moving file %s to %s: %w", path, dstPath, err)
}
return nil
})
}
func (g *generator) cleanup(restoreOld bool) error {
// Remove temporary directories
if err := os.RemoveAll(g.tempProtoRoot); err != nil {
return fmt.Errorf("error removing temporary proto root %s: %w", g.tempProtoRoot, err)
}
if err := os.RemoveAll(g.tempOut); err != nil {
return fmt.Errorf("error removing temporary output directory %s: %w", g.tempOut, err)
}
if restoreOld {
// Restore the old proto output directory
if exists(g.protoBackup) {
if err := os.Rename(g.protoBackup, g.protoOut); err != nil {
return fmt.Errorf("error restoring proto output directory from %s to %s: %w", g.protoBackup, g.protoOut, err)
}
}
} else {
// Remove the old proto output directory if it exists
if err := os.RemoveAll(g.protoBackup); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("error removing backup proto output directory %s: %w", g.protoBackup, err)
}
}
return nil
}
func main() {
ctx := context.Background()
gen, err := newGenerator()
if err != nil {
log.Fatalln("Error creating generator:", err)
}
info("Removing existing CHASM gen directories...")
if err := gen.removeExistingGenDirs(); err != nil {
log.Fatalln("Error removing existing CHASM gen directories:", err)
}
info("Backing up existing proto output directory...")
if err := gen.backupProtos(); err != nil {
log.Fatalln("Error backing up proto output directory:", err)
}
genErr := generate(ctx, gen)
cleanupErr := gen.cleanup(genErr != nil)
if cleanupErr != nil {
cleanupErr = fmt.Errorf("error cleaning up after generation: %w", cleanupErr)
}
err = errors.Join(genErr, cleanupErr)
if err != nil {
log.Fatalln("Generation failed:", err)
}
}
func generate(ctx context.Context, gen *generator) error {
info("Preparing temp directories...")
if err := gen.prepareTempDirs(); err != nil {
return fmt.Errorf("error preparing new gen directory: %w", err)
}
info("Copying CHASM lib protos...")
if err := gen.copyChasmLibProtos(); err != nil {
return fmt.Errorf("error copying CHASM lib protos: %w", err)
}
info("Running protoc for proto files...")
if err := gen.runProtogen(ctx); err != nil {
return fmt.Errorf("error running protogen: %w", err)
}
info("Running goimports for proto files...")
if err := gen.runGoImports(ctx); err != nil {
return fmt.Errorf("error running goimports: %w", err)
}
info("Generating proto mocks...")
if err := gen.generateProtoMocks(ctx); err != nil {
return fmt.Errorf("error generating mock files: %w", err)
}
info("Modifying history service server interface...")
if err := gen.modifyHistoryServiceFile(); err != nil {
return fmt.Errorf("error modifying history service file: %w", err)
}
info("Moving proto files into place...")
if err := gen.moveProtoFiles(); err != nil {
return fmt.Errorf("error moving proto files: %w", err)
}
info("Moving generated CHASM files into place...")
if err := gen.moveGeneratedChasmFiles(); err != nil {
return fmt.Errorf("error moving CHASM files: %w", err)
}
return nil
}