cmd/gio: add iOS/tvOS support for -buildmode=exe

Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
Elias Naur
2019-06-08 15:10:31 +02:00
parent c3697fd189
commit 3c345a67b8
2 changed files with 383 additions and 125 deletions
+11 -125
View File
@@ -10,10 +10,7 @@ import (
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/sync/errgroup"
)
var (
@@ -21,6 +18,7 @@ var (
archNames = flag.String("arch", "", "specify architecture(s) to include")
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)")
appID = flag.String("appid", "org.gioui.app", "app identifier (for -buildmode=exe)")
verbose = flag.Bool("v", false, "verbose output")
)
@@ -92,7 +90,7 @@ func build(bi *buildInfo) error {
defer os.RemoveAll(tmpDir)
switch *target {
case "ios", "tvos":
return archiveIOS(tmpDir, *target, bi)
return buildIOS(tmpDir, *target, bi)
case "android":
return buildAndroid(tmpDir, bi)
default:
@@ -100,140 +98,28 @@ func build(bi *buildInfo) error {
}
}
func archiveIOS(tmpDir, target string, bi *buildInfo) error {
frameworkRoot := *destPath
if frameworkRoot == "" {
appName := filepath.Base(bi.pkg)
frameworkRoot = fmt.Sprintf("%s.framework", strings.Title(appName))
}
framework := filepath.Base(frameworkRoot)
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 {
arch := allArchs[a]
var platformSDK string
var platformOS string
switch target {
case "ios":
platformOS = "ios"
platformSDK = "iphone"
case "tvos":
platformOS = "tvos"
platformSDK = "appletv"
}
switch a {
case "arm", "arm64":
platformSDK += "os"
case "386", "amd64":
platformOS += "-simulator"
platformSDK += "simulator"
default:
return fmt.Errorf("unsupported -arch: %s", a)
}
sdkPathOut, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--show-sdk-path"))
if err != nil {
return err
}
sdkPath := string(bytes.TrimSpace(sdkPathOut))
clangOut, err := runCmd(exec.Command("xcrun", "--sdk", platformSDK, "--find", "clang"))
if err != nil {
return err
}
clang := string(bytes.TrimSpace(clangOut))
cflags := fmt.Sprintf("-fmodules -fobjc-arc -fembed-bitcode -Werror -arch %s -isysroot %s -m%s-version-min=9.0", arch.iosArch, sdkPath, platformOS)
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)
cmd.Env = append(
os.Environ(),
"GOOS=darwin",
"GOARCH="+a,
"CGO_ENABLED=1",
"CC="+clang,
"CGO_CFLAGS="+cflags,
"CGO_LDFLAGS="+cflags,
)
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 := appDir()
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 errorf(format string, args ...interface{}) {
fmt.Fprintf(os.Stderr, format+"\n", args...)
os.Exit(2)
}
func runCmd(cmd *exec.Cmd) (string, error) {
func runCmdRaw(cmd *exec.Cmd) ([]byte, error) {
if *verbose {
fmt.Printf("%s\n", strings.Join(cmd.Args, " "))
}
out, err := cmd.Output()
if err == nil {
return string(bytes.TrimSpace(out)), nil
return out, nil
}
if err, ok := err.(*exec.ExitError); ok {
return "", fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
return nil, fmt.Errorf("%s failed: %s%s", strings.Join(cmd.Args, " "), out, err.Stderr)
}
return "", err
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) {
+372
View File
@@ -0,0 +1,372 @@
// 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"
)
func buildIOS(tmpDir, target string, bi *buildInfo) error {
appName := filepath.Base(bi.pkg)
switch *buildMode {
case "archive":
framework := *destPath
if framework == "" {
framework = fmt.Sprintf("%s.framework", strings.Title(appName))
}
return archiveIOS(tmpDir, target, framework, bi)
case "exe":
tmpFramework := filepath.Join(tmpDir, "Gio.framework")
if err := archiveIOS(tmpDir, target, tmpFramework, bi); err != nil {
return err
}
out := *destPath
if out == "" {
out = appName + ".ipa"
}
isIPA := strings.HasSuffix(out, ".ipa")
if !isIPA && !strings.HasSuffix(out, ".app") {
return fmt.Errorf("the specified output directory %q does not end in .app or .ipa", out)
}
if !isIPA {
return exeIOS(tmpDir, target, out, bi)
}
payload := filepath.Join(tmpDir, "Payload")
appDir := filepath.Join(payload, "gio.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(tmpDir, appDir, out); err != nil {
return err
}
return zipDir(out, tmpDir, "Payload")
default:
panic("unreachable")
}
}
func signIOS(tmpDir, app, ipa 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")
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, *appID)
if expAppID != provAppID {
continue
}
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), 0600); 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", *appID)
}
func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
if *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), 0600); err != nil {
return err
}
exe := filepath.Join(app, "app")
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
}
infoPlistSrc := 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>app</string>
<key>CFBundleIdentifier</key>
<string>%s</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>Gio</string>
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
</dict>
</plist>`, *appID)
infoPlist := filepath.Join(app, "Info.plist")
if err := ioutil.WriteFile(infoPlist, []byte(infoPlistSrc), 0600); err != nil {
return err
}
if _, err := runCmd(exec.Command("plutil", "-convert", "binary1", infoPlist)); err != nil {
return err
}
return nil
}
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 := appDir()
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=9.0",
}
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()
}