mirror of
https://git.sr.ht/~eliasnaur/gio-cmd
synced 2026-07-05 17:35:33 +00:00
Add Android packaging hooks for app resources
This commit is contained in:
+125
-43
@@ -50,6 +50,8 @@ type manifestData struct {
|
|||||||
AppName string
|
AppName string
|
||||||
Schemes []string
|
Schemes []string
|
||||||
PackageQueries []string
|
PackageQueries []string
|
||||||
|
ManifestSnip string
|
||||||
|
AppSnip string
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -123,7 +125,7 @@ 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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -422,6 +424,9 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := copyTree(filepath.Join(bi.pkgDir, "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(
|
||||||
@@ -447,52 +452,15 @@ func exeAndroid(tmpDir string, tools *androidTools, bi *buildInfo, extraJars, pe
|
|||||||
AppName: appName,
|
AppName: appName,
|
||||||
Schemes: bi.schemes,
|
Schemes: bi.schemes,
|
||||||
PackageQueries: bi.packageQueries,
|
PackageQueries: bi.packageQueries,
|
||||||
|
ManifestSnip: readOptionalText(filepath.Join(bi.pkgDir, "android", "manifest_snippets.xml")),
|
||||||
|
AppSnip: readOptionalText(filepath.Join(bi.pkgDir, "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}}">
|
|
||||||
{{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}} <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: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>`)
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -757,6 +725,120 @@ 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 string) ([]string, error) {
|
||||||
|
var jars []string
|
||||||
|
for _, pattern := range []string{
|
||||||
|
filepath.Join(dir, "*.jar"),
|
||||||
|
filepath.Join(dir, "android", "*.jar"),
|
||||||
|
} {
|
||||||
|
matches, err := filepath.Glob(pattern)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
jars = append(jars, matches...)
|
||||||
|
}
|
||||||
|
return jars, nil
|
||||||
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
|||||||
@@ -0,0 +1,103 @@
|
|||||||
|
// 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)
|
||||||
|
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 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 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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user