Unreal build configurations (Development / Shipping / …)
Build Solution now opens a configuration picker (Development Editor, Development, DebugGame, Shipping) and passes /p:Configuration=<cfg> /p:Platform=Win64 to MSBuild, which drives UnrealBuildTool for the chosen config. Streams into the same minimizable build overlay. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@ -774,7 +774,7 @@ async fn find_sln(state: State<'_, AppState>, scope: String) -> Result<String, S
|
|||||||
/// its output line-by-line as `build-log` events. Uses the solution's default
|
/// its output line-by-line as `build-log` events. Uses the solution's default
|
||||||
/// configuration — for an Unreal project that compiles the game module.
|
/// configuration — for an Unreal project that compiles the game module.
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn build_sln(path: String) -> Result<String, String> {
|
async fn build_sln(path: String, config: String) -> Result<String, String> {
|
||||||
if !std::path::Path::new(&path).exists() {
|
if !std::path::Path::new(&path).exists() {
|
||||||
return Err(format!("Solution not found: {path}"));
|
return Err(format!("Solution not found: {path}"));
|
||||||
}
|
}
|
||||||
@ -805,7 +805,11 @@ async fn build_sln(path: String) -> Result<String, String> {
|
|||||||
};
|
};
|
||||||
let msbuild = if msbuild.is_empty() { "MSBuild.exe".to_string() } else { msbuild };
|
let msbuild = if msbuild.is_empty() { "MSBuild.exe".to_string() } else { msbuild };
|
||||||
emit(format!("MSBuild: {msbuild}"), false, false);
|
emit(format!("MSBuild: {msbuild}"), false, false);
|
||||||
emit(format!("Building {path} …"), false, false);
|
emit(
|
||||||
|
format!("Building {path}{} …", if config.is_empty() { String::new() } else { format!(" [{config} | Win64]") }),
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
|
||||||
let mut cmd = Command::new(&msbuild);
|
let mut cmd = Command::new(&msbuild);
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
@ -813,9 +817,19 @@ async fn build_sln(path: String) -> Result<String, String> {
|
|||||||
use std::os::windows::process::CommandExt;
|
use std::os::windows::process::CommandExt;
|
||||||
cmd.creation_flags(0x0800_0000);
|
cmd.creation_flags(0x0800_0000);
|
||||||
}
|
}
|
||||||
cmd.args([&path, "/m", "/nologo", "/v:minimal", "/clp:NoSummary"])
|
// build args; a specific UE configuration ("Development Editor", "Shipping", …) → Win64
|
||||||
.stdout(Stdio::piped())
|
let mut args: Vec<String> = vec![
|
||||||
.stderr(Stdio::piped());
|
path.clone(),
|
||||||
|
"/m".into(),
|
||||||
|
"/nologo".into(),
|
||||||
|
"/v:minimal".into(),
|
||||||
|
"/clp:NoSummary".into(),
|
||||||
|
];
|
||||||
|
if !config.is_empty() {
|
||||||
|
args.push(format!("/p:Configuration={config}"));
|
||||||
|
args.push("/p:Platform=Win64".into());
|
||||||
|
}
|
||||||
|
cmd.args(&args).stdout(Stdio::piped()).stderr(Stdio::piped());
|
||||||
let mut child = match cmd.spawn() {
|
let mut child = match cmd.spawn() {
|
||||||
Ok(c) => c,
|
Ok(c) => c,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|||||||
51
src/App.tsx
51
src/App.tsx
@ -302,6 +302,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|||||||
const [buildOk, setBuildOk] = useState<boolean | null>(null);
|
const [buildOk, setBuildOk] = useState<boolean | null>(null);
|
||||||
const [buildOpen, setBuildOpen] = useState(false);
|
const [buildOpen, setBuildOpen] = useState(false);
|
||||||
const [buildMin, setBuildMin] = useState(false); // collapsed to a small floating chip
|
const [buildMin, setBuildMin] = useState(false); // collapsed to a small floating chip
|
||||||
|
const [buildPick, setBuildPick] = useState(false); // configuration picker before building
|
||||||
const [ctx, setCtx] = useState<null | { x: number; y: number; items: CtxItem[] }>(null); // right-click menu
|
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 [logs, setLogs] = useState<LogEntry[]>([]); // live p4 command log (P4V-style)
|
||||||
const [logOpen, setLogOpen] = useState(false);
|
const [logOpen, setLogOpen] = useState(false);
|
||||||
@ -533,11 +534,17 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|||||||
catch (e) { flash(String(e), true); }
|
catch (e) { flash(String(e), true); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// build the solution found in the working folder (MSBuild) with live output
|
// open the configuration picker (Development / Shipping / …) before building
|
||||||
async function buildSln() {
|
function startBuild() {
|
||||||
if (!slnPath) { flash(t("No .sln found in the working folder. Choose the project folder first."), true); return; }
|
if (!slnPath) { flash(t("No .sln found in the working folder. Choose the project folder first."), true); return; }
|
||||||
|
setBuildPick(true);
|
||||||
|
}
|
||||||
|
// build the solution (MSBuild) with the chosen UE configuration, live output
|
||||||
|
async function buildSln(config: string) {
|
||||||
|
setBuildPick(false);
|
||||||
|
if (!slnPath) return;
|
||||||
setBuildLog([]); setBuildOk(null); setBuilding(true); setBuildOpen(true); setBuildMin(false);
|
setBuildLog([]); setBuildOk(null); setBuilding(true); setBuildOpen(true); setBuildMin(false);
|
||||||
try { await p4.buildSln(slnPath); }
|
try { await p4.buildSln(slnPath, config); }
|
||||||
catch { /* the overlay already shows the failure line */ }
|
catch { /* the overlay already shows the failure line */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -716,7 +723,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("") }],
|
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() }],
|
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) }],
|
Window: [{ label: (logOpen ? "✓ " : "") + t("Log"), kb: "Ctrl+L", act: () => setLogOpen((o) => !o) }],
|
||||||
Tools: [{ label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", act: buildSln }, { label: t("Settings…"), act: () => setModal({ kind: "settings" }) }],
|
Tools: [{ label: slnPath ? t("Build Solution (.sln)") : t("Build Solution — no .sln"), kb: "Ctrl+B", act: startBuild }, { label: t("Settings…"), act: () => setModal({ kind: "settings" }) }],
|
||||||
Help: [{ label: t("About"), act: () => setModal({ kind: "about" }) }],
|
Help: [{ label: t("About"), act: () => setModal({ kind: "about" }) }],
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -759,7 +766,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")}
|
<div className="tzone" onClick={() => browseTo("")} title={t("Choose the project working folder · right-click for more")}
|
||||||
onContextMenu={(e) => openCtx(e, [
|
onContextMenu={(e) => openCtx(e, [
|
||||||
...(uproject ? [{ label: t("Launch Unreal Engine"), icon: I.ue, act: launchUE }] : []),
|
...(uproject ? [{ label: t("Launch Unreal Engine"), icon: I.ue, act: launchUE }] : []),
|
||||||
...(slnPath ? [{ label: t("Build Solution (.sln)"), icon: I.hammer, act: buildSln }] : []),
|
...(slnPath ? [{ label: t("Build Solution (.sln)"), icon: I.hammer, act: startBuild }] : []),
|
||||||
{ label: t("Open in Explorer"), act: () => reveal(activePath || String(info?.clientRoot || "")) },
|
{ label: t("Open in Explorer"), act: () => reveal(activePath || String(info?.clientRoot || "")) },
|
||||||
{ label: t("Choose another folder…"), act: () => browseTo("") },
|
{ label: t("Choose another folder…"), act: () => browseTo("") },
|
||||||
{ label: t("Copy path"), act: () => copyText(activePath || "//…", t("Path")) },
|
{ label: t("Copy path"), act: () => copyText(activePath || "//…", t("Path")) },
|
||||||
@ -920,6 +927,7 @@ function Workbench({ info, session, light, toggleTheme, lang, setLang, zoom, set
|
|||||||
{ask && <ConfirmModal {...ask} onClose={(v) => { ask.resolve(v); setAsk(null); }} />}
|
{ask && <ConfirmModal {...ask} onClose={(v) => { ask.resolve(v); setAsk(null); }} />}
|
||||||
{prompt && <TextPrompt title={prompt.title} label={prompt.label} value={prompt.value} onSave={prompt.onSave} onClose={() => setPrompt(null)} />}
|
{prompt && <TextPrompt title={prompt.title} label={prompt.label} value={prompt.value} onSave={prompt.onSave} onClose={() => setPrompt(null)} />}
|
||||||
{showXfer && transfer && <TransferDialog transfer={transfer} />}
|
{showXfer && transfer && <TransferDialog transfer={transfer} />}
|
||||||
|
{buildPick && <BuildPicker onPick={buildSln} onClose={() => setBuildPick(false)} />}
|
||||||
{buildOpen && !buildMin && <BuildOverlay lines={buildLog} building={building} ok={buildOk} sln={slnPath} onMinimize={() => setBuildMin(true)} onClose={() => setBuildOpen(false)} />}
|
{buildOpen && !buildMin && <BuildOverlay lines={buildLog} building={building} ok={buildOk} sln={slnPath} onMinimize={() => setBuildMin(true)} onClose={() => setBuildOpen(false)} />}
|
||||||
{buildOpen && buildMin && <BuildChip building={building} ok={buildOk} onExpand={() => setBuildMin(false)} onClose={() => setBuildOpen(false)} />}
|
{buildOpen && buildMin && <BuildChip building={building} ok={buildOk} onExpand={() => setBuildMin(false)} onClose={() => setBuildOpen(false)} />}
|
||||||
{ctx && <ContextMenu x={ctx.x} y={ctx.y} items={ctx.items} onClose={() => setCtx(null)} />}
|
{ctx && <ContextMenu x={ctx.x} y={ctx.y} items={ctx.items} onClose={() => setCtx(null)} />}
|
||||||
@ -1151,6 +1159,39 @@ function BuildOverlay({ lines, building, ok, sln, onMinimize, onClose }: { lines
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
/* build configuration picker (Unreal: Development / Shipping / …) */
|
||||||
|
function BuildPicker({ onPick, onClose }: { onPick: (config: string) => void; onClose: () => void }) {
|
||||||
|
useEffect(() => {
|
||||||
|
const onKey = (e: KeyboardEvent) => e.key === "Escape" && onClose();
|
||||||
|
document.addEventListener("keydown", onKey);
|
||||||
|
return () => document.removeEventListener("keydown", onKey);
|
||||||
|
}, [onClose]);
|
||||||
|
const opts: { config: string; name: string; desc: string }[] = [
|
||||||
|
{ config: "Development Editor", name: "Development Editor", desc: t("Editor build for working in-engine (default)") },
|
||||||
|
{ config: "Development", name: "Development", desc: t("Standalone game, dev config") },
|
||||||
|
{ config: "DebugGame", name: "DebugGame", desc: t("Game with debuggable game code") },
|
||||||
|
{ config: "Shipping", name: "Shipping", desc: t("Optimized release build") },
|
||||||
|
];
|
||||||
|
return (
|
||||||
|
<div className="modal-back" onClick={onClose}>
|
||||||
|
<div className="picker" onClick={(e) => e.stopPropagation()}>
|
||||||
|
<div className="picker-head">{I.hammer}<h3>{t("Build configuration")}</h3>
|
||||||
|
<button className="x" onClick={onClose}><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="picker-list">
|
||||||
|
{opts.map((o) => (
|
||||||
|
<div key={o.config} className="picker-item" onClick={() => onPick(o.config)}>
|
||||||
|
<span className="pi-ic">{I.hammer}</span>
|
||||||
|
<span className="pi-body"><span className="pi-name">{o.name} · Win64</span><span className="pi-sub">{o.desc}</span></span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<div className="picker-foot">{t("Builds via MSBuild → UnrealBuildTool")}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/* minimized build indicator — small floating chip, click to expand */
|
/* minimized build indicator — small floating chip, click to expand */
|
||||||
function BuildChip({ building, ok, onExpand, onClose }: { building: boolean; ok: boolean | null; onExpand: () => void; onClose: () => void }) {
|
function BuildChip({ building, ok, onExpand, onClose }: { building: boolean; ok: boolean | null; onExpand: () => void; onClose: () => void }) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -78,6 +78,12 @@ const D: Record<string, Tr> = {
|
|||||||
"Build succeeded": { ru: "Сборка успешна", de: "Build erfolgreich", fr: "Compilation réussie", es: "Compilación correcta" },
|
"Build succeeded": { ru: "Сборка успешна", de: "Build erfolgreich", fr: "Compilation réussie", es: "Compilación correcta" },
|
||||||
"Build failed": { ru: "Сборка провалена", de: "Build fehlgeschlagen", fr: "Échec de la compilation", es: "Compilación fallida" },
|
"Build failed": { ru: "Сборка провалена", de: "Build fehlgeschlagen", fr: "Échec de la compilation", es: "Compilación fallida" },
|
||||||
"Starting MSBuild…": { ru: "Запуск MSBuild…", de: "MSBuild wird gestartet…", fr: "Démarrage de MSBuild…", es: "Iniciando MSBuild…" },
|
"Starting MSBuild…": { ru: "Запуск MSBuild…", de: "MSBuild wird gestartet…", fr: "Démarrage de MSBuild…", es: "Iniciando MSBuild…" },
|
||||||
|
"Build configuration": { ru: "Конфигурация сборки", de: "Build-Konfiguration", fr: "Configuration de build", es: "Configuración de compilación" },
|
||||||
|
"Editor build for working in-engine (default)": { ru: "Сборка редактора для работы в движке (по умолчанию)", de: "Editor-Build zum Arbeiten in der Engine (Standard)", fr: "Build éditeur pour travailler dans le moteur (défaut)", es: "Build del editor para trabajar en el motor (predeterminado)" },
|
||||||
|
"Standalone game, dev config": { ru: "Отдельная игра, dev-конфиг", de: "Eigenständiges Spiel, Dev-Konfig", fr: "Jeu autonome, config dev", es: "Juego independiente, config dev" },
|
||||||
|
"Game with debuggable game code": { ru: "Игра с отлаживаемым кодом", de: "Spiel mit debugfähigem Code", fr: "Jeu avec code débogable", es: "Juego con código depurable" },
|
||||||
|
"Optimized release build": { ru: "Оптимизированный релизный билд", de: "Optimierter Release-Build", fr: "Build de release optimisé", es: "Build de lanzamiento optimizado" },
|
||||||
|
"Builds via MSBuild → UnrealBuildTool": { ru: "Сборка через MSBuild → UnrealBuildTool", de: "Build über MSBuild → UnrealBuildTool", fr: "Compile via MSBuild → UnrealBuildTool", es: "Compila con MSBuild → UnrealBuildTool" },
|
||||||
"Open this project in Unreal Engine": { ru: "Открыть проект в Unreal Engine", de: "Projekt in Unreal Engine öffnen", fr: "Ouvrir le projet dans Unreal Engine", es: "Abrir el proyecto en Unreal Engine" },
|
"Open this project in Unreal Engine": { ru: "Открыть проект в Unreal Engine", de: "Projekt in Unreal Engine öffnen", fr: "Ouvrir le projet dans Unreal Engine", es: "Abrir el proyecto en Unreal Engine" },
|
||||||
"Launching Unreal…": { ru: "Запуск Unreal…", de: "Unreal wird gestartet…", fr: "Lancement d’Unreal…", es: "Iniciando Unreal…" },
|
"Launching Unreal…": { ru: "Запуск Unreal…", de: "Unreal wird gestartet…", fr: "Lancement d’Unreal…", es: "Iniciando Unreal…" },
|
||||||
|
|
||||||
|
|||||||
@ -76,7 +76,7 @@ export const p4 = {
|
|||||||
openInExplorer: (target: string) => invoke<void>("open_in_explorer", { target }),
|
openInExplorer: (target: string) => invoke<void>("open_in_explorer", { target }),
|
||||||
findUproject: (scope: string) => invoke<string>("find_uproject", { scope }),
|
findUproject: (scope: string) => invoke<string>("find_uproject", { scope }),
|
||||||
findSln: (scope: string) => invoke<string>("find_sln", { scope }),
|
findSln: (scope: string) => invoke<string>("find_sln", { scope }),
|
||||||
buildSln: (path: string) => invoke<string>("build_sln", { path }),
|
buildSln: (path: string, config: string) => invoke<string>("build_sln", { path, config }),
|
||||||
launchFile: (path: string) => invoke<void>("launch_file", { path }),
|
launchFile: (path: string) => invoke<void>("launch_file", { path }),
|
||||||
openInVscode: (depot: string) => invoke<void>("open_in_vscode", { depot }),
|
openInVscode: (depot: string) => invoke<void>("open_in_vscode", { depot }),
|
||||||
readDepot: async (depot: string): Promise<ArrayBuffer> => {
|
readDepot: async (depot: string): Promise<ArrayBuffer> => {
|
||||||
|
|||||||
Reference in New Issue
Block a user