clank-together/tools/build-td/build.mjs
Clank Together 125f5c4e7d Batches, auto-merge, standalone td, one-line install, self-update, server Ghidra
- 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
2026-10-18 02:13:09 +02:00

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'));