260 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			6.2 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
package main
 | 
						|
 | 
						|
import (
 | 
						|
	"bytes"
 | 
						|
	"flag"
 | 
						|
	"fmt"
 | 
						|
	"log"
 | 
						|
	"os"
 | 
						|
	"os/exec"
 | 
						|
	"path/filepath"
 | 
						|
	"strings"
 | 
						|
	"text/template"
 | 
						|
)
 | 
						|
 | 
						|
// defines several variables for parameterizing the protoc command. We can pull
 | 
						|
// this out into a toml files in cases where we to vary this per package.
 | 
						|
var (
 | 
						|
	generationPlugin = "gogoctrd"
 | 
						|
 | 
						|
	preIncludePaths = []string{
 | 
						|
		".",
 | 
						|
	}
 | 
						|
 | 
						|
	// vendoredIncludes is used for packages that should be included as vendor
 | 
						|
	// directories. We don't differentiate between packages and projects here.
 | 
						|
	// This should just be the root of where the protos are included from.
 | 
						|
	vendoredIncludes = []string{
 | 
						|
		"github.com/gogo/protobuf",
 | 
						|
	}
 | 
						|
 | 
						|
	// postIncludePaths defines untouched include paths to be added untouched
 | 
						|
	// to the protoc command.
 | 
						|
	postIncludePaths = []string{
 | 
						|
		"/usr/local/include", // common location for protoc installation of WKTs
 | 
						|
	}
 | 
						|
 | 
						|
	plugins = []string{
 | 
						|
		"grpc",
 | 
						|
	}
 | 
						|
 | 
						|
	// packageMap allows us to map protofile imports to specific Go packages. These
 | 
						|
	// becomes the M declarations at the end of the declaration.
 | 
						|
	packageMap = map[string]string{
 | 
						|
		"google/protobuf/timestamp.proto":  "github.com/gogo/protobuf/types",
 | 
						|
		"google/protobuf/any.proto":        "github.com/gogo/protobuf/types",
 | 
						|
		"google/protobuf/descriptor.proto": "github.com/gogo/protobuf/protoc-gen-gogo/descriptor",
 | 
						|
		"gogoproto/gogo.proto":             "github.com/gogo/protobuf/gogoproto",
 | 
						|
	}
 | 
						|
 | 
						|
	tmpl = template.Must(template.New("protoc").Parse(`protoc -I
 | 
						|
	{{- range $index, $include := .Includes -}}
 | 
						|
		{{if $index}}:{{end -}}
 | 
						|
			{{.}}
 | 
						|
		{{- end }} --
 | 
						|
	{{- .Name -}}_out=plugins={{- range $index, $plugin := .Plugins -}}
 | 
						|
		{{- if $index}}+{{end}}
 | 
						|
		{{- $plugin}}
 | 
						|
	{{- end -}}
 | 
						|
	,import_path={{.ImportPath}}
 | 
						|
	{{- range $proto, $gopkg := .PackageMap -}},M
 | 
						|
		{{- $proto}}={{$gopkg -}}
 | 
						|
	{{- end -}}
 | 
						|
	:{{- .OutputDir }}
 | 
						|
	{{- range .Files}} {{.}}{{end -}}
 | 
						|
`))
 | 
						|
)
 | 
						|
 | 
						|
// Protoc defines inputs to a protoc command string.
 | 
						|
type Protoc struct {
 | 
						|
	Name       string // backend name
 | 
						|
	Includes   []string
 | 
						|
	Plugins    []string
 | 
						|
	ImportPath string
 | 
						|
	PackageMap map[string]string
 | 
						|
	Files      []string
 | 
						|
	OutputDir  string
 | 
						|
}
 | 
						|
 | 
						|
func (p *Protoc) mkcmd() (string, error) {
 | 
						|
	var buf bytes.Buffer
 | 
						|
	if err := tmpl.Execute(&buf, p); err != nil {
 | 
						|
		return "", err
 | 
						|
	}
 | 
						|
 | 
						|
	return buf.String(), nil
 | 
						|
}
 | 
						|
 | 
						|
