github.com/cybriq/giocore@v0.0.7-0.20210703034601-cfb9cb5f3900/cmd/gogio/androidbuild.go (about) 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/cybriq/giocore/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/cybriq/giocore/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 }