From 65acaa91b1f3beed73bbc417c81934f096b05105 Mon Sep 17 00:00:00 2001 From: Steven Hugg Date: Tue, 29 Sep 2026 14:25:24 +0200 Subject: [PATCH] vscode: bundle CLI --- extension/.vscodeignore | 1 + extension/CHANGELOG.md | 4 ++ extension/README.md | 5 ++ extension/docs/building.md | 27 +++++++++ extension/docs/projects.md | 4 +- extension/package.json | 5 ++ extension/scripts/build.mjs | 4 +- extension/src/assets.ts | 3 + extension/src/cli.ts | 33 +++++++++++ extension/src/extension.ts | 60 ++++++++++++++++++-- extension/src/terminalcli.ts | 88 ++++++++++++++++++++++++++++++ extension/test/terminalcli.test.ts | 47 ++++++++++++++++ src/tools/8bws.ts | 3 +- src/tools/emutarget.ts | 3 +- src/tools/testlib.ts | 9 +-- src/tools/toolroot.ts | 10 ++++ 16 files changed, 293 insertions(+), 13 deletions(-) create mode 100644 extension/src/cli.ts create mode 100644 extension/src/terminalcli.ts create mode 100644 extension/test/terminalcli.test.ts create mode 100644 src/tools/toolroot.ts diff --git a/extension/.vscodeignore b/extension/.vscodeignore index 5e7de304..0ce7d5a1 100644 --- a/extension/.vscodeignore +++ b/extension/.vscodeignore @@ -13,6 +13,7 @@ !out/extension.js !out/buildworker.js !out/emuworker.js +!out/8bws.js !out/presets.json !out/assets.json !out/assets/8bitworkshop-base-*.tar.br diff --git a/extension/CHANGELOG.md b/extension/CHANGELOG.md index 6dec2b42..618ff9d9 100644 --- a/extension/CHANGELOG.md +++ b/extension/CHANGELOG.md @@ -17,5 +17,9 @@ First preview. frame, and can step back. - `.s`, `.asm`, `.inc` and `.a` files in a project get the highlighting for the platform's CPU. `.acme` files get 6502 highlighting. +- An `8bws` command in the integrated terminal builds and runs programs + from the command line, for scripts and AI agents. +- New Project writes a README with build instructions and the link that + platform detection reads. - The platform list shows the 8bitworkshop book for each platform that has one, with a button to open it. diff --git a/extension/README.md b/extension/README.md index 900b24d4..9caadb70 100644 --- a/extension/README.md +++ b/extension/README.md @@ -50,6 +50,10 @@ To look at the included examples without copying them, run **8bitworkshop: Open To use code you already have, open its folder: the extension detects the appropriate platform and main file. +To build from the command line, or let an AI agent build and test your +program, use the `8bws` command in VS Code's integrated terminal. See +[Building from the terminal](docs/building.md#building-from-the-terminal). + ## Settings Project settings, saved in `.vscode/settings.json`: @@ -71,6 +75,7 @@ Your local user settings: | `8bitworkshop.exportRom` | Write each successful build's ROM to the export folder | | `8bitworkshop.toolchainPath` | Use a local 8bitworkshop checkout instead of the included toolchains | | `8bitworkshop.assetUrl` | Server or local directory to download toolchains from, tried before the default servers | +| `8bitworkshop.terminalCommand` | Add the `8bws` command to the integrated terminal (on by default) | ## Commands diff --git a/extension/docs/building.md b/extension/docs/building.md index 0238b4ec..ec511b5c 100644 --- a/extension/docs/building.md +++ b/extension/docs/building.md @@ -56,6 +56,33 @@ then writes it to the `8bitworkshop.exportRomPath` folder (`bin` by default), next to the main file, with the platform's usual extension such as `.nes` or `.a26`. +## Building from the terminal + +VS Code's integrated terminal has an `8bws` command that builds and runs +programs with the same toolchains and emulators as the extension. It +needs no Node.js install. Scripts and AI agents that run in the +terminal, such as Claude Code, can use it to check their work: + +```bash +8bws build game.c -o game.nes # compile to a ROM +8bws build --check game.c # just report errors +8bws run game.c --frames 120 --png screen.png +8bws run game.c -e "run 60; screen" # a run script; see 8bws help +8bws help +``` + +Add `--json` for machine-readable output on stdout. Without `--platform`, +`8bws` uses [platform detection](detection.md#from-the-command-line); the +README that **New Project** writes names the platform, so a new project +needs no `--platform`. Library files that aren't in your folder come from +the examples, as they do in the editor. + +The command is a launcher script in the extension's storage folder, which +the extension adds to `PATH` for each new terminal. Terminals opened +before the extension started don't have it; open a new one. To turn it +off, set `8bitworkshop.terminalCommand` to `false`. The first run in a +while may take a few seconds while it unpacks the toolchains. + ## Where included files come from 8bitworkshop looks for files your program includes or links in two diff --git a/extension/docs/projects.md b/extension/docs/projects.md index ccbbf18b..942cfbd0 100644 --- a/extension/docs/projects.md +++ b/extension/docs/projects.md @@ -8,7 +8,9 @@ your choices. A folder becomes an 8bitworkshop project in one of these ways: -- **New Project** or **Copy to Workspace** creates it. +- **New Project** or **Copy to Workspace** creates it. It also writes a + `README.md`, unless the folder has one, with the 8bitworkshop link + below and how to build from the terminal. - You choose **Set as Main File** on a file, or run a file that has no project yet, and confirm its platform. - You accept a detected platform when you open a folder. See diff --git a/extension/package.json b/extension/package.json index 44604ed1..f7e7abec 100644 --- a/extension/package.json +++ b/extension/package.json @@ -268,6 +268,11 @@ ], "markdownDescription": "When a successful build restarts the running game." }, + "8bitworkshop.terminalCommand": { + "type": "boolean", + "default": true, + "markdownDescription": "Add the `8bws` command to the integrated terminal, to build and run programs from the command line or an AI agent. Open a new terminal after changing this." + }, "8bitworkshop.exportRom": { "type": "boolean", "default": false, diff --git a/extension/scripts/build.mjs b/extension/scripts/build.mjs index 8afad8e3..abd25409 100644 --- a/extension/scripts/build.mjs +++ b/extension/scripts/build.mjs @@ -4,7 +4,8 @@ // node scripts/build.mjs --watch # rebuild on change // // extension.js stays small; builds and emulation run in worker threads -// (buildworker.js, emuworker.js) that start on first use. presetindex.js +// (buildworker.js, emuworker.js) that start on first use. 8bws.js is the +// command line tool for the integrated terminal (src/cli.ts). presetindex.js // writes out/presets.json and syntaxes.js writes out/syntaxes/ (npm run // build runs both). import esbuild from 'esbuild'; @@ -51,6 +52,7 @@ const ctx = await esbuild.context({ extension: 'src/extension.ts', buildworker: 'src/buildworker.ts', emuworker: 'src/emuworker.ts', + '8bws': 'src/cli.ts', presetindex: 'scripts/presetindex.ts', assetpack: 'scripts/assetpack.ts', syntaxes: 'scripts/syntaxes.ts', diff --git a/extension/src/assets.ts b/extension/src/assets.ts index 4355d9dc..036a2c02 100644 --- a/extension/src/assets.ts +++ b/extension/src/assets.ts @@ -9,6 +9,9 @@ import { createHash } from 'crypto'; import { AssetManifest, PackInfo, readPack } from './assetpacks'; /** Reports download progress: bytes so far of `total`. */ +/** The 8bitworkshop asset servers, tried in order after 8bitworkshop.assetUrl. */ +export const ASSET_URLS = ['https://sehugg.github.io/8bitworkshop/vscode/', 'https://8bitworkshop.com/vscode/']; + export type ProgressFn = (pack: string, received: number, total: number) => void; export class AssetStore { diff --git a/extension/src/cli.ts b/extension/src/cli.ts new file mode 100644 index 00000000..f00aecb1 --- /dev/null +++ b/extension/src/cli.ts @@ -0,0 +1,33 @@ +// cli - the 8bws command in the integrated terminal (out/8bws.js). The +// launcher script from terminalcli.ts runs it with VS Code's own Node. It +// unpacks the bundled toolchains if the extension hasn't yet, then runs the +// repo's CLI (src/tools/8bws.ts) against them. + +import * as fs from 'fs'; +import * as path from 'path'; +import { ASSET_URLS, AssetStore } from './assets'; +import type { AssetManifest } from './assetpacks'; + +/** The asset root: toolchainPath or a checkout, else the extension's store. */ +async function toolchainRoot(): Promise { + var cache = process.env.EIGHTBITWORKSHOP_TOOLCHAINS; + if (!cache) throw new Error('Set EIGHTBITWORKSHOP_ROOT or EIGHTBITWORKSHOP_TOOLCHAINS; the 8bws launcher from VS Code sets them.'); + var manifest: AssetManifest = JSON.parse(fs.readFileSync(path.join(__dirname, 'assets.json'), 'utf-8')); + var urls = [path.join(__dirname, 'assets')]; + if (process.env.EIGHTBITWORKSHOP_ASSET_URL) urls.push(process.env.EIGHTBITWORKSHOP_ASSET_URL); + urls.push(...ASSET_URLS); + // stderr, so --json output stays clean + var store = new AssetStore(cache, manifest, urls, msg => process.stderr.write(msg + '\n')); + return store.ensure(Object.keys(manifest.packs)); +} + +async function main() { + if (!process.env.EIGHTBITWORKSHOP_ROOT) process.env.EIGHTBITWORKSHOP_ROOT = await toolchainRoot(); + // 8bws runs as it loads, so load it only once the root is set + await import('../../src/tools/8bws'); +} + +main().catch(e => { + process.stderr.write(`8bws: ${e && e.message || e}\n`); + process.exit(1); +}); diff --git a/extension/src/extension.ts b/extension/src/extension.ts index eb2622a8..1913df61 100644 --- a/extension/src/extension.ts +++ b/extension/src/extension.ts @@ -20,7 +20,9 @@ import { PRESET_SCHEME, PresetFileSystem, Templates } from './templates'; import { chooseForFile, detectDirectoryAt, detectionSummary, describeDetection, describeFinding, dontAskKey, FolderFinding, platformName, scanFolder } from './detection'; import { Detection, classifyFinding, isBuildableSource } from '../../src/common/detect'; import { TOOL_META } from '../../src/common/toolmeta'; -import { AssetStore } from './assets'; +import { ASSET_URLS, AssetStore } from './assets'; +import { projectReadme, writeLauncher } from './terminalcli'; +import type { PlatformInfo } from './presettypes'; import { AssetManifest, packsForPlatform } from './assetpacks'; import { ErrorTelemetry } from './telemetry'; @@ -72,6 +74,10 @@ export function activate(ctx: vscode.ExtensionContext) { templates = new Templates(ctx.extensionPath, () => toolchainRoot()); scheduler = new BuildScheduler(reason => autoBuild(reason)); ctx.subscriptions.push(output, telemetry, diagnostics, status, scope); + setupTerminalCommand(); + ctx.subscriptions.push(vscode.workspace.onDidChangeConfiguration(e => { + if (['terminalCommand', 'toolchainPath', 'assetUrl'].some(k => e.affectsConfiguration(`${CONFIG}.${k}`))) setupTerminalCommand(); + })); ctx.subscriptions.push(vscode.workspace.registerFileSystemProvider(PRESET_SCHEME, new PresetFileSystem(templates), { isCaseSensitive: true, isReadonly: PresetFileSystem.readonlyMessage() })); @@ -160,14 +166,16 @@ function config(uri?: vscode.Uri) { ////// toolchains -// asset servers, tried in order (8bitworkshop.assetUrl goes first) -const ASSET_URLS = ['https://sehugg.github.io/8bitworkshop/vscode/', 'https://8bitworkshop.com/vscode/']; - /** A local copy (toolchainPath, or the repo in development), if there is one. */ function localRoot(): string | null { return config().get('toolchainPath') || findRootDir(context.extensionPath); } +/** Where downloaded and unpacked toolchains live. */ +function toolchainCacheDir(): string { + return path.join(context.globalStorageUri.fsPath, 'toolchains'); +} + /** Toolchain packs shipped inside the extension (see .vscodeignore). */ function bundledAssetsDir(): string { return path.join(context.extensionPath, 'out', 'assets'); @@ -203,7 +211,7 @@ function getAssets(): AssetStore { if (Object.keys(manifest.packs).some(p => hasBundledPack(manifest, p))) urls.push(bundledAssetsDir()); if (custom) urls.push(custom); urls.push(...ASSET_URLS); - assets = new AssetStore(path.join(context.globalStorageUri.fsPath, 'toolchains'), manifest, + assets = new AssetStore(toolchainCacheDir(), manifest, urls, msg => output.appendLine(msg)); } return assets; @@ -231,6 +239,34 @@ async function installPacks(packs: string[]): Promise { }); } +/** + * Put 8bws on the integrated terminal's PATH (8bitworkshop.terminalCommand): + * a launcher in global storage runs out/8bws.js with VS Code's Node. It is + * rewritten on each activation, since VS Code's and the extension's paths + * change with updates. + */ +function setupTerminalCommand() { + var env = context.environmentVariableCollection; + env.clear(); + var script = path.join(context.extensionPath, 'out', '8bws.js'); + if (!config().get('terminalCommand') || !fs.existsSync(script)) return; + var vars: { [name: string]: string } = {}; + var local = localRoot(); + if (local) vars.EIGHTBITWORKSHOP_ROOT = local; + else vars.EIGHTBITWORKSHOP_TOOLCHAINS = toolchainCacheDir(); + var custom = config().get('assetUrl'); + if (custom) vars.EIGHTBITWORKSHOP_ASSET_URL = custom; + var bin = path.join(context.globalStorageUri.fsPath, 'bin'); + try { + writeLauncher(bin, { node: process.execPath, script, env: vars }); + } catch (e) { + output.appendLine(`Cannot write the 8bws launcher to ${bin}: ${e && e.message || e}`); + return; + } + env.description = 'Adds the 8bws command, to build and run 8bitworkshop programs from the terminal.'; + env.prepend('PATH', bin + path.delimiter); +} + async function prepareToolchains() { if (localRoot()) { vscode.window.showInformationMessage(`8bitworkshop: using the toolchains in ${localRoot()}.`); @@ -985,6 +1021,7 @@ async function createFromTemplate(platformId: string, templateId: string, replac } var main = await templates.copy(platform, t, dest.dir, withLibraries); if (!main) return; + await writeReadme(dest.dir, platform, path.relative(dest.dir.fsPath, main.fsPath).split(path.sep).join('/')); var settings = { platform: platform.id, mainFile: main.fsPath }; if (dest.open) { await writeFolderSettings(dest.dir, { platform: platform.id, mainFile: path.basename(main.fsPath) }); @@ -996,6 +1033,19 @@ async function createFromTemplate(platformId: string, templateId: string, replac await openAndBuild(main.fsPath, false); } +/** A README with build instructions, unless the folder has one. */ +async function writeReadme(dir: vscode.Uri, platform: PlatformInfo, mainFile: string) { + var readme = vscode.Uri.joinPath(dir, 'README.md'); + try { + await vscode.workspace.fs.stat(readme); + return; + } catch (e) { + // none yet + } + var text = projectReadme(path.basename(dir.fsPath), platform, mainFile); + await vscode.workspace.fs.writeFile(readme, new TextEncoder().encode(text)); +} + async function closeEditor(uri: vscode.Uri) { for (var group of vscode.window.tabGroups.all) { for (var tab of group.tabs) { diff --git a/extension/src/terminalcli.ts b/extension/src/terminalcli.ts new file mode 100644 index 00000000..3e665435 --- /dev/null +++ b/extension/src/terminalcli.ts @@ -0,0 +1,88 @@ +// terminalcli - the launcher script that puts 8bws on the integrated +// terminal's PATH. It runs out/8bws.js (cli.ts) with VS Code's own Node, so +// users need no Node install. No vscode import, so tests can drive it. + +import * as fs from 'fs'; +import * as path from 'path'; + +export interface LauncherOptions { + /** VS Code's executable; ELECTRON_RUN_AS_NODE makes it plain Node */ + node: string; + /** out/8bws.js */ + script: string; + /** EIGHTBITWORKSHOP_* variables for cli.ts */ + env: { [name: string]: string }; +} + +function shQuote(s: string): string { + return `'${s.replace(/'/g, `'\\''`)}'`; +} + +/** The launcher files for `platform`: a shell script, plus a .cmd on Windows. */ +export function launcherFiles(opts: LauncherOptions, platform: string = process.platform): { name: string, text: string }[] { + var env = { ELECTRON_RUN_AS_NODE: '1', ...opts.env }; + var sh = [ + '#!/bin/sh', + '# 8bitworkshop command line tool, written by the VS Code extension.', + ...Object.entries(env).map(([k, v]) => `export ${k}=${shQuote(v)}`), + `exec ${shQuote(opts.node)} ${shQuote(opts.script)} "$@"`, + '', + ].join('\n'); + var files = [{ name: '8bws', text: sh }]; + if (platform === 'win32') { + files.push({ + name: '8bws.cmd', text: [ + '@echo off', + 'rem 8bitworkshop command line tool, written by the VS Code extension.', + 'setlocal', + ...Object.entries(env).map(([k, v]) => `set "${k}=${v}"`), + `"${opts.node}" "${opts.script}" %*`, + '', + ].join('\r\n'), + }); + } + return files; +} + +/** Write the launcher into `binDir`, touching only files that changed. */ +export function writeLauncher(binDir: string, opts: LauncherOptions, platform: string = process.platform) { + fs.mkdirSync(binDir, { recursive: true }); + for (var f of launcherFiles(opts, platform)) { + var file = path.join(binDir, f.name); + var old = fs.existsSync(file) ? fs.readFileSync(file, 'utf-8') : null; + if (old !== f.text) fs.writeFileSync(file, f.text); + fs.chmodSync(file, 0o755); + } +} + +/** + * README.md for a new project. The 8bitworkshop link names the platform and + * main file, which detection reads (parseReadmeBadge in src/common/detect.ts), + * so `8bws build` needs no --platform. + */ +export function projectReadme(name: string, platform: { id: string, name: string, romext?: string }, mainFile: string): string { + var base = path.basename(mainFile, path.extname(mainFile)); + var rom = base + (platform.romext || '.bin'); + var link = `https://8bitworkshop.com/redir.html?platform=${encodeURIComponent(platform.id)}&file=${encodeURIComponent(mainFile)}`; + return `# ${name} + +A ${platform.name} program made with [8bitworkshop](${link}). + +## Build and run + +In VS Code with the 8bitworkshop extension, open \`${mainFile}\` and click **Run**. + +From VS Code's integrated terminal, in this folder: + +\`\`\`sh +# compile to a ROM +8bws build ${mainFile} -o ${rom} +# run without a window for 120 frames, and save a screenshot +8bws run ${mainFile} --frames 120 --png screen.png +# all commands and options +8bws help +\`\`\` + +Add \`--json\` for machine-readable output. +`; +} diff --git a/extension/test/terminalcli.test.ts b/extension/test/terminalcli.test.ts new file mode 100644 index 00000000..c275ef72 --- /dev/null +++ b/extension/test/terminalcli.test.ts @@ -0,0 +1,47 @@ +import * as assert from 'assert'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { launcherFiles, projectReadme, writeLauncher } from '../src/terminalcli'; +import { parseReadmeBadge } from '../../src/common/detect'; + +describe('terminalcli', () => { + var opts = { node: "/Apps/Code's Helper", script: '/ext/out/8bws.js', env: { EIGHTBITWORKSHOP_TOOLCHAINS: '/store/toolchains' } }; + + it('quotes paths in the shell launcher', () => { + var files = launcherFiles(opts, 'darwin'); + assert.deepStrictEqual(files.map(f => f.name), ['8bws']); + var sh = files[0].text; + assert.match(sh, /^#!\/bin\/sh\n/); + assert.ok(sh.includes(`export ELECTRON_RUN_AS_NODE='1'`)); + assert.ok(sh.includes(`export EIGHTBITWORKSHOP_TOOLCHAINS='/store/toolchains'`)); + assert.ok(sh.includes(`exec '/Apps/Code'\\''s Helper' '/ext/out/8bws.js' "$@"`)); + }); + + it('adds a .cmd launcher on Windows', () => { + var cmd = launcherFiles(opts, 'win32').find(f => f.name === '8bws.cmd'); + assert.ok(cmd); + assert.ok(cmd.text.includes(`"/Apps/Code's Helper" "/ext/out/8bws.js" %*`)); + assert.ok(cmd.text.includes('set "ELECTRON_RUN_AS_NODE=1"')); + }); + + it('writes an executable launcher, rewriting it when paths change', () => { + var dir = fs.mkdtempSync(path.join(os.tmpdir(), '8bws-launcher-')); + try { + writeLauncher(dir, opts, 'linux'); + var file = path.join(dir, '8bws'); + assert.ok(fs.statSync(file).mode & 0o100); + writeLauncher(dir, { ...opts, script: '/ext2/out/8bws.js' }, 'linux'); + assert.ok(fs.readFileSync(file, 'utf-8').includes('/ext2/out/8bws.js')); + } finally { + fs.rmSync(dir, { recursive: true, force: true }); + } + }); + + it('writes a README that detection reads', () => { + var readme = projectReadme('my-game', { id: 'nes', name: 'NES', romext: '.nes' }, 'game.c'); + assert.deepStrictEqual(parseReadmeBadge(readme), { platform: 'nes', mainFile: 'game.c' }); + assert.ok(readme.includes('8bws build game.c -o game.nes')); + assert.ok(projectReadme('x', { id: 'c64', name: 'C64' }, 'a.c').includes('-o a.bin')); + }); +}); diff --git a/src/tools/8bws.ts b/src/tools/8bws.ts index b3bb6f7f..c91cd3f9 100644 --- a/src/tools/8bws.ts +++ b/src/tools/8bws.ts @@ -23,6 +23,7 @@ import type { BuildInfo } from './debugservice'; import { parseSymbolFile } from '../common/symbols/symbolfile'; import { romBytes, type CompileResult } from './testlib'; import { ROM_PLATFORMS } from '../common/detect'; +import { toolRoot } from './toolroot'; interface Args { [key: string]: string | true | string[]; @@ -349,7 +350,7 @@ async function writeScreenshot(video: ReturnType, pngFile // platform detection function presetsDir(): string | null { - for (const dir of [path.resolve('presets'), path.resolve(__dirname, '../../presets')]) { + for (const dir of [path.resolve(toolRoot(), 'presets'), path.resolve(__dirname, '../../presets')]) { if (fs.existsSync(dir)) return dir; } return null; diff --git a/src/tools/emutarget.ts b/src/tools/emutarget.ts index 3700493d..723b55e8 100644 --- a/src/tools/emutarget.ts +++ b/src/tools/emutarget.ts @@ -6,6 +6,7 @@ import { EmuCore } from "../common/emucore"; import { PLATFORMS } from "../common/emu"; import { getRootBasePlatform } from "../common/util"; import { importPlatform } from "../platform/_index"; +import { toolRoot } from "./toolroot"; import { mockAudio, mockDOM, mockFetch, mockGlobals, mockScripts, resetScripts } from "./nodemock"; export { EmuCore as EmuTarget, DEFAULT_MAX_FRAMES } from "../common/emucore"; @@ -14,7 +15,7 @@ type EmuTarget = EmuCore; /** Load a platform module by ID (e.g. "nes", "c64.wasm", "atari8-5200"). */ export async function loadPlatform(platformId: string): Promise { - installNodeMocks(); + installNodeMocks(toolRoot()); // a headless host may load more than one platform (or the same one twice) // in a process; scripts kept global state, so evaluate them fresh each time resetScripts(); diff --git a/src/tools/testlib.ts b/src/tools/testlib.ts index 13fb7f0a..431c86f2 100644 --- a/src/tools/testlib.ts +++ b/src/tools/testlib.ts @@ -12,6 +12,7 @@ import { FileProvider, buildWorkerMessage, resolveDependencies } from "../common import { setupNodeEnvironment, handleMessage, store } from "../worker/workerlib"; import { PLATFORM_PARAMS } from "../worker/platforms"; import { TOOLS } from "../worker/workertools"; +import { toolRoot } from "./toolroot"; export { store, PLATFORM_PARAMS, TOOLS }; @@ -52,7 +53,7 @@ let initialized = false; */ export async function initialize(): Promise { if (initialized) return; - setupNodeEnvironment(); + setupNodeEnvironment(toolRoot()); // The worker tools are already registered through the import chain: // workerlib -> workertools -> tools/* and builder -> TOOLS // No need to load the esbuild bundle. @@ -110,7 +111,7 @@ export async function compile(options: CompileOptions): Promise { export async function compileFile(tool: string, platform: string, presetPath: string): Promise { await initialize(); - var code = fs.readFileSync('presets/' + platform + '/' + presetPath, 'utf-8'); + var code = fs.readFileSync(path.join(toolRoot(), 'presets', platform, presetPath), 'utf-8'); return compile({ tool: tool, platform: platform, @@ -121,14 +122,14 @@ export async function compileFile(tool: string, platform: string, presetPath: st /** * Reads files for a build from the source file's directory, then - * presets/, then the current directory. + * presets/ under the tool root, then the current directory. */ export class NodeFileProvider implements FileProvider { constructor(readonly sourceDir: string, readonly platform: string) { } async readFile(filePath: string): Promise { var searchPaths = []; if (this.sourceDir) searchPaths.push(path.resolve(this.sourceDir, filePath)); - searchPaths.push(path.resolve('presets', getBasePlatform(this.platform), filePath)); + searchPaths.push(path.resolve(toolRoot(), 'presets', getBasePlatform(this.platform), filePath)); searchPaths.push(path.resolve(filePath)); for (var p of searchPaths) { try { diff --git a/src/tools/toolroot.ts b/src/tools/toolroot.ts new file mode 100644 index 00000000..248b1c30 --- /dev/null +++ b/src/tools/toolroot.ts @@ -0,0 +1,10 @@ +// toolroot - where the CLI finds toolchains, presets and BIOS images: a +// directory laid out like the repo (src/worker/..., presets/, res/). + +/** + * $EIGHTBITWORKSHOP_ROOT when set (the VS Code extension's launcher points it + * at the extension's toolchains), else the current directory (a checkout). + */ +export function toolRoot(): string { + return process.env.EIGHTBITWORKSHOP_ROOT || process.cwd(); +}