mirror of
https://github.com/tinyhumansai/openhuman.git
synced 2026-07-27 21:08:00 +00:00
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:
co-authored by
Claude Opus 4.7
parent
78d1f3d5fa
commit
7ce319362b
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user