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