xref: /aosp_15_r20/build/soong/cmd/go2bp/go2bp.go (revision 333d2b3687b3a337dbcca9d65000bca186795e39)
1// Copyright 2021 Google Inc. All rights reserved.
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 main
16
17import (
18	"bufio"
19	"bytes"
20	"encoding/json"
21	"flag"
22	"fmt"
23	"io/ioutil"
24	"os"
25	"os/exec"
26	"path/filepath"
27	"sort"
28	"strings"
29	"text/template"
30
31	"github.com/google/blueprint/proptools"
32
33	"android/soong/bpfix/bpfix"
34)
35
36type RewriteNames []RewriteName
37type RewriteName struct {
38	prefix string
39	repl   string
40}
41
42func (r *RewriteNames) String() string {
43	return ""
44}
45
46func (r *RewriteNames) Set(v string) error {
47	split := strings.SplitN(v, "=", 2)
48	if len(split) != 2 {
49		return fmt.Errorf("Must be in the form of <prefix>=<replace>")
50	}
51	*r = append(*r, RewriteName{
52		prefix: split[0],
53		repl:   split[1],
54	})
55	return nil
56}
57
58func (r *RewriteNames) GoToBp(name string) string {
59	ret := name
60	for _, r := range *r {
61		prefix := r.prefix
62		if name == prefix {
63			ret = r.repl
64			break
65		}
66		prefix += "/"
67		if strings.HasPrefix(name, prefix) {
68			ret = r.repl + "-" + strings.TrimPrefix(name, prefix)
69		}
70	}
71	return strings.ReplaceAll(ret, "/", "-")
72}
73
74var rewriteNames = RewriteNames{}
75
76type Exclude map[string]bool
77
78func (e Exclude) String() string {
79	return ""
80}
81
82func (e Exclude) Set(v string) error {
83	e[v] = true
84	return nil
85}
86
87var excludes = make(Exclude)
88var excludeDeps = make(Exclude)
89var excludeSrcs = make(Exclude)
90
91type StringList []string
92
93func (l *StringList) String() string {
94	return strings.Join(*l, " ")
95}
96
97func (l *StringList) Set(v string) error {
98	*l = append(*l, strings.Fields(v)...)
99	return nil
100}
101
102type GoModule struct {
103	Dir string
104}
105
106type GoPackage struct {
107	ExportToAndroid bool
108
109	Dir         string
110	ImportPath  string
111	Name        string
112	Imports     []string
113	GoFiles     []string
114	TestGoFiles []string
115	TestImports []string
116
117	Module *GoModule
118}
119
120func (g GoPackage) IsCommand() bool {
121	return g.Name == "main"
122}
123
124func (g GoPackage) BpModuleType() string {
125	if g.IsCommand() {
126		return "blueprint_go_binary"
127	}
128	return "bootstrap_go_package"
129}
130
131func (g GoPackage) BpName() string {
132	if g.IsCommand() {
133		return rewriteNames.GoToBp(filepath.Base(g.ImportPath))
134	}
135	return rewriteNames.GoToBp(g.ImportPath)
136}
137
138func (g GoPackage) BpDeps(deps []string) []string {
139	var ret []string
140	for _, d := range deps {
141		// Ignore stdlib dependencies
142		if !strings.Contains(d, ".") {
143			continue
144		}
145		if _, ok := excludeDeps[d]; ok {
146			continue
147		}
148		name := rewriteNames.GoToBp(d)
149		ret = append(ret, name)
150	}
151	return ret
152}
153
154func (g GoPackage) BpSrcs(srcs []string) []string {
155	var ret []string
156	prefix, err := filepath.Rel(g.Module.Dir, g.Dir)
157	if err != nil {
158		panic(err)
159	}
160	for _, f := range srcs {
161		f = filepath.Join(prefix, f)
162		if _, ok := excludeSrcs[f]; ok {
163			continue
164		}
165		ret = append(ret, f)
166	}
167	return ret
168}
169
170// AllImports combines Imports and TestImports, as blueprint does not differentiate these.
171func (g GoPackage) AllImports() []string {
172	imports := append([]string(nil), g.Imports...)
173	imports = append(imports, g.TestImports...)
174
175	if len(imports) == 0 {
176		return nil
177	}
178
179	// Sort and de-duplicate
180	sort.Strings(imports)
181	j := 0
182	for i := 1; i < len(imports); i++ {
183		if imports[i] == imports[j] {
184			continue
185		}
186		j++
187		imports[j] = imports[i]
188	}
189	return imports[:j+1]
190}
191
192var bpTemplate = template.Must(template.New("bp").Parse(`
193{{.BpModuleType}} {
194    name: "{{.BpName}}",
195    {{- if not .IsCommand}}
196    pkgPath: "{{.ImportPath}}",
197    {{- end}}
198    {{- if .BpDeps .AllImports}}
199    deps: [
200        {{- range .BpDeps .AllImports}}
201        "{{.}}",
202        {{- end}}
203    ],
204    {{- end}}
205    {{- if .BpSrcs .GoFiles}}
206    srcs: [
207        {{- range .BpSrcs .GoFiles}}
208        "{{.}}",
209        {{- end}}
210    ],
211    {{- end}}
212    {{- if .BpSrcs .TestGoFiles}}
213    testSrcs: [
214    	{{- range .BpSrcs .TestGoFiles}}
215        "{{.}}",
216       {{- end}}
217    ],
218    {{- end}}
219}
220`))
221
222func rerunForRegen(filename string) error {
223	buf, err := ioutil.ReadFile(filename)
224	if err != nil {
225		return err
226	}
227
228	scanner := bufio.NewScanner(bytes.NewBuffer(buf))
229
230	// Skip the first line in the file
231	for i := 0; i < 2; i++ {
232		if !scanner.Scan() {
233			if scanner.Err() != nil {
234				return scanner.Err()
235			} else {
236				return fmt.Errorf("unexpected EOF")
237			}
238		}
239	}
240
241	// Extract the old args from the file
242	line := scanner.Text()
243	if strings.HasPrefix(line, "// go2bp ") {
244		line = strings.TrimPrefix(line, "// go2bp ")
245	} else {
246		return fmt.Errorf("unexpected second line: %q", line)
247	}
248	args := strings.Split(line, " ")
249	lastArg := args[len(args)-1]
250	args = args[:len(args)-1]
251
252	// Append all current command line args except -regen <file> to the ones from the file
253	for i := 1; i < len(os.Args); i++ {
254		if os.Args[i] == "-regen" || os.Args[i] == "--regen" {
255			i++
256		} else {
257			args = append(args, os.Args[i])
258		}
259	}
260	args = append(args, lastArg)
261
262	cmd := os.Args[0] + " " + strings.Join(args, " ")
263	// Re-exec pom2bp with the new arguments
264	output, err := exec.Command("/bin/sh", "-c", cmd).Output()
265	if exitErr, _ := err.(*exec.ExitError); exitErr != nil {
266		return fmt.Errorf("failed to run %s\n%s", cmd, string(exitErr.Stderr))
267	} else if err != nil {
268		return err
269	}
270
271	return ioutil.WriteFile(filename, output, 0666)
272}
273
274func main() {
275	flag.Usage = func() {
276		fmt.Fprintf(os.Stderr, `go2bp, a tool to create Android.bp files from go modules
277
278The tool will extract the necessary information from Go files to create an Android.bp that can
279compile them. This needs to be run from the same directory as the go.mod file.
280
281Usage: %s [--rewrite <pkg-prefix>=<replace>] [-exclude <package>] [-regen <file>]
282
283  -rewrite <pkg-prefix>=<replace>
284     rewrite can be used to specify mappings between go package paths and Android.bp modules. The -rewrite
285     option can be specified multiple times. When determining the Android.bp module for a given Go
286     package, mappings are searched in the order they were specified. The first <pkg-prefix> matching
287     either the package directly, or as the prefix '<pkg-prefix>/' will be replaced with <replace>.
288     After all replacements are finished, all '/' characters are replaced with '-'.
289  -exclude <package>
290     Don't put the specified go package in the Android.bp file.
291  -exclude-deps <package>
292     Don't put the specified go package in the dependency lists.
293  -exclude-srcs <module>
294     Don't put the specified source files in srcs or testSrcs lists.
295  -limit <package>
296     If set, limit the output to the specified packages and their dependencies.
297  -skip-tests
298     If passed, don't write out any test srcs or dependencies to the Android.bp output.
299  -regen <file>
300     Read arguments from <file> and overwrite it.
301
302`, os.Args[0])
303	}
304
305	var regen string
306	var skipTests bool
307	limit := StringList{}
308
309	flag.Var(&excludes, "exclude", "Exclude go package")
310	flag.Var(&excludeDeps, "exclude-dep", "Exclude go package from deps")
311	flag.Var(&excludeSrcs, "exclude-src", "Exclude go file from source lists")
312	flag.Var(&rewriteNames, "rewrite", "Regex(es) to rewrite artifact names")
313	flag.Var(&limit, "limit", "If set, only includes the dependencies of the listed packages")
314	flag.BoolVar(&skipTests, "skip-tests", false, "Whether to skip test sources")
315	flag.StringVar(&regen, "regen", "", "Rewrite specified file")
316	flag.Parse()
317
318	if regen != "" {
319		err := rerunForRegen(regen)
320		if err != nil {
321			fmt.Fprintln(os.Stderr, err)
322			os.Exit(1)
323		}
324		os.Exit(0)
325	}
326
327	if flag.NArg() != 0 {
328		fmt.Fprintf(os.Stderr, "Unused argument detected: %v\n", flag.Args())
329		os.Exit(1)
330	}
331
332	if _, err := os.Stat("go.mod"); err != nil {
333		fmt.Fprintln(os.Stderr, "go.mod file not found")
334		os.Exit(1)
335	}
336
337	cmd := exec.Command("go", "list", "-json", "./...")
338	var stdoutb, stderrb bytes.Buffer
339	cmd.Stdout = &stdoutb
340	cmd.Stderr = &stderrb
341	if err := cmd.Run(); err != nil {
342		fmt.Fprintf(os.Stderr, "Running %q to dump the Go packages failed: %v, stderr:\n%s\n",
343			cmd.String(), err, stderrb.Bytes())
344		os.Exit(1)
345	}
346	decoder := json.NewDecoder(bytes.NewReader(stdoutb.Bytes()))
347
348	pkgs := []*GoPackage{}
349	pkgMap := map[string]*GoPackage{}
350	for decoder.More() {
351		pkg := GoPackage{}
352		err := decoder.Decode(&pkg)
353		if err != nil {
354			fmt.Fprintf(os.Stderr, "Failed to parse json: %v\n", err)
355			os.Exit(1)
356		}
357		if len(limit) == 0 {
358			pkg.ExportToAndroid = true
359		}
360		if skipTests {
361			pkg.TestGoFiles = nil
362			pkg.TestImports = nil
363		}
364		pkgs = append(pkgs, &pkg)
365		pkgMap[pkg.ImportPath] = &pkg
366	}
367
368	buf := &bytes.Buffer{}
369
370	fmt.Fprintln(buf, "// Automatically generated with:")
371	fmt.Fprintln(buf, "// go2bp", strings.Join(proptools.ShellEscapeList(os.Args[1:]), " "))
372
373	var mark func(string)
374	mark = func(pkgName string) {
375		if excludes[pkgName] {
376			return
377		}
378		if pkg, ok := pkgMap[pkgName]; ok && !pkg.ExportToAndroid {
379			pkg.ExportToAndroid = true
380			for _, dep := range pkg.AllImports() {
381				if !excludeDeps[dep] {
382					mark(dep)
383				}
384			}
385		}
386	}
387
388	for _, pkgName := range limit {
389		mark(pkgName)
390	}
391
392	for _, pkg := range pkgs {
393		if !pkg.ExportToAndroid || excludes[pkg.ImportPath] {
394			continue
395		}
396		if len(pkg.BpSrcs(pkg.GoFiles)) == 0 && len(pkg.BpSrcs(pkg.TestGoFiles)) == 0 {
397			continue
398		}
399		err := bpTemplate.Execute(buf, pkg)
400		if err != nil {
401			fmt.Fprintln(os.Stderr, "Error writing", pkg.Name, err)
402			os.Exit(1)
403		}
404	}
405
406	out, err := bpfix.Reformat(buf.String())
407	if err != nil {
408		fmt.Fprintln(os.Stderr, "Error formatting output", err)
409		os.Exit(1)
410	}
411
412	os.Stdout.WriteString(out)
413}
414