Skip to content

Windows Build Environment & Repo Relocation

When to use this page. You are building the Expo/React Native app (apps/mobile) on Windows and hit one of: a native build failing with Filename longer than 260 characters, Metro crashing on startup, a C: drive at 0 bytes, or you need to relocate the repo to another drive without losing Claude Code memory. This is the finalized, verified reference — follow it instead of re-deriving the fix. It records the 2026-07-21 migration of the repo from a deep OneDrive path on C: to D:\WorkSpace\DevArea\qrsetu. Tracked in Track B.

TL;DR — the golden rules

  1. Never keep the repo in a deep or OneDrive-synced path on Windows. React Native's native (CMake/ninja) build generates paths that blow past Windows' 260-character MAX_PATH limit. Keep the project base path short: D:\WorkSpace\DevArea\qrsetu (27 chars), not C:\Users\<u>\OneDrive\WorkSpace\DevArea\qrsetu (48+).
  2. Keep dev caches and build temp off C:. Gradle/npm/Playwright caches live on D:\DevCache; the Android SDK and all projects live on D:; TEMP/TMP point to D:\DevCache\temp so Metro/Hermes/Gradle-transforms/ aapt2/clang write their (multi-GB) build scratch to D:, not C:. C: only ever holds regenerable browser/editor cache (wiped on demand). Why TEMP/TMP matter specifically: Windows' page file is C:\pagefile.sys (system- managed), so a full C: caps how large it can grow → caps the commit limit → release builds OOM (see Memory / commit limit). Build scratch on C: fills C: and starves the page file — a double hit. (2026-07-24: TEMP/TMP were the one relocation gap — they had defaulted to C: since setup.) The guarded build script now enforces this regardless of the shell's inherited env — use it.
  3. Relocating is half the job — artifacts must also be pruned. Growth is monotonic, so "not on C:" only moves the wall to D:. npm run clean:dev (dry-run) / clean:dev:apply enforce a published retention policy, run automatically, and npm run check:disk verifies both the layout and that the automation is installed. Full standard: development never touches C:.
  4. Metro watches packages/ + node_modules, never the whole monorepo root — watching the root pulls in legacy/, documentation/, supabase/ and crashes Metro's file watcher.
  5. Maestro runs from WSL Ubuntu against the Windows emulator (WSL2 sees it directly); dev-client flows use extendedWaitUntil, not bare assertVisible.

The root problem: Windows MAX_PATH (260 chars)

RN native builds compile per-package C++ under node_modules/<pkg>/android/.cxx/... and read prefab headers out of the Gradle transforms cache. Header names like RuntimeSchedulerIntersectionObserverDelegate.h nested under the Gradle-home transform path exceed 260 chars once the project base path is long. The build dies with:

> Task :react-native-screens:buildCMakeDebug[x86_64] FAILED
ninja: error: Stat(D:/…/GradleHome/caches/…/transformed/react-android-…/prefab/modules/reactnative/
include/react/renderer/runtimescheduler/RuntimeSchedulerIntersectionObserverDelegate.h):
Filename longer than 260 characters

Two independent levers shorten these paths; use both:

LeverEffect
Short project base path (D:\WorkSpace\DevArea\qrsetu)Shortens the node_modules/<pkg>/android/.cxx/... side
Short GRADLE_USER_HOME (D:\DevCache\gradle, not a long custom dir)Shortens the prefab-header transforms side (≈238 chars, under 260)

Gotcha — stale .cxx bakes in old paths. A package's generated ninja files cache the absolute Gradle-home path from the build that created them. After changing GRADLE_USER_HOME (or moving the repo), delete stale native build dirs or the old long path resurfaces for just that package:

powershell
Get-ChildItem node_modules,apps/mobile/android -Recurse -Directory -Force |
  Where-Object Name -eq ".cxx" | Remove-Item -Recurse -Force
Remove-Item apps/mobile/android/{build,app/build,.gradle} -Recurse -Force

Also set git config core.longpaths true (helps git; does not fix ninja on its own).

Machine layout (this dev box, 2026-07-21)

User environment variables (set once; survive reboots):

