From 5c14d1ba647940886d5dd63aac5370f418bc517b Mon Sep 17 00:00:00 2001 From: Inkeliz Date: Fri, 27 Jan 2023 17:00:01 +0000 Subject: [PATCH] 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 --- gogio/build_info.go | 2 + gogio/help.go | 6 +- gogio/macosbuild.go | 217 ++++++++++++++++++++++++++++++++++++++++++++ gogio/main.go | 4 +- 4 files changed, 226 insertions(+), 3 deletions(-) create mode 100644 gogio/macosbuild.go diff --git a/gogio/build_info.go b/gogio/build_info.go index 13818fa..7963f86 100644 --- a/gogio/build_info.go +++ b/gogio/build_info.go @@ -73,6 +73,8 @@ func getArchs() []string { goarch = runtime.GOARCH } return []string{goarch} + case "macos": + return []string{"arm64", "amd64"} default: // TODO: Add flag tests. panic("The target value has already been validated, this will never execute.") diff --git a/gogio/help.go b/gogio/help.go index 87879ae..625c13d 100644 --- a/gogio/help.go +++ b/gogio/help.go @@ -18,7 +18,8 @@ Compiled Java class files from jar files in the package directory are included in Android builds. 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 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 -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. ` diff --git a/gogio/macosbuild.go b/gogio/macosbuild.go new file mode 100644 index 0000000..f5c8c62 --- /dev/null +++ b/gogio/macosbuild.go @@ -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(` + + + + CFBundleExecutable + {{.Name}} + CFBundleIconFile + icon.icns + CFBundleIdentifier + {{.Bundle}} + NSHighResolutionCapable + + CFBundlePackageType + APPL + +`) + 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(` + + + +com.apple.security.cs.allow-unsigned-executable-memory + +com.apple.security.cs.allow-jit + + +`) + + 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 +} diff --git a/gogio/main.go b/gogio/main.go index 38018f7..8cba044 100644 --- a/gogio/main.go +++ b/gogio/main.go @@ -69,7 +69,7 @@ func flagValidate() error { return errors.New("please specify -target") } switch *target { - case "ios", "tvos", "android", "js", "windows": + case "ios", "tvos", "android", "js", "windows", "macos": default: return fmt.Errorf("invalid -target %s", *target) } @@ -100,6 +100,8 @@ func build(bi *buildInfo) error { return buildAndroid(tmpDir, bi) case "windows": return buildWindows(tmpDir, bi) + case "macos": + return buildMac(tmpDir, bi) default: panic("unreachable") }