mirror of
https://git.sr.ht/~eliasnaur/gio
synced 2026-07-01 07:35:40 +00:00
cmd/gio: add iOS/tvOS support for -buildmode=exe
Signed-off-by: Elias Naur <mail@eliasnaur.com>
This commit is contained in:
+11
-125
@@ -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) {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user