VariableValue
GRADLE_USER_HOMED:\DevCache\gradle
PLAYWRIGHT_BROWSERS_PATHD:\DevCache\playwright
npm cache (npm config get cache)D:\DevCache\npm
TEMPD:\DevCache\temp — build scratch (Metro/Hermes/aapt2/clang) off C:
TMPD:\DevCache\temp — same value as TEMP
ANDROID_HOMED:\WorkSpace\DevArea\AppBuilding\AndroidSdk
JAVA_HOMEJDK 17 (Adoptium)

Set TEMP/TMP (no admin): New-Item -ItemType Directory -Force D:\DevCache\temp; setx TEMP D:\DevCache\temp; setx TMP D:\DevCache\temp. setx persists to the user registry (survives reboots) but does not update already-running processes — open a new terminal (or reboot) for it to take effect. qrsetu-dev-relocate.ps1 must set these two alongside the others.

Reusable scripts (kept in ~\Downloads): qrsetu-disk-cleanup.ps1 (deletes only regenerable caches — browser/VS Code/Claude vm_bundles/npm/temp; never code, settings, or memory) and qrsetu-dev-relocate.ps1 (creates D:\WorkSpace + D:\DevCache and sets the env vars above). Run cleanup with editors/browsers closed.

Repo relocation runbook (C: OneDrive → D:)

This is the procedure that was executed and verified. It is clone-based (not a folder copy) so no stale absolute paths or OneDrive placeholder files travel along. Budget one native rebuild (~20 min) at the end.

  1. Commit + push everything. A clone is only lossless once the working tree is on GitHub. Check git status --short is clean (or committed).
  2. Clone to the short path. git clone <url> D:\WorkSpace\DevArea\qrsetu then git checkout <working-branch>. Confirm git log --oneline -1 matches the commit you pushed. (If the destination already holds unrelated data — e.g. an old bkp/ — move it aside first; never clobber.)
  3. Carry the gitignored files that don't travel via git — copy from the old copy: supabase/.env.dev, supabase/.env.prod, legacy/.env.test (and any local .env, never .env.example).
  4. npm install at the new root (npm cache on D: makes this fast). android/ is gitignored and regenerates via prebuild, so nothing else to carry.
  5. Migrate Claude Code memory (see next section) — the one non-obvious step.
  6. Reopen VS Code at D:\WorkSpace\DevArea\qrsetu. This starts a fresh Claude Code session keyed to the new path; with memory migrated it resumes with full history + memory intact.
  7. Rebuild (cd apps/mobile && npx expo run:android) and run the Maestro smoke. Once green, delete the old OneDrive copy.

Migrating Claude Code memory (the non-obvious part)

Claude Code stores each project's memory + conversation transcripts under a folder named after the project's absolute path, at C:\Users\<user>\.claude\projects\<slug>\. The slug rule: lowercase the drive letter, and replace every : and \ with -.

PathSlug folder
C:\Users\bnlah\OneDrive\WorkSpace\DevArea\qrsetuc--Users-bnlah-OneDrive-WorkSpace-DevArea-qrsetu
D:\WorkSpace\DevArea\qrsetud--WorkSpace-DevArea-qrsetu

