Add Build Solution (MSBuild) with live output
find_sln locates a .sln in the working folder; build_sln locates MSBuild via vswhere and builds the solution's default config (compiles the UE game module for Unreal projects), streaming output as build-log events. A build overlay shows the live log with error/warning coloring and a status. Wired into Tools (Ctrl+B) and the Working-folder right-click menu when a .sln is found. New strings translated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
66
src/App.tsx
66
src/App.tsx
@ -40,6 +40,7 @@ const I = {
|
||||
clock: <svg viewBox="0 0 24 24" fill="none"><circle cx="12" cy="12" r="8.5" stroke="currentColor" strokeWidth="1.6"/><path d="M12 8v4l2.5 2.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
||||
log: <svg viewBox="0 0 24 24" fill="none"><rect x="4" y="3" width="16" height="18" rx="2" stroke="currentColor" strokeWidth="1.6"/><path d="M8 8h8M8 12h8M8 16h5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round"/></svg>,
|
||||
vscode: <svg viewBox="0 0 24 24" fill="none"><path d="m9 8-5 4 5 4M15 8l5 4-5 4" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||
hammer: <svg viewBox="0 0 24 24" fill="none"><path d="M14 6l4 4M17 3l4 4-3 3-4-4 3-3ZM13 7 4 16a2 2 0 0 0 0 3l1 1a2 2 0 0 0 3 0l9-9" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" strokeLinejoin="round"/></svg>,
|
||||
ue: <svg viewBox="0 0 1280 1280"><path fillRule="evenodd" clipRule="evenodd" fill="currentColor" d="M640,1280c353.46,0,640-286.54,640-640S993.46,0,640,0S0,286.54,0,640S286.54,1280,640,1280z M803.7,995.81c156.5-73.92,205.56-210.43,216.6-263.61c-57.22,58.6-120.53,118-163.11,76.88c0,0-2.33-219.45-2.33-309.43c0-121,114.75-211.18,114.75-211.18c-63.11,11.24-138.89,33.71-219.33,112.65c-7.26,7.2-14.14,14.76-20.62,22.67c-34.47-26.39-79.14-18.48-79.14-18.48c24.14,13.26,48.23,51.88,48.23,83.85v314.26c0,0-52.63,46.3-93.19,46.3c-9.14,0.07-18.17-2.05-26.33-6.18c-8.16-4.13-15.21-10.15-20.56-17.56c-3.21-4.19-5.87-8.78-7.91-13.65V424.07c-11.99,9.89-52.51,18.04-52.51-49.22c0-41.79,30.11-91.6,83.73-122.15c-73.63,11.23-142.59,43.04-198.92,91.76c-42.8,36.98-77.03,82.85-100.31,134.4c-23.28,51.55-35.06,107.55-34.51,164.12c0,0,39.21-122.51,88.32-133.83c7.15-1.88,14.65-2.07,21.89-0.54c7.24,1.53,14.02,4.72,19.81,9.34c5.79,4.61,10.41,10.51,13.51,17.23c3.1,6.72,4.59,14.07,4.34,21.46V844.3c0,29.16-18.8,35.53-36.17,35.22c-11.77-0.83-23.4-3.02-34.66-6.53c35.86,48.53,82.46,88.12,136.15,115.66c53.69,27.54,113.03,42.29,173.37,43.1l106.05-106.6L803.7,995.81z"/></svg>,
|
||||
};
|
||||
|
||||
@ -292,6 +293,11 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
const [transfer, setTransfer] = useState<Transfer | null>(null); // live sync/submit file transfer
|
||||
const [showXfer, setShowXfer] = useState(false); // delayed so fast ops don't flash a dialog
|
||||
const [uproject, setUproject] = useState(""); // local .uproject path when the scope is a UE project
|
||||
const [slnPath, setSlnPath] = useState(""); // local .sln path in the scope (for building)
|
||||
const [buildLog, setBuildLog] = useState<string[]>([]); // live MSBuild output
|
||||
const [building, setBuilding] = useState(false);
|
||||
const [buildOk, setBuildOk] = useState<boolean | null>(null);
|
||||
const [buildOpen, setBuildOpen] = useState(false);
|
||||
const [ctx, setCtx] = useState<null | { x: number; y: number; items: CtxItem[] }>(null); // right-click menu
|
||||
const [logs, setLogs] = useState<LogEntry[]>([]); // live p4 command log (P4V-style)
|
||||
const [logOpen, setLogOpen] = useState(false);
|
||||
@ -385,13 +391,24 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
const id = setTimeout(() => setShowXfer(true), 400);
|
||||
return () => clearTimeout(id);
|
||||
}, [xferActive]);
|
||||
// detect a UE project in the current working folder → show the Launch button
|
||||
// detect a UE project / .sln in the current working folder (Launch / Build actions)
|
||||
useEffect(() => {
|
||||
if (!activePath) { setUproject(""); return; }
|
||||
if (!activePath) { setUproject(""); setSlnPath(""); return; }
|
||||
let live = true;
|
||||
p4.findUproject(activePath).then((p) => live && setUproject(p)).catch(() => live && setUproject(""));
|
||||
p4.findSln(activePath).then((p) => live && setSlnPath(p)).catch(() => live && setSlnPath(""));
|
||||
return () => { live = false; };
|
||||
}, [activePath]);
|
||||
// live MSBuild output
|
||||
useEffect(() => {
|
||||
let un: (() => void) | undefined;
|
||||
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?.();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const close = () => setOpenMenu(null);
|
||||
document.addEventListener("click", close);
|
||||
@ -511,6 +528,14 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
catch (e) { flash(String(e), true); }
|
||||
}
|
||||
|
||||
// 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);
|
||||
try { await p4.buildSln(slnPath); }
|
||||
catch { /* the overlay already shows the failure line */ }
|
||||
}
|
||||
|
||||
// reveal a depot path / workspace root in Windows Explorer
|
||||
async function reveal(target: string) {
|
||||
if (!target) { flash(t("Path not set"), true); return; }
|
||||
@ -647,7 +672,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
Connection: [{ label: t("Refresh"), kb: "F5", act: () => refresh() }, { label: t("Choose working folder…"), act: () => browseTo("") }],
|
||||
Actions: [{ label: t("Rescan changes"), kb: "Ctrl+R", act: rescan }, { label: t("Get Latest"), kb: "Ctrl+G", act: getLatest }, { label: t("Commit (local)"), act: doCommit }, { label: t("Submit to server"), kb: "Ctrl+S", act: pushAll }, { label: t("Revert All…"), act: revertAll }, { label: t("Refresh"), kb: "F5", act: () => refresh() }],
|
||||
Window: [{ label: (logOpen ? "✓ " : "") + t("Log"), kb: "Ctrl+L", act: () => setLogOpen((o) => !o) }],
|
||||
Tools: [{ label: t("Settings…"), act: () => setModal({ kind: "settings" }) }],
|
||||
Tools: [{ label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", act: buildSln }, { label: t("Settings…"), act: () => setModal({ kind: "settings" }) }],
|
||||
Help: [{ label: t("About"), act: () => setModal({ kind: "about" }) }],
|
||||
};
|
||||
|
||||
@ -690,6 +715,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
<div className="tzone" onClick={() => browseTo("")} title={t("Choose the project working folder · right-click for more")}
|
||||
onContextMenu={(e) => openCtx(e, [
|
||||
...(uproject ? [{ label: t("Launch Unreal Engine"), icon: I.ue, act: launchUE }] : []),
|
||||
...(slnPath ? [{ label: t("Build Solution (.sln)"), icon: I.hammer, act: buildSln }] : []),
|
||||
{ label: t("Open in Explorer"), act: () => reveal(activePath || String(info?.clientRoot || "")) },
|
||||
{ label: t("Choose another folder…"), act: () => browseTo("") },
|
||||
{ label: t("Copy path"), act: () => copyText(activePath || "//…", t("Path")) },
|
||||
@ -811,6 +837,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
||||
onClose={() => setModal(null)} onPickWorkspace={switchWorkspace} onBrowse={browseTo} onChoose={chooseScope} onDisconnect={onDisconnect} />}
|
||||
{ask && <ConfirmModal {...ask} onClose={(v) => { ask.resolve(v); setAsk(null); }} />}
|
||||
{showXfer && transfer && <TransferDialog transfer={transfer} />}
|
||||
{buildOpen && <BuildOverlay lines={buildLog} building={building} ok={buildOk} sln={slnPath} onClose={() => setBuildOpen(false)} />}
|
||||
{ctx && <ContextMenu x={ctx.x} y={ctx.y} items={ctx.items} onClose={() => setCtx(null)} />}
|
||||
{toast && <div className={"toast" + (toast.err ? " err" : "")}>{toast.text}</div>}
|
||||
</div>
|
||||
@ -1005,6 +1032,39 @@ 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 }) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => { if (ref.current) ref.current.scrollTop = ref.current.scrollHeight; }, [lines]);
|
||||
const name = sln.replace(/\\/g, "/").split("/").pop() || "solution";
|
||||
return (
|
||||
<div className="modal-back">
|
||||
<div className="build">
|
||||
<div className="build-head">
|
||||
<span className={"build-ic" + (building ? " run" : ok ? " ok" : ok === false ? " fail" : "")}>
|
||||
{building ? <span className="ldr sm" /> : ok ? "✓" : ok === false ? "✗" : I.hammer}
|
||||
</span>
|
||||
<div className="build-ttl">
|
||||
<b>{building ? t("Building…") : ok ? t("Build succeeded") : ok === false ? t("Build failed") : t("Build")}</b>
|
||||
<span>{name}</span>
|
||||
</div>
|
||||
<button className="build-x" onClick={onClose} disabled={building} title={building ? t("Building…") : t("Close")}>
|
||||
<svg viewBox="0 0 24 24" width="14" height="14" fill="none"><path d="M6 6l12 12M18 6L6 18" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" /></svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="build-body" ref={ref}>
|
||||
{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" : "";
|
||||
return <div key={i} className={"build-ln" + cls}>{ln}</div>;
|
||||
})}
|
||||
{building && lines.length === 0 && <div className="build-ln">{t("Starting MSBuild…")}</div>}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/* ---------------- confirm modal ---------------- */
|
||||
function ConfirmModal({ title, body, confirm, danger, onClose }: { title: string; body: string; confirm: string; danger?: boolean; onClose: (v: boolean) => void }) {
|
||||
useEffect(() => {
|
||||
|
||||
Reference in New Issue
Block a user