| // Copyright 2021 Google Inc. All rights reserved. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package main |
| |
| import ( |
| "bufio" |
| "bytes" |
| "encoding/json" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "sort" |
| "strings" |
| "text/template" |
| |
| "github.com/google/blueprint/proptools" |
| |
| "android/soong/bpfix/bpfix" |
| ) |
| |
| type RewriteNames []RewriteName |
| type RewriteName struct { |
| prefix string |
| repl string |
| } |
| |
| func (r *RewriteNames) String() string { |
| return "" |
| } |
| |
| func (r *RewriteNames) Set(v string) error { |
| split := strings.SplitN(v, "=", 2) |
| if len(split) != 2 { |
| return fmt.Errorf("Must be in the form of <prefix>=<replace>") |
| } |
| *r = append(*r, RewriteName{ |
| prefix: split[0], |
| repl: split[1], |
| }) |
| return nil |
| } |
| |
| func (r *RewriteNames) GoToBp(name string) string { |
| ret := name |
| for _, r := range *r { |
| prefix := r.prefix |
| if name == prefix { |
| ret = r.repl |
| break |
| } |
| prefix += "/" |
| if strings.HasPrefix(name, prefix) { |
| ret = r.repl + "-" + strings.TrimPrefix(name, prefix) |
| } |
| } |
| return strings.ReplaceAll(ret, "/", "-") |
| } |
| |
| var rewriteNames = RewriteNames{} |
| |
| type Exclude map[string]bool |
| |
| func (e Exclude) String() string { |
| return "" |
| } |
| |
| func (e Exclude) Set(v string) error { |
| e[v] = true |
| return nil |
| } |
| |
| var excludes = make(Exclude) |
| var excludeDeps = make(Exclude) |
| var excludeSrcs = make(Exclude) |
| |
| type StringList []string |
| |
| func (l *StringList) String() string { |
| return strings.Join(*l, " ") |
| } |
| |
| func (l *StringList) Set(v string) error { |
| *l = append(*l, strings.Fields(v)...) |
| return nil |
| } |
| |
| type GoModule struct { |
| Dir string |
| } |
| |
| type GoPackage struct { |
| ExportToAndroid bool |
| |
| Dir string |
| ImportPath string |
| Name string |
| Imports []string |
| GoFiles []string |
| TestGoFiles []string |
| TestImports []string |
| |
| Module *GoModule |
| } |
| |
| func (g GoPackage) IsCommand() bool { |
| return g.Name == "main" |
| } |
| |
| func (g GoPackage) BpModuleType() string { |
| if g.IsCommand() { |
| return "blueprint_go_binary" |
| } |
| return "bootstrap_go_package" |
| } |
| |
| func (g GoPackage) BpName() string { |
| if g.IsCommand() { |
| return rewriteNames.GoToBp(filepath.Base(g.ImportPath)) |
| } |
| return rewriteNames.GoToBp(g.ImportPath) |
| } |
| |
| func (g GoPackage) BpDeps(deps []string) []string { |
| var ret []string |
| for _, d := range deps { |
| // Ignore stdlib dependencies |
| if !strings.Contains(d, ".") { |
| continue |
| } |
| if _, ok := excludeDeps[d]; ok { |
| continue |
| } |
| name := rewriteNames.GoToBp(d) |
| ret = append(ret, name) |
| } |
| return ret |
| } |
| |
| func (g GoPackage) BpSrcs(srcs []string) []string { |
| var ret []string |
| prefix, err := filepath.Rel(g.Module.Dir, g.Dir) |
| if err != nil { |
| panic(err) |
| } |
| for _, f := range srcs { |
| f = filepath.Join(prefix, f) |
| if _, ok := excludeSrcs[f]; ok { |
| continue |
| } |
| ret = append(ret, f) |
| } |
| return ret |
| } |
| |
| // AllImports combines Imports and TestImports, as blueprint does not differentiate these. |
| func (g GoPackage) AllImports() []string { |
| imports := append([]string(nil), g.Imports...) |
| imports = append(imports, g.TestImports...) |
| |
| if len(imports) == 0 { |
| return nil |
| } |
| |
| // Sort and de-duplicate |
| sort.Strings(imports) |
| j := 0 |
| for i := 1; i < len(imports); i++ { |
| if imports[i] == imports[j] { |
| continue |
| } |
| j++ |
| imports[j] = imports[i] |
| } |
| return imports[:j+1] |
| } |
| |
| var bpTemplate = template.Must(template.New("bp").Parse(` |
| {{.BpModuleType}} { |
| name: "{{.BpName}}", |
| {{- if not .IsCommand}} |
| pkgPath: "{{.ImportPath}}", |
| {{- end}} |
| {{- if .BpDeps .AllImports}} |
| deps: [ |
| {{- range .BpDeps .AllImports}} |
| "{{.}}", |
| {{- end}} |
| ], |
| {{- end}} |
| {{- if .BpSrcs .GoFiles}} |
| srcs: [ |
| {{- range .BpSrcs .GoFiles}} |
| "{{.}}", |
| {{- end}} |
| ], |
| {{- end}} |
| {{- if .BpSrcs .TestGoFiles}} |
| testSrcs: [ |
| {{- range .BpSrcs .TestGoFiles}} |
| "{{.}}", |
| {{- end}} |
| ], |
| {{- end}} |
| } |
| `)) |
| |
| func rerunForRegen(filename string) error { |
| buf, err := ioutil.ReadFile(filename) |
| if err != nil { |
| return err |
| } |
| |
| scanner := bufio.NewScanner(bytes.NewBuffer(buf)) |
| |
| // Skip the first line in the file |
| for i := 0; i < 2; i++ { |
| if !scanner.Scan() { |
| if scanner.Err() != nil { |
| return scanner.Err() |
| } else { |
| return fmt.Errorf("unexpected EOF") |
| } |
| } |
| } |
| |
| // Extract the old args from the file |
| line := scanner.Text() |
| if strings.HasPrefix(line, "// go2bp ") { |
| line = strings.TrimPrefix(line, "// go2bp ") |
| } else { |
| return fmt.Errorf("unexpected second line: %q", line) |
| } |
| args := strings.Split(line, " ") |
| lastArg := args[len(args)-1] |
| args = args[:len(args)-1] |
| |
| // Append all current command line args except -regen <file> to the ones from the file |
| for i := 1; i < len(os.Args); i++ { |
| if os.Args[i] == "-regen" || os.Args[i] == "--regen" { |
| i++ |
| } else { |
| args = append(args, os.Args[i]) |
| } |
| } |
| args = append(args, lastArg) |
| |
| cmd := os.Args[0] + " " + strings.Join(args, " ") |
| // Re-exec pom2bp with the new arguments |
| output, err := exec.Command("/bin/sh", "-c", cmd).Output() |
| if exitErr, _ := err.(*exec.ExitError); exitErr != nil { |
| return fmt.Errorf("failed to run %s\n%s", cmd, string(exitErr.Stderr)) |
| } else if err != nil { |
| return err |
| } |
| |
| return ioutil.WriteFile(filename, output, 0666) |
| } |
| |
| func main() { |
| flag.Usage = func() { |
| fmt.Fprintf(os.Stderr, `go2bp, a tool to create Android.bp files from go modules |
| |
| The tool will extract the necessary information from Go files to create an Android.bp that can |
| compile them. This needs to be run from the same directory as the go.mod file. |
| |
| Usage: %s [--rewrite <pkg-prefix>=<replace>] [-exclude <package>] [-regen <file>] |
| |
| -rewrite <pkg-prefix>=<replace> |
| rewrite can be used to specify mappings between go package paths and Android.bp modules. The -rewrite |
| option can be specified multiple times. When determining the Android.bp module for a given Go |
| package, mappings are searched in the order they were specified. The first <pkg-prefix> matching |
| either the package directly, or as the prefix '<pkg-prefix>/' will be replaced with <replace>. |
| After all replacements are finished, all '/' characters are replaced with '-'. |
| -exclude <package> |
| Don't put the specified go package in the Android.bp file. |
| -exclude-deps <package> |
| Don't put the specified go package in the dependency lists. |
| -exclude-srcs <module> |
| Don't put the specified source files in srcs or testSrcs lists. |
| -limit <package> |
| If set, limit the output to the specified packages and their dependencies. |
| -skip-tests |
| If passed, don't write out any test srcs or dependencies to the Android.bp output. |
| -regen <file> |
| Read arguments from <file> and overwrite it. |
| |
| `, os.Args[0]) |
| } |
| |
| var regen string |
| var skipTests bool |
| limit := StringList{} |
| |
| flag.Var(&excludes, "exclude", "Exclude go package") |
| flag.Var(&excludeDeps, "exclude-dep", "Exclude go package from deps") |
| flag.Var(&excludeSrcs, "exclude-src", "Exclude go file from source lists") |
| flag.Var(&rewriteNames, "rewrite", "Regex(es) to rewrite artifact names") |
| flag.Var(&limit, "limit", "If set, only includes the dependencies of the listed packages") |
| flag.BoolVar(&skipTests, "skip-tests", false, "Whether to skip test sources") |
| flag.StringVar(®en, "regen", "", "Rewrite specified file") |
| flag.Parse() |
| |
| if regen != "" { |
| err := rerunForRegen(regen) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, err) |
| os.Exit(1) |
| } |
| os.Exit(0) |
| } |
| |
| if flag.NArg() != 0 { |
| fmt.Fprintf(os.Stderr, "Unused argument detected: %v\n", flag.Args()) |
| os.Exit(1) |
| } |
| |
| if _, err := os.Stat("go.mod"); err != nil { |
| fmt.Fprintln(os.Stderr, "go.mod file not found") |
| os.Exit(1) |
| } |
| |
| cmd := exec.Command("go", "list", "-json", "./...") |
| var stdoutb, stderrb bytes.Buffer |
| cmd.Stdout = &stdoutb |
| cmd.Stderr = &stderrb |
| if err := cmd.Run(); err != nil { |
| fmt.Fprintf(os.Stderr, "Running %q to dump the Go packages failed: %v, stderr:\n%s\n", |
| cmd.String(), err, stderrb.Bytes()) |
| os.Exit(1) |
| } |
| decoder := json.NewDecoder(bytes.NewReader(stdoutb.Bytes())) |
| |
| pkgs := []*GoPackage{} |
| pkgMap := map[string]*GoPackage{} |
| for decoder.More() { |
| pkg := GoPackage{} |
| err := decoder.Decode(&pkg) |
| if err != nil { |
| fmt.Fprintf(os.Stderr, "Failed to parse json: %v\n", err) |
| os.Exit(1) |
| } |
| if len(limit) == 0 { |
| pkg.ExportToAndroid = true |
| } |
| if skipTests { |
| pkg.TestGoFiles = nil |
| pkg.TestImports = nil |
| } |
| pkgs = append(pkgs, &pkg) |
| pkgMap[pkg.ImportPath] = &pkg |
| } |
| |
| buf := &bytes.Buffer{} |
| |
| fmt.Fprintln(buf, "// Automatically generated with:") |
| fmt.Fprintln(buf, "// go2bp", strings.Join(proptools.ShellEscapeList(os.Args[1:]), " ")) |
| |
| var mark func(string) |
| mark = func(pkgName string) { |
| if excludes[pkgName] { |
| return |
| } |
| if pkg, ok := pkgMap[pkgName]; ok && !pkg.ExportToAndroid { |
| pkg.ExportToAndroid = true |
| for _, dep := range pkg.AllImports() { |
| if !excludeDeps[dep] { |
| mark(dep) |
| } |
| } |
| } |
| } |
| |
| for _, pkgName := range limit { |
| mark(pkgName) |
| } |
| |
| for _, pkg := range pkgs { |
| if !pkg.ExportToAndroid || excludes[pkg.ImportPath] { |
| continue |
| } |
| if len(pkg.BpSrcs(pkg.GoFiles)) == 0 && len(pkg.BpSrcs(pkg.TestGoFiles)) == 0 { |
| continue |
| } |
| err := bpTemplate.Execute(buf, pkg) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, "Error writing", pkg.Name, err) |
| os.Exit(1) |
| } |
| } |
| |
| out, err := bpfix.Reformat(buf.String()) |
| if err != nil { |
| fmt.Fprintln(os.Stderr, "Error formatting output", err) |
| os.Exit(1) |
| } |
| |
| os.Stdout.WriteString(out) |
| } |