| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- /** GNU Modula-2 implementation of the compiler service.
- *
- * Runs `gm2` as a child process (never blocking the frontend) and turns
- * its GCC-style `file:line:col: severity: message` output into structured
- * diagnostics. `parseGm2Diagnostics` is exported pure for testing.
- */
- import { injectable } from '@theia/core/shared/inversify';
- import { spawn } from 'child_process';
- import * as fs from 'fs';
- import * as path from 'path';
- import {
- BuildDiagnostic, BuildResult, BuildStats, CompilerInfo, Modula2Compiler,
- Modula2Project, defaultOutput,
- } from '../common/compiler';
- export interface ProcessResult {
- exitCode: number;
- output: string;
- }
- /** Source census plus output size for the build report (best-effort). */
- export async function collectBuildStats(
- project: Modula2Project, out: string,
- ): Promise<BuildStats> {
- let files = 0;
- let lines = 0;
- const visit = async (dir: string, depth: number): Promise<void> => {
- if (depth > 8) return;
- let entries: import('fs').Dirent[];
- try {
- entries = await fs.promises.readdir(dir, { withFileTypes: true });
- } catch {
- return;
- }
- for (const entry of entries) {
- const full = path.join(dir, entry.name);
- if (entry.isDirectory()) {
- if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
- await visit(full, depth + 1);
- } else if (entry.name.endsWith('.mod') || entry.name.endsWith('.def')) {
- files++;
- try {
- const text = await fs.promises.readFile(full, 'utf8');
- lines += text.split('\n').length;
- } catch {
- // Count the file even when its lines cannot be read.
- }
- }
- }
- };
- await visit(project.root, 0);
- let outputSize = -1;
- try {
- outputSize = (await fs.promises.stat(path.join(project.root, out))).size;
- } catch {
- // Leave unavailable.
- }
- return { files, lines, outputSize };
- }
- export function runProcess(command: string, args: string[], cwd: string): Promise<ProcessResult> {
- return new Promise((resolve, reject) => {
- const child = spawn(command, args, { cwd, shell: false });
- let output = '';
- child.stdout.on('data', d => { output += d.toString(); });
- child.stderr.on('data', d => { output += d.toString(); });
- child.on('error', reject);
- child.on('close', code => resolve({ exitCode: code ?? 1, output }));
- });
- }
- const GM2_LINE = /^(.+?):(\d+):(\d+):\s+(warning|error|note|fatal error):\s+(.*)$/;
- /** Parse GCC-style diagnostics; context/caret lines are ignored. */
- export function parseGm2Diagnostics(output: string): BuildDiagnostic[] {
- const diagnostics: BuildDiagnostic[] = [];
- for (const line of output.split('\n')) {
- const m = GM2_LINE.exec(line);
- if (!m) continue;
- const severity = m[4].includes('error') ? 'error' : m[4] === 'warning' ? 'warning' : 'info';
- diagnostics.push({
- file: m[1],
- line: Math.max(0, Number(m[2]) - 1),
- character: Math.max(0, Number(m[3]) - 1),
- message: m[5].trim(),
- severity,
- });
- }
- return diagnostics;
- }
- @injectable()
- export class GnuModula2CompilerService implements Modula2Compiler {
- async detect(compilerPath = 'gm2'): Promise<CompilerInfo | undefined> {
- try {
- const result = await runProcess(compilerPath, ['--version'], process.cwd());
- if (result.exitCode !== 0) return undefined;
- const version = (result.output.split('\n')[0] ?? '').trim();
- return { path: compilerPath, version };
- } catch {
- return undefined;
- }
- }
- async build(project: Modula2Project): Promise<BuildResult> {
- const out = defaultOutput(project);
- await fs.promises.mkdir(path.dirname(path.join(project.root, out)), { recursive: true });
- const args = ['-fiso', '-g', ...project.compiler.options, '-o', out, project.main];
- const command = [project.compiler.path, ...args].join(' ');
- let result: ProcessResult;
- try {
- result = await runProcess(project.compiler.path, args, project.root);
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- return {
- success: false, exitCode: 127, command,
- output: `Could not start compiler: ${message}`,
- diagnostics: [],
- };
- }
- const success = result.exitCode === 0;
- return {
- success,
- exitCode: result.exitCode,
- command,
- output: result.output,
- diagnostics: parseGm2Diagnostics(result.output),
- stats: success ? await collectBuildStats(project, out) : undefined,
- };
- }
- async clean(project: Modula2Project): Promise<void> {
- const out = path.join(project.root, defaultOutput(project));
- await fs.promises.rm(out, { force: true });
- // Best-effort removal of compiler side-products next to the sources.
- const srcDir = path.dirname(path.join(project.root, project.main));
- let entries: string[] = [];
- try {
- entries = await fs.promises.readdir(srcDir);
- } catch {
- return;
- }
- await Promise.all(entries
- .filter(e => e.endsWith('.o') || e.endsWith('.s') || e.endsWith('.lst'))
- .map(e => fs.promises.rm(path.join(srcDir, e), { force: true })));
- }
- async run(project: Modula2Project): Promise<BuildResult> {
- const built = await this.build(project);
- if (!built.success) return built;
- const executable = path.join(project.root, defaultOutput(project));
- let result: ProcessResult;
- try {
- result = await runProcess(executable, [], project.root);
- } catch (error) {
- const message = error instanceof Error ? error.message : String(error);
- return {
- success: false, exitCode: 127, command: executable,
- output: `Could not start program: ${message}`,
- diagnostics: [],
- };
- }
- return {
- success: result.exitCode === 0,
- exitCode: result.exitCode,
- command: executable,
- output: result.output,
- diagnostics: result.exitCode === 0 ? [] : [{
- file: project.main, line: 0, character: 0,
- message: `Program exited with code ${result.exitCode}.`,
- severity: 'error',
- }],
- };
- }
- }
|