Debug Windows on Parallels
The Windows product cannot be built or exercised on macOS directly. This Skill covers the host-to-guest workflow: what channel to use, which failures are environmental rather than code defects, and where Windows-side diagnostics live.
Only load this Skill when the task actually reaches into the VM. Editing
windows/ source and running verify-windows-boundaries.sh on macOS needs
nothing from here.
Reach the VM through prlctl, not SSH
prlctl is the only reliable channel. SSH into the guest requires an
interactive password and cannot be scripted.
prlctl list # names and run state
prlctl exec "<vm-name>" cmd /c "..."
prlctl exec "<vm-name>" powershell -Command "..."
prlctl exec defaults to NT AUTHORITY\\SYSTEM. That context does not have
the interactive user's PATH, Rustup home, Bun installation, Git config, or
mapped drives. For product builds and user-session diagnostics, use the
interactive Windows account instead:
prlctl exec "<vm-name>" --current-user cmd /c "..."
prlctl exec "<vm-name>" --current-user powershell -Command "..."
Verify the context before running a build:
prlctl exec "<vm-name>" --current-user cmd /c "whoami & where rustup & where bun"
Use the default SYSTEM form only for machine-level inspection or actions that
do not depend on the logged-in user's tools and configuration. The
--current-user form is required for Windows builds, Git behavior, and logs
produced by the active Lithe session.
prlctl exec runs as NT AUTHORITY\SYSTEM, not the logged-in user. Three
consequences follow, and all three look like "the tool is not installed":
- The user
PATHis invisible.bun,cargo,rustup, andnodeall appear missing. Invoke them by absolute path, or setPATHinside a.cmdwrapper. gitreportsdetected dubious ownershipbecause the checkout belongs to the interactive user. Pass-c safe.directory=<path>or configure it globally in the guest once.wingetis unavailable. Install guest tooling interactively instead.
PowerShell in the guest emits localized text that arrives as mojibake on the macOS side. Match on timestamps, severities, and English identifiers; do not try to read localized error prose.
Transfer files over the Parallels shared folder
The macOS home directory is mounted in the guest at \\psf\Home. Copy through
it in a single call:
prlctl exec "<vm-name>" powershell -Command "
Copy-Item '\\\\psf\\Home\\<path-under-home>' 'C:\<guest-path>' -Force
"
Use \\psf\Home, not \\Mac\Home; the latter is not resolvable from the
SYSTEM context.
Never chunk file contents through repeated prlctl exec calls (for example
base64 in a shell loop). Every call spawns a guest PowerShell process; a handful
of files is enough to exhaust guest resources, stall the host, and generate
large amounts of disk writes. If a shared folder is genuinely unavailable, stop
and ask rather than looping.
Build through a guest .cmd wrapper
Write a .cmd file in the guest that establishes the environment, then run it
detached with output redirected to a log:
@echo off
set "PATH=C:\Program Files\LLVM\bin;C:\Users\<user>\.cargo\bin;C:\Users\<user>\.bun\bin;%PATH%"
set "CARGO_HOME=C:\Users\<user>\.cargo"
set "RUSTUP_HOME=C:\Users\<user>\.rustup"
cd /d C:\<repo>
powershell -NoProfile -ExecutionPolicy Bypass -File ".\scripts\build-windows.ps1" -Configuration Debug -RustTarget aarch64-pc-windows-msvc
echo === exit: %ERRORLEVEL% ===
prlctl exec "<vm-name>" cmd /c "start /min cmd /c \"C:\\<build>.cmd > C:\\<build>.log 2>&1\""
Driving the build from cmd with cmd-level redirection is deliberate.
build-windows.ps1 sets $ErrorActionPreference = "Stop", and rustup and
bun write progress to stderr; under PowerShell redirection those become
terminating NativeCommandErrors and the build fails while the underlying
command succeeded. A build that dies immediately after a tool prints a normal
informational line is this, not a real failure — check the tool standalone
before treating it as a code defect.
Other environmental failures worth recognizing:
- Wrong target. On Apple Silicon the guest is ARM64, so pass
-RustTarget aarch64-pc-windows-msvc. The script's default is x86_64. - Script execution blocked. First run needs
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned -Forcein the guest. Access is deniedremoving the exe. The app is still running. Stop it first:Get-Process -Name '<product>' -ErrorAction SilentlyContinue | Stop-Process -Force.- Missing native toolchain. Some crates require
clang; the guest may have LLVM installed without it being on theSYSTEMPATH. AddC:\Program Files\LLVM\binto the wrapper'sPATH. - Shared crate edits not picked up. Cargo compares mtimes, and a file
written through the shared folder can keep an older timestamp. Touch it:
(Get-Item <path>).LastWriteTime = Get-Date. Confirm the intended crate actually recompiled by grepping the build log for itsCompilingline. - Debug builds omit bundled language servers.
build-windows.ps1stages them only for Release. Mirror them into the Debug profile when a Debug build must exercise language features:robocopy <target>\release\LanguageServers <target>\debug\LanguageServers /E /NFL /NDL /NJH /NP.
Read Windows-side diagnostics
Application logs live under the product's LOCALAPPDATA directory; the most
recently written file is the current session.
prlctl exec "<vm-name>" powershell -Command "
\$f = Get-ChildItem 'C:\Users\<user>\AppData\Local\<bundle-id>\logs' |
Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
Select-String -Path \$f -Pattern 'error' | Select-Object -Last 20 | ForEach-Object { \$_.Line }
"
Know what the log will and will not show before concluding a feature is broken.
Per windows/tauri/src-tauri/src/logging.rs:
Debug-level records are dropped unless diagnostic logging is enabled for the session (Settings → Logs). Enable it before reproducing, or the most useful records will not exist.- The sanitizer rewrites absolute paths to
<redacted_path>and redacts field values whose key looks credential-like. Keys containingtokenare caught by this, so some benign fields read as<redacted>. - Lines are truncated at 4 KB, which cuts the tail off long React component stacks.
- Only
Warnand above are flushed synchronously, so a hard crash can lose the last fewInforecords.
Frontend stack traces from a production build are minified. Inline source maps
do not fix this: V8 does not apply source maps when building
Error.stack — that is devtools-only behavior. Readable frames require devtools,
which needs the devtools feature on the tauri crate; a --debug build alone
does not enable it.
Language-server internals are not in the application log. JDTLS keeps its own
log at
<LOCALAPPDATA>\<bundle-id>\language-servers\jdtls\<hash>\.metadata\.log.
Maven import results, classpath resolution, and index read/write failures appear
only there. A ProjectRegistryRefreshJob finished time in single-digit
milliseconds means no build system was actually imported.
Match the diagnosis to the layer
Before changing product code, place the symptom:
- Guest environment — missing
PATH, ownership, execution policy, target triple, locked exe. Fix the harness, not the product. - Build staging — a resource the Debug profile never received. Check what
build-windows.ps1stages per configuration. - Durable language-server state — JDTLS keeps per-workspace state that it never invalidates itself. A workspace whose structure changed can keep resolving against a stale project model, which presents as navigation and highlighting working for some files and not others.
- Product defect — only after the three above are excluded.
State which layer the evidence supports. When a check could not run on the current machine, say so rather than implying it passed.
Search tools in this environment
grep is proxied and does not behave like GNU grep for absolute paths or \|
alternation. Prefer rg -n "a|b" with an unescaped pipe, or run separate
patterns.