const fs = require('node:fs');
const path = require('node:path');
const assert = require('node:assert/strict');
const crypto = require('node:crypto');
const abiNames = ['arm64-v8a', 'armeabi-v7a', 'x86_64'];
const macros = {
EVENT__NUMERIC_VERSION: '0x02010d00',
EVENT__PACKAGE_VERSION: '"2.1.13"',
EVENT__VERSION_PATCH: '13',
EVENT__VERSION: '"2.1.13-stable"',
};
function within(root, candidate) {
const relative = path.relative(root, candidate);
assert(relative && relative !== '..' && !relative.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relative), `Path escapes project: ${candidate}`);
}
function files(root, relative = '') {
return fs.readdirSync(path.join(root, relative), {withFileTypes: true}).flatMap(entry => {
assert(!entry.isSymbolicLink(), `Unexpected source symlink: ${entry.name}`);
const name = path.join(relative, entry.name);
return entry.isDirectory() ? files(root, name) : [name];
}).sort();
}
const hash = file => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
function sameTree(source, target, sourceFiles) {
if (!fs.existsSync(target)) return false;
const targetFiles = files(target);
return JSON.stringify(targetFiles) === JSON.stringify(sourceFiles) &&
sourceFiles.every(file => hash(path.join(source, file)) === hash(path.join(target, file)));
}
function main() {
const args = process.argv.slice(2);
assert(args.length >= 1 && args.length <= 2 && (!args[1] || args[1] === '--check'),
'Usage: node patch-libevent-2113.cjs <harmony-root> [--check]');
const checkOnly = args[1] === '--check';
const root = fs.realpathSync(args[0]);
const source = fs.realpathSync(path.join(__dirname, '../vendor/libevent-2.1.13-src'));
assert(/^Changes in version 2\.1\.13-stable/m.test(fs.readFileSync(path.join(source, 'ChangeLog'), 'utf8')),
'Vendor source is not libevent 2.1.13-stable');
assert(fs.existsSync(path.join(source, 'LICENSE')), 'Vendor LICENSE missing');
const sourceFiles = files(source);
const candidates = ['entry/oh_modules', 'oh_modules'].map(prefix =>
path.join(root, prefix, '@rnoh/react-native-openharmony/src/main/cpp'));
const dirs = [...new Set(candidates.filter(p => fs.existsSync(p)).map(p => fs.realpathSync(p)))];
assert(dirs.length, 'Source RNOH missing; run ohpm install --all with react_native_openharmony.har');
for (const prefix of ['entry/oh_modules/.ohpm', 'oh_modules/.ohpm']) {
const store = path.join(root, prefix);
if (!fs.existsSync(store)) continue;
for (const name of fs.readdirSync(store).filter(name => name.startsWith('@rnoh+react-native-openharmony@'))) {
const manifest = path.join(store, name, 'oh_modules/@rnoh/react-native-openharmony/oh-package.json5');
within(root, fs.realpathSync(manifest));
assert.equal(JSON.parse(fs.readFileSync(manifest, 'utf8')).version, '0.72.143',
`Stale RNOH store entry: ${manifest}`);
}
}
const plans = dirs.map(cpp => {
within(root, cpp);
const manifest = path.resolve(cpp, '../../../oh-package.json5');
within(root, fs.realpathSync(manifest));
assert.equal(JSON.parse(fs.readFileSync(manifest, 'utf8')).version, '0.72.143',
`RNOH must be 0.72.143: ${manifest}`);
const target = path.join(cpp, 'third-party/libevent');
within(root, fs.realpathSync(path.dirname(target)));
if (fs.existsSync(target)) {
assert(!fs.lstatSync(target).isSymbolicLink(), `Refusing linked libevent tree: ${target}`);
within(root, fs.realpathSync(target));
}
const cmake = fs.readFileSync(path.join(cpp, 'CMakeLists.txt'), 'utf8');
const sourceNames = [...cmake.matchAll(/\$\{libevent_src_dir\}\/([^"\s]+\.c)/g)].map(m => m[1]);
assert(sourceNames.length > 0, 'No libevent compile sources found in RNOH CMake');
sourceNames.forEach(name => assert(sourceFiles.includes(name), `Missing CMake source: ${name}`));
const configs = abiNames.map(abi => {
const file = path.join(cpp, 'patches/libevent', abi, 'event2/event-config.h');
within(root, fs.realpathSync(file));
const original = fs.readFileSync(file, 'utf8');
let updated = original;
for (const [name, value] of Object.entries(macros)) {
const pattern = new RegExp(`^#define[ \\t]+${name}[ \\t]+[^\\r\\n]+`, 'gm');
assert.equal([...updated.matchAll(pattern)].length, 1, `Missing/duplicate ${name}: ${file}`);
updated = updated.replace(pattern, `#define ${name} ${value}`);
}
return {file, original, updated};
});
return {cpp, target, configs, compileSources: sourceNames.length};
});
for (const {cpp, target, configs, compileSources} of plans) {
const identical = sameTree(source, target, sourceFiles);
if (checkOnly) {
assert(identical && configs.every(c => c.original === c.updated),
`libevent patch missing or stale in ${cpp}; run this script without --check after ohpm install`);
} else {
if (!identical) {
fs.rmSync(target, {recursive: true, force: true});
fs.cpSync(source, target, {recursive: true});
}
configs.forEach(c => { if (c.original !== c.updated) fs.writeFileSync(c.file, c.updated); });
assert(sameTree(source, target, sourceFiles), `Source copy verification failed: ${target}`);
configs.forEach(c => assert.equal(fs.readFileSync(c.file, 'utf8'), c.updated));
}
console.log(JSON.stringify({mode: checkOnly ? 'check' : 'patch', cpp,
rnoh: '0.72.143', libevent: '2.1.13-stable', vendorFiles: sourceFiles.length,
compileSources, abis: abiNames, macros}));
}
}
try { main(); } catch (error) { console.error(error.message); process.exitCode = 1; }