mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-06 01:45:36 +00:00
cmd/gogio: rename the gio too to gogio
The `gio` name clashes with a widely deployed GNOME tool. Rename our tool to `gogio`, "the go tool for gio programs". Fixes gio#20 Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
@@ -0,0 +1,623 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"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 := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/ui/app"))
|
||||
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", 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")
|
||||
aarw.Create("res/")
|
||||
manifest := aarw.Create("AndroidManifest.xml")
|
||||
manifest.Write([]byte(fmt.Sprintf(`<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="%s">
|
||||
<uses-sdk android:minSdkVersion="16"/>
|
||||
<uses-feature android:glEsVersion="0x00030000" android:required="true" />
|
||||
</manifest>`, bi.appID)))
|
||||
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) {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
// Compile resources.
|
||||
resDir := filepath.Join(tmpDir, "res")
|
||||
valDir := filepath.Join(resDir, "values")
|
||||
v21Dir := filepath.Join(resDir, "values-v21")
|
||||
for _, dir := range []string{valDir, v21Dir} {
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
icon := filepath.Join(bi.dir, "appicon.png")
|
||||
iconSnip := ""
|
||||
if _, err := os.Stat(icon); err == nil {
|
||||
err := buildIcons(resDir, icon, []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},
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
iconSnip = `android:icon="@mipmap/ic_launcher"`
|
||||
}
|
||||
themes := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.GioApp" parent="android:style/Theme.NoTitleBar">
|
||||
</style>
|
||||
</resources>`
|
||||
err = ioutil.WriteFile(filepath.Join(valDir, "themes.xml"), []byte(themes), 0660)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
themesV21 := `<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources>
|
||||
<style name="Theme.GioApp" parent="android:style/Theme.NoTitleBar">
|
||||
<item name="android:windowDrawsSystemBarBackgrounds">true</item>
|
||||
<item name="android:navigationBarColor">#40000000</item>
|
||||
<item name="android:statusBarColor">#40000000</item>
|
||||
</style>
|
||||
</resources>`
|
||||
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.
|
||||
appName := strings.Title(bi.name)
|
||||
manifestSrc := fmt.Sprintf(`<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="%s"
|
||||
android:versionCode="%d"
|
||||
android:versionName="1.0.%d">
|
||||
<uses-sdk android:minSdkVersion="16" android:targetSdkVersion="28" />
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-feature android:glEsVersion="0x00030000"/>
|
||||
<application %s android:label="%s">
|
||||
<activity android:name="org.gioui.GioActivity"
|
||||
android:label="%s"
|
||||
android:theme="@style/Theme.GioApp"
|
||||
android:configChanges="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>`, bi.appID, bi.version, bi.version, iconSnip, appName, appName)
|
||||
manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
|
||||
if err := ioutil.WriteFile(manifest, []byte(manifestSrc), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
tmpapk := filepath.Join(tmpDir, "link.apk")
|
||||
link := exec.Command(
|
||||
aapt2,
|
||||
"link",
|
||||
"--manifest", manifest,
|
||||
"-I", tools.androidjar,
|
||||
"-o", tmpapk,
|
||||
resZip,
|
||||
)
|
||||
if _, err := runCmd(link); 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()
|
||||
err = filepath.Walk(apkDir, func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
zpath := path[len(apkDir)+1:]
|
||||
if filepath.Base(path) == "resources.arsc" {
|
||||
apkw.Store(zpath, path)
|
||||
} else {
|
||||
apkw.Add(zpath, path)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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", bi.name)
|
||||
}
|
||||
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")
|
||||
_, 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, f.Name)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
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) 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
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
/*
|
||||
The gogio tool builds and packages Gio programs for Android, iOS/tvOS
|
||||
and WebAssembly.
|
||||
|
||||
Run gogio with no arguments for instructions, or see the examples at
|
||||
https://gioui.org.
|
||||
*/
|
||||
package main
|
||||
@@ -0,0 +1,286 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
var (
|
||||
target = flag.String("target", "", "specify target (ios, tvos, android, js).\n")
|
||||
archNames = flag.String("arch", "", "specify architecture(s) to include (arm, arm64, amd64).")
|
||||
buildMode = flag.String("buildmode", "exe", "specify buildmode (archive, exe)")
|
||||
destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.")
|
||||
appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
|
||||
version = flag.Int("version", 1, "app version (for -buildmode=exe)")
|
||||
printCommands = flag.Bool("x", false, "print the commands")
|
||||
keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.")
|
||||
)
|
||||
|
||||
type buildInfo struct {
|
||||
name string
|
||||
pkg string
|
||||
ldflags string
|
||||
target string
|
||||
appID string
|
||||
version int
|
||||
dir string
|
||||
archs []string
|
||||
}
|
||||
|
||||
func main() {
|
||||
flag.Usage = func() {
|
||||
fmt.Fprintf(os.Stderr, mainUsage)
|
||||
}
|
||||
flag.Parse()
|
||||
if err := mainErr(); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "gogio: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func mainErr() error {
|
||||
pkg := flag.Arg(0)
|
||||
if pkg == "" {
|
||||
return errors.New("specify a package")
|
||||
}
|
||||
if *target == "" {
|
||||
return errors.New("please specify -target")
|
||||
}
|
||||
switch *target {
|
||||
case "ios", "tvos", "android", "js":
|
||||
default:
|
||||
return fmt.Errorf("invalid -target %s", *target)
|
||||
}
|
||||
switch *buildMode {
|
||||
case "archive", "exe":
|
||||
default:
|
||||
return fmt.Errorf("invalid -buildmode %s", *buildMode)
|
||||
}
|
||||
// Find package name.
|
||||
pkgPath, err := runCmd(exec.Command("go", "list", "-f", "{{.ImportPath}}", pkg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
dir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", pkg))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
elems := strings.Split(pkgPath, "/")
|
||||
name := elems[len(elems)-1]
|
||||
bi := &buildInfo{
|
||||
name: name,
|
||||
pkg: pkg,
|
||||
target: *target,
|
||||
appID: *appID,
|
||||
dir: dir,
|
||||
version: *version,
|
||||
}
|
||||
if bi.appID == "" {
|
||||
bi.appID = appIDFromPackage(pkgPath)
|
||||
}
|
||||
|
||||
switch *target {
|
||||
case "js":
|
||||
bi.archs = []string{"wasm"}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func appIDFromPackage(pkgPath string) string {
|
||||
elems := strings.Split(pkgPath, "/")
|
||||
domain := strings.Split(elems[0], ".")
|
||||
name := ""
|
||||
if len(elems) > 1 {
|
||||
name = "." + elems[len(elems)-1]
|
||||
}
|
||||
if len(elems) < 2 && len(domain) < 2 {
|
||||
name = "." + domain[0]
|
||||
domain[0] = "localhost"
|
||||
} else {
|
||||
for i := 0; i < len(domain)/2; i++ {
|
||||
opp := len(domain) - 1 - i
|
||||
domain[i], domain[opp] = domain[opp], domain[i]
|
||||
}
|
||||
}
|
||||
|
||||
pkgDomain := strings.Join(domain, ".")
|
||||
appid := []rune(pkgDomain + name)
|
||||
|
||||
// a Java-language-style package name may contain upper- and lower-case
|
||||
// letters and underscores with individual parts separated by '.'.
|
||||
// https://developer.android.com/guide/topics/manifest/manifest-element
|
||||
for i, c := range appid {
|
||||
if !('a' <= c && c <= 'z' || 'A' <= c && c <= 'Z' ||
|
||||
c == '_' || c == '.') {
|
||||
appid[i] = '_'
|
||||
}
|
||||
}
|
||||
return string(appid)
|
||||
}
|
||||
|
||||
func build(bi *buildInfo) error {
|
||||
tmpDir, err := ioutil.TempDir("", "gogio-")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if *keepWorkdir {
|
||||
fmt.Fprintf(os.Stderr, "WORKDIR=%s\n", tmpDir)
|
||||
} else {
|
||||
defer os.RemoveAll(tmpDir)
|
||||
}
|
||||
switch *target {
|
||||
case "js":
|
||||
return buildJS(bi)
|
||||
case "ios", "tvos":
|
||||
return buildIOS(tmpDir, *target, bi)
|
||||
case "android":
|
||||
return buildAndroid(tmpDir, bi)
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func runCmdRaw(cmd *exec.Cmd) ([]byte, error) {
|
||||
if *printCommands {
|
||||
fmt.Printf("%s\n", strings.Join(cmd.Args, " "))
|
||||
}
|
||||
out, err := cmd.Output()
|
||||
if err == nil {
|
||||
return out, nil
|
||||
}
|
||||
if err, ok := err.(*exec.ExitError); ok {
|
||||
return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
func runCmd(cmd *exec.Cmd) (string, error) {
|
||||
out, err := runCmdRaw(cmd)
|
||||
return string(bytes.TrimSpace(out)), err
|
||||
}
|
||||
|
||||
func copyFile(dst, src string) (err error) {
|
||||
r, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer r.Close()
|
||||
w, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := w.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
_, err = io.Copy(w, r)
|
||||
return err
|
||||
}
|
||||
|
||||
type arch struct {
|
||||
iosArch string
|
||||
jniArch string
|
||||
clang string
|
||||
}
|
||||
|
||||
var allArchs = map[string]arch{
|
||||
"arm": arch{
|
||||
iosArch: "armv7",
|
||||
jniArch: "armeabi-v7a",
|
||||
clang: "armv7a-linux-androideabi16-clang",
|
||||
},
|
||||
"arm64": arch{
|
||||
iosArch: "arm64",
|
||||
jniArch: "arm64-v8a",
|
||||
clang: "aarch64-linux-android21-clang",
|
||||
},
|
||||
"386": arch{
|
||||
iosArch: "i386",
|
||||
jniArch: "x86",
|
||||
clang: "i686-linux-android16-clang",
|
||||
},
|
||||
"amd64": arch{
|
||||
iosArch: "x86_64",
|
||||
jniArch: "x86_64",
|
||||
clang: "x86_64-linux-android21-clang",
|
||||
},
|
||||
}
|
||||
|
||||
type iconVariant struct {
|
||||
path string
|
||||
size int
|
||||
fill bool
|
||||
}
|
||||
|
||||
func buildIcons(baseDir, icon string, variants []iconVariant) error {
|
||||
f, err := os.Open(icon)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var resizes errgroup.Group
|
||||
for _, v := range variants {
|
||||
v := v
|
||||
resizes.Go(func() (err error) {
|
||||
scaled := image.NewNRGBA(image.Rectangle{Max: image.Point{X: v.size, Y: v.size}})
|
||||
op := draw.Src
|
||||
if v.fill {
|
||||
op = draw.Over
|
||||
draw.Draw(scaled, scaled.Bounds(), &image.Uniform{color.White}, image.Point{}, draw.Src)
|
||||
}
|
||||
draw.CatmullRom.Scale(scaled, scaled.Bounds(), img, img.Bounds(), op, nil)
|
||||
path := filepath.Join(baseDir, v.path)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.Create(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
return png.Encode(f, scaled)
|
||||
})
|
||||
}
|
||||
return resizes.Wait()
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
type expval struct {
|
||||
in, out string
|
||||
}
|
||||
|
||||
func TestAppID(t *testing.T) {
|
||||
tests := []expval{
|
||||
{"example", "localhost.example"},
|
||||
{"example.com", "com.example"},
|
||||
{"www.example.com", "com.example.www"},
|
||||
{"examplecom/app", "examplecom.app"},
|
||||
{"example.com/app", "com.example.app"},
|
||||
{"www.example.com/app", "com.example.www.app"},
|
||||
{"www.en.example.com/app", "com.example.en.www.app"},
|
||||
{"example.com/dir/app", "com.example.app"},
|
||||
{"example.com/dir.ext/app", "com.example.app"},
|
||||
{"example.com/dir/app.ext", "com.example.app.ext"},
|
||||
{"example-com.net/dir/app", "net.example_com.app"},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
got := appIDFromPackage(test.in)
|
||||
if exp := test.out; got != exp {
|
||||
t.Errorf("(%d): expected '%s', got '%s'", i, exp, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
const mainUsage = `The gogio command builds and packages Gio (gioui.org) programs.
|
||||
|
||||
Usage:
|
||||
|
||||
gogio -target <target> [flags] <package> [run arguments]
|
||||
|
||||
The go tool is sufficient to build, install and run Gio programs on platforms
|
||||
where a single executable is sufficient. The gogio tool can build and package Gio
|
||||
programs for platforms where additional metadata or support files are required.
|
||||
|
||||
The package argument specifies an import path or a single Go source file to
|
||||
package. Any run arguments are appended to os.Args at runtime.
|
||||
|
||||
If the package contains an appicon.png file, it is used as the app icon on
|
||||
supported platforms.
|
||||
|
||||
The mandatory -target flag selects the target platform: ios or android for the
|
||||
mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL.
|
||||
|
||||
The -arch flag specifies a comma separated list of GOARCHs to include. The
|
||||
default is all supported architectures.
|
||||
|
||||
The -o flag specifies an output file or directory, depending on the target.
|
||||
|
||||
The -buildmode flag selects the build mode. Two build modes are available, exe
|
||||
and archive. Buildmode exe outputs an .ipa file for iOS or tvOS, an .apk file
|
||||
for Android or a directory with the WebAssembly module and support files for
|
||||
a browser.
|
||||
|
||||
As a special case for iOS or tvOS, specifying a path that ends with ".app"
|
||||
will output an app directory suitable for a simulator.
|
||||
|
||||
The other buildmode is archive, which will output an .aar library for Android
|
||||
or a .framework for iOS and tvOS.
|
||||
|
||||
The -appid flag specifies the package name for Android or the bundle id for
|
||||
iOS and tvOS. A bundle id must be provisioned through Xcode before the gogio
|
||||
tool can use it.
|
||||
|
||||
The -version flag specifies the integer version for Android and the last
|
||||
component of the 1.0.X version for iOS and tvOS.
|
||||
|
||||
The -work flag prints the path to the working directory and suppress
|
||||
its deletion.
|
||||
|
||||
The -x flag will print all the external commands executed by the gogio tool.
|
||||
`
|
||||
@@ -0,0 +1,528 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"golang.org/x/sync/errgroup"
|
||||
)
|
||||
|
||||
const minIOSVersion = "9.0"
|
||||
|
||||
func buildIOS(tmpDir, target string, bi *buildInfo) error {
|
||||
appName := bi.name
|
||||
switch *buildMode {
|
||||
case "archive":
|
||||
framework := *destPath
|
||||
if framework == "" {
|
||||
framework = fmt.Sprintf("%s.framework", strings.Title(appName))
|
||||
}
|
||||
return archiveIOS(tmpDir, target, framework, bi)
|
||||
case "exe":
|
||||
out := *destPath
|
||||
if out == "" {
|
||||
out = appName + ".ipa"
|
||||
}
|
||||
forDevice := strings.HasSuffix(out, ".ipa")
|
||||
// Filter out unsupported architectures.
|
||||
for i := len(bi.archs) - 1; i >= 0; i-- {
|
||||
switch bi.archs[i] {
|
||||
case "arm", "arm64":
|
||||
if forDevice {
|
||||
continue
|
||||
}
|
||||
case "386", "amd64":
|
||||
if !forDevice {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
bi.archs = append(bi.archs[:i], bi.archs[i+1:]...)
|
||||
}
|
||||
tmpFramework := filepath.Join(tmpDir, "Gio.framework")
|
||||
if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil {
|
||||
return err
|
||||
}
|
||||
if !forDevice && !strings.HasSuffix(out, ".app") {
|
||||
return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", out)
|
||||
}
|
||||
if !forDevice {
|
||||
return exeIOS(tmpDir, target, out, bi)
|
||||
}
|
||||
payload := filepath.Join(tmpDir, "Payload")
|
||||
appDir := filepath.Join(payload, appName+".app")
|
||||
if err := os.MkdirAll(appDir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := exeIOS(tmpDir, target, appDir, bi); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := signIOS(bi, tmpDir, appDir); err != nil {
|
||||
return err
|
||||
}
|
||||
return zipDir(out, tmpDir, "Payload")
|
||||
default:
|
||||
panic("unreachable")
|
||||
}
|
||||
}
|
||||
|
||||
func signIOS(bi *buildInfo, tmpDir, app string) error {
|
||||
home, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provPattern := filepath.Join(home, "Library", "MobileDevice", "Provisioning Profiles", "*.mobileprovision")
|
||||
provisions, err := filepath.Glob(provPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provInfo := filepath.Join(tmpDir, "provision.plist")
|
||||
var avail []string
|
||||
for _, prov := range provisions {
|
||||
// Decode the provision file to a plist.
|
||||
_, err := runCmd(exec.Command("security", "cms", "-D", "-i", prov, "-o", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expUnix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ExpirationDate", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exp, err := time.Parse(time.UnixDate, expUnix)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: failed to parse expiration date from %q: %v", prov, err)
|
||||
}
|
||||
if exp.Before(time.Now()) {
|
||||
continue
|
||||
}
|
||||
appIDPrefix, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:ApplicationIdentifierPrefix:0", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:Entitlements:application-identifier", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID)
|
||||
avail = append(avail, provAppID)
|
||||
if expAppID != provAppID {
|
||||
continue
|
||||
}
|
||||
// Copy provisioning file.
|
||||
embedded := filepath.Join(app, "embedded.mobileprovision")
|
||||
if err := copyFile(embedded, prov); err != nil {
|
||||
return err
|
||||
}
|
||||
certDER, err := runCmdRaw(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:DeveloperCertificates:0", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Omit trailing newline.
|
||||
certDER = certDER[:len(certDER)-1]
|
||||
cert, err := x509.ParseCertificate(certDER)
|
||||
if err != nil {
|
||||
return fmt.Errorf("sign: failed to parse developer certificate from %q: %v", prov, err)
|
||||
}
|
||||
entitlements, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-x", "-c", "Print:Entitlements", provInfo))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
entFile := filepath.Join(tmpDir, "entitlements.plist")
|
||||
if err := ioutil.WriteFile(entFile, []byte(entitlements), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
signIdentity := cert.Subject.CommonName
|
||||
_, err = runCmd(exec.Command("codesign", "-s", signIdentity, "--entitlements", entFile, app))
|
||||
return err
|
||||
}
|
||||
return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", bi.appID, avail)
|
||||
}
|
||||
|
||||
func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
|
||||
if bi.appID == "" {
|
||||
return errors.New("app id is empty; use -appid to set it")
|
||||
}
|
||||
if err := os.RemoveAll(app); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Mkdir(app, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
mainm := filepath.Join(tmpDir, "main.m")
|
||||
const mainmSrc = `@import UIKit;
|
||||
@import Gio;
|
||||
|
||||
int main(int argc, char * argv[]) {
|
||||
@autoreleasepool {
|
||||
return UIApplicationMain(argc, argv, nil, NSStringFromClass([GioAppDelegate class]));
|
||||
}
|
||||
}`
|
||||
if err := ioutil.WriteFile(mainm, []byte(mainmSrc), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
appName := strings.Title(bi.name)
|
||||
exe := filepath.Join(app, appName)
|
||||
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
|
||||
var builds errgroup.Group
|
||||
for _, a := range bi.archs {
|
||||
clang, cflags, err := iosCompilerFor(target, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
exeSlice := filepath.Join(tmpDir, "app-"+a)
|
||||
lipo.Args = append(lipo.Args, exeSlice)
|
||||
compile := exec.Command(clang, cflags...)
|
||||
compile.Args = append(compile.Args,
|
||||
"-F", tmpDir,
|
||||
"-o", exeSlice,
|
||||
mainm,
|
||||
)
|
||||
builds.Go(func() error {
|
||||
_, err := runCmd(compile)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := builds.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runCmd(lipo); err != nil {
|
||||
return err
|
||||
}
|
||||
infoPlist := buildInfoPlist(bi)
|
||||
plistFile := filepath.Join(app, "Info.plist")
|
||||
if err := ioutil.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil {
|
||||
return err
|
||||
}
|
||||
icon := filepath.Join(bi.dir, "appicon.png")
|
||||
if _, err := os.Stat(icon); err == nil {
|
||||
assetPlist, err := iosIcons(bi, tmpDir, app, icon)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Merge assets plist with Info.plist
|
||||
cmd := exec.Command(
|
||||
"/usr/libexec/PlistBuddy",
|
||||
"-c", "Merge "+assetPlist,
|
||||
plistFile,
|
||||
)
|
||||
if _, err := runCmd(cmd); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", plistFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// iosIcons builds an asset catalog and compile it with the Xcode command actool.
|
||||
// iosIcons returns the asset plist file to be merged into Info.plist.
|
||||
func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
|
||||
assets := filepath.Join(tmpDir, "Assets.xcassets")
|
||||
if err := os.Mkdir(assets, 0700); err != nil {
|
||||
return "", err
|
||||
}
|
||||
appIcon := filepath.Join(assets, "AppIcon.appiconset")
|
||||
err := buildIcons(appIcon, icon, []iconVariant{
|
||||
{path: "ios_2x.png", size: 120},
|
||||
{path: "ios_3x.png", size: 180},
|
||||
// The App Store icon is not allowed to contain
|
||||
// transparent pixels.
|
||||
{path: "ios_store.png", size: 1024, fill: true},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
contentJson := `{
|
||||
"images" : [
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "ios_2x.png",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"size" : "60x60",
|
||||
"idiom" : "iphone",
|
||||
"filename" : "ios_3x.png",
|
||||
"scale" : "3x"
|
||||
},
|
||||
{
|
||||
"size" : "1024x1024",
|
||||
"idiom" : "ios-marketing",
|
||||
"filename" : "ios_store.png",
|
||||
"scale" : "1x"
|
||||
}
|
||||
]
|
||||
}`
|
||||
contentFile := filepath.Join(appIcon, "Contents.json")
|
||||
if err := ioutil.WriteFile(contentFile, []byte(contentJson), 0600); err != nil {
|
||||
return "", err
|
||||
}
|
||||
assetPlist := filepath.Join(tmpDir, "assets.plist")
|
||||
compile := exec.Command(
|
||||
"actool",
|
||||
"--compile", appDir,
|
||||
"--platform", iosPlatformFor(bi.target),
|
||||
"--minimum-deployment-target", minIOSVersion,
|
||||
"--app-icon", "AppIcon",
|
||||
"--output-partial-info-plist", assetPlist,
|
||||
assets)
|
||||
_, err = runCmd(compile)
|
||||
return assetPlist, err
|
||||
}
|
||||
|
||||
func buildInfoPlist(bi *buildInfo) string {
|
||||
appName := strings.Title(bi.name)
|
||||
platform := iosPlatformFor(bi.target)
|
||||
var supportPlatform string
|
||||
switch bi.target {
|
||||
case "ios":
|
||||
supportPlatform = "iPhoneOS"
|
||||
case "tvos":
|
||||
supportPlatform = "AppleTVOS"
|
||||
}
|
||||
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleDevelopmentRegion</key>
|
||||
<string>en</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundleInfoDictionaryVersion</key>
|
||||
<string>6.0</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>%s</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>1.0.%d</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>%d</string>
|
||||
<key>UILaunchStoryboardName</key>
|
||||
<string>LaunchScreen</string>
|
||||
<key>UIRequiredDeviceCapabilities</key>
|
||||
<array><string>arm64</string></array>
|
||||
<key>DTPlatformName</key>
|
||||
<string>%s</string>
|
||||
<key>DTPlatformVersion</key>
|
||||
<string>12.4</string>
|
||||
<key>MinimumOSVersion</key>
|
||||
<string>%s</string>
|
||||
<key>UIDeviceFamily</key>
|
||||
<array>
|
||||
<integer>1</integer>
|
||||
</array>
|
||||
<key>CFBundleSupportedPlatforms</key>
|
||||
<array>
|
||||
<string>%s</string>
|
||||
</array>
|
||||
<key>UISupportedInterfaceOrientations</key>
|
||||
<array>
|
||||
<string>UIInterfaceOrientationPortrait</string>
|
||||
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||
</array>
|
||||
<key>DTCompiler</key>
|
||||
<string>com.apple.compilers.llvm.clang.1_0</string>
|
||||
<key>DTPlatformBuild</key>
|
||||
<string>16G73</string>
|
||||
<key>DTSDKBuild</key>
|
||||
<string>16G73</string>
|
||||
<key>DTSDKName</key>
|
||||
<string>%s12.4</string>
|
||||
<key>DTXcode</key>
|
||||
<string>1030</string>
|
||||
<key>DTXcodeBuild</key>
|
||||
<string>10G8</string>
|
||||
</dict>
|
||||
</plist>`, appName, bi.appID, appName, bi.version, bi.version, platform, minIOSVersion, supportPlatform, platform)
|
||||
}
|
||||
|
||||
func iosPlatformFor(target string) string {
|
||||
switch target {
|
||||
case "ios":
|
||||
return "iphoneos"
|
||||
case "tvos":
|
||||
return "appletvos"
|
||||
default:
|
||||
panic("invalid platform " + target)
|
||||
}
|
||||
}
|
||||
|
||||
func archiveIOS(tmpDir, target, frameworkRoot string, bi *buildInfo) error {
|
||||
framework := filepath.Base(frameworkRoot)
|
||||
const suf = ".framework"
|
||||
if !strings.HasSuffix(framework, suf) {
|
||||
return fmt.Errorf("the specified output %q does not end in '.framework'", frameworkRoot)
|
||||
}
|
||||
framework = framework[:len(framework)-len(suf)]
|
||||
if err := os.RemoveAll(frameworkRoot); err != nil {
|
||||
return err
|
||||
}
|
||||
frameworkDir := filepath.Join(frameworkRoot, "Versions", "A")
|
||||
for _, dir := range []string{"Headers", "Modules"} {
|
||||
p := filepath.Join(frameworkDir, dir)
|
||||
if err := os.MkdirAll(p, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
symlinks := [][2]string{
|
||||
{"Versions/Current/Headers", "Headers"},
|
||||
{"Versions/Current/Modules", "Modules"},
|
||||
{"Versions/Current/" + framework, framework},
|
||||
{"A", filepath.Join("Versions", "Current")},
|
||||
}
|
||||
for _, l := range symlinks {
|
||||
if err := os.Symlink(l[0], filepath.Join(frameworkRoot, l[1])); err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
exe := filepath.Join(frameworkDir, framework)
|
||||
lipo := exec.Command("xcrun", "lipo", "-o", exe, "-create")
|
||||
var builds errgroup.Group
|
||||
for _, a := range bi.archs {
|
||||
clang, cflags, err := iosCompilerFor(target, a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lib := filepath.Join(tmpDir, "gio-"+a)
|
||||
cmd := exec.Command(
|
||||
"go",
|
||||
"build",
|
||||
"-ldflags=-s -w "+bi.ldflags,
|
||||
"-buildmode=c-archive",
|
||||
"-o", lib,
|
||||
"-tags", "ios",
|
||||
bi.pkg,
|
||||
)
|
||||
lipo.Args = append(lipo.Args, lib)
|
||||
cflagsLine := strings.Join(cflags, " ")
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GOOS=darwin",
|
||||
"GOARCH="+a,
|
||||
"CGO_ENABLED=1",
|
||||
"CC="+clang,
|
||||
"CGO_CFLAGS="+cflagsLine,
|
||||
"CGO_LDFLAGS="+cflagsLine,
|
||||
)
|
||||
builds.Go(func() error {
|
||||
_, err := runCmd(cmd)
|
||||
return err
|
||||
})
|
||||
}
|
||||
if err := builds.Wait(); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := runCmd(lipo); err != nil {
|
||||
return err
|
||||
}
|
||||
appDir, err := runCmd(exec.Command("go", "list", "-f", "{{.Dir}}", "gioui.org/ui/app"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
headerDst := filepath.Join(frameworkDir, "Headers", framework+".h")
|
||||
headerSrc := filepath.Join(appDir, "framework_ios.h")
|
||||
if err := copyFile(headerDst, headerSrc); err != nil {
|
||||
return err
|
||||
}
|
||||
module := fmt.Sprintf(`framework module "%s" {
|
||||
header "%[1]s.h"
|
||||
|
||||
export *
|
||||
}`, framework)
|
||||
moduleFile := filepath.Join(frameworkDir, "Modules", "module.modulemap")
|
||||
return ioutil.WriteFile(moduleFile, []byte(module), 0644)
|
||||
}
|
||||
|
||||
func iosCompilerFor(target, arch string) (string, []string, error) {
|
||||
var platformSDK string
|
||||
var platformOS string
|
||||
switch target {
|
||||
case "ios":
|
||||
platformOS = "ios"
|
||||
platformSDK = "iphone"
|
||||
case "tvos":
|
||||
platformOS = "tvos"
|
||||
platformSDK = "appletv"
|
||||
}
|
||||
switch arch {
|
||||
case "arm", "arm64":
|
||||
platformSDK += "os"
|
||||
case "386", "amd64":
|
||||
platformOS += "-simulator"
|
||||
platformSDK += "simulator"
|
||||
default:
|
||||
return "", nil, fmt.Errorf("unsupported -arch: %s", arch)
|
||||
}
|
||||
sdkPath, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--show-sdk-path"))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
clang, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", "clang"))
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
cflags := []string{
|
||||
"-fmodules",
|
||||
"-fobjc-arc",
|
||||
"-fembed-bitcode",
|
||||
"-Werror",
|
||||
"-arch", allArchs[arch].iosArch,
|
||||
"-isysroot", sdkPath,
|
||||
"-m" + platformOS + "-version-min=" + minIOSVersion,
|
||||
}
|
||||
return clang, cflags, nil
|
||||
}
|
||||
|
||||
func zipDir(dst, base, dir string) (err error) {
|
||||
f, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := f.Close(); err == nil {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
zipf := zip.NewWriter(f)
|
||||
err = filepath.Walk(filepath.Join(base, dir), func(path string, f os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if f.IsDir() {
|
||||
return nil
|
||||
}
|
||||
rel := filepath.ToSlash(path[len(base)+1:])
|
||||
entry, err := zipf.Create(rel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
src, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer src.Close()
|
||||
_, err = io.Copy(entry, src)
|
||||
return err
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return zipf.Close()
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Unlicense OR MIT
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func buildJS(bi *buildInfo) error {
|
||||
out := *destPath
|
||||
if out == "" {
|
||||
out = bi.name
|
||||
}
|
||||
if err := os.MkdirAll(out, 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
cmd := exec.Command(
|
||||
"go",
|
||||
"build",
|
||||
"-ldflags="+bi.ldflags,
|
||||
"-o", filepath.Join(out, "main.wasm"),
|
||||
bi.pkg,
|
||||
)
|
||||
cmd.Env = append(
|
||||
os.Environ(),
|
||||
"GOOS=js",
|
||||
"GOARCH=wasm",
|
||||
)
|
||||
_, err := runCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
const indexhtml = `<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, user-scalable=no">
|
||||
<meta name="mobile-web-app-capable" content="yes">
|
||||
<script src="wasm_exec.js"></script>
|
||||
<script>
|
||||
if (!WebAssembly.instantiateStreaming) { // polyfill
|
||||
WebAssembly.instantiateStreaming = async (resp, importObject) => {
|
||||
const source = await (await resp).arrayBuffer();
|
||||
return await WebAssembly.instantiate(source, importObject);
|
||||
};
|
||||
}
|
||||
|
||||
const go = new Go();
|
||||
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
|
||||
go.run(result.instance);
|
||||
});
|
||||
</script>
|
||||
<style>
|
||||
body,pre { margin:0;padding:0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
</body>
|
||||
</html>`
|
||||
if err := ioutil.WriteFile(filepath.Join(out, "index.html"), []byte(indexhtml), 0600); err != nil {
|
||||
return err
|
||||
}
|
||||
goroot, err := runCmd(exec.Command("go", "env", "GOROOT"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
wasmjs := filepath.Join(goroot, "misc", "wasm", "wasm_exec.js")
|
||||
if _, err := os.Stat(wasmjs); err != nil {
|
||||
return fmt.Errorf("failed to find $GOROOT/misc/wasm/wasm_exec.js driver: %v", err)
|
||||
}
|
||||
return copyFile(filepath.Join(out, "wasm_exec.js"), wasmjs)
|
||||
}
|
||||
Reference in New Issue
Block a user