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  }