13 Commits

Author SHA1 Message Date
Joe Julian 1762d36dde gogio: deduplicate Android jars 2026-04-16 21:04:56 -07:00
Joe Julian 7b5a6b418c gogio: load module-root Android jars 2026-04-16 21:03:09 -07:00
Joe Julian f71579e799 gogio: load Android assets from module root 2026-04-16 20:59:20 -07:00
Joe Julian 192acd9d09 Add Android packaging hooks for app resources 2026-04-16 08:42:03 -07:00
inkeliz 2e72e8f0b2 gogio: [android] bump target sdk version
Match the "Target SDK" with the mininum required
by Google Play.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2026-02-28 01:10:27 +00:00
CoyAce f4d6788248 gogio: [ios] add CXX, CGO_CXXFLAGS environment variable for cgo cross-compilation and correct signKey
Signed-off-by: CoyAce <AkeyCoy@gmail.com>
2026-02-10 07:34:11 +01:00
CoyAce e5b1a4e6cd gogio: [android] add CXX environment variable for cgo cross-compilation
Adds the CXX environment variable when building Android shared libraries
to ensure proper C++ compiler selection for cgo-enabled packages.

Signed-off-by: CoyAce <AkeyCoy@gmail.com>
2026-01-26 16:24:59 +01:00
CoyAce f587d2f097 gogio: [Android] map the microphone permission to RECORD_AUDIO
Signed-off-by: CoyAce <AkeyCoy@gmail.com>
2026-01-07 12:34:13 +01:00
inkeliz 8de547d61d gogio: [Android] add support for querying apps
Previously, it was impossible to identify if a specific app
was installed on the user device. It was also impossible to
launch a external app from Gio using Intent.

Now, you can use `-queries` with a comma separed package names,
like `com.another.app`. That allows you to launch Intent to
`com.another.app`.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2025-12-16 21:53:56 +01:00
inkeliz 048614c60e gogio: [iOS] fix compatibility with Apple Connect and iPad requirement
This pach fixes a total of 6 issues caused by gogio,
when uploading .ipa to Apple Connect/Apple Store.

1. Asset validation failed (90474), caused by not set
"UIInterfaceOrientationPortraitUpside" in plist.

2. Asset validation failed (90482), the executable contains
bitcode. Now, gogio will use "bitcode_strip" to remove such
bitcode.

3. Asset validation failed (90060), the version can only have
three non-negative numbers. Using values from semVer is
invalid (such as 1.2.3.4), it must be either 1.2.3 or
1.2.34. Now, gogio uses the later one.

4. Asset validation failed (90476), supporting multitask on
iPad requires UILaunchScreen. That is tricky to solve, instead
gogio will NOT support multitask on iPad.

5. Asset validation failed (90208), version mismatch between
plist and binary. Now, gogio will use compile flags to set
the version AND will use the proper minSdk on plist.

6. Asset validation failed (90023), missing 152x152 icon for
iPad. Now, gogio will create such icon.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-16 18:40:23 +01:00
inkeliz ed8d0aa9a6 gogio: [macOS] support custom profile
This patch enables `-signkey` to load provisioning profiles.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
Signed-off-by: Elias Naur <mail@eliasnaur.com>
2025-12-16 18:04:09 +01:00
inkeliz e1f06eb7b0 gogio: [wasm] fix compatibility with Go 1.23+
Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2025-12-15 22:06:47 +00:00
inkeliz ae8a780af9 gogio: add deeplink support
Add a new flag "-schemes" which links the URL schemes to the app.

