capgo and adk autoupdates. card tags
This commit is contained in:
@@ -0,0 +1,92 @@
|
||||
// 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-<versionCode>.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.');
|
||||
Reference in New Issue
Block a user