github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/tools/protobuf-compile/protobuf-compile.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  // protobuf-compile is a helper tool for running protoc against all of the
     5  // .proto files in this repository using specific versions of protoc and
     6  // protoc-gen-go, to ensure consistent results across all development
     7  // environments.
     8  //
     9  // protoc itself isn't a Go tool, so we need to use a custom strategy to
    10  // install and run it. The official releases are built only for a subset of
    11  // platforms that Go can potentially target, so this tool will fail if you
    12  // are using a platform other than the ones this wrapper tool has explicit
    13  // support for. In that case you'll need to either run this tool on a supported
    14  // platform or to recreate what it does manually using a protoc you've built
    15  // and installed yourself.
    16  package main
    17  
    18  import (
    19  	"fmt"
    20  	"log"
    21  	"os"
    22  	"os/exec"
    23  	"path/filepath"
    24  	"runtime"
    25  	"strings"
    26  
    27  	"github.com/hashicorp/go-getter"
    28  )
    29  
    30  const protocVersion = "3.15.6"
    31  
    32  // We also use protoc-gen-go and its grpc addon, but since these are Go tools
    33  // in Go modules our version selection for these comes from our top-level
    34  // go.mod, as with all other Go dependencies. If you want to switch to a newer
    35  // version of either tool then you can upgrade their modules in the usual way.
    36  const protocGenGoPackage = "github.com/golang/protobuf/protoc-gen-go"
    37  const protocGenGoGrpcPackage = "google.golang.org/grpc/cmd/protoc-gen-go-grpc"
    38  
    39  type protocStep struct {
    40  	DisplayName string
    41  	WorkDir     string
    42  	Args        []string
    43  }
    44  
    45  var protocSteps = []protocStep{
    46  	{
    47  		"tfplugin5 (provider wire protocol version 5)",
    48  		"internal/tfplugin5",
    49  		[]string{"--go_out=paths=source_relative,plugins=grpc:.", "./tfplugin5.proto"},
    50  	},
    51  	{
    52  		"tfplugin6 (provider wire protocol version 6)",
    53  		"internal/tfplugin6",
    54  		[]string{"--go_out=paths=source_relative,plugins=grpc:.", "./tfplugin6.proto"},
    55  	},
    56  	{
    57  		"tfplan (plan file serialization)",
    58  		"internal/plans/internal/planproto",
    59  		[]string{"--go_out=paths=source_relative:.", "planfile.proto"},
    60  	},
    61  	{
    62  		"cloudproto1 (cloud protocol version 1)",
    63  		"internal/cloudplugin/cloudproto1",
    64  		[]string{"--go_out=paths=source_relative,plugins=grpc:.", "cloudproto1.proto"},
    65  	},
    66  }
    67  
    68  func main() {
    69  	if len(os.Args) != 2 {
    70  		log.Fatal("Usage: go run github.com/terramate-io/tf/tools/protobuf-compile <basedir>")
    71  	}
    72  	baseDir := os.Args[1]
    73  	workDir := filepath.Join(baseDir, "tools/protobuf-compile/.workdir")
    74  
    75  	protocLocalDir := filepath.Join(workDir, "protoc-v"+protocVersion)
    76  	if _, err := os.Stat(protocLocalDir); os.IsNotExist(err) {
    77  		err := downloadProtoc(protocVersion, protocLocalDir)
    78  		if err != nil {
    79  			log.Fatal(err)
    80  		}
    81  	} else {
    82  		log.Printf("already have protoc v%s in %s", protocVersion, protocLocalDir)
    83  	}
    84  
    85  	protocExec := filepath.Join(protocLocalDir, "bin/protoc")
    86  
    87  	protocGenGoExec, err := buildProtocGenGo(workDir)
    88  	if err != nil {
    89  		log.Fatal(err)
    90  	}
    91  	_, err = buildProtocGenGoGrpc(workDir)
    92  	if err != nil {
    93  		log.Fatal(err)
    94  	}
    95  
    96  	protocExec, err = filepath.Abs(protocExec)
    97  	if err != nil {
    98  		log.Fatal(err)
    99  	}
   100  	protocGenGoExec, err = filepath.Abs(protocGenGoExec)
   101  	if err != nil {
   102  		log.Fatal(err)
   103  	}
   104  	protocGenGoGrpcExec, err := filepath.Abs(protocGenGoExec)
   105  	if err != nil {
   106  		log.Fatal(err)
   107  	}
   108  
   109  	// For all of our steps we'll run our localized protoc with our localized
   110  	// protoc-gen-go.
   111  	baseCmdLine := []string{protocExec, "--plugin=" + protocGenGoExec, "--plugin=" + protocGenGoGrpcExec}
   112  
   113  	for _, step := range protocSteps {
   114  		log.Printf("working on %s", step.DisplayName)
   115  
   116  		cmdLine := make([]string, 0, len(baseCmdLine)+len(step.Args))
   117  		cmdLine = append(cmdLine, baseCmdLine...)
   118  		cmdLine = append(cmdLine, step.Args...)
   119  
   120  		cmd := &exec.Cmd{
   121  			Path:   cmdLine[0],
   122  			Args:   cmdLine[1:],
   123  			Dir:    step.WorkDir,
   124  			Env:    os.Environ(),
   125  			Stdout: os.Stdout,
   126  			Stderr: os.Stderr,
   127  		}
   128  		err := cmd.Run()
   129  		if err != nil {
   130  			log.Printf("failed to compile: %s", err)
   131  		}
   132  	}
   133  
   134  }
   135  
   136  // downloadProtoc downloads the given version of protoc into the given
   137  // directory.
   138  func downloadProtoc(version string, localDir string) error {
   139  	protocURL, err := protocDownloadURL(version)
   140  	if err != nil {
   141  		return err
   142  	}
   143  
   144  	log.Printf("downloading and extracting protoc v%s from %s into %s", version, protocURL, localDir)
   145  
   146  	// For convenience, we'll be using go-getter to actually download this
   147  	// thing, so we need to turn the real URL into the funny sort of pseudo-URL
   148  	// thing that go-getter wants.
   149  	goGetterURL := protocURL + "?archive=zip"
   150  
   151  	err = getter.Get(localDir, goGetterURL)
   152  	if err != nil {
   153  		return fmt.Errorf("failed to download or extract the package: %s", err)
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  // buildProtocGenGo uses the Go toolchain to fetch the module containing
   160  // protoc-gen-go and then build an executable into the working directory.
   161  //
   162  // If successful, it returns the location of the executable.
   163  func buildProtocGenGo(workDir string) (string, error) {
   164  	exeSuffixRaw, err := exec.Command("go", "env", "GOEXE").Output()
   165  	if err != nil {
   166  		return "", fmt.Errorf("failed to determine executable suffix: %s", err)
   167  	}
   168  	exeSuffix := strings.TrimSpace(string(exeSuffixRaw))
   169  	exePath := filepath.Join(workDir, "protoc-gen-go"+exeSuffix)
   170  	log.Printf("building %s as %s", protocGenGoPackage, exePath)
   171  
   172  	cmd := exec.Command("go", "build", "-o", exePath, protocGenGoPackage)
   173  	cmd.Stdout = os.Stdout
   174  	cmd.Stderr = os.Stderr
   175  	err = cmd.Run()
   176  	if err != nil {
   177  		return "", fmt.Errorf("failed to build %s: %s", protocGenGoPackage, err)
   178  	}
   179  
   180  	return exePath, nil
   181  }
   182  
   183  // buildProtocGenGoGrpc uses the Go toolchain to fetch the module containing
   184  // protoc-gen-go-grpc and then build an executable into the working directory.
   185  //
   186  // If successful, it returns the location of the executable.
   187  func buildProtocGenGoGrpc(workDir string) (string, error) {
   188  	exeSuffixRaw, err := exec.Command("go", "env", "GOEXE").Output()
   189  	if err != nil {
   190  		return "", fmt.Errorf("failed to determine executable suffix: %s", err)
   191  	}
   192  	exeSuffix := strings.TrimSpace(string(exeSuffixRaw))
   193  	exePath := filepath.Join(workDir, "protoc-gen-go-grpc"+exeSuffix)
   194  	log.Printf("building %s as %s", protocGenGoGrpcPackage, exePath)
   195  
   196  	cmd := exec.Command("go", "build", "-o", exePath, protocGenGoGrpcPackage)
   197  	cmd.Stdout = os.Stdout
   198  	cmd.Stderr = os.Stderr
   199  	err = cmd.Run()
   200  	if err != nil {
   201  		return "", fmt.Errorf("failed to build %s: %s", protocGenGoGrpcPackage, err)
   202  	}
   203  
   204  	return exePath, nil
   205  }
   206  
   207  // protocDownloadURL returns the URL to try to download the protoc package
   208  // for the current platform or an error if there's no known URL for the
   209  // current platform.
   210  func protocDownloadURL(version string) (string, error) {
   211  	platformKW := protocPlatform()
   212  	if platformKW == "" {
   213  		return "", fmt.Errorf("don't know where to find protoc for %s on %s", runtime.GOOS, runtime.GOARCH)
   214  	}
   215  	return fmt.Sprintf("https://github.com/protocolbuffers/protobuf/releases/download/v%s/protoc-%s-%s.zip", protocVersion, protocVersion, platformKW), nil
   216  }
   217  
   218  // protocPlatform returns the package name substring for the current platform
   219  // in the naming convention used by official protoc packages, or an empty
   220  // string if we don't know how protoc packaging would describe current
   221  // platform.
   222  func protocPlatform() string {
   223  	goPlatform := runtime.GOOS + "_" + runtime.GOARCH
   224  
   225  	switch goPlatform {
   226  	case "linux_amd64":
   227  		return "linux-x86_64"
   228  	case "linux_arm64":
   229  		return "linux-aarch_64"
   230  	case "darwin_amd64":
   231  		return "osx-x86_64"
   232  	case "darwin_arm64":
   233  		// As of 3.15.6 there isn't yet an osx-aarch_64 package available,
   234  		// so we'll install the x86_64 version and hope Rosetta can handle it.
   235  		return "osx-x86_64"
   236  	case "windows_amd64":
   237  		return "win64" // for some reason the windows packages don't have a CPU architecture part
   238  	default:
   239  		return ""
   240  	}
   241  }