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(®en, "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