go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/grpc/cmd/cproto/discovery.go (about)

     1  // Copyright 2016 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package main
    16  
    17  import (
    18  	"bytes"
    19  	"compress/gzip"
    20  	"fmt"
    21  	"go/build"
    22  	"os"
    23  	"os/exec"
    24  	"path/filepath"
    25  	"strings"
    26  	"text/template"
    27  
    28  	"google.golang.org/protobuf/types/descriptorpb"
    29  
    30  	"go.chromium.org/luci/common/errors"
    31  )
    32  
    33  const (
    34  	discoveryPackagePath = "go.chromium.org/luci/grpc/discovery"
    35  )
    36  
    37  // discoveryTmpl is template for generated Go discovery file.
    38  // The result of execution will also be passed through gofmt.
    39  var discoveryTmpl = template.Must(template.New("").Parse(strings.TrimSpace(`
    40  // Code generated by cproto. DO NOT EDIT.
    41  
    42  package {{.GoPkg}};
    43  
    44  {{if .ImportDiscovery}}
    45  import "go.chromium.org/luci/grpc/discovery"
    46  {{end}}
    47  import "google.golang.org/protobuf/types/descriptorpb"
    48  
    49  func init() {
    50  	{{if .ImportDiscovery}}discovery.{{end}}RegisterDescriptorSetCompressed(
    51  		[]string{
    52  			{{range .ServiceNames}}"{{.}}",{{end}}
    53  		},
    54  		{{.CompressedBytes}},
    55  	)
    56  }
    57  
    58  // FileDescriptorSet returns a descriptor set for this proto package, which
    59  // includes all defined services, and all transitive dependencies.
    60  //
    61  // Will not return nil.
    62  //
    63  // Do NOT modify the returned descriptor.
    64  func FileDescriptorSet() *descriptorpb.FileDescriptorSet {
    65  	// We just need ONE of the service names to look up the FileDescriptorSet.
    66  	ret, err := {{if .ImportDiscovery}}discovery.{{end}}GetDescriptorSet("{{index .ServiceNames 0 }}")
    67  	if err != nil {
    68  		panic(err)
    69  	}
    70  	return ret
    71  }
    72  `)))
    73  
    74  // genDiscoveryFile generates a Go discovery file that calls
    75  // discovery.RegisterDescriptorSetCompressed(serviceNames, compressedDescBytes)
    76  // in an init function.
    77  func genDiscoveryFile(output, goPkg string, desc []*descriptorpb.FileDescriptorProto, raw []byte) error {
    78  	var serviceNames []string
    79  	for _, f := range desc {
    80  		for _, s := range f.Service {
    81  			serviceNames = append(serviceNames, fmt.Sprintf("%s.%s", f.GetPackage(), s.GetName()))
    82  		}
    83  	}
    84  	if len(serviceNames) == 0 {
    85  		// no services, no discovery.
    86  		return nil
    87  	}
    88  
    89  	// Get the package name for "package ..." statement: it may be different from
    90  	// the directory name. Note that pkg.ImportPath almost always end up "." here
    91  	// when running in Go Modules mode, so we still need to keep `goPkg` argument.
    92  	pkg, err := build.ImportDir(filepath.Dir(output), 0)
    93  	if err != nil {
    94  		return errors.Annotate(err, "failed to figure out Go package name for %q", output).Err()
    95  	}
    96  
    97  	compressedDescBytes, err := compress(raw)
    98  	if err != nil {
    99  		return errors.Annotate(err, "failed to compress the descriptor set proto").Err()
   100  	}
   101  
   102  	var buf bytes.Buffer
   103  	err = discoveryTmpl.Execute(&buf, map[string]any{
   104  		"GoPkg":           pkg.Name,
   105  		"ImportDiscovery": goPkg != discoveryPackagePath,
   106  		"ServiceNames":    serviceNames,
   107  		"CompressedBytes": asByteArray(compressedDescBytes),
   108  	})
   109  	if err != nil {
   110  		return errors.Annotate(err, "failed to execute discovery file template").Err()
   111  	}
   112  
   113  	src := buf.Bytes()
   114  	formatted, err := gofmt(src)
   115  	if err != nil {
   116  		return errors.Annotate(err, "failed to gofmt the generated discovery file").Err()
   117  	}
   118  
   119  	return os.WriteFile(output, formatted, 0666)
   120  }
   121  
   122  // asByteArray converts blob to a valid []byte Go literal.
   123  func asByteArray(blob []byte) string {
   124  	out := &bytes.Buffer{}
   125  	fmt.Fprintf(out, "[]byte{")
   126  	for i := 0; i < len(blob); i++ {
   127  		fmt.Fprintf(out, "%d, ", blob[i])
   128  		if i%14 == 1 {
   129  			fmt.Fprintln(out)
   130  		}
   131  	}
   132  	fmt.Fprintf(out, "}")
   133  	return out.String()
   134  }
   135  
   136  // gofmt applies "gofmt -s" to the content of the buffer.
   137  func gofmt(blob []byte) ([]byte, error) {
   138  	out := bytes.Buffer{}
   139  	cmd := exec.Command("gofmt", "-s")
   140  	cmd.Stdin = bytes.NewReader(blob)
   141  	cmd.Stdout = &out
   142  	cmd.Stderr = os.Stderr
   143  	if err := cmd.Run(); err != nil {
   144  		return nil, err
   145  	}
   146  	return out.Bytes(), nil
   147  }
   148  
   149  // compress compresses data with gzip.
   150  func compress(data []byte) ([]byte, error) {
   151  	var buf bytes.Buffer
   152  	w := gzip.NewWriter(&buf)
   153  	if _, err := w.Write(data); err != nil {
   154  		return nil, err
   155  	}
   156  	if err := w.Close(); err != nil {
   157  		return nil, err
   158  	}
   159  	return buf.Bytes(), nil
   160  }