gogio: [MacOS] add MacOS .app compilation

This patch is a initial implementation to make
`.app` file. It supports custom icons and sign.

Signed-off-by: Inkeliz <inkeliz@inkeliz.com>
This commit is contained in:
Inkeliz
2023-01-27 17:00:01 +00:00
committed by Elias Naur
parent 02068d6340
commit 5c14d1ba64
4 changed files with 226 additions and 3 deletions
+2
View File
@@ -73,6 +73,8 @@ func getArchs() []string {
goarch = runtime.GOARCH goarch = runtime.GOARCH
} }
return []string{goarch} return []string{goarch}
case "macos":
return []string{"arm64", "amd64"}
default: default:
// TODO: Add flag tests. // TODO: Add flag tests.
panic("The target value has already been validated, this will never execute.") panic("The target value has already been validated, this will never execute.")
+4 -2
View File
@@ -18,7 +18,8 @@ Compiled Java class files from jar files in the package directory are
included in Android builds. included in Android builds.
The mandatory -target flag selects the target platform: ios or android for the The mandatory -target flag selects the target platform: ios or android for the
mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL. mobile platforms, tvos for Apple's tvOS, js for WebAssembly/WebGL, macos for
MacOS and windows for Windows.
The -arch flag specifies a comma separated list of GOARCHs to include. The The -arch flag specifies a comma separated list of GOARCHs to include. The
default is all supported architectures. default is all supported architectures.
@@ -63,7 +64,8 @@ its deletion.
The -x flag will print all the external commands executed by the gogio tool. The -x flag will print all the external commands executed by the gogio tool.
The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files. The -signkey flag specifies the path of the keystore, used for signing Android apk/aab files
or specifies the name of key on Keychain to sign MacOS app.
The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided. The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided.
` `
+217
View File
@@ -0,0 +1,217 @@
package main
import (
"errors"
"fmt"
"os"
"os/exec"
"path/filepath"
"strings"
"text/template"
)
func buildMac(tmpDir string, bi *buildInfo) error {
builder := &macBuilder{TempDir: tmpDir}
builder.DestDir = *destPath
if builder.DestDir == "" {
builder.DestDir = bi.pkgPath
}
name := bi.name
if *destPath != "" {
if filepath.Ext(*destPath) != ".app" {
return fmt.Errorf("invalid output name %q, it must end with `.app`", *destPath)
}
name = filepath.Base(*destPath)
}
name = strings.TrimSuffix(name, ".app")
if bi.appID == "" {
return errors.New("app id is empty; use -appid to set it")
}
if err := builder.setIcon(bi.iconPath); err != nil {
return err
}
if err := builder.setInfo(bi, name); err != nil {
return fmt.Errorf("can't build the resources: %v", err)
}
for _, arch := range bi.archs {
if err := builder.buildProgram(bi, name, arch); err != nil {
return err
}
if bi.key != "" {
if err := builder.signProgram(bi, name, arch); err != nil {
return err
}
}
}
return nil
}
type macBuilder struct {
TempDir string
DestDir string
Icons []byte
Manifest []byte
Entitlements []byte
}
func (b *macBuilder) setIcon(path string) (err error) {
if _, err := os.Stat(path); err != nil {
return nil
}
out := filepath.Join(b.TempDir, "iconset.iconset")
if err := os.MkdirAll(out, 0777); err != nil {
return err
}
err = buildIcons(out, path, []iconVariant{
{path: "icon_512x512@2x.png", size: 1024},
{path: "icon_512x512.png", size: 512},
{path: "icon_256x256@2x.png", size: 512},
{path: "icon_256x256.png", size: 256},
{path: "icon_128x128@2x.png", size: 256},
{path: "icon_128x128.png", size: 128},
{path: "icon_64x64@2x.png", size: 128},
{path: "icon_64x64.png", size: 64},
{path: "icon_32x32@2x.png", size: 64},
{path: "icon_32x32.png", size: 32},
{path: "icon_16x16@2x.png", size: 32},
{path: "icon_16x16.png", size: 16},
})
if err != nil {
return err
}
cmd := exec.Command("iconutil",
"-c", "icns", out,
"-o", filepath.Join(b.TempDir, "icon.icns"))
if _, err := runCmd(cmd); err != nil {
return err
}
b.Icons, err = os.ReadFile(filepath.Join(b.TempDir, "icon.icns"))
return err
}
func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) error {
t, err := template.New("manifest").Parse(`<?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>CFBundleExecutable</key>
<string>{{.Name}}</string>
<key>CFBundleIconFile</key>
<string>icon.icns</string>
<key>CFBundleIdentifier</key>
<string>{{.Bundle}}</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>CFBundlePackageType</key>
<string>APPL</string>
</dict>
</plist>`)
if err != nil {
return err
}
var manifest bufferCoff
if err := t.Execute(&manifest, struct {
Name, Bundle string
}{
Name: name,
Bundle: buildInfo.appID,
}); err != nil {
return err
}
b.Manifest = manifest.Bytes()
b.Entitlements = []byte(`<?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>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.allow-jit</key>
<true/>
</dict>
</plist>`)
return nil
}
func (b *macBuilder) buildProgram(buildInfo *buildInfo, name string, arch string) error {
dest := b.DestDir
if len(buildInfo.archs) > 1 {
dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".app")
}
for _, path := range []string{"/Contents/MacOS", "/Contents/Resources"} {
if err := os.MkdirAll(filepath.Join(dest, path), 0755); err != nil {
return err
}
}
if len(b.Icons) > 0 {
if err := os.WriteFile(filepath.Join(dest, "/Contents/Resources/icon.icns"), b.Icons, 0755); err != nil {
return err
}
}
if err := os.WriteFile(filepath.Join(dest, "/Contents/Info.plist"), b.Manifest, 0755); err != nil {
return err
}
cmd := exec.Command(
"go",
"build",
"-tags="+buildInfo.tags,
"-o", filepath.Join(dest, "/Contents/MacOS/"+name),
buildInfo.pkgPath,
)
cmd.Env = append(
os.Environ(),
"GOOS=darwin",
"GOARCH="+arch,
"CGO_ENABLED=1", // Required to cross-compile between AMD/ARM
)
_, err := runCmd(cmd)
return err
}
func (b *macBuilder) signProgram(buildInfo *buildInfo, name string, arch string) error {
dest := b.DestDir
if len(buildInfo.archs) > 1 {
dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".app")
}
options := filepath.Join(b.TempDir, "ent.ent")
if err := os.WriteFile(options, b.Entitlements, 0777); err != nil {
return err
}
xattr := exec.Command("xattr", "-rc", dest)
if _, err := runCmd(xattr); err != nil {
return err
}
cmd := exec.Command(
"codesign",
"--deep",
"--force",
"--options", "runtime",
"--entitlements", options,
"--sign", buildInfo.key,
dest,
)
_, err := runCmd(cmd)
return err
}
+3 -1
View File
@@ -69,7 +69,7 @@ func flagValidate() error {
return errors.New("please specify -target") return errors.New("please specify -target")
} }
switch *target { switch *target {
case "ios", "tvos", "android", "js", "windows": case "ios", "tvos", "android", "js", "windows", "macos":
default: default:
return fmt.Errorf("invalid -target %s", *target) return fmt.Errorf("invalid -target %s", *target)
} }
@@ -100,6 +100,8 @@ func build(bi *buildInfo) error {
return buildAndroid(tmpDir, bi) return buildAndroid(tmpDir, bi)
case "windows": case "windows":
return buildWindows(tmpDir, bi) return buildWindows(tmpDir, bi)
case "macos":
return buildMac(tmpDir, bi)
default: default:
panic("unreachable") panic("unreachable")
} }