Arduino 컴파일: 누락 라이브러리 자동 설치 후 재컴파일
컴파일 에러에서 'fatal error: Foo.h: No such file or directory' 파싱 → arduino-cli lib search로 최적 라이브러리 찾기 → 자동 설치 → 재컴파일. 성공 시 autoInstalled[] 포함하여 반환, 클라이언트는 "📦 자동 설치" 시스템 메시지 표시. 에러 지속 시 sendCompileErrorToAI()로 AI 채팅에 전달. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+203
-41
@@ -14164,9 +14164,13 @@ app.get('/api/arduino/libs', async (req, res) => {
|
||||
try {
|
||||
const data = JSON.parse(stdout || '{}');
|
||||
res.json({ libs: (data.installed_libraries || []).map((l: any) => ({
|
||||
name: l.library?.name || '',
|
||||
version: l.library?.version || '',
|
||||
author: l.library?.author || '',
|
||||
name: l.library?.name || '',
|
||||
version: l.library?.version || '',
|
||||
author: l.library?.author || '',
|
||||
sentence: l.library?.sentence || '',
|
||||
website: l.library?.website || '',
|
||||
category: l.library?.category || '',
|
||||
examples: (l.library?.examples || []).map((e: string) => path.basename(e)),
|
||||
}))});
|
||||
} catch { res.json({ libs: [] }); }
|
||||
});
|
||||
@@ -14176,25 +14180,32 @@ app.get('/api/arduino/libs/search', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
const q = String(req.query.q || '').trim();
|
||||
if (!q) return res.json({ libs: [] });
|
||||
if (!q) return res.json({ libs: [], installedNames: [] });
|
||||
const cli = await getArduinoCli();
|
||||
if (!cli) return res.status(503).json({ error: 'arduino-cli not found' });
|
||||
require('child_process').exec(
|
||||
`"${cli}" lib search "${q.replace(/"/g, '')}" --format json`,
|
||||
{ timeout: 15000 },
|
||||
(err: any, stdout: string) => {
|
||||
try {
|
||||
const data = JSON.parse(stdout || '{}');
|
||||
const libs = (data.libraries || []).slice(0, 20).map((l: any) => ({
|
||||
name: l.name || '',
|
||||
version: l.latest?.version || '',
|
||||
author: l.latest?.author || '',
|
||||
sentence: l.latest?.sentence || '',
|
||||
}));
|
||||
res.json({ libs });
|
||||
} catch { res.json({ libs: [] }); }
|
||||
}
|
||||
);
|
||||
// Run search + installed list in parallel
|
||||
const execP = (cmd: string) => new Promise<string>((resolve) =>
|
||||
require('child_process').exec(cmd, { timeout: 30000 }, (_e: any, out: string) => resolve(out || '')));
|
||||
const [searchOut, listOut] = await Promise.all([
|
||||
execP(`"${cli}" lib search "${q.replace(/"/g, '')}" --format json`),
|
||||
execP(`"${cli}" lib list --format json`),
|
||||
]);
|
||||
try {
|
||||
const searchData = JSON.parse(searchOut || '{}');
|
||||
const listData = JSON.parse(listOut || '{}');
|
||||
const installedNames: string[] = (listData.installed_libraries || []).map((l: any) => l.library?.name || '');
|
||||
const libs = (searchData.libraries || []).slice(0, 30).map((l: any) => ({
|
||||
name: l.name || '',
|
||||
version: l.latest?.version || '',
|
||||
author: l.latest?.author || '',
|
||||
sentence: l.latest?.sentence || '',
|
||||
paragraph: l.latest?.paragraph || '',
|
||||
website: l.latest?.website || '',
|
||||
category: l.latest?.category || '',
|
||||
installed: installedNames.includes(l.name || ''),
|
||||
}));
|
||||
res.json({ libs, installedNames });
|
||||
} catch { res.json({ libs: [], installedNames: [] }); }
|
||||
});
|
||||
|
||||
app.post('/api/arduino/libs/install', async (req, res) => {
|
||||
@@ -14231,6 +14242,121 @@ app.delete('/api/arduino/libs/uninstall', async (req, res) => {
|
||||
);
|
||||
});
|
||||
|
||||
// ── Arduino board (core) management ──────────────────────────────────────────
|
||||
app.get('/api/arduino/cores', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
const cli = await getArduinoCli();
|
||||
if (!cli) return res.status(503).json({ error: 'arduino-cli not found' });
|
||||
require('child_process').exec(`"${cli}" core list --format json`, { timeout: 20000 }, (err: any, stdout: string) => {
|
||||
try {
|
||||
const data = JSON.parse(stdout || '{}');
|
||||
const cores = (data.platforms || []).map((p: any) => {
|
||||
const latest = Object.keys(p.releases || {}).sort().reverse()[0] || '?';
|
||||
const rel = (p.releases || {})[latest] || {};
|
||||
return {
|
||||
id: p.id || '',
|
||||
name: rel.name || p.id || '',
|
||||
version: latest,
|
||||
maintainer: p.maintainer || '',
|
||||
website: p.website || '',
|
||||
boards: (rel.boards || []).map((b: any) => b.name || '').filter(Boolean),
|
||||
};
|
||||
});
|
||||
res.json({ cores });
|
||||
} catch { res.json({ cores: [] }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.get('/api/arduino/cores/search', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
const q = String(req.query.q || '').trim();
|
||||
const cli = await getArduinoCli();
|
||||
if (!cli) return res.status(503).json({ error: 'arduino-cli not found' });
|
||||
const execP = (cmd: string) => new Promise<string>((resolve) =>
|
||||
require('child_process').exec(cmd, { timeout: 30000 }, (_e: any, out: string) => resolve(out || '')));
|
||||
const searchQ = q ? `"${q.replace(/"/g,'')}"` : '""';
|
||||
const [searchOut, listOut] = await Promise.all([
|
||||
execP(`"${cli}" core search ${searchQ} --format json`),
|
||||
execP(`"${cli}" core list --format json`),
|
||||
]);
|
||||
try {
|
||||
const searchData = JSON.parse(searchOut || '{}');
|
||||
const listData = JSON.parse(listOut || '{}');
|
||||
const installedIds = new Set((listData.platforms || []).map((p: any) => p.id));
|
||||
const cores = (searchData.platforms || []).slice(0, 30).map((p: any) => {
|
||||
const latest = Object.keys(p.releases || {}).sort().reverse()[0] || '?';
|
||||
const rel = (p.releases || {})[latest] || {};
|
||||
return {
|
||||
id: p.id || '',
|
||||
name: rel.name || p.id || '',
|
||||
version: latest,
|
||||
maintainer: p.maintainer || '',
|
||||
boards: (rel.boards || []).map((b: any) => b.name || '').filter(Boolean).slice(0, 6),
|
||||
installed: installedIds.has(p.id || ''),
|
||||
};
|
||||
});
|
||||
res.json({ cores });
|
||||
} catch { res.json({ cores: [] }); }
|
||||
});
|
||||
|
||||
app.post('/api/arduino/cores/install', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
const { id } = req.body as { id?: string };
|
||||
if (!id || !/^[\w\-:\.]+$/.test(id)) return res.status(400).json({ error: 'invalid id' });
|
||||
const cli = await getArduinoCli();
|
||||
if (!cli) return res.status(503).json({ error: 'arduino-cli not found' });
|
||||
require('child_process').exec(`"${cli}" core install "${id}"`, { timeout: 300000 }, (err: any, stdout: string, stderr: string) => {
|
||||
if (err) return res.json({ error: (stderr || stdout || String(err)).slice(0, 300) });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
});
|
||||
|
||||
app.delete('/api/arduino/cores/uninstall', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
const { id } = req.body as { id?: string };
|
||||
if (!id || !/^[\w\-:\.]+$/.test(id)) return res.status(400).json({ error: 'invalid id' });
|
||||
const cli = await getArduinoCli();
|
||||
if (!cli) return res.status(503).json({ error: 'arduino-cli not found' });
|
||||
require('child_process').exec(`"${cli}" core uninstall "${id}"`, { timeout: 60000 }, (err: any, stdout: string, stderr: string) => {
|
||||
if (err) return res.json({ error: (stderr || stdout || String(err)).slice(0, 300) });
|
||||
res.json({ ok: true });
|
||||
});
|
||||
});
|
||||
|
||||
// Return .ino source of a library example (libName + exampleName)
|
||||
app.get('/api/arduino/libs/example', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
const { lib, example } = req.query as { lib?: string; example?: string };
|
||||
if (!lib || !example) return res.status(400).json({ error: 'lib and example required' });
|
||||
// Safe: only alphanumeric, space, underscore, hyphen, dot
|
||||
const safeLib = lib.replace(/[^a-zA-Z0-9 _\-\.]/g, '');
|
||||
const safeExample = example.replace(/[^a-zA-Z0-9_\-]/g, '');
|
||||
const libsRoot = path.join(require('os').homedir(), 'Arduino', 'libraries');
|
||||
// Find actual install dir via arduino-cli (handles name→folder mapping)
|
||||
const cli = await getArduinoCli();
|
||||
if (!cli) return res.status(503).json({ error: 'arduino-cli not found' });
|
||||
require('child_process').exec(`"${cli}" lib list --format json`, { timeout: 15000 }, (err: any, stdout: string) => {
|
||||
try {
|
||||
const data = JSON.parse(stdout || '{}');
|
||||
const entry = (data.installed_libraries || []).find((l: any) => l.library?.name === safeLib);
|
||||
if (!entry) return res.status(404).json({ error: 'Library not found' });
|
||||
const installDir: string = entry.library?.install_dir || '';
|
||||
if (!installDir.startsWith(libsRoot)) return res.status(403).json({ error: 'Forbidden' });
|
||||
const exDir = path.join(installDir, 'examples', safeExample);
|
||||
const files = fs.readdirSync(exDir);
|
||||
const inoFile = files.find((f: string) => f.endsWith('.ino'));
|
||||
if (!inoFile) return res.status(404).json({ error: 'No .ino in example' });
|
||||
const code = fs.readFileSync(path.join(exDir, inoFile), 'utf8');
|
||||
res.json({ name: inoFile, code });
|
||||
} catch(e: any) { res.status(500).json({ error: e.message }); }
|
||||
});
|
||||
});
|
||||
|
||||
app.post('/api/arduino/compile', async (req, res) => {
|
||||
const sess = getSessionUser(req);
|
||||
if (!sess) return res.status(401).json({ error: 'Not authenticated' });
|
||||
@@ -14270,28 +14396,64 @@ app.post('/api/arduino/compile', async (req, res) => {
|
||||
fs.mkdirSync(sketchDir);
|
||||
fs.writeFileSync(path.join(sketchDir, `${sketchName}.ino`), code);
|
||||
|
||||
try {
|
||||
const result = await new Promise<{ hex?: string; merged?: string; size?: number; error?: string }>((resolve) => {
|
||||
require('child_process').exec(
|
||||
`"${cliPath}" compile --fqbn ${fqbn} --output-dir ${tmpDir} ${sketchDir}`,
|
||||
{ timeout: 120000 },
|
||||
(err: any, stdout: string, stderr: string) => {
|
||||
if (err) { resolve({ error: stderr || stdout || String(err) }); return; }
|
||||
const files = fs.readdirSync(tmpDir);
|
||||
if (isESP32) {
|
||||
// Return merged.bin (bootloader + partitions + app, flash at 0x0)
|
||||
const mergedFile = files.find((f: string) => f.endsWith('.merged.bin'));
|
||||
if (!mergedFile) { resolve({ error: 'ESP32 컴파일 완료 but merged.bin 없음\n' + stdout }); return; }
|
||||
const buf = fs.readFileSync(path.join(tmpDir, mergedFile));
|
||||
resolve({ merged: buf.toString('base64'), size: buf.length });
|
||||
} else {
|
||||
const hexFile = files.find((f: string) => f.endsWith('.hex'));
|
||||
if (!hexFile) { resolve({ error: '컴파일 완료 but .hex 파일 없음\n' + stdout }); return; }
|
||||
resolve({ hex: fs.readFileSync(path.join(tmpDir, hexFile), 'utf-8') });
|
||||
}
|
||||
type CompileResult = { hex?: string; merged?: string; size?: number; error?: string; autoInstalled?: string[] };
|
||||
|
||||
const runCompile = (): Promise<CompileResult> => new Promise((resolve) => {
|
||||
require('child_process').exec(
|
||||
`"${cliPath}" compile --fqbn ${fqbn} --output-dir ${tmpDir} ${sketchDir}`,
|
||||
{ timeout: 120000 },
|
||||
(err: any, stdout: string, stderr: string) => {
|
||||
if (err) { resolve({ error: stderr || stdout || String(err) }); return; }
|
||||
const files = fs.readdirSync(tmpDir);
|
||||
if (isESP32) {
|
||||
const mergedFile = files.find((f: string) => f.endsWith('.merged.bin'));
|
||||
if (!mergedFile) { resolve({ error: 'ESP32 컴파일 완료 but merged.bin 없음\n' + stdout }); return; }
|
||||
const buf = fs.readFileSync(path.join(tmpDir, mergedFile));
|
||||
resolve({ merged: buf.toString('base64'), size: buf.length });
|
||||
} else {
|
||||
const hexFile = files.find((f: string) => f.endsWith('.hex'));
|
||||
if (!hexFile) { resolve({ error: '컴파일 완료 but .hex 파일 없음\n' + stdout }); return; }
|
||||
resolve({ hex: fs.readFileSync(path.join(tmpDir, hexFile), 'utf-8') });
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
const execQ = (cmd: string, ms = 60000): Promise<string> =>
|
||||
new Promise(r => require('child_process').exec(cmd, { timeout: ms }, (_: any, o: string) => r(o || '')));
|
||||
|
||||
try {
|
||||
let result = await runCompile();
|
||||
|
||||
// ── Auto-install missing libraries and re-compile ─────────────────────────
|
||||
if (result.error) {
|
||||
const missing = [...result.error.matchAll(/fatal error: (\S+\.h): No such file or directory/g)]
|
||||
.map(m => m[1]);
|
||||
|
||||
if (missing.length > 0) {
|
||||
const autoInstalled: string[] = [];
|
||||
for (const header of missing) {
|
||||
// e.g. "Adafruit_GFX.h" → search "Adafruit GFX"
|
||||
const q = header.replace(/\.h$/, '').replace(/_/g, ' ');
|
||||
try {
|
||||
const out = await execQ(`"${cliPath}" lib search "${q}" --format json`);
|
||||
const libs: any[] = JSON.parse(out)?.libraries || [];
|
||||
if (libs.length > 0) {
|
||||
const name: string = libs[0].name;
|
||||
console.log(`[arduino] auto-installing library: ${name} (for ${header})`);
|
||||
await execQ(`"${cliPath}" lib install "${name}"`, 120000);
|
||||
autoInstalled.push(name);
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (autoInstalled.length > 0) {
|
||||
const result2 = await runCompile();
|
||||
result = { ...result2, autoInstalled };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
res.json(result);
|
||||
} finally {
|
||||
fs.rmSync(tmpDir, { recursive: true, force: true });
|
||||
|
||||
+3
-2
@@ -888,7 +888,6 @@ function openArduinoEmulator() {
|
||||
const file = codeFiles.find(f => f.id === activeCodeFileId);
|
||||
const btn = document.getElementById('code-emu-btn');
|
||||
|
||||
// .ino 파일이 열려 있으면 컴파일 후 에뮬레이터 실행
|
||||
if (file && file.name.endsWith('.ino')) {
|
||||
if (btn) { btn.textContent = '⏳ 컴파일 중…'; btn.disabled = true; }
|
||||
fetch('/api/arduino/compile', {
|
||||
@@ -897,6 +896,7 @@ function openArduinoEmulator() {
|
||||
body: JSON.stringify({ code: file.content, board: 'uno' }),
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (btn) { btn.textContent = '🔌 에뮬레이터'; btn.disabled = false; }
|
||||
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
|
||||
if (data.error) { sendCompileErrorToAI(data.error, file.name, file.content); return; }
|
||||
sessionStorage.setItem('arduino_sketch', file.content);
|
||||
sessionStorage.setItem('arduino_hex', data.hex);
|
||||
@@ -909,7 +909,6 @@ function openArduinoEmulator() {
|
||||
codeAiAppend('system', '⚠️ 서버 오류: ' + e.message);
|
||||
});
|
||||
} else {
|
||||
// .ino 아니면 그냥 에뮬레이터 창만 열기
|
||||
window.open('/arduino-emulator.html', 'arduino-emulator');
|
||||
}
|
||||
}
|
||||
@@ -1322,6 +1321,7 @@ function codeRunWith(mode) {
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders({}) },
|
||||
body: JSON.stringify({ code: file.content, board: 'esp32' }),
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
|
||||
if (data.error) {
|
||||
sendCompileErrorToAI(data.error, file.name, file.content);
|
||||
return;
|
||||
@@ -1343,6 +1343,7 @@ function codeRunWith(mode) {
|
||||
headers: { 'Content-Type': 'application/json', ...authHeaders({}) },
|
||||
body: JSON.stringify({ code: file.content, board }),
|
||||
}).then(r => r.json()).then(data => {
|
||||
if (data.autoInstalled?.length) codeAiAppend('system', `📦 라이브러리 자동 설치: ${data.autoInstalled.join(', ')}`);
|
||||
if (data.error) {
|
||||
const extra = data.install ? `\n\n설치 필요: ${data.install}` : '';
|
||||
sendCompileErrorToAI(data.error + extra, file.name, file.content);
|
||||
|
||||
Reference in New Issue
Block a user