capgo and adk autoupdates. card tags
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
// Live reload on an Android device/emulator connected via adb.
|
||||
//
|
||||
// 1. `pnpm dev` in mobile-client must be running (Vite on PORT, default 10003).
|
||||
// 2. adb reverse makes http://localhost:PORT on the phone reach the dev server on this computer.
|
||||
// localhost counts as a secure context, so the camera and crypto.subtle (PIN) keep working.
|
||||
// 3. `cap run android` syncs a config pointing the WebView at the dev server, builds and installs the app.
|
||||
// Extra arguments are passed to `cap run` (e.g. --target <deviceId>).
|
||||
//
|
||||
// Afterwards run `pnpm build:android` before building an APK to share: it bundles the web build again.
|
||||
import { spawnSync } from 'node:child_process';
|
||||
import { existsSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
const port = process.env.PORT || '10003';
|
||||
const devUrl = `http://localhost:${port}/m/`;
|
||||
|
||||
function findAdb() {
|
||||
const exe = process.platform === 'win32' ? 'adb.exe' : 'adb';
|
||||
const sdk = process.env.ANDROID_HOME || process.env.ANDROID_SDK_ROOT || (process.env.LOCALAPPDATA && path.join(process.env.LOCALAPPDATA, 'Android', 'Sdk'));
|
||||
const fromSdk = sdk && path.join(sdk, 'platform-tools', exe);
|
||||
return fromSdk && existsSync(fromSdk) ? fromSdk : exe;
|
||||
}
|
||||
|
||||
const adb = findAdb();
|
||||
const reverse = spawnSync(adb, ['reverse', `tcp:${port}`, `tcp:${port}`], { stdio: 'inherit' });
|
||||
if (reverse.status !== 0) {
|
||||
console.error(`\n"${adb} reverse" failed: connect a device with USB debugging (or start an emulator) and make sure adb is installed.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`adb reverse tcp:${port} -> this computer. The app will load ${devUrl}\n`);
|
||||
|
||||
const run = spawnSync('pnpm', ['exec', 'cap', 'run', 'android', ...process.argv.slice(2)], {
|
||||
stdio: 'inherit',
|
||||
shell: true,
|
||||
env: { ...process.env, CAP_DEV_URL: devUrl },
|
||||
});
|
||||
|
||||
console.log('\nReminder: the installed app now loads the dev server. Run `pnpm build:android` before building an APK to share.');
|
||||
process.exit(run.status ?? 1);
|
||||
@@ -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.');
|
||||
@@ -0,0 +1,64 @@
|
||||
// Publishes a live update of the Android app's web part:
|
||||
// 1. builds the Capacitor web bundle (its version comes from the build time),
|
||||
// 2. zips it into updates/android/bundle-<version>.zip,
|
||||
// 3. writes updates/android/version.json (version, zip, SHA-256, minimum native build).
|
||||
// The mobile-client dev/preview server serves updates/android at /m/android/, i.e.
|
||||
// https://dev.aggtaa.com/m/android/version.json. Installed apps pick the update up on the next check.
|
||||
//
|
||||
// minNativeBuild is the versionCode of the latest published APK (updates/android/apk.json, see
|
||||
// publish-android-apk.mjs): after an APK release, web updates go only to that APK or newer, older
|
||||
// installations are offered the APK instead. Publish an APK whenever the native part changes
|
||||
// (plugins, AndroidManifest, Java code).
|
||||
import { execSync } from 'node:child_process';
|
||||
import { createHash } from 'node:crypto';
|
||||
import { mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { zipSync } from 'fflate';
|
||||
|
||||
const root = path.resolve(import.meta.dirname, '..');
|
||||
const distDir = path.join(root, 'dist-capacitor');
|
||||
const outDir = path.join(root, 'updates', 'android');
|
||||
const KEEP_BUNDLES = 3;
|
||||
|
||||
execSync('pnpm exec vite build --mode capacitor', { cwd: root, stdio: 'inherit' });
|
||||
|
||||
const { version } = JSON.parse(readFileSync(path.join(distDir, 'cardy-version.json'), 'utf8'));
|
||||
|
||||
function collect(dir, prefix = '') {
|
||||
const files = {};
|
||||
for (const name of readdirSync(dir)) {
|
||||
const full = path.join(dir, name);
|
||||
const rel = prefix ? `${prefix}/${name}` : name;
|
||||
if (statSync(full).isDirectory()) Object.assign(files, collect(full, rel));
|
||||
else files[rel] = readFileSync(full);
|
||||
}
|
||||
return files;
|
||||
}
|
||||
const zip = Buffer.from(zipSync(collect(distDir), { level: 6 }));
|
||||
const checksum = createHash('sha256').update(zip).digest('hex');
|
||||
|
||||
let minNativeBuild = 1;
|
||||
try {
|
||||
minNativeBuild = Number(JSON.parse(readFileSync(path.join(outDir, 'apk.json'), 'utf8')).versionCode) || 1;
|
||||
} catch {
|
||||
// no APK published yet: any installed APK can take the bundle
|
||||
}
|
||||
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
const zipName = `bundle-${version}.zip`;
|
||||
writeFileSync(path.join(outDir, zipName), zip);
|
||||
// version.json last: clients never see a version whose zip is not there yet.
|
||||
writeFileSync(
|
||||
path.join(outDir, 'version.json'),
|
||||
JSON.stringify({ version, url: zipName, checksum, minNativeBuild, publishedAt: new Date().toISOString() }, null, 2),
|
||||
);
|
||||
|
||||
// Keep a few recent bundles (clients may be mid-download of the previous one).
|
||||
const bundles = readdirSync(outDir)
|
||||
.filter((f) => /^bundle-.+\.zip$/.test(f))
|
||||
.sort((a, b) => statSync(path.join(outDir, b)).mtimeMs - statSync(path.join(outDir, a)).mtimeMs);
|
||||
for (const old of bundles.slice(KEEP_BUNDLES)) unlinkSync(path.join(outDir, old));
|
||||
|
||||
console.log(`\nPublished Android update ${version} (${(zip.length / 1024).toFixed(0)} KiB, native build >= ${minNativeBuild})`);
|
||||
console.log(` ${path.join(outDir, zipName)}`);
|
||||
console.log('Served at /m/android/version.json by the mobile-client dev/preview server.');
|
||||
Reference in New Issue
Block a user