Signed-off-by: inkeliz <inkeliz@inkeliz.com>
2025-12-15 22:25:08 +01:00
10 changed files with 607 additions and 114 deletions
+178 -45
View File
@@ -40,14 +40,18 @@ type errWriter struct {
var exeSuffix string var exeSuffix string
type manifestData struct { type manifestData struct {
AppID string AppID string
Version Semver Version Semver
MinSDK int MinSDK int
TargetSDK int TargetSDK int
Permissions []string Permissions []string
Features []string Features []string
IconSnip string IconSnip string
AppName string AppName string
Schemes []string
PackageQueries []string
ManifestSnip string
AppSnip string
} }
const ( const (
@@ -113,7 +117,9 @@ func buildAndroid(tmpDir string, bi *buildInfo) error {
if err != nil { if err != nil {
return err return err
} }
moduleRoot := moduleRootForDir(bi.pkgDir)
var extraJars []string var extraJars []string
seenJars := make(map[string]bool)
visitedPkgs := make(map[string]bool) visitedPkgs := make(map[string]bool)
var visitPkg func(*packages.Package) error var visitPkg func(*packages.Package) error
visitPkg = func(p *packages.Package) error { visitPkg = func(p *packages.Package) error {
@@ -121,11 +127,17 @@ func buildAndroid(tmpDir string, bi *buildInfo) error {
return nil return nil
} }
dir := filepath.Dir(p.GoFiles[0]) dir := filepath.Dir(p.GoFiles[0])
jars, err := filepath.Glob(filepath.Join(dir, "*.jar")) jars, err := androidExtraJars(dir, moduleRoot)
if err != nil { if err != nil {
return err return err
} }
extraJars = append(extraJars, jars...) for _, jar := range jars {
if seenJars[jar] {
continue
}
seenJars[jar] = true
extraJars = append(extraJars, jar)
}
switch { switch {
case p.PkgPath == "net": case p.PkgPath == "net":
perms = append(perms, "network") perms = append(perms, "network")
@@ -230,6 +242,7 @@ func compileAndroid(tmpDir string, tools *androidTools, bi *buildInfo) (err erro
"GOARM=7", // Avoid softfloat. "GOARM=7", // Avoid softfloat.
"CGO_ENABLED=1", "CGO_ENABLED=1",
"CC="+clang, "CC="+clang,
"CXX="+clang+"++",
) )
builds.Go(func() error { builds.Go(func() error {
_, err := runCmd(cmd) _, err := runCmd(cmd)
@@ -351,7 +364,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
} }
minSDK := max(bi.minsdk, 16) minSDK := max(bi.minsdk, 16)
// https://developer.android.com/distribute/best-practices/develop/target-sdk // https://developer.android.com/distribute/best-practices/develop/target-sdk
targetSDK := 33 targetSDK := 35
if bi.targetsdk > 0 { if bi.targetsdk > 0 {
targetSDK = bi.targetsdk targetSDK = bi.targetsdk
} }
@@ -419,6 +432,10 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
if err != nil { if err != nil {
return err return err
} }
moduleRoot := moduleRootForDir(bi.pkgDir)
if err := copyTree(filepath.Join(moduleRoot, "android", "res"), resDir); err != nil {
return err
}
resZip := filepath.Join(tmpDir, "resources.zip") resZip := filepath.Join(tmpDir, "resources.zip")
aapt2 := filepath.Join(tools.buildtools, "aapt2") aapt2 := filepath.Join(tools.buildtools, "aapt2")
_, err = runCmd(exec.Command( _, err = runCmd(exec.Command(
@@ -434,44 +451,25 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
permissions, features := getPermissions(perms) permissions, features := getPermissions(perms)
appName := UppercaseName(bi.name) appName := UppercaseName(bi.name)
manifestSrc := manifestData{ manifestSrc := manifestData{
AppID: bi.appID, AppID: bi.appID,
Version: bi.version, Version: bi.version,
MinSDK: minSDK, MinSDK: minSDK,
TargetSDK: targetSDK, TargetSDK: targetSDK,
Permissions: permissions, Permissions: permissions,
Features: features, Features: features,
IconSnip: iconSnip, IconSnip: iconSnip,
AppName: appName, AppName: appName,
Schemes: bi.schemes,
PackageQueries: bi.packageQueries,
ManifestSnip: readOptionalText(filepath.Join(moduleRoot, "android", "manifest_snippets.xml")),
AppSnip: readOptionalText(filepath.Join(moduleRoot, "android", "application_snippets.xml")),
} }
tmpl, err := template.New("test").Parse( manifestBuffer, err := renderAndroidManifest(manifestSrc)
`<?xml version="1.0" encoding="utf-8"?> if err != nil {
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{.AppID}}"
android:versionCode="{{.Version.VersionCode}}"
android:versionName="{{.Version}}">
<uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" />
{{range .Permissions}} <uses-permission android:name="{{.}}"/>
{{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
{{end}} <application {{.IconSnip}} android:label="{{.AppName}}">
<activity android:name="org.gioui.GioActivity"
android:label="{{.AppName}}"
android:theme="@style/Theme.GioApp"
android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation|keyboardHidden"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
</manifest>`)
var manifestBuffer bytes.Buffer
if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil {
return err return err
} }
manifest := filepath.Join(tmpDir, "AndroidManifest.xml") manifest := filepath.Join(tmpDir, "AndroidManifest.xml")
if err := os.WriteFile(manifest, manifestBuffer.Bytes(), 0o660); err != nil { if err := os.WriteFile(manifest, manifestBuffer, 0o660); err != nil {
return err return err
} }
@@ -736,6 +734,141 @@ func defaultAndroidKeystore(tmpDir string, bi *buildInfo) error {
return err return err
} }
func renderAndroidManifest(data manifestData) ([]byte, error) {
tmpl, err := template.New("test").Parse(
`<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="{{.AppID}}"
android:versionCode="{{.Version.VersionCode}}"
android:versionName="{{.Version}}">
{{if .PackageQueries}}
<queries>
{{range .PackageQueries}}
<package android:name="{{.}}" />
{{end}}
</queries>
{{end}}
<uses-sdk android:minSdkVersion="{{.MinSDK}}" android:targetSdkVersion="{{.TargetSDK}}" />
{{range .Permissions}} <uses-permission android:name="{{.}}"/>
{{end}}{{range .Features}} <uses-feature android:{{.}} android:required="false"/>
{{end}}{{.ManifestSnip}} <application {{.IconSnip}} android:label="{{.AppName}}">
{{.AppSnip}}
<activity android:name="org.gioui.GioActivity"
android:label="{{.AppName}}"
android:theme="@style/Theme.GioApp"
android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation|keyboardHidden"
android:windowSoftInputMode="adjustResize"
android:launchMode="singleInstance"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
{{range .Schemes}}
<intent-filter>
<action android:name="android.intent.action.VIEW"></action>
<category android:name="android.intent.category.DEFAULT"></category>
<category android:name="android.intent.category.BROWSABLE"></category>
<data android:scheme="{{.}}"></data>
</intent-filter>
{{end}}
</activity>
</application>
</manifest>`)
if err != nil {
return nil, err
}
var manifestBuffer bytes.Buffer
if err := tmpl.Execute(&manifestBuffer, data); err != nil {
return nil, err
}
return manifestBuffer.Bytes(), nil
}
func readOptionalText(path string) string {
data, err := os.ReadFile(path)
if err != nil {
return ""
}
if len(data) == 0 {
return ""
}
return "\n" + string(data) + "\n"
}
func copyTree(src, dst string) error {
info, err := os.Stat(src)
if err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
if !info.IsDir() {
return fmt.Errorf("extra Android resources path is not a directory: %s", src)
}
return filepath.Walk(src, func(path string, entry os.FileInfo, walkErr error) error {
if walkErr != nil {
return walkErr
}
rel, err := filepath.Rel(src, path)
if err != nil {
return err
}
if rel == "." {
return nil
}
target := filepath.Join(dst, rel)
if entry.IsDir() {
return os.MkdirAll(target, 0o755)
}
if err := os.MkdirAll(filepath.Dir(target), 0o755); err != nil {
return err
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
return os.WriteFile(target, data, 0o660)
})
}
func androidExtraJars(dir, moduleRoot string) ([]string, error) {
var jars []string
patterns := []string{
filepath.Join(dir, "*.jar"),
filepath.Join(dir, "android", "*.jar"),
}
if moduleRoot != "" && moduleRoot != dir {
patterns = append(patterns,
filepath.Join(moduleRoot, "*.jar"),
filepath.Join(moduleRoot, "android", "*.jar"),
)
}
for _, pattern := range patterns {
matches, err := filepath.Glob(pattern)
if err != nil {
return nil, err
}
jars = append(jars, matches...)
}
return jars, nil
}
func moduleRootForDir(dir string) string {
current := dir
for {
if _, err := os.Stat(filepath.Join(current, "go.mod")); err == nil {
return current
}
parent := filepath.Dir(current)
if parent == current {
return dir
}
current = parent
}
}
func findNDK(androidHome string) (string, error) { func findNDK(androidHome string) (string, error) {
ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*")) ndks, err := filepath.Glob(filepath.Join(androidHome, "ndk", "*"))
if err != nil { if err != nil {
+193
View File
@@ -0,0 +1,193 @@
// SPDX-License-Identifier: Unlicense OR MIT
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestAndroidExtraJarsIncludesAndroidSubdirectory(t *testing.T) {
t.Parallel()
dir := t.TempDir()
rootJar := filepath.Join(dir, "crew.jar")
androidJar := filepath.Join(dir, "android", "vault.jar")
writeTestFile(t, rootJar, "root")
writeTestFile(t, androidJar, "android")
got, err := androidExtraJars(dir, dir)
if err != nil {
t.Fatalf("androidExtraJars() error = %v", err)
}
want := []string{rootJar, androidJar}
for _, jar := range want {
if !containsString(got, jar) {
t.Fatalf("androidExtraJars() = %v, want %q included", got, jar)
}
}
}
func TestAndroidExtraJarsIncludesModuleRootAndroidSubdirectory(t *testing.T) {
t.Parallel()
root := t.TempDir()
dir := filepath.Join(root, "cmd", "keepassgo")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", dir, err)
}
moduleAndroidJar := filepath.Join(root, "android", "keepassgo-android.jar")
writeTestFile(t, moduleAndroidJar, "module-android")
got, err := androidExtraJars(dir, root)
if err != nil {
t.Fatalf("androidExtraJars() error = %v", err)
}
if !containsString(got, moduleAndroidJar) {
t.Fatalf("androidExtraJars() = %v, want %q included", got, moduleAndroidJar)
}
}
func TestAndroidExtraJarsDoesNotRepeatSharedModuleJarWhenCollectedAcrossPackages(t *testing.T) {
t.Parallel()
root := t.TempDir()
moduleAndroidJar := filepath.Join(root, "android", "keepassgo-android.jar")
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.invalid/crew\n")
writeTestFile(t, moduleAndroidJar, "module-android")
dirs := []string{
filepath.Join(root, "cmd", "keepassgo"),
filepath.Join(root, "internal", "appui"),
}
seen := make(map[string]bool)
var collected []string
for _, dir := range dirs {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", dir, err)
}
jars, err := androidExtraJars(dir, root)
if err != nil {
t.Fatalf("androidExtraJars(%q) error = %v", dir, err)
}
for _, jar := range jars {
if seen[jar] {
continue
}
seen[jar] = true
collected = append(collected, jar)
}
}
count := 0
for _, jar := range collected {
if jar == moduleAndroidJar {
count++
}
}
if count != 1 {
t.Fatalf("collected module jar count = %d, want 1 in %v", count, collected)
}
}
func TestRenderAndroidManifestIncludesOptionalSnippets(t *testing.T) {
t.Parallel()
manifest, err := renderAndroidManifest(manifestData{
AppID: "org.example.heist",
Version: Semver{Major: 1, Minor: 2, Patch: 3, VersionCode: 4},
MinSDK: 28,
TargetSDK: 35,
Permissions: []string{"android.permission.INTERNET"},
Features: []string{`name="android.hardware.fingerprint"`},
IconSnip: `android:icon="@mipmap/ic_launcher"`,
AppName: "Bellagio Crew",
ManifestSnip: "\n\t<uses-permission android:name=\"android.permission.POST_NOTIFICATIONS\" />\n",
AppSnip: "\n\t\t<service android:name=\".CrewService\" />\n",
})
if err != nil {
t.Fatalf("renderAndroidManifest() error = %v", err)
}
got := string(manifest)
for _, want := range []string{
`android.permission.POST_NOTIFICATIONS`,
`<service android:name=".CrewService" />`,
`android.permission.INTERNET`,
`android.hardware.fingerprint`,
} {
if !strings.Contains(got, want) {
t.Fatalf("renderAndroidManifest() missing %q in %s", want, got)
}
}
}
func TestCopyTreeCopiesNestedResources(t *testing.T) {
t.Parallel()
src := filepath.Join(t.TempDir(), "android", "res")
dst := filepath.Join(t.TempDir(), "merged")
resourcePath := filepath.Join(src, "xml", "heist_service.xml")
writeTestFile(t, resourcePath, "<service />")
if err := copyTree(src, dst); err != nil {
t.Fatalf("copyTree() error = %v", err)
}
got, err := os.ReadFile(filepath.Join(dst, "xml", "heist_service.xml"))
if err != nil {
t.Fatalf("ReadFile() error = %v", err)
}
if string(got) != "<service />" {
t.Fatalf("copyTree() copied %q, want %q", string(got), "<service />")
}
}
func TestModuleRootForDirFindsOwningModule(t *testing.T) {
t.Parallel()
root := t.TempDir()
writeTestFile(t, filepath.Join(root, "go.mod"), "module example.invalid/crew\n")
dir := filepath.Join(root, "cmd", "keepassgo")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", dir, err)
}
if got := moduleRootForDir(dir); got != root {
t.Fatalf("moduleRootForDir(%q) = %q, want %q", dir, got, root)
}
}
func TestModuleRootForDirFallsBackToInputDir(t *testing.T) {
t.Parallel()
dir := filepath.Join(t.TempDir(), "cmd", "keepassgo")
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", dir, err)
}
if got := moduleRootForDir(dir); got != dir {
t.Fatalf("moduleRootForDir(%q) = %q, want %q", dir, got, dir)
}
}
func writeTestFile(t *testing.T, path, contents string) {
t.Helper()
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("MkdirAll(%q) error = %v", path, err)
}
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
t.Fatalf("WriteFile(%q) error = %v", path, err)
}
}
func containsString(values []string, want string) bool {
for _, value := range values {
if value == want {
return true
}
}
return false
}
+18
View File
@@ -31,6 +31,8 @@ type buildInfo struct {
notaryAppleID string notaryAppleID string
notaryPassword string notaryPassword string
notaryTeamID string notaryTeamID string
schemes []string
packageQueries []string
} }
type Semver struct { type Semver struct {
@@ -78,6 +80,8 @@ func newBuildInfo(pkgPath string) (*buildInfo, error) {
notaryAppleID: *notaryID, notaryAppleID: *notaryID,
notaryPassword: *notaryPass, notaryPassword: *notaryPass,
notaryTeamID: *notaryTeamID, notaryTeamID: *notaryTeamID,
schemes: getCommaList(*schemes),
packageQueries: getCommaList(*pkgQueries),
} }
return bi, nil return bi, nil
} }
@@ -92,6 +96,11 @@ func (s Semver) String() string {
return fmt.Sprintf("%d.%d.%d.%d", s.Major, s.Minor, s.Patch, s.VersionCode) return fmt.Sprintf("%d.%d.%d.%d", s.Major, s.Minor, s.Patch, s.VersionCode)
} }
func (s Semver) StringCompact() string {
// Used to meet CFBundleShortVersionString format.
return fmt.Sprintf("%d.%d.%d", s.Major, s.Minor, s.Patch)
}
func parseSemver(v string) (Semver, error) { func parseSemver(v string) (Semver, error) {
var sv Semver var sv Semver
_, err := fmt.Sscanf(v, "%d.%d.%d.%d", &sv.Major, &sv.Minor, &sv.Patch, &sv.VersionCode) _, err := fmt.Sscanf(v, "%d.%d.%d.%d", &sv.Major, &sv.Minor, &sv.Patch, &sv.VersionCode)
@@ -147,6 +156,15 @@ func getLdFlags(appID string) string {
return strings.Join(ldflags, " ") return strings.Join(ldflags, " ")
} }
func getCommaList(s string) (list []string) {
for _, v := range strings.Split(s, ",") {
if v := strings.TrimSpace(v); v != "" {
list = append(list, v)
}
}
return list
}
type packageMetadata struct { type packageMetadata struct {
PkgPath string PkgPath string
Dir string Dir string
+11 -1
View File
@@ -69,7 +69,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. or specifies the name of key on Keychain to sign MacOS apps. On iOS and macOS it can be used
to specify the path of a provisioning profile (.mobileprovision/.provisionprofile).
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.
If -signpass is not sepecified it will be read from the environment variable GOGIO_SIGNPASS. If -signpass is not sepecified it will be read from the environment variable GOGIO_SIGNPASS.
@@ -82,4 +83,13 @@ for details. If not provided, the password will be prompted.
The -notaryteamid flag specifies the team ID to use for notarization of MacOS app, ignored if The -notaryteamid flag specifies the team ID to use for notarization of MacOS app, ignored if
-notaryid is not provided. -notaryid is not provided.
The -schemes flag specifies a list of comma separated URI schemes that the program can
handle. For example, use -schemes yourAppName to receive a app.URLEvent for URIs
starting with yourAppName://. It is only supported on Android, iOS, macOS and Windows.
On Windows, it will restrict the program to a single instance.
The -queries flag specifies a list of comma separated package names used to query other apps,
that is useful to launch other apps and verify their presence. For example, use -queries
com.example.otherapp to query the app with that package name. It is only necessary on Android.
` `
+150 -48
View File
@@ -4,6 +4,7 @@ package main
import ( import (
"archive/zip" "archive/zip"
"bytes"
"crypto/sha1" "crypto/sha1"
"encoding/hex" "encoding/hex"
"errors" "errors"
@@ -15,6 +16,7 @@ import (
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
"text/template"
"time" "time"
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
@@ -73,7 +75,29 @@ func buildIOS(tmpDir, target string, bi *buildInfo) error {
if err := exeIOS(tmpDir, target, appDir, bi); err != nil { if err := exeIOS(tmpDir, target, appDir, bi); err != nil {
return err return err
} }
if err := signIOS(bi, tmpDir, appDir); err != nil {
embedded := filepath.Join(appDir, "embedded.mobileprovision")
var provisions []string
if bi.key != "" {
if ext := filepath.Ext(bi.key); ext != ".mobileprovision" && ext != ".provisionprofile" {
return fmt.Errorf("sign: -signkey specifies an Apple provisioning profile, but %q does not end in .mobileprovision or .provisionprofile", bi.key)
}
provisions = []string{bi.key}
} else {
home, err := os.UserHomeDir()
if err != nil {
return err
}
p, err := filepath.Glob(filepath.Join(home, "Library", "MobileDevice", "Provisioning Profiles", "*.mobileprovision"))
if err != nil {
return err
}
provisions = p
}
if err := signApple(bi.appID, tmpDir, embedded, appDir, provisions); err != nil {
return err return err
} }
return zipDir(out, tmpDir, "Payload") return zipDir(out, tmpDir, "Payload")
@@ -82,16 +106,8 @@ func buildIOS(tmpDir, target string, bi *buildInfo) error {
} }
} }
func signIOS(bi *buildInfo, tmpDir, app string) error { // signApple is shared between iOS and macOS.
home, err := os.UserHomeDir() func signApple(appID, tmpDir, embedded, app string, provisions []string) error {
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") provInfo := filepath.Join(tmpDir, "provision.plist")
var avail []string var avail []string
for _, prov := range provisions { for _, prov := range provisions {
@@ -115,17 +131,23 @@ func signIOS(bi *buildInfo, tmpDir, app string) error {
if err != nil { if err != nil {
return err return err
} }
provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", "Print:Entitlements:application-identifier", provInfo))
// iOS/macOS Catalyst
provAppIDSearchKey := "Print:Entitlements:application-identifier"
if filepath.Ext(prov) == ".provisionprofile" {
// macOS
provAppIDSearchKey = "Print:Entitlements:com.apple.application-identifier"
}
provAppID, err := runCmd(exec.Command("/usr/libexec/PlistBuddy", "-c", provAppIDSearchKey, provInfo))
if err != nil { if err != nil {
return err return err
} }
expAppID := fmt.Sprintf("%s.%s", appIDPrefix, bi.appID) expAppID := fmt.Sprintf("%s.%s", appIDPrefix, appID)
avail = append(avail, provAppID) avail = append(avail, provAppID)
if expAppID != provAppID { if expAppID != provAppID {
continue continue
} }
// Copy provisioning file. // Copy provisioning file.
embedded := filepath.Join(app, "embedded.mobileprovision")
if err := copyFile(embedded, prov); err != nil { if err := copyFile(embedded, prov); err != nil {
return err return err
} }
@@ -145,10 +167,18 @@ func signIOS(bi *buildInfo, tmpDir, app string) error {
} }
identity := sha1.Sum(certDER) identity := sha1.Sum(certDER)
idHex := hex.EncodeToString(identity[:]) idHex := hex.EncodeToString(identity[:])
_, err = runCmd(exec.Command("codesign", "-s", idHex, "-v", "--entitlements", entFile, app)) _, err = runCmd(exec.Command(
"codesign",
"--sign", idHex,
"--deep",
"--force",
"--options", "runtime",
"--entitlements",
entFile,
app))
return err return err
} }
return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", bi.appID, avail) return fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", appID, avail)
} }
func exeIOS(tmpDir, target, app string, bi *buildInfo) error { func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
@@ -172,6 +202,7 @@ func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
} }
cflags = append(cflags, cflags = append(cflags,
"-fobjc-arc", "-fobjc-arc",
fmt.Sprintf("-miphoneos-version-min=%d.0", bi.minsdk),
) )
cflagsLine := strings.Join(cflags, " ") cflagsLine := strings.Join(cflags, " ")
exeSlice := filepath.Join(tmpDir, "app-"+a) exeSlice := filepath.Join(tmpDir, "app-"+a)
@@ -190,7 +221,9 @@ func exeIOS(tmpDir, target, app string, bi *buildInfo) error {
"GOARCH="+a, "GOARCH="+a,
"CGO_ENABLED=1", "CGO_ENABLED=1",
"CC="+clang, "CC="+clang,
"CXX="+clang+"++",
"CGO_CFLAGS="+cflagsLine, "CGO_CFLAGS="+cflagsLine,
"CGO_CXXFLAGS="+cflagsLine,
"CGO_LDFLAGS=-lresolv "+cflagsLine, "CGO_LDFLAGS=-lresolv "+cflagsLine,
) )
builds.Go(func() error { builds.Go(func() error {
@@ -241,6 +274,9 @@ func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
err := buildIcons(appIcon, icon, []iconVariant{ err := buildIcons(appIcon, icon, []iconVariant{
{path: "ios_2x.png", size: 120}, {path: "ios_2x.png", size: 120},
{path: "ios_3x.png", size: 180}, {path: "ios_3x.png", size: 180},
{path: "ipad_1x.png", size: 76},
{path: "ipad_2x.png", size: 152},
{path: "ipad_4x.png", size: 228},
// The App Store icon is not allowed to contain // The App Store icon is not allowed to contain
// transparent pixels. // transparent pixels.
{path: "ios_store.png", size: 1024, fill: true}, {path: "ios_store.png", size: 1024, fill: true},
@@ -249,26 +285,44 @@ func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) {
return "", err return "", err
} }
contentJson := `{ contentJson := `{
"images" : [ "images": [
{ {
"size" : "60x60", "size": "60x60",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "ios_2x.png", "filename": "ios_2x.png",
"scale" : "2x" "scale": "2x"
}, },
{ {
"size" : "60x60", "size": "60x60",
"idiom" : "iphone", "idiom": "iphone",
"filename" : "ios_3x.png", "filename": "ios_3x.png",
"scale" : "3x" "scale": "3x"
}, },
{ {
"size" : "1024x1024", "size": "76x76",
"idiom" : "ios-marketing", "idiom": "ipad",
"filename" : "ios_store.png", "filename": "ipad_1x.png",
"scale" : "1x" "scale": "1x"
} },
] {
"size": "76x76",
"idiom": "ipad",
"filename": "ipad_2x.png",
"scale": "2x"
},
{
"size": "152x152",
"idiom": "ipad",
"filename": "ipad_4x.png",
"scale": "2x"
},
{
"size": "1024x1024",
"idiom": "ios-marketing",
"filename": "ios_store.png",
"scale": "1x"
}
]
}` }`
contentFile := filepath.Join(appIcon, "Contents.json") contentFile := filepath.Join(appIcon, "Contents.json")
if err := os.WriteFile(contentFile, []byte(contentJson), 0o600); err != nil { if err := os.WriteFile(contentFile, []byte(contentJson), 0o600); err != nil {
@@ -302,36 +356,57 @@ func buildInfoPlist(bi *buildInfo) string {
case "tvos": case "tvos":
supportPlatform = "AppleTVOS" supportPlatform = "AppleTVOS"
} }
return fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8"?>
manifestSrc := struct {
AppName string
AppID string
Version string
VersionCode uint32
Platform string
MinVersion int
SupportPlatform string
Schemes []string
}{
AppName: appName,
AppID: bi.appID,
Version: bi.version.StringCompact(),
VersionCode: bi.version.VersionCode,
Platform: platform,
MinVersion: bi.minsdk,
SupportPlatform: supportPlatform,
Schemes: bi.schemes,
}
tmpl, 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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>en</string> <string>en</string>
<key>CFBundleExecutable</key> <key>CFBundleExecutable</key>
<string>%s</string> <string>{{.AppName}}</string>
<key>CFBundleIdentifier</key> <key>CFBundleIdentifier</key>
<string>%s</string> <string>{{.AppID}}</string>
<key>CFBundleInfoDictionaryVersion</key> <key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string> <string>6.0</string>
<key>CFBundleName</key> <key>CFBundleName</key>
<string>%s</string> <string>{{.AppName}}</string>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>APPL</string>
<key>CFBundleShortVersionString</key> <key>CFBundleShortVersionString</key>
<string>%s</string> <string>{{.Version}}</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>%d</string> <string>{{.VersionCode}}</string>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIRequiredDeviceCapabilities</key> <key>UIRequiredDeviceCapabilities</key>
<array><string>arm64</string></array> <array><string>arm64</string></array>
<key>DTPlatformName</key> <key>DTPlatformName</key>
<string>%s</string> <string>{{.Platform}}</string>
<key>DTPlatformVersion</key> <key>DTPlatformVersion</key>
<string>12.4</string> <string>12.4</string>
<key>MinimumOSVersion</key> <key>MinimumOSVersion</key>
<string>%d</string> <string>{{.MinVersion}}.0</string>
<key>UIDeviceFamily</key> <key>UIDeviceFamily</key>
<array> <array>
<integer>1</integer> <integer>1</integer>
@@ -339,14 +414,17 @@ func buildInfoPlist(bi *buildInfo) string {
</array> </array>
<key>CFBundleSupportedPlatforms</key> <key>CFBundleSupportedPlatforms</key>
<array> <array>
<string>%s</string> <string>{{.SupportPlatform}}</string>
</array> </array>
<key>UISupportedInterfaceOrientations</key> <key>UISupportedInterfaceOrientations</key>
<array> <array>
<string>UIInterfaceOrientationPortrait</string> <string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>UIRequiresFullScreen</key>
<true/>
<key>DTCompiler</key> <key>DTCompiler</key>
<string>com.apple.compilers.llvm.clang.1_0</string> <string>com.apple.compilers.llvm.clang.1_0</string>
<key>DTPlatformBuild</key> <key>DTPlatformBuild</key>
@@ -354,13 +432,38 @@ func buildInfoPlist(bi *buildInfo) string {
<key>DTSDKBuild</key> <key>DTSDKBuild</key>
<string>16G73</string> <string>16G73</string>
<key>DTSDKName</key> <key>DTSDKName</key>
<string>%s12.4</string> <string>{{.Platform}}12.4</string>
<key>DTXcode</key> <key>DTXcode</key>
<string>1030</string> <string>1030</string>
<key>DTXcodeBuild</key> <key>DTXcodeBuild</key>
<string>10G8</string> <string>10G8</string>
<key>UILaunchScreen</key>
<true/>
{{if .Schemes}}
<key>CFBundleURLTypes</key>
<array>
{{range .Schemes}}
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.}}</string>
</array>
</dict>
{{end}}
</array>
{{end}}
</dict> </dict>
</plist>`, appName, bi.appID, appName, bi.version, bi.version.VersionCode, platform, minIOSVersion, supportPlatform, platform) </plist>`)
if err != nil {
panic(err)
}
var manifestBuffer bytes.Buffer
if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil {
panic(err)
}
return manifestBuffer.String()
} }
func iosPlatformFor(target string) string { func iosPlatformFor(target string) string {
@@ -501,7 +604,6 @@ func iosCompilerFor(target, arch string, minsdk int) (string, []string, error) {
return "", nil, err return "", nil, err
} }
cflags := []string{ cflags := []string{
"-fembed-bitcode",
"-arch", allArchs[arch].iosArch, "-arch", allArchs[arch].iosArch,
"-isysroot", sdkPath, "-isysroot", sdkPath,
"-m" + platformOS + "-version-min=" + strconv.Itoa(minsdk), "-m" + platformOS + "-version-min=" + strconv.Itoa(minsdk),
+1 -1
View File
@@ -174,7 +174,7 @@ const (
</html>` </html>`
// jsSetGo sets the `window.go` variable. // jsSetGo sets the `window.go` variable.
jsSetGo = `(() => { jsSetGo = `(() => {
window.go = {argv: [], env: {}, importObject: {go: {}}}; window.go = {argv: [], env: {}, importObject: {go: {}, gojs: {}}};
const argv = new URLSearchParams(location.search).get("argv"); const argv = new URLSearchParams(location.search).get("argv");
if (argv) { if (argv) {
window.go["argv"] = argv.split(" "); window.go["argv"] = argv.split(" ");
+40 -16
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"os" "os"
@@ -34,9 +35,7 @@ func buildMac(tmpDir string, bi *buildInfo) error {
return err return err
} }
if err := builder.setInfo(bi, name); err != nil { builder.setInfo(bi, name)
return fmt.Errorf("can't build the resources: %v", err)
}
for _, arch := range bi.archs { for _, arch := range bi.archs {
tmpDest := filepath.Join(builder.TempDir, filepath.Base(builder.DestDir)) tmpDest := filepath.Join(builder.TempDir, filepath.Base(builder.DestDir))
@@ -122,7 +121,20 @@ func (b *macBuilder) setIcon(path string) (err error) {
return err return err
} }
func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) error { func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) {
manifestSrc := struct {
Name string
Bundle string
Version Semver
Schemes []string
}{
Name: name,
Bundle: buildInfo.appID,
Version: buildInfo.version,
Schemes: buildInfo.schemes,
}
t, err := template.New("manifest").Parse(`<?xml version="1.0" encoding="UTF-8"?> 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"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
@@ -136,21 +148,29 @@ func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) error {
<key>NSHighResolutionCapable</key> <key>NSHighResolutionCapable</key>
<true/> <true/>
<key>CFBundlePackageType</key> <key>CFBundlePackageType</key>
<string>APPL</string> <string>BNDL</string>
{{if .Schemes}}
<key>CFBundleURLTypes</key>
<array>
{{range .Schemes}}
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>{{.}}</string>
</array>
</dict>
{{end}}
</array>
{{end}}
</dict> </dict>
</plist>`) </plist>`)
if err != nil { if err != nil {
return err panic(err)
} }
var manifest bufferCoff var manifest bytes.Buffer
if err := t.Execute(&manifest, struct { if err := t.Execute(&manifest, manifestSrc); err != nil {
Name, Bundle string panic(err)
}{
Name: name,
Bundle: buildInfo.appID,
}); err != nil {
return err
} }
b.Manifest = manifest.Bytes() b.Manifest = manifest.Bytes()
@@ -164,8 +184,6 @@ func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) error {
<true/> <true/>
</dict> </dict>
</plist>`) </plist>`)
return nil
} }
func (b *macBuilder) buildProgram(buildInfo *buildInfo, binDest string, name string, arch string) error { func (b *macBuilder) buildProgram(buildInfo *buildInfo, binDest string, name string, arch string) error {
@@ -214,6 +232,12 @@ func (b *macBuilder) signProgram(buildInfo *buildInfo, binDest string, name stri
return err return err
} }
// If the key is a provisioning profile use the same signing process as iOS
if filepath.Ext(buildInfo.key) == ".provisionprofile" {
embedded := filepath.Join(binDest, "Contents", "embedded.provisionprofile")
return signApple(buildInfo.appID, b.TempDir, embedded, binDest, []string{buildInfo.key})
}
cmd := exec.Command( cmd := exec.Command(
"codesign", "codesign",
"--deep", "--deep",
+4 -2
View File
@@ -29,18 +29,20 @@ var (
destPath = flag.String("o", "", "output file or directory.\nFor -target ios or tvos, use the .app suffix to target simulators.") 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)") appID = flag.String("appid", "", "app identifier (for -buildmode=exe)")
name = flag.String("name", "", "app name (for -buildmode=exe)") name = flag.String("name", "", "app name (for -buildmode=exe)")
version = flag.String("version", "1.0.0.1", "semver app version (for -buildmode=exe) on the form major.minor.patch.versioncode") version = flag.String("version", "1.0.0.1", "semver app version (for -buildmode=exe) on the form major.minor.patch.versioncode. The versioncode is not used for iOS and macOS.")
printCommands = flag.Bool("x", false, "print the commands") 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.") keepWorkdir = flag.Bool("work", false, "print the name of the temporary work directory and do not delete it when exiting.")
linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool") linkMode = flag.String("linkmode", "", "set the -linkmode flag of the go tool")
extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker") extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker")
extraTags = flag.String("tags", "", "extra tags to the Go tool") extraTags = flag.String("tags", "", "extra tags to the Go tool")
iconPath = flag.String("icon", "", "specify an icon for iOS and Android") iconPath = flag.String("icon", "", "specify an icon for iOS and Android")
signKey = flag.String("signkey", "", "specify the path of the keystore to be used to sign Android apk files.") signKey = flag.String("signkey", "", "specify the path of the keystore (Android) or provisioning profile (macOS or iOS) for signing")
signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.") signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.")
notaryID = flag.String("notaryid", "", "specify the apple id to use for notarization.") notaryID = flag.String("notaryid", "", "specify the apple id to use for notarization.")
notaryPass = flag.String("notarypass", "", "specify app-specific password of the Apple ID to be used for notarization.") notaryPass = flag.String("notarypass", "", "specify app-specific password of the Apple ID to be used for notarization.")
notaryTeamID = flag.String("notaryteamid", "", "specify the team id to use for notarization.") notaryTeamID = flag.String("notaryteamid", "", "specify the team id to use for notarization.")
schemes = flag.String("schemes", "", "specify a list of comma separated URL schemes that the program accepts")
pkgQueries = flag.String("queries", "", "specify a list of comma separated package names used to query other apps on Android.")
) )
func main() { func main() {
+3
View File
@@ -22,6 +22,9 @@ var AndroidPermissions = map[string][]string{
"wakelock": { "wakelock": {
"android.permission.WAKE_LOCK", "android.permission.WAKE_LOCK",
}, },
"microphone": {
"android.permission.RECORD_AUDIO",
},
} }
var AndroidFeatures = map[string][]string{ var AndroidFeatures = map[string][]string{
+9 -1
View File
@@ -202,10 +202,18 @@ func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch st
dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe") dest = filepath.Join(filepath.Dir(b.DestDir), name+"_"+arch+".exe")
} }
ldflags := buildInfo.ldflags
if buildInfo.schemes != nil {
ldflags += ` -X "gioui.org/app.schemesURI=` + strings.Join(buildInfo.schemes, ",") + `" `
}
if buildInfo.appID != "" {
ldflags += ` -X "gioui.org/app.ID=` + buildInfo.appID + `" `
}
cmd := exec.Command( cmd := exec.Command(
"go", "go",
"build", "build",
"-ldflags=-H=windowsgui "+buildInfo.ldflags, "-ldflags=-H=windowsgui "+ldflags,
"-tags="+buildInfo.tags, "-tags="+buildInfo.tags,
"-o", dest, "-o", dest,
buildInfo.pkgPath, buildInfo.pkgPath,