Files
windows95/tools/update-v86.js
Felix Rieseberg 89c0a8575d docs: update-v86.js rewrite + SMB/v86/testing knowledge (#348)
* Rewrite update-v86.js for the current build pipeline

Both v86 fixes we've been carrying are now real branches on the fork
(PR #1540 electron-renderer-fs-loader, PR #1541 ide-shared-registers)
combined as felixrieseberg/v86:windows95-base. update-v86.js no longer
needs to patch sources at build time — it just builds whatever's checked
out and copies the result.

Gone: the fallback-to-copy.sh path, the skew-day check, the structural
regex patches for load_file/exportSymbol/fetch-bind, the phantom-slave
guard (both are in the branch), the --js-only flag. If you don't have
cargo/clang/java/closure, the script fails loudly — no silent fallbacks.

Added: sanity checks against the installed libv86.js for the invariants
our SMB integration and parcel-build shim depend on, so if upstream
changes something load-bearing we see it as a WARN at update time
instead of a runtime failure.

Tested end-to-end: 5/5 sanity checks, fresh boot SUCCESS in 32s.

* docs and skills: capture SMB/v86/testing knowledge from the session

- docs/smb-share.md: user-facing SMB integration overview (how to mount in
  Win95, what's implemented, what's not). Points at the protocol-level
  README inside src/renderer/smb/ for wire-level gotchas.
- .claude/skills/probe-win95: how to boot and test the VM without a human.
  Env vars, file locations, failure modes, the XT scancode keyboard trick,
  bisect rules of thumb.
- .claude/skills/update-v86: how to pull upstream v86 changes, what the
  five sanity-check WARNs mean, how to retire the fork branches when the
  PRs merge upstream.

.gitignore narrowed to exclude only the runtime dirs (scheduled_tasks.lock,
worktrees) instead of the whole .claude/ tree, so skills can be committed.
2026-04-11 09:33:51 -07:00

167 lines
6.0 KiB
JavaScript

#!/usr/bin/env node
/**
* Build and install v86 (wasm + libv86.js + BIOS) from a local checkout.
*
* Usage:
* node tools/update-v86.js [path/to/v86]
*
* Defaults to ../v86 relative to this repo. Expects the checkout to be on
* `fork/windows95-base` (or a branch with both bug fixes applied):
*
* - `electron-renderer-fs-loader` — file loader uses require() instead of
* dynamic import (needed for Electron renderer, PR #1540)
* - `ide-shared-registers` — ATA Command Block register writes hit both
* master and slave, as the spec says they should (fixes Win95/98 boot
* on disks >535MiB, PR #1541)
*
* If either PR is merged into upstream, rebase windows95-base and drop it.
*
* Prereqs (all must be installed — no fallbacks):
* cargo + rustup target add wasm32-unknown-unknown
* clang
* java (e.g. brew install openjdk)
* <v86>/closure-compiler/compiler.jar (v20210601 — pinned by v86's Makefile)
*/
const fs = require('fs');
const path = require('path');
const { execFileSync } = require('child_process');
const WINDOWS95_DIR = path.resolve(__dirname, '..');
const V86_DIR = process.argv[2]
? path.resolve(process.argv[2])
: path.resolve(WINDOWS95_DIR, '../v86');
const LIB_DIR = path.join(WINDOWS95_DIR, 'src/renderer/lib');
const BIOS_DIR = path.join(WINDOWS95_DIR, 'bios');
const JAVA_BIN = '/opt/homebrew/opt/openjdk/bin/java';
function require_tool(cmd, desc) {
try {
execFileSync('sh', ['-c', `command -v ${cmd}`], { stdio: 'ignore' });
} catch {
throw new Error(`Missing prerequisite: ${desc} (${cmd} not on PATH)`);
}
}
function run(cmd, args, opts = {}) {
execFileSync(cmd, args, { stdio: 'inherit', ...opts });
}
function check_prereqs() {
require_tool('cargo', 'rust/cargo');
require_tool('clang', 'clang');
// cargo needs the wasm32 target
const targets = execFileSync('rustup', ['target', 'list', '--installed']).toString();
if (!targets.includes('wasm32-unknown-unknown')) {
throw new Error('Missing rust target. Run: rustup target add wasm32-unknown-unknown');
}
// Java comes from homebrew openjdk on macOS — the v86 Makefile invokes `java`
// directly, so we have to put the homebrew java on PATH for its make calls
// (or install openjdk into the system). We check for an explicit binary so
// the error is clear.
if (!fs.existsSync(JAVA_BIN)) {
throw new Error(`Missing java at ${JAVA_BIN}. Install with: brew install openjdk`);
}
const closureJar = path.join(V86_DIR, 'closure-compiler', 'compiler.jar');
if (!fs.existsSync(closureJar)) {
throw new Error(
`Missing Closure compiler at ${closureJar}.\n` +
`Download v20210601 (pinned by v86's Makefile):\n` +
` mkdir -p ${path.dirname(closureJar)}\n` +
` curl -sL https://repo1.maven.org/maven2/com/google/javascript/closure-compiler/v20210601/closure-compiler-v20210601.jar -o ${closureJar}`
);
}
if (!fs.existsSync(path.join(V86_DIR, 'Makefile'))) {
throw new Error(`No v86 checkout at ${V86_DIR}. Pass a path as the first argument or clone copy/v86 there.`);
}
}
function build_v86() {
const env = { ...process.env, PATH: `/opt/homebrew/opt/openjdk/bin:${process.env.PATH}` };
console.log('Building v86.wasm…');
run('make', ['build/v86.wasm'], { cwd: V86_DIR, env });
console.log('Building libv86.js…');
run('make', ['build/libv86.js'], { cwd: V86_DIR, env });
}
function install() {
const copies = [
['build/v86.wasm', 'build/v86.wasm'],
['build/libv86.js', 'libv86.js'],
];
for (const [src, dest] of copies) {
fs.copyFileSync(path.join(V86_DIR, src), path.join(LIB_DIR, dest));
const size = fs.statSync(path.join(LIB_DIR, dest)).size;
console.log(` ${dest}: ${(size / 1024).toFixed(0)} KB`);
}
for (const bios of ['seabios.bin', 'vgabios.bin']) {
fs.copyFileSync(path.join(V86_DIR, 'bios', bios), path.join(BIOS_DIR, bios));
}
console.log(' seabios.bin + vgabios.bin');
}
/**
* Sanity check the installed files for the invariants our SMB integration
* and Electron renderer depend on. If any of these fail, v86 changed under us
* and src/renderer/smb/index.ts probably needs updating — see the README at
* src/renderer/smb/README.md for why.
*/
function sanity_check() {
const js = fs.readFileSync(path.join(LIB_DIR, 'libv86.js'), 'utf-8');
const checks = [
// The electron-renderer-fs-loader fix: don't use dynamic import for fs
[!/await import\("node:/.test(js),
'libv86.js uses `await import("node:...")` — the Electron renderer fs loader PR was reverted?'],
// The ide-shared-registers fix: writes go to both master and slave
// (minified has no spaces: `this.master.features_reg=(this.master...`)
[/this\.master\.features_reg=\(this\.master\.features_reg/.test(js),
'libv86.js ide.js did not get the shared-register fix — is the windows95-base branch still in sync?'],
// Export pattern still shims the way parcel-build expects
[js.includes('module.exports') && js.includes('window'),
'libv86.js export pattern changed — check the runtime shim in parcel-build.js'],
// SMB integration needs the tcp-connection bus event (new API path in index.ts)
[js.includes('tcp-connection'),
'libv86.js no longer fires the tcp-connection bus event — SMB will fall back to the old-API theft hack'],
// Old-API fallback still present for defense in depth
[js.includes('on_tcp_connection'),
'libv86.js no longer has on_tcp_connection — harmless but surprising'],
];
let passed = 0;
for (const [ok, msg] of checks) {
if (ok) passed++;
else console.warn(' WARN:', msg);
}
console.log(` sanity: ${passed}/${checks.length} checks passed`);
}
function main() {
console.log(`v86 checkout: ${V86_DIR}`);
const head = execFileSync('git', ['log', '-1', '--format=%h %s'], { cwd: V86_DIR }).toString().trim();
console.log(` ${head}`);
check_prereqs();
build_v86();
install();
sanity_check();
console.log('done');
}
try { main(); }
catch (e) {
console.error('✗', e.message);
process.exit(1);
}