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 }