diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 7b15471b40..0a4d0ab478 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -835,6 +835,26 @@ fn show_main_window(app: &AppHandle) -> Result<(), String> { window .set_focus() .map_err(|err| format!("failed to focus main window: {err}"))?; + // `WebviewWindow::set_focus` only dispatches `WindowMessage::SetFocus` + // (vendor/tauri-cef cef_impl.rs `WindowMessage::SetFocus` → `window.request_focus()`), + // which lifts the OS window but does NOT call `CefBrowserHost::SetFocus(true)`. + // Without that CEF-level focus call the renderer never gets wired as the + // keyboard input target on cold launch: the chat textarea accepts focus + // (cursor blinks) but `WM_KEYDOWN` messages aren't forwarded to it, so + // typing is silently dead until the user click-outside / click-back + // triggers `WM_KILLFOCUS`+`WM_SETFOCUS` and CEF's window handler routes + // through `host.set_focus(1)` internally. + // + // Explicitly dispatch `WebviewMessage::SetFocus` (cef_impl.rs handler + // for that variant), which is what actually invokes + // `CefBrowserHost::SetFocus(true)`. + let webview: &tauri::Webview = window.as_ref(); + if let Err(err) = webview.set_focus() { + log::warn!( + "[show_main_window] CEF webview set_focus failed (non-fatal — \ + keyboard routing may not initialize until user click-outside-and-back): {err}" + ); + } Ok(()) } #[cfg(target_os = "linux")] @@ -1595,6 +1615,89 @@ pub fn run() { if let Err(err) = window.show() { log::warn!("[window-state] show main window failed: {err}"); } + // CEF keyboard routing fix — cold launch: + // + // `window.show()` does not wire the renderer as the + // keyboard input target. `Window::set_focus` only + // dispatches `WindowMessage::SetFocus` → `request_focus`, + // which lifts the OS window but does not call + // `CefBrowserHost::SetFocus(true)`. Without that + // CEF-level focus, the textarea accepts focus on cold + // launch (cursor blinks) but `WM_KEYDOWN` messages + // never reach the renderer — typing is silently dead + // until the user click-outside / click-back triggers + // `WM_KILLFOCUS`+`WM_SETFOCUS`, which CEF's window + // handler routes through `host.set_focus(1)` internally. + // + // We need to call `webview.set_focus()` (which dispatches + // `WebviewMessage::SetFocus` → `host.set_focus(1)`) + // *after* CEF has finished creating the browser — too + // early and `browser()`/`host()` return None and the + // call silently no-ops. Defer the call to a spawned + // task with a small delay so CEF's browser-create + // settles. Then call it again after another delay as + // belt-and-suspenders for slower init paths. + // Previous attempts at calling `webview.set_focus()` alone + // confirmed the dispatch reaches CEF (both returned Ok), + // but keyboard routing stayed broken. `host.set_focus(1)` + // alone is insufficient — CEF's internal focus state + // needs a blur-then-focus *cycle* to wire keyboard + // routing on cold launch (matches the user-discovered + // workaround: click outside the window, then click back). + // + // The vendored tauri-cef doesn't expose `set_focus(false)`, + // so we mimic the cycle at the OS-window level: + // minimize triggers `WM_KILLFOCUS` (CEF's window handler + // propagates this to `host.set_focus(0)`), unminimize + // restores the window and triggers `WM_SETFOCUS` → + // `host.set_focus(1)`. Pair with explicit `set_focus` + // calls on both Window and Webview to cover the case + // where minimize/unminimize raced ahead of CEF's + // browser-create. + // Windows-only: the bug class (CEF host-renderer focus + // desync after a `visible: false` → `show()` transition + // without a real `WM_KILLFOCUS`+`WM_SETFOCUS` edge) + // manifests on the Windows CEF integration. macOS and + // Linux CEF use different focus propagation paths and + // don't exhibit the symptom, so running the + // minimize/unminimize cycle there would just be a + // visible flicker for no benefit. (Per CodeRabbit + // review on PR #1528.) + #[cfg(target_os = "windows")] + { + log::info!("[focus-fix] scheduling deferred CEF focus-cycle"); + let webview_window_clone = window.clone(); + tauri::async_runtime::spawn(async move { + // Wait for CEF to finish creating the browser host + // (synchronous setup() returns before this completes). + tokio::time::sleep(std::time::Duration::from_millis(300)).await; + // Blur-then-focus cycle via minimize/unminimize. + // This is what the manual click-outside / click-back + // workaround does at the Win32 level. + log::info!("[focus-fix] starting minimize→unminimize focus cycle"); + if let Err(err) = webview_window_clone.minimize() { + log::warn!("[focus-fix] minimize failed: {err}"); + } + // Tiny pause so Windows actually processes the + // minimize before we ask to restore. + tokio::time::sleep(std::time::Duration::from_millis(80)).await; + if let Err(err) = webview_window_clone.unminimize() { + log::warn!("[focus-fix] unminimize failed: {err}"); + } + // Belt-and-suspenders: explicit Window + Webview focus + // after the cycle in case the minimize→restore path + // didn't propagate. + tokio::time::sleep(std::time::Duration::from_millis(40)).await; + if let Err(err) = webview_window_clone.set_focus() { + log::warn!("[focus-fix] post-cycle window.set_focus failed: {err}"); + } + let webview: &tauri::Webview = webview_window_clone.as_ref(); + if let Err(err) = webview.set_focus() { + log::warn!("[focus-fix] post-cycle webview.set_focus failed: {err}"); + } + log::info!("[focus-fix] focus cycle complete"); + }); + } } } diff --git a/scripts/run-dev-win.sh b/scripts/run-dev-win.sh index a32ec8390e..3840409237 100644 --- a/scripts/run-dev-win.sh +++ b/scripts/run-dev-win.sh @@ -12,8 +12,24 @@ cd "$APP_DIR" # shellcheck source=../scripts/load-dotenv.sh source "$REPO_ROOT/scripts/load-dotenv.sh" +# When pnpm/PowerShell/cmd launch `bash.exe` directly, the spawned shell +# inherits the parent PATH and the MSYS utility directory (`Git\usr\bin`) +# may be absent — bash runs, but `cygpath`, `mktemp`, `grep`, `sort`, etc. +# are missing. Probe known Git-for-Windows install locations and prepend +# `usr/bin` so the rest of the script works regardless of launcher. if ! command -v cygpath >/dev/null 2>&1; then - echo "[run-dev-win] cygpath not found. Run this script from Git Bash or MSYS2." + for git_root in "/c/Program Files/Git" "/c/Program Files (x86)/Git"; do + if [[ -x "$git_root/usr/bin/cygpath.exe" ]]; then + export PATH="$git_root/usr/bin:$PATH" + break + fi + done +fi + +if ! command -v cygpath >/dev/null 2>&1; then + echo "[run-dev-win] cygpath not found. Run this script from Git Bash or MSYS2," + echo "[run-dev-win] or install Git for Windows so cygpath.exe is available at" + echo "[run-dev-win] 'C:\\Program Files\\Git\\usr\\bin\\cygpath.exe'." exit 1 fi @@ -22,6 +38,44 @@ if [[ -z "${LOCALAPPDATA:-}" ]]; then exit 1 fi +# ───────────────────────────────────────────────────────────────────────────── +# Restore the real Windows-side PATH. +# +# Git for Windows' bash sources /etc/profile + /etc/profile.d/* on every +# spawn, which REPLACES the inherited Windows PATH with an MSYS-only +# default (/usr/local/bin:/usr/bin:/bin:…). That wipes every tool the +# parent shell saw — node, cargo, pnpm, ninja, cmake, etc. — and breaks +# any downstream script that assumes PATH inheritance. +# +# Pull the full machine + user PATH from a cmd.exe subprocess (which DOES +# inherit the unaltered Windows PATH from its parent), convert each entry +# to MSYS form, and append it to the current PATH. We append (not prepend) +# so MSYS coreutils (cygpath, grep, sed, mktemp) still resolve first. +# ───────────────────────────────────────────────────────────────────────────── +cmd_exe_for_path="$(command -v cmd.exe 2>/dev/null || command -v cmd 2>/dev/null || echo /c/Windows/System32/cmd.exe)" +if [[ -x "$cmd_exe_for_path" ]]; then + windows_path_raw="$("$cmd_exe_for_path" //c "echo %PATH%" 2>/dev/null | tr -d '\r' | head -n1 || true)" + if [[ -n "$windows_path_raw" ]]; then + windows_path_unix="" + IFS=';' read -ra _wpe <<< "$windows_path_raw" + for _entry in "${_wpe[@]}"; do + [[ -z "$_entry" ]] && continue + _u="$(cygpath -u "$_entry" 2>/dev/null || printf '%s' "$_entry")" + windows_path_unix="${windows_path_unix}${windows_path_unix:+:}${_u}" + done + if [[ -n "$windows_path_unix" ]]; then + export PATH="$PATH:$windows_path_unix" + echo "[run-dev-win] appended Windows-side PATH (node/cargo/pnpm/… now findable)" + else + echo "[run-dev-win] WARNING: cmd.exe PATH query returned no entries — node/cargo may be missing downstream" >&2 + fi + else + echo "[run-dev-win] WARNING: cmd.exe PATH query returned empty — node/cargo may be missing downstream" >&2 + fi +else + echo "[run-dev-win] WARNING: cmd.exe not found at '$cmd_exe_for_path' — Windows PATH restoration skipped; node/cargo may be missing downstream" >&2 +fi + export LIBCLANG_PATH="/c/Program Files/LLVM/bin" # Bootstrap the MSVC C++ build environment in this shell so cl.exe / link.exe / @@ -53,22 +107,46 @@ if ! command -v cl.exe >/dev/null 2>&1; then # file, then have cmd execute the file. Avoids inner quoting entirely. vcvars_launcher="$(mktemp --suffix=.bat)" vcvars_launcher_win="$(cygpath -w "$vcvars_launcher")" + # vcvarsall.bat (called by vcvars64.bat) shells out to `vswhere` by bare + # name to locate Windows SDK / MSVC component versions. If vswhere isn't + # on cmd.exe's PATH, vcvarsall silently degrades — it sets `cl.exe` on + # PATH but skips the Windows SDK `LIB` / `INCLUDE` entries, which then + # fails the link step downstream with `LNK1181: cannot open input file + # 'kernel32.lib'`. The VS Installer dir holding vswhere is rarely on the + # system PATH (Microsoft expects you to invoke vswhere by absolute path), + # so we prepend it inside the launcher .bat before calling vcvars. + vswhere_dir_win="$(cygpath -w "$(dirname "$vswhere_exe")")" # Note: we deliberately do NOT redirect vcvars64.bat's stdout to NUL — MSYS # would rewrite `NUL` to `/dev/null` while writing the .bat. Instead we let # vcvars64 print its banner and filter for `KEY=VALUE` lines below. - printf '@echo off\r\ncall "%s"\r\nset\r\n' "$vcvars_bat" > "$vcvars_launcher" + printf '@echo off\r\nset "PATH=%s;%%PATH%%"\r\ncall "%s"\r\nset\r\n' \ + "$vswhere_dir_win" "$vcvars_bat" > "$vcvars_launcher" # Note: do NOT set MSYS_NO_PATHCONV here — disabling path conversion stops # MSYS from rewriting `//c` to `/c`, leaving cmd to treat `//c` as an # unknown switch and open an interactive shell instead of executing the # launcher. - msvc_env="$(cmd //c "$vcvars_launcher_win" 2>&1 || true)" + # `cmd` may be missing from PATH when bash.exe is spawned by pnpm/PowerShell + # with a stripped environment. Fall back to the well-known absolute path. + cmd_exe="$(command -v cmd.exe 2>/dev/null || command -v cmd 2>/dev/null || echo /c/Windows/System32/cmd.exe)" + if [[ ! -x "$cmd_exe" ]]; then + echo "[run-dev-win] cmd.exe not found on PATH and /c/Windows/System32/cmd.exe missing" >&2 + rm -f "$vcvars_launcher" + exit 1 + fi + msvc_env_raw="$("$cmd_exe" //c "$vcvars_launcher_win" 2>&1 || true)" rm -f "$vcvars_launcher" # Strip lines that aren't key=value (vcvars banner, blank lines). - msvc_env="$(printf '%s\n' "$msvc_env" | grep -E '^[A-Za-z_][A-Za-z0-9_()]*=' || true)" + msvc_env="$(printf '%s\n' "$msvc_env_raw" | grep -E '^[A-Za-z_][A-Za-z0-9_()]*=' || true)" if [[ -z "$msvc_env" ]]; then echo "[run-dev-win] failed to capture MSVC env from vcvars64.bat" >&2 + echo "[run-dev-win] cmd.exe used: $cmd_exe" >&2 + echo "[run-dev-win] launcher: $vcvars_launcher_win" >&2 + echo "[run-dev-win] --- cmd output (first 40 lines) ---" >&2 + printf '%s\n' "$msvc_env_raw" | head -n 40 >&2 + echo "[run-dev-win] --- end cmd output ---" >&2 exit 1 fi + pre_vcvars_path="$PATH" while IFS='=' read -r key value; do case "$key" in PATH) @@ -80,7 +158,11 @@ if ! command -v cl.exe >/dev/null 2>&1; then unix_entry="$(cygpath -u "$entry" 2>/dev/null || printf '%s' "$entry")" new_path="${new_path}${new_path:+:}${unix_entry}" done - export PATH="$new_path" + # Prepend vcvars' PATH so MSVC tools win, but append the pre-vcvars + # PATH so node, pnpm, git, etc. remain findable. vcvars64.bat ships a + # MSVC-only PATH; without re-adding the original, downstream tools + # (pnpm.cmd invoking node, etc.) blow up with "node is not recognized". + export PATH="$new_path:$pre_vcvars_path" ;; INCLUDE|LIB|LIBPATH) # Compiler/linker want Windows-style ;-separated paths — leave as-is. @@ -98,6 +180,65 @@ if ! command -v cl.exe >/dev/null 2>&1; then echo "[run-dev-win] MSVC env loaded (cl.exe at $(command -v cl.exe))" fi +# Windows SDK self-discovery fallback. +# +# vcvars64.bat can silently "succeed" while only setting up the MSVC half +# of the toolchain — when vswhere is missing from PATH at the time +# vcvars runs, or when the Windows SDK isn't registered in the way +# vcvarsall expects, it skips setting `WindowsSdkDir` / `WindowsSDKVersion` +# and only appends MSVC's own libs to `LIB`. The linker then fails with +# `LNK1181: cannot open input file 'kernel32.lib'` because the SDK's +# `um\x64\kernel32.lib` isn't on the search list. +# +# This block runs unconditionally (whether or not we just bootstrapped +# vcvars) and patches in the SDK paths if they're missing. Detects the +# latest installed SDK on disk under `Windows Kits\10\Lib` and appends +# both lib and include trees. +if [[ -z "${WindowsSdkDir:-}" || "${WindowsSDKVersion:-}" == "\\" || -z "${WindowsSDKVersion:-}" ]]; then + sdk_root_unix="/c/Program Files (x86)/Windows Kits/10" + if [[ -d "$sdk_root_unix/Lib" ]]; then + sdk_version="$(ls -d "$sdk_root_unix"/Lib/*/ 2>/dev/null \ + | sort -V | tail -n1 \ + | sed 's|.*/||; s|/||g')" + if [[ -n "$sdk_version" && -f "$sdk_root_unix/Lib/$sdk_version/um/x64/kernel32.lib" ]]; then + sdk_root_win="$(cygpath -w "$sdk_root_unix")" + export WindowsSdkDir="${sdk_root_win}\\" + export WindowsSDKVersion="${sdk_version}\\" + sdk_lib_um="${sdk_root_win}\\Lib\\${sdk_version}\\um\\x64" + sdk_lib_ucrt="${sdk_root_win}\\Lib\\${sdk_version}\\ucrt\\x64" + sdk_inc_shared="${sdk_root_win}\\Include\\${sdk_version}\\shared" + sdk_inc_um="${sdk_root_win}\\Include\\${sdk_version}\\um" + sdk_inc_ucrt="${sdk_root_win}\\Include\\${sdk_version}\\ucrt" + sdk_inc_winrt="${sdk_root_win}\\Include\\${sdk_version}\\winrt" + export LIB="${LIB:+$LIB;}${sdk_lib_um};${sdk_lib_ucrt}" + export INCLUDE="${INCLUDE:+$INCLUDE;}${sdk_inc_shared};${sdk_inc_um};${sdk_inc_ucrt};${sdk_inc_winrt}" + # Prepend the SDK bin dir to PATH so `rc.exe` (Windows Resource + # Compiler) is findable. CMake-driven native crates (cef-dll-sys + # via cmake-rs, whisper-rs-sys, etc.) invoke `rc` by bare name + # during their try-compile probe; vcvars usually adds this dir + # but doesn't when its SDK detection degraded. + sdk_bin_unix="$sdk_root_unix/bin/$sdk_version/x64" + if [[ -x "$sdk_bin_unix/rc.exe" ]]; then + export PATH="$sdk_bin_unix:$PATH" + echo "[run-dev-win] SDK bin dir (with rc.exe) prepended to PATH: $sdk_bin_unix" + else + echo "[run-dev-win] WARNING: rc.exe not found at $sdk_bin_unix — CMake-driven crates will fail" >&2 + fi + echo "[run-dev-win] Windows SDK discovered manually (vcvars degraded): version ${sdk_version}" + else + echo "[run-dev-win] WARNING: Windows SDK version dir or kernel32.lib not found under $sdk_root_unix/Lib" >&2 + echo "[run-dev-win] linker will likely fail with LNK1181." >&2 + fi + else + echo "[run-dev-win] WARNING: Windows SDK not installed at $sdk_root_unix" >&2 + echo "[run-dev-win] Install via Visual Studio Build Tools and retry." >&2 + fi +fi + +echo "[run-dev-win] LIB = ${LIB:-}" +echo "[run-dev-win] WindowsSdkDir = ${WindowsSdkDir:-}" +echo "[run-dev-win] WindowsSDKVersion = ${WindowsSDKVersion:-}" + # Pin the linker by absolute path — runs whether or not we just bootstrapped # the MSVC env. PATH ordering alone isn't reliable: the bash-side reorder # doesn't always survive into the Windows-side %PATH% that rustc sees when @@ -167,7 +308,54 @@ find_pnpm() { command -v pnpm return 0 fi - find_winget_exe "pnpm.pnpm" "pnpm.exe" + # WinGet (preferred on a fresh contributor machine). + if winget_pnpm="$(find_winget_exe "pnpm.pnpm" "pnpm.exe")"; then + printf '%s\n' "$winget_pnpm" + return 0 + fi + # npm-global install — `npm i -g pnpm` drops a shim under %APPDATA%\npm. + # The shim is a `.cmd` on Windows; bash invokes .cmd via the same path. + local appdata_unix="" + if [[ -n "${APPDATA:-}" ]]; then + appdata_unix="$(to_unix_path "$APPDATA" 2>/dev/null || true)" + fi + if [[ -z "$appdata_unix" && -n "${USERPROFILE:-}" ]]; then + local userprofile_unix + userprofile_unix="$(to_unix_path "$USERPROFILE" 2>/dev/null || true)" + if [[ -n "$userprofile_unix" ]]; then + appdata_unix="$userprofile_unix/AppData/Roaming" + fi + fi + # Ordering matters: prefer the bare shebang shim (a `#!/bin/sh` script) + # over `pnpm.cmd`. The .cmd shim invokes `node` through cmd.exe, which + # ignores the bash-side PATH after vcvars rewriting and blows up with + # `'"node"' is not recognized`. The bash shim execs node directly using + # bash's PATH, which we've taken care to keep node on. + # + # NB: MSYS does NOT set the execute bit on .cmd files (only on .exe and + # shebang-prefixed scripts), so we test with `-f` (regular file) rather + # than `-x`. + if [[ -n "$appdata_unix" ]]; then + for candidate in \ + "$appdata_unix/npm/pnpm" \ + "$appdata_unix/npm/pnpm.cmd" \ + "$appdata_unix/npm/pnpm.exe"; do + if [[ -f "$candidate" ]]; then + printf '%s\n' "$candidate" + return 0 + fi + done + fi + # Chocolatey shim — same pattern as find_ninja above. + for choco_pnpm in \ + "/c/ProgramData/chocolatey/bin/pnpm.cmd" \ + "/c/ProgramData/chocolatey/bin/pnpm.exe"; do + if [[ -f "$choco_pnpm" ]]; then + printf '%s\n' "$choco_pnpm" + return 0 + fi + done + return 1 } find_ninja() { @@ -175,14 +363,133 @@ find_ninja() { command -v ninja return 0 fi - find_winget_exe "Ninja-build.Ninja" "ninja.exe" + # WinGet (preferred on a fresh contributor machine). + if winget_ninja="$(find_winget_exe "Ninja-build.Ninja" "ninja.exe")"; then + printf '%s\n' "$winget_ninja" + return 0 + fi + # Chocolatey shim — common on engineering desktops that pre-date WinGet. + # `-f` rather than `-x` because MSYS leaves .cmd files unmarked-executable. + for choco_ninja in \ + "/c/ProgramData/chocolatey/bin/ninja.exe" \ + "/c/ProgramData/chocolatey/bin/ninja.cmd" \ + "/c/ProgramData/chocolatey/lib/ninja/tools/ninja.exe"; do + if [[ -f "$choco_ninja" ]]; then + printf '%s\n' "$choco_ninja" + return 0 + fi + done + # CMake's own bundled ninja, if a recent CMake install dropped one alongside. + local bundled="/c/Program Files/CMake/bin/ninja.exe" + if [[ -f "$bundled" ]]; then + printf '%s\n' "$bundled" + return 0 + fi + return 1 +} + +# pnpm.cmd / the bare pnpm shim both ultimately `exec node ...`. When +# PowerShell launches pnpm which launches bash.exe, the inherited PATH +# does NOT reliably include Node.js — and vcvars wipes the rest. Probe +# the common Windows install locations and prepend whatever we find so +# downstream `exec node` calls in pnpm shims and Tauri scripts succeed. +find_nodejs_dir() { + # 1) Already on PATH (unlikely if we got here, but cheap to check). + if command -v node >/dev/null 2>&1 || command -v node.exe >/dev/null 2>&1; then + dirname "$(command -v node 2>/dev/null || command -v node.exe)" + return 0 + fi + # 2) Standard installer locations. + for nodejs_dir in \ + "/c/Program Files/nodejs" \ + "/c/Program Files (x86)/nodejs"; do + if [[ -f "$nodejs_dir/node.exe" ]]; then + printf '%s\n' "$nodejs_dir" + return 0 + fi + done + # 3) nvm-for-windows: %LOCALAPPDATA%\nvm\v. Pick the highest. + if [[ -n "${LOCALAPPDATA:-}" ]]; then + local nvm_root + nvm_root="$(to_unix_path "$LOCALAPPDATA" 2>/dev/null || true)/nvm" + if [[ -d "$nvm_root" ]]; then + local nvm_pick + nvm_pick="$(ls -d "$nvm_root"/v* 2>/dev/null | sort -V | tail -n1)" + if [[ -n "$nvm_pick" && -f "$nvm_pick/node.exe" ]]; then + printf '%s\n' "$nvm_pick" + return 0 + fi + fi + fi + # 4) Chocolatey shim. + if [[ -f "/c/ProgramData/chocolatey/bin/node.exe" ]]; then + printf '%s\n' "/c/ProgramData/chocolatey/bin" + return 0 + fi + return 1 +} + +NODEJS_DIR="$(find_nodejs_dir || true)" +if [[ -z "$NODEJS_DIR" ]]; then + echo "[run-dev-win] node.exe not found on PATH or in common Windows install dirs." >&2 + echo "[run-dev-win] Install Node.js (https://nodejs.org/) and retry." >&2 + exit 1 +fi +export PATH="$NODEJS_DIR:$PATH" +echo "[run-dev-win] nodejs dir prepended to PATH: $NODEJS_DIR" + +# Same trick for cargo. Git Bash's /etc/profile.d scripts wipe the parent +# Windows PATH and re-install a MSYS-default one; rustup's +# `~/.cargo/bin` (or `$CARGO_HOME/bin`) doesn't survive that. We need +# cargo for the vendored tauri-cli install (`ensure-tauri-cli.sh`), +# `core:stage`, and `cargo tauri dev` itself. +find_cargo_dir() { + if command -v cargo >/dev/null 2>&1 || command -v cargo.exe >/dev/null 2>&1; then + dirname "$(command -v cargo 2>/dev/null || command -v cargo.exe)" + return 0 + fi + # 1) Honour CARGO_HOME (rustup, workspace .env conventions). + if [[ -n "${CARGO_HOME:-}" ]]; then + local ch + ch="$(to_unix_path "$CARGO_HOME" 2>/dev/null || printf '%s' "$CARGO_HOME")" + if [[ -f "$ch/bin/cargo.exe" ]]; then + printf '%s\n' "$ch/bin" + return 0 + fi + fi + # 2) Default rustup install at %USERPROFILE%\.cargo\bin. + if [[ -n "${USERPROFILE:-}" ]]; then + local up + up="$(to_unix_path "$USERPROFILE" 2>/dev/null || true)" + if [[ -n "$up" && -f "$up/.cargo/bin/cargo.exe" ]]; then + printf '%s\n' "$up/.cargo/bin" + return 0 + fi + fi + # 3) Same path via $HOME (Git Bash sometimes only sets HOME, not USERPROFILE). + if [[ -n "${HOME:-}" && -f "$HOME/.cargo/bin/cargo.exe" ]]; then + printf '%s\n' "$HOME/.cargo/bin" + return 0 + fi + return 1 } +CARGO_DIR="$(find_cargo_dir || true)" +if [[ -z "$CARGO_DIR" ]]; then + echo "[run-dev-win] cargo.exe not found. Install Rust via rustup (https://rustup.rs/) and retry." >&2 + exit 1 +fi +export PATH="$CARGO_DIR:$PATH" +echo "[run-dev-win] cargo dir prepended to PATH: $CARGO_DIR" + PNPM_EXE="$(find_pnpm || true)" if [[ -z "$PNPM_EXE" ]]; then echo "[run-dev-win] pnpm not found. Install pnpm and retry." exit 1 fi +echo "[run-dev-win] pnpm resolved to: $PNPM_EXE" +echo "[run-dev-win] node on bash PATH: $(command -v node 2>/dev/null || echo '')" +echo "[run-dev-win] node.exe on bash PATH: $(command -v node.exe 2>/dev/null || echo '')" NINJA_EXE="$(find_ninja || true)" if [[ -z "$NINJA_EXE" ]]; then @@ -204,6 +511,38 @@ export PATH="$PATH_PREFIX:$PATH" "$PNPM_EXE" tauri:ensure "$PNPM_EXE" core:stage + +# ───────────────────────────────────────────────────────────────────────────── +# Stage the CEF runtime next to the dev OpenHuman.exe. +# +# `cargo tauri build` (release) copies CEF into the bundle automatically, but +# `cargo tauri dev` doesn't — the dev .exe lands at /debug/OpenHuman.exe +# alone, and Windows can't find libcef.dll. The .exe panics during boot with +# `cef::library_loader::LibraryLoader::new` errors (or just refuses to launch +# with "libcef.dll not found"). Without this step every fresh contributor +# session hits the wall. +# +# We stage by copying (not symlinking) so the script runs without admin / +# Developer-Mode privileges. `cp -ru` only copies entries newer than the +# destination, so subsequent dev runs are essentially free. +# ───────────────────────────────────────────────────────────────────────────── +if [[ -n "${CEF_RUNTIME_PATH:-}" && -f "$CEF_RUNTIME_PATH/libcef.dll" ]]; then + CARGO_TARGET_DIR_UNIX="$(to_unix_path "${CARGO_TARGET_DIR:-$REPO_ROOT/target}" 2>/dev/null || printf '%s' "${CARGO_TARGET_DIR:-$REPO_ROOT/target}")" + CEF_STAGE_DIR="$CARGO_TARGET_DIR_UNIX/debug" + mkdir -p "$CEF_STAGE_DIR" + if [[ ! -f "$CEF_STAGE_DIR/libcef.dll" \ + || "$CEF_RUNTIME_PATH/libcef.dll" -nt "$CEF_STAGE_DIR/libcef.dll" ]]; then + echo "[run-dev-win] staging CEF runtime → $CEF_STAGE_DIR (first run only — copies ~270MB)" + cp -ru "$CEF_RUNTIME_PATH"/. "$CEF_STAGE_DIR/" + echo "[run-dev-win] CEF runtime staged" + else + echo "[run-dev-win] CEF runtime already staged at $CEF_STAGE_DIR (libcef.dll up to date)" + fi +else + echo "[run-dev-win] WARNING: CEF_RUNTIME_PATH not set or libcef.dll missing — the dev exe will fail to load" >&2 + echo "[run-dev-win] expected: $CEF_PATH//cef_windows_x86_64/libcef.dll" >&2 +fi + # Use the vendored tauri-cef CLI (via the pnpm tauri script) so the # CEF runtime is correctly bundled. APPLE_SIGNING_IDENTITY is macOS-only # and is intentionally omitted here.