Reproducible guide for getting from a fresh clone to a working M1+ dev environment. Goal: under an hour on a good day (excluding Windows Update).
Status: Sections marked
[TODO]are placeholders — fill them in as Tasks #4–#8 complete.
- Windows 11 Pro
- Hyper-V enabled (see below)
- The project cloned to
C:\Users\<you>\claude-code\xmp-reader(or adjust paths throughout)
In an elevated PowerShell:
Enable-WindowsOptionalFeature -Online -FeatureName Microsoft-Hyper-V-All -NoRestart
Restart-ComputerOr via Settings → System → Optional features → More Windows features → check Hyper-V → OK → reboot.
Confirm after reboot:
(Get-CimInstance Win32_ComputerSystem).HyperVisorPresent # should be TrueMicrosoft does not offer a standalone VHDX download. Use Hyper-V Quick Create:
- Open Hyper-V Manager (Start → search "Hyper-V Manager")
- Click Quick Create in the Actions panel (right side)
- Select Windows 11 development environment from the gallery
- Click Create Virtual Machine — the app handles the download (~20 GB) and VM creation
The image comes with Visual Studio and the Windows 11 SDK pre-installed.
The Default Switch is unreliable when the host is on Wi-Fi (known issue on Lenovo and other hardware). Use a manual NAT switch instead.
On the host (elevated PowerShell, run once):
# Create an internal switch
New-VMSwitch -Name "NATSwitch" -SwitchType Internal
# Assign an IP to the host side
New-NetIPAddress -IPAddress 192.168.100.1 -PrefixLength 24 -InterfaceAlias "vEthernet (NATSwitch)"
# Create the NAT rule
New-NetNat -Name "NATNetwork" -InternalIPInterfaceAddressPrefix 192.168.100.0/24In Hyper-V Manager:
VM Settings → Network Adapter → switch from "Default Switch" to NATSwitch → Apply.
Inside the VM (elevated PowerShell, run once):
$idx = (Get-NetAdapter | Where-Object Status -eq Up).ifIndex
New-NetIPAddress -InterfaceIndex $idx -IPAddress 192.168.100.10 -PrefixLength 24 -DefaultGateway 192.168.100.1
Set-DnsClientServerAddress -InterfaceIndex $idx -ServerAddresses 8.8.8.8Verify:
Test-NetConnection google.com -InformationLevel Quiet # should return TrueBoot the VM, sign in, then run Windows Update to completion:
Start → Settings → Windows Update → Check for updates
Expect one or two reboot cycles. The prebuilt image can be months old.
Once fully updated, take a snapshot from the host:
Checkpoint-VM -Name "Windows 11 dev environment" -SnapshotName "clean-baseline"Or in Hyper-V Manager: right-click VM → Checkpoint → rename to clean-baseline.
This is your rollback target. Any time the VM gets into a bad state, revert here.
Inside the VM, go to rustup.rs, download and run rustup-init.exe. Accept the
defaults (stable, MSVC toolchain).
Open a new PowerShell (to pick up the updated PATH) and confirm:
rustc --version # rustc 1.94.1
cargo --version # cargo 1.94.1
rustup show active-toolchain # stable-x86_64-pc-windows-msvc (default)Install cmake (required by xmp_toolkit build.rs):
winget install Kitware.CMake --source wingetOpen a new PowerShell and confirm:
cmake --version # cmake 4.3.1Smoke test:
cargo new hello-xmp && cd hello-xmp && cargo build
# Expected: Finished `dev` profile in ~1s
cd .. && Remove-Item -Recurse -Force hello-xmpThe xmp_toolkit crate (v1.12.1 verified, Adobe-maintained) bundles the C++
Adobe XMP Toolkit SDK and builds it automatically via build.rs using cmake.
No separate SDK install is needed — the cmake + MSVC toolchain from section 5
is sufficient.
Cargo dependency:
[dependencies]
xmp_toolkit = { version = "1", features = ["crt_static"] }The crt_static feature statically links the MSVC CRT — important for a DLL loaded
into prevhost.exe to avoid runtime version conflicts with other handlers.
Two things will bite you in M2:
1. from_file() does NOT read .xmp sidecars. It reads embedded XMP out of
image file containers (JPEG, TIFF, etc.). For standalone .xmp sidecar files,
read the file as a string and parse it:
let xml = fs::read_to_string("photo.xmp")?;
let xmp: XmpMeta = xml.parse()?; // via FromStr — use .parse(), not XmpMeta::from_str2. dc:title and dc:description are Lang Alt arrays, not strings. Reading
them via property() returns an empty string (the array container value). Use
localized_text() with "x-default" as the locale:
let title = xmp.localized_text(xmp_ns::DC, "title", None, "x-default")
.map(|(v, _)| v.value);This applies to any Dublin Core alt-text field. Simple properties like xmp:Rating
use property() directly.
Strategy: Build inside the VM. Source lives on the host, exposed to the VM via
an SMB share over the NAT switch. The Rust target/ directory stays VM-local
(set via CARGO_TARGET_DIR) — building into an SMB share is slow and causes file
lock conflicts with incremental compilation.
# 1. Create the SMB share
New-SmbShare -Name "xmp-reader" `
-Path "C:\path\to\xmp-reader" `
-FullAccess "[COMPUTER-NAME]\[USERNAME]"
# 2. Firewall rule scoped to the NAT subnet
New-NetFirewallRule -DisplayName "SMB from Hyper-V NAT" `
-Direction Inbound `
-Protocol TCP `
-LocalPort 445 `
-RemoteAddress 192.168.100.0/24 `
-Profile Private `
-Action Allow `
-Enabled True
# 3. Set the NAT switch interface to the Private network profile
Set-NetConnectionProfile -InterfaceAlias "vEthernet (NATSwitch)" -NetworkCategory PrivateWindows Firewall has a per-profile AllowInboundRules setting. When it is False,
every inbound allow rule on that profile is silently ignored — rules still
appear as Enabled: True but never match traffic. On this host the Private profile
shipped with AllowInboundRules: False. Fix:
# Check the setting
Get-NetFirewallProfile -Name Private |
Select-Object Name, Enabled, AllowInboundRules
# If False, flip it
Set-NetFirewallProfile -Profile Private -AllowInboundRules TrueVerify from the VM:
Test-NetConnection 192.168.100.1 -Port 445 # should return TcpTestSucceeded : TrueBefore mounting, ensure network sharing is turned on in the VM:
Settings → Network & internet → Advanced network settings → Advanced sharing settings → expand Private networks → enable Network discovery and File and printer sharing.
# Mount \\host\xmp-reader as drive Z: persistently
New-PSDrive -Name "Z" -PSProvider FileSystem `
-Root "\\192.168.100.1\xmp-reader" `
-Credential [COMPUTER-NAME]\[USERNAME] -Persist
# Prompt appears for the host account password.
# Keep target/ off the share — VM-local disk only
[System.Environment]::SetEnvironmentVariable("CARGO_TARGET_DIR", "C:\cargo-target", "User")Verify:
cd Z:\
ls # should show CLAUDE.md, docs, sandbox, scripts
cargo build # writes to C:\cargo-target\, not Z:\target\Self-signed cert for M1–M7. Production signing (Azure Trusted Signing) is deferred to M8.
# 1. Create a self-signed code-signing certificate (valid 2 years)
$cert = New-SelfSignedCertificate `
-Subject "CN=xmp-reader dev" `
-Type CodeSigningCert `
-CertStoreLocation Cert:\CurrentUser\My `
-NotAfter (Get-Date).AddYears(2)
Write-Host "Thumbprint: $($cert.Thumbprint)"
# 2. Trust it: export the cert and import into Trusted Root (requires elevation)
Export-Certificate -Cert $cert -FilePath "$env:TEMP\xmp-reader-dev.cer" | Out-Null
Import-Certificate -FilePath "$env:TEMP\xmp-reader-dev.cer" `
-CertStoreLocation Cert:\LocalMachine\Root
Remove-Item "$env:TEMP\xmp-reader-dev.cer"Verify the cert exists:
Get-ChildItem Cert:\CurrentUser\My -CodeSigningCert |
Where-Object { $_.Subject -match "xmp-reader dev" } |
Format-Table Subject, Thumbprint, NotAfterUse the helper script (no elevation required):
.\scripts\sign-dll.ps1 # sign the default release DLL
.\scripts\sign-dll.ps1 -DllPath "C:\other.dll" # sign a specific file
.\scripts\sign-dll.ps1 -Verify # verify the signatureThe script finds the cert by subject name (xmp-reader dev) and calls signtool
with SHA-256 and a DigiCert timestamp. signtool.exe ships with Visual Studio /
Windows SDK (both pre-installed in the dev VM image).
.\scripts\reset-handler.ps1 -Release # release DLL lock
cargo build --release
.\scripts\sign-dll.ps1 # sign the new DLL
.\scripts\reset-handler.ps1 -Install # register + restart Explorerscripts/reset-handler.ps1 — run from an elevated PowerShell inside the VM.
# 1. Before cargo build — release the DLL file lock held by prevhost.exe
.\scripts\reset-handler.ps1 -Release
# 2. Build
cargo build --release
# 3. After build — register the new DLL and reload Explorer
.\scripts\reset-handler.ps1 -Install# Quick Explorer refresh, no registration change
.\scripts\reset-handler.ps1
# Remove the handler entirely
.\scripts\reset-handler.ps1 -Uninstall
# Override the DLL path
.\scripts\reset-handler.ps1 -Install -DllPath "C:\path\to\xmp_reader.dll"Why kill prevhost.exe?
Explorer loads property handlers into prevhost.exe (a surrogate process), not into
explorer.exe itself. Restarting Explorer alone does not unload the DLL — you must
kill prevhost.exe too. -Release kills it before the build so the linker can
overwrite the file; -Install kills it after so the fresh copy is loaded.
sandbox/smoke-test.wsb — a clean-install smoke test environment independent of
the persistent Hyper-V VM.
Prerequisites: build the project first (cargo build --release).
To run:
Double-click sandbox\smoke-test.wsb on the host. The sandbox will:
- Map the release build output and sandbox scripts as read-only folders
- Run
sandbox\install.ps1automatically on login - Register the DLL, restart Explorer, and open
sandbox\test-fixtures\in a window
Select a file in the Explorer window → View → Details pane to verify XMP fields are visible.
Add JPEG + .xmp sidecar pairs to sandbox\test-fixtures\ (done in M2).
| Name | When to create | Purpose |
|---|---|---|
clean-baseline |
After Windows Update, before any dev tools | Rollback to a clean OS |
ready-for-m1 |
After completing all M0.5 tasks (#4–#11) | Rollback to a known-good toolchain state |
cargo build fails with "file in use" / access denied on the DLL
Run .\scripts\reset-handler.ps1 -Release first — prevhost.exe holds a lock on the
loaded DLL.
VM has no internet (169.254.x.x address) The Default Switch failed to assign an IP. Follow section 3 to set up the manual NAT switch.
regsvr32 fails
Ensure you are running from an elevated PowerShell. On first run the DLL must export
DllRegisterServer — this is implemented in M1.
Explorer Details pane shows no XMP fields Check that the handler is registered for the correct extension:
Get-ItemProperty "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\PropertySystem\PropertyHandlers\.jpg"The default value should be our handler's CLSID.