go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/cmd/cproto/main.go (about) 1 // Copyright 2016 The LUCI Authors. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package main 16 17 import ( 18 "context" 19 "flag" 20 "fmt" 21 "io/ioutil" 22 "os" 23 "path" 24 "path/filepath" 25 "strings" 26 27 "google.golang.org/protobuf/proto" 28 "google.golang.org/protobuf/types/descriptorpb" 29 30 "go.chromium.org/luci/common/data/stringset" 31 "go.chromium.org/luci/common/errors" 32 "go.chromium.org/luci/common/flag/stringlistflag" 33 "go.chromium.org/luci/common/flag/stringmapflag" 34 "go.chromium.org/luci/common/logging" 35 "go.chromium.org/luci/common/logging/gologger" 36 "go.chromium.org/luci/common/proto/protoc" 37 "go.chromium.org/luci/common/system/exitcode" 38 ) 39 40 const protocGenValidatePkg = "github.com/envoyproxy/protoc-gen-validate" 41 42 var ( 43 verbose = flag.Bool("verbose", false, "print debug messages to stderr") 44 protoImportPaths = stringlistflag.Flag{} 45 goModules = stringlistflag.Flag{} 46 goRootModules = stringlistflag.Flag{} 47 pathMap = stringmapflag.Value{} 48 withDiscovery = flag.Bool( 49 "discovery", true, 50 "generate pb.discovery.go file") 51 descFile = flag.String( 52 "desc", 53 "", 54 "write FileDescriptorSet file containing all the .proto files and their transitive dependencies", 55 ) 56 disableGRPC = flag.Bool( 57 "disable-grpc", false, 58 "disable grpc and prpc stubs generation, implies -discovery=false", 59 ) 60 useGRPCPlugin = flag.Bool( 61 "use-grpc-plugin", false, 62 "use protoc-gen-go-grpc to generate gRPC stubs instead of protoc-gen-go", 63 ) 64 enablePGV = flag.Bool( 65 "enable-pgv", false, 66 "enable protoc-gen-validate generation. Makes 'validate/validate.proto' and annotations available.", 67 ) 68 ) 69 70 func run(ctx context.Context, inputDir string) error { 71 if *enablePGV { 72 needAddPkg := true 73 for _, mod := range goRootModules { 74 if mod == protocGenValidatePkg { 75 needAddPkg = false 76 break 77 } 78 } 79 if needAddPkg { 80 logging.Infof(ctx, "adding -go-root-module %s", goRootModules) 81 goRootModules = append(goRootModules, protocGenValidatePkg) 82 } 83 } 84 85 // Stage all requested Go modules under a single root. 86 inputs, err := protoc.StageGoInputs(ctx, inputDir, goModules, goRootModules, protoImportPaths) 87 if err != nil { 88 return err 89 } 90 defer inputs.Cleanup() 91 92 // Prep a path to the generated descriptors file. 93 descPath := *descFile 94 if descPath == "" { 95 tmpDir, err := ioutil.TempDir("", "") 96 if err != nil { 97 return err 98 } 99 defer os.RemoveAll(tmpDir) 100 descPath = filepath.Join(tmpDir, "package.desc") 101 } 102 103 // Compile all .proto files. 104 err = protoc.Compile(ctx, &protoc.CompileParams{ 105 Inputs: inputs, 106 OutputDescriptorSet: descPath, 107 GoEnabled: true, 108 GoPackageMap: pathMap, 109 GoDeprecatedGRPCPlugin: !*disableGRPC && !*useGRPCPlugin, 110 GoGRPCEnabled: !*disableGRPC && *useGRPCPlugin, 111 GoPGVEnabled: *enablePGV, 112 }) 113 if err != nil { 114 return err 115 } 116 117 // protoc-gen-go puts generated files based on go_package option, rooting them 118 // in the inputs.OutputDir. We can't generally guess the Go package name just 119 // based on proto file names, but we can extract it from the generated 120 // descriptor. 121 // 122 // Doc: 123 // https://developers.google.com/protocol-buffers/docs/reference/go-generated 124 descSet, rawDesc, err := loadDescriptorSet(descPath) 125 if err != nil { 126 return errors.Annotate(err, "failed to load the descriptor set with generated files").Err() 127 } 128 129 generatedDesc := make([]*descriptorpb.FileDescriptorProto, 0, len(inputs.ProtoFiles)) 130 goPackages := stringset.New(0) 131 132 // Since we use --include_imports, there may be a lot of descriptors in the 133 // set. Visit only ones we care about. 134 for _, protoFile := range inputs.ProtoFiles { 135 fileDesc := descSet[path.Join(inputs.ProtoPackage, protoFile)] 136 if fileDesc == nil { 137 return errors.Reason("descriptor for %q is unexpectedly absent", protoFile).Err() 138 } 139 generatedDesc = append(generatedDesc, fileDesc) 140 141 // "go_package" option is required now. 142 goPackage := fileDesc.Options.GetGoPackage() 143 if goPackage == "" { 144 return errors.Reason("file %q has no go_package option set, it is required", protoFile).Err() 145 } 146 // Convert e.g. "foo/bar;pkgname" => "foo/bar". 147 if idx := strings.LastIndex(goPackage, ";"); idx != -1 { 148 goPackage = goPackage[:idx] 149 } 150 goPackages.Add(goPackage) 151 152 // A file that protoc must have generated for us. 153 goFile := filepath.Join( 154 inputs.OutputDir, 155 filepath.FromSlash(goPackage), 156 strings.TrimSuffix(protoFile, ".proto")+".pb.go", 157 ) 158 if _, err := os.Stat(goFile); err != nil { 159 return errors.Reason("could not find *.pb.go file generated from %q, is go_package option correct?", protoFile).Err() 160 } 161 162 // Transform .go files by adding pRPC stubs after gPRC stubs. Code generated 163 // by protoc-gen-go-grpc plugin doesn't need this, since it uses interfaces 164 // in the generated code (that pRPC implements) instead of concrete gRPC 165 // types. 166 if !*disableGRPC && !*useGRPCPlugin { 167 var t transformer 168 if err := t.transformGoFile(goFile); err != nil { 169 return errors.Annotate(err, "could not transform %q", goFile).Err() 170 } 171 } 172 173 // _test.proto's should go into the test package. 174 if strings.HasSuffix(protoFile, "_test.proto") { 175 newName := strings.TrimSuffix(goFile, ".go") + "_test.go" 176 if err := os.Rename(goFile, newName); err != nil { 177 return err 178 } 179 } 180 } 181 182 if !*disableGRPC && *withDiscovery { 183 // We support generating a discovery file only when all generated *.pb.go 184 // ended up in the same Go package. Otherwise it's not clear what package to 185 // put the pb.discovery.go into. 186 if goPackages.Len() != 1 { 187 return errors.Reason( 188 "cannot generate pb.discovery.go: generated *.pb.go files are in multiple packages %v", 189 goPackages.ToSortedSlice(), 190 ).Err() 191 } 192 goPkg := goPackages.ToSlice()[0] 193 out := filepath.Join( 194 inputs.OutputDir, 195 filepath.FromSlash(goPkg), 196 "pb.discovery.go", 197 ) 198 if err := genDiscoveryFile(out, goPkg, generatedDesc, rawDesc); err != nil { 199 return err 200 } 201 } 202 203 return nil 204 } 205 206 // loadDescriptorSet reads and parses FileDescriptorSet proto. 207 // 208 // Returns it as a map: *.proto path in the registry => FileDescriptorProto, 209 // as well as a raw byte blob. 210 func loadDescriptorSet(path string) (map[string]*descriptorpb.FileDescriptorProto, []byte, error) { 211 blob, err := os.ReadFile(path) 212 if err != nil { 213 return nil, nil, err 214 } 215 set := &descriptorpb.FileDescriptorSet{} 216 if proto.Unmarshal(blob, set); err != nil { 217 return nil, nil, err 218 } 219 mapping := make(map[string]*descriptorpb.FileDescriptorProto, len(set.File)) 220 for _, f := range set.File { 221 mapping[f.GetName()] = f 222 } 223 return mapping, blob, nil 224 } 225 226 func setupLogging(ctx context.Context) context.Context { 227 lvl := logging.Warning 228 if *verbose { 229 lvl = logging.Debug 230 } 231 return logging.SetLevel(gologger.StdConfig.Use(context.Background()), lvl) 232 } 233 234 func usage() { 235 fmt.Fprintln(os.Stderr, 236 `Compiles all .proto files in a directory to .go with grpc+prpc+validate support. 237 usage: cproto [flags] [dir] 238 239 This also has support for github.com/envoyproxy/protoc-gen-validate. Have your 240 proto files import "validate/validate.proto" and then add '-enable-pgv' to your 241 cproto invocation to generate Validate() calls for your proto library. 242 243 Flags:`) 244 flag.PrintDefaults() 245 } 246 247 func main() { 248 flag.Var( 249 &protoImportPaths, 250 "proto-path", 251 "additional proto import paths; "+ 252 "May be relative to CWD; "+ 253 "May be specified multiple times.") 254 flag.Var( 255 &goModules, 256 "go-module", 257 "make protos in the given module available in proto import path. "+ 258 "May be specified multiple times.") 259 flag.Var( 260 &goRootModules, 261 "go-root-module", 262 "make protos relative to the root of the given module available in proto import path. "+ 263 "May be specified multiple times.") 264 flag.Var( 265 &pathMap, 266 "map-package", 267 "maps a proto path to a go package name. "+ 268 "May be specified multiple times.") 269 flag.Usage = usage 270 flag.Parse() 271 272 if flag.NArg() > 1 { 273 flag.Usage() 274 os.Exit(1) 275 } 276 dir := "." 277 if flag.NArg() == 1 { 278 dir = flag.Arg(0) 279 } 280 281 ctx := setupLogging(context.Background()) 282 if err := run(ctx, dir); err != nil { 283 fmt.Fprintln(os.Stderr, err.Error()) 284 exitCode := 1 285 if rc, ok := exitcode.Get(err); ok { 286 exitCode = rc 287 } 288 os.Exit(exitCode) 289 } 290 }