| // Copyright 2017 The Bazel Authors. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| // protoc invokes the protobuf compiler and captures the resulting .pb.go file. |
| package main |
| |
| import ( |
| "bytes" |
| "errors" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| ) |
| |
| type genFileInfo struct { |
| base string // The basename of the path |
| path string // The full path to the final file |
| expected bool // Whether the file is expected by the rules |
| created bool // Whether the file was created by protoc |
| from *genFileInfo // The actual file protoc produced if not Path |
| unique bool // True if this base name is unique in expected results |
| ambiguious bool // True if there were more than one possible outputs that matched this file |
| } |
| |
| func run(args []string) error { |
| // process the args |
| args, useParamFile, err := expandParamsFiles(args) |
| if err != nil { |
| return err |
| } |
| options := multiFlag{} |
| descriptors := multiFlag{} |
| expected := multiFlag{} |
| imports := multiFlag{} |
| flags := flag.NewFlagSet("protoc", flag.ExitOnError) |
| protoc := flags.String("protoc", "", "The path to the real protoc.") |
| outPath := flags.String("out_path", "", "The base output path to write to.") |
| plugin := flags.String("plugin", "", "The go plugin to use.") |
| importpath := flags.String("importpath", "", "The importpath for the generated sources.") |
| flags.Var(&options, "option", "The plugin options.") |
| flags.Var(&descriptors, "descriptor_set", "The descriptor set to read.") |
| flags.Var(&expected, "expected", "The expected output files.") |
| flags.Var(&imports, "import", "Map a proto file to an import path.") |
| if err := flags.Parse(args); err != nil { |
| return err |
| } |
| |
| // Output to a temporary folder and then move the contents into place below. |
| // This is to work around long file paths on Windows. |
| tmpDir, err := ioutil.TempDir("", "go_proto") |
| if err != nil { |
| return err |
| } |
| tmpDir = abs(tmpDir) // required to work with long paths on Windows |
| absOutPath := abs(*outPath) // required to work with long paths on Windows |
| defer os.RemoveAll(tmpDir) |
| |
| pluginBase := filepath.Base(*plugin) |
| pluginName := strings.TrimSuffix( |
| strings.TrimPrefix(filepath.Base(*plugin), "protoc-gen-"), ".exe") |
| for _, m := range imports { |
| options = append(options, fmt.Sprintf("M%v", m)) |
| } |
| if runtime.GOOS == "windows" { |
| // Turn the plugin path into raw form, since we're handing it off to a non-go binary. |
| // This is required to work with long paths on Windows. |
| *plugin = "\\\\?\\" + abs(*plugin) |
| } |
| protoc_args := []string{ |
| fmt.Sprintf("--%v_out=%v:%v", pluginName, strings.Join(options, ","), tmpDir), |
| "--plugin", fmt.Sprintf("%v=%v", strings.TrimSuffix(pluginBase, ".exe"), *plugin), |
| "--descriptor_set_in", strings.Join(descriptors, string(os.PathListSeparator)), |
| } |
| protoc_args = append(protoc_args, flags.Args()...) |
| |
| var cmd *exec.Cmd |
| if useParamFile { |
| paramFile, err := ioutil.TempFile(tmpDir, "protoc-*.params") |
| if err != nil { |
| return fmt.Errorf("error creating param file for protoc: %v", err) |
| } |
| for _, arg := range protoc_args { |
| _, err := fmt.Fprintln(paramFile, arg) |
| if err != nil { |
| return fmt.Errorf("error writing param file for protoc: %v", err) |
| } |
| } |
| cmd = exec.Command(*protoc, "@"+paramFile.Name()) |
| } else { |
| cmd = exec.Command(*protoc, protoc_args...) |
| } |
| |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return fmt.Errorf("error running protoc: %v", err) |
| } |
| // Build our file map, and test for existance |
| files := map[string]*genFileInfo{} |
| byBase := map[string]*genFileInfo{} |
| for _, path := range expected { |
| info := &genFileInfo{ |
| path: path, |
| base: filepath.Base(path), |
| expected: true, |
| unique: true, |
| } |
| files[info.path] = info |
| if byBase[info.base] != nil { |
| info.unique = false |
| byBase[info.base].unique = false |
| } else { |
| byBase[info.base] = info |
| } |
| } |
| // Walk the generated files |
| filepath.Walk(tmpDir, func(path string, f os.FileInfo, err error) error { |
| relPath, err := filepath.Rel(tmpDir, path) |
| if err != nil { |
| return err |
| } |
| if relPath == "." { |
| return nil |
| } |
| |
| if f.IsDir() { |
| if err := os.Mkdir(filepath.Join(absOutPath, relPath), f.Mode()); !os.IsExist(err) { |
| return err |
| } |
| return nil |
| } |
| |
| if !strings.HasSuffix(path, ".go") { |
| return nil |
| } |
| |
| info := &genFileInfo{ |
| path: path, |
| base: filepath.Base(path), |
| created: true, |
| } |
| |
| if foundInfo, ok := files[relPath]; ok { |
| foundInfo.created = true |
| foundInfo.from = info |
| return nil |
| } |
| files[relPath] = info |
| copyTo := byBase[info.base] |
| switch { |
| case copyTo == nil: |
| // Unwanted output |
| case !copyTo.unique: |
| // not unique, no copy allowed |
| case copyTo.from != nil: |
| copyTo.ambiguious = true |
| info.ambiguious = true |
| default: |
| copyTo.from = info |
| copyTo.created = true |
| info.expected = true |
| } |
| return nil |
| }) |
| buf := &bytes.Buffer{} |
| for _, f := range files { |
| switch { |
| case f.expected && !f.created: |
| // Some plugins only create output files if the proto source files have |
| // have relevant definitions (e.g., services for grpc_gateway). Create |
| // trivial files that the compiler will ignore for missing outputs. |
| data := []byte("// +build ignore\n\npackage ignore") |
| if err := ioutil.WriteFile(abs(f.path), data, 0644); err != nil { |
| return err |
| } |
| case f.expected && f.ambiguious: |
| fmt.Fprintf(buf, "Ambiguious output %v.\n", f.path) |
| case f.from != nil: |
| data, err := ioutil.ReadFile(f.from.path) |
| if err != nil { |
| return err |
| } |
| if err := ioutil.WriteFile(abs(f.path), data, 0644); err != nil { |
| return err |
| } |
| case !f.expected: |
| //fmt.Fprintf(buf, "Unexpected output %v.\n", f.path) |
| } |
| if buf.Len() > 0 { |
| fmt.Fprintf(buf, "Check that the go_package option is %q.", *importpath) |
| return errors.New(buf.String()) |
| } |
| } |
| |
| return nil |
| } |
| |
| func main() { |
| if err := run(os.Args[1:]); err != nil { |
| log.Fatal(err) |
| } |
| } |