github.com/cloudwego/kitex@v0.9.0/tool/cmd/kitex/args/args.go (about)

     1  // Copyright 2021 CloudWeGo 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 args
    16  
    17  import (
    18  	"flag"
    19  	"fmt"
    20  	"io"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"regexp"
    25  	"strconv"
    26  	"strings"
    27  
    28  	"github.com/cloudwego/kitex/pkg/klog"
    29  	"github.com/cloudwego/kitex/tool/internal_pkg/generator"
    30  	"github.com/cloudwego/kitex/tool/internal_pkg/log"
    31  	"github.com/cloudwego/kitex/tool/internal_pkg/pluginmode/protoc"
    32  	"github.com/cloudwego/kitex/tool/internal_pkg/pluginmode/thriftgo"
    33  	"github.com/cloudwego/kitex/tool/internal_pkg/util"
    34  )
    35  
    36  // EnvPluginMode is an environment that kitex uses to distinguish run modes.
    37  const EnvPluginMode = "KITEX_PLUGIN_MODE"
    38  
    39  // ExtraFlag is designed for adding flags that is irrelevant to
    40  // code generation.
    41  type ExtraFlag struct {
    42  	// apply may add flags to the FlagSet.
    43  	Apply func(*flag.FlagSet)
    44  
    45  	// check may perform any value checking for flags added by apply above.
    46  	// When an error occur, check should directly terminate the program by
    47  	// os.Exit with exit code 1 for internal error and 2 for invalid arguments.
    48  	Check func(*Arguments)
    49  }
    50  
    51  // Arguments .
    52  type Arguments struct {
    53  	generator.Config
    54  	extends []*ExtraFlag
    55  }
    56  
    57  const cmdExample = `  # Generate client codes or update kitex_gen codes when a project is in $GOPATH:
    58    kitex {{path/to/IDL_file.thrift}}
    59  
    60    # Generate client codes or update kitex_gen codes  when a project is not in $GOPATH:
    61    kitex -module {{github.com/xxx_org/xxx_name}} {{path/to/IDL_file.thrift}}
    62  
    63    # Generate server codes:
    64    kitex -service {{svc_name}} {{path/to/IDL_file.thrift}}
    65  `
    66  
    67  // AddExtraFlag .
    68  func (a *Arguments) AddExtraFlag(e *ExtraFlag) {
    69  	a.extends = append(a.extends, e)
    70  }
    71  
    72  func (a *Arguments) guessIDLType() (string, bool) {
    73  	switch {
    74  	case strings.HasSuffix(a.IDL, ".thrift"):
    75  		return "thrift", true
    76  	case strings.HasSuffix(a.IDL, ".proto"):
    77  		return "protobuf", true
    78  	}
    79  	return "unknown", false
    80  }
    81  
    82  func (a *Arguments) buildFlags(version string) *flag.FlagSet {
    83  	f := flag.NewFlagSet(os.Args[0], flag.ContinueOnError)
    84  	f.BoolVar(&a.NoFastAPI, "no-fast-api", false,
    85  		"Generate codes without injecting fast method.")
    86  	f.StringVar(&a.ModuleName, "module", "",
    87  		"Specify the Go module name to generate go.mod.")
    88  	f.StringVar(&a.ServiceName, "service", "",
    89  		"Specify the service name to generate server side codes.")
    90  	f.StringVar(&a.Use, "use", "",
    91  		"Specify the kitex_gen package to import when generate server side codes.")
    92  	f.BoolVar(&a.Verbose, "v", false, "") // short for -verbose
    93  	f.BoolVar(&a.Verbose, "verbose", false,
    94  		"Turn on verbose mode.")
    95  	f.BoolVar(&a.GenerateInvoker, "invoker", false,
    96  		"Generate invoker side codes when service name is specified.")
    97  	f.StringVar(&a.IDLType, "type", "unknown", "Specify the type of IDL: 'thrift' or 'protobuf'.")
    98  	f.Var(&a.Includes, "I", "Add an IDL search path for includes.")
    99  	f.Var(&a.ThriftOptions, "thrift", "Specify arguments for the thrift go compiler.")
   100  	f.Var(&a.Hessian2Options, "hessian2", "Specify arguments for the hessian2 codec.")
   101  	f.DurationVar(&a.ThriftPluginTimeLimit, "thrift-plugin-time-limit", generator.DefaultThriftPluginTimeLimit, "Specify thrift plugin execution time limit.")
   102  	f.StringVar(&a.CompilerPath, "compiler-path", "", "Specify the path of thriftgo/protoc.")
   103  	f.Var(&a.ThriftPlugins, "thrift-plugin", "Specify thrift plugin arguments for the thrift compiler.")
   104  	f.Var(&a.ProtobufOptions, "protobuf", "Specify arguments for the protobuf compiler.")
   105  	f.Var(&a.ProtobufPlugins, "protobuf-plugin", "Specify protobuf plugin arguments for the protobuf compiler.(plugin_name:options:out_dir)")
   106  	f.BoolVar(&a.CombineService, "combine-service", false,
   107  		"Combine services in root thrift file.")
   108  	f.BoolVar(&a.CopyIDL, "copy-idl", false,
   109  		"Copy each IDL file to the output path.")
   110  	f.BoolVar(&a.HandlerReturnKeepResp, "handler-return-keep-resp", false,
   111  		"When the server-side handler returns both err and resp, the resp return is retained for use in middleware where both err and resp can be used simultaneously. Note: At the RPC communication level, if the handler returns an err, the framework still only returns err to the client without resp.")
   112  	f.StringVar(&a.ExtensionFile, "template-extension", a.ExtensionFile,
   113  		"Specify a file for template extension.")
   114  	f.BoolVar(&a.FrugalPretouch, "frugal-pretouch", false,
   115  		"Use frugal to compile arguments and results when new clients and servers.")
   116  	f.BoolVar(&a.Record, "record", false,
   117  		"Record Kitex cmd into kitex-all.sh.")
   118  	f.StringVar(&a.TemplateDir, "template-dir", "",
   119  		"Use custom template to generate codes.")
   120  	f.StringVar(&a.GenPath, "gen-path", generator.KitexGenPath,
   121  		"Specify a code gen path.")
   122  	f.BoolVar(&a.DeepCopyAPI, "deep-copy-api", false,
   123  		"Generate codes with injecting deep copy method.")
   124  	f.StringVar(&a.Protocol, "protocol", "",
   125  		"Specify a protocol for codec")
   126  	a.RecordCmd = os.Args
   127  	a.Version = version
   128  	a.ThriftOptions = append(a.ThriftOptions,
   129  		"naming_style=golint",
   130  		"ignore_initialisms",
   131  		"gen_setter",
   132  		"gen_deep_equal",
   133  		"compatible_names",
   134  		"frugal_tag",
   135  		"thrift_streaming",
   136  	)
   137  
   138  	for _, e := range a.extends {
   139  		e.Apply(f)
   140  	}
   141  	f.Usage = func() {
   142  		fmt.Fprintf(os.Stderr, `Version %s
   143  Usage: %s [flags] IDL
   144  
   145  Examples:
   146  %s
   147  Flags:
   148  `, a.Version, os.Args[0], cmdExample)
   149  		f.PrintDefaults()
   150  		os.Exit(1)
   151  	}
   152  	return f
   153  }
   154  
   155  // ParseArgs parses command line arguments.
   156  func (a *Arguments) ParseArgs(version string) {
   157  	f := a.buildFlags(version)
   158  	if err := f.Parse(os.Args[1:]); err != nil {
   159  		log.Warn(os.Stderr, err)
   160  		os.Exit(2)
   161  	}
   162  
   163  	log.Verbose = a.Verbose
   164  
   165  	for _, e := range a.extends {
   166  		e.Check(a)
   167  	}
   168  
   169  	a.checkIDL(f.Args())
   170  	a.checkServiceName()
   171  	// todo finish protobuf
   172  	if a.IDLType != "thrift" {
   173  		a.GenPath = generator.KitexGenPath
   174  	}
   175  	a.checkPath()
   176  }
   177  
   178  func (a *Arguments) checkIDL(files []string) {
   179  	if len(files) == 0 {
   180  		log.Warn("No IDL file found; Please make your IDL file the last parameter, for example: " +
   181  			"\"kitex -service demo idl.thrift\".")
   182  		os.Exit(2)
   183  	}
   184  	if len(files) != 1 {
   185  		log.Warn("Require exactly 1 IDL file; Please make your IDL file the last parameter, for example: " +
   186  			"\"kitex -service demo idl.thrift\".")
   187  		os.Exit(2)
   188  	}
   189  	a.IDL = files[0]
   190  
   191  	switch a.IDLType {
   192  	case "thrift", "protobuf":
   193  	case "unknown":
   194  		if typ, ok := a.guessIDLType(); ok {
   195  			a.IDLType = typ
   196  		} else {
   197  			log.Warnf("Can not guess an IDL type from %q (unknown suffix), please specify with the '-type' flag.", a.IDL)
   198  			os.Exit(2)
   199  		}
   200  	default:
   201  		log.Warn("Unsupported IDL type:", a.IDLType)
   202  		os.Exit(2)
   203  	}
   204  }
   205  
   206  func (a *Arguments) checkServiceName() {
   207  	if a.ServiceName == "" && a.TemplateDir == "" {
   208  		if a.Use != "" {
   209  			log.Warn("-use must be used with -service or -template-dir")
   210  			os.Exit(2)
   211  		}
   212  	}
   213  	if a.ServiceName != "" && a.TemplateDir != "" {
   214  		log.Warn("-template-dir and -service cannot be specified at the same time")
   215  		os.Exit(2)
   216  	}
   217  	if a.ServiceName != "" {
   218  		a.GenerateMain = true
   219  	}
   220  }
   221  
   222  func (a *Arguments) checkPath() {
   223  	pathToGo, err := exec.LookPath("go")
   224  	if err != nil {
   225  		log.Warn(err)
   226  		os.Exit(1)
   227  	}
   228  
   229  	gosrc := util.JoinPath(util.GetGOPATH(), "src")
   230  	gosrc, err = filepath.Abs(gosrc)
   231  	if err != nil {
   232  		log.Warn("Get GOPATH/src path failed:", err.Error())
   233  		os.Exit(1)
   234  	}
   235  	curpath, err := filepath.Abs(".")
   236  	if err != nil {
   237  		log.Warn("Get current path failed:", err.Error())
   238  		os.Exit(1)
   239  	}
   240  
   241  	if strings.HasPrefix(curpath, gosrc) {
   242  		if a.PackagePrefix, err = filepath.Rel(gosrc, curpath); err != nil {
   243  			log.Warn("Get GOPATH/src relpath failed:", err.Error())
   244  			os.Exit(1)
   245  		}
   246  		a.PackagePrefix = util.JoinPath(a.PackagePrefix, a.GenPath)
   247  	} else {
   248  		if a.ModuleName == "" {
   249  			log.Warn("Outside of $GOPATH. Please specify a module name with the '-module' flag.")
   250  			os.Exit(1)
   251  		}
   252  	}
   253  
   254  	if a.ModuleName != "" {
   255  		module, path, ok := util.SearchGoMod(curpath)
   256  		if ok {
   257  			// go.mod exists
   258  			if module != a.ModuleName {
   259  				log.Warnf("The module name given by the '-module' option ('%s') is not consist with the name defined in go.mod ('%s' from %s)\n",
   260  					a.ModuleName, module, path)
   261  				os.Exit(1)
   262  			}
   263  			if a.PackagePrefix, err = filepath.Rel(path, curpath); err != nil {
   264  				log.Warn("Get package prefix failed:", err.Error())
   265  				os.Exit(1)
   266  			}
   267  			a.PackagePrefix = util.JoinPath(a.ModuleName, a.PackagePrefix, a.GenPath)
   268  		} else {
   269  			if err = initGoMod(pathToGo, a.ModuleName); err != nil {
   270  				log.Warn("Init go mod failed:", err.Error())
   271  				os.Exit(1)
   272  			}
   273  			a.PackagePrefix = util.JoinPath(a.ModuleName, a.GenPath)
   274  		}
   275  	}
   276  
   277  	if a.Use != "" {
   278  		a.PackagePrefix = a.Use
   279  	}
   280  	a.OutputPath = curpath
   281  }
   282  
   283  // BuildCmd builds an exec.Cmd.
   284  func (a *Arguments) BuildCmd(out io.Writer) *exec.Cmd {
   285  	exe, err := os.Executable()
   286  	if err != nil {
   287  		log.Warn("Failed to detect current executable:", err.Error())
   288  		os.Exit(1)
   289  	}
   290  
   291  	for i, inc := range a.Includes {
   292  		if strings.HasPrefix(inc, "git@") || strings.HasPrefix(inc, "http://") || strings.HasPrefix(inc, "https://") {
   293  			localGitPath, errMsg, gitErr := util.RunGitCommand(inc)
   294  			if gitErr != nil {
   295  				if errMsg == "" {
   296  					errMsg = gitErr.Error()
   297  				}
   298  				log.Warn("failed to pull IDL from git:", errMsg)
   299  				log.Warn("You can execute 'rm -rf ~/.kitex' to clean the git cache and try again.")
   300  				os.Exit(1)
   301  			}
   302  			a.Includes[i] = localGitPath
   303  		}
   304  	}
   305  
   306  	kas := strings.Join(a.Config.Pack(), ",")
   307  	cmd := &exec.Cmd{
   308  		Path:   LookupTool(a.IDLType, a.CompilerPath),
   309  		Stdin:  os.Stdin,
   310  		Stdout: io.MultiWriter(out, os.Stdout),
   311  		Stderr: io.MultiWriter(out, os.Stderr),
   312  	}
   313  
   314  	ValidateCMD(cmd.Path, a.IDLType)
   315  
   316  	if a.IDLType == "thrift" {
   317  		os.Setenv(EnvPluginMode, thriftgo.PluginName)
   318  		cmd.Args = append(cmd.Args, "thriftgo")
   319  		for _, inc := range a.Includes {
   320  			cmd.Args = append(cmd.Args, "-i", inc)
   321  		}
   322  		if thriftgo.IsHessian2(a.Config) {
   323  			thriftgo.Hessian2PreHook(&a.Config)
   324  		}
   325  		a.ThriftOptions = append(a.ThriftOptions, "package_prefix="+a.PackagePrefix)
   326  		gas := "go:" + strings.Join(a.ThriftOptions, ",")
   327  		if a.Verbose {
   328  			cmd.Args = append(cmd.Args, "-v")
   329  		}
   330  		if a.Use == "" {
   331  			cmd.Args = append(cmd.Args, "-r")
   332  		}
   333  		cmd.Args = append(cmd.Args,
   334  			"-o", a.GenPath,
   335  			"-g", gas,
   336  			"-p", "kitex="+exe+":"+kas,
   337  		)
   338  		if a.ThriftPluginTimeLimit != generator.DefaultThriftPluginTimeLimit {
   339  			cmd.Args = append(cmd.Args,
   340  				"--plugin-time-limit", a.ThriftPluginTimeLimit.String(),
   341  			)
   342  		}
   343  		for _, p := range a.ThriftPlugins {
   344  			cmd.Args = append(cmd.Args, "-p", p)
   345  		}
   346  		cmd.Args = append(cmd.Args, a.IDL)
   347  	} else {
   348  		os.Setenv(EnvPluginMode, protoc.PluginName)
   349  		a.ThriftOptions = a.ThriftOptions[:0]
   350  		// "protobuf"
   351  		cmd.Args = append(cmd.Args, "protoc")
   352  		for _, inc := range a.Includes {
   353  			cmd.Args = append(cmd.Args, "-I", inc)
   354  		}
   355  		outPath := util.JoinPath(".", a.GenPath)
   356  		if a.Use == "" {
   357  			os.MkdirAll(outPath, 0o755)
   358  		} else {
   359  			outPath = "."
   360  		}
   361  		cmd.Args = append(cmd.Args,
   362  			"--plugin=protoc-gen-kitex="+exe,
   363  			"--kitex_out="+outPath,
   364  			"--kitex_opt="+kas,
   365  		)
   366  		for _, po := range a.ProtobufOptions {
   367  			cmd.Args = append(cmd.Args, "--kitex_opt="+po)
   368  		}
   369  		for _, p := range a.ProtobufPlugins {
   370  			pluginParams := strings.Split(p, ":")
   371  			if len(pluginParams) != 3 {
   372  				log.Warnf("Failed to get the correct protoc plugin parameters for %. Please specify the protoc plugin in the form of \"plugin_name:options:out_dir\"", p)
   373  				os.Exit(1)
   374  			}
   375  			// pluginParams[0] -> plugin name, pluginParams[1] -> plugin options, pluginParams[2] -> out_dir
   376  			cmd.Args = append(cmd.Args,
   377  				fmt.Sprintf("--%s_out=%s", pluginParams[0], pluginParams[2]),
   378  				fmt.Sprintf("--%s_opt=%s", pluginParams[0], pluginParams[1]),
   379  			)
   380  		}
   381  
   382  		cmd.Args = append(cmd.Args, a.IDL)
   383  	}
   384  	log.Info(strings.ReplaceAll(strings.Join(cmd.Args, " "), kas, fmt.Sprintf("%q", kas)))
   385  	return cmd
   386  }
   387  
   388  // ValidateCMD check if the path exists and if the version is satisfied
   389  func ValidateCMD(path, idlType string) {
   390  	// check if the path exists
   391  	if _, err := os.Stat(path); err != nil {
   392  		tool := "thriftgo"
   393  		if idlType == "protobuf" {
   394  			tool = "protoc"
   395  		}
   396  		log.Warnf("[ERROR] %s is also unavailable, please install %s first.\n", path, tool)
   397  		if idlType == "thrift" {
   398  			log.Warn("Refer to https://github.com/cloudwego/thriftgo, or simple run:\n")
   399  			log.Warn("  go install -v github.com/cloudwego/thriftgo@latest\n")
   400  		} else {
   401  			log.Warn("Refer to https://github.com/protocolbuffers/protobuf\n")
   402  		}
   403  		os.Exit(1)
   404  	}
   405  
   406  	// check if the version is satisfied
   407  	if idlType == "thrift" {
   408  		// run `thriftgo -versions and get the output
   409  		cmd := exec.Command(path, "-version")
   410  		out, err := cmd.CombinedOutput()
   411  		if err != nil {
   412  			log.Warnf("Failed to get thriftgo version: %s\n", err.Error())
   413  			os.Exit(1)
   414  		}
   415  		if !strings.HasPrefix(string(out), "thriftgo ") {
   416  			log.Warnf("thriftgo -version returns '%s', please reinstall thriftgo first.\n", string(out))
   417  			os.Exit(1)
   418  		}
   419  		version := strings.Replace(strings.TrimSuffix(string(out), "\n"), "thriftgo ", "", 1)
   420  		if !versionSatisfied(version, requiredThriftGoVersion) {
   421  			log.Warnf("[ERROR] thriftgo version(%s) not satisfied, please install version >= %s\n",
   422  				version, requiredThriftGoVersion)
   423  			os.Exit(1)
   424  		}
   425  		return
   426  	}
   427  }
   428  
   429  var versionSuffixPattern = regexp.MustCompile(`-.*$`)
   430  
   431  func removeVersionPrefixAndSuffix(version string) string {
   432  	version = strings.TrimPrefix(version, "v")
   433  	version = strings.TrimSuffix(version, "\n")
   434  	version = versionSuffixPattern.ReplaceAllString(version, "")
   435  	return version
   436  }
   437  
   438  func versionSatisfied(current, required string) bool {
   439  	currentSegments := strings.SplitN(removeVersionPrefixAndSuffix(current), ".", 3)
   440  	requiredSegments := strings.SplitN(removeVersionPrefixAndSuffix(required), ".", 3)
   441  
   442  	requiredHasSuffix := versionSuffixPattern.MatchString(required)
   443  	if requiredHasSuffix {
   444  		return false // required version should be a formal version
   445  	}
   446  
   447  	for i := 0; i < 3; i++ {
   448  		var currentSeg, minimalSeg int
   449  		var err error
   450  		if currentSeg, err = strconv.Atoi(currentSegments[i]); err != nil {
   451  			klog.Warnf("invalid current version: %s, seg=%v, err=%v", current, currentSegments[i], err)
   452  			return false
   453  		}
   454  		if minimalSeg, err = strconv.Atoi(requiredSegments[i]); err != nil {
   455  			klog.Warnf("invalid required version: %s, seg=%v, err=%v", required, requiredSegments[i], err)
   456  			return false
   457  		}
   458  		if currentSeg > minimalSeg {
   459  			return true
   460  		} else if currentSeg < minimalSeg {
   461  			return false
   462  		}
   463  	}
   464  	if currentHasSuffix := versionSuffixPattern.MatchString(current); currentHasSuffix {
   465  		return false
   466  	}
   467  	return true
   468  }
   469  
   470  // LookupTool returns the compiler path found in $PATH; if not found, returns $GOPATH/bin/$tool
   471  func LookupTool(idlType, compilerPath string) string {
   472  	tool := "thriftgo"
   473  	if idlType == "protobuf" {
   474  		tool = "protoc"
   475  	}
   476  	if compilerPath != "" {
   477  		log.Infof("Will use the specified %s: %s\n", tool, compilerPath)
   478  		return compilerPath
   479  	}
   480  
   481  	path, err := exec.LookPath(tool)
   482  	if err != nil {
   483  		log.Warnf("Failed to find %q from $PATH: %s.\nTry $GOPATH/bin/%s instead\n", path, err.Error(), tool)
   484  		path = util.JoinPath(util.GetGOPATH(), "bin", tool)
   485  	}
   486  	return path
   487  }
   488  
   489  func initGoMod(pathToGo, module string) error {
   490  	if util.Exists("go.mod") {
   491  		return nil
   492  	}
   493  
   494  	cmd := &exec.Cmd{
   495  		Path:   pathToGo,
   496  		Args:   []string{"go", "mod", "init", module},
   497  		Stdin:  os.Stdin,
   498  		Stdout: os.Stdout,
   499  		Stderr: os.Stderr,
   500  	}
   501  	return cmd.Run()
   502  }