mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 23:55:39 +00:00
668d0e7d79
Tools such as the Android SDK `d8` looks for Java tools in JAVA_HOME before PATH. We should match so we use the same tools, in particular to make sure our Java version check report the version actually used. Fixes: https://todo.sr.ht/~eliasnaur/gio/349 Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
1045 lines
25 KiB
Go
1045 lines
25 KiB
Go
// SPDX-License-Identifier: Unlicense OR MIT
|
|
|
|
package main
|
|
|
|
import (
|
|
"archive/zip"
|
|
"bytes"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"io/ioutil"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
"text/template"
|
|
|
|
"golang.org/x/sync/errgroup"
|
|
"golang.org/x/tools/go/packages"
|
|
)
|
|
|
|
type androidTools struct {
|
|
buildtools string
|
|
androidjar string
|
|
}
|
|
|
|
// zip.Writer with a sticky error.
|
|
type zipWriter struct {
|
|
err error
|
|
w *zip.Writer
|
|
}
|
|
|
|
// Writer that saves any errors.
|
|
type errWriter struct {
|
|
w io.Writer
|
|
err *error
|
|
}
|
|
|
|
var exeSuffix string
|
|
|
|
type manifestData struct {
|
|
AppID string
|
|
Version int
|
|
MinSDK int
|
|
TargetSDK int
|
|
Permissions []string
|
|
Features []string
|
|
IconSnip string
|
|
AppName string
|
|
}
|
|
|
|
const (
|
|
themes = `<?xml version="1.0" encoding="utf-8"?>
|
|
<resources>
|
|
<style name="Theme.GioApp" parent="android:style/Theme.NoTitleBar">
|
|
<item name="android:windowBackground">@android:color/white</item>
|
|
</style>
|
|
</resources>`
|
|
themesV21 = `<?xml version="1.0" encoding="utf-8"?>
|
|
<resources>
|
|
<style name="Theme.GioApp" parent="android:style/Theme.NoTitleBar">
|
|
<item name="android:windowBackground">@android:color/white</item>
|
|
|
|
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
|
<item name="android:navigationBarColor">#40000000</item>
|
|
<item name="android:statusBarColor">#40000000</item>
|
|
</style>
|
|
</resources>`
|
|
)
|
|
|
|
func init() {
|
|
if runtime.GOOS == "windows" {
|
|
exeSuffix = ".exe"
|
|
}
|
|
}
|
|
|
|
func buildAndroid(tmpDir string, bi *buildInfo) error {
|
|
sdk := os.Getenv("ANDROID_SDK_ROOT")
|
|
if sdk == "" {
|
|
return errors.New("please set ANDROID_SDK_ROOT to the Android SDK path")
|
|
}
|
|
if _, err := os.Stat(sdk); err != nil {
|
|
return err
|
|
}
|
|
platform, err := latestPlatform(sdk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
buildtools, err := latestTools(sdk)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tools := &androidTools{
|
|
buildtools: buildtools,
|
|
androidjar: filepath.Join(platform, "android.jar"),
|
|
}
|
|
perms := []string{"default"}
|
|
const permPref = "gioui.org/app/permission/"
|
|
cfg := &packages.Config{
|
|
Mode: packages.NeedName +
|
|
packages.NeedFiles +
|
|
packages.NeedImports +
|
|
packages.NeedDeps,
|
|
Env: append(
|
|
os.Environ(),
|
|
"GOOS=android",
|
|
"CGO_ENABLED=1",
|
|
),
|
|
}
|
|
pkgs, err := packages.Load(cfg, bi.pkgPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var extraJars []string
|
|
visitedPkgs := make(map[string]bool)
|
|
var visitPkg func(*packages.Package) error
|
|
visitPkg = func(p *packages.Package) error {
|
|
if len(p.GoFiles) == 0 {
|
|
return nil
|
|
}
|
|
dir := filepath.Dir(p.GoFiles[0])
|
|
jars, err := filepath.Glob(filepath.Join(dir, "*.jar"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
extraJars = append(extraJars, jars...)
|
|
switch {
|
|
case p.PkgPath == "net":
|
|
perms = append(perms, "network")
|
|
case strings.HasPrefix(p.PkgPath, permPref):
|
|
perms = append(perms, p.PkgPath[len(permPref):])
|
|
}
|
|
|
|
for _, imp := range p.Imports {
|
|
if !visitedPkgs[imp.ID] {
|
|
visitPkg(imp)
|
|
visitedPkgs[imp.ID] = true
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
if err := visitPkg(pkgs[0]); err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := compileAndroid(tmpDir, tools, bi); err != nil {
|
|
return err
|
|
}
|
|
switch *buildMode {
|
|
case "archive":
|
|
return archiveAndroid(tmpDir, bi, perms)
|
|
case "exe":
|
|
file := *destPath
|
|
if file == "" {
|
|
file = fmt.Sprintf("%s.apk", bi.name)
|
|
}
|
|
|
|
isBundle := false
|
|
switch filepath.Ext(file) {
|
|
case ".apk":
|
|
case ".aab":
|
|
isBundle = true
|
|
default:
|
|
return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'", file)
|
|
}
|
|
|
|
if err := exeAndroid(tmpDir, tools, bi, extraJars, perms, isBundle); err != nil {
|
|
return err
|
|
}
|
|
if isBundle {
|
|
return signAAB(tmpDir, file, tools, bi)
|
|
}
|
|
return signAPK(tmpDir, file, tools, bi)
|
|
default:
|
|
panic("unreachable")
|
|
}
|
|
}
|
|
|
|
func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err error) {
|
|
androidHome := os.Getenv("ANDROID_SDK_ROOT")
|
|
if androidHome == "" {
|
|
return errors.New("ANDROID_SDK_ROOT is not set. Please point it to the root of the Android SDK")
|
|
}
|
|
javac, err := findJavaC()
|
|
if err != nil {
|
|
return fmt.Errorf("could not find javac: %v", err)
|
|
}
|
|
ndkRoot, err := findNDK(androidHome)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
minSDK := 17
|
|
if bi.minsdk > minSDK {
|
|
minSDK = bi.minsdk
|
|
}
|
|
tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK())
|
|
var builds errgroup.Group
|
|
for _, a := range bi.archs {
|
|
arch := allArchs[a]
|
|
clang, err := latestCompiler(tcRoot, a, minSDK)
|
|
if err != nil {
|
|
return fmt.Errorf("%s. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it.", err)
|
|
}
|
|
if runtime.GOOS == "windows" {
|
|
// Because of https://github.com/android-ndk/ndk/issues/920,
|
|
// we need NDK r19c, not just r19b. Check for the presence of
|
|
// clang++.cmd which is only available in r19c.
|
|
clangpp := clang + "++.cmd"
|
|
if _, err := os.Stat(clangpp); err != nil {
|
|
return fmt.Errorf("NDK version r19b detected, but >= r19c is required. Use the command `sdkmanager ndk-bundle` to install it")
|
|
}
|
|
}
|
|
archDir := filepath.Join(tmpDir, "jni", arch.jniArch)
|
|
if err := os.MkdirAll(archDir, 0755); err != nil {
|
|
return fmt.Errorf("failed to create %q: %v", archDir, err)
|
|
}
|
|
libFile := filepath.Join(archDir, "libgio.so")
|
|
cmd := exec.Command(
|
|
"go",
|
|
"build",
|
|
"-ldflags=-w -s "+bi.ldflags,
|
|
"-buildmode=c-shared",
|
|
"-tags", bi.tags,
|
|
"-o", libFile,
|
|
bi.pkgPath,
|
|
)
|
|
cmd.Env = append(
|
|
os.Environ(),
|
|
"GOOS=android",
|
|
"GOARCH="+a,
|
|
"GOARM=7", // Avoid softfloat.
|
|
"CGO_ENABLED=1",
|
|
"CC="+clang,
|
|
)
|
|
builds.Go(func() error {
|
|
_, err := runCmd(cmd)
|
|
return err
|
|
})
|
|
}
|
|
appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/app/"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(javaFiles) == 0 {
|
|
return fmt.Errorf("the gioui.org/app package contains no .java files (gioui.org module too old?)")
|
|
}
|
|
if len(javaFiles) > 0 {
|
|
classes := filepath.Join(tmpDir, "classes")
|
|
if err := os.MkdirAll(classes, 0755); err != nil {
|
|
return err
|
|
}
|
|
javac := exec.Command(
|
|
javac,
|
|
"-target", "1.8",
|
|
"-source", "1.8",
|
|
"-sourcepath", appDir,
|
|
"-bootclasspath", tools.androidjar,
|
|
"-d", classes,
|
|
)
|
|
javac.Args = append(javac.Args, javaFiles...)
|
|
builds.Go(func() error {
|
|
_, err := runCmd(javac)
|
|
return err
|
|
})
|
|
}
|
|
return builds.Wait()
|
|
}
|
|
|
|
func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) {
|
|
aarFile := *destPath
|
|
if aarFile == "" {
|
|
aarFile = fmt.Sprintf("%s.aar", bi.name)
|
|
}
|
|
if filepath.Ext(aarFile) != ".aar" {
|
|
return fmt.Errorf("the specified output %q does not end in '.aar'", aarFile)
|
|
}
|
|
aar, err := os.Create(aarFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if cerr := aar.Close(); err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
aarw := newZipWriter(aar)
|
|
defer aarw.Close()
|
|
aarw.Create("R.txt")
|
|
themesXML := aarw.Create("res/values/themes.xml")
|
|
themesXML.Write([]byte(themes))
|
|
themesXML21 := aarw.Create("res/values-v21/themes.xml")
|
|
themesXML21.Write([]byte(themesV21))
|
|
permissions, features := getPermissions(perms)
|
|
// Disable input emulation on ChromeOS.
|
|
manifest := aarw.Create("AndroidManifest.xml")
|
|
manifestSrc := manifestData{
|
|
AppID: bi.appID,
|
|
MinSDK: bi.minsdk,
|
|
Permissions: permissions,
|
|
Features: features,
|
|
}
|
|
tmpl, err := template.New("manifest").Parse(
|
|
`<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="{{.AppID}}">
|
|
<uses-sdk android:minSdkVersion="{{.MinSDK}}"/>
|
|
{{range .Permissions}} <uses-permission android:name="{{.}}"/>
|
|
{{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
|
|
{{end}}</manifest>
|
|
`)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
err = tmpl.Execute(manifest, manifestSrc)
|
|
proguard := aarw.Create("proguard.txt")
|
|
proguard.Write([]byte(`-keep class org.gioui.** { *; }`))
|
|
|
|
for _, a := range bi.archs {
|
|
arch := allArchs[a]
|
|
libFile := filepath.Join("jni", arch.jniArch, "libgio.so")
|
|
aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile))
|
|
}
|
|
classes := filepath.Join(tmpDir, "classes")
|
|
if _, err := os.Stat(classes); err == nil {
|
|
jarFile := filepath.Join(tmpDir, "classes.jar")
|
|
if err := writeJar(jarFile, classes); err != nil {
|
|
return err
|
|
}
|
|
aarw.Add("classes.jar", jarFile)
|
|
}
|
|
return aarw.Close()
|
|
}
|
|
|
|
func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, perms []string, isBundle bool) (err error) {
|
|
classes := filepath.Join(tmpDir, "classes")
|
|
var classFiles []string
|
|
err = filepath.Walk(classes, func(path string, f os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if filepath.Ext(path) == ".class" {
|
|
classFiles = append(classFiles, path)
|
|
}
|
|
return nil
|
|
})
|
|
classFiles = append(classFiles, extraJars...)
|
|
dexDir := filepath.Join(tmpDir, "apk")
|
|
if err := os.MkdirAll(dexDir, 0755); err != nil {
|
|
return err
|
|
}
|
|
// Currently, new apps must have a target SDK version of at least 30.
|
|
// https://developer.android.com/distribute/best-practices/develop/target-sdk
|
|
targetSDK := 30
|
|
if bi.minsdk > targetSDK {
|
|
targetSDK = bi.minsdk
|
|
}
|
|
minSDK := 16
|
|
if bi.minsdk > minSDK {
|
|
minSDK = bi.minsdk
|
|
}
|
|
if len(classFiles) > 0 {
|
|
d8 := exec.Command(
|
|
filepath.Join(tools.buildtools, "d8"),
|
|
"--lib", tools.androidjar,
|
|
"--output", dexDir,
|
|
"--min-api", strconv.Itoa(minSDK),
|
|
)
|
|
d8.Args = append(d8.Args, classFiles...)
|
|
if _, err := runCmd(d8); err != nil {
|
|
major, minor, ok := determineJDKVersion()
|
|
if ok && (major != 1 || minor != 8) {
|
|
return fmt.Errorf("unsupported JDK version %d.%d, expected 1.8\nd8 error: %v", major, minor, err)
|
|
}
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Compile resources.
|
|
resDir := filepath.Join(tmpDir, "res")
|
|
valDir := filepath.Join(resDir, "values")
|
|
v21Dir := filepath.Join(resDir, "values-v21")
|
|
v26mipmapDir := filepath.Join(resDir, `mipmap-anydpi-v26`)
|
|
for _, dir := range []string{valDir, v21Dir, v26mipmapDir} {
|
|
if err := os.MkdirAll(dir, 0755); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
iconSnip := ""
|
|
if _, err := os.Stat(bi.iconPath); err == nil {
|
|
err := buildIcons(resDir, bi.iconPath, []iconVariant{
|
|
{path: filepath.Join("mipmap-hdpi", "ic_launcher.png"), size: 72},
|
|
{path: filepath.Join("mipmap-xhdpi", "ic_launcher.png"), size: 96},
|
|
{path: filepath.Join("mipmap-xxhdpi", "ic_launcher.png"), size: 144},
|
|
{path: filepath.Join("mipmap-xxxhdpi", "ic_launcher.png"), size: 192},
|
|
{path: filepath.Join("mipmap-mdpi", "ic_launcher_adaptive.png"), size: 108},
|
|
{path: filepath.Join("mipmap-hdpi", "ic_launcher_adaptive.png"), size: 162},
|
|
{path: filepath.Join("mipmap-xhdpi", "ic_launcher_adaptive.png"), size: 216},
|
|
{path: filepath.Join("mipmap-xxhdpi", "ic_launcher_adaptive.png"), size: 324},
|
|
{path: filepath.Join("mipmap-xxxhdpi", "ic_launcher_adaptive.png"), size: 432},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ioutil.WriteFile(filepath.Join(v26mipmapDir, `ic_launcher.xml`), []byte(`<?xml version="1.0" encoding="utf-8"?>
|
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
|
<background android:drawable="@mipmap/ic_launcher_adaptive" />
|
|
<foreground android:drawable="@mipmap/ic_launcher_adaptive" />
|
|
</adaptive-icon>`), 0660)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
iconSnip = `android:icon="@mipmap/ic_launcher"`
|
|
}
|
|
err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes), 0660)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
err = ioutil.WriteFile(filepath.Join(v21Dir, "themes.xml"), []byte(themesV21), 0660)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resZip := filepath.Join(tmpDir, "resources.zip")
|
|
aapt2 := filepath.Join(tools.buildtools, "aapt2")
|
|
_, err = runCmd(exec.Command(
|
|
aapt2,
|
|
"compile",
|
|
"-o", resZip,
|
|
"--dir", resDir))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Link APK.
|
|
permissions, features := getPermissions(perms)
|
|
appName := strings.Title(bi.name)
|
|
manifestSrc := manifestData{
|
|
AppID: bi.appID,
|
|
Version: bi.version,
|
|
MinSDK: minSDK,
|
|
TargetSDK: targetSDK,
|
|
Permissions: permissions,
|
|
Features: features,
|
|
IconSnip: iconSnip,
|
|
AppName: appName,
|
|
}
|
|
tmpl, err := template.New("test").Parse(
|
|
`<?xml version="1.0" encoding="utf-8"?>
|
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
package="{{.AppID}}"
|
|
android:versionCode="{{.Version}}"
|
|
android:versionName="1.0.{{.Version}}">
|
|
<uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" />
|
|
{{range .Permissions}} <uses-permission android:name="{{.}}"/>
|
|
{{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
|
|
{{end}} <application {{.IconSnip}} android:label="{{.AppName}}">
|
|
<activity android:name="org.gioui.GioActivity"
|
|
android:label="{{.AppName}}"
|
|
android:theme="@style/Theme.GioApp"
|
|
android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation|keyboardHidden"
|
|
android:windowSoftInputMode="adjustResize">
|
|
<intent-filter>
|
|
<action android:name="android.intent.action.MAIN" />
|
|
<category android:name="android.intent.category.LAUNCHER" />
|
|
</intent-filter>
|
|
</activity>
|
|
</application>
|
|
</manifest>`)
|
|
var manifestBuffer bytes.Buffer
|
|
if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil {
|
|
return err
|
|
}
|
|
manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
|
|
if err := ioutil.WriteFile(manifest, manifestBuffer.Bytes(), 0660); err != nil {
|
|
return err
|
|
}
|
|
|
|
linkAPK := filepath.Join(tmpDir, "link.apk")
|
|
|
|
args := []string{
|
|
"link",
|
|
"--manifest", manifest,
|
|
"-I", tools.androidjar,
|
|
"-o", linkAPK,
|
|
}
|
|
if isBundle {
|
|
args = append(args, "--proto-format")
|
|
}
|
|
args = append(args, resZip)
|
|
|
|
if _, err := runCmd(exec.Command(aapt2, args...)); err != nil {
|
|
return err
|
|
}
|
|
|
|
// The Go standard library archive/zip doesn't support appending to zip
|
|
// files. Copy files from `link.apk` (generated by aapt2) along with classes.dex and
|
|
// the Go libraries to a new `app.zip` file.
|
|
|
|
// Load link.apk as zip.
|
|
linkAPKZip, err := zip.OpenReader(linkAPK)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer linkAPKZip.Close()
|
|
|
|
// Create new "APK".
|
|
unsignedAPK := filepath.Join(tmpDir, "app.zip")
|
|
unsignedAPKFile, err := os.Create(unsignedAPK)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if cerr := unsignedAPKFile.Close(); err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
unsignedAPKZip := zip.NewWriter(unsignedAPKFile)
|
|
defer unsignedAPKZip.Close()
|
|
|
|
// Copy files from linkAPK to unsignedAPK.
|
|
for _, f := range linkAPKZip.File {
|
|
header := zip.FileHeader{
|
|
Name: f.FileHeader.Name,
|
|
Method: f.FileHeader.Method,
|
|
}
|
|
|
|
if isBundle {
|
|
// AAB have pre-defined folders.
|
|
switch header.Name {
|
|
case "AndroidManifest.xml":
|
|
header.Name = "manifest/AndroidManifest.xml"
|
|
}
|
|
}
|
|
|
|
w, err := unsignedAPKZip.CreateHeader(&header)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if _, err := io.Copy(w, r); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Append new files (that doesn't exists inside the link.apk).
|
|
appendToZip := func(path string, file string) error {
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
w, err := unsignedAPKZip.CreateHeader(&zip.FileHeader{
|
|
Name: filepath.ToSlash(path),
|
|
Method: zip.Deflate,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = io.Copy(w, f)
|
|
return err
|
|
}
|
|
|
|
// Append Go binaries (libgio.so).
|
|
for _, a := range bi.archs {
|
|
arch := allArchs[a]
|
|
libFile := filepath.Join(arch.jniArch, "libgio.so")
|
|
if err := appendToZip(filepath.Join("lib", libFile), filepath.Join(tmpDir, "jni", libFile)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Append classes.dex.
|
|
if len(classFiles) > 0 {
|
|
classesFolder := "classes.dex"
|
|
if isBundle {
|
|
classesFolder = "dex/classes.dex"
|
|
}
|
|
if err := appendToZip(classesFolder, filepath.Join(dexDir, "classes.dex")); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return unsignedAPKZip.Close()
|
|
}
|
|
|
|
func determineJDKVersion() (int, int, bool) {
|
|
path, err := findJavaC()
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
java := exec.Command(filepath.Join(filepath.Dir(path), "java"), "-version")
|
|
out, err := java.CombinedOutput()
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
var vendor string
|
|
var major, minor int
|
|
_, err = fmt.Sscanf(string(out), "%s version \"%d.%d", &vendor, &major, &minor)
|
|
return major, minor, err == nil
|
|
}
|
|
|
|
func signAPK(tmpDir string, apkFile string, tools *androidTools, bi *buildInfo) error {
|
|
if err := zipalign(tools, filepath.Join(tmpDir, "app.zip"), apkFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
if bi.key == "" {
|
|
if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
_, err := runCmd(exec.Command(
|
|
filepath.Join(tools.buildtools, "apksigner"),
|
|
"sign",
|
|
"--ks-pass", "pass:"+bi.password,
|
|
"--ks", bi.key,
|
|
apkFile,
|
|
))
|
|
|
|
return err
|
|
}
|
|
|
|
func signAAB(tmpDir string, aabFile string, tools *androidTools, bi *buildInfo) error {
|
|
allBundleTools, err := filepath.Glob(filepath.Join(tools.buildtools, "bundletool*.jar"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
bundletool := ""
|
|
for _, v := range allBundleTools {
|
|
bundletool = v
|
|
break
|
|
}
|
|
|
|
if bundletool == "" {
|
|
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)
|
|
}
|
|
|
|
_, err = runCmd(exec.Command(
|
|
"java",
|
|
"-jar", bundletool,
|
|
"build-bundle",
|
|
"--modules="+filepath.Join(tmpDir, "app.zip"),
|
|
"--output="+filepath.Join(tmpDir, "app.aab"),
|
|
))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if err := zipalign(tools, filepath.Join(tmpDir, "app.aab"), aabFile); err != nil {
|
|
return err
|
|
}
|
|
|
|
if bi.key == "" {
|
|
if err := defaultAndroidKeystore(tmpDir, bi); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
keytoolList, err := runCmd(exec.Command(
|
|
"keytool",
|
|
"-keystore", bi.key,
|
|
"-list",
|
|
"-keypass", bi.password,
|
|
"-v",
|
|
))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var alias string
|
|
for _, t := range strings.Split(keytoolList, "\n") {
|
|
if i, _ := fmt.Sscanf(t, "Alias name: %s", &alias); i > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
_, err = runCmd(exec.Command(
|
|
filepath.Join("jarsigner"),
|
|
"-sigalg", "SHA256withRSA",
|
|
"-digestalg", "SHA-256",
|
|
"-keystore", bi.key,
|
|
"-storepass", bi.password,
|
|
aabFile,
|
|
strings.TrimSpace(alias),
|
|
))
|
|
|
|
return err
|
|
}
|
|
|
|
func zipalign(tools *androidTools, input, output string) error {
|
|
_, err := runCmd(exec.Command(
|
|
filepath.Join(tools.buildtools, "zipalign"),
|
|
"-f",
|
|
"4", // 32-bit alignment.
|
|
input,
|
|
output,
|
|
))
|
|
return err
|
|
}
|
|
|
|
func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error {
|
|
home, err := os.UserHomeDir()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Use debug.keystore, if exists.
|
|
bi.key = filepath.Join(home, ".android", "debug.keystore")
|
|
bi.password = "android"
|
|
if _, err := os.Stat(bi.key); err == nil {
|
|
return nil
|
|
}
|
|
|
|
// Generate new key.
|
|
bi.key = filepath.Join(tmpDir, "sign.keystore")
|
|
keytool, err := findKeytool()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
_, err = runCmd(exec.Command(
|
|
keytool,
|
|
"-genkey",
|
|
"-keystore", bi.key,
|
|
"-storepass", bi.password,
|
|
"-alias", "android",
|
|
"-keyalg", "RSA", "-keysize", "2048",
|
|
"-validity", "10000",
|
|
"-noprompt",
|
|
"-dname", "CN=android",
|
|
))
|
|
return err
|
|
}
|
|
|
|
func findNDK(androidHome string) (string, error) {
|
|
ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if bestNDK, found := latestVersionPath(ndks); found {
|
|
return bestNDK, nil
|
|
}
|
|
// The old NDK path was $ANDROID_SDK_ROOT/ndk-bundle.
|
|
ndkBundle := filepath.Join(androidHome, "ndk-bundle")
|
|
if _, err := os.Stat(ndkBundle); err == nil {
|
|
return ndkBundle, nil
|
|
}
|
|
// Certain non-standard NDK isntallations set the $ANDROID_NDK_ROOT
|
|
// environment variable
|
|
if ndkBundle, ok := os.LookupEnv("ANDROID_NDK_ROOT"); ok {
|
|
if _, err := os.Stat(ndkBundle); err == nil {
|
|
return ndkBundle, nil
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
func findKeytool() (string, error) {
|
|
javaHome := os.Getenv("JAVA_HOME")
|
|
if javaHome == "" {
|
|
return exec.LookPath("keytool")
|
|
}
|
|
keytool := filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix)
|
|
if _, err := os.Stat(keytool); err != nil {
|
|
return "", err
|
|
}
|
|
return keytool, nil
|
|
}
|
|
|
|
func findJavaC() (string, error) {
|
|
javaHome := os.Getenv("JAVA_HOME")
|
|
if javaHome == "" {
|
|
return exec.LookPath("javac")
|
|
}
|
|
javac := filepath.Join(javaHome, "bin", "javac"+exeSuffix)
|
|
if _, err := os.Stat(javac); err != nil {
|
|
return "", err
|
|
}
|
|
return javac, nil
|
|
}
|
|
|
|
func writeJar(jarFile, dir string) (err error) {
|
|
jar, err := os.Create(jarFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if cerr := jar.Close(); err == nil {
|
|
err = cerr
|
|
}
|
|
}()
|
|
jarw := newZipWriter(jar)
|
|
const manifestHeader = `Manifest-Version: 1.0
|
|
Created-By: 1.0 (Go)
|
|
|
|
`
|
|
jarw.Create("META-INF/MANIFEST.MF").Write([]byte(manifestHeader))
|
|
err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if f.IsDir() {
|
|
return nil
|
|
}
|
|
if filepath.Ext(path) == ".class" {
|
|
rel := filepath.ToSlash(path[len(dir)+1:])
|
|
jarw.Add(rel, path)
|
|
}
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return jarw.Close()
|
|
}
|
|
|
|
func archNDK() string {
|
|
var arch string
|
|
switch runtime.GOARCH {
|
|
case "386":
|
|
arch = "x86"
|
|
case "amd64":
|
|
arch = "x86_64"
|
|
case "arm64":
|
|
if runtime.GOOS == "darwin" {
|
|
// Workaround for arm64 macOS. This will keep working until
|
|
// Apple deprecates Rosetta 2.
|
|
arch = "x86_64"
|
|
} else {
|
|
panic("unsupported GOARCH: " + runtime.GOARCH)
|
|
}
|
|
default:
|
|
panic("unsupported GOARCH: " + runtime.GOARCH)
|
|
}
|
|
return runtime.GOOS + "-" + arch
|
|
}
|
|
|
|
func getPermissions(ps []string) ([]string, []string) {
|
|
var permissions, features []string
|
|
seenPermissions := make(map[string]bool)
|
|
seenFeatures := make(map[string]bool)
|
|
for _, perm := range ps {
|
|
for _, x := range AndroidPermissions[perm] {
|
|
if !seenPermissions[x] {
|
|
permissions = append(permissions, x)
|
|
seenPermissions[x] = true
|
|
}
|
|
}
|
|
for _, x := range AndroidFeatures[perm] {
|
|
if !seenFeatures[x] {
|
|
features = append(features, x)
|
|
seenFeatures[x] = true
|
|
}
|
|
}
|
|
}
|
|
return permissions, features
|
|
}
|
|
|
|
func latestPlatform(sdk string) (string, error) {
|
|
allPlats, err := filepath.Glob(filepath.Join(sdk, "platforms", "android-*"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var bestVer int
|
|
var bestPlat string
|
|
for _, platform := range allPlats {
|
|
_, name := filepath.Split(platform)
|
|
// The glob above guarantees the "android-" prefix.
|
|
verStr := name[len("android-"):]
|
|
ver, err := strconv.Atoi(verStr)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if ver < bestVer {
|
|
continue
|
|
}
|
|
bestVer = ver
|
|
bestPlat = platform
|
|
}
|
|
if bestPlat == "" {
|
|
return "", fmt.Errorf("no platforms found in %q", sdk)
|
|
}
|
|
return bestPlat, nil
|
|
}
|
|
|
|
func latestCompiler(tcRoot, a string, minsdk int) (string, error) {
|
|
arch := allArchs[a]
|
|
allComps, err := filepath.Glob(filepath.Join(tcRoot, "bin", arch.clangArch+"*-clang"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
var bestVer int
|
|
var firstVer int
|
|
var bestCompiler string
|
|
var firstCompiler string
|
|
for _, compiler := range allComps {
|
|
var ver int
|
|
pattern := filepath.Join(tcRoot, "bin", arch.clangArch) + "%d-clang"
|
|
if n, err := fmt.Sscanf(compiler, pattern, &ver); n < 1 || err != nil {
|
|
continue
|
|
}
|
|
if firstCompiler == "" || ver < firstVer {
|
|
firstVer = ver
|
|
firstCompiler = compiler
|
|
}
|
|
if ver < bestVer {
|
|
continue
|
|
}
|
|
if ver > minsdk {
|
|
continue
|
|
}
|
|
bestVer = ver
|
|
bestCompiler = compiler
|
|
}
|
|
if bestCompiler == "" {
|
|
bestCompiler = firstCompiler
|
|
}
|
|
if bestCompiler == "" {
|
|
return "", fmt.Errorf("no NDK compiler found for architecture %s in %s", a, tcRoot)
|
|
}
|
|
return bestCompiler, nil
|
|
}
|
|
|
|
func latestTools(sdk string) (string, error) {
|
|
allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*"))
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
tools, found := latestVersionPath(allTools)
|
|
if !found {
|
|
return "", fmt.Errorf("no build-tools found in %q", sdk)
|
|
}
|
|
return tools, nil
|
|
}
|
|
|
|
// latestVersionFile finds the path with the highest version
|
|
// among paths on the form
|
|
//
|
|
// /some/path/major.minor.patch
|
|
func latestVersionPath(paths []string) (string, bool) {
|
|
var bestVer [3]int
|
|
var bestDir string
|
|
loop:
|
|
for _, path := range paths {
|
|
name := filepath.Base(path)
|
|
s := strings.SplitN(name, ".", 3)
|
|
var version [3]int
|
|
for i, v := range s {
|
|
v, err := strconv.Atoi(v)
|
|
if err != nil {
|
|
continue loop
|
|
}
|
|
if v < bestVer[i] {
|
|
continue loop
|
|
}
|
|
if v > bestVer[i] {
|
|
break
|
|
}
|
|
version[i] = v
|
|
}
|
|
bestVer = version
|
|
bestDir = path
|
|
}
|
|
return bestDir, bestDir != ""
|
|
}
|
|
|
|
func newZipWriter(w io.Writer) *zipWriter {
|
|
return &zipWriter{
|
|
w: zip.NewWriter(w),
|
|
}
|
|
}
|
|
|
|
func (z *zipWriter) Close() error {
|
|
err := z.w.Close()
|
|
if z.err == nil {
|
|
z.err = err
|
|
}
|
|
return z.err
|
|
}
|
|
|
|
func (z *zipWriter) Create(name string) io.Writer {
|
|
if z.err != nil {
|
|
return ioutil.Discard
|
|
}
|
|
w, err := z.w.Create(name)
|
|
if err != nil {
|
|
z.err = err
|
|
return ioutil.Discard
|
|
}
|
|
return &errWriter{w: w, err: &z.err}
|
|
}
|
|
|
|
func (z *zipWriter) Store(name, file string) {
|
|
z.add(name, file, false)
|
|
}
|
|
|
|
func (z *zipWriter) Add(name, file string) {
|
|
z.add(name, file, true)
|
|
}
|
|
|
|
func (z *zipWriter) add(name, file string, compressed bool) {
|
|
if z.err != nil {
|
|
return
|
|
}
|
|
f, err := os.Open(file)
|
|
if err != nil {
|
|
z.err = err
|
|
return
|
|
}
|
|
defer f.Close()
|
|
fh := &zip.FileHeader{
|
|
Name: name,
|
|
}
|
|
if compressed {
|
|
fh.Method = zip.Deflate
|
|
}
|
|
w, err := z.w.CreateHeader(fh)
|
|
if err != nil {
|
|
z.err = err
|
|
return
|
|
}
|
|
if _, err := io.Copy(w, f); err != nil {
|
|
z.err = err
|
|
return
|
|
}
|
|
}
|
|
|
|
func (w *errWriter) Write(p []byte) (n int, err error) {
|
|
if err := *w.err; err != nil {
|
|
return 0, err
|
|
}
|
|
n, err = w.w.Write(p)
|
|
*w.err = err
|
|
return
|
|
}
|