fix(windows): wire CEF keyboard input routing on cold launch (#1528)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
sanil-23
2026-05-12 17:07:00 +05:30
committed by GitHub
co-authored by Claude Opus 4.7
parent 78d1f3d5fa
commit 7ce319362b
2 changed files with 453 additions and 11 deletions
+103
View File
@@ -835,6 +835,26 @@ fn show_main_window(app: &AppHandle<AppRuntime>) -> 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<AppRuntime> = 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<AppRuntime> = 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");
});
}
}
}
+350 -11
View File
@@ -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)"
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)"
if [[ -z "$msvc_env" ]]; then
echo "[run-dev-win] failed to capture MSVC env from vcvars64.bat" >&2
# `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_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:-<unset>}"
echo "[run-dev-win] WindowsSdkDir = ${WindowsSdkDir:-<unset>}"
echo "[run-dev-win] WindowsSDKVersion = ${WindowsSDKVersion:-<unset>}"
# 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<version>. 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 '<not found>')"
echo "[run-dev-win] node.exe on bash PATH: $(command -v node.exe 2>/dev/null || echo '<not found>')"
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 <target>/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/<version>/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.