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:
kim
2026-06-11 23:03:54 +09:00
co-authored by Claude Sonnet 4.6
parent 0c46d52f42
commit 8b649e96d5
2 changed files with 206 additions and 43 deletions
+203 -41
View File
@@ -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
View File
@@ -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);