功能基本可用

This commit is contained in:
2026-06-08 18:56:20 +08:00
parent e3a01a93b9
commit 457fcfdafd
4 changed files with 401 additions and 11 deletions
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "fmd-c-compiler",
"version": "0.1.0",
"version": "0.2.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "fmd-c-compiler",
"version": "0.1.0",
"version": "0.2.8",
"devDependencies": {
"@types/node": "^20.0.0",
"@types/vscode": "^1.85.0",
+23 -3
View File
@@ -2,7 +2,7 @@
"name": "fmd-c-compiler",
"displayName": "FMD C Compiler",
"description": "FMD/FT61FC6X 系列 MCU 编译器支持(C.exe 工具链)",
"version": "0.2.0",
"version": "0.2.8",
"license": "MIT",
"icon": "resources/icon.png",
"engines": {
@@ -78,6 +78,11 @@
{
"command": "fmdCompiler.exportEepromHex",
"title": "FMD: Export EEPROM HEX"
},
{
"command": "fmdCompiler.regenerateConfig",
"title": "FMD: Regenerate VS Code Config",
"icon": "$(gear)"
}
],
"keybindings": [
@@ -98,6 +103,11 @@
"command": "fmdCompiler.download",
"when": "resourceExtname =~ /\\.[cChH]$/",
"group": "navigation"
},
{
"command": "fmdCompiler.regenerateConfig",
"when": "resourceExtname =~ /\\.[cChH]$/",
"group": "navigation"
}
],
"editor/context": [
@@ -125,6 +135,11 @@
"command": "fmdCompiler.openEeprom",
"when": "resourceExtname =~ /\\.[cChH]$/",
"group": "fmd@5"
},
{
"command": "fmdCompiler.regenerateConfig",
"when": "resourceExtname =~ /\\.[cChH]$/",
"group": "fmd@6"
}
],
"explorer/context": [
@@ -152,6 +167,11 @@
"command": "fmdCompiler.openEeprom",
"when": "resourceExtname == '.prj' || resourceExtname == '.hex'",
"group": "fmd@5"
},
{
"command": "fmdCompiler.regenerateConfig",
"when": "resourceExtname == '.prj' || resourceExtname =~ /\\.[cChH]$/",
"group": "fmd@6"
}
]
},
@@ -186,8 +206,8 @@
},
"fmdCompiler.outputDir": {
"type": "string",
"default": "",
"description": "输出目录(留空则与工程同目录)"
"default": "build",
"description": "输出目录。默认 build 会输出到工程目录下的 build 文件夹;留空则与工程同目录;也可填写绝对路径。"
},
"fmdCompiler.extraArgs": {
"type": "string",
+10 -2
View File
@@ -70,7 +70,8 @@ export class FmdCompiler {
}
const projectName = projectInfo?.projectName || path.basename(projectDir);
const outputDir = cfg.outputDir || projectDir;
const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir);
fs.mkdirSync(outputDir, { recursive: true });
const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir);
this.building = true;
@@ -229,7 +230,7 @@ export class FmdCompiler {
}
const projectName = projectInfo?.projectName || path.basename(projectDir);
const outputDir = cfg.outputDir || projectDir;
const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir);
return this.getOutputArtifacts(projectDir, projectName, outputDir);
}
@@ -243,6 +244,13 @@ export class FmdCompiler {
};
}
private resolveOutputDir(projectDir: string, outputDir: string): string {
if (!outputDir) {
return projectDir;
}
return path.isAbsolute(outputDir) ? outputDir : path.join(projectDir, outputDir);
}
/**
* 构造编译参数
* 基于对 .map 文件的分析,c.exe 是 XC8-style 驱动器
+366 -4
View File
@@ -36,13 +36,16 @@ export function activate(context: vscode.ExtensionContext) {
}
}),
vscode.commands.registerCommand('fmdCompiler.clean', () => compiler.cleanProject()),
vscode.commands.registerCommand('fmdCompiler.selectProject', (uri?: vscode.Uri) => {
vscode.commands.registerCommand('fmdCompiler.selectProject', async (uri?: vscode.Uri) => {
if (uri) {
projectManager.setProjectFile(uri.fsPath);
vscode.window.showInformationMessage(`已选择工程: ${path.basename(uri.fsPath)}`);
} else {
projectManager.pickProjectFile();
await projectManager.pickProjectFile();
}
await ensureWorkspaceSettings();
ensureCppProperties();
ensureGitignore();
updateStatusBars();
}),
vscode.commands.registerCommand('fmdCompiler.openOutput', () => {
@@ -59,6 +62,7 @@ export function activate(context: vscode.ExtensionContext) {
vscode.commands.registerCommand('fmdCompiler.readEeprom', () => eepromManager.readEeprom()),
vscode.commands.registerCommand('fmdCompiler.writeEeprom', () => eepromManager.writeEeprom()),
vscode.commands.registerCommand('fmdCompiler.exportEepromHex', () => eepromManager.exportEepromHex()),
vscode.commands.registerCommand('fmdCompiler.regenerateConfig', () => regenerateConfig()),
diagnosticsCollection
);
@@ -69,6 +73,8 @@ export function activate(context: vscode.ExtensionContext) {
chipStatusBar.command = 'fmdCompiler.selectChip';
const downloadStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 98);
downloadStatusBar.command = 'fmdCompiler.download';
const configStatusBar = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 97);
configStatusBar.command = 'fmdCompiler.regenerateConfig';
const update = () => {
const cfg = getConfig();
@@ -79,13 +85,16 @@ export function activate(context: vscode.ExtensionContext) {
chipStatusBar.tooltip = '切换 FMD 目标芯片';
downloadStatusBar.text = '$(cloud-upload) FMD Download';
downloadStatusBar.tooltip = cfg.programmerPath ? `下载程序: ${cfg.programmerPath}` : '未配置烧录工具,点击配置后可下载';
configStatusBar.text = '$(gear) FMD Config';
configStatusBar.tooltip = '一键重新生成 .gitignore 和 .vscode 配置';
statusBar.show();
chipStatusBar.show();
downloadStatusBar.show();
configStatusBar.show();
};
updateStatusBars = update;
updateStatusBars();
context.subscriptions.push(statusBar, chipStatusBar, downloadStatusBar);
context.subscriptions.push(statusBar, chipStatusBar, downloadStatusBar, configStatusBar);
// 监听配置变化
context.subscriptions.push(
@@ -99,6 +108,9 @@ export function activate(context: vscode.ExtensionContext) {
// 尝试自动找工程文件
projectManager.autoDetectProject();
ensureWorkspaceSettings();
ensureCppProperties();
ensureGitignore();
updateStatusBars();
outputChannel.appendLine('[FMD] 插件已激活');
@@ -112,6 +124,18 @@ export function deactivate() {
let updateStatusBars = () => {};
async function regenerateConfig(): Promise<void> {
outputChannel.show(true);
outputChannel.appendLine('');
outputChannel.appendLine('========== FMD 重新生成 VS Code 配置 ==========');
await ensureWorkspaceSettings();
ensureCppProperties();
ensureGitignore();
updateStatusBars();
outputChannel.appendLine('========== FMD 配置生成完成 ==========');
vscode.window.showInformationMessage('FMD: 已重新生成 .gitignore 和 .vscode 配置');
}
async function setCompilerPath(): Promise<void> {
const cfg = getConfig();
const files = await vscode.window.showOpenDialog({
@@ -191,6 +215,344 @@ async function syncChipFromProject(): Promise<void> {
vscode.window.showInformationMessage(`FMD: 已从工程同步芯片: ${projectChip}`);
}
async function ensureWorkspaceSettings(): Promise<void> {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
return;
}
for (const folder of folders) {
const settingsDir = path.join(folder.uri.fsPath, '.vscode');
const settingsFile = path.join(settingsDir, 'settings.json');
let settings: Record<string, unknown> = {};
try {
if (fs.existsSync(settingsFile)) {
settings = JSON.parse(fs.readFileSync(settingsFile, 'utf8')) as Record<string, unknown>;
}
} catch (err) {
outputChannel.appendLine(`[警告] 无法解析工作区设置,跳过自动写入: ${settingsFile}: ${err}`);
continue;
}
let changed = false;
const cfg = getConfig();
const projectFile = projectManager.getProjectFile() || findSinglePrjFile(folder.uri.fsPath) || cfg.projectFile;
const projectChip = projectFile ? projectManager.getCurrentChip(projectFile) : undefined;
if (settings['fmdCompiler.outputDir'] === undefined) {
settings['fmdCompiler.outputDir'] = 'build';
changed = true;
}
if (settings['fmdCompiler.compilerPath'] === undefined) {
settings['fmdCompiler.compilerPath'] = cfg.compilerPath;
changed = true;
}
if (settings['fmdCompiler.chip'] === undefined) {
settings['fmdCompiler.chip'] = projectChip || cfg.chip || 'FT61FC6X';
changed = true;
}
if (projectFile && settings['fmdCompiler.projectFile'] === undefined) {
settings['fmdCompiler.projectFile'] = projectFile;
changed = true;
}
if (settings['fmdCompiler.autoSaveBeforeBuild'] === undefined) {
settings['fmdCompiler.autoSaveBeforeBuild'] = true;
changed = true;
}
if (settings['fmdCompiler.showOutputOnBuild'] === undefined) {
settings['fmdCompiler.showOutputOnBuild'] = true;
changed = true;
}
if (changed) {
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(settingsFile, JSON.stringify(settings, null, 2) + '\n');
outputChannel.appendLine(`[FMD] 已自动生成/更新工作区设置: ${settingsFile}`);
}
}
}
function ensureCppProperties(): void {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
return;
}
const cfg = getConfig();
const compilerInclude = path.join(path.dirname(cfg.compilerPath), '..', 'include');
for (const folder of folders) {
const vscodeDir = path.join(folder.uri.fsPath, '.vscode');
const propertiesFile = path.join(vscodeDir, 'c_cpp_properties.json');
const projectFile = projectManager.getProjectFile() || findSinglePrjFile(folder.uri.fsPath) || cfg.projectFile;
const projectDir = projectFile ? path.dirname(projectFile) : folder.uri.fsPath;
const chip = (projectFile ? projectManager.getCurrentChip(projectFile) : undefined) || cfg.chip;
const intellisenseHeader = ensureFmdIntellisenseHeader(vscodeDir, compilerInclude, chip);
const includePath = [
'${workspaceFolder}/**',
normalizeForCppProperties(projectDir),
normalizeForCppProperties(path.join(projectDir, '**')),
normalizeForCppProperties(compilerInclude),
];
const defines = [
`_${chip}`,
'__GCC8PRO__',
'_CHIP_SELECT_H_',
];
const forcedInclude = [
normalizeForCppProperties(intellisenseHeader),
];
let properties: {
configurations?: Array<Record<string, unknown>>;
version?: number;
[key: string]: unknown;
} = {};
try {
if (fs.existsSync(propertiesFile)) {
properties = JSON.parse(fs.readFileSync(propertiesFile, 'utf8'));
}
} catch (err) {
outputChannel.appendLine(`[警告] 无法解析 C/C++ 配置,跳过自动写入: ${propertiesFile}: ${err}`);
continue;
}
if (!Array.isArray(properties.configurations) || properties.configurations.length === 0) {
properties.configurations = [{
name: 'FMD',
includePath,
defines,
forcedInclude,
compilerPath: cfg.compilerPath,
cStandard: 'c99',
intelliSenseMode: 'windows-gcc-x86',
}];
} else {
const configuration = properties.configurations[0];
const currentIncludePath = Array.isArray(configuration.includePath) ? configuration.includePath as string[] : [];
const currentDefines = Array.isArray(configuration.defines) ? configuration.defines as string[] : [];
const currentForcedInclude = Array.isArray(configuration.forcedInclude) ? configuration.forcedInclude as string[] : [];
configuration.includePath = mergeUnique(currentIncludePath, includePath);
configuration.defines = mergeUnique(currentDefines, defines);
configuration.forcedInclude = mergeUnique(currentForcedInclude, forcedInclude);
if (!configuration.compilerPath) {
configuration.compilerPath = cfg.compilerPath;
}
if (!configuration.cStandard) {
configuration.cStandard = 'c99';
}
if (!configuration.intelliSenseMode) {
configuration.intelliSenseMode = 'windows-gcc-x86';
}
}
if (!properties.version) {
properties.version = 4;
}
fs.mkdirSync(vscodeDir, { recursive: true });
fs.writeFileSync(propertiesFile, JSON.stringify(properties, null, 2) + '\n');
outputChannel.appendLine(`[FMD] 已自动生成/更新 C/C++ 头文件路径: ${propertiesFile}`);
}
}
function ensureFmdIntellisenseHeader(vscodeDir: string, compilerInclude: string, chip: string): string {
fs.mkdirSync(vscodeDir, { recursive: true });
const target = path.join(vscodeDir, 'fmd_intellisense.h');
const chipHeader = findChipHeader(compilerInclude, chip);
const names = chipHeader ? extractChipSymbols(chipHeader) : [];
const lines = [
'/* Auto-generated by FMD C Compiler extension. */',
'/* This file is only for VS Code IntelliSense and is not used by c.exe. */',
'#ifndef FMD_INTELLISENSE_H',
'#define FMD_INTELLISENSE_H',
'',
'#ifndef __FMD_INTELLISENSE__',
'#define __FMD_INTELLISENSE__ 1',
'#endif',
'',
'#ifndef bit',
'typedef unsigned char bit;',
'#endif',
'',
'#ifndef asm',
'#define asm(...)',
'#endif',
'',
'#ifndef interrupt',
'#define interrupt',
'#endif',
'',
`#ifndef _${chip}`,
`#define _${chip}`,
'#endif',
'',
...names.map(name => `extern volatile unsigned char ${name};`),
'',
'#endif',
'',
];
fs.writeFileSync(target, lines.join('\n'));
return target;
}
function findChipHeader(compilerInclude: string, chip: string): string | undefined {
const candidates = [
path.join(compilerInclude, `${chip}.h`),
path.join(compilerInclude, `${chip}.H`),
];
return candidates.find(file => fs.existsSync(file));
}
function extractChipSymbols(chipHeader: string): string[] {
const text = fs.readFileSync(chipHeader, 'utf8');
const names = new Set<string>();
const patterns = [
/volatile\s+(?:unsigned\s+char|bit)\s+([A-Za-z_][A-Za-z0-9_]*)\s*@/g,
/volatile\s+union\s*\{[\s\S]*?\}\s*([A-Za-z_][A-Za-z0-9_]*)\s*@/g,
];
for (const pattern of patterns) {
let m: RegExpExecArray | null;
while ((m = pattern.exec(text)) !== null) {
names.add(m[1]);
}
}
return Array.from(names).sort();
}
function normalizeForCppProperties(filePath: string): string {
return filePath.replace(/\\/g, '/');
}
function mergeUnique(first: string[], second: string[]): string[] {
const result: string[] = [];
for (const value of [...first, ...second]) {
if (value && !result.includes(value)) {
result.push(value);
}
}
return result;
}
function ensureGitignore(): void {
const folders = vscode.workspace.workspaceFolders;
if (!folders || folders.length === 0) {
return;
}
const patterns = [
'.vscode',
'**/*.as',
'**/*.asm',
'**/*.bin',
'**/*.cmf',
'**/*.cof',
'**/*.d',
'**/*.hex',
'**/*.lpp',
'**/*.map',
'**/*.obj',
'**/*.p1',
'**/*.pre',
'**/*.rlf',
'**/*.sdb',
'**/*.sym',
'**/*.hxl',
'**/*.ini',
'**/*.rar',
'**/*.o',
'**/*.crf',
'**/*.htm',
'**/*.dep',
'**/*.bak',
'**/*.lnp',
'**/*.lst',
'**/*.iex',
'**/*.sct',
'**/*.scvd',
'**/*.uvguix',
'**/*.dbg*',
'**/*.uvguix.*',
'**/.mxproject',
'**/*.uvopt',
'**/*.uvgui.*',
'**/Listings',
'**/output',
'**/*.zip',
];
const blockStart = '# FMD generated ignores';
const blockEnd = '# End FMD generated ignores';
const block = [blockStart, ...patterns, blockEnd].join('\n');
for (const folder of folders) {
const gitignoreFile = path.join(folder.uri.fsPath, '.gitignore');
let text = '';
if (fs.existsSync(gitignoreFile)) {
text = fs.readFileSync(gitignoreFile, 'utf8');
if (text.includes(blockStart) && text.includes(blockEnd)) {
const pattern = new RegExp(`${escapeRegExp(blockStart)}[\\s\\S]*?${escapeRegExp(blockEnd)}`);
const nextText = text.replace(pattern, block);
if (nextText !== text) {
fs.writeFileSync(gitignoreFile, ensureTrailingNewline(nextText));
outputChannel.appendLine(`[FMD] 已更新 .gitignore: ${gitignoreFile}`);
}
continue;
}
}
const missing = patterns.filter(p => !hasGitignorePattern(text, p));
if (missing.length === 0) {
continue;
}
const prefix = text.trim().length > 0 ? ensureTrailingNewline(text).replace(/\s*$/, '\n\n') : '';
fs.writeFileSync(gitignoreFile, `${prefix}${block}\n`);
outputChannel.appendLine(`[FMD] 已自动生成/更新 .gitignore: ${gitignoreFile}`);
}
}
function hasGitignorePattern(text: string, pattern: string): boolean {
return text.split(/\r?\n/).some(line => line.trim() === pattern);
}
function ensureTrailingNewline(text: string): string {
return text.endsWith('\n') ? text : text + '\n';
}
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function findSinglePrjFile(folderPath: string): string | undefined {
const result: string[] = [];
const walk = (dir: string, depth: number) => {
if (depth > 2 || result.length > 1) {
return;
}
try {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const fullPath = path.join(dir, entry.name);
if (entry.isFile() && entry.name.toLowerCase().endsWith('.prj')) {
result.push(fullPath);
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
walk(fullPath, depth + 1);
}
}
} catch {
// 忽略权限错误
}
};
walk(folderPath, 0);
return result.length === 1 ? result[0] : undefined;
}
function collectChipCandidates(): string[] {
const cfg = getConfig();
const chips = new Set<string>(['FT61FC6X', cfg.chip]);
@@ -224,7 +586,7 @@ export function getConfig() {
]),
projectFile: cfg.get<string>('projectFile', ''),
chip: cfg.get<string>('chip', 'FT61FC6X'),
outputDir: cfg.get<string>('outputDir', ''),
outputDir: cfg.get<string>('outputDir', 'build'),
extraArgs: cfg.get<string>('extraArgs', ''),
autoSaveBeforeBuild: cfg.get<boolean>('autoSaveBeforeBuild', true),
showOutputOnBuild: cfg.get<boolean>('showOutputOnBuild', true),