diff --git a/cmd/fyne/internal/commands/install.go b/cmd/fyne/internal/commands/install.go index 79fb0ab263..0c1d82340f 100644 --- a/cmd/fyne/internal/commands/install.go +++ b/cmd/fyne/internal/commands/install.go @@ -172,7 +172,7 @@ func (i *Installer) install() error { } func (i *Installer) installAndroid() error { - target := mobile.AppOutputName(i.os, i.Packager.name) + target := mobile.AppOutputName(i.os, i.Packager.name, i.release) _, err := os.Stat(target) if os.IsNotExist(err) { @@ -186,7 +186,7 @@ func (i *Installer) installAndroid() error { } func (i *Installer) installIOS() error { - target := mobile.AppOutputName(i.os, i.Packager.name) + target := mobile.AppOutputName(i.os, i.Packager.name, i.release) // Always redo the package because the codesign for ios and iossimulator // must be different. diff --git a/cmd/fyne/internal/commands/package-mobile.go b/cmd/fyne/internal/commands/package-mobile.go index 4484ec85d2..5f506b14c8 100644 --- a/cmd/fyne/internal/commands/package-mobile.go +++ b/cmd/fyne/internal/commands/package-mobile.go @@ -61,7 +61,7 @@ func (p *Packager) packageIOS(target string) error { return err } - appDir := filepath.Join(p.dir, mobile.AppOutputName(p.os, p.name)) + appDir := filepath.Join(p.dir, mobile.AppOutputName(p.os, p.name, p.release)) return runCmdCaptureOutput("xcrun", "actool", "Images.xcassets", "--compile", appDir, "--platform", "iphoneos", "--target-device", "iphone", "--minimum-deployment-target", "9.0", "--app-icon", "AppIcon", "--output-format", "human-readable-text", "--output-partial-info-plist", "/dev/null") diff --git a/cmd/fyne/internal/commands/release.go b/cmd/fyne/internal/commands/release.go index 6e1a1adc8f..ecf8de1d85 100644 --- a/cmd/fyne/internal/commands/release.go +++ b/cmd/fyne/internal/commands/release.go @@ -51,6 +51,11 @@ func Release() *cli.Command { Usage: "Android: password for the .keystore file, default take the password from stdin", Destination: &r.keyStorePass, }, + &cli.StringFlag{ + Name: "keyName", + Usage: "Android: alias for the signer's private key, which is needed when reading a .keystore file", + Destination: &r.keyName, + }, &cli.StringFlag{ Name: "keyPass", Usage: "Android: password for the signer's private key, which is needed if the private key is password-protected. Default take the password from stdin", @@ -127,6 +132,7 @@ type Releaser struct { keyStore string keyStorePass string + keyName string keyPass string developer string password string @@ -144,6 +150,7 @@ func (r *Releaser) AddFlags() { flag.IntVar(&r.appBuild, "appBuild", 0, "Build number, should be greater than 0 and incremented for each build") flag.StringVar(&r.keyStore, "keyStore", "", "Android: location of .keystore file containing signing information") flag.StringVar(&r.keyStorePass, "keyStorePass", "", "Android: password for the .keystore file, default take the password from stdin") + flag.StringVar(&r.keyName, "keyName", "", "Android: alias for the signer's private key, which is needed when reading a .keystore file") flag.StringVar(&r.keyPass, "keyPass", "", "Android: password for the signer's private key, which is needed if the private key is password-protected. Default take the password from stdin") flag.StringVar(&r.certificate, "certificate", "", "iOS/macOS/Windows: name of the certificate to sign the build") flag.StringVar(&r.profile, "profile", "", "iOS/macOS: name of the provisioning profile for this release build") @@ -204,7 +211,7 @@ func (r *Releaser) releaseAction(_ *cli.Context) error { func (r *Releaser) afterPackage() error { if util.IsAndroid(r.os) { - target := mobile.AppOutputName(r.os, r.Packager.name) + target := mobile.AppOutputName(r.os, r.Packager.name, r.release) apk := filepath.Join(r.dir, target) if err := r.zipAlign(apk); err != nil { return err @@ -264,7 +271,7 @@ func (r *Releaser) packageIOSRelease() error { payload := filepath.Join(r.dir, "Payload") _ = os.Mkdir(payload, 0750) defer os.RemoveAll(payload) - appName := mobile.AppOutputName(r.os, r.name) + appName := mobile.AppOutputName(r.os, r.name, r.release) payloadAppDir := filepath.Join(payload, appName) if err := os.Rename(filepath.Join(r.dir, appName), payloadAppDir); err != nil { return err @@ -363,16 +370,36 @@ func (r *Releaser) packageWindowsRelease(outFile string) error { } func (r *Releaser) signAndroid(path string) error { - signer := filepath.Join(util.AndroidBuildToolsPath(), "/apksigner") + signer := "jarsigner" + args := []string{} + if r.release { + args = []string{"-keystore", r.keyStore} + } else { + signer = filepath.Join(util.AndroidBuildToolsPath(), "/apksigner") + args = []string{"sign", "--ks", r.keyStore} + } - args := []string{"sign", "--ks", r.keyStore} if r.keyStorePass != "" { - args = append(args, "--ks-pass", "pass:"+r.keyStorePass) + if r.release { + args = append(args, "-storepass", r.keyStorePass) + } else { + args = append(args, "--ks-pass", "pass:"+r.keyStorePass) + } } if r.keyPass != "" { - args = append(args, "--key-pass", "pass:"+r.keyPass) + if r.release { + args = append(args, "-keypass", r.keyPass) + } else { + args = append(args, "--key-pass", "pass:"+r.keyPass) + } } args = append(args, path) + if r.release { + if r.keyName == "" { // Required to sign Google Play .aab + return errors.New("missing required -keyName (alias) parameter") + } + args = append(args, r.keyName) + } cmd := execabs.Command(signer, args...) cmd.Stdout = os.Stdout diff --git a/cmd/fyne/internal/mobile/build.go b/cmd/fyne/internal/mobile/build.go index d2f0854f97..6806867002 100644 --- a/cmd/fyne/internal/mobile/build.go +++ b/cmd/fyne/internal/mobile/build.go @@ -79,10 +79,14 @@ func runBuild(cmd *command) (err error) { } // AppOutputName provides the name of a build resource for a given os - "ios" or "android". -func AppOutputName(os, name string) string { +func AppOutputName(os, name string, release bool) string { switch os { case "android": - return androidPkgName(name) + ".apk" + if release { + return androidPkgName(name) + ".aab" + } else { + return androidPkgName(name) + ".apk" + } case "ios", "iossimulator": return rfc1034Label(name) + ".app" } diff --git a/cmd/fyne/internal/mobile/build_androidapp.go b/cmd/fyne/internal/mobile/build_androidapp.go index 6cd2622fc7..b71b57b3f2 100644 --- a/cmd/fyne/internal/mobile/build_androidapp.go +++ b/cmd/fyne/internal/mobile/build_androidapp.go @@ -20,6 +20,8 @@ import ( "strings" "fyne.io/fyne/v2/cmd/fyne/internal/mobile/binres" + "fyne.io/fyne/v2/cmd/fyne/internal/util" + "golang.org/x/sys/execabs" "golang.org/x/tools/go/packages" ) @@ -96,16 +98,20 @@ func goAndroidBuild(pkg *packages.Package, bundleID string, androidArchs []strin libFiles = append(libFiles, libPath) } + ext := ".apk" + if release { + ext = ".aab" + } if buildO == "" { - buildO = androidPkgName(appName) + ".apk" + buildO = androidPkgName(appName) + ext } - if !strings.HasSuffix(buildO, ".apk") { - return nil, fmt.Errorf("output file name %q does not end in '.apk'", buildO) + if !strings.HasSuffix(buildO, ext) { + return nil, fmt.Errorf("output file name %q does not end in '%s", buildO, ext) } var out io.Writer if !buildN { - f, err := os.Create(buildO) + f, err := os.Create(buildO[:len(buildO)-3]+"apk") if err != nil { return nil, err } @@ -133,6 +139,12 @@ func goAndroidBuild(pkg *packages.Package, bundleID string, androidArchs []strin return nil, err } } + if release { + err = convertAPKToAAB(buildO) + if err != nil { + return nil, err + } + } // TODO: return nmpkgs return nmpkgs[androidArchs[0]], nil @@ -349,6 +361,57 @@ func androidPkgName(name string) string { return s } +func convertAPKToAAB(aabPath string) error { + apkPath := buildO[:len(aabPath)-3]+"apk" + apkProtoPath := buildO[:len(aabPath)-3]+"apk-proto" + tmpPath := filepath.Join(filepath.Dir(aabPath), "tmpbundle") + err := os.MkdirAll(tmpPath, 0755) + if err != nil { + return err + } + defer os.Remove(tmpPath) + + aapt2 := filepath.Join(util.AndroidBuildToolsPath(), "aapt2") + cmd := execabs.Command(aapt2, "convert", "--output-format", "proto", "-o", apkProtoPath, apkPath) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + err = cmd.Run() + if err != nil { + return err + } + _ = os.Remove(apkPath) + + cmd = execabs.Command("unzip", apkProtoPath, "-x", "META-INF/*", "-d", tmpPath) + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + err = cmd.Run() + if err != nil { + return err + } + _ = os.Remove(apkProtoPath) + + _ = os.MkdirAll(filepath.Join(tmpPath, "dex"), 0755) + _ = os.MkdirAll(filepath.Join(tmpPath, "manifest"), 0755) + _ = os.Rename(filepath.Join(tmpPath, "AndroidManifest.xml"), filepath.Join(tmpPath, "manifest", "AndroidManifest.xml")) + _ = os.Rename(filepath.Join(tmpPath, "classes.dex"), filepath.Join(tmpPath, "dex", "classes.dex")) + + cmd = execabs.Command("zip", "../base.zip", "-r", ".") + cmd.Dir = tmpPath + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + err = cmd.Run() + if err != nil { + return err + } + defer os.Remove(filepath.Join(filepath.Dir(aabPath), "base.zip")) + +// TODO bundletool is not an exe, but a jar file... + cmd = execabs.Command("java", "-jar", "bundletool.jar", "build-bundle", "--output", aabPath, "--modules", "base.zip") + cmd.Stderr = os.Stderr + cmd.Stdout = os.Stdout + return cmd.Run() +} + // A random uninteresting private key. // Must be consistent across builds so newer app versions can be installed. const debugCert = `