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:
@ -651,52 +651,141 @@ async fn launch_file(path: String) -> Result<(), String> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Look for an Unreal `.uproject` inside the working-folder scope (resolved to a
|
||||
/// local dir via `p4 where`). Checks the folder itself and one level of subdirs.
|
||||
/// Returns the local path of the first match, or "" if the scope isn't a UE project.
|
||||
#[tauri::command]
|
||||
async fn find_uproject(state: State<'_, AppState>, scope: String) -> Result<String, String> {
|
||||
/// Resolve a working-folder scope to a local directory via `p4 where`.
|
||||
fn scope_local_dir(conn: &Conn, scope: &str) -> Option<String> {
|
||||
if scope.is_empty() {
|
||||
return Ok(String::new());
|
||||
return None;
|
||||
}
|
||||
let conn = current(&state)?;
|
||||
let spec = format!("{}/...", scope.trim_end_matches(['/', '.']));
|
||||
let dir = match run_json(&conn, &["where", &spec])?
|
||||
run_json(conn, &["where", &spec])
|
||||
.ok()?
|
||||
.into_iter()
|
||||
.next()
|
||||
.and_then(|v| v.get("path").and_then(|p| p.as_str()).map(String::from))
|
||||
{
|
||||
Some(p) => p.trim_end_matches(['.', '/', '\\']).to_string(),
|
||||
None => return Ok(String::new()),
|
||||
};
|
||||
let is_uproject = |p: &std::path::Path| {
|
||||
p.extension().map_or(false, |x| x.eq_ignore_ascii_case("uproject"))
|
||||
};
|
||||
// the scope folder itself
|
||||
if let Ok(entries) = std::fs::read_dir(&dir) {
|
||||
.map(|p| p.trim_end_matches(['.', '/', '\\']).to_string())
|
||||
}
|
||||
|
||||
/// First file with the given extension in `dir` (checked, then one level of subdirs).
|
||||
fn find_ext(dir: &str, ext: &str) -> String {
|
||||
let matches = |p: &std::path::Path| p.extension().map_or(false, |x| x.eq_ignore_ascii_case(ext));
|
||||
if let Ok(entries) = std::fs::read_dir(dir) {
|
||||
let mut subdirs = Vec::new();
|
||||
for e in entries.flatten() {
|
||||
let p = e.path();
|
||||
if is_uproject(&p) {
|
||||
return Ok(p.to_string_lossy().to_string());
|
||||
if matches(&p) {
|
||||
return p.to_string_lossy().to_string();
|
||||
}
|
||||
if p.is_dir() {
|
||||
subdirs.push(p);
|
||||
}
|
||||
}
|
||||
// one level down
|
||||
for sd in subdirs {
|
||||
if let Ok(inner) = std::fs::read_dir(&sd) {
|
||||
for e in inner.flatten() {
|
||||
let p = e.path();
|
||||
if is_uproject(&p) {
|
||||
return Ok(p.to_string_lossy().to_string());
|
||||
if matches(&p) {
|
||||
return p.to_string_lossy().to_string();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(String::new())
|
||||
String::new()
|
||||
}
|
||||
|
||||
/// Local path of a `.uproject` in the scope (or "" if not a UE project).
|
||||
#[tauri::command]
|
||||
async fn find_uproject(state: State<'_, AppState>, scope: String) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
Ok(scope_local_dir(&conn, &scope).map(|d| find_ext(&d, "uproject")).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Local path of a Visual Studio `.sln` in the scope (or "" if none).
|
||||
#[tauri::command]
|
||||
async fn find_sln(state: State<'_, AppState>, scope: String) -> Result<String, String> {
|
||||
let conn = current(&state)?;
|
||||
Ok(scope_local_dir(&conn, &scope).map(|d| find_ext(&d, "sln")).unwrap_or_default())
|
||||
}
|
||||
|
||||
/// Build a Visual Studio solution with MSBuild (located via vswhere), streaming
|
||||
/// its output line-by-line as `build-log` events. Uses the solution's default
|
||||
/// configuration — for an Unreal project that compiles the game module.
|
||||
#[tauri::command]
|
||||
async fn build_sln(path: String) -> Result<String, String> {
|
||||
if !std::path::Path::new(&path).exists() {
|
||||
return Err(format!("Solution not found: {path}"));
|
||||
}
|
||||
let emit = |line: String, done: bool, ok: bool| {
|
||||
if let Some(app) = APP.get() {
|
||||
let _ = app.emit(
|
||||
"build-log",
|
||||
serde_json::json!({ "line": line, "done": done, "ok": ok }),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// locate MSBuild.exe via vswhere (falls back to PATH)
|
||||
let pf86 = std::env::var("ProgramFiles(x86)").unwrap_or_else(|_| "C:\\Program Files (x86)".into());
|
||||
let vswhere = format!("{pf86}\\Microsoft Visual Studio\\Installer\\vswhere.exe");
|
||||
let msbuild = {
|
||||
let mut vc = Command::new(&vswhere);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
vc.creation_flags(0x0800_0000);
|
||||
}
|
||||
vc.args(["-latest", "-requires", "Microsoft.Component.MSBuild", "-find", "MSBuild\\**\\Bin\\MSBuild.exe"]);
|
||||
match vc.output() {
|
||||
Ok(o) => String::from_utf8_lossy(&o.stdout).lines().next().unwrap_or("").trim().to_string(),
|
||||
Err(_) => String::new(),
|
||||
}
|
||||
};
|
||||
let msbuild = if msbuild.is_empty() { "MSBuild.exe".to_string() } else { msbuild };
|
||||
emit(format!("MSBuild: {msbuild}"), false, false);
|
||||
emit(format!("Building {path} …"), false, false);
|
||||
|
||||
let mut cmd = Command::new(&msbuild);
|
||||
#[cfg(windows)]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
cmd.creation_flags(0x0800_0000);
|
||||
}
|
||||
cmd.args([&path, "/m", "/nologo", "/v:minimal", "/clp:NoSummary"])
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped());
|
||||
let mut child = match cmd.spawn() {
|
||||
Ok(c) => c,
|
||||
Err(e) => {
|
||||
let m = format!("Could not start MSBuild (Visual Studio C++ tools installed?): {e}");
|
||||
emit(m.clone(), true, false);
|
||||
return Err(m);
|
||||
}
|
||||
};
|
||||
|
||||
if let Some(out) = child.stdout.take() {
|
||||
for line in BufReader::new(out).lines().map_while(Result::ok) {
|
||||
if !line.trim().is_empty() {
|
||||
emit(line, false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
let status = child.wait().map_err(|e| e.to_string())?;
|
||||
if let Some(mut se) = child.stderr.take() {
|
||||
let mut buf = String::new();
|
||||
let _ = std::io::Read::read_to_string(&mut se, &mut buf);
|
||||
for line in buf.lines() {
|
||||
if !line.trim().is_empty() {
|
||||
emit(line.to_string(), false, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
let ok = status.success();
|
||||
emit(if ok { "✓ Build succeeded.".into() } else { "✗ Build FAILED.".into() }, true, ok);
|
||||
if ok {
|
||||
Ok("ok".into())
|
||||
} else {
|
||||
Err("Build failed".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// Files and metadata of a submitted changelist (History detail).
|
||||
@ -792,6 +881,8 @@ pub fn run() {
|
||||
p4_undo_change,
|
||||
open_in_explorer,
|
||||
find_uproject,
|
||||
find_sln,
|
||||
build_sln,
|
||||
launch_file,
|
||||
open_in_vscode,
|
||||
p4_describe,
|
||||
|
||||
Reference in New Issue
Block a user