| // protobuf-compile is a helper tool for running protoc against all of the |
| // .proto files in this repository using specific versions of protoc and |
| // protoc-gen-go, to ensure consistent results across all development |
| // environments. |
| // |
| // protoc itself isn't a Go tool, so we need to use a custom strategy to |
| // install and run it. The official releases are built only for a subset of |
| // platforms that Go can potentially target, so this tool will fail if you |
| // are using a platform other than the ones this wrapper tool has explicit |
| // support for. In that case you'll need to either run this tool on a supported |
| // platform or to recreate what it does manually using a protoc you've built |
| // and installed yourself. |
| package main |
| |
| import ( |
| "fmt" |
| "log" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "runtime" |
| "strings" |
| |
| "github.com/hashicorp/go-getter" |
| ) |
| |
| const protocVersion = "3.15.6" |
| |
| // We also use protoc-gen-go and its grpc addon, but since these are Go tools |
| // in Go modules our version selection for these comes from our top-level |
| // go.mod, as with all other Go dependencies. If you want to switch to a newer |
| // version of either tool then you can upgrade their modules in the usual way. |
| const protocGenGoPackage = "github.com/golang/protobuf/protoc-gen-go" |
| const protocGenGoGrpcPackage = "google.golang.org/grpc/cmd/protoc-gen-go-grpc" |
| |
| type protocStep struct { |
| DisplayName string |
| WorkDir string |
| Args []string |
| } |
| |
| var protocSteps = []protocStep{ |
| { |
| "tfplugin5 (provider wire protocol version 5)", |
| "internal/tfplugin5", |
| []string{"--go_out=paths=source_relative,plugins=grpc:.", "./tfplugin5.proto"}, |
| }, |
| { |
| "tfplugin6 (provider wire protocol version 6)", |
| "internal/tfplugin6", |
| []string{"--go_out=paths=source_relative,plugins=grpc:.", "./tfplugin6.proto"}, |
| }, |
| { |
| "tfplan (plan file serialization)", |
| "internal/plans/internal/planproto", |
| []string{"--go_out=paths=source_relative:.", "planfile.proto"}, |
| }, |
| } |
| |
| func main() { |
| if len(os.Args) != 2 { |
| log.Fatal("Usage: go run github.com/hashicorp/terraform/tools/protobuf-compile <basedir>") |
| } |
| baseDir := os.Args[1] |
| workDir := filepath.Join(baseDir, "tools/protobuf-compile/.workdir") |
| |
| protocLocalDir := filepath.Join(workDir, "protoc-v"+protocVersion) |
| if _, err := os.Stat(protocLocalDir); os.IsNotExist(err) { |
| err := downloadProtoc(protocVersion, protocLocalDir) |
| if err != nil { |
| log.Fatal(err) |
| } |
| } else { |
| log.Printf("already have protoc v%s in %s", protocVersion, protocLocalDir) |
| } |
| |
| protocExec := filepath.Join(protocLocalDir, "bin/protoc") |
| |
| protocGenGoExec, err := buildProtocGenGo(workDir) |
| if err != nil { |
| log.Fatal(err) |
| } |
| _, err = buildProtocGenGoGrpc(workDir) |
| if err != nil { |
| log.Fatal(err) |
| } |
| |
| protocExec, err = filepath.Abs(protocExec) |
| if err != nil { |
| log.Fatal(err) |
| } |
| protocGenGoExec, err = filepath.Abs(protocGenGoExec) |
| if err != nil { |
| log.Fatal(err) |
| } |
| protocGenGoGrpcExec, err := filepath.Abs(protocGenGoExec) |
| if err != nil { |
| log.Fatal(err) |
| } |
| |
| // For all of our steps we'll run our localized protoc with our localized |
| // protoc-gen-go. |
| baseCmdLine := []string{protocExec, "--plugin=" + protocGenGoExec, "--plugin=" + protocGenGoGrpcExec} |
| |
| for _, step := range protocSteps { |
| log.Printf("working on %s", step.DisplayName) |
| |
| cmdLine := make([]string, 0, len(baseCmdLine)+len(step.Args)) |
| cmdLine = append(cmdLine, baseCmdLine...) |
| cmdLine = append(cmdLine, step.Args...) |
| |
| cmd := &exec.Cmd{ |
| Path: cmdLine[0], |
| Args: cmdLine[1:], |
| Dir: step.WorkDir, |
| Env: os.Environ(), |
| Stdout: os.Stdout, |
| Stderr: os.Stderr, |
| } |
| err := cmd.Run() |
| if err != nil { |
| log.Printf("failed to compile: %s", err) |
| } |
| } |
| |
| } |
| |
| // downloadProtoc downloads the given version of protoc into the given |
| // directory. |
| func downloadProtoc(version string, localDir string) error { |
| protocURL, err := protocDownloadURL(version) |
| if err != nil { |
| return err |
| } |
| |
| log.Printf("downloading and extracting protoc v%s from %s into %s", version, protocURL, localDir) |
| |
| // For convenience, we'll be using go-getter to actually download this |
| // thing, so we need to turn the real URL into the funny sort of pseudo-URL |
| // thing that go-getter wants. |
| goGetterURL := protocURL + "?archive=zip" |
| |
| err = getter.Get(localDir, goGetterURL) |
| if err != nil { |
| return fmt.Errorf("failed to download or extract the package: %s", err) |
| } |
| |
| return nil |
| } |
| |
| // buildProtocGenGo uses the Go toolchain to fetch the module containing |
| // protoc-gen-go and then build an executable into the working directory. |
| // |
| // If successful, it returns the location of the executable. |
| func buildProtocGenGo(workDir string) (string, error) { |
| exeSuffixRaw, err := exec.Command("go", "env", "GOEXE").Output() |
| if err != nil { |
| return "", fmt.Errorf("failed to determine executable suffix: %s", err) |
| } |
| exeSuffix := strings.TrimSpace(string(exeSuffixRaw)) |
| exePath := filepath.Join(workDir, "protoc-gen-go"+exeSuffix) |
| log.Printf("building %s as %s", protocGenGoPackage, exePath) |
| |
| cmd := exec.Command("go", "build", "-o", exePath, protocGenGoPackage) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| err = cmd.Run() |
| if err != nil { |
| return "", fmt.Errorf("failed to build %s: %s", protocGenGoPackage, err) |
| } |
| |
| return exePath, nil |
| } |
| |
| // buildProtocGenGoGrpc uses the Go toolchain to fetch the module containing |
| // protoc-gen-go-grpc and then build an executable into the working directory. |
| // |
| // If successful, it returns the location of the executable. |
| func buildProtocGenGoGrpc(workDir string) (string, error) { |
| exeSuffixRaw, err := exec.Command("go", "env", "GOEXE").Output() |
| if err != nil { |
| return "", fmt.Errorf("failed to determine executable suffix: %s", err) |
| } |
| exeSuffix := strings.TrimSpace(string(exeSuffixRaw)) |
| exePath := filepath.Join(workDir, "protoc-gen-go-grpc"+exeSuffix) |
| log.Printf("building %s as %s", protocGenGoGrpcPackage, exePath) |
| |
| cmd := exec.Command("go", "build", "-o", exePath, protocGenGoGrpcPackage) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| err = cmd.Run() |
| if err != nil { |
| return "", fmt.Errorf("failed to build %s: %s", protocGenGoGrpcPackage, err) |
| } |
| |
| return exePath, nil |
| } |
| |
| // protocDownloadURL returns the URL to try to download the protoc package |
| // for the current platform or an error if there's no known URL for the |
| // current platform. |
| func protocDownloadURL(version string) (string, error) { |
| platformKW := protocPlatform() |
| if platformKW == "" { |
| return "", fmt.Errorf("don't know where to find protoc for %s on %s", runtime.GOOS, runtime.GOARCH) |
| } |
| return fmt.Sprintf("https://github.com/protocolbuffers/protobuf/releases/download/v%s/protoc-%s-%s.zip", protocVersion, protocVersion, platformKW), nil |
| } |
| |
| // protocPlatform returns the package name substring for the current platform |
| // in the naming convention used by official protoc packages, or an empty |
| // string if we don't know how protoc packaging would describe current |
| // platform. |
| func protocPlatform() string { |
| goPlatform := runtime.GOOS + "_" + runtime.GOARCH |
| |
| switch goPlatform { |
| case "linux_amd64": |
| return "linux-x86_64" |
| case "linux_arm64": |
| return "linux-aarch_64" |
| case "darwin_amd64": |
| return "osx-x86_64" |
| case "darwin_arm64": |
| // As of 3.15.6 there isn't yet an osx-aarch_64 package available, |
| // so we'll install the x86_64 version and hope Rosetta can handle it. |
| return "osx-x86_64" |
| case "windows_amd64": |
| return "win64" // for some reason the windows packages don't have a CPU architecture part |
| default: |
| return "" |
| } |
| } |