feat: sign Android APK and generate icons from logo.png or favicon.ico
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
47
.github/scripts/generate-app-icons.mjs
vendored
Normal file
47
.github/scripts/generate-app-icons.mjs
vendored
Normal file
@@ -0,0 +1,47 @@
|
||||
import { execSync } from "node:child_process";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "../..");
|
||||
const distDir = path.join(root, "template", "dist");
|
||||
const templateDir = path.join(root, "template");
|
||||
|
||||
const ICON_PRIORITY = ["logo.png", "favicon.ico"];
|
||||
|
||||
function findIconSource(baseDir) {
|
||||
for (const name of ICON_PRIORITY) {
|
||||
const direct = path.join(baseDir, name);
|
||||
if (fs.existsSync(direct)) return direct;
|
||||
}
|
||||
|
||||
for (const entry of fs.readdirSync(baseDir, { withFileTypes: true })) {
|
||||
if (!entry.isDirectory()) continue;
|
||||
for (const name of ICON_PRIORITY) {
|
||||
const nested = path.join(baseDir, entry.name, name);
|
||||
if (fs.existsSync(nested)) return nested;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function generateAppIcons() {
|
||||
if (!fs.existsSync(distDir)) {
|
||||
console.log("No dist directory, keeping default icons");
|
||||
return;
|
||||
}
|
||||
|
||||
const iconSource = findIconSource(distDir);
|
||||
if (!iconSource) {
|
||||
console.log("No logo.png or favicon.ico found, keeping default icons");
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Generating app icons from: ${path.relative(root, iconSource)}`);
|
||||
execSync(`npx tauri icon "${iconSource}"`, {
|
||||
cwd: templateDir,
|
||||
stdio: "inherit",
|
||||
});
|
||||
}
|
||||
61
.github/scripts/patch-android-gradle.mjs
vendored
Normal file
61
.github/scripts/patch-android-gradle.mjs
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "../..");
|
||||
const gradlePath = path.join(
|
||||
root,
|
||||
"template/src-tauri/gen/android/app/build.gradle.kts",
|
||||
);
|
||||
|
||||
if (!fs.existsSync(gradlePath)) {
|
||||
console.log("Android gradle file not found, skipping patch");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
let content = fs.readFileSync(gradlePath, "utf8");
|
||||
|
||||
if (content.includes("signingConfigs")) {
|
||||
console.log("Android gradle already has signingConfigs");
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (!content.includes("import java.util.Properties")) {
|
||||
content = content.replace(
|
||||
/^(import .+\n)+/m,
|
||||
(block) => `${block}import java.util.Properties\nimport java.io.FileInputStream\n`,
|
||||
);
|
||||
}
|
||||
|
||||
const signingBlock = `
|
||||
signingConfigs {
|
||||
create("release") {
|
||||
val keystorePropertiesFile = rootProject.file("keystore.properties")
|
||||
val keystoreProperties = Properties()
|
||||
if (keystorePropertiesFile.exists()) {
|
||||
keystoreProperties.load(FileInputStream(keystorePropertiesFile))
|
||||
}
|
||||
|
||||
keyAlias = keystoreProperties["keyAlias"] as String
|
||||
keyPassword = keystoreProperties["password"] as String
|
||||
storeFile = file(keystoreProperties["storeFile"] as String)
|
||||
storePassword = keystoreProperties["password"] as String
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
if (content.includes("buildTypes {")) {
|
||||
content = content.replace("buildTypes {", `${signingBlock}\n buildTypes {`);
|
||||
}
|
||||
|
||||
if (!content.includes('signingConfig = signingConfigs.getByName("release")')) {
|
||||
content = content.replace(
|
||||
/getByName\("release"\)\s*\{/,
|
||||
`getByName("release") {
|
||||
signingConfig = signingConfigs.getByName("release")`,
|
||||
);
|
||||
}
|
||||
|
||||
fs.writeFileSync(gradlePath, content);
|
||||
console.log("Patched Android gradle signing configuration");
|
||||
10
.github/scripts/prepare-template.mjs
vendored
10
.github/scripts/prepare-template.mjs
vendored
@@ -2,6 +2,7 @@ import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import AdmZip from "adm-zip";
|
||||
import { generateAppIcons } from "./generate-app-icons.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const root = path.resolve(__dirname, "../..");
|
||||
@@ -68,13 +69,6 @@ if (fs.existsSync(androidConfPath)) {
|
||||
patchTauriConfig(androidConfPath);
|
||||
}
|
||||
|
||||
const iconCandidates = ["icon.png", "favicon.png", "logo.png"];
|
||||
for (const candidate of iconCandidates) {
|
||||
const iconPath = path.join(distDir, candidate);
|
||||
if (fs.existsSync(iconPath)) {
|
||||
console.log(`Found icon candidate: ${candidate}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
generateAppIcons();
|
||||
|
||||
console.log("Template prepared successfully");
|
||||
|
||||
39
.github/scripts/setup-android-signing.sh
vendored
Normal file
39
.github/scripts/setup-android-signing.sh
vendored
Normal file
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
KEYSTORE_PATH="${RUNNER_TEMP}/web2app-release.jks"
|
||||
STORE_PASS="${ANDROID_KEYSTORE_PASSWORD:-web2app-ci}"
|
||||
KEY_PASS="${ANDROID_KEY_PASSWORD:-web2app-ci}"
|
||||
KEY_ALIAS="${ANDROID_KEY_ALIAS:-web2app}"
|
||||
|
||||
if [ -n "${ANDROID_KEYSTORE_BASE64:-}" ]; then
|
||||
echo "Using Android keystore from repository secrets"
|
||||
echo "${ANDROID_KEYSTORE_BASE64}" | base64 -d > "${KEYSTORE_PATH}"
|
||||
else
|
||||
echo "Generating CI Android keystore (sideload demo; set secrets for production)"
|
||||
keytool -genkeypair -v \
|
||||
-keystore "${KEYSTORE_PATH}" \
|
||||
-alias "${KEY_ALIAS}" \
|
||||
-keyalg RSA \
|
||||
-keysize 2048 \
|
||||
-validity 10000 \
|
||||
-storepass "${STORE_PASS}" \
|
||||
-keypass "${KEY_PASS}" \
|
||||
-dname "CN=Web2App, OU=CI, O=Web2App, L=Local, ST=Local, C=CN"
|
||||
fi
|
||||
|
||||
export WEB2APP_ANDROID_KEYSTORE="${KEYSTORE_PATH}"
|
||||
export WEB2APP_ANDROID_STORE_PASS="${STORE_PASS}"
|
||||
export WEB2APP_ANDROID_KEY_PASS="${KEY_PASS}"
|
||||
export WEB2APP_ANDROID_KEY_ALIAS="${KEY_ALIAS}"
|
||||
|
||||
ANDROID_DIR="template/src-tauri/gen/android"
|
||||
if [ -d "${ANDROID_DIR}" ]; then
|
||||
cat > "${ANDROID_DIR}/keystore.properties" <<EOF
|
||||
keyAlias=${KEY_ALIAS}
|
||||
password=${KEY_PASS}
|
||||
storeFile=${KEYSTORE_PATH}
|
||||
EOF
|
||||
fi
|
||||
|
||||
echo "Android signing material ready at ${KEYSTORE_PATH}"
|
||||
46
.github/scripts/sign-android-apk.sh
vendored
Normal file
46
.github/scripts/sign-android-apk.sh
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
BUILD_TOOLS="${ANDROID_HOME}/build-tools/34.0.0"
|
||||
KEYSTORE="${WEB2APP_ANDROID_KEYSTORE:?Missing keystore}"
|
||||
STORE_PASS="${WEB2APP_ANDROID_STORE_PASS:?Missing store password}"
|
||||
KEY_PASS="${WEB2APP_ANDROID_KEY_PASS:?Missing key password}"
|
||||
KEY_ALIAS="${WEB2APP_ANDROID_KEY_ALIAS:-web2app}"
|
||||
|
||||
UNSIGNED_APK="$(find template/src-tauri -name '*unsigned*.apk' -type f | head -n 1 || true)"
|
||||
APK="${UNSIGNED_APK}"
|
||||
if [ -z "${APK}" ]; then
|
||||
APK="$(find template/src-tauri -name '*.apk' -type f | head -n 1 || true)"
|
||||
fi
|
||||
|
||||
if [ -z "${APK}" ]; then
|
||||
echo "No APK found to sign"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p artifacts/android
|
||||
OUTPUT_NAME="$(basename "${APK%.apk}")-signed.apk"
|
||||
|
||||
if "${BUILD_TOOLS}/apksigner" verify "${APK}" >/dev/null 2>&1; then
|
||||
echo "APK already signed: ${APK}"
|
||||
cp "${APK}" "artifacts/android/${OUTPUT_NAME}"
|
||||
ls -la artifacts/android
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Signing APK: ${APK}"
|
||||
ALIGNED="${RUNNER_TEMP}/app-aligned.apk"
|
||||
SIGNED="${RUNNER_TEMP}/app-signed.apk"
|
||||
|
||||
"${BUILD_TOOLS}/zipalign" -f -p 4 "${APK}" "${ALIGNED}"
|
||||
"${BUILD_TOOLS}/apksigner" sign \
|
||||
--ks "${KEYSTORE}" \
|
||||
--ks-key-alias "${KEY_ALIAS}" \
|
||||
--ks-pass "pass:${STORE_PASS}" \
|
||||
--key-pass "pass:${KEY_PASS}" \
|
||||
--out "${SIGNED}" \
|
||||
"${ALIGNED}"
|
||||
|
||||
"${BUILD_TOOLS}/apksigner" verify --verbose "${SIGNED}"
|
||||
cp "${SIGNED}" "artifacts/android/${OUTPUT_NAME}"
|
||||
ls -la artifacts/android
|
||||
Reference in New Issue
Block a user