func main() {
 | 
						|
	flag.Parse()
 | 
						|
 | 
						|
	pkgInfos, err := goPkgInfo(flag.Args()...)
 | 
						|
	if err != nil {
 | 
						|
		log.Fatalln(err)
 | 
						|
	}
 | 
						|
 | 
						|
	gopath, err := gopathSrc()
 | 
						|
	if err != nil {
 | 
						|
		log.Fatalln(err)
 | 
						|
	}
 | 
						|
 | 
						|
	gopathCurrent, err := gopathCurrent()
 | 
						|
	if err != nil {
 | 
						|
		log.Fatalln(err)
 | 
						|
	}
 | 
						|
 | 
						|
	// For some reason, the golang protobuf generator makes the god awful
 | 
						|
	// decision to output the files relative to the gopath root. It doesn't do
 | 
						|
	// this only in the case where you give it ".".
 | 
						|
	outputDir := filepath.Join(gopathCurrent, "src")
 | 
						|
 | 
						|
	for _, pkg := range pkgInfos {
 | 
						|
		var includes []string
 | 
						|
		includes = append(includes, preIncludePaths...)
 | 
						|
 | 
						|
		vendor, err := closestVendorDir(pkg.Dir)
 | 
						|
		if err != nil {
 | 
						|
			log.Fatalln(err)
 | 
						|
		}
 | 
						|
 | 
						|
		// we also special case the inclusion of gogoproto in the vendor dir.
 | 
						|
		// We could parameterize this better if we find it to be a common case.
 | 
						|
		var vendoredIncludesResolved []string
 | 
						|
		for _, vendoredInclude := range vendoredIncludes {
 | 
						|
			vendoredIncludesResolved = append(vendoredIncludesResolved,
 | 
						|
				filepath.Join(vendor, vendoredInclude))
 | 
						|
		}
 | 
						|
 | 
						|
		includes = append(includes, vendoredIncludesResolved...)
 | 
						|
		includes = append(includes, vendor, gopath)
 | 
						|
		includes = append(includes, postIncludePaths...)
 | 
						|
 | 
						|
		protoc := Protoc{
 | 
						|
			Name:       generationPlugin,
 | 
						|
			ImportPath: pkg.GoImportPath,
 | 
						|
			PackageMap: packageMap,
 | 
						|
			Plugins:    plugins,
 | 
						|
			Files:      pkg.ProtoFiles,
 | 
						|
			OutputDir:  outputDir,
 | 
						|
			Includes:   includes,
 | 
						|
		}
 | 
						|
 | 
						|
		arg, err := protoc.mkcmd()
 | 
						|
		if err != nil {
 | 
						|
			log.Fatalln(err)
 | 
						|
		}
 | 
						|
 | 
						|
		fmt.Println(arg)
 | 
						|
 | 
						|
		// pass to sh -c so we don't need to re-split here.
 | 
						|
		args := []string{"-c", arg}
 | 
						|
		cmd := exec.Command("sh", args...)
 | 
						|
		out, err := cmd.CombinedOutput()
 | 
						|
		if err != nil {
 | 
						|
			log.Fatalf("%s %s\n", out, err)
 | 
						|
		}
 | 
						|
	}
 | 
						|
}
 | 
						|
 | 
						|
type protoGoPkgInfo struct {
 | 
						|
	Dir          string
 | 
						|
	GoImportPath string
 | 
						|
	ProtoFiles   []string
 | 
						|
}
 | 
						|
 | 
						|
