capgo and adk autoupdates. card tags

This commit is contained in:
azykov@mail.ru
2026-09-29 19:36:08 +03:00
parent 1981d976eb
commit b6fb2fe40f
43 changed files with 1329 additions and 54 deletions
@@ -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.');