diff --git a/gogio/androidbuild.go b/gogio/androidbuild.go index 79943de..0428f3a 100644 --- a/gogio/androidbuild.go +++ b/gogio/androidbuild.go @@ -48,6 +48,7 @@ type manifestData struct { Features []string IconSnip string AppName string + Schemes []string } const ( @@ -114,6 +115,7 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { return err } var extraJars []string + var extraAARs []string visitedPkgs := make(map[string]bool) var visitPkg func(*packages.Package) error visitPkg = func(p *packages.Package) error { @@ -126,6 +128,11 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { return err } extraJars = append(extraJars, jars...) + aars, err := filepath.Glob(filepath.Join(dir, "*.aar")) + if err != nil { + return err + } + extraAARs = append(extraAARs, aars...) switch { case p.PkgPath == "net": perms = append(perms, "network") @@ -166,7 +173,7 @@ func buildAndroid(tmpDir string, bi *buildInfo) error { return fmt.Errorf("the specified output %q does not end in '.apk' or '.aab'", file) } - if err := exeAndroid(tmpDir, tools, bi, extraJars, perms, isBundle); err != nil { + if err := exeAndroid(tmpDir, tools, bi, extraJars, extraAARs, perms, isBundle); err != nil { return err } if isBundle { @@ -335,7 +342,7 @@ func archiveAndroid(tmpDir string, bi *buildInfo, perms []string) (err error) { return aarw.Close() } -func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, perms []string, isBundle bool) (err error) { +func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, extraAARs, perms []string, isBundle bool) (err error) { classes := filepath.Join(tmpDir, "classes") var classFiles []string err = filepath.Walk(classes, func(path string, f os.FileInfo, err error) error { @@ -347,7 +354,26 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe } return nil }) + + // extract the jar files from the aars + aarOut := filepath.Join(tmpDir, "aars") + for _, aar := range extraAARs { + name := filepath.Base(aar) + name = strings.TrimSuffix(name, filepath.Ext(name)) + + if err := extractZip(filepath.Join(aarOut, name), aar); err != nil { + return err + } + } + + // extract the jar files from the aars + jarsFromAAR, err := filepath.Glob(filepath.Join(aarOut, "*", "*.jar")) + if err != nil { + return err + } + extraJars = append(extraJars, jarsFromAAR...) classFiles = append(classFiles, extraJars...) + dexDir := filepath.Join(tmpDir, "apk") if err := os.MkdirAll(dexDir, 0755); err != nil { return err @@ -433,6 +459,24 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe return err } + resFromAAR, err := filepath.Glob(filepath.Join(aarOut, "*", "res")) + if err != nil { + return err + } + + for i, res := range resFromAAR { + resZip := filepath.Join(tmpDir, fmt.Sprintf("aar-%d-resources.zip", i)) + + _, err = runCmd(exec.Command( + aapt2, + "compile", + "-o", resZip, + "--dir", res)) + if err != nil { + return err + } + } + // Link APK. permissions, features := getPermissions(perms) appName := UppercaseName(bi.name) @@ -445,6 +489,7 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe Features: features, IconSnip: iconSnip, AppName: appName, + Schemes: bi.schemes, } tmpl, err := template.New("test").Parse( ` @@ -461,11 +506,20 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe android:theme="@style/Theme.GioApp" android:configChanges="screenSize|screenLayout|smallestScreenSize|orientation|keyboardHidden" android:windowSoftInputMode="adjustResize" + android:launchMode= "singleInstance" android:exported="true"> + {{range .Schemes}} + + + + + + + {{end}} `) @@ -478,6 +532,29 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe return err } + manifestsFromAAR, err := filepath.Glob(filepath.Join(aarOut, "*", "AndroidManifest.xml")) + if err != nil { + return err + } + + // Merge manifests, if any. + if len(manifestsFromAAR) > 0 { + if _, err := os.Stat(filepath.Join(tools.buildtools, "manifest-merger.jar")); err != nil { + return fmt.Errorf("manifest-merger.jar not found in buildtools. Download it from https://github.com/distriqt/android-manifest-merger and place it in %s", tools.buildtools) + } + + cmd := exec.Command("java", + "-jar", + filepath.Join(tools.buildtools, "manifest-merger.jar"), + "--main", manifest, + "--libs", strings.Join(manifestsFromAAR, ":"), + "--out", manifest, + ) + if _, err := runCmd(cmd); err != nil { + return err + } + } + linkAPK := filepath.Join(tmpDir, "link.apk") args := []string{ @@ -485,10 +562,19 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe "--manifest", manifest, "-I", tools.androidjar, "-o", linkAPK, + "--auto-add-overlay", } if isBundle { args = append(args, "--proto-format") } + + allResZip, err := filepath.Glob(filepath.Join(tmpDir, "*-resources.zip")) + if err != nil { + return err + } + for _, resZip := range allResZip { + args = append(args, "-R", resZip) + } args = append(args, resZip) if _, err := runCmd(exec.Command(aapt2, args...)); err != nil { @@ -1043,3 +1129,33 @@ func (w *errWriter) Write(p []byte) (n int, err error) { *w.err = err return } + +func extractZip(out string, zipFile string) error { + //extract the zip file + r, err := zip.OpenReader(zipFile) + if err != nil { + return err + } + defer r.Close() + + for _, f := range r.File { + if err := os.MkdirAll(filepath.Dir(filepath.Join(out, f.Name)), 0777); err != nil { + return err + } + if f.FileInfo().IsDir() { + continue + } + out, err := os.Create(filepath.Join(out, f.Name)) + rc, err := f.Open() + if err != nil { + return err + } + if _, err := io.Copy(out, rc); err != nil { + return err + } + rc.Close() + out.Close() + } + + return nil +} diff --git a/gogio/build_info.go b/gogio/build_info.go index 1797296..0dc8edc 100644 --- a/gogio/build_info.go +++ b/gogio/build_info.go @@ -30,6 +30,7 @@ type buildInfo struct { notaryAppleID string notaryPassword string notaryTeamID string + schemes []string } type Semver struct { @@ -51,6 +52,10 @@ func newBuildInfo(pkgPath string) (*buildInfo, error) { if *name != "" { appName = *name } + schemes := strings.Split(*schemes, ",") + for i, scheme := range schemes { + schemes[i] = strings.TrimSpace(scheme) + } ver, err := parseSemver(*version) if err != nil { return nil, err @@ -72,6 +77,7 @@ func newBuildInfo(pkgPath string) (*buildInfo, error) { notaryAppleID: *notaryID, notaryPassword: *notaryPass, notaryTeamID: *notaryTeamID, + schemes: schemes, } return bi, nil } diff --git a/gogio/help.go b/gogio/help.go index 2561c2b..5541bb8 100644 --- a/gogio/help.go +++ b/gogio/help.go @@ -65,7 +65,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 -or specifies the name of key on Keychain to sign MacOS app. +or specifies the name of key on Keychain to sign MacOS app. On iOS/macOS it can be used to +specify the path of provisioning profile (.mobileprovision/.provisionprofile). The -signpass flag specifies the password of the keystore, ignored if -signkey is not provided. @@ -77,4 +78,9 @@ 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 -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 transfer.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. ` diff --git a/gogio/iosbuild.go b/gogio/iosbuild.go index 1126cd5..437af2b 100644 --- a/gogio/iosbuild.go +++ b/gogio/iosbuild.go @@ -4,6 +4,7 @@ package main import ( "archive/zip" + "bytes" "crypto/sha1" "encoding/hex" "errors" @@ -14,6 +15,7 @@ import ( "path/filepath" "strconv" "strings" + "text/template" "time" "golang.org/x/sync/errgroup" @@ -72,7 +74,8 @@ func buildIOS(tmpDir, target string, bi *buildInfo) error { if err := exeIOS(tmpDir, target, appDir, bi); err != nil { return err } - if err := signIOS(bi, tmpDir, appDir); err != nil { + embedded := filepath.Join(appDir, "embedded.mobileprovision") + if err := signApple(bi, tmpDir, embedded, appDir); err != nil { return err } return zipDir(out, tmpDir, "Payload") @@ -81,16 +84,27 @@ func buildIOS(tmpDir, target string, bi *buildInfo) error { } } -func signIOS(bi *buildInfo, tmpDir, app string) error { +// signApple is shared between iOS and macOS. +func signApple(bi *buildInfo, tmpDir, embedded, app 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 + + var provisions []string + if bi.key != "" { + if filepath.Ext(bi.key) != ".mobileprovision" && filepath.Ext(bi.key) != ".provisionprofile" { + return fmt.Errorf("sign: on iOS/macOS -key is a provisioning profile, %q does not end in .mobileprovision/.provisionprofile", bi.key) + } + provisions = []string{bi.key} + } else { + 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") var avail []string for _, prov := range provisions { @@ -114,7 +128,14 @@ func signIOS(bi *buildInfo, tmpDir, app string) error { if err != nil { 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 { return err } @@ -124,7 +145,6 @@ func signIOS(bi *buildInfo, tmpDir, app string) error { continue } // Copy provisioning file. - embedded := filepath.Join(app, "embedded.mobileprovision") if err := copyFile(embedded, prov); err != nil { return err } @@ -144,7 +164,15 @@ func signIOS(bi *buildInfo, tmpDir, app string) error { } identity := sha1.Sum(certDER) 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 fmt.Errorf("sign: no valid provisioning profile found for bundle id %q among %v", bi.appID, avail) @@ -203,7 +231,10 @@ func exeIOS(tmpDir, target, app string, bi *buildInfo) error { if _, err := runCmd(lipo); err != nil { return err } - infoPlist := buildInfoPlist(bi) + infoPlist, err := buildInfoPlist(bi) + if err != nil { + return err + } plistFile := filepath.Join(app, "Info.plist") if err := os.WriteFile(plistFile, []byte(infoPlist), 0660); err != nil { return err @@ -291,7 +322,7 @@ func iosIcons(bi *buildInfo, tmpDir, appDir, icon string) (string, error) { return assetPlist, err } -func buildInfoPlist(bi *buildInfo) string { +func buildInfoPlist(bi *buildInfo) (string, error) { appName := UppercaseName(bi.name) platform := iosPlatformFor(bi.target) var supportPlatform string @@ -301,36 +332,57 @@ func buildInfoPlist(bi *buildInfo) string { case "tvos": supportPlatform = "AppleTVOS" } - return fmt.Sprintf(` + + 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.String(), + VersionCode: bi.version.VersionCode, + Platform: platform, + MinVersion: minIOSVersion, + SupportPlatform: supportPlatform, + Schemes: bi.schemes, + } + + tmpl, err := template.New("manifest").Parse(` CFBundleDevelopmentRegion en CFBundleExecutable - %s + {{.AppName}} CFBundleIdentifier - %s + {{.AppID}} CFBundleInfoDictionaryVersion 6.0 CFBundleName - %s + {{.AppName}} CFBundlePackageType APPL CFBundleShortVersionString - %s + {{.Version}} CFBundleVersion - %d + {{.VersionCode}} UILaunchStoryboardName LaunchScreen UIRequiredDeviceCapabilities arm64 DTPlatformName - %s + {{.Platform}} DTPlatformVersion 12.4 MinimumOSVersion - %d + {{.MinVersion}} UIDeviceFamily 1 @@ -338,7 +390,7 @@ func buildInfoPlist(bi *buildInfo) string { CFBundleSupportedPlatforms - %s + {{.SupportPlatform}} UISupportedInterfaceOrientations @@ -353,13 +405,36 @@ func buildInfoPlist(bi *buildInfo) string { DTSDKBuild 16G73 DTSDKName - %s12.4 + {{.Platform}}12.4 DTXcode 1030 DTXcodeBuild 10G8 + {{if .Schemes}} + CFBundleURLTypes + + {{range .Schemes}} + + CFBundleURLSchemes + + {{.}} + + + {{end}} + + {{end}} -`, appName, bi.appID, appName, bi.version, bi.version.VersionCode, platform, minIOSVersion, supportPlatform, platform) +`) + if err != nil { + panic(err) + } + + var manifestBuffer bytes.Buffer + if err := tmpl.Execute(&manifestBuffer, manifestSrc); err != nil { + panic(err) + } + + return manifestBuffer.String(), nil } func iosPlatformFor(target string) string { diff --git a/gogio/macosbuild.go b/gogio/macosbuild.go index 88e9463..bfe2ece 100644 --- a/gogio/macosbuild.go +++ b/gogio/macosbuild.go @@ -1,6 +1,7 @@ package main import ( + "bytes" "errors" "fmt" "os" @@ -124,6 +125,19 @@ func (b *macBuilder) setIcon(path string) (err error) { } func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) error { + + 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(` @@ -137,21 +151,29 @@ func (b *macBuilder) setInfo(buildInfo *buildInfo, name string) error { NSHighResolutionCapable CFBundlePackageType - APPL + BNDL + {{if .Schemes}} + CFBundleURLTypes + + {{range .Schemes}} + + CFBundleURLSchemes + + {{.}} + + + {{end}} + + {{end}} `) if err != nil { - return err + panic(err) } - var manifest bufferCoff - if err := t.Execute(&manifest, struct { - Name, Bundle string - }{ - Name: name, - Bundle: buildInfo.appID, - }); err != nil { - return err + var manifest bytes.Buffer + if err := t.Execute(&manifest, manifestSrc); err != nil { + panic(err) } b.Manifest = manifest.Bytes() @@ -215,6 +237,12 @@ func (b *macBuilder) signProgram(buildInfo *buildInfo, binDest string, name stri return err } + // If the key is a provisioning profile use the same signing process as iOS + if strings.HasSuffix(buildInfo.key, ".provisionprofile") { + embedded := filepath.Join(binDest, "Contents", "embedded.provisionprofile") + return signApple(buildInfo, b.TempDir, embedded, binDest) + } + cmd := exec.Command( "codesign", "--deep", diff --git a/gogio/main.go b/gogio/main.go index 5fe373e..e29407d 100644 --- a/gogio/main.go +++ b/gogio/main.go @@ -35,11 +35,12 @@ var ( extraLdflags = flag.String("ldflags", "", "extra flags to the Go linker") extraTags = flag.String("tags", "", "extra tags to the Go tool") 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 to be used to sign Android apk files and macOS app. It can be used for iOS and macOS to specify Provisioning Profiles.") signPass = flag.String("signpass", "", "specify the password to decrypt the signkey.") 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.") notaryTeamID = flag.String("notaryteamid", "", "specify the team id to use for notarization.") + schemes = flag.String("schemes", "", "specify a list of comma separated deep-linking schemes that the program accepts") ) func main() { diff --git a/gogio/windowsbuild.go b/gogio/windowsbuild.go index c867e03..5404893 100644 --- a/gogio/windowsbuild.go +++ b/gogio/windowsbuild.go @@ -202,10 +202,18 @@ func (b *windowsBuilder) buildProgram(buildInfo *buildInfo, name string, arch st 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( "go", "build", - "-ldflags=-H=windowsgui "+buildInfo.ldflags, + "-ldflags=-H=windowsgui "+ldflags, "-tags="+buildInfo.tags, "-o", dest, buildInfo.pkgPath,