Sprinter: добавить отладку C-исходников и интеграцию VS Code
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
# Sprinter MAME Debug
|
||||
|
||||
VS Code-клиент сборки и отладки для `toolchain/sdbg_dap.py`. Режим `launch` сам
|
||||
поднимает изолированные MAME и session server; перед `attach` их запускают
|
||||
вручную.
|
||||
|
||||
> [!WARNING]
|
||||
> Native Windows пока не поддерживает полный launch/attach: `windows`
|
||||
> выбирает только debugger provider MAME, а host-часть всё ещё зависит от
|
||||
> `fcntl`, Unix domain sockets и Unix launcher.
|
||||
|
||||
Для разработки расширение можно открыть отдельным окном VS Code и запустить
|
||||
Extension Development Host. Конфигурация проекта:
|
||||
|
||||
```sh
|
||||
code --extensionDevelopmentPath="$PWD/toolchain/vscode-sprinter-debug" "$PWD"
|
||||
```
|
||||
|
||||
Python выбирается без зависимости от `PATH` GUI: при наличии local
|
||||
`.python-version` используется `~/.pyenv/shims/python`; для внешнего workspace
|
||||
без неё расширение ищет установленный `~/.pyenv/versions/3.12*/bin/python`.
|
||||
Версия local передаётся задаче через `PYENV_VERSION`, поэтому сборка работает
|
||||
и если `project` лежит вне workspace. При необходимости задайте абсолютный путь через
|
||||
`sprinterDebugger.pythonCommand`. Выбранные пути печатаются в канале Output
|
||||
`Sprinter MAME Debug`.
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "sprinter-mame",
|
||||
"request": "launch",
|
||||
"name": "Sprinter MAME: Launch",
|
||||
"build": "${workspaceFolder}/tests/hello/.sprinter-cc-hello"
|
||||
}
|
||||
```
|
||||
|
||||
Перед F5 расширение по умолчанию находит Makefile проекта по пути `build` и
|
||||
выполняет задачу `make SRC_DEBUG=1` в каталоге проекта через local Python 3.12.
|
||||
Ошибки SDCC с файлом и строкой попадают в Problems. При ненулевом коде
|
||||
сборки debug launch отменяется до старта MAME. Для нестандартной раскладки
|
||||
укажите `"project": "${workspaceFolder}/path/to/app"`; для уже собранного
|
||||
пакета можно задать `"autoBuild": false`. Явный `preLaunchTask` остаётся под
|
||||
контролем стандартного механизма VS Code и отключает автоматическую задачу
|
||||
расширения.
|
||||
|
||||
Команда палитры `Sprinter: Build Active Project` строит проект открытого
|
||||
C-файла. В `Tasks: Run Task` доступны задачи `Sprinter: Build ...` для
|
||||
Makefile, включающих `app.mk`. Сборка не заменяет исходный `make` и не пишет
|
||||
в общий образ дискеты MAME.
|
||||
|
||||
По умолчанию patched backend `sdbg` не открывает отдельное окно debugger MAME.
|
||||
Чтобы пользоваться им одновременно с VS Code, добавьте в launch:
|
||||
|
||||
```json
|
||||
"debugger": "osx"
|
||||
```
|
||||
|
||||
Без project patch можно задать `auto`; явные native provider: `osx` на macOS,
|
||||
`windows` в Windows, `qt` или `imgui` в Linux. Host-инструменты sdbg сейчас
|
||||
рассчитаны на macOS/Linux (`fcntl`, Unix sockets и Unix launcher); native
|
||||
Windows transport и end-to-end тесты ещё требуются.
|
||||
|
||||
Готовность DSS определяется по стабильному prompt в VRAM. `dssTimeout`
|
||||
задаёт таймаут (по умолчанию 30 секунд), а `launchAt` — необязательное самое
|
||||
раннее время ввода команды.
|
||||
|
||||
Ручной attach:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "sprinter-mame",
|
||||
"request": "attach",
|
||||
"name": "Sprinter MAME: Attach",
|
||||
"socket": "/tmp/sprinter-sdbg.sock"
|
||||
}
|
||||
```
|
||||
|
||||
Сейчас доступны точки по исходнику/функции, `logMessage` с безопасными
|
||||
подстановками `{variable}`, один проверенный frame, регистры, поддержанные
|
||||
global/static, continue/pause и instruction step. Source step поддержан для
|
||||
F10/F11/Shift+F11; банковский step-over и step-out проходят
|
||||
служебные trampoline до следующей C-позиции.
|
||||
@@ -0,0 +1,53 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function absolutePath(value, workspace) {
|
||||
if (!value || !workspace) return null;
|
||||
const expanded = value.replace(/\$\{workspaceFolder\}/g, workspace);
|
||||
return path.resolve(workspace, expanded);
|
||||
}
|
||||
|
||||
function projectForLaunch(configuration, workspace) {
|
||||
const explicit = absolutePath(configuration.project, workspace);
|
||||
if (explicit) {
|
||||
if (!fs.existsSync(path.join(explicit, 'Makefile'))) {
|
||||
throw new Error(`Нет Makefile в каталоге проекта: ${explicit}`);
|
||||
}
|
||||
return explicit;
|
||||
}
|
||||
const build = absolutePath(configuration.build, workspace);
|
||||
if (!build) return null;
|
||||
let directory = path.dirname(build);
|
||||
while (directory !== path.dirname(directory)) {
|
||||
if (fs.existsSync(path.join(directory, 'Makefile'))) {
|
||||
return directory;
|
||||
}
|
||||
if (directory === workspace) break;
|
||||
directory = path.dirname(directory);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function isSprinterMakefile(filename) {
|
||||
try {
|
||||
return /include\s+\$\(PROJ_ROOT\)\/app\.mk/.test(
|
||||
fs.readFileSync(filename, 'utf8'));
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function taskLabel(project, workspace) {
|
||||
const relative = path.relative(workspace, project);
|
||||
return `Sprinter: Build ${relative || '.'}`;
|
||||
}
|
||||
|
||||
function makeCommand() {
|
||||
if (process.platform === 'win32') {
|
||||
throw new Error('Native Windows build/debug пока не поддерживается');
|
||||
}
|
||||
return fs.existsSync('/usr/bin/make') ? '/usr/bin/make' : 'make';
|
||||
}
|
||||
|
||||
module.exports = {absolutePath, projectForLaunch, isSprinterMakefile,
|
||||
taskLabel, makeCommand};
|
||||
@@ -0,0 +1,40 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const {projectForLaunch, isSprinterMakefile, taskLabel} =
|
||||
require('./build');
|
||||
const manifest = require('./package.json');
|
||||
|
||||
const workspace = path.resolve(__dirname, '..', '..');
|
||||
const hello = path.join(workspace, 'tests', 'hello');
|
||||
|
||||
test('launch находит Makefile рядом с debug-пакетом', () => {
|
||||
assert.equal(projectForLaunch({
|
||||
build: '${workspaceFolder}/tests/hello/.sprinter-cc-hello',
|
||||
}, workspace), hello);
|
||||
assert.equal(taskLabel(hello, workspace), 'Sprinter: Build tests/hello');
|
||||
assert.equal(isSprinterMakefile(path.join(hello, 'Makefile')), true);
|
||||
});
|
||||
|
||||
test('пакет в build/ находит Makefile приложения уровнем выше', () => {
|
||||
const project = path.join(workspace, 'applications', 'SprPoP');
|
||||
assert.equal(projectForLaunch({
|
||||
build: '${workspaceFolder}/applications/SprPoP/build/.sprinter-cc-sprpop',
|
||||
}, workspace), project);
|
||||
});
|
||||
|
||||
test('явный project без Makefile даёт ошибку вместо stale запуска', () => {
|
||||
assert.throws(() => projectForLaunch({
|
||||
project: 'tests/sdbg/fixtures', build: 'tests/hello/.sprinter-cc-hello',
|
||||
}, workspace), /Нет Makefile/);
|
||||
});
|
||||
|
||||
test('matcher разрешает относительную ошибку SDCC внутри проекта', () => {
|
||||
const matcher = manifest.contributes.problemMatchers[0];
|
||||
const line = 'hello.c:62: error 20: Undefined identifier \'missing_name\'';
|
||||
const match = new RegExp(matcher.pattern.regexp).exec(line);
|
||||
assert.deepEqual(match?.slice(1),
|
||||
['hello.c', '62', 'error', '20',
|
||||
"Undefined identifier 'missing_name'"]);
|
||||
assert.equal(matcher.fileLocation, 'relative');
|
||||
});
|
||||
@@ -0,0 +1,195 @@
|
||||
const path = require('path');
|
||||
const vscode = require('vscode');
|
||||
const {resolvePython, pyenvEnvironment} = require('./runtime');
|
||||
const {projectForLaunch, isSprinterMakefile, taskLabel, makeCommand} =
|
||||
require('./build');
|
||||
|
||||
class SprinterAdapterFactory {
|
||||
constructor(output) {
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
createDebugAdapterDescriptor(session) {
|
||||
const settings = vscode.workspace.getConfiguration('sprinterDebugger');
|
||||
const folder = session.workspaceFolder || vscode.workspace.workspaceFolders?.[0];
|
||||
const configured = session.configuration.adapterPath;
|
||||
if (!configured && !folder) {
|
||||
throw new Error('Откройте workspace C-Compiler или задайте adapterPath');
|
||||
}
|
||||
const adapter = configured || path.join(folder.uri.fsPath, 'toolchain', 'sdbg_dap.py');
|
||||
const runtime = resolvePython({
|
||||
command: settings.get('pythonCommand', 'auto'),
|
||||
args: settings.get('pythonArguments', []),
|
||||
workspace: folder?.uri.fsPath,
|
||||
});
|
||||
const cwd = folder?.uri.fsPath || path.dirname(adapter);
|
||||
this.output.appendLine(`Python: ${runtime.command}`);
|
||||
this.output.appendLine(`DAP: ${adapter}`);
|
||||
const options = {cwd};
|
||||
const env = pyenvEnvironment(runtime.command, cwd);
|
||||
if (env) options.env = env;
|
||||
return new vscode.DebugAdapterExecutable(
|
||||
runtime.command, [...runtime.args, adapter], options);
|
||||
}
|
||||
}
|
||||
|
||||
class SprinterTaskProvider {
|
||||
createTask(folder, project, definitionOverride) {
|
||||
const settings = vscode.workspace.getConfiguration('sprinterDebugger');
|
||||
const runtime = resolvePython({
|
||||
command: settings.get('pythonCommand', 'auto'),
|
||||
args: settings.get('pythonArguments', []),
|
||||
workspace: folder.uri.fsPath,
|
||||
});
|
||||
const relative = path.relative(folder.uri.fsPath, project);
|
||||
const definition = definitionOverride ||
|
||||
{type: 'sprinter', project: relative || '.'};
|
||||
const options = {cwd: project};
|
||||
const env = pyenvEnvironment(runtime.command, project) ||
|
||||
pyenvEnvironment(runtime.command, folder.uri.fsPath);
|
||||
if (env) options.env = env;
|
||||
const task = new vscode.Task(
|
||||
definition, folder, taskLabel(project, folder.uri.fsPath), 'sprinter',
|
||||
new vscode.ProcessExecution(makeCommand(), [
|
||||
'SRC_DEBUG=1', `PYTHON=${runtime.command}`,
|
||||
], options), ['$sprinter-sdcc']);
|
||||
task.group = vscode.TaskGroup.Build;
|
||||
return task;
|
||||
}
|
||||
|
||||
async provideTasks() {
|
||||
const folders = vscode.workspace.workspaceFolders || [];
|
||||
const results = [];
|
||||
for (const folder of folders) {
|
||||
const files = await vscode.workspace.findFiles(
|
||||
new vscode.RelativePattern(folder, '**/Makefile'),
|
||||
'**/{third_party,mame,libc,libbgi,toolchain}/**');
|
||||
for (const file of files) {
|
||||
if (isSprinterMakefile(file.fsPath)) {
|
||||
results.push(this.createTask(folder, path.dirname(file.fsPath)));
|
||||
}
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
resolveTask(task) {
|
||||
const folder = task.scope?.uri ? task.scope :
|
||||
vscode.workspace.workspaceFolders?.[0];
|
||||
const project = task.definition.project;
|
||||
if (!folder || typeof project !== 'string') return undefined;
|
||||
const directory = path.resolve(folder.uri.fsPath, project);
|
||||
if (!isSprinterMakefile(path.join(directory, 'Makefile'))) return undefined;
|
||||
return this.createTask(folder, directory, task.definition);
|
||||
}
|
||||
}
|
||||
|
||||
async function runBuildTask(task) {
|
||||
const early = [], ended = [];
|
||||
let execution, resolveResult, completed = false;
|
||||
const result = new Promise(resolve => {resolveResult = resolve;});
|
||||
function finish(code) {
|
||||
if (!completed) {
|
||||
completed = true;
|
||||
resolveResult(code);
|
||||
}
|
||||
}
|
||||
const processListener = vscode.tasks.onDidEndTaskProcess(event => {
|
||||
early.push(event);
|
||||
if (execution && event.execution === execution) finish(event.exitCode);
|
||||
});
|
||||
const endListener = vscode.tasks.onDidEndTask(event => {
|
||||
ended.push(event);
|
||||
if (execution && event.execution === execution) {
|
||||
// При отмене задачи process exit event может не появиться.
|
||||
setTimeout(() => finish(undefined), 100);
|
||||
}
|
||||
});
|
||||
try {
|
||||
execution = await vscode.tasks.executeTask(task);
|
||||
const finished = early.find(event => event.execution === execution);
|
||||
if (finished) finish(finished.exitCode);
|
||||
if (ended.some(event => event.execution === execution)) {
|
||||
setTimeout(() => finish(undefined), 100);
|
||||
}
|
||||
return await result;
|
||||
} finally {
|
||||
processListener.dispose();
|
||||
endListener.dispose();
|
||||
}
|
||||
}
|
||||
|
||||
class SprinterConfigurationProvider {
|
||||
constructor(tasks, output) {
|
||||
this.tasks = tasks;
|
||||
this.output = output;
|
||||
}
|
||||
|
||||
async resolveDebugConfiguration(folder, configuration) {
|
||||
if (configuration.type !== 'sprinter-mame' ||
|
||||
configuration.request !== 'launch' ||
|
||||
configuration.autoBuild === false || configuration.preLaunchTask) {
|
||||
return configuration;
|
||||
}
|
||||
const workspace = folder?.uri.fsPath ||
|
||||
vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
|
||||
if (!workspace) return configuration;
|
||||
try {
|
||||
const project = projectForLaunch(configuration, workspace);
|
||||
if (project && isSprinterMakefile(path.join(project, 'Makefile'))) {
|
||||
const scope = folder || vscode.workspace.workspaceFolders[0];
|
||||
const task = this.tasks.createTask(scope, project);
|
||||
this.output.appendLine(`Build: ${project}`);
|
||||
const code = await runBuildTask(task);
|
||||
if (code !== 0) {
|
||||
vscode.window.showErrorMessage(
|
||||
`Сборка Sprinter не прошла (код ${code ?? 'отмена'}); MAME не запущен`);
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Sprinter Build: ${error.message}`);
|
||||
return undefined;
|
||||
}
|
||||
return configuration;
|
||||
}
|
||||
}
|
||||
|
||||
function activate(context) {
|
||||
const output = vscode.window.createOutputChannel('Sprinter MAME Debug');
|
||||
context.subscriptions.push(output);
|
||||
context.subscriptions.push(vscode.debug.registerDebugAdapterDescriptorFactory(
|
||||
'sprinter-mame', new SprinterAdapterFactory(output)));
|
||||
const tasks = new SprinterTaskProvider();
|
||||
context.subscriptions.push(vscode.tasks.registerTaskProvider('sprinter', tasks));
|
||||
context.subscriptions.push(vscode.debug.registerDebugConfigurationProvider(
|
||||
'sprinter-mame', new SprinterConfigurationProvider(tasks, output)));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('sprinter.buildActive', async () => {
|
||||
const editor = vscode.window.activeTextEditor;
|
||||
const folder = editor && vscode.workspace.getWorkspaceFolder(editor.document.uri);
|
||||
if (!editor || !folder) {
|
||||
vscode.window.showErrorMessage('Откройте C-файл проекта Sprinter');
|
||||
return;
|
||||
}
|
||||
let directory = path.dirname(editor.document.uri.fsPath);
|
||||
const root = folder.uri.fsPath;
|
||||
while (directory !== path.dirname(directory)) {
|
||||
if (isSprinterMakefile(path.join(directory, 'Makefile'))) {
|
||||
try {
|
||||
await vscode.tasks.executeTask(tasks.createTask(folder, directory));
|
||||
} catch (error) {
|
||||
vscode.window.showErrorMessage(`Sprinter Build: ${error.message}`);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (directory === root) break;
|
||||
directory = path.dirname(directory);
|
||||
}
|
||||
vscode.window.showErrorMessage('Не найден проект Sprinter с app.mk');
|
||||
}));
|
||||
}
|
||||
|
||||
function deactivate() {}
|
||||
|
||||
module.exports = {activate, deactivate, SprinterTaskProvider,
|
||||
SprinterConfigurationProvider, runBuildTask};
|
||||
@@ -0,0 +1,134 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const vm = require('node:vm');
|
||||
|
||||
test('F5 выполняет TaskProvider build и не запускает MAME после ошибки', async () => {
|
||||
let factory, configurationProvider, taskProvider;
|
||||
let onProcess, onEnd, exitCode = 0;
|
||||
const errors = [];
|
||||
const output = {appendLine() {}, dispose() {}};
|
||||
class DebugAdapterExecutable {
|
||||
constructor(command, args, options) {
|
||||
Object.assign(this, {command, args, options});
|
||||
}
|
||||
}
|
||||
class ProcessExecution {
|
||||
constructor(command, args, options) {
|
||||
Object.assign(this, {command, args, options});
|
||||
}
|
||||
}
|
||||
class Task {
|
||||
constructor(definition, scope, name, source, execution, problemMatchers) {
|
||||
Object.assign(this, {definition, scope, name, source,
|
||||
execution, problemMatchers});
|
||||
}
|
||||
}
|
||||
class RelativePattern {
|
||||
constructor(folder, pattern) {Object.assign(this, {folder, pattern});}
|
||||
}
|
||||
const vscode = {
|
||||
workspace: {
|
||||
workspaceFolders: [],
|
||||
getConfiguration: () => ({get: (key, fallback) => fallback}),
|
||||
findFiles: async () => [],
|
||||
},
|
||||
window: {createOutputChannel: () => output,
|
||||
showErrorMessage: message => errors.push(message)},
|
||||
debug: {
|
||||
registerDebugAdapterDescriptorFactory: (type, value) => {
|
||||
assert.equal(type, 'sprinter-mame');
|
||||
factory = value;
|
||||
return {dispose() {}};
|
||||
},
|
||||
registerDebugConfigurationProvider: (type, value) => {
|
||||
assert.equal(type, 'sprinter-mame');
|
||||
configurationProvider = value;
|
||||
return {dispose() {}};
|
||||
},
|
||||
},
|
||||
tasks: {
|
||||
registerTaskProvider: (type, value) => {
|
||||
assert.equal(type, 'sprinter');
|
||||
taskProvider = value;
|
||||
return {dispose() {}};
|
||||
},
|
||||
onDidEndTaskProcess: listener => {
|
||||
onProcess = listener;
|
||||
return {dispose() {onProcess = null;}};
|
||||
},
|
||||
onDidEndTask: listener => {
|
||||
onEnd = listener;
|
||||
return {dispose() {onEnd = null;}};
|
||||
},
|
||||
executeTask: async task => {
|
||||
const execution = {task};
|
||||
onProcess({execution, exitCode});
|
||||
onEnd({execution});
|
||||
return execution;
|
||||
},
|
||||
},
|
||||
commands: {registerCommand: (name) => {
|
||||
assert.equal(name, 'sprinter.buildActive');
|
||||
return {dispose() {}};
|
||||
}},
|
||||
DebugAdapterExecutable,
|
||||
ProcessExecution, Task, RelativePattern,
|
||||
TaskGroup: {Build: 'build'},
|
||||
};
|
||||
const source = fs.readFileSync(path.join(__dirname, 'extension.js'), 'utf8');
|
||||
const module = {exports: {}};
|
||||
vm.runInNewContext(source, {
|
||||
require: name => name === 'vscode' ? vscode : require(name),
|
||||
module, setTimeout,
|
||||
}, {filename: 'extension.js'});
|
||||
const context = {subscriptions: []};
|
||||
module.exports.activate(context);
|
||||
assert.equal(context.subscriptions.length, 5);
|
||||
|
||||
const workspace = path.resolve(__dirname, '..', '..');
|
||||
const session = {
|
||||
workspaceFolder: {uri: {fsPath: workspace}},
|
||||
configuration: {},
|
||||
};
|
||||
const descriptor = factory.createDebugAdapterDescriptor(session);
|
||||
assert.equal(descriptor.command,
|
||||
path.join(require('node:os').homedir(), '.pyenv', 'shims', 'python'));
|
||||
assert.equal(descriptor.args.at(-1),
|
||||
path.join(workspace, 'toolchain', 'sdbg_dap.py'));
|
||||
assert.equal(descriptor.options.cwd, workspace);
|
||||
assert.equal(descriptor.options.env.PYENV_VERSION, '3.12');
|
||||
const configuration = {
|
||||
type: 'sprinter-mame', request: 'launch',
|
||||
build: '${workspaceFolder}/tests/hello/.sprinter-cc-hello',
|
||||
};
|
||||
assert.equal(await configurationProvider.resolveDebugConfiguration(
|
||||
session.workspaceFolder, configuration), configuration, errors.join('\n'));
|
||||
const task = taskProvider.createTask(session.workspaceFolder,
|
||||
path.join(workspace, 'tests', 'hello'));
|
||||
assert.equal(task.name, 'Sprinter: Build tests/hello');
|
||||
assert.equal(task.execution.command, '/usr/bin/make');
|
||||
assert.equal(task.execution.options.cwd,
|
||||
path.join(workspace, 'tests', 'hello'));
|
||||
assert.equal(task.execution.options.env.PYENV_VERSION, '3.12');
|
||||
assert.deepEqual(Array.from(task.execution.args),
|
||||
['SRC_DEBUG=1', `PYTHON=${descriptor.command}`]);
|
||||
assert.equal(task.problemMatchers[0], '$sprinter-sdcc');
|
||||
vscode.workspace.workspaceFolders = [session.workspaceFolder];
|
||||
vscode.workspace.findFiles = async () => [
|
||||
{fsPath: path.join(workspace, 'tests', 'hello', 'Makefile')},
|
||||
{fsPath: path.join(workspace, 'Makefile')},
|
||||
];
|
||||
const discovered = await taskProvider.provideTasks();
|
||||
assert.equal(discovered.length, 1);
|
||||
assert.equal(discovered[0].name, task.name);
|
||||
const unresolved = {scope: session.workspaceFolder,
|
||||
definition: {type: 'sprinter', project: 'tests/hello'}};
|
||||
assert.equal(taskProvider.resolveTask(unresolved).definition,
|
||||
unresolved.definition);
|
||||
exitCode = 1;
|
||||
assert.equal(await configurationProvider.resolveDebugConfiguration(
|
||||
session.workspaceFolder, {...configuration}), undefined);
|
||||
assert.match(errors.at(-1), /MAME не запущен/);
|
||||
});
|
||||
@@ -0,0 +1,170 @@
|
||||
{
|
||||
"name": "sprinter-mame-debug",
|
||||
"displayName": "Sprinter MAME Debug",
|
||||
"description": "Сборка и DAP-отладка C-приложений Sprinter в MAME",
|
||||
"version": "0.2.0",
|
||||
"publisher": "sprinter-c-compiler",
|
||||
"engines": {"vscode": "^1.85.0"},
|
||||
"categories": ["Debuggers"],
|
||||
"main": "./extension.js",
|
||||
"activationEvents": ["onDebug", "onTaskType:sprinter", "onCommand:sprinter.buildActive"],
|
||||
"contributes": {
|
||||
"commands": [
|
||||
{"command": "sprinter.buildActive", "title": "Sprinter: Build Active Project"}
|
||||
],
|
||||
"taskDefinitions": [
|
||||
{
|
||||
"type": "sprinter",
|
||||
"required": ["project"],
|
||||
"properties": {
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Путь к каталогу с Makefile приложения относительно workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"problemMatchers": [
|
||||
{
|
||||
"name": "sprinter-sdcc",
|
||||
"owner": "sprinter-sdcc",
|
||||
"fileLocation": "relative",
|
||||
"pattern": {
|
||||
"regexp": "^(.+?):(\\d+):\\s+(error|warning)\\s+(\\d+):\\s+(.+)$",
|
||||
"file": 1,
|
||||
"line": 2,
|
||||
"severity": 3,
|
||||
"code": 4,
|
||||
"message": 5
|
||||
}
|
||||
}
|
||||
],
|
||||
"configuration": {
|
||||
"title": "Sprinter MAME Debug",
|
||||
"properties": {
|
||||
"sprinterDebugger.pythonCommand": {
|
||||
"type": "string",
|
||||
"default": "auto",
|
||||
"description": "Python 3.12 для DAP; auto выбирает local pyenv shim без зависимости от PATH GUI"
|
||||
},
|
||||
"sprinterDebugger.pythonArguments": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"default": [],
|
||||
"description": "Аргументы пользовательской Python-команды перед путём DAP-адаптера"
|
||||
}
|
||||
}
|
||||
},
|
||||
"debuggers": [
|
||||
{
|
||||
"type": "sprinter-mame",
|
||||
"label": "Sprinter MAME",
|
||||
"languages": ["c"],
|
||||
"configurationAttributes": {
|
||||
"launch": {
|
||||
"required": ["build"],
|
||||
"properties": {
|
||||
"build": {
|
||||
"type": "string",
|
||||
"description": "Путь к каталогу .sprinter-cc-NAME"
|
||||
},
|
||||
"project": {
|
||||
"type": "string",
|
||||
"description": "Каталог с Makefile для сборки перед запуском; определяется из build, если не задан"
|
||||
},
|
||||
"autoBuild": {
|
||||
"type": "boolean",
|
||||
"default": true,
|
||||
"description": "Добавить Sprinter build task перед F5, когда найден app.mk"
|
||||
},
|
||||
"data": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "Дополнительные файлы на debug-дискету"
|
||||
},
|
||||
"mame": {
|
||||
"type": "string",
|
||||
"description": "Путь к mame.arm"
|
||||
},
|
||||
"debugger": {
|
||||
"type": "string",
|
||||
"default": "sdbg",
|
||||
"enum": ["sdbg", "auto", "osx", "windows", "qt", "imgui"],
|
||||
"enumDescriptions": [
|
||||
"Только окно Sprinter и управление из VS Code",
|
||||
"Выбрать доступный штатный backend MAME",
|
||||
"VS Code вместе с Cocoa debugger MAME на macOS",
|
||||
"VS Code вместе с native debugger MAME на Windows",
|
||||
"Qt debugger MAME, если сборка включает USE_QTDEBUG",
|
||||
"Debugger MAME внутри основного графического окна"
|
||||
],
|
||||
"description": "OSD debugger provider; sdbg не открывает Cocoa debugger"
|
||||
},
|
||||
"launchAt": {
|
||||
"type": "number",
|
||||
"default": 0,
|
||||
"description": "Не начинать ввод раньше этой секунды эмуляции"
|
||||
},
|
||||
"dssTimeout": {
|
||||
"type": "number",
|
||||
"default": 30,
|
||||
"description": "Таймаут появления стабильного prompt DSS"
|
||||
}
|
||||
}
|
||||
},
|
||||
"attach": {
|
||||
"required": ["socket"],
|
||||
"properties": {
|
||||
"socket": {
|
||||
"type": "string",
|
||||
"description": "Unix socket запущенного sdbg_server.py"
|
||||
},
|
||||
"adapterPath": {
|
||||
"type": "string",
|
||||
"description": "Путь к toolchain/sdbg_dap.py; по умолчанию из workspace"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"configurationSnippets": [
|
||||
{
|
||||
"label": "Sprinter MAME: Launch",
|
||||
"description": "Запустить изолированный MAME и остановиться на main",
|
||||
"body": {
|
||||
"type": "sprinter-mame",
|
||||
"request": "launch",
|
||||
"name": "Sprinter MAME: Launch",
|
||||
"build": "^\"${workspaceFolder}/tests/hello/.sprinter-cc-hello\""
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Sprinter MAME: Launch + native debugger",
|
||||
"description": "Запустить VS Code debugger вместе с Cocoa debugger MAME",
|
||||
"body": {
|
||||
"type": "sprinter-mame",
|
||||
"request": "launch",
|
||||
"name": "Sprinter MAME: Launch + native debugger",
|
||||
"build": "^\"${workspaceFolder}/tests/hello/.sprinter-cc-hello\"",
|
||||
"debugger": "osx"
|
||||
}
|
||||
},
|
||||
{
|
||||
"label": "Sprinter MAME: Attach",
|
||||
"description": "Подключиться к проверенной sdbg-сессии",
|
||||
"body": {
|
||||
"type": "sprinter-mame",
|
||||
"request": "attach",
|
||||
"name": "Sprinter MAME: Attach",
|
||||
"socket": "^\"/tmp/sprinter-sdbg.sock\""
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"check": "node --check extension.js && node --check build.js && node --test runtime.test.js build.test.js extension.test.js"
|
||||
},
|
||||
"files": ["extension.js", "runtime.js", "build.js", "README.md"],
|
||||
"license": "BSD-3-Clause"
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
function executable(filename) {
|
||||
try {
|
||||
fs.accessSync(filename, fs.constants.X_OK);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function localPythonVersion(workspace) {
|
||||
if (!workspace) return null;
|
||||
let directory = path.resolve(workspace);
|
||||
while (directory !== path.dirname(directory)) {
|
||||
try {
|
||||
const version = fs.readFileSync(path.join(directory, '.python-version'),
|
||||
'utf8').trim().split(/\s+/)[0];
|
||||
return /^[A-Za-z0-9._-]+$/.test(version) ? version : null;
|
||||
} catch (_) {}
|
||||
directory = path.dirname(directory);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function installedPyenvPythons(home) {
|
||||
try {
|
||||
const versions = fs.readdirSync(path.join(home, '.pyenv', 'versions'));
|
||||
return versions.filter(version => /^3\.12(?:\.\d+)?$/.test(version))
|
||||
.sort((a, b) => Number(b.split('.')[2] || 0) -
|
||||
Number(a.split('.')[2] || 0))
|
||||
.map(version => path.join(home, '.pyenv', 'versions', version,
|
||||
'bin', 'python'));
|
||||
} catch (_) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePython(options = {}) {
|
||||
const command = String(options.command || 'auto').trim();
|
||||
const args = Array.isArray(options.args) ? options.args : [];
|
||||
if (command !== 'auto') {
|
||||
return {command, args};
|
||||
}
|
||||
|
||||
const home = options.home || os.homedir();
|
||||
const workspace = options.workspace;
|
||||
const isExecutable = options.isExecutable || executable;
|
||||
const candidates = [];
|
||||
const shim = path.join(home, '.pyenv', 'shims', 'python');
|
||||
const version = options.localVersion === undefined ?
|
||||
localPythonVersion(workspace) : options.localVersion;
|
||||
const installed = options.installedPyenv === undefined ?
|
||||
installedPyenvPythons(home) : options.installedPyenv;
|
||||
if (version) candidates.push(shim);
|
||||
candidates.push(...installed);
|
||||
if (!version) candidates.push(shim);
|
||||
if (workspace) {
|
||||
candidates.push(path.join(workspace, '.venv', 'bin', 'python'));
|
||||
}
|
||||
candidates.push(
|
||||
'/opt/homebrew/bin/python3.12',
|
||||
'/usr/local/bin/python3.12',
|
||||
'/usr/bin/python3.12',
|
||||
);
|
||||
const found = candidates.find(isExecutable);
|
||||
if (!found) {
|
||||
throw new Error(
|
||||
'Не найден Python 3.12. Задайте абсолютный путь в ' +
|
||||
'sprinterDebugger.pythonCommand.');
|
||||
}
|
||||
return {command: found, args: []};
|
||||
}
|
||||
|
||||
function pyenvEnvironment(command, workspace, home = os.homedir()) {
|
||||
const shim = path.join(home, '.pyenv', 'shims', 'python');
|
||||
const version = command === shim ? localPythonVersion(workspace) : null;
|
||||
return version ? {PYENV_VERSION: version} : undefined;
|
||||
}
|
||||
|
||||
module.exports = {resolvePython, localPythonVersion, installedPyenvPythons,
|
||||
pyenvEnvironment};
|
||||
@@ -0,0 +1,67 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const path = require('node:path');
|
||||
const test = require('node:test');
|
||||
const {resolvePython, localPythonVersion, pyenvEnvironment} = require('./runtime');
|
||||
|
||||
test('auto использует pyenv shim без PATH GUI', () => {
|
||||
const home = path.join(path.sep, 'Users', 'tester');
|
||||
const shim = path.join(home, '.pyenv', 'shims', 'python');
|
||||
const result = resolvePython({
|
||||
command: 'auto',
|
||||
workspace: path.join(home, 'project'),
|
||||
home,
|
||||
isExecutable: filename => filename === shim,
|
||||
});
|
||||
assert.deepEqual(result, {command: shim, args: []});
|
||||
});
|
||||
|
||||
test('local pyenv имеет приоритет над случайным .venv', () => {
|
||||
const home = path.join(path.sep, 'Users', 'tester');
|
||||
const workspace = path.join(home, 'project');
|
||||
const shim = path.join(home, '.pyenv', 'shims', 'python');
|
||||
const venv = path.join(workspace, '.venv', 'bin', 'python');
|
||||
const result = resolvePython({
|
||||
command: 'auto', workspace, home,
|
||||
isExecutable: filename => filename === shim || filename === venv,
|
||||
});
|
||||
assert.equal(result.command, shim);
|
||||
});
|
||||
|
||||
test('явная команда и аргументы сохраняются', () => {
|
||||
const result = resolvePython({
|
||||
command: '/python/custom',
|
||||
args: ['-I'],
|
||||
isExecutable: () => false,
|
||||
});
|
||||
assert.deepEqual(result, {command: '/python/custom', args: ['-I']});
|
||||
});
|
||||
|
||||
test('auto сообщает понятную ошибку без Python 3.12', () => {
|
||||
assert.throws(
|
||||
() => resolvePython({command: 'auto', isExecutable: () => false}),
|
||||
/Задайте абсолютный путь/,
|
||||
);
|
||||
});
|
||||
|
||||
test('внешний workspace без .python-version выбирает установленный pyenv 3.12', () => {
|
||||
const home = path.join(path.sep, 'Users', 'tester');
|
||||
const installed = path.join(home, '.pyenv', 'versions', '3.12.14',
|
||||
'bin', 'python');
|
||||
const result = resolvePython({
|
||||
command: 'auto', workspace: '/tmp/external-project', home,
|
||||
localVersion: null, installedPyenv: [installed],
|
||||
isExecutable: filename => filename === installed ||
|
||||
filename === path.join(home, '.pyenv', 'shims', 'python'),
|
||||
});
|
||||
assert.equal(result.command, installed);
|
||||
});
|
||||
|
||||
test('shim получает версию workspace при сборке за пределами его дерева', () => {
|
||||
const workspace = path.resolve(__dirname, '..', '..');
|
||||
const version = localPythonVersion(workspace);
|
||||
assert.match(version, /^3\.12/);
|
||||
const shim = path.join(require('node:os').homedir(), '.pyenv',
|
||||
'shims', 'python');
|
||||
assert.deepEqual(pyenvEnvironment(shim, workspace),
|
||||
{PYENV_VERSION: version});
|
||||
});
|
||||
Reference in New Issue
Block a user