Repository navigation
Expand file tree
/
Copy pathrun-dev-win.sh
More file actions
714 lines (678 loc) · 32.9 KB
/
Copy pathrun-dev-win.sh
File metadata and controls
714 lines (678 loc) · 32.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
APP_DIR="$REPO_ROOT/app"
cd "$APP_DIR"
# Load .env first so project env vars are available, but before we compute
# Windows-specific paths so tailored values (CEF_PATH, PATH, etc.) are set
# after .env is applied and cannot be clobbered by it.
# 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
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
if [[ -z "${LOCALAPPDATA:-}" ]]; then
echo "[run-dev-win] LOCALAPPDATA is unset; cannot resolve the CEF cache directory." >&2
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, mkt
10BC8
emp) 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 /
# Windows SDK headers are reachable without launching the "x64 Native Tools
# Command Prompt for VS 2022" first. This is a no-op if the env is already
# loaded (cl.exe is on PATH). Otherwise we discover the latest VS install via
# vswhere, run `vcvars64.bat` inside cmd, and re-export the relevant variables
# back into this bash session.
#
# Without this, the Ninja generator fails to find cl.exe and CMake-driven
# native crates (libgit2-sys, libsqlite3-sys, etc.) error out at the C compilation step.
if ! command -v cl.exe >/dev/null 2>&1; then
vswhere_exe="/c/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
if [[ ! -x "$vswhere_exe" ]]; then
echo "[run-dev-win] vswhere.exe not found at $vswhere_exe" >&2
echo "[run-dev-win] install Visual Studio 2022 Build Tools with the 'Desktop development with C++' workload." >&2
exit 1
fi
vs_install_path="$("$vswhere_exe" -latest -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath || true)"
if [[ -z "$vs_install_path" ]]; then
echo "[run-dev-win] no VS install with MSVC C++ tools found via vswhere" >&2
exit 1
fi
vcvars_bat="${vs_install_path}\\VC\\Auxiliary\\Build\\vcvars64.bat"
echo "[run-dev-win] loading MSVC env from $vcvars_bat"
# Git Bash's MSYS layer mangles inner quotes when we invoke `cmd //c`
# directly (the literal backslash-quotes get passed through to cmd, which
# rejects the path). Workaround: write a small launcher .bat to a temp
# 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\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.
# `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)
# cmd's PATH uses ; and Windows paths; convert each entry to bash form.
new_path=""
IFS=';' read -ra path_entries <<< "$value"
for entry in "${path_entries[@]}"; do
[[ -z "$entry" ]] && continue
unix_entry="$(cygpath -u "$entry" 2>/dev/null || printf '%s' "$entry")"
new_path="${new_path}${new_path:+:}${unix_entry}"
done
# 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.
export "$key=$value"
;;
VSCMD_*|VS[0-9]*COMNTOOLS|VCToolsInstallDir|VCToolsRedistDir|VCINSTALLDIR|VSINSTALLDIR|WindowsSdkDir|WindowsSDKVersion|UCRTVersion|UniversalCRTSdkDir|Platform)
export "$key=$value"
;;
esac
done <<< "$msvc_env"
if ! command -v cl.exe >/dev/null 2>&1; then
echo "[run-dev-win] MSVC env load failed — cl.exe still not on PATH" >&2
exit 1
fi
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|.*/||')"
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, the *-sys crates, 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
# it resolves `link.exe`, so it can still find Git's
# `C:\Program Files\Git\usr\bin\link.exe` (GNU coreutils symlink utility)
# first and produce `/usr/bin/link: extra operand '...rcgu.o'`. Setting
# `CARGO_TARGET_<TRIPLE>_LINKER` makes cargo pass `-C linker=<path>` to
# rustc directly, no PATH lookup involved.
#
# This block sits outside the bootstrap `if` so the pin still runs when
# the user launches from a shell that already has `cl.exe` on PATH (e.g.
# the "x64 Native Tools Command Prompt for VS 2022"). Without that, a
# ready-to-go MSVC shell would skip the linker pin and fall back to PATH
# resolution, where Git's coreutils `link.exe` can still win.
msvc_cl_dir="$(dirname "$(command -v cl.exe)")"
msvc_link_unix="$msvc_cl_dir/link.exe"
if [[ ! -x "$msvc_link_unix" ]]; then
echo "[run-dev-win] expected link.exe alongside cl.exe at $msvc_link_unix" >&2
exit 1
fi
msvc_link_win="$(cygpath -w "$msvc_link_unix")"
export CARGO_TARGET_X86_64_PC_WINDOWS_MSVC_LINKER="$msvc_link_win"
# Also push MSVC bin to the front of PATH so any other tool that bare-resolves
# `link.exe` (CMake-driven builds, etc.) hits MSVC's, not Git's.
export PATH="$msvc_cl_dir:$PATH"
echo "[run-dev-win] linker pinned: $msvc_link_win"
# Pin Ninja as the CMake generator end-to-end. The default on Windows would be
# the Visual Studio generator, which produces .sln/.vcxproj files; if anything
# downstream then invokes ninja (because CMAKE_MAKE_PROGRAM is set below),
# you get the "ninja: error: loading 'build.ninja'" mismatch.
export CMAKE_GENERATOR=Ninja
# CEF runtime lives under LOCALAPPDATA on Windows.
# ensure-tauri-cli.sh stages it here; fall back to a default if unset.
CEF_PATH="${CEF_PATH:-$(cygpath -u "$LOCALAPPDATA")/tauri-cef}"
export CEF_PATH
to_unix_path() {
if [[ -z "${1:-}" ]]; then
return 1
fi
cygpath -u "$1"
}
# Resolve a WinGet-installed executable.
# Usage: find_winget_exe <package-glob> <exe-name>
# Prints the full path to the exe and returns 0, or returns 1 if not found.
find_winget_exe() {
local pkg_glob="$1"
local exe_name="$2"
local local_appdata_unix
local_appdata_unix="$(to_unix_path "${LOCALAPPDATA:-}")" || return 1
local candidate
# Sort by version
10BC8
(lexicographic on directory name) and pick the newest.
candidate="$(ls -d "$local_appdata_unix"/Microsoft/WinGet/Packages/${pkg_glob}_* 2>/dev/null \
| sort -V | tail -n1 || true)"
if [[ -n "$candidate" && -x "$candidate/$exe_name" ]]; then
printf '%s\n' "$candidate/$exe_name"
return 0
fi
return 1
}
find_pnpm() {
if command -v pnpm >/dev/null 2>&1; then
command -v pnpm
return 0
fi
# 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() {
if command -v ninja >/dev/null 2>&1; then
command -v ninja
return 0
fi
# 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"
# `cargo tauri dev` runs its beforeDevCommand (`pnpm run dev`) via a native
# `cmd /S /C` that resolves bare `pnpm` off PATH. This script otherwise only
# ever calls pnpm by absolute path, so its dir was never on PATH and Tauri
# dies with "'pnpm' is not recognized". Prepend the resolved pnpm's dir — it
# ships pnpm.CMD alongside the bash shim, which cmd.exe uses.
# Split the dirname computation out of the export so a `dirname` failure
# surfaces with a non-zero exit (SC2155) instead of being swallowed by the
# enclosing `export`. `dirname` on a validated absolute path is reliable
# in practice, but the strict-mode posture is worth the extra line.
PNPM_DIR="$(dirname "$PNPM_EXE")"
# `dirname` returns `.` for a bare filename (e.g. if PNPM_EXE somehow
# resolved to just "pnpm" without a path component). Prepending `.` would
# inject the current working directory into PATH on a Windows dev machine
# — a privilege-escalation-flavoured surprise. Skip the prepend in that
# case (and on the also-degenerate empty result); the absolute-path call
# sites elsewhere in this script still work.
if [[ -n "$PNPM_DIR" && "$PNPM_DIR" != "." ]]; then
export PATH="$PNPM_DIR:$PATH"
echo "[run-dev-win] pnpm dir prepended to PATH: $PNPM_DIR"
else
echo "[run-dev-win] pnpm dir not prepended to PATH (PNPM_EXE has no path component: $PNPM_EXE)"
fi
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
echo "[run-dev-win] ninja not found. Install ninja and retry."
exit 1
fi
export CMAKE_MAKE_PROGRAM="$NINJA_EXE"
CEF_RUNTIME_PATH="$(ls -d "$CEF_PATH"/*/cef_windows_x86_64 2>/dev/null | sort -Vr | head -n1 || true)"
if [[ -n "$CEF_RUNTIME_PATH" ]]; then
export CEF_RUNTIME_PATH
fi
PATH_PREFIX="/c/Program Files/CMake/bin:$(dirname "$NINJA_EXE")"
if [[ -n "${CEF_RUNTIME_PATH:-}" ]]; then
PATH_PREFIX="$PATH_PREFIX:$CEF_RUNTIME_PATH"
fi
# Ensure the workspace node_modules/.bin is on PATH so pnpm's child
# spawns (e.g. `pnpm tauri dev` → `tauri.CMD`) can resolve the shims.
# Pnpm normally prepends `./node_modules/.bin` for script execution, but
# when the script body is `tauri "dev"` and the child shell is cmd.exe
# under the long bash→cmd→bash chain, the relative entry sometimes
# resolves against the wrong cwd and tauri.CMD is not found.
PATH_PREFIX="$APP_DIR/node_modules/.bin:$PATH_PREFIX"
# Ensure pnpm itself stays on PATH for cargo-tauri's beforeDevCommand
# (`pnpm run dev` → vite). When run-dev-win.sh restores the Windows PATH
# via cmd.exe %PATH%, some setups (WinGet-installed pnpm with no
# AppData/Roaming/npm entry) don't surface a pnpm dir consistently
# downstream. Prepend the resolved $PNPM_EXE dir to guarantee it.
if [[ -n "${PNPM_EXE:-}" ]]; then
PNPM_EXE_DIR="$(dirname "$PNPM_EXE")"
PATH_PREFIX="$PNPM_EXE_DIR:$PATH_PREFIX"
fi
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
# The dev OpenHuman.exe is produced by the *Tauri shell* crate
# (app/src-tauri/Cargo.toml), not the root core crate. When
# CARGO_TARGET_DIR is set both workspaces share it; when unset, the
# Tauri shell builds into app/src-tauri/target while the root crate
# builds into target/. Stage CEF next to where OpenHuman.exe will
# actually live so Windows' DLL search order finds libcef.dll
# regardless of how the exe is launched (terminal, OAuth deep-link,
# double-click, etc).
if [[ -n "${CARGO_TARGET_DIR:-}" ]]; then
CEF_STAGE_DIR="$(to_unix_path "$CARGO_TARGET_DIR" 2>/dev/null || printf '%s' "$CARGO_TARGET_DIR")/debug"
else
CEF_STAGE_DIR="$REPO_ROOT/app/src-tauri/target/debug"
fi
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.
#
# OPENHUMAN_DEV_PORT lets parallel worktree dev sessions avoid the
# hardcoded 1420 collision. Vite reads the same env var directly; the
# tauri-cli inline override patches tauri.conf.json's `devUrl` so the
# shell connects to the right Vite instance.
# Validate OPENHUMAN_DEV_PORT before interpolating into JSON — a stray
# space, alphabetic char, or out-of-range value would produce an invalid
# devUrl and tauri would refuse to start (or worse, drift from Vite's
# own numeric fallback). Trim whitespace, require pure digits in
# [1, 65535], fall back to 1420 with a warning otherwise.
raw_dev_port="${OPENHUMAN_DEV_PORT:-1420}"
raw_dev_port="${raw_dev_port//[[:space:]]/}"
if [[ "$raw_dev_port" =~ ^[0-9]+$ ]] && (( raw_dev_port >= 1 && raw_dev_port <= 65535 )); then
DEV_PORT="$raw_dev_port"
else
echo "[run-dev-win] WARNING: invalid OPENHUMAN_DEV_PORT='$raw_dev_port'; falling back to 1420" >&2
DEV_PORT=1420
fi
# Invoke cargo-tauri directly rather than going through `pnpm tauri dev`.
#
# The pnpm chain (pnpm.exe → cmd.exe → tauri.CMD) is fragile on Windows:
# whether `tauri.CMD` is resolvable in the spawned cmd subprocess depends
# on which pnpm shim was picked up by `find_pnpm`. The self-managing
# `~/AppData/Local/pnpm/.tools/.../pnpm` variant auto-prepends
# `node_modules/.bin` for script children; the WinGet-installed
# `pnpm.exe` does not, so the script body `tauri "dev"` then fails with
# "'tauri' is not recognized" inside cmd.
#
# `ensure-tauri-cli.sh` already installed the vendored CEF-aware
# cargo-tauri at `$REPO_ROOT/.cache/cargo-install/bin/cargo-tauri.exe`,
# so we can invoke that binary directly and skip the wrapper layer.
#
# Historical note: a previous version of this script ran a PATH
# deduplication loop (collapsing repeated entries that MSYS→Windows
# conversion stacked during vcvars / Git-Bash re-runs / pnpm layering).
# That loop was needed because the overflowing env block left child
# processes with an EMPTY PATH — even `where.exe` was gone, causing
# "'pnpm' is not recognized". Direct cargo-tauri.exe invocation with
# absolute paths in the .bat wrapper makes the env block size irrelevant:
# beforeDevCommand no longer needs PATH at all.
CARGO_TAURI_EXE="$REPO_ROOT/.cache/cargo-install/bin/cargo-tauri.exe"
if [[ ! -x "$CARGO_TAURI_EXE" ]]; then
echo "[run-dev-win] cargo-tauri.exe not found at $CARGO_TAURI_EXE" >&2
echo "[run-dev-win] tauri:ensure should have installed it. Aborting." >&2
exit 1
fi
# Build a tauri.conf.json `-c` JSON merge that:
# - pins `beforeDevCommand` to the absolute pnpm path so cargo-tauri's
# cmd.exe child can find pnpm regardless of any PATH stripping
# between bash → cargo-tauri → cmd. The default in tauri.conf.json
# is `"pnpm run dev"` (bare name) which depends on PATH.
# - overrides `devUrl` when OPENHUMAN_DEV_PORT is non-default.
# Point beforeDevCommand at vite via a wrapper batch file in a
# space-free temp directory.
#
# Why a wrapper instead of the absolute path directly:
# cargo-tauri runs beforeDevCommand as `cmd.exe /S /C <string>`. Rust's
# argv-to-cmd argument escaping strips literal double-quotes from the
# string, so if our `<string>` is `"E:\Office Files\…\vite.CMD"`,
# cmd ends up parsing `E:\Office` as the program name and the rest as
# arguments — "'E:\Office' is not recognized". 8.3 short-name fallback
# also fails when 8dot3name is disabled on the drive (as it is on this
# workspace's E: drive).
#
# The fix is to call the spacey path from INSIDE a .bat file, where we
# can quote it however we want without involving cargo-tauri's outer
# escaping. The wrapper lives under %TEMP% (which is normally
# space-free) so its own path doesn't need quoting either.
VITE_JS_UNIX="$APP_DIR/node_modules/vite/bin/vite.js"
if [[ ! -f "$VITE_JS_UNIX" ]]; then
echo "[run-dev-win] vite entry not found at $VITE_JS_UNIX" >&2
echo "[run-dev-win] Did 'pnpm install' run? Aborting." >&2
exit 1
fi
VITE_JS_WIN="$(cygpath -w "$VITE_JS_UNIX" 2>/dev/null || printf '%s' "$VITE_JS_UNIX")"
# Resolve node.exe absolute path so the wrapper doesn't depend on
# whatever PATH cargo-tauri hands to its cmd child.
NODE_EXE_UNIX="$(command -v node.exe 2>/dev/null || command -v node 2>/dev/null)"
if [[ -z "$NODE_EXE_UNIX" || ! -f "$NODE_EXE_UNIX" ]]; then
echo "[run-dev-win] node.exe not findable on bash PATH at wrapper-build time" >&2
exit 1
fi
NODE_EXE_WIN="$(cygpath -w "$NODE_EXE_UNIX" 2>/dev/null || printf '%s' "$NODE_EXE_UNIX")"
WRAPPER_DIR_UNIX="$(cygpath -u "${TEMP:-${TMP:-/tmp}}" 2>/dev/null || echo /tmp)/openhuman-dev"
mkdir -p "$WRAPPER_DIR_UNIX"
VITE_WRAPPER_UNIX="$WRAPPER_DIR_UNIX/run-vite.bat"
# Invoke node.exe with vite's JS entry directly. The vite.CMD shim
# falls back to bare `node` when its sibling doesn't have node.exe,
# which fails inside cargo-tauri's cmd child (no node on PATH).
{
printf '@echo off\r\n'
printf '"%s" "%s" %%*\r\n' "$NODE_EXE_WIN" "$VITE_JS_WIN"
} > "$VITE_WRAPPER_UNIX"
VITE_WRAPPER_WIN="$(cygpath -w "$VITE_WRAPPER_UNIX" 2>/dev/null || printf '%s' "$VITE_WRAPPER_UNIX")"
if [[ "$VITE_WRAPPER_WIN" == *" "* ]]; then
echo "[run-dev-win] wrapper path contains spaces: $VITE_WRAPPER_WIN" >&2
echo "[run-dev-win] set TEMP/TMP to a space-free path (e.g. C:\\Temp) and retry." >&2
exit 1
fi
echo "[run-dev-win] vite wrapper at: $VITE_WRAPPER_WIN"
BEFORE_DEV_CMD="${VITE_WRAPPER_WIN//\\/\\\\}"
CONFIG_OVERRIDE="{\"build\":{\"beforeDevCommand\":\"$BEFORE_DEV_CMD\""
if (( DEV_PORT != 1420 )); then
echo "[run-dev-win] OPENHUMAN_DEV_PORT=$DEV_PORT — overriding tauri devUrl"
CONFIG_OVERRIDE+=",\"devUrl\":\"http://localhost:$DEV_PORT\""
fi
CONFIG_OVERRIDE+="}}"
echo "[run-dev-win] tauri config override: $CONFIG_OVERRIDE"
"$CARGO_TAURI_EXE" dev -c "$CONFIG_OVERRIDE"