apply.cjs 5.71 KB
// Idempotent post-install patches for running @deepseek-ai/dsh on Termux/Android (bionic).
// Re-run safely after every `npm install`. If dsh's upstream source changes shape,
// each patch just logs a skip instead of corrupting the file.
'use strict';
const fs = require('fs');
const path = require('path');
const { spawnSync } = require('child_process');

const projectRoot = path.join(__dirname, '..');
const nodeModules = path.join(projectRoot, 'node_modules');

function patchFile(file, oldStr, newStr, label) {
  if (!fs.existsSync(file)) {
    console.log(`[dsh-patch] ${label}: file not found, skip (${file})`);
    return;
  }
  const content = fs.readFileSync(file, 'utf8');
  if (content.includes(newStr)) {
    console.log(`[dsh-patch] ${label}: already patched, skip`);
    return;
  }
  if (!content.includes(oldStr)) {
    console.error(`[dsh-patch] ${label}: expected original text not found - dsh internals may have changed, skipping patch`);
    return;
  }
  fs.writeFileSync(file, content.replace(oldStr, newStr));
  console.log(`[dsh-patch] ${label}: patched`);
}

// --- Patch 1: dsh-app-boot's internalModules() unconditionally requires the
// native "node-addon-require-builtin" addon, which has no Android prebuild and
// deliberately refuses to compile one locally. cordis-plugin-loader already
// has a --expose-internals fast path elsewhere; mirror it here. ---
patchFile(
  path.join(nodeModules, '@deepseek-ai/dsh-app-boot/lib/index.js'),
  `function internalModules() {
	const addon = createRequire(import.meta.url)("node-addon-require-builtin");
	const esmModule = addon.requireBuiltin("internal/modules/esm/loader");
	const cjsModule = addon.requireBuiltin("internal/modules/cjs/loader");
	const cjsHelpers = addon.requireBuiltin("internal/modules/helpers");
	const esmUtils = addon.requireBuiltin("internal/modules/esm/utils");
	const esmResolve = addon.requireBuiltin("internal/modules/esm/resolve");`,
  `function internalModules() {
	const req = createRequire(import.meta.url);
	const requireBuiltin = process.execArgv.includes("--expose-internals")
		? (id) => req(id)
		: (() => { const addon = req("node-addon-require-builtin"); return (id) => addon.requireBuiltin(id); })();
	const esmModule = requireBuiltin("internal/modules/esm/loader");
	const cjsModule = requireBuiltin("internal/modules/cjs/loader");
	const cjsHelpers = requireBuiltin("internal/modules/helpers");
	const esmUtils = requireBuiltin("internal/modules/esm/utils");
	const esmResolve = requireBuiltin("internal/modules/esm/resolve");`,
  'dsh-app-boot internalModules() --expose-internals fallback',
);

// --- Patch 2: node-addon-system's flock.js only allowlists linux/darwin.
// Add an android branch that loads a locally-compiled binding instead of a
// (nonexistent) @deepseek-ai/node-addon-system-android-arm64 package. ---
const flockFile = path.join(nodeModules, '@deepseek-ai/node-addon-system/lib/flock.js');
patchFile(
  flockFile,
  `function loadBinding() {
    if (binding)
        return binding;
    const { platform, arch } = process;
    if (platform !== 'linux' && platform !== 'darwin') {`,
  `function loadBinding() {
    if (binding)
        return binding;
    const { platform, arch } = process;
    if (platform === 'android') {
        const require = createRequire(import.meta.url);
        binding = require(join(import.meta.dirname, '..', 'bin', 'android-arm64', 'system.node'));
        return binding;
    }
    if (platform !== 'linux' && platform !== 'darwin') {`,
  'node-addon-system flock.js android branch',
);

// --- Patch 3: compile flock.c (published in the package, BSD-3-Clause) into
// bin/android-arm64/system.node with Termux's own toolchain. Node-gyp's
// bundled common.gypi references ${android_ndk_path} unconditionally on
// Android hosts; Termux's own CMake/gyp Android detection additionally needs
// $PREFIX set so it can find $PREFIX/include/android/api-level.h. ---
const systemPkgDir = path.join(nodeModules, '@deepseek-ai/node-addon-system');
const srcFile = path.join(systemPkgDir, 'src/flock.c');
const outBinary = path.join(systemPkgDir, 'bin', 'android-arm64', 'system.node');

if (process.platform !== 'android') {
  console.log('[dsh-patch] not running on Android - skipping flock native build');
} else if (!fs.existsSync(srcFile)) {
  console.log(`[dsh-patch] flock.c not found (${srcFile}), skip native build`);
} else if (fs.existsSync(outBinary)) {
  console.log('[dsh-patch] flock system.node already built, skip');
} else {
  const prefix = process.env.PREFIX || '/data/data/com.termux/files/usr';
  const buildDir = path.join(__dirname, '.flock-build');
  fs.mkdirSync(buildDir, { recursive: true });
  fs.copyFileSync(srcFile, path.join(buildDir, 'flock.c'));
  fs.writeFileSync(
    path.join(buildDir, 'binding.gyp'),
    JSON.stringify({ targets: [{ target_name: 'system', sources: ['flock.c'], defines: ['NAPI_VERSION=8'] }] }, null, 2),
  );
  fs.writeFileSync(
    path.join(buildDir, 'package.json'),
    JSON.stringify({ name: 'flock-build', version: '1.0.0', private: true, gypfile: true }, null, 2),
  );
  const nodeGyp = path.join(prefix, 'lib/node_modules/npm/node_modules/node-gyp/bin/node-gyp.js');
  const env = { ...process.env, PREFIX: prefix, npm_config_android_ndk_path: prefix };
  console.log('[dsh-patch] compiling flock native addon for android-arm64...');
  const result = spawnSync(process.execPath, [nodeGyp, 'rebuild'], { cwd: buildDir, env, stdio: 'inherit' });
  if (result.status === 0) {
    fs.mkdirSync(path.dirname(outBinary), { recursive: true });
    fs.copyFileSync(path.join(buildDir, 'build/Release/system.node'), outBinary);
    console.log('[dsh-patch] flock system.node: built and installed');
  } else {
    console.error('[dsh-patch] WARNING: flock native build failed; session file-locking (flock) will not work');
  }
}