mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 15:45:38 +00:00
99e3481419
This commit changes the way that gogio searches for build tools so that it correctly identifies versions like '31' as equivalent to '31.0.0'. The Android SDK appears to not provide version components in the path name when the component is zero. Signed-off-by: Chris Waldon <christopher.waldon.dev@gmail.com>
1047 lines
25 KiB
Go
1047 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
|
|
}
|
|
if len(classFiles) > 0 {
|
|
d8 := exec.Command(
|
|
filepath.Join(tools.buildtools, "d8"),
|
|
"--classpath", tools.androidjar,
|
|
"--output", dexDir,
|
|
)
|
|
d8.Args = append(d8.Args, classFiles...)
|
|
if _, err := runCmd(d8); err != nil {
|
|
major, minor, ok := determineJDKVersion()
|
|
if ok && (major != 1 || minor < 7 || 8 < minor) {
|
|
return fmt.Errorf("unsupported JDK version %d.%d, expected 1.7 or 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.
|
|
// 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
|
|
}
|
|
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) {
|
|
java := exec.Command("java", "-version")
|
|
out, err := java.CombinedOutput()
|
|
if err != nil {
|
|
return 0, 0, false
|
|
}
|
|
var major, minor int
|
|
_, err = fmt.Sscanf(string(out), "java version \"%d.%d", &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) {
|
|
keytool, err := exec.LookPath("keytool")
|
|
if err == nil {
|
|
return keytool, err
|
|
}
|
|
javaHome := os.Getenv("JAVA_HOME")
|
|
if javaHome == "" {
|
|
return "", err
|
|
}
|
|
keytool = filepath.Join(javaHome, "jre", "bin", "keytool"+exeSuffix)
|
|
if _, serr := os.Stat(keytool); serr == nil {
|
|
return keytool, nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
func findJavaC() (string, error) {
|
|
javac, err := exec.LookPath("javac")
|
|
if err == nil {
|
|
return javac, err
|
|
}
|
|
javaHome := os.Getenv("JAVA_HOME")
|
|
if javaHome == "" {
|
|
return "", err
|
|
}
|
|
javac = filepath.Join(javaHome, "bin", "javac"+exeSuffix)
|
|
if _, serr := os.Stat(javac); serr == nil {
|
|
return javac, nil
|
|
}
|
|
return "", err
|
|
}
|
|
|
|
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
|
|
}
|