From afc4824b261890dbc492c21d2ef8ee5a89c354c9 Mon Sep 17 00:00:00 2001 From: Bonchellon Date: Tue, 7 Jul 2026 16:16:03 +0300 Subject: [PATCH] Fix build success detection + minimizable build window + de-dupe event logs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - build_sln now trusts UnrealBuildTool's "Result: Succeeded" (and absence of real compile errors) over MSBuild's exit code, which is non-zero for a side project even when the UE game module built fine. - Fixed double event-listener registration (async unlisten under StrictMode) that duplicated every p4-log / p4-transfer / build-log line. - Build overlay can be minimized to a small floating chip (spinner while building, ✓/✗ when done), click to expand, × to dismiss. Co-Authored-By: Claude Opus 4.8 --- src-tauri/src/lib.rs | 24 +++++++++++++++++++++++- src/App.css | 15 +++++++++++++++ src/App.tsx | 43 ++++++++++++++++++++++++++++++------------- src/i18n.ts | 1 + 4 files changed, 69 insertions(+), 14 deletions(-) diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 5c41910..1f556f0 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -762,9 +762,25 @@ async fn build_sln(path: String) -> Result { } }; + // For Unreal, the .sln drives UnrealBuildTool; MSBuild can return non-zero for a + // side project even when UBT reports the game module built fine. So trust UBT's + // "Result: Succeeded" (and the absence of real compile errors) over the exit code. + let mut ubt_result = false; + let mut ubt_success = false; + let mut compile_error = false; + let mut scan = |line: &str| { + let l = line.to_ascii_lowercase(); + if l.contains("result: succeeded") { ubt_result = true; ubt_success = true; } + if l.contains("result: failed") { ubt_result = true; } + if l.contains(": error ") || l.contains(") error ") || l.contains("error msb") { + compile_error = true; + } + }; + if let Some(out) = child.stdout.take() { for line in BufReader::new(out).lines().map_while(Result::ok) { if !line.trim().is_empty() { + scan(&line); emit(line, false, false); } } @@ -775,11 +791,17 @@ async fn build_sln(path: String) -> Result { let _ = std::io::Read::read_to_string(&mut se, &mut buf); for line in buf.lines() { if !line.trim().is_empty() { + scan(line); emit(line.to_string(), false, false); } } } - let ok = status.success(); + // UBT verdict wins when present; otherwise fall back to MSBuild's exit code + let ok = if ubt_result { + ubt_success && !compile_error + } else { + status.success() + }; emit(if ok { "✓ Build succeeded.".into() } else { "✗ Build FAILED.".into() }, true, ok); if ok { Ok("ok".into()) diff --git a/src/App.css b/src/App.css index 7301579..ab66e26 100644 --- a/src/App.css +++ b/src/App.css @@ -376,8 +376,23 @@ body.resizing{cursor:col-resize!important;user-select:none} .build-ttl b{font-size:14px} .build-ttl span{font-size:11.5px;color:var(--faint);font-family:var(--mono);white-space:nowrap;overflow:hidden;text-overflow:ellipsis} .build-x{margin-left:auto;width:28px;height:28px;flex:0 0 auto;border:1px solid var(--border);background:var(--panel);color:var(--muted);border-radius:8px;cursor:pointer;display:grid;place-items:center} +.build-x + .build-x{margin-left:6px} .build-x:hover:not(:disabled){color:var(--txt);border-color:var(--accent)} .build-x:disabled{opacity:.4;cursor:not-allowed} + +/* minimized build chip */ +.buildchip{position:fixed;right:18px;bottom:18px;z-index:360;display:flex;align-items:center;gap:10px; + padding:9px 10px 9px 13px;border-radius:12px;cursor:pointer;background:var(--elevated);border:1px solid var(--border); + box-shadow:0 16px 40px -14px rgba(0,0,0,.55);animation:updin .25s ease;max-width:300px} +.buildchip.run{border-color:rgba(124,110,246,.45)} +.buildchip.ok{border-color:rgba(62,207,142,.5)} +.buildchip.fail{border-color:rgba(242,99,126,.5)} +.buildchip:hover{filter:brightness(1.06)} +.bc-ic{width:20px;height:20px;flex:0 0 auto;display:grid;place-items:center;font-weight:700;font-size:14px} +.buildchip.run .bc-ic{color:var(--accent-2)}.buildchip.ok .bc-ic{color:var(--add)}.buildchip.fail .bc-ic{color:var(--del)} +.bc-txt{font-size:12.5px;font-weight:500;white-space:nowrap;overflow:hidden;text-overflow:ellipsis} +.bc-x{width:22px;height:22px;flex:0 0 auto;border:none;background:none;color:var(--faint);border-radius:6px;cursor:pointer;display:grid;place-items:center} +.bc-x:hover{background:var(--hover);color:var(--txt)} .build-body{flex:1;overflow:auto;min-height:0;padding:10px 14px;font-family:var(--mono);font-size:11.5px;line-height:1.6;background:var(--stage-bg)} .build-ln{white-space:pre-wrap;word-break:break-word;color:var(--muted)} .build-ln.err{color:var(--del)} diff --git a/src/App.tsx b/src/App.tsx index d9c651d..6651a6b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -298,6 +298,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set const [building, setBuilding] = useState(false); const [buildOk, setBuildOk] = useState(null); const [buildOpen, setBuildOpen] = useState(false); + const [buildMin, setBuildMin] = useState(false); // collapsed to a small floating chip const [ctx, setCtx] = useState(null); // right-click menu const [logs, setLogs] = useState([]); // live p4 command log (P4V-style) const [logOpen, setLogOpen] = useState(false); @@ -365,24 +366,24 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set useEffect(() => { refresh(); /* eslint-disable-next-line */ }, []); // live Perforce command log — the Rust backend emits one event per p4 call useEffect(() => { - let un: (() => void) | undefined; + let un: (() => void) | undefined; let dead = false; listen<{ cmd: string; count: number; ok: boolean; err?: string | null }>("p4-log", (e) => { setLogs((L) => { const next = [...L, { ...e.payload, id: ++logId.current }]; return next.length > 600 ? next.slice(-600) : next; }); - }).then((u) => (un = u)); - return () => un?.(); + }).then((u) => { if (dead) u(); else un = u; }); + return () => { dead = true; un?.(); }; }, []); // live file-transfer progress (Get Latest / Submit) streamed from the backend useEffect(() => { - let un: (() => void) | undefined; + let un: (() => void) | undefined; let dead = false; listen<{ op: "sync" | "submit"; count: number; file?: string; done: boolean }>("p4-transfer", (e) => { const p = e.payload; if (p.done) setTransfer(null); else setTransfer({ op: p.op, count: p.count, file: p.file }); - }).then((u) => (un = u)); - return () => un?.(); + }).then((u) => { if (dead) u(); else un = u; }); + return () => { dead = true; un?.(); }; }, []); // only surface the progress dialog once a transfer has run for a moment const xferActive = transfer !== null; @@ -401,13 +402,13 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set }, [activePath]); // live MSBuild output useEffect(() => { - let un: (() => void) | undefined; + let un: (() => void) | undefined; let dead = false; listen<{ line: string; done: boolean; ok: boolean }>("build-log", (e) => { const p = e.payload; setBuildLog((L) => (L.length > 4000 ? [...L.slice(-4000), p.line] : [...L, p.line])); if (p.done) { setBuilding(false); setBuildOk(p.ok); } - }).then((u) => (un = u)); - return () => un?.(); + }).then((u) => { if (dead) u(); else un = u; }); + return () => { dead = true; un?.(); }; }, []); useEffect(() => { const close = () => setOpenMenu(null); @@ -531,7 +532,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set // build the solution found in the working folder (MSBuild) with live output async function buildSln() { if (!slnPath) { flash(t("No .sln found in the working folder. Choose the project folder first."), true); return; } - setBuildLog([]); setBuildOk(null); setBuilding(true); setBuildOpen(true); + setBuildLog([]); setBuildOk(null); setBuilding(true); setBuildOpen(true); setBuildMin(false); try { await p4.buildSln(slnPath); } catch { /* the overlay already shows the failure line */ } } @@ -837,7 +838,8 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set onClose={() => setModal(null)} onPickWorkspace={switchWorkspace} onBrowse={browseTo} onChoose={chooseScope} onDisconnect={onDisconnect} />} {ask && { ask.resolve(v); setAsk(null); }} />} {showXfer && transfer && } - {buildOpen && setBuildOpen(false)} />} + {buildOpen && !buildMin && setBuildMin(true)} onClose={() => setBuildOpen(false)} />} + {buildOpen && buildMin && setBuildMin(false)} onClose={() => setBuildOpen(false)} />} {ctx && setCtx(null)} />} {toast &&
{toast.text}
} @@ -1033,7 +1035,7 @@ function TransferDialog({ transfer }: { transfer: Transfer }) { } /* ---------------- build output (MSBuild) ---------------- */ -function BuildOverlay({ lines, building, ok, sln, onClose }: { lines: string[]; building: boolean; ok: boolean | null; sln: string; onClose: () => void }) { +function BuildOverlay({ lines, building, ok, sln, onMinimize, onClose }: { lines: string[]; building: boolean; ok: boolean | null; sln: string; onMinimize: () => void; onClose: () => void }) { const ref = useRef(null); useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [lines]); const name = sln.replace(/\\/g, "/").split("/").pop() || "solution"; @@ -1048,6 +1050,9 @@ function BuildOverlay({ lines, building, ok, sln, onClose }: { lines: string[]; {building ? t("Building…") : ok ? t("Build succeeded") : ok === false ? t("Build failed") : t("Build")} {name} + @@ -1055,7 +1060,7 @@ function BuildOverlay({ lines, building, ok, sln, onClose }: { lines: string[];
{lines.map((ln, i) => { const low = ln.toLowerCase(); - const cls = /\berror\b|\bfailed\b|✗/.test(low) ? " err" : /\bwarning\b/.test(low) ? " warn" : /succeeded|✓/.test(low) ? " ok" : ""; + const cls = /: error |\bfailed\b|✗/.test(low) ? " err" : /\bwarning\b/.test(low) ? " warn" : /succeeded|✓/.test(low) ? " ok" : ""; return
{ln}
; })} {building && lines.length === 0 &&
{t("Starting MSBuild…")}
} @@ -1064,6 +1069,18 @@ function BuildOverlay({ lines, building, ok, sln, onClose }: { lines: string[];
); } +/* minimized build indicator — small floating chip, click to expand */ +function BuildChip({ building, ok, onExpand, onClose }: { building: boolean; ok: boolean | null; onExpand: () => void; onClose: () => void }) { + return ( +
+ {building ? : ok ? "✓" : "✗"} + {building ? t("Building…") : ok ? t("Build succeeded") : t("Build failed")} + +
+ ); +} /* ---------------- confirm modal ---------------- */ function ConfirmModal({ title, body, confirm, danger, onClose }: { title: string; body: string; confirm: string; danger?: boolean; onClose: (v: boolean) => void }) { diff --git a/src/i18n.ts b/src/i18n.ts index 7ded43b..5bb6af2 100644 --- a/src/i18n.ts +++ b/src/i18n.ts @@ -25,6 +25,7 @@ const D: Record = { "Minimize": { ru: "Свернуть", de: "Minimieren", fr: "Réduire", es: "Minimizar" }, "Maximize": { ru: "Развернуть", de: "Maximieren", fr: "Agrandir", es: "Maximizar" }, "Close": { ru: "Закрыть", de: "Schließen", fr: "Fermer", es: "Cerrar" }, + "Expand": { ru: "Развернуть", de: "Erweitern", fr: "Agrandir", es: "Expandir" }, "Theme": { ru: "Тема", de: "Thema", fr: "Thème", es: "Tema" }, "Restoring session…": { ru: "Восстановление сессии…", de: "Sitzung wird wiederhergestellt…", fr: "Restauration de la session…", es: "Restaurando sesión…" }, // startup splash