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  }