trpc.group/trpc-go/trpc-cmdline@v1.0.9/util/pb/protoc.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 pb encapsulates the protoc execution logic. 11 package pb 12 13 import ( 14 "errors" 15 "fmt" 16 "os" 17 "os/exec" 18 "path/filepath" 19 "sort" 20 "strings" 21 22 "trpc.group/trpc-go/trpc-cmdline/util/fs" 23 "trpc.group/trpc-go/trpc-cmdline/util/log" 24 "trpc.group/trpc-go/trpc-cmdline/util/paths" 25 ) 26 27 // Constants definition. 28 const ( 29 ProtoTRPC = "trpc/proto/trpc_options.proto" 30 ProtoValidate = "trpc/validate/validate.proto" 31 ProtoProtocGenValidate = "validate/validate.proto" // https://github.com/bufbuild/protoc-gen-validate 32 ProtoSwagger = "trpc/swagger/swagger.proto" 33 34 ProtoDir = "protobuf" 35 TrpcImportPrefix = "trpc/" 36 ) 37 38 // IsInternalProto tests if `fname` is internal. 39 func IsInternalProto(fname string) bool { 40 if strings.HasPrefix(fname, "google/protobuf") || 41 fname == ProtoTRPC || 42 fname == ProtoValidate || 43 fname == ProtoProtocGenValidate || 44 fname == ProtoSwagger || 45 strings.HasPrefix(fname, TrpcImportPrefix) { 46 return true 47 } 48 return false 49 } 50 51 // Protoc process `protofile` to generate *.pb.go, which is specified by `language` 52 // 53 // When using protoc, the following should also be taken into consideration: 54 // 1. The pb file being processed may import other pb files, e.g., a.proto imports b.proto, 55 // and the resulting a.pb.go file will call the initialization function in b.pb.go, 56 // typically named file_${path_to}b_proto_init(); 57 // 2. When executing protoc, the options passed can have an impact on the generated code. 58 // For instance, protoc -I path-to a.proto and protoc path-to/a.proto will generate different code, 59 // with the difference being reflected in the function name file${path_to}_b_proto_init(). 60 // The former will generate an empty ${path_to} part, leading to compilation failure. 61 // 62 // How to avoid this problem? 63 // - The import statements in pb files may contain virtual path information, 64 // which needs to be determined based on the search path specified with -I. 65 // - When processing pb files with protoc, the pb file names must contain virtual path information. 66 // For example, it should be protoc path-to/a.proto instead of protoc -I path-to a.proto. 67 // - Additionally, it is essential that there exists a search path in protoc's search path list 68 // that is the parent path of the pb file being processed. This is because of how protoc resolves paths. 69 // 70 // About the use of optional labels in proto syntax3: 71 // Compatibility logic needs to be implemented for different versions of protoc: 72 // - protoc (~, v3.12.0), pb syntax3 does not support optional labels 73 // - protoc [v3.12.0, v3.15.0), pb syntax3 supports optional labels, 74 // but the option --experimental_allow_proto3_optional needs to be added. 75 // - protoc v3.15.0+, optional labels in pb syntax3 syntax are parsed by default. 76 // 77 // ------------------------------------------------------------------------------------------------------------------ 78 // 79 // Regarding the issue of output paths for pb.go files: 80 // The paths=source_relative option controls the output filenames, not the import paths. 81 // The proto compiler associates an import path with each .proto file. When a.proto imports b.proto, 82 // the import path is used to determine what (if any) import statement to put in a.pb.go. You can set the import paths 83 // with go_package options in the .proto files, or with --go_opt=M<filename>=<import_path> on the command line. 84 // 85 // The proto compiler generates a .pb.go file for each .proto file. There are several ways in which the output 86 // directory may be determined. For example, if source/a.proto has an import path of example.com/m/foo: 87 // 88 // --go_opt=paths=import: Import path; e.g., example.com/m/foo/a.pb.go 89 // --go_opt=paths=source_relative: Source path; e.g., source/a.pb.go 90 // --go_opt=module=example.com/m: Path relative to the module flag; e.g., foo/a.pb.go 91 // 92 // In the worst case, if none of these suit your needs, you can always generate into a temporary directory and copy the 93 // file into the desired location. Neither the paths nor module flags have any effect on the contents of the generated 94 // files. 95 func Protoc(protodirs []string, protofile, lang, outputdir string, opts ...Option) error { 96 options := options{ 97 pb2ImportPath: make(map[string]string), 98 pkg2ImportPath: make(map[string]string), 99 } 100 for _, o := range opts { 101 o(&options) 102 } 103 104 protocArgs, err := genProtocArgs(protodirs, protofile, lang, outputdir, options) 105 if err != nil { 106 return fmt.Errorf("generate protoc args err: %w", err) 107 } 108 109 importPath, ok := options.pb2ImportPath[protofile] 110 if ok { 111 defer movePbGoFile(protocArgs.argsGoOut, importPath, protocArgs.baseDir, protofile) 112 } 113 114 var args []string 115 args = append(args, protocArgs.argsProtoPath...) 116 args = append(args, protocArgs.argsGoOut) 117 args = append(args, protofile) 118 if protocArgs.descriptorSetIn != "" { 119 args = append(args, protocArgs.descriptorSetIn) 120 } 121 122 // pb3 supports "optional" and other labels. 123 args, err = makePb3Labels(args) 124 if err != nil { 125 panic(err) 126 } 127 128 return execProtocCommand(args) 129 } 130 131 func genRelPathFromWdWithDirs(protodirs []string, protofile, wd string) (string, error) { 132 for _, dir := range protodirs { 133 pbPath := filepath.Join(dir, protofile) 134 if fin, err := os.Lstat(pbPath); err != nil || fin.IsDir() { 135 // If there is an error getting file information or the path is a directory, 136 // continue searching for the next one. 137 continue 138 } 139 140 rel, err := genRelPathFromWd(pbPath, wd) 141 if err != nil { 142 return "", err 143 } 144 145 return rel, nil 146 } 147 return "", errors.New("no valid relative path found, please check if the file exists") 148 } 149 150 func genRelPathFromWd(protofile, wd string) (string, error) { 151 absWd, err := filepath.Abs(wd) 152 if err != nil { 153 return "", fmt.Errorf("failed to obtain the absolute path for %s: %w", wd, err) 154 } 155 absPbFile, err := filepath.Abs(protofile) 156 if err != nil { 157 return "", fmt.Errorf("failed to obtain the absolute path for %s: %w", protofile, err) 158 } 159 relPath, err := filepath.Rel(absWd, absPbFile) 160 if err != nil { 161 log.Error("error getting the relative path from %s to %s", absWd, absPbFile) 162 return "", fmt.Errorf("Error getting the relative path: %w", err) 163 } 164 return relPath, nil 165 } 166 167 func execProtocCommand(args []string) error { 168 log.Debug("protoc %s", strings.Join(args, " ")) 169 170 cmd := exec.Command("protoc", args...) 171 if output, err := cmd.CombinedOutput(); err != nil { 172 msg := `Explicit 'optional' labels are disallowed in the Proto3 syntax` 173 str := strings.Join(cmd.Args, " ") 174 if strings.Contains(string(output), msg) { 175 return fmt.Errorf("run command: `%s`, error: %s...upgrade `protoc` to v3.15.0+", str, string(output)) 176 } 177 return fmt.Errorf("run command: `%s`, error: %s", str, string(output)) 178 } 179 180 return nil 181 } 182 183 type protocArgs struct { 184 baseDir string 185 argsProtoPath []string 186 argsGoOut string 187 descriptorSetIn string 188 } 189 190 func genProtocArgs(protodirs []string, protofile, lang, outputdir string, options options) (*protocArgs, error) { 191 baseDir, baseName := filepath.Split(protofile) 192 outputdir = strings.TrimSuffix(filepath.Clean(outputdir), "/"+filepath.Clean(baseDir)) 193 194 pb2ImportPath := options.pb2ImportPath 195 dirs, err := protoSearchDirs(pb2ImportPath) 196 if err != nil { 197 return nil, fmt.Errorf("proto search dirs err: %w", err) 198 } 199 protodirs = append(protodirs, dirs...) 200 201 // make --go_out 202 argsGoOut := makeProtocOut(pb2ImportPath, lang, outputdir, options) 203 args := &protocArgs{baseDir, nil, argsGoOut, ""} 204 if options.descriptorSetIn == "" { // --proto_path and --descriptor_set_in cannot coexist. 205 p, err := paths.Locate(baseName, protodirs...) 206 if err != nil { 207 return nil, fmt.Errorf("paths locate err: %w", err) 208 } 209 p = strings.TrimSuffix(filepath.Clean(p), filepath.Clean(baseDir)) 210 // make --proto_path 211 args.argsProtoPath, err = makeProtoPath(protodirs, p, options) 212 if err != nil { 213 return nil, fmt.Errorf("make proto path err: %w", err) 214 } 215 return args, nil 216 } 217 args.descriptorSetIn = "--descriptor_set_in=" + options.descriptorSetIn 218 return args, nil 219 } 220 221 func makePb3Labels(args []string) ([]string, error) { 222 // proto3 allow optional 223 v, err := protocVersion() 224 if err != nil { 225 return nil, err 226 } 227 if CheckVersionGreaterThanOrEqualTo(v, "v3.15.0") { 228 // enabled by default, and no longer require --experimental_allow_proto3_optional flag 229 } else if CheckVersionGreaterThanOrEqualTo(v, "v3.12.0") { 230 // [experimental] adding the "optional" field label, need passing --experimental_allow_proto3_optional flag 231 args = append(args, "--experimental_allow_proto3_optional") 232 } else if CheckVersionGreaterThanOrEqualTo(v, "v3.6.0") { 233 // Not supported, no need for special settings. 234 } else { 235 // Not supported and version is below the recommended version of trpc. 236 log.Info("protoc version too low, please upgrade it") 237 } 238 return args, nil 239 } 240 241 func movePbGoFile(argsGoOut, pkg, baseDir, protofile string) { 242 v := strings.Split(argsGoOut, ":") 243 if len(v) != 2 { 244 return 245 } 246 vv := strings.Split(v[1], "stub/") 247 if len(vv) != 2 { 248 return 249 } 250 if vv[1] == pkg { 251 pdir := filepath.Join(v[1], baseDir) 252 target := filepath.Join(pdir, fs.BaseNameWithoutExt(protofile)+".pb.go") 253 fs.Move(target, v[1]) 254 255 idx := strings.Index(baseDir, "/") 256 if idx != -1 { 257 path := filepath.Join(v[1], baseDir[0:idx]) 258 os.RemoveAll(path) 259 } 260 } 261 } 262 263 func protoSearchDirs(pb2ImportPath map[string]string) ([]string, error) { 264 var protodirs []string 265 var err error 266 267 // locate trpc.proto 268 protodirs, err = trpcProtoSearchDir(pb2ImportPath, protodirs) 269 if err != nil { 270 return nil, err 271 } 272 273 // locate validate.proto 274 protodirs, err = validateProtoSearchDir(pb2ImportPath, protodirs) 275 if err != nil { 276 return nil, err 277 } 278 279 // locate protobuf dir 280 if isTrpcProtoImported(pb2ImportPath) { 281 pbDir, err := paths.Locate(ProtoDir) 282 if err == nil { 283 // If the trpc system pb directory is found, it is imported, otherwise it is skipped. 284 protodirs = append(protodirs, filepath.Join(pbDir, "protobuf")) 285 } 286 } 287 288 return protodirs, nil 289 } 290 291 func validateProtoSearchDir(pb2ImportPath map[string]string, protodirs []string) ([]string, error) { 292 _, secvdep := pb2ImportPath[ProtoValidate] 293 if secvdep { 294 secvp, err := paths.Locate(ProtoValidate) 295 if err != nil { 296 return nil, err 297 } 298 protodirs = append(protodirs, secvp) 299 } 300 return protodirs, nil 301 } 302 303 func trpcProtoSearchDir(pb2ImportPath map[string]string, protodirs []string) ([]string, error) { 304 _, dep := pb2ImportPath[ProtoTRPC] 305 if dep { 306 p, err := paths.Locate(ProtoTRPC) 307 if err != nil { 308 return nil, err 309 } 310 protodirs = append(protodirs, p) 311 } 312 return protodirs, nil 313 } 314 315 func isTrpcProtoImported(pbpkgMapping map[string]string) bool { 316 for k := range pbpkgMapping { 317 if strings.HasPrefix(k, TrpcImportPrefix) { 318 return true 319 } 320 } 321 322 return false 323 } 324 325 func makeProtocOut(pb2ImportPath map[string]string, language, outputdir string, options options) string { 326 pbpkg := genPbpkg(pb2ImportPath) 327 argsGoOut := makeProtocOutByLanguage(language, pbpkg, outputdir) 328 329 if options.validationEnabled { 330 _, ok := options.pkg2ImportPath["validate"] 331 if ok { 332 argsGoOut = fixProtocOut("validate", argsGoOut, language) 333 } 334 } else if options.secvEnabled { 335 _, ok := options.pkg2ImportPath["validate"] 336 secvOut := "secv" 337 if !ok { 338 _, ok = options.pkg2ImportPath["trpc.v2.validate"] 339 secvOut = "secv-v2" 340 } 341 if ok { 342 argsGoOut = fixProtocOut(secvOut, argsGoOut, language) 343 } 344 } 345 return argsGoOut 346 } 347 348 func makeProtocOutByLanguage(language string, pbpkg string, outputdir string) string { 349 languageToOut := map[string]string{ 350 "go": fmt.Sprintf("--%s_out=paths=source_relative%s:%s", language, pbpkg, outputdir), 351 } 352 353 out := languageToOut[language] 354 if len(out) > 0 { 355 return out 356 } 357 358 // Other unexpected programming languages. 359 _ = os.MkdirAll(outputdir, os.ModePerm) 360 if len(pbpkg) != 0 { 361 pbpkg += ":" 362 } 363 out = fmt.Sprintf("--%s_out=%s%s", language, pbpkg, outputdir) 364 return out 365 } 366 367 func genPbpkg(pb2ImportPath map[string]string) string { 368 var pbpkg string 369 370 if len(pb2ImportPath) != 0 { 371 for k, v := range pb2ImportPath { 372 373 // 1. The official Google library should be left to protoc and protoc-gen-go to handle. 374 // 2. For other imported pb files, if they have the same validGoPkg as the protofile, 375 // then the package parsed by protoreflect/jhump for the pb file is empty. 376 // To solve the circular dependency problem here! 377 if strings.HasPrefix(k, "google/protobuf") || len(v) == 0 { 378 continue 379 } 380 //BUG: protoc-gen-go, https://google.golang.org/protobuf/issues/1151 381 //if v == protofileValidGoPkg { 382 // v = "." 383 //} 384 //if v == protofileValidGoPkg { 385 // continue 386 //} 387 //pbpkg += ",M" + k + "=" + lang.PBValidGoPackage(v) 388 pbpkg += ",M" + k + "=" + v 389 } 390 } 391 return pbpkg 392 } 393 394 func fixProtocOut(secvOut, protocOut, lang string) string { 395 new := fmt.Sprintf("--%s_out=lang=%s", secvOut, lang) 396 397 vals := strings.SplitN(protocOut, "=", 2) 398 params := vals[1] 399 400 switch lang { 401 case "go": 402 return new + "," + params 403 default: 404 return protocOut 405 } 406 } 407 408 func makeProtoPath(protodirs []string, must string, options options) ([]string, error) { 409 protodirs = append(protodirs, must) 410 protodirs = fs.UniqFilePath(protodirs) 411 412 args := []string{} 413 414 // BUG protoc/protoc-gen-go 415 // see: https://github.com/golang/protobuf/issues/1252#issuecomment-741626261 416 sort.Strings(protodirs) 417 418 wd, _ := os.Getwd() 419 for pos, each := range protodirs { 420 if wd == each { 421 var newProtodirs []string 422 newProtodirs = append(newProtodirs, protodirs[0:pos]...) 423 newProtodirs = append(newProtodirs, protodirs[pos+1:]...) 424 newProtodirs = append(newProtodirs, wd) 425 protodirs = newProtodirs 426 break 427 } 428 } 429 430 return genProtoPathArgs(protodirs, args) 431 } 432 433 func genProtoPathArgs(protodirs []string, args []string) ([]string, error) { 434 //for _, protodir := range protodirs { 435 for i := len(protodirs) - 1; i >= 0; i-- { 436 protodir := protodirs[i] 437 protodir, err := filepath.Abs(protodir) 438 if err != nil { 439 continue 440 } 441 442 // filter out non-existing directories. 443 fin, err := os.Lstat(protodir) 444 if err != nil || !fin.IsDir() { 445 continue 446 } 447 448 args = append(args, fmt.Sprintf("--proto_path=%s", protodir)) 449 } 450 return args, nil 451 }