github.com/Zenithar/prototool@v1.3.0/internal/protoc/compiler.go (about) 1 // Copyright (c) 2018 Uber Technologies, Inc. 2 // 3 // Permission is hereby granted, free of charge, to any person obtaining a copy 4 // of this software and associated documentation files (the "Software"), to deal 5 // in the Software without restriction, including without limitation the rights 6 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 // copies of the Software, and to permit persons to whom the Software is 8 // furnished to do so, subject to the following conditions: 9 // 10 // The above copyright notice and this permission notice shall be included in 11 // all copies or substantial portions of the Software. 12 // 13 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 // THE SOFTWARE. 20 21 package protoc 22 23 import ( 24 "bytes" 25 "errors" 26 "fmt" 27 "io/ioutil" 28 "os" 29 "os/exec" 30 "os/signal" 31 "path/filepath" 32 "regexp" 33 "runtime" 34 "strconv" 35 "strings" 36 "sync" 37 "syscall" 38 39 "github.com/golang/protobuf/proto" 40 "github.com/golang/protobuf/protoc-gen-go/descriptor" 41 "github.com/uber/prototool/internal/file" 42 "github.com/uber/prototool/internal/settings" 43 "github.com/uber/prototool/internal/text" 44 "github.com/uber/prototool/internal/wkt" 45 "go.uber.org/zap" 46 ) 47 48 var ( 49 // special cased 50 pluginFailedRegexp = regexp.MustCompile("^--.*_out: protoc-gen-(.*): Plugin failed with status code (.*).$") 51 otherPluginFailureRegexp = regexp.MustCompile("^--(.*)_out: (.*)$") 52 53 extraImportRegexp = regexp.MustCompile("^(.*): warning: Import (.*) but not used.$") 54 fileNotFoundRegexp = regexp.MustCompile("^(.*): File not found.$") 55 // protoc outputs both this line and fileNotFound, so we end up ignoring this one 56 // TODO figure out what the error is for errors in the import 57 importNotFoundRegexp = regexp.MustCompile("^(.*): Import (.*) was not found or had errors.$") 58 noSyntaxSpecifiedRegexp = regexp.MustCompile("No syntax specified for the proto file: (.*)\\. Please use") 59 jsonCamelCaseRegexp = regexp.MustCompile("^(.*): (The JSON camel-case name of field.*)$") 60 isNotDefinedRegexp = regexp.MustCompile("^(.*): (.*) is not defined.$") 61 seemsToBeDefinedRegexp = regexp.MustCompile(`^(.*): (".*" seems to be defined in ".*", which is not imported by ".*". To use it here, please add the necessary import.)$`) 62 explicitDefaultValuesProto3Regexp = regexp.MustCompile("^(.*): Explicit default values are not allowed in proto3.$") 63 optionValueRegexp = regexp.MustCompile("^(.*): Error while parsing option value for (.*)$") 64 programNotFoundRegexp = regexp.MustCompile("protoc-gen-(.*): program not found or is not executable$") 65 firstEnumValueZeroRegexp = regexp.MustCompile("^(.*): The first enum value must be zero in proto3.$") 66 ) 67 68 type compiler struct { 69 logger *zap.Logger 70 cachePath string 71 protocBinPath string 72 protocWKTPath string 73 protocURL string 74 doGen bool 75 doFileDescriptorSet bool 76 } 77 78 func newCompiler(options ...CompilerOption) *compiler { 79 compiler := &compiler{ 80 logger: zap.NewNop(), 81 } 82 for _, option := range options { 83 option(compiler) 84 } 85 return compiler 86 } 87 88 func (c *compiler) Compile(protoSet *file.ProtoSet) (*CompileResult, error) { 89 cmdMetas, err := c.getCmdMetas(protoSet) 90 if err != nil { 91 cleanCmdMetas(cmdMetas) 92 return nil, err 93 } 94 95 // we potentially create temporary files if doFileDescriptorSet is true 96 // if so, we try to remove them when we return no matter what 97 // by putting this defer here, we get this catch early 98 defer cleanCmdMetas(cmdMetas) 99 100 if c.doGen { 101 // the directories for the output files have to exist 102 // so if we are generating, we create them before running 103 // protoc, which calls the plugins, which results in created 104 // generated files potentially 105 // we know the directories from the output option in the 106 // config files 107 if err := c.makeGenDirs(protoSet); err != nil { 108 return nil, err 109 } 110 } 111 var failures []*text.Failure 112 var errs []error 113 var lock sync.Mutex 114 var wg sync.WaitGroup 115 for _, cmdMeta := range cmdMetas { 116 cmdMeta := cmdMeta 117 wg.Add(1) 118 go func() { 119 defer wg.Done() 120 iFailures, iErr := c.runCmdMeta(cmdMeta) 121 lock.Lock() 122 failures = append(failures, iFailures...) 123 if iErr != nil { 124 errs = append(errs, iErr) 125 } 126 lock.Unlock() 127 }() 128 } 129 wg.Wait() 130 // errors are not text.Failures, these are actual unhandled 131 // system errors from calling protoc, so we short circuit 132 if len(errs) > 0 { 133 // I want newlines instead of spaces so not using multierr 134 errStrings := make([]string, 0, len(errs)) 135 for _, err := range errs { 136 // errors.New("") is a non-nil error, so even 137 // if all error strings are empty, we still get an error 138 if errString := err.Error(); errString != "" { 139 errStrings = append(errStrings, errString) 140 } 141 } 142 return nil, errors.New(strings.Join(errStrings, "\n")) 143 } 144 // if we have failures, it does not matter if we have file descriptor sets 145 // as we should error out, so we do not do any parsing of file descriptor sets 146 // this decision could be revisited 147 if len(failures) > 0 { 148 text.SortFailures(failures) 149 return &CompileResult{ 150 Failures: failures, 151 }, nil 152 } 153 154 fileDescriptorSets := make([]*descriptor.FileDescriptorSet, 0, len(cmdMetas)) 155 for _, cmdMeta := range cmdMetas { 156 // if doFileDescriptorSet is not set, we won't get a fileDescriptorSet anyways, 157 // so the end result will be an empty CompileResult at this point 158 fileDescriptorSet, err := getFileDescriptorSet(cmdMeta) 159 if err != nil { 160 return nil, err 161 } 162 if fileDescriptorSet != nil { 163 fileDescriptorSets = append(fileDescriptorSets, fileDescriptorSet) 164 } 165 } 166 return &CompileResult{ 167 FileDescriptorSets: fileDescriptorSets, 168 }, nil 169 } 170 171 func (c *compiler) ProtocCommands(protoSet *file.ProtoSet) ([]string, error) { 172 // we end up calling the logic that creates temporary files for file descriptor sets 173 // anyways, so we need to clean them up with cleanCmdMetas 174 // this logic could be simplified to have a "dry run" option, but ProtocCommands 175 // is more for debugging anyways 176 cmdMetas, err := c.getCmdMetas(protoSet) 177 if err != nil { 178 return nil, err 179 } 180 cmdMetaStrings := make([]string, 0, len(cmdMetas)) 181 for _, cmdMeta := range cmdMetas { 182 cmdMetaStrings = append(cmdMetaStrings, cmdMeta.String()) 183 } 184 cleanCmdMetas(cmdMetas) 185 return cmdMetaStrings, nil 186 } 187 188 func (c *compiler) makeGenDirs(protoSet *file.ProtoSet) error { 189 genDirs := make(map[string]struct{}) 190 for _, genPlugin := range protoSet.Config.Gen.Plugins { 191 genDirs[genPlugin.OutputPath.AbsPath] = struct{}{} 192 } 193 for genDir := range genDirs { 194 // we could choose a different permission set, but this seems reasonable 195 // in a perfect world, if directories are created and we error out, we 196 // would want to remove any newly created directories, but this seems 197 // like overkill as these directories would be created on success as 198 // generated directories anyways 199 if err := os.MkdirAll(genDir, 0744); err != nil { 200 return err 201 } 202 } 203 return nil 204 } 205 206 func (c *compiler) runCmdMeta(cmdMeta *cmdMeta) ([]*text.Failure, error) { 207 c.logger.Debug("running protoc", zap.String("command", cmdMeta.String())) 208 buffer := bytes.NewBuffer(nil) 209 cmdMeta.execCmd.Stderr = buffer 210 // We only need stderr to parse errors 211 // you have to explicitly set to ioutil.Discard, otherwise if there 212 // is a stdout, it will be printed to os.Stdout. 213 cmdMeta.execCmd.Stdout = ioutil.Discard 214 215 // Prepare a signal buffer so that we can kill the protoc 216 // process when Prototool receives a SIGINT or SIGTERM. 217 sig := make(chan os.Signal, 1) 218 done := make(chan error, 1) 219 signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM) 220 221 go func() { 222 done <- cmdMeta.execCmd.Run() 223 }() 224 225 var runErr error 226 select { 227 case s := <-sig: 228 // Kill the process, and terminate early. 229 c.logger.Debug( 230 "terminating protoc", 231 zap.String("command", cmdMeta.String()), 232 zap.String("signal", s.String()), 233 ) 234 return nil, cmdMeta.execCmd.Process.Kill() 235 case runErr = <-done: 236 // Exit errors are ok, we can probably parse them into text.Failures 237 // if not an exec.ExitError, short circuit. 238 if _, ok := runErr.(*exec.ExitError); !ok && runErr != nil { 239 return nil, runErr 240 } 241 } 242 output := strings.TrimSpace(buffer.String()) 243 if output != "" { 244 c.logger.Debug("protoc output", zap.String("output", output)) 245 } 246 // We want to treat any output from protoc as a failure, even if 247 // protoc exited with 0 status. This is because there are outputs 248 // from protoc that we consider errors that protoc considers warnings, 249 // and plugins in general do not produce output unless there is an error. 250 // See https://github.com/uber/prototool/issues/128 for a full discussion. 251 failures := c.parseProtocOutput(cmdMeta, output) 252 // We had a run error but for whatever reason did not get any parsed 253 // output lines, we still want to fail in this case 254 // this generally should not happen, especially as plugins that fail 255 // will result in a pluginFailedRegexp matching line but this 256 // is just to make sure. 257 if len(failures) == 0 && runErr != nil { 258 return nil, runErr 259 } 260 return failures, nil 261 } 262 263 func (c *compiler) getCmdMetas(protoSet *file.ProtoSet) (cmdMetas []*cmdMeta, retErr error) { 264 defer func() { 265 // if we error in this function, we clean ourselves up 266 if retErr != nil { 267 cleanCmdMetas(cmdMetas) 268 cmdMetas = nil 269 } 270 }() 271 // you need a new downloader for every ProtoSet as each configuration file could 272 // have a different protoc.version value 273 downloader, err := c.newDownloader(protoSet.Config) 274 if err != nil { 275 return nil, err 276 } 277 if _, err := downloader.Download(); err != nil { 278 return cmdMetas, err 279 } 280 for dirPath, protoFiles := range protoSet.DirPathToFiles { 281 // you want your proto files to be in at least one of the -I directories 282 // or otherwise things can get weird 283 // we make best effort to make sure we have the a parent directory of the file 284 // if we have a config, use that directory, otherwise use the working directory 285 // 286 // This does what I'd expect `prototool` to do out of the box: 287 // 288 // - If a configuration file is present, use that as the root for your imports. 289 // So if you have a/b/prototool.yaml and a/b/c/d/one.proto, a/b/c/e/two.proto, 290 // you'd import c/d/one.proto in two.proto. 291 // - If there's no configuration file, I expect my imports to start with the current directory. 292 configDirPath := protoSet.Config.DirPath 293 if configDirPath == "" { 294 configDirPath = protoSet.WorkDirPath 295 } 296 includes, err := getIncludes(downloader, protoSet.Config, dirPath, configDirPath) 297 if err != nil { 298 return cmdMetas, err 299 } 300 var args []string 301 for _, include := range includes { 302 args = append(args, "-I", include) 303 } 304 protocPath, err := downloader.ProtocPath() 305 if err != nil { 306 return cmdMetas, err 307 } 308 // this could really use some refactoring 309 // descriptorSetFilePath will either be a temporary file that we output 310 // a file descriptor set to, or the system equivalent of /dev/null 311 // isTempFile is effectively != /dev/null for all intents and purposes 312 // we do -o /dev/null because protoc needs at least one output, but in the compile-only 313 // mode, we want to just test for compile failures 314 descriptorSetFilePath, isTempFile, err := c.getDescriptorSetFilePath(protoSet) 315 if err != nil { 316 return cmdMetas, err 317 } 318 if descriptorSetFilePath != "" { 319 descriptorSetTempFilePath := descriptorSetFilePath 320 if !isTempFile { 321 descriptorSetTempFilePath = "" 322 } 323 // either /dev/null or a temporary file 324 iArgs := append(args, "-o", descriptorSetFilePath) 325 // if its a temporary file, that means we actually care about the output 326 // so we do --include_imports to get all necessary info in the output file descriptor set 327 if descriptorSetTempFilePath != "" { 328 // TODO(pedge): we will need source info if we switch out emicklei/proto 329 //iArgs = append(iArgs, "--include_source_info") 330 iArgs = append(iArgs, "--include_imports") 331 } 332 for _, protoFile := range protoFiles { 333 iArgs = append(iArgs, protoFile.Path) 334 } 335 cmdMetas = append(cmdMetas, &cmdMeta{ 336 execCmd: exec.Command(protocPath, iArgs...), 337 protoSet: protoSet, 338 protoFiles: protoFiles, 339 // used for cleaning up the cmdMeta after everything is done 340 descriptorSetTempFilePath: descriptorSetTempFilePath, 341 }) 342 } 343 pluginFlagSets, err := c.getPluginFlagSets(protoSet, dirPath) 344 if err != nil { 345 return cmdMetas, err 346 } 347 for _, pluginFlagSet := range pluginFlagSets { 348 iArgs := append(args, pluginFlagSet...) 349 for _, protoFile := range protoFiles { 350 iArgs = append(iArgs, protoFile.Path) 351 } 352 cmdMetas = append(cmdMetas, &cmdMeta{ 353 execCmd: exec.Command(protocPath, iArgs...), 354 protoSet: protoSet, 355 protoFiles: protoFiles, 356 }) 357 } 358 } 359 return cmdMetas, nil 360 } 361 362 func (c *compiler) newDownloader(config settings.Config) (Downloader, error) { 363 downloaderOptions := []DownloaderOption{ 364 DownloaderWithLogger(c.logger), 365 } 366 if c.cachePath != "" { 367 downloaderOptions = append( 368 downloaderOptions, 369 DownloaderWithCachePath(c.cachePath), 370 ) 371 } 372 if c.protocBinPath != "" { 373 downloaderOptions = append( 374 downloaderOptions, 375 DownloaderWithProtocBinPath(c.protocBinPath), 376 ) 377 } 378 if c.protocWKTPath != "" { 379 downloaderOptions = append( 380 downloaderOptions, 381 DownloaderWithProtocWKTPath(c.protocWKTPath), 382 ) 383 } 384 if c.protocURL != "" { 385 downloaderOptions = append( 386 downloaderOptions, 387 DownloaderWithProtocURL(c.protocURL), 388 ) 389 } 390 return NewDownloader(config, downloaderOptions...) 391 } 392 393 // return true if a temp file 394 func (c *compiler) getDescriptorSetFilePath(protoSet *file.ProtoSet) (string, bool, error) { 395 if c.doFileDescriptorSet { 396 tempFilePath, err := getTempFilePath() 397 if err != nil { 398 return "", false, err 399 } 400 return tempFilePath, true, nil 401 } 402 if c.doGen && len(protoSet.Config.Gen.Plugins) > 0 { 403 return "", false, nil 404 } 405 devNullFilePath, err := devNull() 406 return devNullFilePath, false, err 407 } 408 409 // each value in the slice of string slices is a flag passed to protoc 410 // examples: 411 // []string{"--go_out=plugins=grpc:."} 412 // []string{"--grpc-cpp_out=.", "--plugin=protoc-gen-grpc-cpp=/path/to/foo"} 413 func (c *compiler) getPluginFlagSets(protoSet *file.ProtoSet, dirPath string) ([][]string, error) { 414 // if not generating, or there are no plugins, nothing to do 415 if !c.doGen || len(protoSet.Config.Gen.Plugins) == 0 { 416 return nil, nil 417 } 418 pluginFlagSets := make([][]string, 0, len(protoSet.Config.Gen.Plugins)) 419 for _, genPlugin := range protoSet.Config.Gen.Plugins { 420 pluginFlagSet, err := getPluginFlagSet(protoSet, dirPath, genPlugin) 421 if err != nil { 422 return nil, err 423 } 424 pluginFlagSets = append(pluginFlagSets, pluginFlagSet) 425 } 426 return pluginFlagSets, nil 427 } 428 429 func getPluginFlagSet(protoSet *file.ProtoSet, dirPath string, genPlugin settings.GenPlugin) ([]string, error) { 430 protoFlags, err := getPluginFlagSetProtoFlags(protoSet, dirPath, genPlugin) 431 if err != nil { 432 return nil, err 433 } 434 flagSet := []string{fmt.Sprintf("--%s_out=%s", genPlugin.Name, genPlugin.OutputPath.AbsPath)} 435 if len(protoFlags) > 0 { 436 flagSet = []string{fmt.Sprintf("--%s_out=%s:%s", genPlugin.Name, protoFlags, genPlugin.OutputPath.AbsPath)} 437 } 438 if genPlugin.Path != "" { 439 flagSet = append(flagSet, fmt.Sprintf("--plugin=protoc-gen-%s=%s", genPlugin.Name, genPlugin.Path)) 440 } 441 return flagSet, nil 442 } 443 444 // the return value corresponds to CodeGeneratorRequest.Parameter 445 // https://github.com/golang/protobuf/blob/b4deda0973fb4c70b50d226b1af49f3da59f5265/protoc-gen-go/plugin/plugin.pb.go#L103 446 // this function basically just sets the Mfile=package values for go and gogo plugins 447 func getPluginFlagSetProtoFlags(protoSet *file.ProtoSet, dirPath string, genPlugin settings.GenPlugin) (string, error) { 448 // the type just denotes what Well-Known Type map to use from the wkt package 449 // if not go or gogo, we don't have any special automatic handling, so just return what we have 450 if !genPlugin.Type.IsGo() && !genPlugin.Type.IsGogo() { 451 return genPlugin.Flags, nil 452 } 453 if genPlugin.Type.IsGo() && genPlugin.Type.IsGogo() { 454 return "", fmt.Errorf("internal error: plugin %s is both a go and gogo plugin", genPlugin.Name) 455 } 456 var goFlags []string 457 if genPlugin.Flags != "" { 458 goFlags = append(goFlags, genPlugin.Flags) 459 } 460 genGoPluginOptions := protoSet.Config.Gen.GoPluginOptions 461 modifiers := make(map[string]string) 462 for subDirPath, protoFiles := range protoSet.DirPathToFiles { 463 // you cannot include the files in the same package in the Mfile=package map 464 // or otherwise protoc-gen-go, protoc-gen-gogo, etc freak out and put 465 // these packages in as imports 466 if subDirPath != dirPath { 467 for _, protoFile := range protoFiles { 468 path, err := filepath.Rel(protoSet.Config.DirPath, protoFile.Path) 469 if err != nil { 470 // TODO: best effort, maybe error 471 path = protoFile.Path 472 } 473 // TODO: if relative path in OutputPath.RelPath jumps out of import path context, this will be wrong 474 modifiers[path] = filepath.Clean(filepath.Join(genGoPluginOptions.ImportPath, genPlugin.OutputPath.RelPath, filepath.Dir(path))) 475 } 476 } 477 } 478 for key, value := range modifiers { 479 goFlags = append(goFlags, fmt.Sprintf("M%s=%s", key, value)) 480 } 481 if protoSet.Config.Compile.IncludeWellKnownTypes { 482 var wktModifiers map[string]string 483 // one of these two must be true, we validate this above 484 if genPlugin.Type.IsGo() { 485 wktModifiers = wkt.FilenameToGoModifierMap 486 } else if genPlugin.Type.IsGogo() { 487 wktModifiers = wkt.FilenameToGogoModifierMap 488 } 489 for key, value := range wktModifiers { 490 goFlags = append(goFlags, fmt.Sprintf("M%s=%s", key, value)) 491 } 492 } 493 for key, value := range genGoPluginOptions.ExtraModifiers { 494 goFlags = append(goFlags, fmt.Sprintf("M%s=%s", key, value)) 495 } 496 return strings.Join(goFlags, ","), nil 497 } 498 499 func getIncludes(downloader Downloader, config settings.Config, dirPath string, configDirPath string) ([]string, error) { 500 var includes []string 501 fileInIncludePath := false 502 includedConfigDirPath := false 503 for _, includePath := range config.Compile.IncludePaths { 504 includes = append(includes, includePath) 505 // TODO: not exactly platform independent 506 if strings.HasPrefix(dirPath, includePath) { 507 fileInIncludePath = true 508 } 509 if includePath == configDirPath { 510 includedConfigDirPath = true 511 } 512 } 513 if config.Compile.IncludeWellKnownTypes { 514 wellKnownTypesIncludePath, err := downloader.WellKnownTypesIncludePath() 515 if err != nil { 516 return nil, err 517 } 518 includes = append(includes, wellKnownTypesIncludePath) 519 // TODO: not exactly platform independent 520 if strings.HasPrefix(dirPath, wellKnownTypesIncludePath) { 521 fileInIncludePath = true 522 } 523 } 524 // you want your proto files to be in at least one of the -I directories 525 // or otherwise things can get weird 526 // if the file is not in one of the -I directories and we haven't included 527 // the config directory set, at least do that to try to help out 528 // this logic could be removed as it is special casing a bit 529 if !fileInIncludePath && !includedConfigDirPath { 530 includes = append(includes, configDirPath) 531 } 532 return includes, nil 533 } 534 535 // we try to handle all protoc errors to convert them into text.Failures 536 // so we can output failures in the standard filename:line:column:message format 537 func (c *compiler) parseProtocOutput(cmdMeta *cmdMeta, output string) []*text.Failure { 538 var failures []*text.Failure 539 for _, line := range strings.Split(strings.TrimSpace(output), "\n") { 540 line = strings.TrimSpace(line) 541 if line != "" { 542 if failure := c.parseProtocLine(cmdMeta, line); failure != nil { 543 failures = append(failures, failure) 544 } 545 } 546 } 547 return failures 548 } 549 550 func (c *compiler) parseProtocLine(cmdMeta *cmdMeta, protocLine string) *text.Failure { 551 if matches := pluginFailedRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 552 return &text.Failure{ 553 Message: fmt.Sprintf("protoc-gen-%s failed with status code %s.", matches[1], matches[2]), 554 } 555 } 556 if matches := otherPluginFailureRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 557 return &text.Failure{ 558 Message: fmt.Sprintf("protoc-gen-%s: %s", matches[1], matches[2]), 559 } 560 } 561 split := strings.Split(protocLine, ":") 562 if len(split) != 4 { 563 if matches := noSyntaxSpecifiedRegexp.FindStringSubmatch(protocLine); len(matches) > 1 { 564 return &text.Failure{ 565 Filename: bestFilePath(cmdMeta, matches[1]), 566 Message: `No syntax specified. Please use 'syntax = "proto2";' or 'syntax = "proto3";' to specify a syntax version.`, 567 } 568 } 569 if matches := extraImportRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 570 if cmdMeta.protoSet.Config.Compile.AllowUnusedImports { 571 return nil 572 } 573 return &text.Failure{ 574 Filename: bestFilePath(cmdMeta, matches[1]), 575 Message: fmt.Sprintf(`Import "%s" was not used.`, matches[2]), 576 } 577 } 578 if matches := fileNotFoundRegexp.FindStringSubmatch(protocLine); len(matches) > 1 { 579 return &text.Failure{ 580 // TODO: can we figure out the file name? 581 Filename: "", 582 Message: fmt.Sprintf(`Import "%s" was not found.`, matches[1]), 583 } 584 } 585 if matches := explicitDefaultValuesProto3Regexp.FindStringSubmatch(protocLine); len(matches) > 1 { 586 return &text.Failure{ 587 Filename: bestFilePath(cmdMeta, matches[1]), 588 Message: `Explicit default values are not allowed in proto3.`, 589 } 590 } 591 if matches := importNotFoundRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 592 // handled by fileNotFoundRegexp 593 // see comments at top 594 return nil 595 } 596 if matches := jsonCamelCaseRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 597 return &text.Failure{ 598 Filename: bestFilePath(cmdMeta, matches[1]), 599 Message: matches[2], 600 } 601 } 602 if matches := isNotDefinedRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 603 return &text.Failure{ 604 Filename: bestFilePath(cmdMeta, matches[1]), 605 Message: fmt.Sprintf(`%s is not defined.`, matches[2]), 606 } 607 } 608 if matches := seemsToBeDefinedRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 609 return &text.Failure{ 610 Filename: bestFilePath(cmdMeta, matches[1]), 611 Message: matches[2], 612 } 613 } 614 if matches := optionValueRegexp.FindStringSubmatch(protocLine); len(matches) > 2 { 615 return &text.Failure{ 616 Filename: bestFilePath(cmdMeta, matches[1]), 617 Message: fmt.Sprintf(`Error while parsing option value for %s`, matches[2]), 618 } 619 } 620 if matches := programNotFoundRegexp.FindStringSubmatch(protocLine); len(matches) > 1 { 621 return &text.Failure{ 622 Message: fmt.Sprintf("protoc-gen-%s not found or is not executable.", matches[1]), 623 } 624 } 625 if matches := firstEnumValueZeroRegexp.FindStringSubmatch(protocLine); len(matches) > 1 { 626 return &text.Failure{ 627 Filename: bestFilePath(cmdMeta, matches[1]), 628 Message: `The first enum value must be zero in proto3.`, 629 } 630 } 631 // TODO: plugins can output to stderr as well and we have no way to redirect the output 632 // this will error if there are any logging line from a plugin 633 // I would prefer to error so that we signal that we don't know what the line is 634 // but if this becomes problematic with some plugin in the future, we should 635 // return nil, nil here 636 return c.handleUninterpretedProtocLine(protocLine) 637 } 638 line, err := strconv.Atoi(split[1]) 639 if err != nil { 640 return c.handleUninterpretedProtocLine(protocLine) 641 } 642 column, err := strconv.Atoi(split[2]) 643 if err != nil { 644 return c.handleUninterpretedProtocLine(protocLine) 645 } 646 message := strings.TrimSpace(split[3]) 647 if message == "" { 648 return c.handleUninterpretedProtocLine(protocLine) 649 } 650 return &text.Failure{ 651 Filename: bestFilePath(cmdMeta, split[0]), 652 Line: line, 653 Column: column, 654 Message: message, 655 } 656 } 657 658 func (c *compiler) handleUninterpretedProtocLine(protocLine string) *text.Failure { 659 c.logger.Warn("protoc returned a line we do not understand, please file this as an issue "+ 660 "at https://github.com/uber/prototool/issues/new", zap.String("protocLine", protocLine)) 661 return &text.Failure{ 662 Message: protocLine, 663 } 664 } 665 666 // protoc does weird things with the outputted filename depending 667 // on what is on the include path, it finds the highest directory 668 // that the file is on apparently 669 // -I etc etc/testdata/foo.proto will result in testdata/foo.proto 670 // this makes it consistent if possible 671 // TODO: if the file name is not in the given compile command, ie 672 // if it is imported from another directory, we do not handle this, 673 // do we want to do a full search of all files in the ProtoSet? 674 // 675 // this does getDisplayFilePath but returns match if there is an error 676 func bestFilePath(cmdMeta *cmdMeta, match string) string { 677 displayFilePath, err := getDisplayFilePath(cmdMeta, match) 678 if err != nil { 679 return match 680 } 681 return displayFilePath 682 } 683 684 // this does bestFilePath but if there is not exactly one match, 685 // returns an error 686 func getDisplayFilePath(cmdMeta *cmdMeta, match string) (string, error) { 687 matchingFile := "" 688 for _, protoFile := range cmdMeta.protoFiles { 689 // if the suffix is the file name, this is a better display name 690 // we don't handle the reverse case, ie display path is a suffix of match 691 if strings.HasSuffix(protoFile.DisplayPath, match) { 692 // if there is more than one match, we don't know what to do 693 if matchingFile != "" { 694 return "", fmt.Errorf("duplicate matching file: %s", matchingFile) 695 } 696 matchingFile = protoFile.DisplayPath 697 } 698 } 699 if matchingFile == "" { 700 return "", fmt.Errorf("no matching file for %s", match) 701 } 702 return matchingFile, nil 703 } 704 705 func getFileDescriptorSet(cmdMeta *cmdMeta) (*descriptor.FileDescriptorSet, error) { 706 if cmdMeta.descriptorSetTempFilePath == "" { 707 return nil, nil 708 } 709 data, err := ioutil.ReadFile(cmdMeta.descriptorSetTempFilePath) 710 if err != nil { 711 return nil, err 712 } 713 fileDescriptorSet := &descriptor.FileDescriptorSet{} 714 if err := proto.Unmarshal(data, fileDescriptorSet); err != nil { 715 return nil, err 716 } 717 return fileDescriptorSet, nil 718 } 719 720 func devNull() (string, error) { 721 switch runtime.GOOS { 722 case "darwin", "linux": 723 return "/dev/null", nil 724 case "windows": 725 return "nul", nil 726 default: 727 return "", fmt.Errorf("unsupported operating system: %s", runtime.GOOS) 728 } 729 } 730 731 func getTempFilePath() (string, error) { 732 tempFile, err := ioutil.TempFile("", "prototool") 733 if err != nil { 734 return "", err 735 } 736 return tempFile.Name(), nil 737 } 738 739 func cleanCmdMetas(cmdMetas []*cmdMeta) { 740 for _, cmdMeta := range cmdMetas { 741 cmdMeta.Clean() 742 } 743 } 744 745 type cmdMeta struct { 746 execCmd *exec.Cmd 747 protoSet *file.ProtoSet 748 protoFiles []*file.ProtoFile 749 descriptorSetTempFilePath string 750 } 751 752 func (c *cmdMeta) String() string { 753 return strings.Join(c.execCmd.Args, " ") 754 } 755 756 func (c *cmdMeta) Clean() { 757 tryRemoveTempFile(c.descriptorSetTempFilePath) 758 } 759 760 func tryRemoveTempFile(tempFilePath string) { 761 if tempFilePath != "" { 762 _ = os.Remove(tempFilePath) 763 } 764 }