compiler-service.ts 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. /** GNU Modula-2 implementation of the compiler service.
  2. *
  3. * Runs `gm2` as a child process (never blocking the frontend) and turns
  4. * its GCC-style `file:line:col: severity: message` output into structured
  5. * diagnostics. `parseGm2Diagnostics` is exported pure for testing.
  6. */
  7. import { injectable } from '@theia/core/shared/inversify';
  8. import { spawn } from 'child_process';
  9. import * as fs from 'fs';
  10. import * as path from 'path';
  11. import {
  12. BuildDiagnostic, BuildResult, BuildStats, CompilerInfo, Modula2Compiler,
  13. Modula2Project, defaultOutput,
  14. } from '../common/compiler';
  15. export interface ProcessResult {
  16. exitCode: number;
  17. output: string;
  18. }
  19. /** Source census plus output size for the build report (best-effort). */
  20. export async function collectBuildStats(
  21. project: Modula2Project, out: string,
  22. ): Promise<BuildStats> {
  23. let files = 0;
  24. let lines = 0;
  25. const visit = async (dir: string, depth: number): Promise<void> => {
  26. if (depth > 8) return;
  27. let entries: import('fs').Dirent[];
  28. try {
  29. entries = await fs.promises.readdir(dir, { withFileTypes: true });
  30. } catch {
  31. return;
  32. }
  33. for (const entry of entries) {
  34. const full = path.join(dir, entry.name);
  35. if (entry.isDirectory()) {
  36. if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
  37. await visit(full, depth + 1);
  38. } else if (entry.name.endsWith('.mod') || entry.name.endsWith('.def')) {
  39. files++;
  40. try {
  41. const text = await fs.promises.readFile(full, 'utf8');
  42. lines += text.split('\n').length;
  43. } catch {
  44. // Count the file even when its lines cannot be read.
  45. }
  46. }
  47. }
  48. };
  49. await visit(project.root, 0);
  50. let outputSize = -1;
  51. try {
  52. outputSize = (await fs.promises.stat(path.join(project.root, out))).size;
  53. } catch {
  54. // Leave unavailable.
  55. }
  56. return { files, lines, outputSize };
  57. }
  58. export function runProcess(command: string, args: string[], cwd: string): Promise<ProcessResult> {
  59. return new Promise((resolve, reject) => {
  60. const child = spawn(command, args, { cwd, shell: false });
  61. let output = '';
  62. child.stdout.on('data', d => { output += d.toString(); });
  63. child.stderr.on('data', d => { output += d.toString(); });
  64. child.on('error', reject);
  65. child.on('close', code => resolve({ exitCode: code ?? 1, output }));
  66. });
  67. }
  68. const GM2_LINE = /^(.+?):(\d+):(\d+):\s+(warning|error|note|fatal error):\s+(.*)$/;
  69. /** Parse GCC-style diagnostics; context/caret lines are ignored. */
  70. export function parseGm2Diagnostics(output: string): BuildDiagnostic[] {
  71. const diagnostics: BuildDiagnostic[] = [];
  72. for (const line of output.split('\n')) {
  73. const m = GM2_LINE.exec(line);
  74. if (!m) continue;
  75. const severity = m[4].includes('error') ? 'error' : m[4] === 'warning' ? 'warning' : 'info';
  76. diagnostics.push({
  77. file: m[1],
  78. line: Math.max(0, Number(m[2]) - 1),
  79. character: Math.max(0, Number(m[3]) - 1),
  80. message: m[5].trim(),
  81. severity,
  82. });
  83. }
  84. return diagnostics;
  85. }
  86. @injectable()
  87. export class GnuModula2CompilerService implements Modula2Compiler {
  88. async detect(compilerPath = 'gm2'): Promise<CompilerInfo | undefined> {
  89. try {
  90. const result = await runProcess(compilerPath, ['--version'], process.cwd());
  91. if (result.exitCode !== 0) return undefined;
  92. const version = (result.output.split('\n')[0] ?? '').trim();
  93. return { path: compilerPath, version };
  94. } catch {
  95. return undefined;
  96. }
  97. }
  98. async build(project: Modula2Project): Promise<BuildResult> {
  99. const out = defaultOutput(project);
  100. await fs.promises.mkdir(path.dirname(path.join(project.root, out)), { recursive: true });
  101. const args = ['-fiso', '-g', ...project.compiler.options, '-o', out, project.main];
  102. const command = [project.compiler.path, ...args].join(' ');
  103. let result: ProcessResult;
  104. try {
  105. result = await runProcess(project.compiler.path, args, project.root);
  106. } catch (error) {
  107. const message = error instanceof Error ? error.message : String(error);
  108. return {
  109. success: false, exitCode: 127, command,
  110. output: `Could not start compiler: ${message}`,
  111. diagnostics: [],
  112. };
  113. }
  114. const success = result.exitCode === 0;
  115. return {
  116. success,
  117. exitCode: result.exitCode,
  118. command,
  119. output: result.output,
  120. diagnostics: parseGm2Diagnostics(result.output),
  121. stats: success ? await collectBuildStats(project, out) : undefined,
  122. };
  123. }
  124. async clean(project: Modula2Project): Promise<void> {
  125. const out = path.join(project.root, defaultOutput(project));
  126. await fs.promises.rm(out, { force: true });
  127. // Best-effort removal of compiler side-products next to the sources.
  128. const srcDir = path.dirname(path.join(project.root, project.main));
  129. let entries: string[] = [];
  130. try {
  131. entries = await fs.promises.readdir(srcDir);
  132. } catch {
  133. return;
  134. }
  135. await Promise.all(entries
  136. .filter(e => e.endsWith('.o') || e.endsWith('.s') || e.endsWith('.lst'))
  137. .map(e => fs.promises.rm(path.join(srcDir, e), { force: true })));
  138. }
  139. async run(project: Modula2Project): Promise<BuildResult> {
  140. const built = await this.build(project);
  141. if (!built.success) return built;
  142. const executable = path.join(project.root, defaultOutput(project));
  143. let result: ProcessResult;
  144. try {
  145. result = await runProcess(executable, [], project.root);
  146. } catch (error) {
  147. const message = error instanceof Error ? error.message : String(error);
  148. return {
  149. success: false, exitCode: 127, command: executable,
  150. output: `Could not start program: ${message}`,
  151. diagnostics: [],
  152. };
  153. }
  154. return {
  155. success: result.exitCode === 0,
  156. exitCode: result.exitCode,
  157. command: executable,
  158. output: result.output,
  159. diagnostics: result.exitCode === 0 ? [] : [{
  160. file: project.main, line: 0, character: 0,
  161. message: `Program exited with code ${result.exitCode}.`,
  162. severity: 'error',
  163. }],
  164. };
  165. }
  166. }