blob: 67138f19fbf5125db1477caaaa99feb7cf7558af [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
91type GoModule struct {
92 Dir string
93}
94
95type GoPackage struct {
96 Dir string
97 ImportPath string
98 Name string
99 Imports []string
100 GoFiles []string
101 TestGoFiles []string
102 TestImports []string
103
104 Module *GoModule
105}
106
107func (g GoPackage) IsCommand() bool {
108 return g.Name == "main"
109}
110
111func (g GoPackage) BpModuleType() string {
112 if g.IsCommand() {
113 return "blueprint_go_binary"
114 }
115 return "bootstrap_go_package"
116}
117
118func (g GoPackage) BpName() string {
119 if g.IsCommand() {
120 return filepath.Base(g.ImportPath)
121 }
122 return rewriteNames.GoToBp(g.ImportPath)
123}
124
125func (g GoPackage) BpDeps(deps []string) []string {
126 var ret []string
127 for _, d := range deps {
128 // Ignore stdlib dependencies
129 if !strings.Contains(d, ".") {
130 continue
131 }
132 if _, ok := excludeDeps[d]; ok {
133 continue
134 }
135 name := rewriteNames.GoToBp(d)
136 ret = append(ret, name)
137 }
138 return ret
139}
140
141func (g GoPackage) BpSrcs(srcs []string) []string {
142 var ret []string
143 prefix, err := filepath.Rel(g.Module.Dir, g.Dir)
144 if err != nil {
145 panic(err)
146 }
147 for _, f := range srcs {
148 f = filepath.Join(prefix, f)
149 if _, ok := excludeSrcs[f]; ok {
150 continue
151 }
152 ret = append(ret, f)
153 }
154 return ret
155}
156
157// AllImports combines Imports and TestImports, as blueprint does not differentiate these.
158func (g GoPackage) AllImports() []string {
159 imports := append([]string(nil), g.Imports...)
160 imports = append(imports, g.TestImports...)
161
162 if len(imports) == 0 {
163 return nil
164 }
165
166 // Sort and de-duplicate
167 sort.Strings(imports)
168 j := 0
169 for i := 1; i < len(imports); i++ {
170 if imports[i] == imports[j] {
171 continue
172 }
173 j++
174 imports[j] = imports[i]
175 }
176 return imports[:j+1]
177}
178
179var bpTemplate = template.Must(template.New("bp").Parse(`
180{{.BpModuleType}} {
181 name: "{{.BpName}}",
182 {{- if not .IsCommand}}
183 pkgPath: "{{.ImportPath}}",
184 {{- end}}
185 {{- if .BpDeps .AllImports}}
186 deps: [
187 {{- range .BpDeps .AllImports}}
188 "{{.}}",
189 {{- end}}
190 ],
191 {{- end}}
192 {{- if .BpSrcs .GoFiles}}
193 srcs: [
194 {{- range .BpSrcs .GoFiles}}
195 "{{.}}",
196 {{- end}}
197 ],
198 {{- end}}
199 {{- if .BpSrcs .TestGoFiles}}
200 testSrcs: [
201 {{- range .BpSrcs .TestGoFiles}}
202 "{{.}}",
203 {{- end}}
204 ],
205 {{- end}}
206}
207`))
208
209func rerunForRegen(filename string) error {
210 buf, err := ioutil.ReadFile(filename)
211 if err != nil {
212 return err
213 }
214
215 scanner := bufio.NewScanner(bytes.NewBuffer(buf))
216
217 // Skip the first line in the file
218 for i := 0; i < 2; i++ {
219 if !scanner.Scan() {
220 if scanner.Err() != nil {
221 return scanner.Err()
222 } else {
223 return fmt.Errorf("unexpected EOF")
224 }
225 }
226 }
227
228 // Extract the old args from the file
229 line := scanner.Text()
230 if strings.HasPrefix(line, "// go2bp ") {
231 line = strings.TrimPrefix(line, "// go2bp ")
232 } else {
233 return fmt.Errorf("unexpected second line: %q", line)
234 }
235 args := strings.Split(line, " ")
236 lastArg := args[len(args)-1]
237 args = args[:len(args)-1]
238
239 // Append all current command line args except -regen <file> to the ones from the file
240 for i := 1; i < len(os.Args); i++ {
241 if os.Args[i] == "-regen" || os.Args[i] == "--regen" {
242 i++
243 } else {
244 args = append(args, os.Args[i])
245 }
246 }
247 args = append(args, lastArg)
248
249 cmd := os.Args[0] + " " + strings.Join(args, " ")
250 // Re-exec pom2bp with the new arguments
251 output, err := exec.Command("/bin/sh", "-c", cmd).Output()
252 if exitErr, _ := err.(*exec.ExitError); exitErr != nil {
253 return fmt.Errorf("failed to run %s\n%s", cmd, string(exitErr.Stderr))
254 } else if err != nil {
255 return err
256 }
257
258 return ioutil.WriteFile(filename, output, 0666)
259}
260
261func main() {
262 flag.Usage = func() {
263 fmt.Fprintf(os.Stderr, `go2bp, a tool to create Android.bp files from go modules
264
265The tool will extract the necessary information from Go files to create an Android.bp that can
266compile them. This needs to be run from the same directory as the go.mod file.
267
268Usage: %s [--rewrite <pkg-prefix>=<replace>] [-exclude <package>] [-regen <file>]
269
270 -rewrite <pkg-prefix>=<replace>
271 rewrite can be used to specify mappings between go package paths and Android.bp modules. The -rewrite
272 option can be specified multiple times. When determining the Android.bp module for a given Go
273 package, mappings are searched in the order they were specified. The first <pkg-prefix> matching
274 either the package directly, or as the prefix '<pkg-prefix>/' will be replaced with <replace>.
275 After all replacements are finished, all '/' characters are replaced with '-'.
276 -exclude <package>
277 Don't put the specified go package in the Android.bp file.
278 -exclude-deps <package>
279 Don't put the specified go package in the dependency lists.
280 -exclude-srcs <module>
281 Don't put the specified source files in srcs or testSrcs lists.
282 -regen <file>
283 Read arguments from <file> and overwrite it.
284
285`, os.Args[0])
286 }
287
288 var regen string
289
290 flag.Var(&excludes, "exclude", "Exclude go package")
291 flag.Var(&excludeDeps, "exclude-dep", "Exclude go package from deps")
292 flag.Var(&excludeSrcs, "exclude-src", "Exclude go file from source lists")
293 flag.Var(&rewriteNames, "rewrite", "Regex(es) to rewrite artifact names")
294 flag.StringVar(&regen, "regen", "", "Rewrite specified file")
295 flag.Parse()
296
297 if regen != "" {
298 err := rerunForRegen(regen)
299 if err != nil {
300 fmt.Fprintln(os.Stderr, err)
301 os.Exit(1)
302 }
303 os.Exit(0)
304 }
305
306 if flag.NArg() != 0 {
307 fmt.Fprintf(os.Stderr, "Unused argument detected: %v\n", flag.Args())
308 os.Exit(1)
309 }
310
311 if _, err := os.Stat("go.mod"); err != nil {
312 fmt.Fprintln(os.Stderr, "go.mod file not found")
313 os.Exit(1)
314 }
315
316 cmd := exec.Command("go", "list", "-json", "./...")
317 output, err := cmd.Output()
318 if err != nil {
319 fmt.Fprintf(os.Stderr, "Failed to dump the go packages: %v\n", err)
320 os.Exit(1)
321 }
322 decoder := json.NewDecoder(bytes.NewReader(output))
323
324 pkgs := []GoPackage{}
325 for decoder.More() {
326 pkg := GoPackage{}
327 err := decoder.Decode(&pkg)
328 if err != nil {
329 fmt.Fprintf(os.Stderr, "Failed to parse json: %v\n", err)
330 os.Exit(1)
331 }
332 pkgs = append(pkgs, pkg)
333 }
334
335 buf := &bytes.Buffer{}
336
337 fmt.Fprintln(buf, "// Automatically generated with:")
338 fmt.Fprintln(buf, "// go2bp", strings.Join(proptools.ShellEscapeList(os.Args[1:]), " "))
339
340 for _, pkg := range pkgs {
341 if excludes[pkg.ImportPath] {
342 continue
343 }
344 if len(pkg.BpSrcs(pkg.GoFiles)) == 0 && len(pkg.BpSrcs(pkg.TestGoFiles)) == 0 {
345 continue
346 }
347 err := bpTemplate.Execute(buf, pkg)
348 if err != nil {
349 fmt.Fprintln(os.Stderr, "Error writing", pkg.Name, err)
350 os.Exit(1)
351 }
352 }
353
354 out, err := bpfix.Reformat(buf.String())
355 if err != nil {
356 fmt.Fprintln(os.Stderr, "Error formatting output", err)
357 os.Exit(1)
358 }
359
360 os.Stdout.WriteString(out)
361}