Copy (don't move — keep the old as a safety net) the whole slug folder to the new name before reopening:

powershell
$base = "$env:USERPROFILE\.claude\projects"
Copy-Item "$base\c--Users-bnlah-OneDrive-WorkSpace-DevArea-qrsetu" `
          "$base\d--WorkSpace-DevArea-qrsetu" -Recurse

This carries memory\ (MEMORY.md + all memory files) and the conversation *.jsonl. Verify the new MEMORY.md has content after reopening. (The cleanup script never touches .claude — memory is only ever moved deliberately, here.)

Metro on a monorepo (watch scope)

Watching the whole monorepo root crashes Metro's file watcher when it indexes the legacy/ reference SPA (worsened by OneDrive file churn):

Error: TreeFS: Failed to make parent directory entry for ..\..\legacy\src\components\…\PublicImageBlock.jsx

Fix in apps/mobile/metro.config.js — watch only what the app consumes:

js
config.watchFolders = [
  path.resolve(workspaceRoot, 'packages'),
  path.resolve(workspaceRoot, 'node_modules'),
];

Maestro from WSL

Maestro is installed in WSL Ubuntu (~/.maestro/bin), not Windows. WSL2 sees the Windows emulator (emulator-5554) over adb directly — no bridge configuration needed (adb devices in WSL already lists it).

bash
# from Windows: make Metro reachable from the device (dev-client builds)
adb reverse tcp:8081 tcp:8081

# from WSL: run the flow (repo is under /mnt/<drive>/…)
wsl -d Ubuntu -- bash -lc \
  'cd /mnt/d/WorkSpace/DevArea/qrsetu/apps/mobile && \
   export PATH="$PATH:$HOME/.maestro/bin" && \
   maestro test .maestro/smoke.yaml'

Dev-client race. A dev-client cold launchApp fetches the JS bundle from Metro (a few seconds) before it renders. Flows must wait for content rather than assert instantly, or the assertion races the load:

yaml
- launchApp
- extendedWaitUntil:
    visible: "QRSETU"
    timeout: 30000

A release build embeds the bundle and renders immediately, so it wouldn't need the wait — but extendedWaitUntil is correct for both and is what apps/mobile/.maestro/smoke.yaml uses.

Fresh-install seeding. On a brand-new dev-client install the first launchApp lands on the "development servers" launcher and won't auto-connect (no remembered URL), so even extendedWaitUntil times out. Seed it once — open the dev-client deep link (adb shell am start -a android.intent.action.VIEW -d "qrsetu://expo-development-client/?url=http://localhost:8081") so the client connects and remembers the URL, or simply run the flow twice. After the URL is remembered, every launchApp reconnects on its own.

Memory / commit limit (local release builds)

Local release builds (assembleRelease) fail on this 16 GB box with an out-of-memory abort — either the Gradle JVM (hs_err_pid*.log, "daemon disappeared") or, more often, node/Metro during the cold JS bundle (createBundleReleaseJsAndAssetsProcess 'command 'cmd'' finished with non-zero exit value -1073740791/ -2147483645, i.e. 0xC0000409 / 0x80000003 — V8 aborting because it can't get memory). This is commit-limit exhaustion, not physical-RAM: commit limit = RAM + page file.

The mechanism (and why C: matters): the page file is C:\pagefile.sys, system-managed — it only grows while C: has free disk. With C: near-full the commit limit is pinned (~33.5 GB here) and ~30 GB is already held by the OS + editors + browsers, leaving too little for a cold RN release bundle. Metro compounds it by spawning one worker per CPU core, each reserving a V8 heap.

Fixes, in order:

  1. Keep build scratch off C:TEMP/TMP on D:\DevCache\temp (see the env table). Frees C:, letting the page file grow, and stops builds filling C:.
  2. Cap Metro workersmetro.config.js honors METRO_MAX_WORKERS (unset → Metro default). Build with METRO_MAX_WORKERS=1 (or 2) so the bundle runs in one/two node processes.
  3. Cap native parallelism — single ABI + serialized Gradle: -PreactNativeArchitectures=arm64-v8a --no-daemon --max-workers=1 CMAKE_BUILD_PARALLEL_LEVEL=1.
  4. Durable fix (needs admin + reboot): put a page file on D:. D: has the free space C: lacks. System → Advanced system settings → Performance → Advanced → Virtual memory → uncheck "automatically manage" → set D: to system-managed (or a fixed 16–24 GB) → reboot. This lifts the commit limit off C:'s free-space hostage entirely. Alternatively move release builds to EAS/CI (cloud, unconstrained) — the recommended long-term path.

Full local release-APK recipe (arm64, sideloadable, debug-signed):

bash
# free memory first: kill stray gradle/metro (java/node < ~15 min old), then:
cd apps/mobile/android
TEMP='D:\DevCache\temp' TMP='D:\DevCache\temp' METRO_MAX_WORKERS=1 CMAKE_BUILD_PARALLEL_LEVEL=1 \
  ./gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon --max-workers=1
# → android/app/build/outputs/apk/release/app-release.apk

Never pipe gradlew to tail/head — a pipeline's exit code is the last command's, so a failed build reports "success". Redirect to a file or read the exit code directly, and confirm the APK's timestamp is current.

Guarded build script — the only sanctioned local build path

The recipe above is fragile: it depends on the operator's shell having the right env, and setx does not update already-running shells — so a terminal (or an automation/agent shell) opened before the relocation still inherits TEMP/TMP on C: and silently reintroduces the OOM regression. That is exactly how the 2026-07-24 breach recurred.

apps/mobile/scripts/build-android.mjs (run via npm run build:android / build:android:clean) removes the foot-gun by owning the environment instead of trusting the shell:

  1. Refuses to build with scratch or the Gradle cache on the system drive. It forces the child build's TEMP/TMP/TMPDIRD:\DevCache\temp and GRADLE_USER_HOME → a non-system drive regardless of what the shell inherited (creating the dirs if missing), and hard-fails only if no non-system drive is available. Override the scratch target with QRSETU_BUILD_TMP. A build can no longer leak to C:, even from a stale shell.
  2. Caps memory-parallelism — sets METRO_MAX_WORKERS=1 + CMAKE_BUILD_PARALLEL_LEVEL=1 for the child, so the cold release bundle can't overrun the commit limit.
  3. Self-maintains disk — before the build it removes the previous APK (so a failed build can't leave a stale one masquerading as success) and prunes Metro/Haste/Gradle scratch older than 24 h (crashed-build residue; never the warm cache, which would force an expensive cold bundle); it prunes again after the build.
  4. Runs the unchanged gradlew assembleRelease -PreactNativeArchitectures=arm64-v8a --no-daemon --max-workers=1 (never piped — real exit code), then verifies the APK's mtime is newer than the build start before declaring success, and prints path + size.
bash
cd apps/mobile
npm run build:android          # guarded release build (arm64)
npm run build:android:clean    # also wipes stale .cxx + android/{build,app/build,.gradle} first

This script — not the raw gradlew line — is the sanctioned local build path going forward. The raw recipe above is retained only to explain the mechanism the script automates. expo-build-properties was evaluated for pinning Gradle memory/ABI durably across prebuilds but not adopted: it cannot set reactNativeArchitectures (the ABI cap lives on the script's -P flag), and the current gradle.properties heap default already matches Expo's, so a new build-time dependency would add surface without moving the OOM needle.

Disk hygiene — npm run check:disk

Relocating caches by environment variable was necessary but incomplete, and nothing verified it. On 2026-07-27 C: was back to 4.2 GB free — yet nothing had regressed: TEMP/TMP, GRADLE_USER_HOME, ANDROID_HOME and PLAYWRIGHT_BROWSERS_PATH were all still on D:, and the Android AVDs were on D: behind the .android junction. Three different things had happened instead (QRS-204):

  1. Docker Desktop's WSL2 disk — 15.1 GB, and structurally out of reach of the whole strategy. Every other relocation is an env var; Docker's data folder is a GUI setting, so an env-var strategy has a blind spot shaped exactly like Docker. Compounding it, a .vhdx never shrinks: docker system prune frees space inside the VM while the host file stays at its high-water mark. It only ever grows.
  2. Caches never in scope — npm's cache (npm_config_cache was unset), tool runtimes under ~/.cache.
  3. The guard was bypassed. An agent ran gradlew.bat assembleRelease directly instead of npm run build:android, which is precisely the stale-shell case the guarded script exists to prevent. A guard that can be walked around is a convention, not a control — and the walk-around is now demonstrated, not hypothetical.

Measurement trap, worth knowing before you audit this yourself. Get-ChildItem -Recurse follows junctions. The first pass billed ~8 GB of D:\Android content to C: because .android and AppData\Local\Android are junctions to D:. Resolve the target (dir /a:l, or fs.realpathSync) before attributing size to a drive; the hygiene script skips reparse points for this reason.

npm run check:disk (tools/check-disk-hygiene.js) asserts the invariants instead of trusting the agreement:

  • every relocated env var resolves — through junctions — off the system drive, and flags any that are unset (an unset var silently falls back to a C: default, which is how npm's cache got there);
  • the repo itself is on the work drive (the standard covers source, not only caches);
  • both drives are above a 15 GB floor (see the next section for why the work drive is not exempt);
  • the scheduled sweep is actually registered — an uninstalled automation looks identical to a working one until a drive fills;
  • the caches no env var can move (Docker, npm, .expo) are printed with their sizes and their specific fix, so they stay visible rather than creeping back.

It runs standalone and is also called (advisory, non-fatal) from the guarded build's preflight, so a doomed build fails in the first second with a clear message rather than ten minutes in with 0xC0000409.

On macOS/Linux it reports and exits 0 — the Mac builds iOS from this same repo and has one volume, so there is no rule to apply and inventing one would just add a false gate.

The standard: development never touches the system drive

Non-negotiable (QRS-205, codified in CLAUDE.md). No source, build artifacts, caches, temp/scratch, dependencies, SDKs, VM disks or generated output on C:. D: is the dedicated development volume. C: holds the OS and regenerable browser/editor cache, nothing else.

ConcernLocation
Source / projectsD:\WorkSpace\DevArea\<project> (short path — MAX_PATH)
Shared tool cachesD:\DevCache\{gradle,npm,playwright,temp}
Android SDK + AVDsD:\WorkSpace\DevArea\AppBuilding\AndroidSdk (+ the .android junction)
Docker data diskD:\DevCache\DockerDesktopWSL (GUI setting, not an env var)
Cleanup logD:\DevCache\logs\dev-cleanup.log

Relocation alone is not a solution — it moves the wall

This is the correction QRS-205 makes to QRS-204. Relocating growth off C: is necessary and it worked; what it does not do is bound the growth. Dev artifacts are monotonic by nature — every release build writes a fresh ~49 MB APK, every OOM-killed Gradle daemon leaves a multi-MB replay_pid*.log, every Metro run seeds another scratch directory, and Gradle's build cache has no bound at all. Within days of the relocation the measured picture was:

DriveFree
C:20.2 GBfixed — Docker moved off it
D:21.8 GBnow the tighter drive, and it carries D:\WorkSpace (41.4 GB) + D:\DevCache (11.7 GB) + D:\Android (8.1 GB)

A standard that only says "not C:" therefore reaches the same wall on a different letter. The floor and the retention policy below apply to both drives for that reason.

Retention policy

tools/clean-dev-artifacts.js is the policy engine. Dry-run is the default — a destructive tool whose default is destruction gets run by accident exactly once.

bash
npm run clean:dev          # report what WOULD be freed (safe, default)
npm run clean:dev:apply    # delete it
npm run clean:dev:deep     # also the expensive-to-rebuild tier

Safe tier — runs unattended, safe mid-session:

ClassKeepWhy it is safe
Build scratch (metro-*, haste-map-*, expo-*, …)3 daysRegenerated by the next bundle
JVM crash dumps (hs_err_pid*, replay_pid*)0 daysDiagnostic value expires with the session
Superseded APK/AAB outputsnewest per variant, then 7 daysOnly the newest is ever sideloaded
Playwright test-results / reports7 daysPer-run; only the last is ever opened
Expo web/metro cache14 daysPure cache. apps/mobile/dist is never touched — the web preview serves it
Gradle daemon logs7 daysOne per daemon, forever, in a directory nothing prunes
Claude Code scratchpads14 daysSession temp only. Never ~/.claude (memory + transcripts)
Xcode DerivedData (macOS)14 daysThe Mac's equivalent runaway

Deep tier (--deep) — correct but costs a rebuild, so it is not automatic:

ClassKeepCost of removing
Gradle build-cache entries (1400+ here)30 daysOne slower build
Superseded Gradle distributions (wrapper/dists)60 daysRe-download on wrapper change
Native build dirs (android/{build,app/build,.gradle}, .cxx)21 days~20 min cold rebuild

Two rules that are load-bearing, not incidental. (1) The sweeper never follows reparse points.android and AppData\Local\Android are junctions to D:, and a recursive delete that follows them destroys the target, not the link. (2) It deliberately does not age out caches/<version> directories: a directory's mtime does not update when Gradle writes into its subdirectories, so an age test there can flag the version currently in use and force a multi-GB re-resolve. wrapper/dists is unambiguous; the version caches are not.

Automation — three triggers, escalation on pressure

bash
npm run setup:disk-automation                                    # install (no admin)
powershell -File tools/setup-disk-automation.ps1 -Remove         # uninstall
  1. Scheduled task QRSetu-DevDiskCleanup — daily 02:00 and 15 min after logon (so a machine that is off at 02:00 still gets swept), StartWhenAvailable for catch-up. Runs as the current user with no administrator rights, deliberately: an automation that needs an elevated prompt gets skipped once and then forever.
  2. After every guarded Android build — a build is the moment artifacts are created, so it is the moment worth sweeping at. Best-effort; it never fails the build.
  3. Escalation on pressure, not on a calendar (--auto-escalate, used by the scheduled task) — the deep tier runs only when a drive is below the 15 GB floor. A fixed weekly deep sweep either throws away warm caches for nothing or misses the week the drive actually fills; tying it to measured free space makes the schedule self-adjusting.

npm run check:disk verifies the task is registered, so a machine where the automation was never installed — or was removed — fails the check rather than looking healthy.

Gotcha for anyone editing tools/setup-disk-automation.ps1: keep it ASCII-only. Windows PowerShell 5.1 reads a BOM-less UTF-8 .ps1 as CP1252, and an em dash (E2 80 94) decodes with byte 0x94 = U+201D, a smart closing quote — which PS accepts as a string delimiter. One em dash in a comment produced The string is missing the terminator pointing at a line 50 lines away.

Troubleshooting quick reference

SymptomCauseFix
ninja: … Filename longer than 260 charactersProject and/or Gradle-home path too deepRepo on short D: path + GRADLE_USER_HOME=D:\DevCache\gradle; delete stale .cxx; rebuild
Same error for one package onlyThat package's .cxx baked an old long Gradle-homeDelete all .cxx + android/{build,app/build,.gradle}; rebuild
TreeFS: Failed to make parent directory entry for …legacy…Metro watching the whole monorepo rootNarrow watchFolders to packages + node_modules
C: at ~0 bytes; tooling can't write temp filesDev caches accumulated on C:Run qrsetu-disk-cleanup.ps1; then qrsetu-dev-relocate.ps1 to move caches to D:
D: filling up even though nothing is on C:Relocation bounds where artifacts go, not how manynpm run clean:devclean:dev:applyclean:dev:deep; see retention
Disk fills again weeks after a cleanupThe scheduled sweep was never installed (or was removed)npm run setup:disk-automation; npm run check:disk now fails when it is missing
check:disk says an env var is unset that you already setx-edsetx does not update already-running processesOpen a new terminal (or reboot) — the value is in the user registry
Editing setup-disk-automation.ps1The string is missing the terminatorA non-ASCII char (em dash) in a BOM-less UTF-8 .ps1 decodes as a smart quote under CP1252Keep that file ASCII-only
C: fills during builds even though caches are on D:TEMP/TMP still default to C:\Users\…\AppData\Local\Temp — Metro/Hermes/Gradle scratch lands on C:Set TEMP/TMPD:\DevCache\temp (env table); open a new terminal
Release build OOMs: hs_err_pid*.log / bundle exit value -1073740791/-2147483645 (0xC0000409)Commit-limit exhaustion — C:-only page file capped by a full C:, worsened by Metro's per-core workersTEMP/TMP on D: + METRO_MAX_WORKERS=1 + serialized single-ABI build; durable = page file on D: (admin+reboot) or EAS/CI — see Memory / commit limit
Build leaked to C: again despite the env fixA shell (or agent) opened before setx still inherits TEMP/TMP=C:Build via npm run build:android — the guarded script forces scratch onto D: regardless of the inherited shell env
gradlew build "succeeded" but the APK is stale/old-datedThe command was piped (`tail`), so the exit code was the pipe's, not Gradle's
APK installs on the emulator then dies instantly: SoLoaderDSONotFoundError: couldn't find DSO to load: libreactnative.soABI mismatch, not a code fault. The shipped local build is arm64-only (commit limit + size budget), and the standard emulator is x86_64 — the log shows Native lib dir: …/lib/arm64 beside base.apk!/lib/x86_64npm run build:android:emulator (= --abi=x86_64). Use a physical arm64 device for anything you intend to ship; the x86_64 APK is a verification artifact only
expo run:android says Port 8081 is being used and skips the dev serverAn orphaned Metro from a prior sessionKill the node PID on 8081 (Get-NetTCPConnection -LocalPort 8081), start Metro from the current repo
Maestro assertVisible "QRSETU" fails but the screen shows QRSETU seconds laterDev-client bundle-load raceUse extendedWaitUntil with a 30s timeout
Memory/history "empty" after moving the repoNew project path → new .claude/projects slugCopy the old slug folder to the new slug name (see mapping above)

Replicating on a new machine or new project

  • New project: create it under D:\WorkSpace\DevArea\<name> (short path, off OneDrive) from day one. Caches are already shared via the D:\DevCache env vars — nothing per-project to set.
  • New machine: install the toolchain (see prerequisites), run qrsetu-dev-relocate.ps1 to establish D:\DevCache + env vars, put the Android SDK on D:, then clone projects under D:\WorkSpace\DevArea.