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  }