androidbuild.go raw
1 // SPDX-License-Identifier: Unlicense OR MIT
2
3 package main
4
5 import (
6 "archive/zip"
7 "bytes"
8 "errors"
9 "fmt"
10 "io"
11 "io/ioutil"
12 "os"
13 "os/exec"
14 "path/filepath"
15 "runtime"
16 "strconv"
17 "strings"
18 "text/template"
19
20 "golang.org/x/sync/errgroup"
21 "golang.org/x/tools/go/packages"
22 )
23
24 type androidTools struct {
25 buildtools string
26 androidjar string
27 }
28
29 // zip.Writer with a sticky error.
30 type zipWriter struct {
31 err error
32 w *zip.Writer
33 }
34
35 // Writer that saves any errors.
36 type errWriter struct {
37 w io.Writer
38 err *error
39 }
40
41 var exeSuffix string
42
43 type manifestData struct {
44 AppID string
45 Version int
46 MinSDK int
47 TargetSDK int
48 Permissions []string
49 Features []string
50 IconSnip string
51 AppName string
52 }
53
54 const (
55 themes = `<?xml version="1.0" encoding="utf-8"?>
56 <resources>
57 <style name="Theme.GioApp" parent="android:style/Theme.NoTitleBar">
58 <item name="android:windowBackground">@android:color/white</item>
59 </style>
60 </resources>`
61 themesV21 = `<?xml version="1.0" encoding="utf-8"?>
62 <resources>
63 <style name="Theme.GioApp" parent="android:style/Theme.NoTitleBar">
64 <item name="android:windowBackground">@android:color/white</item>
65
66 <item name="android:windowDrawsSystemBarBackgrounds">true</item>
67 <item name="android:navigationBarColor">#40000000</item>
68 <item name="android:statusBarColor">#40000000</item>
69 </style>
70 </resources>`
71 )
72
73 func init() {
74 if runtime.GOOS == "windows" {
75 exeSuffix = ".exe"
76 }
77 }
78
79 func buildAndroid(tmpDir string, bi *buildInfo) error {
80 sdk := os.Getenv("ANDROID_SDK_ROOT")
81 if sdk == "" {
82 return errors.New("please set ANDROID_SDK_ROOT to the Android SDK path")
83 }
84 if _, err := os.Stat(sdk); err != nil {
85 return err
86 }
87 platform, err := latestPlatform(sdk)
88 if err != nil {
89 return err
90 }
91 buildtools, err := latestTools(sdk)
92 if err != nil {
93 return err
94 }
95
96 tools := &androidTools{
97 buildtools: buildtools,
98 androidjar: filepath.Join(platform, "android.jar"),
99 }
100 perms := []string{"default"}
101 const permPref = "github.com/p9c/p9/pkg/gel/gio/app/permission/"
102 cfg := &packages.Config{
103 Mode: packages.NeedName +
104 packages.NeedFiles +
105 packages.NeedImports +
106 packages.NeedDeps,
107 Env: append(
108 os.Environ(),
109 "GOOS=android",
110 "CGO_ENABLED=1",
111 ),
112 }
113 pkgs, err := packages.Load(cfg, bi.pkgPath)
114 if err != nil {
115 return err
116 }
117 var extraJars []string
118 visitedPkgs := make(map[string]bool)
119 var visitPkg func(*packages.Package) error
120 visitPkg = func(p *packages.Package) error {
121 if len(p.GoFiles) == 0 {
122 return nil
123 }
124 dir := filepath.Dir(p.GoFiles[0])
125 jars, err := filepath.Glob(filepath.Join(dir, "*.jar"))
126 if err != nil {
127 return err
128 }
129 extraJars = append(extraJars, jars...)
130 switch {
131 case p.PkgPath == "net":
132 perms = append(perms, "network")
133 case strings.HasPrefix(p.PkgPath, permPref):
134 perms = append(perms, p.PkgPath[len(permPref):])
135 }
136
137 for _, imp := range p.Imports {
138 if !visitedPkgs[imp.ID] {
139 visitPkg(imp)
140 visitedPkgs[imp.ID] = true
141 }
142 }
143 return nil
144 }
145 if err := visitPkg(pkgs[0]); err != nil {
146 return err
147 }
148
149 if err := compileAndroid(tmpDir, tools, bi); err != nil {
150 return err
151 }
152 switch *buildMode {
153 case "archive":
154 return archiveAndroid(tmpDir, bi, perms)
155 case "exe":
156 file := *destPath
157 if file == "" {
158 file = fmt.Sprintf("%s.apk", bi.name)
159 }
160
161 isBundle := false
162 switch filepath.Ext(file) {
163 case ".apk":
164 case ".aab":
165 isBundle = true
166 default:
167 return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'", file)
168 }
169
170 if err := exeAndroid(tmpDir, tools, bi, extraJars, perms, isBundle); err != nil {
171 return err
172 }
173 if isBundle {
174 return signAAB(tmpDir, file, tools, bi)
175 }
176 return signAPK(tmpDir, file, tools, bi)
177 default:
178 panic("unreachable")
179 }
180 }
181
182 func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err error) {
183 androidHome := os.Getenv("ANDROID_SDK_ROOT")
184 if androidHome == "" {
185 return errors.New("ANDROID_SDK_ROOT is not set. Please point it to the root of the Android SDK")
186 }
187 javac, err := findJavaC()
188 if err != nil {
189 return fmt.Errorf("could not find javac: %v", err)
190 }
191 ndkRoot, err := findNDK(androidHome)
192 if err != nil {
193 return err
194 }
195 minSDK := 16
196 if bi.minsdk > minSDK {
197 minSDK = bi.minsdk
198 }
199 tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK())
200 var builds errgroup.Group
201 for _, a := range bi.archs {
202 arch := allArchs[a]
203 clang, err := latestCompiler(tcRoot, a, minSDK)
204 if err != nil {
205 return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.", err)
206 }
207 if runtime.GOOS == "windows" {
208 // Because of https://github.com/android-ndk/ndk/issues/920,
209 // we need NDK r19c, not just r19b. Check for the presence of
210 // clang++.cmd which is only available in r19c.
211 clangpp := clang + "++.cmd"
212 if _, err := os.Stat(clangpp); err != nil {
213 return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it")
214 }
215 }
216 archDir := filepath.Join(tmpDir, "jni", arch.jniArch)
217 if err := os.MkdirAll(archDir, 0755); err != nil {
218 return fmt.Errorf("failed to create %q: %v", archDir, err)
219 }
220 libFile := filepath.Join(archDir, "libgio.so")
221 cmd := exec.Command(
222 "go",
223 "build",
224 "-ldflags=-w -s "+bi.ldflags,
225 "-buildmode=c-shared",
226 "-tags", bi.tags,
227 "-o", libFile,
228 bi.pkgPath,
229 )
230 cmd.Env = append(
231 os.Environ(),
232 "GOOS=android",
233 "GOARCH="+a,
234 "GOARM=7", // Avoid softfloat.
235 "CGO_ENABLED=1",
236 "CC="+clang,
237 )
238 builds.Go(func() error {
239 _, err := runCmd(cmd)
240 return err
241 })
242 }
243 appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "github.com/p9c/p9/pkg/gel/gio/app/internal/wm"))
244 if err != nil {
245 return err
246 }
247 javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java"))
248 if err != nil {
249 return err
250 }
251 if len(javaFiles) > 0 {
252 classes := filepath.Join(tmpDir, "classes")
253 if err := os.MkdirAll(classes, 0755); err != nil {
254 return err
255 }
256 javac := exec.Command(
257 javac,
258 "-target", "1.8",
259 "-source", "1.8",
260 "-sourcepath", appDir,
261 "-bootclasspath", tools.androidjar,
262 "-d", classes,
263 )
264 javac.Args = append(javac.Args, javaFiles...)
265 builds.Go(func() error {
266 _, err := runCmd(javac)
267 return err
268 })
269 }
270 return builds.Wait()
271 }
272
273 func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) {
274 aarFile := *destPath
275 if aarFile == "" {
276 aarFile = fmt.Sprintf("%s.aar", bi.name)
277 }
278 if filepath.Ext(aarFile) != ".aar" {
279 return fmt.Errorf("the specified output %q does not end in '.aar'", aarFile)
280 }
281 aar, err := os.Create(aarFile)
282 if err != nil {
283 return err
284 }
285 defer func() {
286 if cerr := aar.Close(); err == nil {
287 err = cerr
288 }
289 }()
290 aarw := newZipWriter(aar)
291 defer aarw.Close()
292 aarw.Create("R.txt")
293 themesXML := aarw.Create("res/values/themes.xml")
294 themesXML.Write([]byte(themes))
295 themesXML21 := aarw.Create("res/values-v21/themes.xml")
296 themesXML21.Write([]byte(themesV21))
297 permissions, features := getPermissions(perms)
298 // Disable input emulation on ChromeOS.
299 manifest := aarw.Create("AndroidManifest.xml")
300 manifestSrc := manifestData{
301 AppID: bi.appID,
302 MinSDK: bi.minsdk,
303 Permissions: permissions,
304 Features: features,
305 }
306 tmpl, err := template.New("manifest").Parse(
307 `<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{{.AppID}}">
308 <uses-sdk android:minSdkVersion="{{.MinSDK}}"/>
309 {{range .Permissions}} <uses-permission android:name="{{.}}"/>
310 {{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
311 {{end}}</manifest>
312 `)
313 if err != nil {
314 panic(err)
315 }
316 err = tmpl.Execute(manifest, manifestSrc)
317 proguard := aarw.Create("proguard.txt")
318 proguard.Write([]byte(`-keep class org.gioui.** { *; }`))
319
320 for _, a := range bi.archs {
321 arch := allArchs[a]
322 libFile := filepath.Join("jni", arch.jniArch, "libgio.so")
323 aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile))
324 }
325 classes := filepath.Join(tmpDir, "classes")
326 if _, err := os.Stat(classes); err == nil {
327 jarFile := filepath.Join(tmpDir, "classes.jar")
328 if err := writeJar(jarFile, classes); err != nil {
329 return err
330 }
331 aarw.Add("classes.jar", jarFile)
332 }
333 return aarw.Close()
334 }
335
336 func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, perms []string, isBundle bool) (err error) {
337 classes := filepath.Join(tmpDir, "classes")
338 var classFiles []string
339 err = filepath.Walk(classes, func(path string, f os.FileInfo, err error) error {
340 if err != nil {
341 return err
342 }
343 if filepath.Ext(path) == ".class" {
344 classFiles = append(classFiles, path)
345 }
346 return nil
347 })
348 classFiles = append(classFiles, extraJars...)
349 dexDir := filepath.Join(tmpDir, "apk")
350 if err := os.MkdirAll(dexDir, 0755); err != nil {
351 return err
352 }
353 if len(classFiles) > 0 {
354 d8 := exec.Command(
355 filepath.Join(tools.buildtools, "d8"),
356 "--classpath", tools.androidjar,
357 "--output", dexDir,
358 )
359 d8.Args = append(d8.Args, classFiles...)
360 if _, err := runCmd(d8); err != nil {
361 return err
362 }
363 }
364
365 // Compile resources.
366 resDir := filepath.Join(tmpDir, "res")
367 valDir := filepath.Join(resDir, "values")
368 v21Dir := filepath.Join(resDir, "values-v21")
369 for _, dir := range []string{valDir, v21Dir} {
370 if err := os.MkdirAll(dir, 0755); err != nil {
371 return err
372 }
373 }
374 iconSnip := ""
375 if _, err := os.Stat(bi.iconPath); err == nil {
376 err := buildIcons(resDir, bi.iconPath, []iconVariant{
377 {path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72},
378 {path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96},
379 {path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"), size: 144},
380 {path: filepath.Join("mipmap-xxxhdpi", "ic_launcher.png"), size: 192},
381 })
382 if err != nil {
383 return err
384 }
385 iconSnip = `android:icon="@mipmap/ic_launcher"`
386 }
387 err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes), 0660)
388 if err != nil {
389 return err
390 }
391 err = ioutil.WriteFile(filepath.Join(v21Dir, "themes.xml"), []byte(themesV21), 0660)
392 if err != nil {
393 return err
394 }
395 resZip := filepath.Join(tmpDir, "resources.zip")
396 aapt2 := filepath.Join(tools.buildtools, "aapt2")
397 _, err = runCmd(exec.Command(
398 aapt2,
399 "compile",
400 "-o", resZip,
401 "--dir", resDir))
402 if err != nil {
403 return err
404 }
405
406 // Link APK.
407 // Currently, new apps must have a target SDK version of at least 30.
408 // https://developer.android.com/distribute/best-practices/develop/target-sdk
409 targetSDK := 30
410 if bi.minsdk > targetSDK {
411 targetSDK = bi.minsdk
412 }
413 minSDK := 16
414 if bi.minsdk > minSDK {
415 minSDK = bi.minsdk
416 }
417 permissions, features := getPermissions(perms)
418 appName := strings.Title(bi.name)
419 manifestSrc := manifestData{
420 AppID: bi.appID,
421 Version: bi.version,
422 MinSDK: minSDK,
423 TargetSDK: targetSDK,
424 Permissions: permissions,
425 Features: features,
426 IconSnip: iconSnip,
427 AppName: appName,
428 }
429 tmpl, err := template.New("test").Parse(
430 `<?xml version="1.0" encoding="utf-8"?>
431 <manifest xmlns:android="http://schemas.android.com/apk/res/android"
432 package="{{.AppID}}"
433 android:versionCode="{{.Version}}"
434 android:versionName="1.0.{{.Version}}">
435 <uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" />
436 {{range .Permissions}} <uses-permission android:name="{{.}}"/>
437 {{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
438 {{end}} <application {{.IconSnip}} android:label="{{.AppName}}">
439 <activity android:name="org.gioui.GioActivity"
440 android:label="{{.AppName}}"
441 android:theme="@style/Theme.GioApp"
442 android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation|keyboardHidden"
443 android:windowSoftInputMode="adjustResize">
444 <intent-filter>
445 <action android:name="android.intent.action.MAIN" />
446 <category android:name="android.intent.category.LAUNCHER" />
447 </intent-filter>
448 </activity>
449 </application>
450 </manifest>`)
451 var manifestBuffer bytes.Buffer
452 if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil {
453 return err
454 }
455 manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
456 if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(), 0660); err != nil {
457 return err
458 }
459
460 linkAPK := filepath.Join(tmpDir, "link.apk")
461
462 args := []string{
463 "link",
464 "--manifest", manifest,
465 "-I", tools.androidjar,
466 "-o", linkAPK,
467 }
468 if isBundle {
469 args = append(args, "--proto-format")
470 }
471 args = append(args, resZip)
472
473 if _, err := runCmd(exec.Command(aapt2, args...)); err != nil {
474 return err
475 }
476
477 // The Go standard library archive/zip doesn't support appending to zip
478 // files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and
479 // the Go libraries to a new `app.zip` file.
480
481 // Load link.apk as zip.
482 linkAPKZip, err := zip.OpenReader(linkAPK)
483 if err != nil {
484 return err
485 }
486 defer linkAPKZip.Close()
487
488 // Create new "APK".
489 unsignedAPK := filepath.Join(tmpDir, "app.zip")
490 unsignedAPKFile, err := os.Create(unsignedAPK)
491 if err != nil {
492 return err
493 }
494 defer func() {
495 if cerr := unsignedAPKFile.Close(); err == nil {
496 err = cerr
497 }
498 }()
499 unsignedAPKZip := zip.NewWriter(unsignedAPKFile)
500 defer unsignedAPKZip.Close()
501
502 // Copy files from linkAPK to unsignedAPK.
503 for _, f := range linkAPKZip.File {
504 header := zip.FileHeader{
505 Name: f.FileHeader.Name,
506 Method: f.FileHeader.Method,
507 }
508
509 if isBundle {
510 // AAB have pre-defined folders.
511 switch header.Name {
512 case "AndroidManifest.xml":
513 header.Name = "manifest/AndroidManifest.xml"
514 }
515 }
516
517 w, err := unsignedAPKZip.CreateHeader(&header)
518 if err != nil {
519 return err
520 }
521 r, err := f.Open()
522 if err != nil {
523 return err
524 }
525 if _, err := io.Copy(w, r); err != nil {
526 return err
527 }
528 }
529
530 // Append new files (that doesn't exists inside the link.apk).
531 appendToZip := func(path string, file string) error {
532 f, err := os.Open(file)
533 if err != nil {
534 return err
535 }
536 defer f.Close()
537 w, err := unsignedAPKZip.CreateHeader(&zip.FileHeader{
538 Name: filepath.ToSlash(path),
539 Method: zip.Deflate,
540 })
541 if err != nil {
542 return err
543 }
544 _, err = io.Copy(w, f)
545 return err
546 }
547
548 // Append Go binaries (libgio.so).
549 for _, a := range bi.archs {
550 arch := allArchs[a]
551 libFile := filepath.Join(arch.jniArch, "libgio.so")
552 if err := appendToZip(filepath.Join("lib", libFile), filepath.Join(tmpDir, "jni", libFile)); err != nil {
553 return err
554 }
555 }
556
557 // Append classes.dex.
558 classesFolder := "classes.dex"
559 if isBundle {
560 classesFolder = "dex/classes.dex"
561 }
562 if err := appendToZip(classesFolder, filepath.Join(dexDir, "classes.dex")); err != nil {
563 return err
564 }
565
566 return unsignedAPKZip.Close()
567 }
568
569 func signAPK(tmpDir string, apkFile string, tools *androidTools, bi *buildInfo) error {
570 if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"), apkFile); err != nil {
571 return err
572 }
573
574 if bi.key == "" {
575 if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
576 return err
577 }
578 }
579
580 _, err := runCmd(exec.Command(
581 filepath.Join(tools.buildtools, "apksigner"),
582 "sign",
583 "--ks-pass", "pass:"+bi.password,
584 "--ks", bi.key,
585 apkFile,
586 ))
587
588 return err
589 }
590
591 func signAAB(tmpDir string, aabFile string, tools *androidTools, bi *buildInfo) error {
592 allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools, "bundletool*.jar"))
593 if err != nil {
594 return err
595 }
596
597 bundletool := ""
598 for _, v := range allBundleTools {
599 bundletool = v
600 break
601 }
602
603 if bundletool == "" {
604 return fmt.Errorf("bundletool was not found at %s. Download it from https://github.com/google/bundletool/releases and move to the respective folder", tools.buildtools)
605 }
606
607 _, err = runCmd(exec.Command(
608 "java",
609 "-jar", bundletool,
610 "build-bundle",
611 "--modules="+filepath.Join(tmpDir, "app.zip"),
612 "--output="+filepath.Join(tmpDir, "app.aab"),
613 ))
614 if err != nil {
615 return err
616 }
617
618 if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"), aabFile); err != nil {
619 return err
620 }
621
622 if bi.key == "" {
623 if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
624 return err
625 }
626 }
627
628 keytoolList, err := runCmd(exec.Command(
629 "keytool",
630 "-keystore", bi.key,
631 "-list",
632 "-keypass", bi.password,
633 "-v",
634 ))
635 if err != nil {
636 return err
637 }
638
639 var alias string
640 for _, t := range strings.Split(keytoolList, "\n") {
641 if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 {
642 break
643 }
644 }
645
646 _, err = runCmd(exec.Command(
647 filepath.Join("jarsigner"),
648 "-sigalg", "SHA256withRSA",
649 "-digestalg", "SHA-256",
650 "-keystore", bi.key,
651 "-storepass", bi.password,
652 aabFile,
653 strings.TrimSpace(alias),
654 ))
655
656 return err
657 }
658
659 func zipalign(tools *androidTools, input, output string) error {
660 _, err := runCmd(exec.Command(
661 filepath.Join(tools.buildtools, "zipalign"),
662 "-f",
663 "4", // 32-bit alignment.
664 input,
665 output,
666 ))
667 return err
668 }
669
670 func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error {
671 home, err := os.UserHomeDir()
672 if err != nil {
673 return err
674 }
675
676 // Use debug.keystore, if exists.
677 bi.key = filepath.Join(home, ".android", "debug.keystore")
678 bi.password = "android"
679 if _, err := os.Stat(bi.key); err == nil {
680 return nil
681 }
682
683 // Generate new key.
684 bi.key = filepath.Join(tmpDir, "sign.keystore")
685 keytool, err := findKeytool()
686 if err != nil {
687 return err
688 }
689 _, err = runCmd(exec.Command(
690 keytool,
691 "-genkey",
692 "-keystore", bi.key,
693 "-storepass", bi.password,
694 "-alias", "android",
695 "-keyalg", "RSA", "-keysize", "2048",
696 "-validity", "10000",
697 "-noprompt",
698 "-dname", "CN=android",
699 ))
700 return err
701 }
702
703 func findNDK(androidHome string) (string, error) {
704 ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*"))
705 if err != nil {
706 return "", err
707 }
708 if bestNDK, found := latestVersionPath(ndks); found {
709 return bestNDK, nil
710 }
711 // The old NDK path was $ANDROID_SDK_ROOT/ndk-bundle.
712 ndkBundle := filepath.Join(androidHome, "ndk-bundle")
713 if _, err := os.Stat(ndkBundle); err == nil {
714 return ndkBundle, nil
715 }
716 // Certain non-standard NDK isntallations set the $ANDROID_NDK_ROOT
717 // environment variable
718 if ndkBundle, ok := os.LookupEnv("ANDROID_NDK_ROOT"); ok {
719 if _, err := os.Stat(ndkBundle); err == nil {
720 return ndkBundle, nil
721 }
722 }
723
724 return "", fmt.Errorf("no NDK found in $ANDROID_SDK_ROOT (%s). Set $ANDROID_NDK_ROOT or use `sdkmanager ndk-bundle` to install the NDK", androidHome)
725 }
726
727 func findKeytool() (string, error) {
728 keytool, err := exec.LookPath("keytool")
729 if err == nil {
730 return keytool, err
731 }
732 javaHome := os.Getenv("JAVA_HOME")
733 if javaHome == "" {
734 return "", err
735 }
736 keytool = filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix)
737 if _, serr := os.Stat(keytool); serr == nil {
738 return keytool, nil
739 }
740 return "", err
741 }
742
743 func findJavaC() (string, error) {
744 javac, err := exec.LookPath("javac")
745 if err == nil {
746 return javac, err
747 }
748 javaHome := os.Getenv("JAVA_HOME")
749 if javaHome == "" {
750 return "", err
751 }
752 javac = filepath.Join(javaHome, "bin", "javac"+exeSuffix)
753 if _, serr := os.Stat(javac); serr == nil {
754 return javac, nil
755 }
756 return "", err
757 }
758
759 func writeJar(jarFile, dir string) (err error) {
760 jar, err := os.Create(jarFile)
761 if err != nil {
762 return err
763 }
764 defer func() {
765 if cerr := jar.Close(); err == nil {
766 err = cerr
767 }
768 }()
769 jarw := newZipWriter(jar)
770 const manifestHeader = `Manifest-Version: 1.0
771 Created-By: 1.0 (Go)
772
773 `
774 jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader))
775 err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
776 if err != nil {
777 return err
778 }
779 if f.IsDir() {
780 return nil
781 }
782 if filepath.Ext(path) == ".class" {
783 rel := filepath.ToSlash(path[len(dir)+1:])
784 jarw.Add(rel, path)
785 }
786 return nil
787 })
788 if err != nil {
789 return err
790 }
791 return jarw.Close()
792 }
793
794 func archNDK() string {
795 var arch string
796 switch runtime.GOARCH {
797 case "386":
798 arch = "x86"
799 case "amd64":
800 arch = "x86_64"
801 default:
802 panic("unsupported GOARCH: " + runtime.GOARCH)
803 }
804 return runtime.GOOS + "-" + arch
805 }
806
807 func getPermissions(ps []string) ([]string, []string) {
808 var permissions, features []string
809 seenPermissions := make(map[string]bool)
810 seenFeatures := make(map[string]bool)
811 for _, perm := range ps {
812 for _, x := range AndroidPermissions[perm] {
813 if !seenPermissions[x] {
814 permissions = append(permissions, x)
815 seenPermissions[x] = true
816 }
817 }
818 for _, x := range AndroidFeatures[perm] {
819 if !seenFeatures[x] {
820 features = append(features, x)
821 seenFeatures[x] = true
822 }
823 }
824 }
825 return permissions, features
826 }
827
828 func latestPlatform(sdk string) (string, error) {
829 allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
830 if err != nil {
831 return "", err
832 }
833 var bestVer int
834 var bestPlat string
835 for _, platform := range allPlats {
836 _, name := filepath.Split(platform)
837 // The glob above guarantees the "android-" prefix.
838 verStr := name[len("android-"):]
839 ver, err := strconv.Atoi(verStr)
840 if err != nil {
841 continue
842 }
843 if ver < bestVer {
844 continue
845 }
846 bestVer = ver
847 bestPlat = platform
848 }
849 if bestPlat == "" {
850 return "", fmt.Errorf("no platforms found in %q", sdk)
851 }
852 return bestPlat, nil
853 }
854
855 func latestCompiler(tcRoot, a string, minsdk int) (string, error) {
856 arch := allArchs[a]
857 allComps, err := filepath.Glob(filepath.Join(tcRoot, "bin", arch.clangArch+"*-clang"))
858 if err != nil {
859 return "", err
860 }
861 var bestVer int
862 var firstVer int
863 var bestCompiler string
864 var firstCompiler string
865 for _, compiler := range allComps {
866 var ver int
867 pattern := filepath.Join(tcRoot, "bin", arch.clangArch) + "%d-clang"
868 if n, err := fmt.Sscanf(compiler, pattern, &ver); n < 1 || err != nil {
869 continue
870 }
871 if firstCompiler == "" || ver < firstVer {
872 firstVer = ver
873 firstCompiler = compiler
874 }
875 if ver < bestVer {
876 continue
877 }
878 if ver > minsdk {
879 continue
880 }
881 bestVer = ver
882 bestCompiler = compiler
883 }
884 if bestCompiler == "" {
885 bestCompiler = firstCompiler
886 }
887 if bestCompiler == "" {
888 return "", fmt.Errorf("no NDK compiler found for architecture %s in %s", a, tcRoot)
889 }
890 return bestCompiler, nil
891 }
892
893 func latestTools(sdk string) (string, error) {
894 allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*"))
895 if err != nil {
896 return "", err
897 }
898 tools, found := latestVersionPath(allTools)
899 if !found {
900 return "", fmt.Errorf("no build-tools found in %q", sdk)
901 }
902 return tools, nil
903 }
904
905 // latestVersionFile finds the path with the highest version
906 // among paths on the form
907 //
908 // /some/path/major.minor.patch
909 func latestVersionPath(paths []string) (string, bool) {
910 var bestVer [3]int
911 var bestDir string
912 loop:
913 for _, path := range paths {
914 name := filepath.Base(path)
915 s := strings.SplitN(name, ".", 3)
916 if len(s) != len(bestVer) {
917 continue
918 }
919 var version [3]int
920 for i, v := range s {
921 v, err := strconv.Atoi(v)
922 if err != nil {
923 continue loop
924 }
925 if v < bestVer[i] {
926 continue loop
927 }
928 if v > bestVer[i] {
929 break
930 }
931 version[i] = v
932 }
933 bestVer = version
934 bestDir = path
935 }
936 return bestDir, bestDir != ""
937 }
938
939 func newZipWriter(w io.Writer) *zipWriter {
940 return &zipWriter{
941 w: zip.NewWriter(w),
942 }
943 }
944
945 func (z *zipWriter) Close() error {
946 err := z.w.Close()
947 if z.err == nil {
948 z.err = err
949 }
950 return z.err
951 }
952
953 func (z *zipWriter) Create(name string) io.Writer {
954 if z.err != nil {
955 return ioutil.Discard
956 }
957 w, err := z.w.Create(name)
958 if err != nil {
959 z.err = err
960 return ioutil.Discard
961 }
962 return &errWriter{w: w, err: &z.err}
963 }
964
965 func (z *zipWriter) Store(name, file string) {
966 z.add(name, file, false)
967 }
968
969 func (z *zipWriter) Add(name, file string) {
970 z.add(name, file, true)
971 }
972
973 func (z *zipWriter) add(name, file string, compressed bool) {
974 if z.err != nil {
975 return
976 }
977 f, err := os.Open(file)
978 if err != nil {
979 z.err = err
980 return
981 }
982 defer f.Close()
983 fh := &zip.FileHeader{
984 Name: name,
985 }
986 if compressed {
987 fh.Method = zip.Deflate
988 }
989 w, err := z.w.CreateHeader(fh)
990 if err != nil {
991 z.err = err
992 return
993 }
994 if _, err := io.Copy(w, f); err != nil {
995 z.err = err
996 return
997 }
998 }
999
1000 func (w *errWriter) Write(p []byte) (n int, err error) {
1001 if err := *w.err; err != nil {
1002 return 0, err
1003 }
1004 n, err = w.w.Write(p)
1005 *w.err = err
1006 return
1007 }
1008