blob: 07cb5dfb0c5f3e59f1a6574bbd514eb1c494e68c [file] [log] [blame]
Dan Willemsenbeb51ac2021-05-19 17:11:07 -07001// 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
Dan Willemsen64e61e12021-06-30 01:30:46 -070091type 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
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700102type GoModule struct {
103 Dir string
104}
105
106type GoPackage struct {
Dan Willemsen64e61e12021-06-30 01:30:46 -0700107 ExportToAndroid bool
108
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700109 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() {
Dan Willemsen64e61e12021-06-30 01:30:46 -0700133 return rewriteNames.GoToBp(filepath.Base(g.ImportPath))
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700134 }
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.
Dan Willemsen64e61e12021-06-30 01:30:46 -0700295 -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.
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700299 -regen <file>
300 Read arguments from <file> and overwrite it.
301
302`, os.Args[0])
303 }
304
305 var regen string
Dan Willemsen64e61e12021-06-30 01:30:46 -0700306 var skipTests bool
307 limit := StringList{}
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700308
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")
Dan Willemsen64e61e12021-06-30 01:30:46 -0700313 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")
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700315 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 output, err := cmd.Output()
339 if err != nil {
340 fmt.Fprintf(os.Stderr, "Failed to dump the go packages: %v\n", err)
341 os.Exit(1)
342 }
343 decoder := json.NewDecoder(bytes.NewReader(output))
344
Dan Willemsen64e61e12021-06-30 01:30:46 -0700345 pkgs := []*GoPackage{}
346 pkgMap := map[string]*GoPackage{}
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700347 for decoder.More() {
348 pkg := GoPackage{}
349 err := decoder.Decode(&pkg)
350 if err != nil {
351 fmt.Fprintf(os.Stderr, "Failed to parse json: %v\n", err)
352 os.Exit(1)
353 }
Dan Willemsen64e61e12021-06-30 01:30:46 -0700354 if len(limit) == 0 {
355 pkg.ExportToAndroid = true
356 }
357 if skipTests {
358 pkg.TestGoFiles = nil
359 pkg.TestImports = nil
360 }
361 pkgs = append(pkgs, &pkg)
362 pkgMap[pkg.ImportPath] = &pkg
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700363 }
364
365 buf := &bytes.Buffer{}
366
367 fmt.Fprintln(buf, "// Automatically generated with:")
368 fmt.Fprintln(buf, "// go2bp", strings.Join(proptools.ShellEscapeList(os.Args[1:]), " "))
369
Dan Willemsen64e61e12021-06-30 01:30:46 -0700370 var mark func(string)
371 mark = func(pkgName string) {
372 if excludes[pkgName] {
373 return
374 }
375 if pkg, ok := pkgMap[pkgName]; ok && !pkg.ExportToAndroid {
376 pkg.ExportToAndroid = true
377 for _, dep := range pkg.AllImports() {
378 if !excludeDeps[dep] {
379 mark(dep)
380 }
381 }
382 }
383 }
384
385 for _, pkgName := range limit {
386 mark(pkgName)
387 }
388
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700389 for _, pkg := range pkgs {
Dan Willemsen64e61e12021-06-30 01:30:46 -0700390 if !pkg.ExportToAndroid || excludes[pkg.ImportPath] {
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700391 continue
392 }
393 if len(pkg.BpSrcs(pkg.GoFiles)) == 0 && len(pkg.BpSrcs(pkg.TestGoFiles)) == 0 {
394 continue
395 }
396 err := bpTemplate.Execute(buf, pkg)
397 if err != nil {
398 fmt.Fprintln(os.Stderr, "Error writing", pkg.Name, err)
399 os.Exit(1)
400 }
401 }
402
403 out, err := bpfix.Reformat(buf.String())
404 if err != nil {
405 fmt.Fprintln(os.Stderr, "Error formatting output", err)
406 os.Exit(1)
407 }
408
409 os.Stdout.WriteString(out)
410}