func goPkgInfo(golistpath ...string) ([]protoGoPkgInfo, error) {
 | 
						|
	args := []string{
 | 
						|
		"list", "-e", "-f", "{{.ImportPath}} {{.Dir}}"}
 | 
						|
	args = append(args, golistpath...)
 | 
						|
	cmd := exec.Command("go", args...)
 | 
						|
 | 
						|
	p, err := cmd.Output()
 | 
						|
	if err != nil {
 | 
						|
		return nil, err
 | 
						|
	}
 | 
						|
 | 
						|
	var pkgInfos []protoGoPkgInfo
 | 
						|
	lines := bytes.Split(p, []byte("\n"))
 | 
						|
	for _, line := range lines {
 | 
						|
		if len(line) == 0 {
 | 
						|
			continue
 | 
						|
		}
 | 
						|
		parts := bytes.Fields(line)
 | 
						|
		if len(parts) != 2 {
 | 
						|
			return nil, fmt.Errorf("bad output from command: %s", p)
 | 
						|
		}
 | 
						|
 | 
						|
		pkgInfo := protoGoPkgInfo{
 | 
						|
			Dir:          string(parts[1]),
 | 
						|
			GoImportPath: string(parts[0]),
 | 
						|
		}
 | 
						|
 | 
						|
		protoFiles, err := filepath.Glob(filepath.Join(pkgInfo.Dir, "*.proto"))
 | 
						|
		if err != nil {
 | 
						|
			return nil, err
 | 
						|
		}
 | 
						|
		if len(protoFiles) == 0 {
 | 
						|
			continue // not a proto directory, skip
 | 
						|
		}
 | 
						|
 | 
						|
		pkgInfo.ProtoFiles = protoFiles
 | 
						|
		pkgInfos = append(pkgInfos, pkgInfo)
 | 
						|
	}
 | 
						|
 | 
						|
	return pkgInfos, nil
 | 
						|
}
 | 
						|
 | 
						|
// gopathSrc modifies GOPATH elements from env to include the src directory.
 | 
						|
func gopathSrc() (string, error) {
 | 
						|
	gopathAll := os.Getenv("GOPATH")
 | 
						|
 | 
						|
	if gopathAll == "" {
 | 
						|
		return "", fmt.Errorf("must be run from a gopath")
 | 
						|
	}
 | 
						|
 | 
						|
	var elements []string
 | 
						|
	for _, element := range strings.Split(gopathAll, ":") { // TODO(stevvooe): Make this work on windows.
 | 
						|
		elements = append(elements, filepath.Join(element, "src"))
 | 
						|
	}
 | 
						|
 | 
						|
	return strings.Join(elements, ":"), nil
 | 
						|
}
 | 
						|
 | 
						|
// gopathCurrent provides the top-level gopath for the current generation.
 | 
						|
func gopathCurrent() (string, error) {
 | 
						|
	gopathAll := os.Getenv("GOPATH")
 | 
						|
 | 
						|
	if gopathAll == "" {
 | 
						|
		return "", fmt.Errorf("must be run from a gopath")
 | 
						|
	}
 | 
						|
 | 
						|
	return strings.Split(gopathAll, ":")[0], nil
 | 
						|
}
 | 
						|
 | 
						|
// closestVendorDir walks up from dir until it finds the vendor directory.
 | 
						|
func closestVendorDir(dir string) (string, error) {
 | 
						|
	dir = filepath.Clean(dir)
 | 
						|
	for dir != "" && dir != string(filepath.Separator) { // TODO(stevvooe): May not work on windows
 | 
						|
		vendor := filepath.Join(dir, "vendor")
 | 
						|
		fi, err := os.Stat(vendor)
 | 
						|
		if err != nil {
 | 
						|
			if os.IsNotExist(err) {
 | 
						|
				// up we go!
 | 
						|
				dir = filepath.Dir(dir)
 | 
						|
				continue
 | 
						|
			}
 | 
						|
			return "", err
 | 
						|
		}
 | 
						|
 | 
						|
		if !fi.IsDir() {
 | 
						|
			// up we go!
 | 
						|
			dir = filepath.Dir(dir)
 | 
						|
			continue
 | 
						|
		}
 | 
						|
 | 
						|
		return vendor, nil
 | 
						|
	}
 | 
						|
 | 
						|
	return "", fmt.Errorf("no vendor dir found")
 | 
						|
}
 |