mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-07 10:25:37 +00:00
cmd/gio: add buildmode=exe for building gio programs ready to run
And add Android implementation. Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -0,0 +1,538 @@
|
|||||||
|
// SPDX-License-Identifier: Unlicense OR MIT
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sync/errgroup"
|
||||||
|
)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildAndroid(tmpDir string, bi *buildInfo) error {
|
||||||
|
sdk := os.Getenv("ANDROID_HOME")
|
||||||
|
if sdk == "" {
|
||||||
|
return errors.New("Please set ANDROID_HOME 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"),
|
||||||
|
}
|
||||||
|
if err := compileAndroid(tmpDir, tools, bi); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch *buildMode {
|
||||||
|
case "archive":
|
||||||
|
return archiveAndroid(tmpDir, bi)
|
||||||
|
case "exe":
|
||||||
|
if err := exeAndroid(tmpDir, tools, bi); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return signAPK(tmpDir, tools, bi)
|
||||||
|
default:
|
||||||
|
panic("unreachable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err error) {
|
||||||
|
androidHome := os.Getenv("ANDROID_HOME")
|
||||||
|
if androidHome == "" {
|
||||||
|
return errors.New("ANDROID_HOME is not set. Please point it to the root of the Android SDK.")
|
||||||
|
}
|
||||||
|
ndkRoot := filepath.Join(androidHome, "ndk-bundle")
|
||||||
|
if _, err := os.Stat(ndkRoot); err != nil {
|
||||||
|
return fmt.Errorf("No NDK found in $ANDROID_HOME/ndk-bundle (%s). Use `sdkmanager ndk-bundle` to install it.", ndkRoot)
|
||||||
|
}
|
||||||
|
tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK())
|
||||||
|
var builds errgroup.Group
|
||||||
|
for _, a := range bi.archs {
|
||||||
|
arch := allArchs[a]
|
||||||
|
clang := filepath.Join(tcRoot, "bin", arch.clang)
|
||||||
|
if _, err := os.Stat(clang); err != nil {
|
||||||
|
return fmt.Errorf("No NDK compiler found. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it. Path %s", clang)
|
||||||
|
}
|
||||||
|
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 := filepath.Join(tcRoot, "bin", arch.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",
|
||||||
|
"-o", libFile,
|
||||||
|
bi.pkg,
|
||||||
|
)
|
||||||
|
cmd.Env = append(
|
||||||
|
os.Environ(),
|
||||||
|
"GOOS=android",
|
||||||
|
"GOARCH="+a,
|
||||||
|
"CGO_ENABLED=1",
|
||||||
|
"CC="+clang,
|
||||||
|
"CGO_CFLAGS=-Werror",
|
||||||
|
)
|
||||||
|
builds.Go(func() error {
|
||||||
|
_, err := runCmd(cmd)
|
||||||
|
return err
|
||||||
|
})
|
||||||
|
}
|
||||||
|
appDir, err := appDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
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) (err error) {
|
||||||
|
aarFile := *destPath
|
||||||
|
if aarFile == "" {
|
||||||
|
aarFile = fmt.Sprintf("%s.aar", filepath.Base(bi.pkg))
|
||||||
|
}
|
||||||
|
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")
|
||||||
|
aarw.Create("res/")
|
||||||
|
manifest := aarw.Create("AndroidManifest.xml")
|
||||||
|
manifest.Write([]byte(`<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.gioui.app">
|
||||||
|
<uses-sdk android:minSdkVersion="16"/>
|
||||||
|
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
|
||||||
|
</manifest>`))
|
||||||
|
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) (err error) {
|
||||||
|
if *appID == "" {
|
||||||
|
return errors.New("app id is empty; use -appid to set it")
|
||||||
|
}
|
||||||
|
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
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
apkDir := filepath.Join(tmpDir, "apk")
|
||||||
|
if err := os.MkdirAll(apkDir, 0755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(classFiles) > 0 {
|
||||||
|
d8 := exec.Command(
|
||||||
|
filepath.Join(tools.buildtools, "d8"),
|
||||||
|
"--classpath", tools.androidjar,
|
||||||
|
"--output", apkDir,
|
||||||
|
)
|
||||||
|
d8.Args = append(d8.Args, classFiles...)
|
||||||
|
if _, err := runCmd(d8); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
manifestSrc := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
package="%s"
|
||||||
|
android:versionCode="1"
|
||||||
|
android:versionName="1.0">
|
||||||
|
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="28" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-feature android:glEsVersion="0x00030000"/>
|
||||||
|
<application android:label="Gio">
|
||||||
|
<activity android:name="org.gioui.GioActivity"
|
||||||
|
android:label="Gio"
|
||||||
|
android:theme="@android:style/Theme.NoTitleBar"
|
||||||
|
android:configChanges="orientation|keyboardHidden">
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
</activity>
|
||||||
|
</application>
|
||||||
|
</manifest>`, *appID)
|
||||||
|
manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
|
||||||
|
if err := ioutil.WriteFile(manifest, []byte(manifestSrc), 0600); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpapk := filepath.Join(tmpDir, "link.apk")
|
||||||
|
_, err = runCmd(exec.Command(
|
||||||
|
filepath.Join(tools.buildtools, "aapt2"),
|
||||||
|
"link",
|
||||||
|
"--manifest", manifest,
|
||||||
|
"-I", tools.androidjar,
|
||||||
|
"-o", tmpapk,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// The Go standard library archive/zip doesn't support appending to zip
|
||||||
|
// files. Unpack the apk from aapt2 and re-zip its contents along with
|
||||||
|
// classes.dex and the Go libraries.
|
||||||
|
if err := unzip(apkDir, tmpapk); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
tmpApk := filepath.Join(tmpDir, "app.ap_")
|
||||||
|
ap_, err := os.Create(tmpApk)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := ap_.Close(); err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
apkw := newZipWriter(ap_)
|
||||||
|
defer apkw.Close()
|
||||||
|
apkFiles, err := filepath.Glob(filepath.Join(apkDir, "*"))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, f := range apkFiles {
|
||||||
|
name := filepath.Base(f)
|
||||||
|
apkw.Add(name, f)
|
||||||
|
}
|
||||||
|
for _, a := range bi.archs {
|
||||||
|
arch := allArchs[a]
|
||||||
|
libFile := filepath.Join(arch.jniArch, "libgio.so")
|
||||||
|
apkw.Add(filepath.ToSlash(filepath.Join("lib", libFile)), filepath.Join(tmpDir, "jni", libFile))
|
||||||
|
}
|
||||||
|
return apkw.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func signAPK(tmpDir string, tools *androidTools, bi *buildInfo) error {
|
||||||
|
apkFile := *destPath
|
||||||
|
if apkFile == "" {
|
||||||
|
apkFile = fmt.Sprintf("%s.apk", filepath.Base(bi.pkg))
|
||||||
|
}
|
||||||
|
if filepath.Ext(apkFile) != ".apk" {
|
||||||
|
return fmt.Errorf("the specified output %q does not end in '.apk'", apkFile)
|
||||||
|
}
|
||||||
|
_, err := runCmd(exec.Command(
|
||||||
|
filepath.Join(tools.buildtools, "zipalign"),
|
||||||
|
"-f",
|
||||||
|
"4", // 32-bit alignment.
|
||||||
|
filepath.Join(tmpDir, "app.ap_"),
|
||||||
|
apkFile,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keystore := filepath.Join(home, ".android", "debug.keystore")
|
||||||
|
if _, err := os.Stat(keystore); err == nil {
|
||||||
|
_, err = runCmd(exec.Command(
|
||||||
|
filepath.Join(tools.buildtools, "apksigner"),
|
||||||
|
"sign",
|
||||||
|
"--ks-pass", "pass:android",
|
||||||
|
"--ks", keystore,
|
||||||
|
apkFile,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unzip(dir, zipfile string) (err error) {
|
||||||
|
zipr, err := zip.OpenReader(zipfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer zipr.Close()
|
||||||
|
for _, f := range zipr.File {
|
||||||
|
path := filepath.Join(dir, path.Base(f.Name))
|
||||||
|
out, err := os.Create(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if cerr := out.Close(); err == nil {
|
||||||
|
err = cerr
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
in, err := f.Open()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer in.Close()
|
||||||
|
if _, err := io.Copy(out, in); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 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 {
|
||||||
|
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
|
||||||
|
return "windows"
|
||||||
|
} else {
|
||||||
|
var arch string
|
||||||
|
switch runtime.GOARCH {
|
||||||
|
case "386":
|
||||||
|
arch = "x86"
|
||||||
|
case "amd64":
|
||||||
|
arch = "x86_64"
|
||||||
|
default:
|
||||||
|
panic("unsupported GOARCH: " + runtime.GOARCH)
|
||||||
|
}
|
||||||
|
return runtime.GOOS + "-" + arch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 latestTools(sdk string) (string, error) {
|
||||||
|
allTools, err := filepath.Glob(filepath.Join(sdk, "build-tools", "*"))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
var bestVer [3]int
|
||||||
|
var bestTools string
|
||||||
|
loop:
|
||||||
|
for _, tools := range allTools {
|
||||||
|
_, name := filepath.Split(tools)
|
||||||
|
s := strings.SplitN(name, ".", 3)
|
||||||
|
if len(s) != len(bestVer) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
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
|
||||||
|
bestTools = tools
|
||||||
|
}
|
||||||
|
if bestTools == "" {
|
||||||
|
return "", fmt.Errorf("no build-tools found in %q", sdk)
|
||||||
|
}
|
||||||
|
return bestTools, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) Add(name, file string) {
|
||||||
|
if z.err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
w := z.Create(name)
|
||||||
|
f, err := os.Open(file)
|
||||||
|
if err != nil {
|
||||||
|
z.err = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
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
|
||||||
|
}
|
||||||
+45
-314
@@ -3,9 +3,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"archive/zip"
|
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -13,32 +11,25 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"golang.org/x/sync/errgroup"
|
"golang.org/x/sync/errgroup"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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 (
|
var (
|
||||||
target = flag.String("target", "", "specify target (ios, tvos, android)")
|
target = flag.String("target", "", "specify target (ios, tvos, android)")
|
||||||
archNames = flag.String("arch", "", "specify architecture(s) to include")
|
archNames = flag.String("arch", "", "specify architecture(s) to include")
|
||||||
destPath = flag.String("o", "", "output file (for Android .aar) or directory (for iOS/tvOS .framework)")
|
buildMode = flag.String("buildmode", "archive", "specify buildmode: archive or exe")
|
||||||
|
destPath = flag.String("o", "", "output file (Android .aar or .apk file) or directory (iOS/tvOS .framework)")
|
||||||
verbose = flag.Bool("v", false, "verbose output")
|
verbose = flag.Bool("v", false, "verbose output")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type buildInfo struct {
|
||||||
|
pkg string
|
||||||
|
ldflags string
|
||||||
|
archs []string
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Usage = func() {
|
flag.Usage = func() {
|
||||||
fmt.Fprintf(os.Stderr, "Gio is a tool for building and running gio programs.\n\n")
|
fmt.Fprintf(os.Stderr, "Gio is a tool for building and running gio programs.\n\n")
|
||||||
@@ -56,6 +47,16 @@ func main() {
|
|||||||
flag.PrintDefaults()
|
flag.PrintDefaults()
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
|
switch *target {
|
||||||
|
case "ios", "tvos", "android":
|
||||||
|
default:
|
||||||
|
errorf("invalid -target %s\n", *target)
|
||||||
|
}
|
||||||
|
switch *buildMode {
|
||||||
|
case "archive", "exe":
|
||||||
|
default:
|
||||||
|
errorf("invalid -buildmode %s\n", *buildMode)
|
||||||
|
}
|
||||||
// Expand relative package paths.
|
// Expand relative package paths.
|
||||||
out, err := exec.Command("go", "list", pkg).Output()
|
out, err := exec.Command("go", "list", pkg).Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -64,49 +65,48 @@ func main() {
|
|||||||
}
|
}
|
||||||
errorf("gio: failed to run the go tool: %v", err)
|
errorf("gio: failed to run the go tool: %v", err)
|
||||||
}
|
}
|
||||||
pkg = string(bytes.TrimSpace(out))
|
bi := &buildInfo{
|
||||||
appArgs := flag.Args()[1:]
|
pkg: string(bytes.TrimSpace(out)),
|
||||||
if err := run(pkg, appArgs); err != nil {
|
}
|
||||||
|
switch *target {
|
||||||
|
case "ios", "tvos":
|
||||||
|
// Only 64-bit support.
|
||||||
|
bi.archs = []string{"arm64", "amd64"}
|
||||||
|
case "android":
|
||||||
|
bi.archs = []string{"arm", "arm64", "386", "amd64"}
|
||||||
|
}
|
||||||
|
if *archNames != "" {
|
||||||
|
bi.archs = strings.Split(*archNames, ",")
|
||||||
|
}
|
||||||
|
if appArgs := flag.Args()[1:]; len(appArgs) > 0 {
|
||||||
|
// Pass along arguments to the app.
|
||||||
|
bi.ldflags = fmt.Sprintf("-X gioui.org/ui/app.extraArgs=%s", strings.Join(appArgs, "|"))
|
||||||
|
}
|
||||||
|
if err := build(bi); err != nil {
|
||||||
errorf("gio: %v", err)
|
errorf("gio: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func run(pkg string, appArgs []string) error {
|
func build(bi *buildInfo) error {
|
||||||
tmpDir, err := ioutil.TempDir("", "gio-")
|
tmpDir, err := ioutil.TempDir("", "gio-")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
defer os.RemoveAll(tmpDir)
|
defer os.RemoveAll(tmpDir)
|
||||||
var ldflags string
|
|
||||||
if len(appArgs) > 0 {
|
|
||||||
// Pass along arguments to the app.
|
|
||||||
ldflags = fmt.Sprintf("-X gioui.org/ui/app.extraArgs=%s", strings.Join(appArgs, "|"))
|
|
||||||
}
|
|
||||||
var archs []string
|
|
||||||
switch *target {
|
switch *target {
|
||||||
case "ios", "tvos":
|
case "ios", "tvos":
|
||||||
// Only 64-bit support.
|
return archiveIOS(tmpDir, *target, bi)
|
||||||
archs = []string{"arm64", "amd64"}
|
|
||||||
case "android":
|
case "android":
|
||||||
archs = []string{"arm", "arm64", "386", "amd64"}
|
return buildAndroid(tmpDir, bi)
|
||||||
}
|
|
||||||
if *archNames != "" {
|
|
||||||
archs = strings.Split(*archNames, ",")
|
|
||||||
}
|
|
||||||
switch *target {
|
|
||||||
case "ios", "tvos":
|
|
||||||
return runIOS(tmpDir, *target, pkg, archs, ldflags)
|
|
||||||
case "android":
|
|
||||||
return runAndroid(tmpDir, pkg, archs, ldflags)
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("invalid -target %s\n", *target)
|
panic("unreachable")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func runIOS(tmpDir, target, pkg string, archs []string, ldflags string) error {
|
func archiveIOS(tmpDir, target string, bi *buildInfo) error {
|
||||||
frameworkRoot := *destPath
|
frameworkRoot := *destPath
|
||||||
if frameworkRoot == "" {
|
if frameworkRoot == "" {
|
||||||
appName := filepath.Base(pkg)
|
appName := filepath.Base(bi.pkg)
|
||||||
frameworkRoot = fmt.Sprintf("%s.framework", strings.Title(appName))
|
frameworkRoot = fmt.Sprintf("%s.framework", strings.Title(appName))
|
||||||
}
|
}
|
||||||
framework := filepath.Base(frameworkRoot)
|
framework := filepath.Base(frameworkRoot)
|
||||||
@@ -139,7 +139,7 @@ func runIOS(tmpDir, target, pkg string, archs []string, ldflags string) error {
|
|||||||
exe := filepath.Join(frameworkDir, framework)
|
exe := filepath.Join(frameworkDir, framework)
|
||||||
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
|
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
|
||||||
var builds errgroup.Group
|
var builds errgroup.Group
|
||||||
for _, a := range archs {
|
for _, a := range bi.archs {
|
||||||
arch := allArchs[a]
|
arch := allArchs[a]
|
||||||
var platformSDK string
|
var platformSDK string
|
||||||
var platformOS string
|
var platformOS string
|
||||||
@@ -175,11 +175,11 @@ func runIOS(tmpDir, target, pkg string, archs []string, ldflags string) error {
|
|||||||
cmd := exec.Command(
|
cmd := exec.Command(
|
||||||
"go",
|
"go",
|
||||||
"build",
|
"build",
|
||||||
"-ldflags=-s -w "+ldflags,
|
"-ldflags=-s -w "+bi.ldflags,
|
||||||
"-buildmode=c-archive",
|
"-buildmode=c-archive",
|
||||||
"-o", lib,
|
"-o", lib,
|
||||||
"-tags", "ios",
|
"-tags", "ios",
|
||||||
pkg,
|
bi.pkg,
|
||||||
)
|
)
|
||||||
lipo.Args = append(lipo.Args, lib)
|
lipo.Args = append(lipo.Args, lib)
|
||||||
cmd.Env = append(
|
cmd.Env = append(
|
||||||
@@ -220,192 +220,6 @@ func runIOS(tmpDir, target, pkg string, archs []string, ldflags string) error {
|
|||||||
return ioutil.WriteFile(moduleFile, []byte(module), 0644)
|
return ioutil.WriteFile(moduleFile, []byte(module), 0644)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runAndroid(tmpDir, pkg string, archs []string, ldflags string) (err error) {
|
|
||||||
androidHome := os.Getenv("ANDROID_HOME")
|
|
||||||
if androidHome == "" {
|
|
||||||
return errors.New("ANDROID_HOME is not set. Please point it to the root of the Android SDK.")
|
|
||||||
}
|
|
||||||
ndkRoot := filepath.Join(androidHome, "ndk-bundle")
|
|
||||||
if _, err := os.Stat(ndkRoot); err != nil {
|
|
||||||
return fmt.Errorf("No NDK found in $ANDROID_HOME/ndk-bundle (%s). Use `sdkmanager ndk-bundle` to install it.", ndkRoot)
|
|
||||||
}
|
|
||||||
tcRoot := filepath.Join(ndkRoot, "toolchains", "llvm", "prebuilt", archNDK())
|
|
||||||
sdk := os.Getenv("ANDROID_HOME")
|
|
||||||
if sdk == "" {
|
|
||||||
return errors.New("Please set ANDROID_HOME to the Android SDK path")
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(sdk); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
platform, err := latestPlatform(sdk)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
var builds errgroup.Group
|
|
||||||
for _, a := range archs {
|
|
||||||
arch := allArchs[a]
|
|
||||||
clang := filepath.Join(tcRoot, "bin", arch.clang)
|
|
||||||
if _, err := os.Stat(clang); err != nil {
|
|
||||||
return fmt.Errorf("No NDK compiler found. Please make sure you have NDK >= r19c installed. Use the command `sdkmanager ndk-bundle` to install it. Path %s", clang)
|
|
||||||
}
|
|
||||||
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 := filepath.Join(tcRoot, "bin", arch.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 "+ldflags,
|
|
||||||
"-buildmode=c-shared",
|
|
||||||
"-o", libFile,
|
|
||||||
pkg,
|
|
||||||
)
|
|
||||||
cmd.Env = append(
|
|
||||||
os.Environ(),
|
|
||||||
"GOOS=android",
|
|
||||||
"GOARCH="+a,
|
|
||||||
"CGO_ENABLED=1",
|
|
||||||
"CC="+clang,
|
|
||||||
"CGO_CFLAGS=-Werror",
|
|
||||||
)
|
|
||||||
builds.Go(func() error {
|
|
||||||
_, err := runCmd(cmd)
|
|
||||||
return err
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if err := builds.Wait(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
aarFile := *destPath
|
|
||||||
if aarFile == "" {
|
|
||||||
aarFile = fmt.Sprintf("%s.aar", filepath.Base(pkg))
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
aarw.Create("res/")
|
|
||||||
manifest := aarw.Create("AndroidManifest.xml")
|
|
||||||
manifest.Write([]byte(`<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.gioui.app">
|
|
||||||
<uses-sdk android:minSdkVersion="16"/>
|
|
||||||
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
|
|
||||||
</manifest>`))
|
|
||||||
proguard := aarw.Create("proguard.txt")
|
|
||||||
proguard.Write([]byte(`-keep class org.gioui.** { *; }`))
|
|
||||||
|
|
||||||
for _, a := range archs {
|
|
||||||
arch := allArchs[a]
|
|
||||||
libFile := filepath.Join("jni", arch.jniArch, "libgio.so")
|
|
||||||
aarw.Add(filepath.ToSlash(libFile), filepath.Join(tmpDir, libFile))
|
|
||||||
}
|
|
||||||
appDir, err := appDir()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
javaFiles, err := filepath.Glob(filepath.Join(appDir, "*.java"))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if len(javaFiles) > 0 {
|
|
||||||
clsPath := filepath.Join(platform, "android.jar")
|
|
||||||
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", clsPath,
|
|
||||||
"-d", classes,
|
|
||||||
)
|
|
||||||
javac.Args = append(javac.Args, javaFiles...)
|
|
||||||
if _, err := runCmd(javac); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
jarFile := filepath.Join(tmpDir, "classes.jar")
|
|
||||||
if err := writeJar(jarFile, classes); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
aarw.Add("classes.jar", jarFile)
|
|
||||||
}
|
|
||||||
return aarw.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
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) Add(name, file string) {
|
|
||||||
if z.err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w := z.Create(name)
|
|
||||||
f, err := os.Open(file)
|
|
||||||
if err != nil {
|
|
||||||
z.err = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
func errorf(format string, args ...interface{}) {
|
func errorf(format string, args ...interface{}) {
|
||||||
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
fmt.Fprintf(os.Stderr, format+"\n", args...)
|
||||||
os.Exit(2)
|
os.Exit(2)
|
||||||
@@ -446,10 +260,6 @@ func copyFile(dst, src string) (err error) {
|
|||||||
|
|
||||||
func appDir() (string, error) {
|
func appDir() (string, error) {
|
||||||
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/ui/app")
|
cmd := exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/ui/app")
|
||||||
cmd.Env = append(
|
|
||||||
os.Environ(),
|
|
||||||
"GOOS=android",
|
|
||||||
)
|
|
||||||
out, err := runCmd(cmd)
|
out, err := runCmd(cmd)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@@ -458,41 +268,6 @@ func appDir() (string, error) {
|
|||||||
return appDir, nil
|
return appDir, 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()
|
|
||||||
}
|
|
||||||
|
|
||||||
type arch struct {
|
type arch struct {
|
||||||
iosArch string
|
iosArch string
|
||||||
jniArch string
|
jniArch string
|
||||||
@@ -521,47 +296,3 @@ var allArchs = map[string]arch{
|
|||||||
clang: "x86_64-linux-android21-clang",
|
clang: "x86_64-linux-android21-clang",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func archNDK() string {
|
|
||||||
if runtime.GOOS == "windows" && runtime.GOARCH == "386" {
|
|
||||||
return "windows"
|
|
||||||
} else {
|
|
||||||
var arch string
|
|
||||||
switch runtime.GOARCH {
|
|
||||||
case "386":
|
|
||||||
arch = "x86"
|
|
||||||
case "amd64":
|
|
||||||
arch = "x86_64"
|
|
||||||
default:
|
|
||||||
panic("unsupported GOARCH: " + runtime.GOARCH)
|
|
||||||
}
|
|
||||||
return runtime.GOOS + "-" + arch
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user