1// Copyright 2019 Google LLC
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
15package generator
16
17import (
18	"context"
19	"errors"
20	"fmt"
21	"io/ioutil"
22	"log"
23	"os"
24	"path/filepath"
25	"regexp"
26	"strconv"
27	"strings"
28
29	"golang.org/x/sync/errgroup"
30)
31
32var goPkgOptRe = regexp.MustCompile(`(?m)^option go_package = (.*);`)
33
34// regenGenproto regenerates the genproto repository.
35//
36// regenGenproto recursively walks through each directory named by given
37// arguments, looking for all .proto files. (Symlinks are not followed.) Any
38// proto file without `go_package` option or whose option does not begin with
39// the genproto prefix is ignored.
40//
41// If multiple roots contain files with the same name, eg "root1/path/to/file"
42// and "root2/path/to/file", only the first file is processed; the rest are
43// ignored.
44//
45// Protoc is executed on remaining files, one invocation per set of files
46// declaring the same Go package.
47func regenGenproto(ctx context.Context, genprotoDir, googleapisDir, protoDir string) error {
48	log.Println("regenerating genproto")
49
50	// The protoc include directory is actually the "src" directory of the repo.
51	protoDir += "/src"
52
53	// Create space to put generated .pb.go's.
54	c := command("mkdir", "generated")
55	c.Stdout = os.Stdout
56	c.Stderr = os.Stderr
57	c.Dir = genprotoDir
58	if err := c.Run(); err != nil {
59		return err
60	}
61
62	// Record and map all .proto files to their Go packages.
63	seenFiles := make(map[string]bool)
64	pkgFiles := make(map[string][]string)
65	for _, root := range []string{googleapisDir, protoDir} {
66		walkFn := func(path string, info os.FileInfo, err error) error {
67			if err != nil {
68				return err
69			}
70			if !info.Mode().IsRegular() || !strings.HasSuffix(path, ".proto") {
71				return nil
72			}
73
74			switch rel, err := filepath.Rel(root, path); {
75			case err != nil:
76				return err
77			case seenFiles[rel]:
78				return nil
79			default:
80				seenFiles[rel] = true
81			}
82
83			pkg, err := goPkg(path)
84			if err != nil {
85				return err
86			}
87			pkgFiles[pkg] = append(pkgFiles[pkg], path)
88			return nil
89		}
90		if err := filepath.Walk(root, walkFn); err != nil {
91			return err
92		}
93	}
94
95	if len(pkgFiles) == 0 {
96		return errors.New("couldn't find any pkgfiles")
97	}
98
99	// Run protoc on all protos of all packages.
100	grp, _ := errgroup.WithContext(ctx)
101	for pkg, fnames := range pkgFiles {
102		if !strings.HasPrefix(pkg, "google.golang.org/genproto") {
103			continue
104		}
105		pk := pkg
106		fn := fnames
107		grp.Go(func() error {
108			log.Println("running protoc on", pk)
109			return protoc(genprotoDir, googleapisDir, protoDir, fn)
110		})
111	}
112	if err := grp.Wait(); err != nil {
113		return err
114	}
115
116	// Move all generated content to their correct locations in the repository,
117	// because protoc puts it in a folder called generated/.
118
119	// The period at the end is analagous to * (copy everything in this dir).
120	c = command("cp", "-R", "generated/google.golang.org/genproto/.", ".")
121	c.Stdout = os.Stdout
122	c.Stderr = os.Stderr
123	c.Dir = genprotoDir
124	if err := c.Run(); err != nil {
125		return err
126	}
127
128	c = command("rm", "-rf", "generated")
129	c.Stdout = os.Stdout
130	c.Stderr = os.Stderr
131	c.Dir = genprotoDir
132	if err := c.Run(); err != nil {
133		return err
134	}
135
136	// Throw away changes to some special libs.
137	for _, lib := range []string{"googleapis/grafeas/v1", "googleapis/devtools/containeranalysis/v1"} {
138		c = command("git", "checkout", lib)
139		c.Stdout = os.Stdout
140		c.Stderr = os.Stderr
141		c.Dir = genprotoDir
142		if err := c.Run(); err != nil {
143			return err
144		}
145
146		c = command("git", "clean", "-df", lib)
147		c.Stdout = os.Stdout
148		c.Stderr = os.Stderr
149		c.Dir = genprotoDir
150		if err := c.Run(); err != nil {
151			return err
152		}
153	}
154
155	// Clean up and check it all compiles.
156	if err := vet(genprotoDir); err != nil {
157		return err
158	}
159
160	if err := build(genprotoDir); err != nil {
161		return err
162	}
163
164	return nil
165}
166
167// goPkg reports the import path declared in the given file's `go_package`
168// option. If the option is missing, goPkg returns empty string.
169func goPkg(fname string) (string, error) {
170	content, err := ioutil.ReadFile(fname)
171	if err != nil {
172		return "", err
173	}
174
175	var pkgName string
176	if match := goPkgOptRe.FindSubmatch(content); len(match) > 0 {
177		pn, err := strconv.Unquote(string(match[1]))
178		if err != nil {
179			return "", err
180		}
181		pkgName = pn
182	}
183	if p := strings.IndexRune(pkgName, ';'); p > 0 {
184		pkgName = pkgName[:p]
185	}
186	return pkgName, nil
187}
188
189// protoc executes the "protoc" command on files named in fnames, and outputs
190// to "<genprotoDir>/generated".
191func protoc(genprotoDir, googleapisDir, protoDir string, fnames []string) error {
192	args := []string{fmt.Sprintf("--go_out=plugins=grpc:%s/generated", genprotoDir), "-I", googleapisDir, "-I", protoDir}
193	args = append(args, fnames...)
194	c := command("protoc", args...)
195	c.Stdout = os.Stdout
196	c.Stderr = os.Stderr
197	c.Dir = genprotoDir
198	return c.Run()
199}
200