- Batches: claim every open function of a unit or address range (td claim --unit/--range), submit the file once; one compile, per-function routing, merged as a whole. A failing function returns the batch; peer verdicts carry over to functions whose compiled code is unchanged (code fingerprints). Submissions may not change functions they do not hold. - Byte-identical functions are approved and merged without human review (matchPolicy.autoMergeIdentical); peer-confirmed ones need one click per batch. - Remote check: td check compiles on the server when there is no local toolchain. - /api/original: assembly and pseudocode of the original from the server's Ghidra export (objdump before an export exists); td disasm/decompile fall back to it. - td as a standalone executable (Node single executable application; tools/build-td), one-line installers (install.ps1 / install.sh) with checksum verification, td connect for any MCP client, td mcp --workspace, self-update (td update, hourly check). - Server-side Ghidra project: built once per version, refreshed after merges, downloadable for contributors (td ghidra download); JDK 21 and the pinned Ghidra in deploy/install.sh. - Agent instructions: work continuously, batches first, peer reviews in between. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RNwtXPYqFu3oKaaXwYwNWW
79 lines
4.1 KiB
JavaScript
79 lines
4.1 KiB
JavaScript
#!/usr/bin/env node
|
|
// Builds `td` as standalone executables (Node single executable applications): contributors need
|
|
// no Node.js. Output in dist/: td-windows-x64.exe, td-linux-x64, SHA256SUMS.
|
|
//
|
|
// node tools/build-td/build.mjs
|
|
//
|
|
// Needs network access once (esbuild and postject through npx, the Linux Node runtime from
|
|
// nodejs.org, checked against its published SHA256SUMS). Uses the Node runtime that runs this
|
|
// script for Windows, so build with the same Node version as NODE_VERSION.
|
|
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { execFileSync } from 'node:child_process';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
const DIST = path.join(ROOT, 'dist');
|
|
const NODE_VERSION = 'v22.21.0';
|
|
const FUSE = 'NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2';
|
|
const SCRIPTS = ['ApplySymbols.java', 'DecompileAt.java', 'DisassembleAt.java', 'ExportFunctions.java', 'ExportOriginals.java'];
|
|
const npx = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
const sh = (cmd, args) => execFileSync(cmd, args, { cwd: ROOT, stdio: 'inherit', shell: process.platform === 'win32' });
|
|
const sha256 = (file) => crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex');
|
|
|
|
if (process.version !== NODE_VERSION) throw new Error(`build with Node ${NODE_VERSION} (this is ${process.version})`);
|
|
fs.rmSync(DIST, { recursive: true, force: true });
|
|
fs.mkdirSync(DIST, { recursive: true });
|
|
|
|
// 1. One CommonJS file (single executable applications run a CommonJS script).
|
|
sh(npx, ['--yes', 'esbuild@0.24.0', 'src/client/cli.js', '--bundle', '--platform=node', '--target=node22', '--format=cjs', '--log-level=warning', `--outfile=${path.join(DIST, 'td.cjs')}`]);
|
|
|
|
// 2. The preparation blob: code plus the embedded Ghidra scripts. No code cache or snapshot, so
|
|
// the same blob works for every platform's Node runtime.
|
|
const config = {
|
|
main: path.join(DIST, 'td.cjs'),
|
|
output: path.join(DIST, 'td.blob'),
|
|
disableExperimentalSEAWarning: true,
|
|
useCodeCache: false,
|
|
useSnapshot: false,
|
|
assets: Object.fromEntries(SCRIPTS.map((s) => [s, path.join(ROOT, 'ghidra_scripts', s)])),
|
|
};
|
|
fs.writeFileSync(path.join(DIST, 'sea-config.json'), JSON.stringify(config, null, 2));
|
|
execFileSync(process.execPath, ['--experimental-sea-config', path.join(DIST, 'sea-config.json')], { stdio: 'inherit' });
|
|
|
|
// 3. Runtimes: this Node for Windows (when building on Windows), the official Linux build otherwise.
|
|
const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'td-build-'));
|
|
const targets = [];
|
|
if (process.platform === 'win32') {
|
|
const exe = path.join(DIST, 'td-windows-x64.exe');
|
|
fs.copyFileSync(process.execPath, exe);
|
|
targets.push(exe);
|
|
}
|
|
{
|
|
const name = `node-${NODE_VERSION}-linux-x64`;
|
|
const base = `https://nodejs.org/dist/${NODE_VERSION}`;
|
|
const sums = await (await fetch(`${base}/SHASUMS256.txt`)).text();
|
|
const want = new RegExp(`^([0-9a-f]{64})\\s+${name}\\.tar\\.gz$`, 'm').exec(sums)?.[1];
|
|
if (!want) throw new Error('no checksum for the Linux runtime');
|
|
const tgz = path.join(tmp, `${name}.tar.gz`);
|
|
fs.writeFileSync(tgz, Buffer.from(await (await fetch(`${base}/${name}.tar.gz`)).arrayBuffer()));
|
|
if (sha256(tgz) !== want) throw new Error('Linux runtime checksum mismatch');
|
|
execFileSync(process.platform === 'win32' ? path.join(process.env.SystemRoot, 'System32', 'tar.exe') : 'tar', ['-xzf', tgz, '-C', tmp, `${name}/bin/node`], { stdio: 'inherit' });
|
|
const bin = path.join(DIST, 'td-linux-x64');
|
|
fs.copyFileSync(path.join(tmp, name, 'bin', 'node'), bin);
|
|
targets.push(bin);
|
|
}
|
|
|
|
// 4. Inject the blob into each runtime.
|
|
for (const t of targets) {
|
|
const args = ['--yes', 'postject@1.0.0-alpha.6', t, 'NODE_SEA_BLOB', config.output, '--sentinel-fuse', FUSE];
|
|
sh(npx, args);
|
|
}
|
|
|
|
fs.writeFileSync(path.join(DIST, 'SHA256SUMS'), targets.map((t) => `${sha256(t)} ${path.basename(t)}`).join('\n') + '\n');
|
|
for (const f of ['td.cjs', 'td.blob', 'sea-config.json']) fs.rmSync(path.join(DIST, f));
|
|
fs.rmSync(tmp, { recursive: true, force: true });
|
|
console.log(fs.readFileSync(path.join(DIST, 'SHA256SUMS'), 'utf8'));
|