trpc.group/trpc-go/trpc-cmdline@v1.0.9/cmd/create/generate.go (about)

     1  // Tencent is pleased to support the open source community by making tRPC available.
     2  //
     3  // Copyright (C) 2023 THL A29 Limited, a Tencent company.
     4  // All rights reserved.
     5  //
     6  // If you have downloaded a copy of the tRPC source code from Tencent,
     7  // please note that tRPC source code is licensed under the  Apache 2.0 License,
     8  // A copy of the Apache 2.0 License is included in this file.
     9  
    10  package create
    11  
    12  import (
    13  	"fmt"
    14  	"os"
    15  	"os/exec"
    16  	"path/filepath"
    17  	"strings"
    18  
    19  	"github.com/pkg/errors"
    20  
    21  	"trpc.group/trpc-go/trpc-cmdline/config"
    22  	"trpc.group/trpc-go/trpc-cmdline/params"
    23  	"trpc.group/trpc-go/trpc-cmdline/parser"
    24  	"trpc.group/trpc-go/trpc-cmdline/util/fb"
    25  	"trpc.group/trpc-go/trpc-cmdline/util/fs"
    26  	"trpc.group/trpc-go/trpc-cmdline/util/lang"
    27  	"trpc.group/trpc-go/trpc-cmdline/util/log"
    28  	"trpc.group/trpc-go/trpc-cmdline/util/pb"
    29  )
    30  
    31  // generateIDLStub generates *.pb.go under outputdir/rpc/.
    32  func (c *Create) generateIDLStub(dir string) error {
    33  	// Cpp IDL stub code will be generated using bazel rules, do nothing here.
    34  	if c.options.Language == "cpp" {
    35  		return nil
    36  	}
    37  
    38  	fd, options := c.fileDescriptor, c.options
    39  	stubdir, err := prepareOutputStub(dir)
    40  	if err != nil {
    41  		return fmt.Errorf("prepare output stub: %w", err)
    42  	}
    43  
    44  	pkg, err := parser.GetPackage(fd, options.Language)
    45  	if err != nil {
    46  		return fmt.Errorf("parser get package: %w", err)
    47  	}
    48  
    49  	if err := generatePBFB(fd, options, pkg, stubdir); err != nil {
    50  		return fmt.Errorf("generate pb fb: %w", err)
    51  	}
    52  
    53  	if !options.RPCOnly || options.DependencyStub {
    54  		if err := handleDependencies(fd, options, pkg, stubdir); err != nil {
    55  			return fmt.Errorf("handle dependencies: %w", err)
    56  		}
    57  	}
    58  
    59  	// Move dir/rpc into dir/$gopkgdir/.
    60  	src := filepath.Join(dir, "rpc")
    61  	dest := filepath.Join(stubdir, pkg)
    62  	defer os.RemoveAll(src)
    63  
    64  	return filepath.Walk(src, func(fpath string, _ os.FileInfo, _ error) (e error) {
    65  		if fpath == src {
    66  			return nil
    67  		}
    68  		if fname := filepath.Base(fpath); fname == "trpc.go" {
    69  			return fs.Move(fpath, filepath.Join(dest, fs.BaseNameWithoutExt(fd.FilePath)+".trpc.go"))
    70  		}
    71  		return fs.Move(fpath, filepath.Join(dest, filepath.Base(fpath)))
    72  	})
    73  }
    74  
    75  func prepareOutputStub(outputdir string) (string, error) {
    76  	stubDir := filepath.Join(outputdir, "stub")
    77  
    78  	if _, err := os.Lstat(stubDir); err != nil {
    79  		if !os.IsNotExist(err) {
    80  			return "", err
    81  		}
    82  		if err := os.Mkdir(stubDir, os.ModePerm); err != nil {
    83  			return "", err
    84  		}
    85  		return stubDir, nil
    86  	}
    87  	return stubDir, nil
    88  }
    89  
    90  // generatePBFB generates stub code based on option.IDLType by calling runProtoc or runFlatc.
    91  func generatePBFB(fd *FD, option *params.Option, packageName, stubDir string) error {
    92  	out := filepath.Join(stubDir, packageName)
    93  	if err := os.MkdirAll(out, os.ModePerm); err != nil {
    94  		return err
    95  	}
    96  	if option.IDLType == config.IDLTypeProtobuf {
    97  		log.Debug("generate code for file %s from %v into %s", option.Protofile, option.Protodirs, out)
    98  		// Invoke protoc and copy the .proto file to the generated folder.
    99  		return protocAndCopy(fd, option, out)
   100  	}
   101  	log.Debug("generate code for file %s into %s", option.Protofile, out)
   102  	// Invoke flatc and copy the .fbs file to the generated folder.
   103  	return flatcAndCopy(fd, option, out)
   104  }
   105  
   106  // protocAndCopy invokes runProtoc and copies the .proto file to the generated folder.
   107  func protocAndCopy(fd *FD, option *params.Option, pbOutDir string) error {
   108  	if _, err := runProtoc(fd, pbOutDir, option); err != nil {
   109  		return fmt.Errorf("run protoc err: %w", err)
   110  	}
   111  	if option.DescriptorSetIn != "" {
   112  		// When passing in the descriptor_set_in parameter, skip copying the .proto file as it does not exist.
   113  		return nil
   114  	}
   115  	// copy *.proto to outpoutdir/rpc/
   116  	basename := filepath.Base(fd.FilePath)
   117  	return fs.Copy(fd.FilePath, filepath.Join(pbOutDir, basename))
   118  }
   119  
   120  // runProtoc sets the required options and invokes util/pb.Protoc for processing.
   121  func runProtoc(fd *FD, pbOutDir string, option *params.Option) ([]string, error) {
   122  	opts := []pb.Option{
   123  		pb.WithPb2ImportPath(fd.Pb2ImportPath),
   124  		pb.WithPkg2ImportPath(fd.Pkg2ImportPath),
   125  		pb.WithDescriptorSetIn(option.DescriptorSetIn),
   126  	}
   127  
   128  	var files []string
   129  
   130  	// run protoc --$lang_out
   131  	if err := pb.Protoc(option.Protodirs, option.Protofile, option.Language, pbOutDir, opts...); err != nil {
   132  		return nil, fmt.Errorf("run protoc --$lang_out err: %v", err)
   133  	}
   134  
   135  	// Run protoc-gen-secv.
   136  	if support, ok := config.SupportValidate[option.Language]; ok && support && option.SecvEnabled {
   137  		if err := pb.Protoc(
   138  			option.Protodirs, option.Protofile, option.Language, pbOutDir,
   139  			append(opts, pb.WithSecvEnabled(true))...,
   140  		); err != nil {
   141  			return nil, fmt.Errorf("run protoc-gen-secv for %s err: %w", option.Protofile, err)
   142  		}
   143  	}
   144  	// Run protoc-gen-validate.
   145  	if support, ok := config.SupportValidate[option.Language]; ok && support && option.ValidateEnabled {
   146  		if err := pb.Protoc(
   147  			option.Protodirs, option.Protofile, option.Language, pbOutDir,
   148  			append(opts, pb.WithValidateEnabled(true))...,
   149  		); err != nil {
   150  			return nil, fmt.Errorf("run protoc-gen-validate for %s err: %w", option.Protofile, err)
   151  		}
   152  	}
   153  	log.Debug("pbOutDir = %s", pbOutDir)
   154  	// collect the generated files
   155  	matches, err := filepath.Glob(pbOutDir)
   156  	if err != nil {
   157  		return nil, fmt.Errorf("filepath glob pb out dir: %s, err: %w", pbOutDir, err)
   158  	}
   159  	for _, v := range matches {
   160  		if v == pbOutDir {
   161  			continue
   162  		}
   163  		inf, err := os.Lstat(v)
   164  		if err != nil {
   165  			continue
   166  		}
   167  		if inf.IsDir() {
   168  			continue
   169  		}
   170  		files = append(files, v)
   171  	}
   172  
   173  	return files, nil
   174  }
   175  
   176  // runFlatc sets the required options and invokes util.fb.Flatc.
   177  func runFlatc(fd *FD, fbsOutDir string, option *params.Option) ([]string, error) {
   178  	opts := []fb.Option{
   179  		fb.WithFbsDirs(option.Protodirs),
   180  		fb.WithFbsfile(option.Protofile),
   181  		fb.WithLanguage(option.Language),
   182  		fb.WithPackagePath(fd.BaseGoPackageName),
   183  		fb.WithOutputdir(fbsOutDir),
   184  		fb.WithFb2ImportPath(fd.Pb2ImportPath),
   185  		fb.WithPkg2ImportPath(fd.Pkg2ImportPath),
   186  	}
   187  	// FIXME, return generate filenames
   188  	return nil, fb.NewFbs(opts...).Flatc()
   189  }
   190  
   191  // flatcAndCopy constructs the parameter list and invokes fb.Flatc to generate stub code for each type in the .fbs file.
   192  // Then, the .fbs file is copied to the generated folder.
   193  func flatcAndCopy(fd *FD, option *params.Option, outdir string) error {
   194  	if _, err := runFlatc(fd, outdir, option); err != nil {
   195  		return err
   196  	}
   197  
   198  	// The basename is in the form of "file1.fbs".
   199  	basename := filepath.Base(fd.FilePath)
   200  
   201  	// Copy the *.fbs file to the directory where the generated files are located.
   202  	return fs.Copy(fd.FilePath, filepath.Join(outdir, basename))
   203  }
   204  
   205  // handleDependencies processes other pb files imported by the pb files specified in the "-protofile" option.
   206  // It also processes protoc and copies pb files.
   207  //
   208  // Preparing to generate *.pb.go files corresponding to the PB files using protoc.
   209  // Note that to avoid generating code with circular dependencies.
   210  //
   211  // Parse the result using jhump/protoreflect.
   212  // If the pkgname is the same as the one specified in "-protofile", the importpath will be "".
   213  //
   214  // runProtoc --go_out=M$pb=$pkgname, we need to do compatibility processing:
   215  //  1. Avoid passing $pkgname as empty, otherwise protoc will generate code like this.:
   216  //     ```go
   217  //     package $pkgname
   218  //     import (
   219  //     "."
   220  //     )
   221  //     ```
   222  //  2. Avoid passing the same pkgname as -protofile, otherwise it will cause circular dependencies.
   223  //     ```go
   224  //     package $pkgname
   225  //     import (
   226  //     "$pkgname"
   227  //     )
   228  //     ```
   229  func handleDependencies(fd *FD, option *params.Option, pbpkg string, outputDir string) error {
   230  	outputDir, err := filepath.Abs(outputDir)
   231  	if err != nil {
   232  		return fmt.Errorf("filepath abs output dir: %s, err: %w", outputDir, err)
   233  	}
   234  
   235  	wd, err := os.Getwd()
   236  	if err != nil {
   237  		return fmt.Errorf("os get working directory err: %w", err)
   238  	}
   239  	defer os.Chdir(wd)
   240  
   241  	return doHandleDependencies(fd, pbpkg, outputDir, wd, option)
   242  }
   243  
   244  func doHandleDependencies(fd *FD, pbpkg, outputDir, wd string, option *params.Option) error {
   245  	includeDirs := genIncludeDirs(fd)
   246  	for fname, importPath := range fd.Pb2ImportPath {
   247  		if skipThisProtofile(fd, fname) {
   248  			continue
   249  		}
   250  
   251  		param := &genDependencyRPCStubParam{
   252  			fd:          fd,
   253  			option:      option,
   254  			pbpkg:       pbpkg,
   255  			outputDir:   outputDir,
   256  			fname:       fname,
   257  			importPath:  importPath,
   258  			wd:          wd,
   259  			includeDirs: includeDirs,
   260  		}
   261  		param.importPath = lang.TrimRight(";", param.importPath)
   262  		pbOutDir, err := param.genDependencyRPCStub()
   263  		if err != nil {
   264  			return fmt.Errorf("generate dependency rpc stub err: %w", err)
   265  		}
   266  		importPath = lang.TrimRight(";", importPath)
   267  		if err := moduleInit(option, pbpkg, fname, importPath, pbOutDir); err != nil {
   268  			return fmt.Errorf("module init err: %w", err)
   269  		}
   270  	}
   271  	return nil
   272  }
   273  
   274  func genIncludeDirs(fd *FD) []string {
   275  	includeDirs := []string{}
   276  	for fname := range fd.Pb2ImportPath {
   277  		dir, _ := filepath.Split(fname)
   278  		includeDirs = append(includeDirs, dir)
   279  	}
   280  	return includeDirs
   281  }
   282  
   283  func skipThisProtofile(fd *FD, fname string) bool {
   284  	// If it is ${protofile}, skip and do not process it.
   285  	if filepath.Base(fd.FilePath) == fname {
   286  		return true
   287  	}
   288  
   289  	// Skip the pb files, trpc extension files, and swagger extension files provided by Google.
   290  	return pb.IsInternalProto(fname)
   291  }
   292  
   293  type genDependencyRPCStubParam struct {
   294  	fd          *FD
   295  	option      *params.Option
   296  	pbpkg       string
   297  	outputDir   string
   298  	fname       string
   299  	importPath  string
   300  	wd          string
   301  	includeDirs []string
   302  }
   303  
   304  func (g *genDependencyRPCStubParam) genDependencyRPCStub() (string, error) {
   305  	var err error
   306  
   307  	g.outputDir, err = prepareOutputDir(g.outputDir, g.importPath, g.option.Language, g.pbpkg)
   308  	if err != nil {
   309  		return "", fmt.Errorf("prepare output dir, err: %w", err)
   310  	}
   311  
   312  	switch g.option.IDLType {
   313  	case config.IDLTypeProtobuf:
   314  		err = g.genDependencyRPCStubPB()
   315  	case config.IDLTypeFlatBuffers:
   316  		err = g.genDependencyRPCStubFB()
   317  	default:
   318  		return "", errors.New("invalid IDL type")
   319  	}
   320  
   321  	return g.outputDir, err
   322  }
   323  
   324  func prepareOutputDir(outputDir, importPath, lang, pbPackage string) (string, error) {
   325  	var pbOutDir string
   326  	if lang == "go" {
   327  		pbOutDir = filepath.Join(outputDir, importPath)
   328  	} else {
   329  		pbOutDir = filepath.Join(outputDir, pbPackage)
   330  	}
   331  	if err := os.MkdirAll(pbOutDir, os.ModePerm); err != nil {
   332  		return "", err
   333  	}
   334  	return pbOutDir, nil
   335  }
   336  
   337  func (g *genDependencyRPCStubParam) genDependencyRPCStubPB() error {
   338  	// Inherit the directory from the parent level to avoid directory not found issues.
   339  	searchPath, err := genProtocProtoPath(g.option, g.wd, g.includeDirs)
   340  	if err != nil {
   341  		return fmt.Errorf("generate protoc proto path err: %w", err)
   342  	}
   343  	log.Debug("Generate code for proto file %s from %v into %s", g.fname, searchPath, g.outputDir)
   344  
   345  	// run protoc-gen-go
   346  	opts := []pb.Option{
   347  		pb.WithPb2ImportPath(g.fd.Pb2ImportPath),
   348  		pb.WithPkg2ImportPath(g.fd.Pkg2ImportPath),
   349  		pb.WithDescriptorSetIn(g.option.DescriptorSetIn),
   350  	}
   351  	if err = pb.Protoc(searchPath, g.fname, g.option.Language, g.outputDir, opts...); err != nil {
   352  		return fmt.Errorf("GenerateFiles: %v", err)
   353  	}
   354  
   355  	// Run protoc-gen-secv.
   356  	if support, ok := config.SupportValidate[g.option.Language]; ok && support && g.option.SecvEnabled {
   357  		if err := pb.Protoc(
   358  			searchPath, g.fname, g.option.Language, g.outputDir,
   359  			append(opts, pb.WithSecvEnabled(true))...,
   360  		); err != nil {
   361  			return fmt.Errorf("generate secv file for %s err: %w", g.fname, err)
   362  		}
   363  	}
   364  	// Run protoc-gen-validate.
   365  	if support, ok := config.SupportValidate[g.option.Language]; ok && support && g.option.ValidateEnabled {
   366  		if err := pb.Protoc(
   367  			searchPath, g.fname, g.option.Language, g.outputDir,
   368  			append(opts, pb.WithValidateEnabled(true))...,
   369  		); err != nil {
   370  			return fmt.Errorf("generate validation file for %s err: %w", g.fname, err)
   371  		}
   372  	}
   373  	if g.option.DescriptorSetIn != "" {
   374  		return nil // skip copy if descriptor_set_in is passed.
   375  	}
   376  
   377  	// Copy pb file.
   378  	if err := copyProtofile(g.fname, g.outputDir, g.option); err != nil {
   379  		return fmt.Errorf("copy proto file err: %w", err)
   380  	}
   381  	return nil
   382  }
   383  
   384  func genProtocProtoPath(option *params.Option, wd string, includeDirs []string) ([]string, error) {
   385  	searchPath := option.Protodirs
   386  	parentDirs := []string{wd}
   387  	parentDirs = append(parentDirs, option.Protodirs...)
   388  	for _, pDir := range parentDirs {
   389  		newSearchPath, err := genProtocProtoPathByParentDir(includeDirs, pDir)
   390  		if err != nil {
   391  			return nil, err
   392  		}
   393  		searchPath = append(searchPath, newSearchPath...)
   394  	}
   395  	return fs.UniqFilePath(searchPath), nil
   396  }
   397  
   398  func genProtocProtoPathByParentDir(includeDirs []string, pDir string) ([]string, error) {
   399  	var searchPath []string
   400  	for _, incDir := range includeDirs {
   401  
   402  		includeDir := filepath.Join(pDir, incDir)
   403  		includeDir = filepath.Clean(includeDir)
   404  
   405  		if fin, err := os.Lstat(includeDir); err != nil {
   406  			if !os.IsNotExist(err) {
   407  				return nil, fmt.Errorf("os lstat err err: %w", err)
   408  			}
   409  		} else {
   410  			if !fin.IsDir() {
   411  				return nil, fmt.Errorf("import path: %s, not directory", includeDir)
   412  			}
   413  			searchPath = append(searchPath, includeDir)
   414  		}
   415  	}
   416  	return searchPath, nil
   417  }
   418  
   419  func copyProtofile(fname, pbOutDir string, option *params.Option) error {
   420  	p, err := fs.LocateFile(fname, option.Protodirs)
   421  	if err != nil {
   422  		return fmt.Errorf("fs locate file err: %w", err)
   423  	}
   424  
   425  	_, baseName := filepath.Split(fname)
   426  	src := p
   427  	dst := filepath.Join(pbOutDir, baseName)
   428  
   429  	log.Debug("Copy file %s to %s", src, dst)
   430  	if err := fs.Copy(src, dst); err != nil {
   431  		return err
   432  	}
   433  	return nil
   434  }
   435  
   436  func (g *genDependencyRPCStubParam) genDependencyRPCStubFB() error {
   437  	strs := strings.Split(g.importPath, "/")
   438  	baseGoPackageName := strs[len(strs)-1]
   439  	filename, err := fs.LocateFile(g.fname, g.option.Protodirs)
   440  	if err != nil {
   441  		return fmt.Errorf("cannot locate file %v: %v", g.fname, err)
   442  	}
   443  	opts := []fb.Option{
   444  		fb.WithFbsDirs(g.option.Protodirs),
   445  		fb.WithFbsfile(filename),
   446  		fb.WithLanguage(g.option.Language),
   447  		fb.WithPackagePath(baseGoPackageName),
   448  		fb.WithOutputdir(g.outputDir),
   449  		fb.WithFb2ImportPath(g.fd.Pb2ImportPath),
   450  		fb.WithPkg2ImportPath(g.fd.Pkg2ImportPath),
   451  	}
   452  	f := fb.NewFbs(opts...)
   453  	if err := f.Flatc(); err != nil {
   454  		return fmt.Errorf("flatc: %v", err)
   455  	}
   456  	// Copy fbs file.
   457  	_, baseName := filepath.Split(filename)
   458  	src := filename
   459  	dst := filepath.Join(g.outputDir, baseName)
   460  	log.Debug("Copy file %s to %s", src, dst)
   461  	if err := fs.Copy(src, dst); err != nil {
   462  		return err
   463  	}
   464  	return nil
   465  }
   466  
   467  func moduleInit(option *params.Option, pbpkg string, fname string, importPath string, pbOutDir string) error {
   468  	// Fixme: move to createCmd.PostRun
   469  	// Run "go mod init". If it is the same as pbPackage, no initialization is required.
   470  	if option.Language != "go" {
   471  		return nil
   472  	}
   473  
   474  	return genGoModInit(importPath, pbpkg, pbOutDir, fname)
   475  }
   476  
   477  func genGoModInit(importPath, pbPackage, pbOutDir, fname string) error {
   478  	// Initialize go.mod to avoid duplicating initialization of go.mod.
   479  	fp := filepath.Join(pbOutDir, "go.mod")
   480  	fin, err := os.Stat(fp)
   481  	if err == nil && !fin.IsDir() {
   482  		return nil
   483  	}
   484  
   485  	// Run "go mod init".
   486  	if !canExecGoModInit(importPath, pbPackage) {
   487  		return nil
   488  	}
   489  	_ = os.Chdir(pbOutDir)
   490  
   491  	cmd := exec.Command("go", "mod", "init", importPath)
   492  	if buf, err := cmd.CombinedOutput(); err != nil {
   493  		return fmt.Errorf("process %s, init go.mod in stub/%s error: %v", fname, importPath, string(buf))
   494  	}
   495  	log.Debug("process %s, init go.mod success in stub/%s: go mod init %s", fname, importPath, importPath)
   496  	return nil
   497  }
   498  
   499  func canExecGoModInit(importPath string, pbPackage string) bool {
   500  	return len(importPath) != 0 && importPath != pbPackage
   501  }