diff --git a/src/worker/server/buildenv.ts b/src/worker/server/buildenv.ts index 4fc33854..a94d70fd 100644 --- a/src/worker/server/buildenv.ts +++ b/src/worker/server/buildenv.ts @@ -2,9 +2,10 @@ import fs from 'fs'; import path from 'path'; import { spawn } from 'child_process'; -import { WorkerBuildStep, WorkerError, WorkerFileUpdate, WorkerResult } from '../../common/workertypes'; +import { CodeListingMap, WorkerBuildStep, WorkerError, WorkerErrorResult, WorkerFileUpdate, WorkerResult, isOutputResult } from '../../common/workertypes'; import { getBasePlatform, getRootBasePlatform } from '../../common/util'; import { BuildStep, makeErrorMatcher } from '../workermain'; +import { parseObjDumpListing, parseObjDumpSymbolTable } from './clang'; const LLVM_MOS_TOOL: ServerBuildTool = { @@ -14,16 +15,26 @@ const LLVM_MOS_TOOL: ServerBuildTool = { archs: ['6502'], platforms: ['atari8', 'c64', 'nes'], platform_configs: { - c64: { + default: { binpath: 'llvm-mos/bin', - command: 'mos-c64-clang', - args: ['-Os', '-g', '-o', '$OUTFILE', '$INFILES'] + command: 'mos-clang', + args: ['-Os', '-g', '-o', '$OUTFILE', '$INFILES'], }, debug: { // TODO binpath: 'llvm-mos/bin', command: 'llvm-objdump', - args: ['-l', '-t', '$ELFFILE'] - } + args: ['-l', '-t', '$WORKDIR/a.out.elf', '>$WORKDIR/debug.out'] + }, + c64: { + command: 'mos-c64-clang', + }, + atari8: { + command: 'mos-atari8-clang', + }, + nes: { + command: 'mos-nes-nrom-clang', // TODO + libargs: ['-lneslib'] + }, } } @@ -52,22 +63,23 @@ interface ServerBuildTool { } interface ServerBuildToolPlatformConfig { - binpath: string; - command: string; - args: string[]; + binpath?: string; + command?: string; + args?: string[]; + libargs?: string[]; } + export class ServerBuildEnv { rootdir: string; sessionID: string; tool: ServerBuildTool; sessionDir: string; - errors: WorkerError[] = []; constructor(rootdir: string, sessionID: string, tool: ServerBuildTool) { - this.rootdir = rootdir; + this.rootdir = path.resolve(rootdir); this.sessionID = sessionID; this.tool = tool; // make sure sessionID is well-formed @@ -75,7 +87,7 @@ export class ServerBuildEnv { throw new Error(`Invalid sessionID: ${sessionID}`); } // create sessionID directory if it doesn't exist - this.sessionDir = path.join(rootdir, 'sessions', sessionID); + this.sessionDir = path.join(this.rootdir, 'sessions', sessionID); if (!fs.existsSync(this.sessionDir)) { fs.mkdirSync(this.sessionDir); } @@ -89,20 +101,26 @@ export class ServerBuildEnv { await fs.promises.writeFile(path.join(this.sessionDir, file.path), file.data); } - async build(step: WorkerBuildStep): Promise { - let platformID = getRootBasePlatform(step.platform); + async build(step: WorkerBuildStep, platform?: string): Promise { + // build config + let platformID = platform || getRootBasePlatform(step.platform); let config = this.tool.platform_configs[platformID]; if (!config) { throw new Error(`No config for platform ${platformID}`); } + let defaultConfig = this.tool.platform_configs.default; + if (!defaultConfig) { + throw new Error(`No default config for tool ${this.tool.name}`); + } + config = Object.assign({}, defaultConfig, config); // combine configs + // copy args let args = config.args.slice(0); //copy array let command = config.command; // replace $OUTFILE - let outfile = path.join(this.sessionDir, 'a.out'); + let outfile = path.join(this.sessionDir, 'a.out'); // TODO? a.out for (let i = 0; i < args.length; i++) { - if (args[i] === '$OUTFILE') { - args[i] = outfile; - } + args[i] = args[i].replace(/\$OUTFILE/g, outfile); + args[i] = args[i].replace(/\$WORKDIR/g, this.sessionDir); } // replace $INFILES with the list of input files // TODO @@ -119,44 +137,87 @@ export class ServerBuildEnv { break; } } + if (config.libargs) { + args = args.concat(config.libargs); + } console.log(`Running: ${command} ${args.join(' ')}`); // spawn after setting PATH env var // TODO - let childProcess = spawn(command, args, { shell: true, env: { PATH: path.join(this.rootdir, config.binpath) } }); + let childProcess = spawn(command, args, { + shell: true, + cwd: this.rootdir, + env: { PATH: path.join(this.rootdir, config.binpath) + } }); let outputData = ''; let errorData = ''; - let errors = []; - let errorMatcher = makeErrorMatcher(errors, /([^:/]+):(\d+):(\d+):\s*(.+)/, 2, 4, step.path, 1); + // TODO? childProcess.stdout.on('data', (data) => { outputData += data.toString(); }); childProcess.stderr.on('data', (data) => { errorData += data.toString(); }); - await new Promise((resolve, reject) => { - childProcess.on('close', (code) => { - // split errorData into lines - for (let line of errorData.split('\n')) { - errorMatcher(line); - } + return new Promise((resolve, reject) => { + childProcess.on('close', async (code) => { if (code === 0) { - this.errors = []; - resolve(0); + if (platform === 'debug') { + resolve(this.processDebugInfo(step)); + } else { + resolve(this.processOutput(step)); + } } else { - this.errors = errors; - resolve(0); + let errorResult = await this.processErrors(step, errorData); + if (errorResult.errors.length === 0) { + errorResult.errors.push({ line: 0, msg: `Build failed.\n\n${errorData}` }); + } + resolve(errorResult); } }); }); } - async processResult(): Promise { - if (this.errors.length) { - return { errors: this.errors }; - } else { - let outfile = path.join(this.sessionDir, 'a.out'); - let output = await fs.promises.readFile(outfile, { encoding: 'base64' }); - return { output }; + async processErrors(step: WorkerBuildStep, errorData: string): Promise { + let errors = []; + // split errorData into lines + let errorMatcher = makeErrorMatcher(errors, /([^:/]+):(\d+):(\d+):\s*(.+)/, 2, 4, step.path, 1); + for (let line of errorData.split('\n')) { + errorMatcher(line); + } + return { errors }; + } + + async processOutput(step: WorkerBuildStep): Promise { + let outfile = path.join(this.sessionDir, 'a.out'); + let output = await fs.promises.readFile(outfile, { encoding: 'base64' }); + return { output }; + } + + async processDebugInfo(step: WorkerBuildStep): Promise { + let dbgfile = path.join(this.sessionDir, 'debug.out'); + let dbglist = await fs.promises.readFile(dbgfile); + let listings = parseObjDumpListing(dbglist.toString()); + let symbolmap = parseObjDumpSymbolTable(dbglist.toString()); + return { output: [], listings, symbolmap }; + } + + async compileAndLink(step: WorkerBuildStep, updates: WorkerFileUpdate[]): Promise { + for (let file of updates) { + await this.addFileUpdate(file); + } + try { + let result = await this.build(step); + // did we succeed? + if (isOutputResult(result)) { + // do the debug info + const debugInfo = await this.build(step, 'debug'); + if (isOutputResult(debugInfo)) { + result.listings = debugInfo.listings; + result.symbolmap = debugInfo.symbolmap; + } + } + return result; + } catch (err) { + return { errors: [{line:0, msg: err.toString()}] }; } } } diff --git a/src/worker/server/clang.ts b/src/worker/server/clang.ts new file mode 100644 index 00000000..9deb8132 --- /dev/null +++ b/src/worker/server/clang.ts @@ -0,0 +1,49 @@ +import path from 'path'; +import { CodeListing, CodeListingMap } from "../../common/workertypes"; + +export function parseObjDumpSymbolTable(symbolTable) { + const lines = symbolTable.split('\n'); + const result = {}; + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith('00')) { + const parts = line.split(/\s+/); + if (parts.length < 5) continue; + const symbol = parts[parts.length-1]; + const address = parseInt(parts[0], 16); + result[symbol] = address; + } + } + return result; +} + +export function parseObjDumpListing(lst: string): CodeListingMap { + const lines = lst.split('\n'); + const result: CodeListingMap = {}; + var lastListing : CodeListing = null; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i].trim(); + if (line.startsWith(';')) { + const fileInfoIndex = line.indexOf(':'); + if (fileInfoIndex !== -1) { + const fileInfo = line.substring(1).trim(); + const fileParts = fileInfo.split(':'); + const file = path.basename(fileParts[0].trim()).split('.')[0] + '.lst'; + const lineNumber = parseInt(fileParts[1], 10); + if (lineNumber > 0) { + if (!result[file]) result[file] = { lines: [], text: lst }; + lastListing = result[file]; + lastListing.lines.push({ line: lineNumber, offset: null }); + } + } + } else if (lastListing && line.match(/^\s*[A-F0-9]+:.+/i)) { + const offsetIndex = line.indexOf(':'); + if (offsetIndex !== -1) { + const offset = parseInt(line.substring(0, offsetIndex).trim(), 16); + lastListing.lines[lastListing.lines.length - 1].offset = offset; + } + } + } + return result; +} \ No newline at end of file diff --git a/src/worker/server/server.ts b/src/worker/server/server.ts index 4d2eb2e0..fd6720b3 100644 --- a/src/worker/server/server.ts +++ b/src/worker/server/server.ts @@ -8,18 +8,11 @@ import { ServerBuildEnv, TOOLS, findBestTool } from './buildenv'; //////////////////// -const SERVER_ROOT = './server-root'; -const SESSION_ROOT = path.join(SERVER_ROOT, 'sessions'); - -if (!fs.existsSync(SESSION_ROOT)) { - fs.mkdirSync(SESSION_ROOT); -} - const app = express(); app.use(cors()); -app.use(express.json()); +app.use(express.json({ limit: 1024*1024 })); // limit 1 MB app.get('/info', (req: Request, res: Response) => { // send a list of supported tools @@ -32,11 +25,7 @@ app.get('/test', async (req: Request, res: Response, next) => { const updates: WorkerFileUpdate[] = [{ path: 'test.c', data: 'int main() { return 0; }' }]; const buildStep: WorkerBuildStep = { tool: 'llvm-mos', platform: 'c64', files: ['test.c'] }; const env = new ServerBuildEnv(SERVER_ROOT, 'test', TOOLS[0]); - for (let file of updates) { - await env.addFileUpdate(file); - } - await env.build(buildStep); - const result = await env.processResult(); + const result = await env.compileAndLink(buildStep, updates); res.json(result); } catch (err) { return next(err); @@ -50,11 +39,7 @@ app.post('/build', async (req: Request, res: Response, next) => { const sessionID = req.body.sessionID; const bestTool = findBestTool(buildStep); const env = new ServerBuildEnv(SERVER_ROOT, sessionID, bestTool); - for (let file of updates) { - await env.addFileUpdate(file); - } - await env.build(buildStep); - const result = await env.processResult(); + const result = await env.compileAndLink(buildStep, updates); res.json(result); } catch (err) { return next(err); @@ -74,6 +59,13 @@ const port = 3009; origin: [`http://localhost:${port}`, 'http://localhost:8000'] }));*/ +const SERVER_ROOT = process.env['8BITWS_SERVER_ROOT'] || path.resolve('./server-root'); +const SESSION_ROOT = path.join(SERVER_ROOT, 'sessions'); +if (!fs.existsSync(SESSION_ROOT)) { + fs.mkdirSync(SESSION_ROOT); +} +process.chdir(SESSION_ROOT); + app.listen(port, () => { console.log(`Server is listening on port ${port}`); });