blob: fb5a7469d690c87f698e5d0aee7b152a78609fe0 [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", "./...")
Sasha Smundakd5fc4692022-04-13 18:45:57 -0700338 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())
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700344 os.Exit(1)
345 }
Sasha Smundakd5fc4692022-04-13 18:45:57 -0700346 decoder := json.NewDecoder(bytes.NewReader(stdoutb.Bytes()))
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700347
Dan Willemsen64e61e12021-06-30 01:30:46 -0700348 pkgs := []*GoPackage{}
349 pkgMap := map[string]*GoPackage{}
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700350 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 }
Dan Willemsen64e61e12021-06-30 01:30:46 -0700357 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
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700366 }
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
Dan Willemsen64e61e12021-06-30 01:30:46 -0700373 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
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700392 for _, pkg := range pkgs {
Dan Willemsen64e61e12021-06-30 01:30:46 -0700393 if !pkg.ExportToAndroid || excludes[pkg.ImportPath] {
Dan Willemsenbeb51ac2021-05-19 17:11:07 -0700394 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}