Skip to content

Commit

Permalink
First pass on .aab support for google play upload
Browse files Browse the repository at this point in the history
Some rough edges still to work out.
Fixes fyne-io#2663
  • Loading branch information
andydotxyz committed May 17, 2022
1 parent 858209d commit f2d0514
Show file tree
Hide file tree
Showing 5 changed files with 109 additions and 15 deletions.
4 changes: 2 additions & 2 deletions cmd/fyne/internal/commands/install.go
Expand Up @@ -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) {
Expand All @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion cmd/fyne/internal/commands/package-mobile.go
Expand Up @@ -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")
Expand Down
39 changes: 33 additions & 6 deletions cmd/fyne/internal/commands/release.go
Expand Up @@ -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",
Expand Down Expand Up @@ -127,6 +132,7 @@ type Releaser struct {

keyStore string
keyStorePass string
keyName string
keyPass string
developer string
password string
Expand All @@ -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")
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
8 changes: 6 additions & 2 deletions cmd/fyne/internal/mobile/build.go
Expand Up @@ -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"
}
Expand Down
71 changes: 67 additions & 4 deletions cmd/fyne/internal/mobile/build_androidapp.go
Expand Up @@ -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"
)

Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 = `
Expand Down

0 comments on commit f2d0514

Please sign in to comment.