Skip to content

Commit dccae52

Browse files
duckbrainraphael
andauthored
Allow setting protoc command via Meta (#3633)
* Allow setting protoc command via Meta * Expose DefaultProtoc for resetting protoc command and update documentation * Add a test for passing alternate protoc commands * Document const DefaultProtoc --------- Co-authored-by: Raphael Simon <[email protected]>
1 parent d04a160 commit dccae52

File tree

5 files changed

+122
-5
lines changed

5 files changed

+122
-5
lines changed

dsl/meta.go

+32
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,12 @@ import (
55
"goa.design/goa/v3/expr"
66
)
77

8+
// DefaultProtoc is the default command to be invoked for generating code from protobuf schemas.
9+
// You may use this to prepend arguments/flags to the command or revert to the default command.
10+
//
11+
// See also [Meta]; this is useful with the "protoc:cmd" key.
12+
const DefaultProtoc = expr.DefaultProtoc
13+
814
// Meta defines a set of key/value pairs that can be assigned to an object. Each
915
// value consists of a slice of strings so that multiple invocation of the Meta
1016
// function on the same target using the same key builds up the slice.
@@ -124,6 +130,32 @@ import (
124130
// })
125131
// })
126132
//
133+
// - "protoc:cmd" provides an alternate command to execute for protoc with
134+
// optional arguments. Applicable to API and service definitions only. If used
135+
// on an API definition the include paths are used for all services, unless
136+
// specified otherwise for specific services. The first value will be used as
137+
// the command, and the following values will be used as initial arguments to
138+
// that command. The given command will have additional arguments appended and
139+
// is expected to behave similar to protoc.
140+
//
141+
// Can be used to specify custom options or alternate implementations. The
142+
// default command can be specified using DefaultProtoc.
143+
//
144+
// // Use Go run to run a drop-in replacement for protoc.
145+
// var _ = API("myapi", func() {
146+
// Meta("protoc:cmd", "go", "run", "github.com/duckbrain/goprotoc")
147+
// })
148+
//
149+
// // Specify the full path to protoc and turn on fatal warnings.
150+
// var _ = Service("service1", func() {
151+
// Meta("protoc:cmd", "/usr/bin/protoc", "--fatal_warnings")
152+
// })
153+
//
154+
// // Restore defaults for a specific service.
155+
// var _ = Service("service2", func() {
156+
// Meta("protoc:cmd", DefaultProtoc)
157+
// })
158+
//
127159
// - "protoc:include" provides the list of import paths used to invoke protoc.
128160
// Applicable to API and service definitions only. If used on an API definition
129161
// the include paths are used for all services.

expr/root.go

+3
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@ import (
1111
// Root is the root object built by the DSL.
1212
var Root = new(RootExpr)
1313

14+
// DefaultProtoc is the default command to be invoked for generating code from protobuf schemas.
15+
const DefaultProtoc = "protoc"
16+
1417
type (
1518
// RootExpr is the struct built by the DSL on process start.
1619
RootExpr struct {

grpc/codegen/proto.go

+17-3
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,19 @@ func protoFile(genpkg string, svc *expr.GRPCServiceExpr) *codegen.File {
8383
runProtoc := func(path string) error {
8484
includes := svc.ServiceExpr.Meta["protoc:include"]
8585
includes = append(includes, expr.Root.API.Meta["protoc:include"]...)
86-
return protoc(path, includes)
86+
87+
cmd := defaultProtocCmd
88+
if c, ok := expr.Root.API.Meta["protoc:cmd"]; ok {
89+
cmd = c
90+
}
91+
if c, ok := svc.ServiceExpr.Meta["protoc:cmd"]; ok {
92+
cmd = c
93+
}
94+
if len(cmd) == 0 {
95+
return fmt.Errorf(`Meta("protoc:cmd"): must be given arguments`)
96+
}
97+
98+
return protoc(cmd, path, includes)
8799
}
88100

89101
return &codegen.File{
@@ -100,7 +112,9 @@ func pkgName(svc *expr.GRPCServiceExpr, svcName string) string {
100112
return codegen.SnakeCase(svcName)
101113
}
102114

103-
func protoc(path string, includes []string) error {
115+
var defaultProtocCmd = []string{expr.DefaultProtoc}
116+
117+
func protoc(protocCmd []string, path string, includes []string) error {
104118
dir := filepath.Dir(path)
105119
if err := os.MkdirAll(dir, 0750); err != nil {
106120
return err
@@ -117,7 +131,7 @@ func protoc(path string, includes []string) error {
117131
for _, include := range includes {
118132
args = append(args, "-I", include)
119133
}
120-
cmd := exec.Command("protoc", args...)
134+
cmd := exec.Command(protocCmd[0], append(protocCmd[1:len(protocCmd):len(protocCmd)], args...)...)
121135
cmd.Dir = filepath.Dir(path)
122136

123137
if output, err := cmd.CombinedOutput(); err != nil {

grpc/codegen/proto_test.go

+49-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package codegen
22

33
import (
4+
"os"
5+
"os/exec"
6+
"path/filepath"
47
"runtime"
58
"strings"
69
"testing"
@@ -49,7 +52,7 @@ func TestProtoFiles(t *testing.T) {
4952
}
5053
assert.Equal(t, c.Code, code)
5154
fpath := codegen.CreateTempFile(t, code)
52-
assert.NoError(t, protoc(fpath, nil), "error occurred when compiling proto file %q", fpath)
55+
assert.NoError(t, protoc(defaultProtocCmd, fpath, nil), "error occurred when compiling proto file %q", fpath)
5356
})
5457
}
5558
}
@@ -85,7 +88,51 @@ func TestMessageDefSection(t *testing.T) {
8588
}
8689
assert.Equal(t, c.Code, msgCode)
8790
fpath := codegen.CreateTempFile(t, code+msgCode)
88-
assert.NoError(t, protoc(fpath, nil), "error occurred when compiling proto file %q", fpath)
91+
assert.NoError(t, protoc(defaultProtocCmd, fpath, nil), "error occurred when compiling proto file %q", fpath)
92+
})
93+
}
94+
}
95+
96+
func TestProtoc(t *testing.T) {
97+
const code = testdata.UnaryRPCsProtoCode
98+
99+
fakeBin := filepath.Join(os.TempDir(), t.Name()+"-fakeprotoc")
100+
if runtime.GOOS == "windows" {
101+
fakeBin += ".exe"
102+
}
103+
out, err := exec.Command("go", "build", "-o", fakeBin, "./testdata/protoc").CombinedOutput()
104+
t.Log("go build output: ", string(out))
105+
require.NoError(t, err, "compile a fake protoc that requires a prefix")
106+
t.Cleanup(func() { assert.NoError(t, os.Remove(fakeBin)) })
107+
108+
cases := []struct {
109+
Name string
110+
Cmd []string
111+
}{
112+
{"protoc", defaultProtocCmd},
113+
{"fakepc", []string{fakeBin, "required-ignored-arg"}},
114+
}
115+
116+
var firstOutput string
117+
118+
for _, c := range cases {
119+
t.Run(c.Name, func(t *testing.T) {
120+
dir, err := os.MkdirTemp("", strings.ReplaceAll(t.Name(), "/", "-"))
121+
require.NoError(t, err)
122+
t.Cleanup(func() { assert.NoError(t, os.RemoveAll(dir)) })
123+
fpath := filepath.Join(dir, "schema")
124+
require.NoError(t, os.WriteFile(fpath, []byte(code), 0o600), "error occured writing proto schema")
125+
require.NoError(t, protoc(c.Cmd, fpath, nil), "error occurred when compiling proto file with the standard protoc %q", fpath)
126+
127+
fcontents, err := os.ReadFile(fpath + ".pb.go")
128+
require.NoError(t, err)
129+
130+
if firstOutput == "" {
131+
firstOutput = string(fcontents)
132+
return
133+
}
134+
135+
assert.Equal(t, firstOutput, string(fcontents))
89136
})
90137
}
91138
}

grpc/codegen/testdata/protoc/main.go

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
)
8+
9+
func main() {
10+
if len(os.Args) < 2 {
11+
fmt.Println("must pass a prefix arg to be ignored, to test it being passed")
12+
os.Exit(1)
13+
}
14+
cmd := exec.Command("protoc", os.Args[2:]...)
15+
fmt.Println(cmd)
16+
cmd.Stdin = os.Stdin
17+
cmd.Stdout = os.Stdout
18+
cmd.Stderr = os.Stderr
19+
cmd.Run()
20+
os.Exit(cmd.ProcessState.ExitCode())
21+
}

0 commit comments

Comments
 (0)