For developers. How to build WinAppMigrator from source, the checks every change has to pass, and why some tests only run on CI. The authority is CONTRIBUTING.md.
-
Windows. The project builds for
x86_64-pc-windows-msvc. The client is the one exception: see The client. -
Rust through rustup.
rust-toolchain.tomlpins Rust 1.99.1, with clippy and rustfmt, and rustup picks it by itself. The client also needs therust-srccomponent, which its script adds. - The Visual Studio C++ build tools, for the MSVC linker.
- For the last two checks:
cargo install cargo-audit cargo-deny --locked.
The app carries the installer stub inside itself, so build the stub first:
cargo build --release -p winappmigrator-installer
$env:WINAPPMIGRATOR_INSTALLER_STUB = "$PWD\target\release\winappmigrator-installer.exe"
cargo build --release -p winappmigrator-gui
.\target\release\winappmigrator.exeHow the app's build script finds the stub:
-
WINAPPMIGRATOR_INSTALLER_STUB, if it names a file; - otherwise
winappmigrator-installer.exein the sametarget\<profile>folder.
A release build of the app with no stub to embed fails. A debug build with none embeds nothing, and at run time takes a winappmigrator-installer.exe beside it, so a development build can come before the stub does. A release never looks beside itself, since whatever is beside it in a Downloads folder could be anything.
cargo build --workspace alone can leave the app holding the stub from the build before. CI checks the app holds the exact bytes of the stub and the client it just built, with .github/scripts/check-stub-embedded.ps1.
The release profile uses link-time optimization, one codegen unit, and stripped symbols. The C runtime is linked statically, set in .cargo/config.toml, so an installer can start on a PC that has no Visual C++ runtime, which may be the very runtime it is about to install.
The app also carries WinAppMigrator Client, which it saves for the old PC. Build it before the app, with the stub:
cargo build --release -p winappmigrator-installer
scripts/build-client.ps1
$env:WINAPPMIGRATOR_INSTALLER_STUB = "$PWD\target\release\winappmigrator-installer.exe"
cargo build --release -p winappmigrator-guiThe client has to run on Windows 7, and Rust's own Windows targets stopped running there at 1.78. So the script builds it for i686-win7-windows-msvc, one 32-bit program for every Windows from 7 on. That target needs the standard library built for it (-Zbuild-std), which the script lets the pinned compiler do with RUSTC_BOOTSTRAP=1, so the client is built by the same compiler as the rest. It also links the C runtime in, since Windows 7 has no Universal C runtime of its own, and builds into target\win7.
How the app's build script finds the client:
-
WINAPPMIGRATOR_CLIENT, if it names a file; - otherwise
target\win7\i686-win7-windows-msvc\release\winappmigrator-client.exe.
A release build of the app with no client fails, as one with no stub does. A debug build with none embeds nothing, and Network PC… then says the build "does not carry the client" when you try to save it.
A release builds the client in a job of its own, has it signed in another, and builds the app around the signed client, with WINAPPMIGRATOR_CLIENT. See Releases and signing.
Every change has to pass these, in this order:
cargo fmt --check
cargo clippy --workspace --all-targets --all-features -- -D warnings
cargo test --workspace --all-features
cargo build --release -p winappmigrator-installer
scripts/build-client.ps1
cargo build --workspace --release
cargo audit
cargo deny check| Check | What it proves |
|---|---|
cargo fmt --check |
The code is formatted. |
cargo clippy ... -D warnings |
No lint warnings, in any target. The workspace denies unwrap, expect, todo, unimplemented, dbg!, unused must_use results, and unsafe outside hardening.rs. Dead code fails it too. |
cargo test --workspace --all-features |
The unit tests, the headless UI tests, the end-to-end test, and log_has_no_secrets.rs. The elevated_* tests are built and skip. |
| The release builds | The stub, the client for Windows 7, then everything, in release. |
cargo audit |
No dependency has a known security advisory. |
cargo deny check |
Dependencies meet the policy in deny.toml. |
Two aliases in .cargo/config.toml save typing:
-
cargo qualityiscargo test --workspace --all-features; -
cargo typecheckiscargo check --workspace --all-targets --all-features.
Never add --all-targets to cargo test. It would also run both executables as test harnesses. Both embed a manifest that demands administrator rights, so from a normal shell Windows refuses to start them, with error 740, and the test run fails. That is why both executables keep all their logic, and their tests, in a library, and main.rs only calls into it. Clippy and cargo check do use --all-targets, since they do not run anything.
Tests and manual runs stay in temp folders. They must not change the machine they run on. What really changes a PC, such as installing a service, writing to HKLM, or adding an entry to Settings, is covered by the elevated_* tests in crates/winappmigrator-installer/tests/, which run only on GitHub's throwaway runners. See CI jobs.
The elevated_* tests do nothing unless all three of these are set:
GITHUB_ACTIONS=trueRUNNER_ENVIRONMENT=github-hostedWAM_ELEVATED_E2E=1
elevated_move.rs also needs WAM_MOVE_OUTPUT, and elevated_real_apps.rs needs one of WAM_REAL_APPS_OUTPUT, WAM_REAL_APPS_USER_DATA, or WAM_REAL_APPS_RUNTIME. Once their variables are set, they fail if the process is not elevated, so a CI job cannot pass without having run them. Without them, they skip, so they are safe to run locally, which shows only that they compile.
The end-to-end test needs only the first two. With them set, from an elevated shell, it installs for real and leaves the install in place. Never set any of these variables on a real PC.
The end-to-end test, installer_e2e.rs, builds a real installer with the real stub and runs it:
- from a normal shell with no
CIvariable set, it skips running the installer, because the installer demands elevation. WithCIset, it fails instead; - from an elevated shell, it stops at a preview, because an install writes to
ProgramDataand to Settings; - it installs for real only on GitHub's runner.
When you change how something is restored: record what it replaces in the journal, teach rollback.rs to undo it, extend elevated_round_trip.rs, and push to see it run.
Logs hold paths and key names only. crates/winappmigrator-core/tests/log_has_no_secrets.rs pins that what a registry value, license file, or environment variable holds never reaches a log.
Do not drive a live window with scripted clicks. The UI tests drive the real screens of the app, the installer, and the uninstaller with no window (egui_kittest), and need no elevation. They can also write the main states as pictures, so you can see what you changed:
$env:WAM_UI_PREVIEW_DIR = "$env:TEMP\wam-preview"
cargo test -p winappmigrator-gui -p winappmigrator-installer --lib -- --test-threads=1Writing the pictures draws with Direct3D 12, which falls back to a software renderer if there is no graphics card.
The README's screenshots in docs/images are copies of those previews, in both themes. Regenerate them when a screen changes:
| Preview | README image |
|---|---|
footprint- |
app-pick- |
footprint-left-out-folder- |
app-choose- |
done- |
app-done- |
installer-ready- |
installer- |
network-found- |
network-pair- |
Each comes as -dark and -light. They tell one story, of an app built with both options off, so the sample data has to agree with itself.
docs/images also holds images for the docs and this wiki that are not on the README's first screen: windows-7-drive- as source-windows7-, and network-steps- and network-paired- under their own names. client-windows7.png is the client itself, taken from a Windows 7 virtual machine's screen, not from a preview.
For a visual check of the real window, launch the app and take a single screenshot. Setting __COMPAT_LAYER=RunAsInvoker starts it without the UAC prompt, for that only. The conventions for screens are in docs/gui.md.
These examples in winappmigrator-core build or read, and never install anything:
| Command | What it does |
|---|---|
cargo run --release -p winappmigrator-core --example wam_measure -- "<app name>" <stub.exe> <work folder> |
Builds an installer of a real app on this PC, and says how long each step took, how much it read and wrote, and the most it had on disk. Removes the work folder afterward. |
cargo run --release -p winappmigrator-core --example wam_pkg -- <source_exe> <stub_exe> <out_dir> <app_name> |
Builds an installer from one executable, as the app would. |
cargo run --release -p winappmigrator-core --example offline_scan -- <volume_root> [output_file] |
Lists the apps on a mounted disk. Needs an elevated shell, as it loads copies of the disk's registry hives. |
cargo run --release -p winappmigrator-core --example wam_near_matches -- <out.txt> [--scan "<app name>"]... |
Says what near matches would be offered for every app on this PC. Its output holds this PC's paths, so keep it out of anything you share. |
Run the checks above. For anything large, open an issue first. Keep the docs true: update the README, docs/, and CHANGELOG.md in the same change as the code.
Last changed 2026-11-11. Describes WinAppMigrator as of its latest release, 0.6.0.