525 lines
20 KiB
TypeScript
525 lines
20 KiB
TypeScript
import * as vscode from 'vscode';
|
||
import * as path from 'path';
|
||
import * as fs from 'fs';
|
||
import { spawn } from 'child_process';
|
||
import { getConfig } from './extension';
|
||
import { FmdDiagnostics } from './diagnostics';
|
||
import { FmdProjectInfo, FmdProjectManager } from './projectManager';
|
||
|
||
export interface FmdOutputArtifacts {
|
||
projectDir: string;
|
||
projectName: string;
|
||
outputDir: string;
|
||
hexFile: string;
|
||
binFile: string;
|
||
}
|
||
|
||
export interface FmdBuildResult {
|
||
success: boolean;
|
||
exitCode: number;
|
||
artifacts?: FmdOutputArtifacts;
|
||
}
|
||
|
||
export class FmdCompiler {
|
||
private outputChannel: vscode.OutputChannel;
|
||
private diagnostics: FmdDiagnostics;
|
||
private projectManager: FmdProjectManager;
|
||
private building = false;
|
||
|
||
constructor(
|
||
outputChannel: vscode.OutputChannel,
|
||
diagnostics: FmdDiagnostics,
|
||
projectManager: FmdProjectManager
|
||
) {
|
||
this.outputChannel = outputChannel;
|
||
this.diagnostics = diagnostics;
|
||
this.projectManager = projectManager;
|
||
}
|
||
|
||
reloadConfig() {
|
||
this.outputChannel.appendLine('[FMD] 配置已更新');
|
||
}
|
||
|
||
/**
|
||
* 编译整个工程(所有 .c/.C 文件 + 链接)
|
||
*/
|
||
async buildProject(): Promise<void> {
|
||
const result = await this.buildProjectWithResult();
|
||
if (!result.success && result.exitCode !== -1) {
|
||
vscode.window.showErrorMessage(`FMD: 编译失败,退出码 ${result.exitCode},请查看输出面板`);
|
||
}
|
||
}
|
||
|
||
async buildProjectWithResult(): Promise<FmdBuildResult> {
|
||
if (this.building) {
|
||
vscode.window.showWarningMessage('FMD: 编译正在进行中...');
|
||
return { success: false, exitCode: -1 };
|
||
}
|
||
|
||
const cfg = getConfig();
|
||
|
||
// 自动保存
|
||
if (cfg.autoSaveBeforeBuild) {
|
||
await vscode.workspace.saveAll(false);
|
||
}
|
||
|
||
const projectInfo = await this.getProjectInfo();
|
||
const projectDir = projectInfo?.projectDir || await this.getProjectDir();
|
||
if (!projectDir) {
|
||
return { success: false, exitCode: -1 };
|
||
}
|
||
|
||
const projectName = projectInfo?.projectName || path.basename(projectDir);
|
||
const projectChip = projectInfo?.device || cfg.chip;
|
||
const compilerChip = this.resolveCompilerChip(projectDir, projectName, projectChip, cfg.compilerPath);
|
||
const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir);
|
||
fs.mkdirSync(outputDir, { recursive: true });
|
||
const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir);
|
||
const compilerOutputBaseName = projectName.toLowerCase();
|
||
const compilerArtifacts = this.getOutputArtifacts(projectDir, compilerOutputBaseName, projectDir);
|
||
|
||
this.building = true;
|
||
this.diagnostics.clear();
|
||
|
||
if (cfg.showOutputOnBuild) {
|
||
this.outputChannel.show(true);
|
||
}
|
||
|
||
this.outputChannel.appendLine('');
|
||
this.outputChannel.appendLine(`========== FMD 开始编译: ${projectName} ==========`);
|
||
this.outputChannel.appendLine(`工程目录: ${projectDir}`);
|
||
this.outputChannel.appendLine(`工程芯片: ${projectChip}${projectInfo?.device ? '(来自 .prj Device)' : '(来自 VS Code 配置)'}`);
|
||
if (compilerChip !== projectChip) {
|
||
this.outputChannel.appendLine(`编译器芯片: ${compilerChip}(由工程芯片映射/历史 map 推导)`);
|
||
}
|
||
if (projectInfo?.device && projectInfo.device !== cfg.chip) {
|
||
this.outputChannel.appendLine(`[提示] VS Code 配置芯片 ${cfg.chip} 与工程文件 Device ${projectInfo.device} 不一致,本次编译使用工程文件 Device ${projectInfo.device}`);
|
||
}
|
||
this.outputChannel.appendLine(new Date().toLocaleString());
|
||
this.outputChannel.appendLine('');
|
||
|
||
try {
|
||
const support = this.checkCompilerChipSupport(cfg.compilerPath, compilerChip);
|
||
if (!support.supported) {
|
||
const suggestions = this.findChipSuggestions(support.chips, projectChip);
|
||
this.outputChannel.appendLine(`[错误] 当前编译器芯片库不支持: ${compilerChip}(工程芯片: ${projectChip})`);
|
||
this.outputChannel.appendLine(`芯片库: ${support.chipInfoFile || '未找到 gcc8.ini'}`);
|
||
if (suggestions.length > 0) {
|
||
this.outputChannel.appendLine(`相近芯片: ${suggestions.join(', ')}`);
|
||
}
|
||
vscode.window.showErrorMessage(`FMD: 当前编译器不支持芯片 ${compilerChip}(工程 Device: ${projectChip}),请确认 CCompiler 版本或工程 Device 设置`);
|
||
return { success: false, exitCode: -1, artifacts };
|
||
}
|
||
|
||
const cFiles = this.getSourceFiles(projectDir, projectInfo);
|
||
|
||
if (cFiles.length === 0) {
|
||
vscode.window.showErrorMessage('工程目录中没有找到 .c/.C 文件');
|
||
return { success: false, exitCode: -1, artifacts };
|
||
}
|
||
|
||
this.outputChannel.appendLine(`源文件 (${cFiles.length} 个):`);
|
||
cFiles.forEach(f => this.outputChannel.appendLine(` ${path.basename(f)}`));
|
||
this.outputChannel.appendLine('');
|
||
|
||
// 构建编译命令
|
||
// c.exe 是 XC8-like 驱动,可以接受多文件一次编译+链接
|
||
const args = this.buildCompileArgs(cFiles, projectDir, compilerArtifacts.outputDir, compilerArtifacts.projectName, compilerChip, cfg);
|
||
|
||
this.outputChannel.appendLine('编译命令:');
|
||
this.outputChannel.appendLine(` ${cfg.compilerPath} ${args.join(' ')}`);
|
||
this.outputChannel.appendLine('');
|
||
|
||
const exitCode = await this.runCompiler(cfg.compilerPath, args, projectDir);
|
||
|
||
if (exitCode === 0) {
|
||
this.outputChannel.appendLine('');
|
||
this.outputChannel.appendLine('========== 编译成功 ==========');
|
||
|
||
this.copyCompilerArtifacts(compilerArtifacts, artifacts);
|
||
this.logArtifact(artifacts.hexFile);
|
||
this.logArtifact(artifacts.binFile);
|
||
|
||
vscode.window.showInformationMessage('FMD: 编译成功 ✓');
|
||
return { success: true, exitCode, artifacts };
|
||
}
|
||
|
||
this.outputChannel.appendLine('');
|
||
this.outputChannel.appendLine(`========== 编译失败 (退出码: ${exitCode}) ==========`);
|
||
return { success: false, exitCode, artifacts };
|
||
} catch (err) {
|
||
const msg = err instanceof Error ? err.message : String(err);
|
||
this.outputChannel.appendLine(`[错误] ${msg}`);
|
||
vscode.window.showErrorMessage(`FMD 编译异常: ${msg}`);
|
||
return { success: false, exitCode: -1, artifacts };
|
||
} finally {
|
||
this.building = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 仅编译单个 .c 文件(生成 .obj,不链接)
|
||
*/
|
||
async buildFile(filePath: string): Promise<void> {
|
||
if (this.building) {
|
||
vscode.window.showWarningMessage('FMD: 编译正在进行中...');
|
||
return;
|
||
}
|
||
|
||
const cfg = getConfig();
|
||
if (cfg.autoSaveBeforeBuild) {
|
||
await vscode.workspace.saveAll(false);
|
||
}
|
||
|
||
const projectDir = path.dirname(filePath);
|
||
const fileName = path.basename(filePath);
|
||
|
||
this.building = true;
|
||
this.diagnostics.clear();
|
||
|
||
if (cfg.showOutputOnBuild) {
|
||
this.outputChannel.show(true);
|
||
}
|
||
|
||
this.outputChannel.appendLine('');
|
||
this.outputChannel.appendLine(`========== FMD 编译文件: ${fileName} ==========`);
|
||
|
||
try {
|
||
// 单文件编译:只预处理+编译到 .obj,不链接
|
||
const args = [
|
||
`--chip=${cfg.chip}`,
|
||
'-Q', // 静默链接器
|
||
'-P', // 仅编译,不链接(生成 .p1 和 .obj)
|
||
`-I${path.join(path.dirname(cfg.compilerPath), '..', 'include')}`,
|
||
filePath,
|
||
];
|
||
|
||
if (cfg.extraArgs) {
|
||
args.push(...cfg.extraArgs.split(' ').filter(a => a));
|
||
}
|
||
|
||
this.outputChannel.appendLine(`命令: ${cfg.compilerPath} ${args.join(' ')}`);
|
||
|
||
const exitCode = await this.runCompiler(cfg.compilerPath, args, projectDir);
|
||
|
||
if (exitCode === 0) {
|
||
this.outputChannel.appendLine('单文件编译成功');
|
||
vscode.window.showInformationMessage(`FMD: ${fileName} 编译成功 ✓`);
|
||
} else {
|
||
vscode.window.showErrorMessage(`FMD: ${fileName} 编译失败`);
|
||
}
|
||
} finally {
|
||
this.building = false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 清理编译产物
|
||
*/
|
||
async cleanProject(): Promise<void> {
|
||
const projectDir = await this.getProjectDir();
|
||
if (!projectDir) {
|
||
return;
|
||
}
|
||
|
||
const cleanExts = ['.obj', '.p1', '.pre', '.d', '.lpp', '.cmf', '.sym', '.map', '.rlf', '.sdb', '.asm'];
|
||
let count = 0;
|
||
|
||
try {
|
||
const files = fs.readdirSync(projectDir);
|
||
for (const f of files) {
|
||
const ext = path.extname(f).toLowerCase();
|
||
if (cleanExts.includes(ext)) {
|
||
fs.unlinkSync(path.join(projectDir, f));
|
||
count++;
|
||
}
|
||
}
|
||
this.outputChannel.appendLine(`[FMD] 清理完成,删除 ${count} 个中间文件`);
|
||
vscode.window.showInformationMessage(`FMD: 清理完成,删除 ${count} 个中间文件`);
|
||
} catch (err) {
|
||
vscode.window.showErrorMessage(`FMD 清理失败: ${err}`);
|
||
}
|
||
}
|
||
|
||
async resolveOutputArtifacts(): Promise<FmdOutputArtifacts | undefined> {
|
||
const cfg = getConfig();
|
||
const projectInfo = await this.getProjectInfo();
|
||
const projectDir = projectInfo?.projectDir || await this.getProjectDir();
|
||
if (!projectDir) {
|
||
return undefined;
|
||
}
|
||
|
||
const projectName = projectInfo?.projectName || path.basename(projectDir);
|
||
const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir);
|
||
return this.getOutputArtifacts(projectDir, projectName, outputDir);
|
||
}
|
||
|
||
getOutputArtifacts(projectDir: string, projectName: string, outputDir: string): FmdOutputArtifacts {
|
||
return {
|
||
projectDir,
|
||
projectName,
|
||
outputDir,
|
||
hexFile: path.join(outputDir, projectName + '.hex'),
|
||
binFile: path.join(outputDir, projectName + '.bin'),
|
||
};
|
||
}
|
||
|
||
private resolveOutputDir(projectDir: string, outputDir: string): string {
|
||
if (!outputDir) {
|
||
return projectDir;
|
||
}
|
||
return path.isAbsolute(outputDir) ? outputDir : path.join(projectDir, outputDir);
|
||
}
|
||
|
||
private copyCompilerArtifacts(from: FmdOutputArtifacts, to: FmdOutputArtifacts): void {
|
||
fs.mkdirSync(to.outputDir, { recursive: true });
|
||
const pairs = [
|
||
[from.hexFile, to.hexFile],
|
||
[from.binFile, to.binFile],
|
||
];
|
||
|
||
for (const [source, target] of pairs) {
|
||
if (source === target || !fs.existsSync(source)) {
|
||
continue;
|
||
}
|
||
fs.copyFileSync(source, target);
|
||
this.outputChannel.appendLine(`复制输出: ${source} -> ${target}`);
|
||
}
|
||
}
|
||
|
||
private resolveCompilerChip(projectDir: string, projectName: string, projectChip: string, compilerPath: string): string {
|
||
const historicalChip = this.readMachineTypeFromMap(projectDir, projectName);
|
||
if (historicalChip && this.checkCompilerChipSupport(compilerPath, historicalChip).supported) {
|
||
return historicalChip;
|
||
}
|
||
|
||
if (this.checkCompilerChipSupport(compilerPath, projectChip).supported) {
|
||
return projectChip;
|
||
}
|
||
|
||
const mappedChip = this.mapProjectChipToCompilerChip(projectChip);
|
||
if (mappedChip && this.checkCompilerChipSupport(compilerPath, mappedChip).supported) {
|
||
return mappedChip;
|
||
}
|
||
|
||
return mappedChip || projectChip;
|
||
}
|
||
|
||
private readMachineTypeFromMap(projectDir: string, projectName: string): string | undefined {
|
||
const candidates = [
|
||
path.join(projectDir, projectName + '.map'),
|
||
path.join(projectDir, projectName.toLowerCase() + '.map'),
|
||
];
|
||
|
||
for (const file of candidates) {
|
||
if (!fs.existsSync(file)) {
|
||
continue;
|
||
}
|
||
const text = fs.readFileSync(file, 'utf8');
|
||
const m = /Machine\s+type\s+is\s+([A-Za-z0-9_]+)/i.exec(text);
|
||
if (m) {
|
||
return m[1].trim();
|
||
}
|
||
}
|
||
|
||
return undefined;
|
||
}
|
||
|
||
private mapProjectChipToCompilerChip(projectChip: string): string | undefined {
|
||
// 官方 IDE 的 Device 名称可能是市场/工程型号,c.exe 使用的是芯片库型号。
|
||
// 例如 .prj: FT61E13X,链接器实际 Machine type: FT61F13X。
|
||
const known: Record<string, string> = {
|
||
FT61E13X: 'FT61F13X',
|
||
};
|
||
const upper = projectChip.toUpperCase();
|
||
return known[upper] || upper.replace(/^FT61E/, 'FT61F');
|
||
}
|
||
|
||
private checkCompilerChipSupport(compilerPath: string, chip: string): { supported: boolean; chips: string[]; chipInfoFile?: string } {
|
||
const compilerBinDir = path.dirname(compilerPath);
|
||
const compilerDataDir = path.dirname(compilerBinDir);
|
||
const chipInfoFile = path.join(compilerDataDir, 'dat', 'gcc8.ini');
|
||
if (!fs.existsSync(chipInfoFile)) {
|
||
return { supported: true, chips: [], chipInfoFile };
|
||
}
|
||
|
||
const text = fs.readFileSync(chipInfoFile, 'utf8');
|
||
const chips = Array.from(text.matchAll(/^\[([^\]]+)\]/gm)).map(m => m[1].trim());
|
||
return {
|
||
supported: chips.some(c => c.toUpperCase() === chip.toUpperCase()),
|
||
chips,
|
||
chipInfoFile,
|
||
};
|
||
}
|
||
|
||
private findChipSuggestions(chips: string[], chip: string): string[] {
|
||
const normalized = chip.toUpperCase();
|
||
const prefix = normalized.slice(0, Math.max(4, normalized.length - 2));
|
||
return chips
|
||
.filter(c => c.toUpperCase().startsWith(prefix) || normalized.startsWith(c.toUpperCase().slice(0, Math.max(4, c.length - 2))))
|
||
.slice(0, 10);
|
||
}
|
||
|
||
/**
|
||
* 构造编译参数
|
||
* 基于对 .map 文件的分析,c.exe 是 XC8-style 驱动器
|
||
* 标准调用:c.exe --chip=CHIP [options] file1.c file2.c ...
|
||
*/
|
||
private buildCompileArgs(
|
||
cFiles: string[],
|
||
projectDir: string,
|
||
outputDir: string,
|
||
projectName: string,
|
||
chip: string,
|
||
cfg: ReturnType<typeof getConfig>
|
||
): string[] {
|
||
const compilerBinDir = path.dirname(cfg.compilerPath);
|
||
const compilerDataDir = path.dirname(compilerBinDir); // data/
|
||
const includeDir = path.join(compilerDataDir, 'include');
|
||
|
||
const args: string[] = [
|
||
`--chip=${chip}`,
|
||
`-I${includeDir}`,
|
||
`-o${path.join(outputDir, projectName + '.hex')}`,
|
||
];
|
||
|
||
// 额外参数
|
||
if (cfg.extraArgs) {
|
||
args.push(...cfg.extraArgs.split(' ').filter(a => a));
|
||
}
|
||
|
||
// 源文件
|
||
args.push(...cFiles);
|
||
|
||
return args;
|
||
}
|
||
|
||
/**
|
||
* 执行编译器,实时输出日志
|
||
*/
|
||
private runCompiler(
|
||
compilerPath: string,
|
||
args: string[],
|
||
cwd: string
|
||
): Promise<number> {
|
||
return new Promise((resolve, reject) => {
|
||
// 检查编译器是否存在
|
||
if (!fs.existsSync(compilerPath)) {
|
||
reject(new Error(`编译器不存在: ${compilerPath}\n请检查 fmdCompiler.compilerPath 配置`));
|
||
return;
|
||
}
|
||
|
||
const proc = spawn(compilerPath, args, {
|
||
cwd,
|
||
shell: false,
|
||
env: { ...process.env },
|
||
});
|
||
|
||
let stdout = '';
|
||
let stderr = '';
|
||
|
||
proc.stdout.on('data', (data: Buffer) => {
|
||
const text = data.toString();
|
||
stdout += text;
|
||
// 实时输出,逐行过滤空行
|
||
text.split('\n').forEach(line => {
|
||
if (line.trim()) {
|
||
this.outputChannel.appendLine(line.replace(/\r$/, ''));
|
||
}
|
||
});
|
||
});
|
||
|
||
proc.stderr.on('data', (data: Buffer) => {
|
||
const text = data.toString();
|
||
stderr += text;
|
||
text.split('\n').forEach(line => {
|
||
if (line.trim()) {
|
||
this.outputChannel.appendLine(line.replace(/\r$/, ''));
|
||
}
|
||
});
|
||
});
|
||
|
||
proc.on('close', (code: number | null) => {
|
||
const allOutput = stdout + stderr;
|
||
// 解析错误/警告,推送到诊断
|
||
this.diagnostics.parse(allOutput);
|
||
resolve(code ?? 1);
|
||
});
|
||
|
||
proc.on('error', (err: Error) => {
|
||
reject(err);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 获取工程目录
|
||
*/
|
||
private async getProjectDir(): Promise<string | undefined> {
|
||
const cfg = getConfig();
|
||
|
||
// 优先用配置指定的 .prj 文件目录
|
||
if (cfg.projectFile && fs.existsSync(cfg.projectFile)) {
|
||
return path.dirname(cfg.projectFile);
|
||
}
|
||
|
||
// 其次用 ProjectManager 自动检测到的
|
||
const detected = this.projectManager.getProjectDir();
|
||
if (detected) {
|
||
return detected;
|
||
}
|
||
|
||
// 最后用当前打开的文件目录
|
||
const editor = vscode.window.activeTextEditor;
|
||
if (editor) {
|
||
return path.dirname(editor.document.fileName);
|
||
}
|
||
|
||
vscode.window.showErrorMessage('FMD: 未找到工程目录,请打开 .c 文件或配置 fmdCompiler.projectFile');
|
||
return undefined;
|
||
}
|
||
|
||
private async getProjectInfo(): Promise<FmdProjectInfo | undefined> {
|
||
const cfg = getConfig();
|
||
try {
|
||
if (cfg.projectFile && fs.existsSync(cfg.projectFile)) {
|
||
return this.projectManager.getProjectInfo(cfg.projectFile);
|
||
}
|
||
return this.projectManager.getProjectInfo();
|
||
} catch (err) {
|
||
this.outputChannel.appendLine(`[警告] 读取工程文件失败: ${err}`);
|
||
return undefined;
|
||
}
|
||
}
|
||
|
||
private getSourceFiles(projectDir: string, projectInfo?: FmdProjectInfo): string[] {
|
||
const projectSources = projectInfo?.sourceFiles
|
||
.filter(f => /\.[cC]$/.test(f) && fs.existsSync(f));
|
||
|
||
if (projectSources && projectSources.length > 0) {
|
||
return this.sortSourceFiles(projectSources);
|
||
}
|
||
|
||
// 收集所有 .c/.C 文件
|
||
return this.sortSourceFiles(fs.readdirSync(projectDir)
|
||
.filter(f => /\.[cC]$/.test(f))
|
||
.map(f => path.join(projectDir, f)));
|
||
}
|
||
|
||
private sortSourceFiles(files: string[]): string[] {
|
||
// 这个 8 位 MCU 编译器对多源文件顺序较敏感。历史 IDE 输出中源文件按文件名稳定排序,
|
||
// 例如 1028.c 在 2288_test.C 前;保持该顺序可避免部分工程触发工具链内部错误。
|
||
return [...files].sort((a, b) => path.basename(a).localeCompare(path.basename(b), undefined, {
|
||
numeric: false,
|
||
sensitivity: 'base',
|
||
}));
|
||
}
|
||
|
||
private logArtifact(filePath: string): void {
|
||
if (fs.existsSync(filePath)) {
|
||
const stat = fs.statSync(filePath);
|
||
this.outputChannel.appendLine(`输出: ${filePath} (${stat.size} 字节)`);
|
||
}
|
||
}
|
||
}
|