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 }