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 }