mirror of
https://github.com/opentffoundation/opentf.git
synced 2025-12-22 03:07:51 -05:00
Previously we were using a mixture of old and new, with our code generation using the plugin from the old github.com/golang/protobuf library but our callers using the modern google.golang.org/protobuf . We were also using pretty ancient version of protoc. This brings us up to the current latest releases and consistently using the new Go protobuf library. There have been some notable changes to these tools in the meantime: Previously the protoc-gen-go plugin handled grpc by having its own additional level of Go-specific "plugins" of which the gRPC codegen was an example. Now the protobuf generator and the gRPC generator are separate plugins handled directly by protoc, which means the command line arguments are a different shape and the gRPC stubs get generated in a separate file from the main protobuf messages, rather than all being in one .pb.go file as before.The results are otherwise similar, though. The grpc codegen now also defaults to requiring that implementations embed the generated "unimplemented" server, which is an implementation of each service where the methods just immediately return the "unimplemented" error. This is not super important for us because we maintain the generated interfaces and their implementations together in the same repository anyway, but adding the "unimplemented" server embeds was not a big change and so seems better to follow the prevailing convention. Using these new versions means that we could in principle now switch to using protobuf edition 2024 and the new "sealed" style for Go code generation, but this commit does not include any such changes and focuses only on getting things upgraded with as few other changes as possible. We can discuss using different codegen style later and deal with that in separate commits. Signed-off-by: Martin Atkins <mart@degeneration.co.uk>
239 lines
7.6 KiB
Go
239 lines
7.6 KiB
Go
// Copyright (c) The OpenTofu Authors
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
// Copyright (c) 2023 HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
// 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 = "32.1"
|
|
|
|
// 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 = "google.golang.org/protobuf/cmd/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=.", "--go_opt=paths=source_relative", "--go-grpc_out=.", "--go-grpc_opt=paths=source_relative", "./tfplugin5.proto"},
|
|
},
|
|
{
|
|
"tfplugin6 (provider wire protocol version 6)",
|
|
"internal/tfplugin6",
|
|
[]string{"--go_out=.", "--go_opt=paths=source_relative", "--go-grpc_out=.", "--go-grpc_opt=paths=source_relative", "./tfplugin6.proto"},
|
|
},
|
|
{
|
|
"tfplan (plan file serialization)",
|
|
"internal/plans/internal/planproto",
|
|
[]string{"--go_out=.", "--go_opt=paths=source_relative", "planfile.proto"},
|
|
},
|
|
}
|
|
|
|
func main() {
|
|
if len(os.Args) != 2 {
|
|
log.Fatal("Usage: go run github.com/opentofu/opentofu/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)
|
|
}
|
|
protocGenGoGrpcExec, 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(protocGenGoGrpcExec)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
|
|
// For all of our steps we'll run our localized protoc with our localized
|
|
// protoc-gen-go and protoc-gen-go-grpc.
|
|
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,
|
|
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 ""
|
|
}
|
|
}
|