1// Copyright 2017 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	"archive/zip"
19	"bufio"
20	"bytes"
21	"encoding/xml"
22	"flag"
23	"fmt"
24	"io/ioutil"
25	"os"
26	"os/exec"
27	"path/filepath"
28	"regexp"
29	"sort"
30	"strings"
31	"text/template"
32
33	"github.com/google/blueprint/proptools"
34
35	"android/soong/bpfix/bpfix"
36)
37
38type RewriteNames []RewriteName
39type RewriteName struct {
40	regexp *regexp.Regexp
41	repl   string
42}
43
44func (r *RewriteNames) String() string {
45	return ""
46}
47
48func (r *RewriteNames) Set(v string) error {
49	split := strings.SplitN(v, "=", 2)
50	if len(split) != 2 {
51		return fmt.Errorf("Must be in the form of <regex>=<replace>")
52	}
53	regex, err := regexp.Compile(split[0])
54	if err != nil {
55		return nil
56	}
57	*r = append(*r, RewriteName{
58		regexp: regex,
59		repl:   split[1],
60	})
61	return nil
62}
63
64func (r *RewriteNames) MavenToBp(groupId string, artifactId string) string {
65	for _, r := range *r {
66		if r.regexp.MatchString(groupId + ":" + artifactId) {
67			return r.regexp.ReplaceAllString(groupId+":"+artifactId, r.repl)
68		} else if r.regexp.MatchString(artifactId) {
69			return r.regexp.ReplaceAllString(artifactId, r.repl)
70		}
71	}
72	return artifactId
73}
74
75var rewriteNames = RewriteNames{}
76
77type ExtraDeps map[string][]string
78
79func (d ExtraDeps) String() string {
80	return ""
81}
82
83func (d ExtraDeps) Set(v string) error {
84	split := strings.SplitN(v, "=", 2)
85	if len(split) != 2 {
86		return fmt.Errorf("Must be in the form of <module>=<module>[,<module>]")
87	}
88	d[split[0]] = strings.Split(split[1], ",")
89	return nil
90}
91
92var extraStaticLibs = make(ExtraDeps)
93
94var extraLibs = make(ExtraDeps)
95
96type Exclude map[string]bool
97
98func (e Exclude) String() string {
99	return ""
100}
101
102func (e Exclude) Set(v string) error {
103	e[v] = true
104	return nil
105}
106
107var excludes = make(Exclude)
108
109type HostModuleNames map[string]bool
110
111func (n HostModuleNames) IsHostModule(groupId string, artifactId string) bool {
112	_, found := n[groupId+":"+artifactId]
113	return found
114}
115
116func (n HostModuleNames) String() string {
117	return ""
118}
119
120func (n HostModuleNames) Set(v string) error {
121	n[v] = true
122	return nil
123}
124
125var hostModuleNames = HostModuleNames{}
126
127type HostAndDeviceModuleNames map[string]bool
128
129func (n HostAndDeviceModuleNames) IsHostAndDeviceModule(groupId string, artifactId string) bool {
130	_, found := n[groupId+":"+artifactId]
131
132	return found
133}
134
135func (n HostAndDeviceModuleNames) String() string {
136	return ""
137}
138
139func (n HostAndDeviceModuleNames) Set(v string) error {
140	n[v] = true
141	return nil
142}
143
144var hostAndDeviceModuleNames = HostAndDeviceModuleNames{}
145
146var sdkVersion string
147var defaultMinSdkVersion string
148var useVersion string
149var staticDeps bool
150var jetifier bool
151
152func InList(s string, list []string) bool {
153	for _, l := range list {
154		if l == s {
155			return true
156		}
157	}
158
159	return false
160}
161
162type Dependency struct {
163	XMLName xml.Name `xml:"dependency"`
164
165	BpTarget string `xml:"-"`
166
167	GroupId    string `xml:"groupId"`
168	ArtifactId string `xml:"artifactId"`
169	Version    string `xml:"version"`
170	Type       string `xml:"type"`
171	Scope      string `xml:"scope"`
172}
173
174func (d Dependency) BpName() string {
175	if d.BpTarget == "" {
176		d.BpTarget = rewriteNames.MavenToBp(d.GroupId, d.ArtifactId)
177	}
178	return d.BpTarget
179}
180
181type Pom struct {
182	XMLName xml.Name `xml:"http://maven.apache.org/POM/4.0.0 project"`
183
184	PomFile       string `xml:"-"`
185	ArtifactFile  string `xml:"-"`
186	BpTarget      string `xml:"-"`
187	MinSdkVersion string `xml:"-"`
188
189	GroupId    string `xml:"groupId"`
190	ArtifactId string `xml:"artifactId"`
191	Version    string `xml:"version"`
192	Packaging  string `xml:"packaging"`
193
194	Dependencies []*Dependency `xml:"dependencies>dependency"`
195}
196
197func (p Pom) IsAar() bool {
198	return p.Packaging == "aar"
199}
200
201func (p Pom) IsJar() bool {
202	return p.Packaging == "jar"
203}
204
205func (p Pom) IsHostModule() bool {
206	return hostModuleNames.IsHostModule(p.GroupId, p.ArtifactId)
207}
208
209func (p Pom) IsDeviceModule() bool {
210	return !p.IsHostModule()
211}
212
213func (p Pom) IsHostAndDeviceModule() bool {
214	return hostAndDeviceModuleNames.IsHostAndDeviceModule(p.GroupId, p.ArtifactId)
215}
216
217func (p Pom) IsHostOnly() bool {
218	return p.IsHostModule() && !p.IsHostAndDeviceModule()
219}
220
221func (p Pom) ModuleType() string {
222	if p.IsAar() {
223		return "android_library"
224	} else if p.IsHostOnly() {
225		return "java_library_host"
226	} else {
227		return "java_library_static"
228	}
229}
230
231func (p Pom) ImportModuleType() string {
232	if p.IsAar() {
233		return "android_library_import"
234	} else if p.IsHostOnly() {
235		return "java_import_host"
236	} else {
237		return "java_import"
238	}
239}
240
241func (p Pom) ImportProperty() string {
242	if p.IsAar() {
243		return "aars"
244	} else {
245		return "jars"
246	}
247}
248
249func (p Pom) BpName() string {
250	if p.BpTarget == "" {
251		p.BpTarget = rewriteNames.MavenToBp(p.GroupId, p.ArtifactId)
252	}
253	return p.BpTarget
254}
255
256func (p Pom) BpJarDeps() []string {
257	return p.BpDeps("jar", []string{"compile", "runtime"})
258}
259
260func (p Pom) BpAarDeps() []string {
261	return p.BpDeps("aar", []string{"compile", "runtime"})
262}
263
264func (p Pom) BpExtraStaticLibs() []string {
265	return extraStaticLibs[p.BpName()]
266}
267
268func (p Pom) BpExtraLibs() []string {
269	return extraLibs[p.BpName()]
270}
271
272// BpDeps obtains dependencies filtered by type and scope. The results of this
273// method are formatted as Android.bp targets, e.g. run through MavenToBp rules.
274func (p Pom) BpDeps(typeExt string, scopes []string) []string {
275	var ret []string
276	for _, d := range p.Dependencies {
277		if d.Type != typeExt || !InList(d.Scope, scopes) {
278			continue
279		}
280		name := rewriteNames.MavenToBp(d.GroupId, d.ArtifactId)
281		ret = append(ret, name)
282	}
283	return ret
284}
285
286func (p Pom) SdkVersion() string {
287	return sdkVersion
288}
289
290func (p Pom) DefaultMinSdkVersion() string {
291	return defaultMinSdkVersion
292}
293
294func (p Pom) Jetifier() bool {
295	return jetifier
296}
297
298func (p *Pom) FixDeps(modules map[string]*Pom) {
299	for _, d := range p.Dependencies {
300		if d.Type == "" {
301			if depPom, ok := modules[d.BpName()]; ok {
302				// We've seen the POM for this dependency, use its packaging
303				// as the dependency type rather than Maven spec default.
304				d.Type = depPom.Packaging
305			} else {
306				// Dependency type was not specified and we don't have the POM
307				// for this artifact, use the default from Maven spec.
308				d.Type = "jar"
309			}
310		}
311		if d.Scope == "" {
312			// Scope was not specified, use the default from Maven spec.
313			d.Scope = "compile"
314		}
315	}
316}
317
318// ExtractMinSdkVersion extracts the minSdkVersion from the AndroidManifest.xml file inside an aar file, or sets it
319// to "current" if it is not present.
320func (p *Pom) ExtractMinSdkVersion() error {
321	aar, err := zip.OpenReader(p.ArtifactFile)
322	if err != nil {
323		return err
324	}
325	defer aar.Close()
326
327	var manifest *zip.File
328	for _, f := range aar.File {
329		if f.Name == "AndroidManifest.xml" {
330			manifest = f
331			break
332		}
333	}
334
335	if manifest == nil {
336		return fmt.Errorf("failed to find AndroidManifest.xml in %s", p.ArtifactFile)
337	}
338
339	r, err := manifest.Open()
340	if err != nil {
341		return err
342	}
343	defer r.Close()
344
345	decoder := xml.NewDecoder(r)
346
347	manifestData := struct {
348		XMLName  xml.Name `xml:"manifest"`
349		Uses_sdk struct {
350			MinSdkVersion string `xml:"http://schemas.android.com/apk/res/android minSdkVersion,attr"`
351		} `xml:"uses-sdk"`
352	}{}
353
354	err = decoder.Decode(&manifestData)
355	if err != nil {
356		return err
357	}
358
359	p.MinSdkVersion = manifestData.Uses_sdk.MinSdkVersion
360	if p.MinSdkVersion == "" {
361		p.MinSdkVersion = "current"
362	}
363
364	return nil
365}
366
367var bpTemplate = template.Must(template.New("bp").Parse(`
368{{.ImportModuleType}} {
369    name: "{{.BpName}}",
370    {{.ImportProperty}}: ["{{.ArtifactFile}}"],
371    sdk_version: "{{.SdkVersion}}",
372    {{- if .Jetifier}}
373    jetifier: true,
374    {{- end}}
375    {{- if .IsHostAndDeviceModule}}
376    host_supported: true,
377    {{- end}}
378    {{- if not .IsHostOnly}}
379    apex_available: [
380        "//apex_available:platform",
381        "//apex_available:anyapex",
382    ],
383    {{- end}}
384    {{- if .IsAar}}
385    min_sdk_version: "{{.MinSdkVersion}}",
386    static_libs: [
387        {{- range .BpJarDeps}}
388        "{{.}}",
389        {{- end}}
390        {{- range .BpAarDeps}}
391        "{{.}}",
392        {{- end}}
393        {{- range .BpExtraStaticLibs}}
394        "{{.}}",
395        {{- end}}
396    ],
397    {{- if .BpExtraLibs}}
398    libs: [
399        {{- range .BpExtraLibs}}
400        "{{.}}",
401        {{- end}}
402    ],
403    {{- end}}
404    {{- else if not .IsHostOnly}}
405    min_sdk_version: "{{.DefaultMinSdkVersion}}",
406    {{- end}}
407}
408`))
409
410var bpDepsTemplate = template.Must(template.New("bp").Parse(`
411{{.ImportModuleType}} {
412    name: "{{.BpName}}-nodeps",
413    {{.ImportProperty}}: ["{{.ArtifactFile}}"],
414    sdk_version: "{{.SdkVersion}}",
415    {{- if .Jetifier}}
416    jetifier: true,
417    {{- end}}
418    {{- if .IsHostAndDeviceModule}}
419    host_supported: true,
420    {{- end}}
421    {{- if not .IsHostOnly}}
422    apex_available: [
423        "//apex_available:platform",
424        "//apex_available:anyapex",
425    ],
426    {{- end}}
427    {{- if .IsAar}}
428    min_sdk_version: "{{.MinSdkVersion}}",
429    static_libs: [
430        {{- range .BpJarDeps}}
431        "{{.}}",
432        {{- end}}
433        {{- range .BpAarDeps}}
434        "{{.}}",
435        {{- end}}
436        {{- range .BpExtraStaticLibs}}
437        "{{.}}",
438        {{- end}}
439    ],
440    {{- if .BpExtraLibs}}
441    libs: [
442        {{- range .BpExtraLibs}}
443        "{{.}}",
444        {{- end}}
445    ],
446    {{- end}}
447    {{- else if not .IsHostOnly}}
448    min_sdk_version: "{{.DefaultMinSdkVersion}}",
449    {{- end}}
450}
451
452{{.ModuleType}} {
453    name: "{{.BpName}}",
454    {{- if .IsDeviceModule}}
455    sdk_version: "{{.SdkVersion}}",
456    {{- if .IsHostAndDeviceModule}}
457    host_supported: true,
458    {{- end}}
459    {{- if not .IsHostOnly}}
460    apex_available: [
461        "//apex_available:platform",
462        "//apex_available:anyapex",
463    ],
464    {{- end}}
465    {{- if .IsAar}}
466    min_sdk_version: "{{.MinSdkVersion}}",
467    manifest: "manifests/{{.BpName}}/AndroidManifest.xml",
468    {{- else if not .IsHostOnly}}
469    min_sdk_version: "{{.DefaultMinSdkVersion}}",
470    {{- end}}
471    {{- end}}
472    static_libs: [
473        "{{.BpName}}-nodeps",
474        {{- range .BpJarDeps}}
475        "{{.}}",
476        {{- end}}
477        {{- range .BpAarDeps}}
478        "{{.}}",
479        {{- end}}
480        {{- range .BpExtraStaticLibs}}
481        "{{.}}",
482        {{- end}}
483    ],
484    {{- if .BpExtraLibs}}
485    libs: [
486        {{- range .BpExtraLibs}}
487        "{{.}}",
488        {{- end}}
489    ],
490    {{- end}}
491    java_version: "1.7",
492}
493`))
494
495func parse(filename string) (*Pom, error) {
496	data, err := ioutil.ReadFile(filename)
497	if err != nil {
498		return nil, err
499	}
500
501	var pom Pom
502	err = xml.Unmarshal(data, &pom)
503	if err != nil {
504		return nil, err
505	}
506
507	if useVersion != "" && pom.Version != useVersion {
508		return nil, nil
509	}
510
511	if pom.Packaging == "" {
512		pom.Packaging = "jar"
513	}
514
515	pom.PomFile = filename
516	pom.ArtifactFile = strings.TrimSuffix(filename, ".pom") + "." + pom.Packaging
517
518	return &pom, nil
519}
520
521func rerunForRegen(filename string) error {
522	buf, err := ioutil.ReadFile(filename)
523	if err != nil {
524		return err
525	}
526
527	scanner := bufio.NewScanner(bytes.NewBuffer(buf))
528
529	// Skip the first line in the file
530	for i := 0; i < 2; i++ {
531		if !scanner.Scan() {
532			if scanner.Err() != nil {
533				return scanner.Err()
534			} else {
535				return fmt.Errorf("unexpected EOF")
536			}
537		}
538	}
539
540	// Extract the old args from the file
541	line := scanner.Text()
542	if strings.HasPrefix(line, "// pom2bp ") {
543		line = strings.TrimPrefix(line, "// pom2bp ")
544	} else if strings.HasPrefix(line, "// pom2mk ") {
545		line = strings.TrimPrefix(line, "// pom2mk ")
546	} else if strings.HasPrefix(line, "# pom2mk ") {
547		line = strings.TrimPrefix(line, "# pom2mk ")
548	} else {
549		return fmt.Errorf("unexpected second line: %q", line)
550	}
551	args := strings.Split(line, " ")
552	lastArg := args[len(args)-1]
553	args = args[:len(args)-1]
554
555	// Append all current command line args except -regen <file> to the ones from the file
556	for i := 1; i < len(os.Args); i++ {
557		if os.Args[i] == "-regen" || os.Args[i] == "--regen" {
558			i++
559		} else {
560			args = append(args, os.Args[i])
561		}
562	}
563	args = append(args, lastArg)
564
565	cmd := os.Args[0] + " " + strings.Join(args, " ")
566	// Re-exec pom2bp with the new arguments
567	output, err := exec.Command("/bin/sh", "-c", cmd).Output()
568	if exitErr, _ := err.(*exec.ExitError); exitErr != nil {
569		return fmt.Errorf("failed to run %s\n%s", cmd, string(exitErr.Stderr))
570	} else if err != nil {
571		return err
572	}
573
574	// If the old file was a .mk file, replace it with a .bp file
575	if filepath.Ext(filename) == ".mk" {
576		os.Remove(filename)
577		filename = strings.TrimSuffix(filename, ".mk") + ".bp"
578	}
579
580	return ioutil.WriteFile(filename, output, 0666)
581}
582
583func main() {
584	flag.Usage = func() {
585		fmt.Fprintf(os.Stderr, `pom2bp, a tool to create Android.bp files from maven repos
586
587The tool will extract the necessary information from *.pom files to create an Android.bp whose
588aar libraries can be linked against when using AAPT2.
589
590Usage: %s [--rewrite <regex>=<replace>] [-exclude <module>] [--extra-static-libs <module>=<module>[,<module>]] [--extra-libs <module>=<module>[,<module>]] [<dir>] [-regen <file>]
591
592  -rewrite <regex>=<replace>
593     rewrite can be used to specify mappings between Maven projects and Android.bp modules. The -rewrite
594     option can be specified multiple times. When determining the Android.bp module for a given Maven
595     project, mappings are searched in the order they were specified. The first <regex> matching
596     either the Maven project's <groupId>:<artifactId> or <artifactId> will be used to generate
597     the Android.bp module name using <replace>. If no matches are found, <artifactId> is used.
598  -exclude <module>
599     Don't put the specified module in the Android.bp file.
600  -extra-static-libs <module>=<module>[,<module>]
601     Some Android.bp modules have transitive static dependencies that must be specified when they
602     are depended upon (like android-support-v7-mediarouter requires android-support-v7-appcompat).
603     This may be specified multiple times to declare these dependencies.
604  -extra-libs <module>=<module>[,<module>]
605     Some Android.bp modules have transitive runtime dependencies that must be specified when they
606     are depended upon (like androidx.test.rules requires android.test.base).
607     This may be specified multiple times to declare these dependencies.
608  -sdk-version <version>
609     Sets sdk_version: "<version>" for all modules.
610  -default-min-sdk-version
611     The default min_sdk_version to use for a module if one cannot be mined from AndroidManifest.xml
612  -use-version <version>
613     If the maven directory contains multiple versions of artifacts and their pom files,
614     -use-version can be used to only write Android.bp files for a specific version of those artifacts.
615  -jetifier
616     Sets jetifier: true for all modules.
617  <dir>
618     The directory to search for *.pom files under.
619     The contents are written to stdout, to be put in the current directory (often as Android.bp)
620  -regen <file>
621     Read arguments from <file> and overwrite it (if it ends with .bp) or move it to .bp (if it
622     ends with .mk).
623
624`, os.Args[0])
625	}
626
627	var regen string
628
629	flag.Var(&excludes, "exclude", "Exclude module")
630	flag.Var(&extraStaticLibs, "extra-static-libs", "Extra static dependencies needed when depending on a module")
631	flag.Var(&extraLibs, "extra-libs", "Extra runtime dependencies needed when depending on a module")
632	flag.Var(&rewriteNames, "rewrite", "Regex(es) to rewrite artifact names")
633	flag.Var(&hostModuleNames, "host", "Specifies that the corresponding module (specified in the form 'module.group:module.artifact') is a host module")
634	flag.Var(&hostAndDeviceModuleNames, "host-and-device", "Specifies that the corresponding module (specified in the form 'module.group:module.artifact') is both a host and device module.")
635	flag.StringVar(&sdkVersion, "sdk-version", "", "What to write to sdk_version")
636	flag.StringVar(&defaultMinSdkVersion, "default-min-sdk-version", "24", "Default min_sdk_version to use, if one is not available from AndroidManifest.xml. Default: 24")
637	flag.StringVar(&useVersion, "use-version", "", "Only read artifacts of a specific version")
638	flag.BoolVar(&staticDeps, "static-deps", false, "Statically include direct dependencies")
639	flag.BoolVar(&jetifier, "jetifier", false, "Sets jetifier: true on all modules")
640	flag.StringVar(&regen, "regen", "", "Rewrite specified file")
641	flag.Parse()
642
643	if regen != "" {
644		err := rerunForRegen(regen)
645		if err != nil {
646			fmt.Fprintln(os.Stderr, err)
647			os.Exit(1)
648		}
649		os.Exit(0)
650	}
651
652	if flag.NArg() == 0 {
653		fmt.Fprintln(os.Stderr, "Directory argument is required")
654		os.Exit(1)
655	} else if flag.NArg() > 1 {
656		fmt.Fprintln(os.Stderr, "Multiple directories provided:", strings.Join(flag.Args(), " "))
657		os.Exit(1)
658	}
659
660	dir := flag.Arg(0)
661	absDir, err := filepath.Abs(dir)
662	if err != nil {
663		fmt.Fprintln(os.Stderr, "Failed to get absolute directory:", err)
664		os.Exit(1)
665	}
666
667	var filenames []string
668	err = filepath.Walk(absDir, func(path string, info os.FileInfo, err error) error {
669		if err != nil {
670			return err
671		}
672
673		name := info.Name()
674		if info.IsDir() {
675			if strings.HasPrefix(name, ".") {
676				return filepath.SkipDir
677			}
678			return nil
679		}
680
681		if strings.HasPrefix(name, ".") {
682			return nil
683		}
684
685		if strings.HasSuffix(name, ".pom") {
686			path, err = filepath.Rel(absDir, path)
687			if err != nil {
688				return err
689			}
690			filenames = append(filenames, filepath.Join(dir, path))
691		}
692		return nil
693	})
694	if err != nil {
695		fmt.Fprintln(os.Stderr, "Error walking files:", err)
696		os.Exit(1)
697	}
698
699	if len(filenames) == 0 {
700		fmt.Fprintln(os.Stderr, "Error: no *.pom files found under", dir)
701		os.Exit(1)
702	}
703
704	sort.Strings(filenames)
705
706	poms := []*Pom{}
707	modules := make(map[string]*Pom)
708	duplicate := false
709	for _, filename := range filenames {
710		pom, err := parse(filename)
711		if err != nil {
712			fmt.Fprintln(os.Stderr, "Error converting", filename, err)
713			os.Exit(1)
714		}
715
716		if pom != nil {
717			key := pom.BpName()
718			if excludes[key] {
719				continue
720			}
721
722			if old, ok := modules[key]; ok {
723				fmt.Fprintln(os.Stderr, "Module", key, "defined twice:", old.PomFile, pom.PomFile)
724				duplicate = true
725			}
726
727			poms = append(poms, pom)
728			modules[key] = pom
729		}
730	}
731	if duplicate {
732		os.Exit(1)
733	}
734
735	for _, pom := range poms {
736		if pom.IsAar() {
737			err := pom.ExtractMinSdkVersion()
738			if err != nil {
739				fmt.Fprintf(os.Stderr, "Error reading manifest for %s: %s", pom.ArtifactFile, err)
740				os.Exit(1)
741			}
742		}
743		pom.FixDeps(modules)
744	}
745
746	buf := &bytes.Buffer{}
747
748	fmt.Fprintln(buf, "// Automatically generated with:")
749	fmt.Fprintln(buf, "// pom2bp", strings.Join(proptools.ShellEscapeList(os.Args[1:]), " "))
750
751	for _, pom := range poms {
752		var err error
753		if staticDeps {
754			err = bpDepsTemplate.Execute(buf, pom)
755		} else {
756			err = bpTemplate.Execute(buf, pom)
757		}
758		if err != nil {
759			fmt.Fprintln(os.Stderr, "Error writing", pom.PomFile, pom.BpName(), err)
760			os.Exit(1)
761		}
762	}
763
764	out, err := bpfix.Reformat(buf.String())
765	if err != nil {
766		fmt.Fprintln(os.Stderr, "Error formatting output", err)
767		os.Exit(1)
768	}
769
770	os.Stdout.WriteString(out)
771}
772