Mobile-BillTracker/scripts/prepare-android-assets.js

205 lines
8.3 KiB
JavaScript
Raw Permalink Normal View History

#!/usr/bin/env node
// Copies mobile/nodejs-assets/nodejs-project into the Android project's assets
// so the nodejs-mobile-cordova plugin can find/copy it, and generates
// dir.list/file.list for the plugin's runtime copy step.
//
// nodejs-mobile-cordova's build.gradle is `apply from`'d into BOTH the `app`
// and `capacitor-cordova-android-plugins` modules, and each checks for
// `<module>/src/main/assets/www/` at *configuration* time — the build fails
// with "couldn't find the www folder" if either is missing. So we place a
// copy in both.
//
// IMPORTANT: `npx cap sync` regenerates capacitor-cordova-android-plugins'
// assets folder (wiping anything placed here), so this script must run AFTER
// `npx cap sync` and the build must run with `--no-sync`
// (npm run android does this in the right order).
//
// Run after `./scripts/sync-nodejs-project.sh` and (optionally) `npm install`
// inside nodejs-assets/nodejs-project.
'use strict';
const fs = require('fs');
const path = require('path');
const zlib = require('zlib');
const MOBILE_DIR = path.join(__dirname, '..');
const SRC = path.join(MOBILE_DIR, 'nodejs-assets', 'nodejs-project');
if (!fs.existsSync(SRC)) {
console.error(`Source not found: ${SRC}\nRun ./scripts/sync-nodejs-project.sh first.`);
process.exit(1);
}
function rmrf(p) {
fs.rmSync(p, { recursive: true, force: true });
}
function copyDir(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
if (entry.name === '.bin') continue; // symlinks under node_modules/.bin break the asset copy
// Android's asset merger treats foo.js + foo.js.gz as duplicate resources.
if (entry.name.endsWith('.gz')) continue;
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isSymbolicLink()) {
const real = fs.realpathSync(srcPath);
if (fs.statSync(real).isDirectory()) copyDir(real, destPath);
else fs.copyFileSync(real, destPath);
} else if (entry.isDirectory()) {
copyDir(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
function enumerate(absDir, relDir, dirs, files) {
for (const entry of fs.readdirSync(absDir, { withFileTypes: true })) {
if (entry.name.startsWith('.')) continue;
if (entry.name.endsWith('.gz') || entry.name.endsWith('~')) continue;
const absPath = path.join(absDir, entry.name);
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
if (entry.isDirectory()) {
dirs.push(relPath);
enumerate(absPath, relPath, dirs, files);
} else {
files.push(relPath);
}
}
}
// Patch a known issue in nodejs-mobile-cordova 0.4.3: NodeJS.java references
// `BuildConfig.DEBUG`, which doesn't resolve in Capacitor's cordova-plugins
// library module (no matching BuildConfig in that package). This file is
// re-copied verbatim from node_modules by `cap sync`, so patch it each time.
const nodeJsJava = path.join(
MOBILE_DIR, 'android', 'capacitor-cordova-android-plugins', 'src', 'main', 'java',
'com', 'janeasystems', 'cdvnodejsmobile', 'NodeJS.java'
);
if (fs.existsSync(nodeJsJava)) {
const src = fs.readFileSync(nodeJsJava, 'utf8');
const patched = src.replace('if (BuildConfig.DEBUG) {', 'if (false) {');
if (patched !== src) {
fs.writeFileSync(nodeJsJava, patched);
console.log(`Patched ${path.relative(MOBILE_DIR, nodeJsJava)} (BuildConfig.DEBUG -> false)`);
}
}
// Local mode runs the embedded Node server on http://127.0.0.1:3000, which
// Android's Network Security Config blocks by default (cleartext traffic).
// `cap sync`/`cap add android` don't know about this, so (re)write the config
// and wire it into AndroidManifest.xml's <application> tag each time.
const networkSecurityConfigXml = `<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<!-- Local mode runs an embedded Node.js server on 127.0.0.1; the WebView
(https://localhost) needs to reach it over plain HTTP. -->
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="false">127.0.0.1</domain>
<domain includeSubdomains="false">localhost</domain>
</domain-config>
</network-security-config>
`;
const networkSecurityConfigPath = path.join(
MOBILE_DIR, 'android', 'app', 'src', 'main', 'res', 'xml', 'network_security_config.xml'
);
fs.mkdirSync(path.dirname(networkSecurityConfigPath), { recursive: true });
fs.writeFileSync(networkSecurityConfigPath, networkSecurityConfigXml);
const manifestPath = path.join(MOBILE_DIR, 'android', 'app', 'src', 'main', 'AndroidManifest.xml');
if (fs.existsSync(manifestPath)) {
const src = fs.readFileSync(manifestPath, 'utf8');
if (!src.includes('android:networkSecurityConfig')) {
const patched = src.replace(
/(<application\b)/,
'$1\n android:networkSecurityConfig="@xml/network_security_config"'
);
fs.writeFileSync(manifestPath, patched);
console.log(`Patched ${path.relative(MOBILE_DIR, manifestPath)} (added networkSecurityConfig)`);
}
}
// nodejs-mobile-cordova's build.gradle sets up a CMake build with
// `cmake.path = "libs/cdvnodejsmobile/CMakeLists.txt"` (relative to each
// module's projectDir) for both `app` and `capacitor-cordova-android-plugins`.
// `cap sync` doesn't replicate the plugin.xml `<source-file target-dir="libs/cdvnodejsmobile">`
// copies, so populate that directory ourselves in both modules.
const NODEJS_MOBILE = path.join(MOBILE_DIR, 'node_modules', 'nodejs-mobile-cordova');
const ABIS = ['arm64-v8a', 'armeabi-v7a', 'x86', 'x86_64'];
function gunzipFile(src, dest) {
const data = fs.readFileSync(src);
fs.writeFileSync(dest, zlib.gunzipSync(data));
}
function populateNativeLibs(moduleDir) {
const dest = path.join(moduleDir, 'libs', 'cdvnodejsmobile');
rmrf(dest);
fs.mkdirSync(dest, { recursive: true });
for (const f of ['CMakeLists.txt']) {
fs.copyFileSync(path.join(NODEJS_MOBILE, 'src', 'android', f), path.join(dest, f));
}
for (const f of ['native-lib.cpp']) {
fs.copyFileSync(path.join(NODEJS_MOBILE, 'src', 'android', 'jni', f), path.join(dest, f));
}
for (const f of ['cordova-bridge.cpp', 'cordova-bridge.h']) {
fs.copyFileSync(path.join(NODEJS_MOBILE, 'src', 'common', 'cordova-bridge', f), path.join(dest, f));
}
const libnodeSrc = path.join(NODEJS_MOBILE, 'libs', 'android', 'libnode');
const libnodeDest = path.join(dest, 'libnode');
copyDir(path.join(libnodeSrc, 'include'), path.join(libnodeDest, 'include'));
for (const abi of ABIS) {
const binDest = path.join(libnodeDest, 'bin', abi);
fs.mkdirSync(binDest, { recursive: true });
gunzipFile(
path.join(libnodeSrc, 'bin', abi, 'libnode.so.gz'),
path.join(binDest, 'libnode.so')
);
}
console.log(`Populated ${path.relative(MOBILE_DIR, dest)} (native CMake build sources + libnode)`);
}
for (const moduleDir of [
path.join(MOBILE_DIR, 'android', 'app'),
path.join(MOBILE_DIR, 'android', 'capacitor-cordova-android-plugins'),
]) {
populateNativeLibs(moduleDir);
}
const targets = [
path.join(MOBILE_DIR, 'android', 'capacitor-cordova-android-plugins', 'src', 'main', 'assets'),
path.join(MOBILE_DIR, 'android', 'app', 'src', 'main', 'assets'),
];
// The plugin's copyNodeJSAssets() also expects a `nodejs-mobile-cordova-assets`
// folder at the root of the assets (builtin modules like cordova-bridge);
// without it, assetManager.list() returns empty and the fallback
// assetManager.open() throws FileNotFoundException.
const BUILTIN_ASSETS_SRC = path.join(NODEJS_MOBILE, 'install', 'nodejs-mobile-cordova-assets');
for (const assetsRoot of targets) {
const builtinDest = path.join(assetsRoot, 'nodejs-mobile-cordova-assets');
rmrf(builtinDest);
copyDir(BUILTIN_ASSETS_SRC, builtinDest);
const dest = path.join(assetsRoot, 'www', 'nodejs-project');
console.log(`Copying ${SRC} -> ${dest}`);
rmrf(dest);
copyDir(SRC, dest);
// dir.list/file.list, paths relative to assetsRoot (e.g. "www/nodejs-project/main.js"),
// matching the format produced by the cordova after_prepare hook.
const dirs = [];
const files = [];
enumerate(dest, 'www/nodejs-project', dirs, files);
fs.writeFileSync(path.join(assetsRoot, 'dir.list'), dirs.join('\n') + '\n');
fs.writeFileSync(path.join(assetsRoot, 'file.list'), files.join('\n') + '\n');
console.log(`Wrote ${dirs.length} dirs / ${files.length} files to ${path.relative(MOBILE_DIR, assetsRoot)}/{dir,file}.list`);
}