更新旧芯片支持
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
# FMD C Compiler
|
# FMD C Compiler
|
||||||
|
|
||||||
FMD C Compiler 是一个用于 FMD / FT61FC6X 系列 MCU 工程开发的 VS Code 插件。插件封装厂商 `c.exe` 编译器,并自动生成 VS Code 工程配置,让传统 MCU 工程可以在 VS Code 中编辑、编译、管理输出文件和配置 IntelliSense。
|
FMD C Compiler 是一个用于 FMD 系列 MCU 工程开发的 VS Code 插件。插件封装厂商 `c.exe` 编译器,并自动生成 VS Code 工程配置,让传统 MCU 工程可以在 VS Code 中编辑、编译、管理输出文件和配置 IntelliSense。
|
||||||
|
|
||||||
## 功能特性
|
## 功能特性
|
||||||
|
|
||||||
@@ -35,25 +35,7 @@ xxx.C
|
|||||||
C:\Program Files (x86)\CCompiler\Compiler\data\bin\c.exe
|
C:\Program Files (x86)\CCompiler\Compiler\data\bin\c.exe
|
||||||
```
|
```
|
||||||
|
|
||||||
默认芯片:
|
目标芯片会优先从 `.prj` 文件中的 `Device = ...` 字段自动识别。部分官方工程型号会映射为编译器芯片库中的实际型号,例如 `FT61E13X` 会按官方工具输出映射为 `FT61F13X` 后传给 `c.exe --chip=...`。
|
||||||
|
|
||||||
```text
|
|
||||||
FT61FC6X
|
|
||||||
```
|
|
||||||
|
|
||||||
## 安装方式
|
|
||||||
|
|
||||||
从 VSIX 安装:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
code --install-extension fmd-c-compiler-0.2.9.vsix
|
|
||||||
```
|
|
||||||
|
|
||||||
也可以在 VS Code 扩展面板中选择:
|
|
||||||
|
|
||||||
```text
|
|
||||||
Install from VSIX...
|
|
||||||
```
|
|
||||||
|
|
||||||
## 快速开始
|
## 快速开始
|
||||||
|
|
||||||
@@ -138,7 +120,7 @@ FMD: Regenerate VS Code Config
|
|||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"fmdCompiler.compilerPath": "C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe",
|
"fmdCompiler.compilerPath": "C:\\Program Files (x86)\\CCompiler\\Compiler\\data\\bin\\c.exe",
|
||||||
"fmdCompiler.chip": "FT61FC6X",
|
"fmdCompiler.chip": "从 .prj 的 Device 自动识别,或手动指定",
|
||||||
"fmdCompiler.projectFile": "C:\\path\\to\\project.prj",
|
"fmdCompiler.projectFile": "C:\\path\\to\\project.prj",
|
||||||
"fmdCompiler.outputDir": "build",
|
"fmdCompiler.outputDir": "build",
|
||||||
"fmdCompiler.autoSaveBeforeBuild": true,
|
"fmdCompiler.autoSaveBeforeBuild": true,
|
||||||
@@ -159,7 +141,7 @@ FMD: Regenerate VS Code Config
|
|||||||
"C:/Program Files (x86)/CCompiler/Compiler/data/include"
|
"C:/Program Files (x86)/CCompiler/Compiler/data/include"
|
||||||
],
|
],
|
||||||
"defines": [
|
"defines": [
|
||||||
"_FT61FC6X",
|
"_当前工程芯片型号",
|
||||||
"__GCC8PRO__",
|
"__GCC8PRO__",
|
||||||
"_CHIP_SELECT_H_"
|
"_CHIP_SELECT_H_"
|
||||||
],
|
],
|
||||||
@@ -199,6 +181,8 @@ VS Code C/C++ IntelliSense 可能无法识别这些寄存器。插件会自动
|
|||||||
工程目录\build\
|
工程目录\build\
|
||||||
```
|
```
|
||||||
|
|
||||||
|
为了兼容官方工具链,插件会先让 `c.exe` 按官方风格输出到工程根目录的小写文件名前缀,例如 `ft61e132a.hex`,编译成功后再复制为配置输出目录中的工程名文件,例如 `build\FT61E132A.hex`。
|
||||||
|
|
||||||
也可以设置为绝对路径,例如:
|
也可以设置为绝对路径,例如:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
@@ -328,7 +312,21 @@ C/C++: Reset IntelliSense Database
|
|||||||
FMD: Regenerate VS Code Config
|
FMD: Regenerate VS Code Config
|
||||||
```
|
```
|
||||||
|
|
||||||
### 4. 下载功能不能直接使用
|
### 4. 编译提示芯片不在 gcc8.ini 中
|
||||||
|
|
||||||
|
如果输出类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
chip "FT61E13X" not present in chipinfo file "...gcc8.ini"
|
||||||
|
```
|
||||||
|
|
||||||
|
说明插件已经从 `.prj` 的 `Device` 字段识别出了芯片,但当前安装的 CCompiler 芯片数据库不支持该型号。插件会尝试从历史 `.map` 的 `Machine type is ...` 和内置别名规则推导实际编译器芯片名,例如 `FT61E13X -> FT61F13X`。如果仍失败,请确认:
|
||||||
|
|
||||||
|
- 是否安装了支持该芯片的新版本 CCompiler
|
||||||
|
- `.prj` 中的 `Device = ...` 是否写成了官方工具支持的芯片名
|
||||||
|
- `fmdCompiler.compilerPath` 是否指向正确的 CCompiler 安装目录
|
||||||
|
|
||||||
|
### 5. 下载功能不能直接使用
|
||||||
|
|
||||||
需要先配置实际使用的外部烧录工具路径和参数:
|
需要先配置实际使用的外部烧录工具路径和参数:
|
||||||
|
|
||||||
|
|||||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "fmd-c-compiler",
|
"name": "fmd-c-compiler",
|
||||||
"version": "0.2.10",
|
"version": "0.2.13",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "fmd-c-compiler",
|
"name": "fmd-c-compiler",
|
||||||
"version": "0.2.10",
|
"version": "0.2.13",
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/node": "^20.0.0",
|
"@types/node": "^20.0.0",
|
||||||
"@types/vscode": "^1.85.0",
|
"@types/vscode": "^1.85.0",
|
||||||
|
|||||||
+1
-1
@@ -3,7 +3,7 @@
|
|||||||
"publisher": "kevinngmanfong",
|
"publisher": "kevinngmanfong",
|
||||||
"displayName": "FMD C Compiler",
|
"displayName": "FMD C Compiler",
|
||||||
"description": "FMD/FT61FC6X 系列 MCU 编译器支持(C.exe 工具链)",
|
"description": "FMD/FT61FC6X 系列 MCU 编译器支持(C.exe 工具链)",
|
||||||
"version": "0.2.10",
|
"version": "0.2.13",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"icon": "resources/icon.png",
|
"icon": "resources/icon.png",
|
||||||
"engines": {
|
"engines": {
|
||||||
|
|||||||
+114
-4
@@ -70,9 +70,13 @@ export class FmdCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const projectName = projectInfo?.projectName || path.basename(projectDir);
|
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);
|
const outputDir = this.resolveOutputDir(projectDir, cfg.outputDir);
|
||||||
fs.mkdirSync(outputDir, { recursive: true });
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir);
|
const artifacts = this.getOutputArtifacts(projectDir, projectName, outputDir);
|
||||||
|
const compilerOutputBaseName = projectName.toLowerCase();
|
||||||
|
const compilerArtifacts = this.getOutputArtifacts(projectDir, compilerOutputBaseName, projectDir);
|
||||||
|
|
||||||
this.building = true;
|
this.building = true;
|
||||||
this.diagnostics.clear();
|
this.diagnostics.clear();
|
||||||
@@ -84,14 +88,29 @@ export class FmdCompiler {
|
|||||||
this.outputChannel.appendLine('');
|
this.outputChannel.appendLine('');
|
||||||
this.outputChannel.appendLine(`========== FMD 开始编译: ${projectName} ==========`);
|
this.outputChannel.appendLine(`========== FMD 开始编译: ${projectName} ==========`);
|
||||||
this.outputChannel.appendLine(`工程目录: ${projectDir}`);
|
this.outputChannel.appendLine(`工程目录: ${projectDir}`);
|
||||||
this.outputChannel.appendLine(`芯片: ${cfg.chip}`);
|
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) {
|
if (projectInfo?.device && projectInfo.device !== cfg.chip) {
|
||||||
this.outputChannel.appendLine(`[警告] 配置芯片 ${cfg.chip} 与工程文件 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(new Date().toLocaleString());
|
||||||
this.outputChannel.appendLine('');
|
this.outputChannel.appendLine('');
|
||||||
|
|
||||||
try {
|
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);
|
const cFiles = this.getSourceFiles(projectDir, projectInfo);
|
||||||
|
|
||||||
if (cFiles.length === 0) {
|
if (cFiles.length === 0) {
|
||||||
@@ -105,7 +124,7 @@ export class FmdCompiler {
|
|||||||
|
|
||||||
// 构建编译命令
|
// 构建编译命令
|
||||||
// c.exe 是 XC8-like 驱动,可以接受多文件一次编译+链接
|
// c.exe 是 XC8-like 驱动,可以接受多文件一次编译+链接
|
||||||
const args = this.buildCompileArgs(cFiles, projectDir, outputDir, projectName, cfg);
|
const args = this.buildCompileArgs(cFiles, projectDir, compilerArtifacts.outputDir, compilerArtifacts.projectName, compilerChip, cfg);
|
||||||
|
|
||||||
this.outputChannel.appendLine('编译命令:');
|
this.outputChannel.appendLine('编译命令:');
|
||||||
this.outputChannel.appendLine(` ${cfg.compilerPath} ${args.join(' ')}`);
|
this.outputChannel.appendLine(` ${cfg.compilerPath} ${args.join(' ')}`);
|
||||||
@@ -117,6 +136,7 @@ export class FmdCompiler {
|
|||||||
this.outputChannel.appendLine('');
|
this.outputChannel.appendLine('');
|
||||||
this.outputChannel.appendLine('========== 编译成功 ==========');
|
this.outputChannel.appendLine('========== 编译成功 ==========');
|
||||||
|
|
||||||
|
this.copyCompilerArtifacts(compilerArtifacts, artifacts);
|
||||||
this.logArtifact(artifacts.hexFile);
|
this.logArtifact(artifacts.hexFile);
|
||||||
this.logArtifact(artifacts.binFile);
|
this.logArtifact(artifacts.binFile);
|
||||||
|
|
||||||
@@ -251,6 +271,95 @@ export class FmdCompiler {
|
|||||||
return path.isAbsolute(outputDir) ? outputDir : path.join(projectDir, outputDir);
|
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 驱动器
|
* 基于对 .map 文件的分析,c.exe 是 XC8-style 驱动器
|
||||||
@@ -261,6 +370,7 @@ export class FmdCompiler {
|
|||||||
projectDir: string,
|
projectDir: string,
|
||||||
outputDir: string,
|
outputDir: string,
|
||||||
projectName: string,
|
projectName: string,
|
||||||
|
chip: string,
|
||||||
cfg: ReturnType<typeof getConfig>
|
cfg: ReturnType<typeof getConfig>
|
||||||
): string[] {
|
): string[] {
|
||||||
const compilerBinDir = path.dirname(cfg.compilerPath);
|
const compilerBinDir = path.dirname(cfg.compilerPath);
|
||||||
@@ -268,7 +378,7 @@ export class FmdCompiler {
|
|||||||
const includeDir = path.join(compilerDataDir, 'include');
|
const includeDir = path.join(compilerDataDir, 'include');
|
||||||
|
|
||||||
const args: string[] = [
|
const args: string[] = [
|
||||||
`--chip=${cfg.chip}`,
|
`--chip=${chip}`,
|
||||||
`-I${includeDir}`,
|
`-I${includeDir}`,
|
||||||
`-o${path.join(outputDir, projectName + '.hex')}`,
|
`-o${path.join(outputDir, projectName + '.hex')}`,
|
||||||
];
|
];
|
||||||
|
|||||||
+39
-1
@@ -287,7 +287,8 @@ function ensureCppProperties(): void {
|
|||||||
const propertiesFile = path.join(vscodeDir, 'c_cpp_properties.json');
|
const propertiesFile = path.join(vscodeDir, 'c_cpp_properties.json');
|
||||||
const projectFile = projectManager.getProjectFile() || findSinglePrjFile(folder.uri.fsPath) || cfg.projectFile;
|
const projectFile = projectManager.getProjectFile() || findSinglePrjFile(folder.uri.fsPath) || cfg.projectFile;
|
||||||
const projectDir = projectFile ? path.dirname(projectFile) : folder.uri.fsPath;
|
const projectDir = projectFile ? path.dirname(projectFile) : folder.uri.fsPath;
|
||||||
const chip = (projectFile ? projectManager.getCurrentChip(projectFile) : undefined) || cfg.chip;
|
const projectChip = (projectFile ? projectManager.getCurrentChip(projectFile) : undefined) || cfg.chip;
|
||||||
|
const chip = resolveIntellisenseChip(projectDir, path.basename(projectDir), projectChip, compilerInclude);
|
||||||
const intellisenseHeader = ensureFmdIntellisenseHeader(vscodeDir, compilerInclude, chip);
|
const intellisenseHeader = ensureFmdIntellisenseHeader(vscodeDir, compilerInclude, chip);
|
||||||
const includePath = [
|
const includePath = [
|
||||||
'${workspaceFolder}/**',
|
'${workspaceFolder}/**',
|
||||||
@@ -357,6 +358,43 @@ function ensureCppProperties(): void {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resolveIntellisenseChip(projectDir: string, projectName: string, projectChip: string, compilerInclude: string): string {
|
||||||
|
const historicalChip = readMachineTypeFromMap(projectDir, projectName);
|
||||||
|
if (historicalChip && findChipHeader(compilerInclude, historicalChip)) {
|
||||||
|
return historicalChip;
|
||||||
|
}
|
||||||
|
if (findChipHeader(compilerInclude, projectChip)) {
|
||||||
|
return projectChip;
|
||||||
|
}
|
||||||
|
const mappedChip = mapProjectChipToCompilerChip(projectChip);
|
||||||
|
return findChipHeader(compilerInclude, mappedChip) ? mappedChip : projectChip;
|
||||||
|
}
|
||||||
|
|
||||||
|
function 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 m = /Machine\s+type\s+is\s+([A-Za-z0-9_]+)/i.exec(fs.readFileSync(file, 'utf8'));
|
||||||
|
if (m) {
|
||||||
|
return m[1].trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function mapProjectChipToCompilerChip(projectChip: string): string {
|
||||||
|
const known: Record<string, string> = {
|
||||||
|
FT61E13X: 'FT61F13X',
|
||||||
|
};
|
||||||
|
const upper = projectChip.toUpperCase();
|
||||||
|
return known[upper] || upper.replace(/^FT61E/, 'FT61F');
|
||||||
|
}
|
||||||
|
|
||||||
function ensureFmdIntellisenseHeader(vscodeDir: string, compilerInclude: string, chip: string): string {
|
function ensureFmdIntellisenseHeader(vscodeDir: string, compilerInclude: string, chip: string): string {
|
||||||
fs.mkdirSync(vscodeDir, { recursive: true });
|
fs.mkdirSync(vscodeDir, { recursive: true });
|
||||||
const target = path.join(vscodeDir, 'fmd_intellisense.h');
|
const target = path.join(vscodeDir, 'fmd_intellisense.h');
|
||||||
|
|||||||
Reference in New Issue
Block a user