// Publishes a new APK for in-app self-update (use it whenever the native part changes: plugins, // AndroidManifest, Java code; web-only changes go through publish-android-update.mjs): // 1. builds the Capacitor web bundle and syncs it into the Android project, // 2. builds a release APK signed with the key from android/keystore.properties, // versionCode = build time (yyDDDHHmm, UTC), versionName = web build version, // 3. copies it to updates/android/cardy-.apk and writes updates/android/apk.json. // Web updates published afterwards require this APK (see minNativeBuild in publish-android-update.mjs). // // Needs a JDK (JAVA_HOME, or Android Studio's bundled one) and the Android SDK (android/local.properties). import { execSync } from 'node:child_process'; import { createHash } from 'node:crypto'; import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs'; import path from 'node:path'; const root = path.resolve(import.meta.dirname, '..'); const androidDir = path.join(root, 'android'); const outDir = path.join(root, 'updates', 'android'); const KEEP_APKS = 2; function fail(message) { console.error(`\n${message}`); process.exit(1); } // ---- preconditions ---- const keystoreProps = path.join(androidDir, 'keystore.properties'); if (!existsSync(keystoreProps)) fail(`Missing ${keystoreProps}: copy keystore.properties.example and fill it in.`); const props = Object.fromEntries( readFileSync(keystoreProps, 'utf8') .split(/\r?\n/) .filter((l) => l.includes('=') && !l.trimStart().startsWith('#')) .map((l) => [l.slice(0, l.indexOf('=')).trim(), l.slice(l.indexOf('=') + 1).trim()]), ); for (const key of ['storeFile', 'storePassword', 'keyAlias', 'keyPassword']) { if (!props[key]) fail(`${keystoreProps}: ${key} is empty.`); } if (!existsSync(props.storeFile)) fail(`Keystore not found: ${props.storeFile}`); const env = { ...process.env }; if (!env.JAVA_HOME) { const studioJbr = 'C:/Program Files/Android/Android Studio/jbr'; if (existsSync(studioJbr)) env.JAVA_HOME = studioJbr; else fail('Set JAVA_HOME to a JDK (e.g. Android Studio\'s "jbr" folder).'); } // ---- version ---- const now = new Date(); const dayOfYear = Math.floor((Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate()) - Date.UTC(now.getUTCFullYear(), 0, 0)) / 86_400_000); const pad = (n, w) => String(n).padStart(w, '0'); const versionCode = Number( `${pad(now.getUTCFullYear() % 100, 2)}${pad(dayOfYear, 3)}${pad(now.getUTCHours(), 2)}${pad(now.getUTCMinutes(), 2)}`, ); // ---- build ---- execSync('pnpm exec vite build --mode capacitor', { cwd: root, stdio: 'inherit' }); execSync('pnpm exec cap sync android', { cwd: root, stdio: 'inherit' }); const { version: versionName } = JSON.parse(readFileSync(path.join(root, 'dist-capacitor', 'cardy-version.json'), 'utf8')); const gradlew = process.platform === 'win32' ? 'gradlew.bat' : './gradlew'; execSync(`${gradlew} assembleRelease -PcardyVersionCode=${versionCode} -PcardyVersionName=${versionName}`, { cwd: androidDir, stdio: 'inherit', env, }); const built = path.join(androidDir, 'app', 'build', 'outputs', 'apk', 'release', 'app-release.apk'); if (!existsSync(built)) fail(`Signed APK not found at ${built} (is the signing configured?).`); // ---- publish ---- mkdirSync(outDir, { recursive: true }); const apkName = `cardy-${versionCode}.apk`; copyFileSync(built, path.join(outDir, apkName)); const data = readFileSync(path.join(outDir, apkName)); const sha256 = createHash('sha256').update(data).digest('hex'); // apk.json last: clients never see an APK that is not there yet. writeFileSync( path.join(outDir, 'apk.json'), JSON.stringify({ versionCode, versionName, url: apkName, sha256, size: data.length, publishedAt: now.toISOString() }, null, 2), ); const apks = readdirSync(outDir) .filter((f) => /^cardy-\d+\.apk$/.test(f)) .sort((a, b) => statSync(path.join(outDir, b)).mtimeMs - statSync(path.join(outDir, a)).mtimeMs); for (const old of apks.slice(KEEP_APKS)) unlinkSync(path.join(outDir, old)); console.log(`\nPublished APK ${versionName} (versionCode ${versionCode}, ${(data.length / 1024 / 1024).toFixed(1)} MB)`); console.log(` ${path.join(outDir, apkName)}`); console.log('Served at /m/android/apk.json; web updates published from now on require this APK.');