qml-build-deploy
QML Application Build & DLL Deployment
Build scripts for QML applications must handle three concerns beyond compilation: deploying Qt runtime DLLs next to the executable, verifying QML plugin directories exist, and ensuring platform plugins are present. Missing any of these causes silent failures — the app compiles and runs but shows no window.
When to Use This Skill
- Writing a build/deploy script for any application that loads
.qmlfiles at runtime - Debugging a Qt/QML app that compiles but shows no visible window
- Setting up CI/CD for a CxxQt (Rust) or PySide6 (Python) QML project on Windows
- Expanding an existing build script to handle Qt DLL deployment robustly
Qt Installation Discovery
Always resolve the Qt installation path early and fail fast if it's missing.
PowerShell Pattern
param(
[string]$QtDir = $(if ($env:QT_DIR) { $env:QT_DIR } else { 'C:\Qt\6.10.2\msvc2022_64' })
)
$ErrorActionPreference = 'Stop'
if (-not (Test-Path $QtDir)) {
throw "Qt directory not found at: $QtDir. Set `$env:QT_DIR` to your Qt installation."
}
$qtBin = Join-Path $QtDir 'bin'
$qmakePath = Join-Path $qtBin 'qmake6.exe'
if (-not (Test-Path $qmakePath)) {
throw "qmake6.exe not found at: $qmakePath"
}
# Add Qt bin to PATH for runtime discovery
$env:PATH = "$qtBin;$env:PATH"
Key Directories
| Variable | Path | Purpose |
|---|---|---|
$QtDir |
C:\Qt\6.x.x\msvc2022_64 |
Qt installation root |
$qtBin |
$QtDir\bin |
DLLs and tools (windeployqt, qmake6) |
$QtDir\plugins\platforms |
Platform plugins source | qwindows.dll |
$QtDir\qml |
QML modules source | QtQuick, QtQml, etc. |
windeployqt: The Primary Deployment Tool
windeployqt scans an executable for Qt dependencies and copies them alongside it. Always run it first, then verify its output — it frequently misses DLLs.
Running windeployqt
$deployTool = Join-Path $qtBin 'windeployqt.exe'
if (-not (Test-Path $deployTool)) {
throw "windeployqt.exe not found at: $deployTool"
}
$deployArgs = @()
if ($Profile -eq 'release') {
$deployArgs += '--release'
} else {
$deployArgs += '--debug'
}
# --qmldir tells windeployqt where to scan for QML imports
$deployArgs += @('--qmldir', $qmlSourceDir, $exePath)
& $deployTool @deployArgs
if ($LASTEXITCODE -ne 0) {
throw 'windeployqt deployment failed.'
}
Critical: The --qmldir Flag
Without --qmldir, windeployqt cannot detect which QML modules your app uses and will skip deploying them. Always point it at the directory containing your .qml source files.
# For CxxQt projects — QML files are in the project root or qml/ subfolder
$deployArgs += @('--qmldir', $projectDir, $exePath)
# For PySide6 projects — QML files are typically in resources/qml/
$deployArgs += @('--qmldir', (Join-Path $projectDir 'resources\qml'), $exePath)
Comprehensive DLL Verification
windeployqt misses DLLs regularly, especially style plugins and less common modules. Always verify after deployment and copy missing files from the Qt installation.
Required DLL Manifest
Maintain an explicit list of every DLL your app needs. This list varies by what QML imports you use.
$requiredDlls = @(
# ── Core Qt runtime ──
'Qt6Core.dll',
'Qt6Gui.dll',
'Qt6Network.dll',
'Qt6OpenGL.dll',
# ── QML engine ──
'Qt6Qml.dll',
'Qt6QmlMeta.dll',
'Qt6QmlModels.dll',
'Qt6QmlWorkerScript.dll',
# ── QtQuick rendering ──
'Qt6Quick.dll',
'Qt6QuickControls2.dll',
'Qt6QuickControls2Impl.dll',
'Qt6QuickTemplates2.dll',
'Qt6QuickLayouts.dll',
'Qt6QuickShapes.dll',
'Qt6QuickEffects.dll',
# ── Controls style plugins (windeployqt often misses these) ──
'Qt6QuickControls2Basic.dll',
'Qt6QuickControls2BasicStyleImpl.dll',
'Qt6QuickControls2Fusion.dll',
'Qt6QuickControls2FusionStyleImpl.dll',
'Qt6QuickControls2Material.dll',
'Qt6QuickControls2MaterialStyleImpl.dll',
'Qt6QuickControls2Universal.dll',
'Qt6QuickControls2UniversalStyleImpl.dll',
'Qt6QuickControls2WindowsStyleImpl.dll',
'Qt6QuickControls2Imagine.dll',
'Qt6QuickControls2ImagineStyleImpl.dll',
'Qt6QuickControls2FluentWinUI3StyleImpl.dll',
# ── Optional modules (add based on your QML imports) ──
'Qt6Svg.dll', # If using SVG images
'Qt6LabsQmlModels.dll', # If using Qt.labs.qmlmodels
# ── Rendering support ──
'D3Dcompiler_47.dll',
'opengl32sw.dll'
)
DLL Categories Quick Reference
| Category | DLLs | When Needed |
|---|---|---|
| Core | Qt6Core, Qt6Gui, Qt6Network |
Always |
| QML Engine | Qt6Qml, Qt6QmlMeta, Qt6QmlModels |
Any QML app |
| Quick Rendering | Qt6Quick, Qt6QuickLayouts, Qt6QuickShapes |
Any QtQuick UI |
| Controls | Qt6QuickControls2* |
Using import QtQuick.Controls |
| Style Plugins | Qt6QuickControls2Material*, *Fusion*, etc. |
Using styled controls |
| Graphics | D3Dcompiler_47, opengl32sw |
Windows GPU rendering |
| SVG | Qt6Svg |
Image { source: "*.svg" } |
| Network/SSL | Qt6Network + OpenSSL DLLs |
TCP/HTTP/WebSocket usage |
Verify-and-Copy Pattern
After windeployqt runs, iterate the manifest and copy anything it missed:
$outputDir = Split-Path -Parent $exePath
$missingDlls = @()
$copiedDlls = @()
foreach ($dll in $requiredDlls) {
$dllDest = Join-Path $outputDir $dll
if (-not (Test-Path $dllDest)) {
# Fallback: copy from Qt bin directory
$dllSrc = Join-Path $qtBin $dll
if (Test-Path $dllSrc) {
Copy-Item $dllSrc $dllDest -Force
$copiedDlls += $dll
} else {
$missingDlls += $dll
}
}
}
if ($copiedDlls.Count -gt 0) {
Write-Host "Copied $($copiedDlls.Count) missing DLL(s) from Qt:" -ForegroundColor Yellow
$copiedDlls | ForEach-Object { Write-Host " + $_" -ForegroundColor Yellow }
}
if ($missingDlls.Count -gt 0) {
throw "Qt deployment incomplete. Missing: $($missingDlls -join ', ')"
}
Write-Host 'Qt runtime deployment verified.' -ForegroundColor Green
Platform Plugin Verification
The platform plugin (qwindows.dll) is the single most critical deployment artifact. Without it, Qt cannot create any windows and exits silently.
$platformDir = Join-Path $outputDir 'platforms'
if (-not (Test-Path (Join-Path $platformDir 'qwindows.dll'))) {
$srcPlatform = Join-Path $QtDir 'plugins\platforms\qwindows.dll'
if (Test-Path $srcPlatform) {
New-Item -ItemType Directory -Force -Path $platformDir | Out-Null
Copy-Item $srcPlatform (Join-Path $platformDir 'qwindows.dll') -Force
Write-Host 'Copied platform plugin: platforms\qwindows.dll' -ForegroundColor Yellow
} else {
throw 'FATAL: platforms\qwindows.dll not found in Qt installation.'
}
}
QML Module Directory Verification
QML imports (import QtQuick, import QtQuick.Controls, etc.) resolve to directories under qml/ next to the executable. windeployqt creates these, but verify they exist.
$requiredQmlDirs = @(
'qml\QtQuick',
'qml\QtQuick\Controls',
'qml\QtQuick\Layouts',
'qml\QtQml'
)
foreach ($qmlDir in $requiredQmlDirs) {
$qmlPath = Join-Path $outputDir $qmlDir
if (-not (Test-Path $qmlPath)) {
Write-Host "WARNING: QML module directory missing: $qmlDir" -ForegroundColor Yellow
# Fallback: copy from Qt installation
$srcQmlPath = Join-Path $QtDir $qmlDir
if (Test-Path $srcQmlPath) {
Copy-Item $srcQmlPath $qmlPath -Recurse -Force
Write-Host " Copied from Qt installation" -ForegroundColor Yellow
} else {
$missingDlls += "qml-dir:$qmlDir"
}
}
}
Common QML Import → Directory Mapping
| QML Import | Required Directory |
|---|---|
import QtQuick |
qml\QtQuick |
import QtQuick.Controls |
qml\QtQuick\Controls |
import QtQuick.Layouts |
qml\QtQuick\Layouts |
import QtQml |
qml\QtQml |
import QtQuick.Shapes |
qml\QtQuick\Shapes |
import Qt.labs.qmlmodels |
qml\Qt\labs\qmlmodels |
QRC Resource Path Convention (CxxQt)
CxxQt compiles QML files into the binary via the Qt Resource System (QRC). The QmlModule in build.rs creates QRC entries with a qml/ alias prefix, so the load URL must include that prefix.
build.rs Configuration
CxxQtBuilder::new_qml_module(
QmlModule::new("com.mycompany.myapp")
.qml_files(["qml/main.qml", "qml/MyComponent.qml"]),
)
.file("src/cxxqt_bridge/ffi.rs")
.build();
Correct Load URL
The QRC URL pattern is: qrc:/qt/qml/{uri-as-path}/qml/{filename}
// ✅ CORRECT — includes qml/ prefix matching the QRC alias
engine.load(&QUrl::from("qrc:/qt/qml/com/mycompany/myapp/qml/main.qml"));
// ❌ WRONG — missing qml/ prefix, file not found at runtime
engine.load(&QUrl::from("qrc:/qt/qml/com/mycompany/myapp/main.qml"));
Diagnosing QRC Failures
If QML fails to load from QRC, the engine silently produces no window. Add a post-load check:
if let Some(engine) = engine.as_mut() {
engine.load(&QUrl::from("qrc:/qt/qml/com/mycompany/myapp/qml/main.qml"));
}
// Check if any root objects were created
// If the list is empty, QML failed to load
eprintln!("QML engine loaded — checking for root objects...");
Debugging Qt Runtime Failures
When a QML app compiles but shows no window, use these environment variables to get diagnostics.
Debug Environment Variables
$env:QT_DEBUG_PLUGINS = '1' # Verbose plugin loading (qwindows.dll, etc.)
$env:QML_IMPORT_TRACE = '1' # Trace every QML import resolution
$env:QT_LOGGING_RULES = '*.debug=true' # Full Qt debug logging (very verbose)
Diagnostic Launch Pattern
# Capture stderr (where Qt writes diagnostics) to a log file
$proc = Start-Process -FilePath $exePath -ArgumentList '--debug' `
-RedirectStandardError 'qt_debug.log' `
-PassThru -NoNewWindow
Start-Sleep -Seconds 5
$proc | Stop-Process -Force -ErrorAction SilentlyContinue
Get-Content 'qt_debug.log'
Common Error Messages and Fixes
| Error in Log | Root Cause | Fix |
|---|---|---|
No such file or directory for .qml |
Wrong QRC URL path | Add qml/ prefix to match QRC alias |
module "QtQuick" is not installed |
QML module directory missing | Run windeployqt with --qmldir or copy qml\QtQuick |
Could not find the Qt platform plugin "windows" |
platforms\qwindows.dll missing |
Copy from $QtDir\plugins\platforms\ |
Invalid property assignment: "X" is a read-only property |
QML syntax error crashing engine | Fix the QML property assignment |
QQmlApplicationEngine failed to load component |
QML file not found or has errors | Check QRC path + run with QML_IMPORT_TRACE=1 |
Plugin uses incompatible Qt library |
Mixing debug/release DLLs | Ensure --release or --debug flag matches build profile |
Complete Build Script Template
This template covers build, deploy, test, and run for a CxxQt Rust+QML project. Adapt for PySide6 by replacing cargo build with your Python packaging tool.
param(
[switch]$Clean,
[switch]$Test,
[switch]$Run,
[switch]$Deploy,
[ValidateSet('debug', 'release')]
[string]$Profile = 'release',
[string]$QtDir = $(if ($env:QT_DIR) { $env:QT_DIR } else { 'C:\Qt\6.10.2\msvc2022_64' })
)
$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
Set-Location $scriptDir
# ── Qt discovery ──
if (-not (Test-Path $QtDir)) {
throw "Qt not found at: $QtDir. Set `$env:QT_DIR`."
}
$qtBin = Join-Path $QtDir 'bin'
$env:QMAKE = Join-Path $qtBin 'qmake6.exe'
$env:PATH = "$qtBin;$env:PATH"
Write-Host "=== QML App Build ===" -ForegroundColor Cyan
Write-Host "Qt: $QtDir" -ForegroundColor Gray
Write-Host "Profile: $Profile" -ForegroundColor Gray
# ── Clean ──
if ($Clean) {
cargo clean
if ($LASTEXITCODE -ne 0) { throw 'cargo clean failed.' }
}
# ── Build ──
$buildArgs = @('build')
if ($Profile -eq 'release') { $buildArgs += '--release' }
Write-Host "Running: cargo $($buildArgs -join ' ')" -ForegroundColor Cyan
cargo @buildArgs
if ($LASTEXITCODE -ne 0) { throw 'cargo build failed.' }
$exeName = 'my-app.exe' # ← Replace with your binary name
$exePath = Join-Path $scriptDir "target\$Profile\$exeName"
# ── Deploy Qt runtime ──
$doDeploy = ($Profile -eq 'release') -or $Deploy.IsPresent
if ($doDeploy -and (Test-Path $exePath)) {
$deployTool = Join-Path $qtBin 'windeployqt.exe'
# Step 1: Run windeployqt
$deployArgs = @($(if ($Profile -eq 'release') { '--release' } else { '--debug' }))
$deployArgs += @('--qmldir', $scriptDir, $exePath)
& $deployTool @deployArgs
if ($LASTEXITCODE -ne 0) { throw 'windeployqt failed.' }
# Step 2: Verify and copy missing DLLs
$outputDir = Split-Path -Parent $exePath
$requiredDlls = @(
'Qt6Core.dll', 'Qt6Gui.dll', 'Qt6Network.dll', 'Qt6OpenGL.dll',
'Qt6Qml.dll', 'Qt6QmlMeta.dll', 'Qt6QmlModels.dll', 'Qt6QmlWorkerScript.dll',
'Qt6Quick.dll', 'Qt6QuickControls2.dll', 'Qt6QuickControls2Impl.dll',
'Qt6QuickTemplates2.dll', 'Qt6QuickLayouts.dll',
'Qt6QuickControls2Basic.dll', 'Qt6QuickControls2BasicStyleImpl.dll',
'Qt6QuickControls2Material.dll', 'Qt6QuickControls2MaterialStyleImpl.dll',
'D3Dcompiler_47.dll', 'opengl32sw.dll'
)
$missing = @(); $copied = @()
foreach ($dll in $requiredDlls) {
$dest = Join-Path $outputDir $dll
if (-not (Test-Path $dest)) {
$src = Join-Path $qtBin $dll
if (Test-Path $src) {
Copy-Item $src $dest -Force; $copied += $dll
} else { $missing += $dll }
}
}
# Step 3: Platform plugin
$platformDir = Join-Path $outputDir 'platforms'
if (-not (Test-Path (Join-Path $platformDir 'qwindows.dll'))) {
$src = Join-Path $QtDir 'plugins\platforms\qwindows.dll'
if (Test-Path $src) {
New-Item -ItemType Directory -Force -Path $platformDir | Out-Null
Copy-Item $src (Join-Path $platformDir 'qwindows.dll') -Force
$copied += 'platforms\qwindows.dll'
} else { $missing += 'platforms\qwindows.dll' }
}
# Step 4: QML module directories
foreach ($qmlDir in @('qml\QtQuick', 'qml\QtQuick\Controls', 'qml\QtQuick\Layouts', 'qml\QtQml')) {
if (-not (Test-Path (Join-Path $outputDir $qmlDir))) {
$missing += "qml-dir:$qmlDir"
}
}
if ($copied.Count -gt 0) {
Write-Host "Copied $($copied.Count) missing file(s):" -ForegroundColor Yellow
$copied | ForEach-Object { Write-Host " + $_" -ForegroundColor Yellow }
}
if ($missing.Count -gt 0) {
throw "Deployment incomplete. Missing: $($missing -join ', ')"
}
Write-Host 'Qt deployment verified.' -ForegroundColor Green
}
# ── Test ──
if ($Test) {
cargo test
if ($LASTEXITCODE -ne 0) { throw 'Tests failed.' }
}
# ── Run ──
if ($Run) {
if (-not (Test-Path $exePath)) { throw "Executable not found: $exePath" }
Write-Host "Launching..." -ForegroundColor Cyan
& $exePath
}
Write-Host 'Done.' -ForegroundColor Green
Script Output Refinement Playbook (Reproducible)
Use this when you want a build script that is informative but not noisy. These steps mirror the refinements applied to build-interactive-terminal.ps1.
1) Stream Cargo Progress with Live Warning Count
Problem: cargo build can appear silent for long periods.
Pattern:
- Run cargo with redirected stdout/stderr to temp files.
- Poll stderr at a moderate cadence (default ~1.5s) for
^warning:count. - Print heartbeat every few seconds and on count change.
Write-Host "Running: cargo $($buildArgs -join ' ')" -ForegroundColor Cyan
while (-not $process.HasExited) {
$warningCount = (Select-String -Path $stderrFile -Pattern '^warning:' -SimpleMatch:$false).Count
Write-Host ("cargo build in progress... warnings so far: {0}" -f $warningCount) -ForegroundColor DarkGray
Start-Sleep -Milliseconds 1500
}
Performance note:
- Make polling interval configurable (
PollIntervalMs) and clamp to a safe floor (e.g. 200ms). - Use a separate heartbeat interval (
HeartbeatSeconds) to avoid frequent duplicate updates when warning count is unchanged.
2) Keep Warnings Yellow, Errors Red
Problem: all native stderr can render as “error-looking” output.
Pattern:
- Capture output first.
- Replay lines with explicit coloring:
^warning→ Yellow^error|^ERROR→ Red- everything else → default
if ($trimmed -match '^warning[:\[]') {
Write-Host $line -ForegroundColor Yellow
} elseif ($trimmed -match '^error[:\[]|^ERROR\b') {
Write-Host $line -ForegroundColor Red
} else {
Write-Host $line
}
3) Collapse windeployqt Chatter into Summaries
Problem: windeployqt prints thousands of low-value lines (is up to date, per-file copy details).
Pattern:
- Suppress repetitive lines and count them.
- Print compact summary lines at the end.
Recommended counters:
upToDateLineCounttranslationCreateCountpluginTypeAddCountpluginResolvedDependencyCountskippedPluginCountadditionalPassCount
Example summaries:
windeployqt up-to-date items: 1325windeployqt translation files created: 32windeployqt dependency summary: binary=1, scanPaths=1, localDeps=1, pluginTypes=14, resolvedDeps=2, skippedPlugins=2, additionalPasses=1
4) Replace Raw QML Import Dump with Progress
Problem: full QML imports: listing is useful for logs but noisy in terminal.
Pattern:
- Capture import lines into an array.
- Print count + 10% incremental progress updates only.
Write-Host ("QML imports discovered: {0}" -f $capturedQmlImports.Count) -ForegroundColor DarkGray
for ($pct = 10; $pct -le 100; $pct += 10) {
$processed = [Math]::Ceiling(($capturedQmlImports.Count * $pct) / 100.0)
Write-Host ("QML import scan progress: {0}% ({1}/{2})" -f $pct, $processed, $capturedQmlImports.Count) -ForegroundColor DarkGray
}
5) Add Optional Warnings/Imports Report Output
Problem: terminal is readable, but teams still need full traceability.
Pattern:
- Add optional parameter:
[string]$WarningsImportsLogPath = ''
- If set:
- treat path as file OR directory,
- if directory, generate timestamped file name,
- write:
- warning lines (cargo + deploy warnings),
- full captured QML import list.
Suggested file format sections:
Warnings (N)QML Imports (N)
6) Robust Exit Criteria for Cargo in Progress Mode
Problem: some process wrappers can occasionally return bad/unstable exit states.
Pattern:
- Primary failure rule: non-zero exit code.
- Validation rule: accept completion only when output includes cargo finished marker:
Finished `release` profile [optimized] target(s) in ...
- If non-zero and no finished marker: fail hard.
7) Known-Good UX Goals
A refined QML build/deploy script should provide:
- live cargo progress every few seconds,
- visible warning growth signal during compile,
- no red text unless true errors,
- compact deploy summaries instead of per-file spam,
- optional machine-readable text artifact of warnings/imports.
PySide6 Deployment Differences
PySide6 bundles its own Qt DLLs inside the Python package, but standalone deployment (via pyinstaller or cx_freeze) still needs explicit handling.
PySide6 DLL Location
# Find where PySide6 installed Qt DLLs
$pyside6Dir = python -c "import PySide6; print(PySide6.__path__[0])"
# Typically: C:\Python3x\Lib\site-packages\PySide6
# Key subdirectories:
# $pyside6Dir\ → Qt6Core.dll, Qt6Gui.dll, etc.
# $pyside6Dir\plugins\ → platforms\qwindows.dll
# $pyside6Dir\qml\ → QtQuick\, QtQml\, etc.
PyInstaller with QML
pyinstaller --name MyApp `
--add-data "resources/qml;resources/qml" `
--add-binary "$pyside6Dir\plugins\platforms\qwindows.dll;platforms" `
--hidden-import PySide6.QtQuick `
--hidden-import PySide6.QtQml `
main.py
Common Pitfalls
-
Missing
--qmldirin windeployqt — Causes no QML modules to be deployed. App runs but shows blank window or crashes on first QML import. -
Wrong QRC URL path — CxxQt's
QmlModulealiases QML files with aqml/prefix. The load URL must beqrc:/qt/qml/{uri-path}/qml/main.qml, notqrc:/qt/qml/{uri-path}/main.qml. -
windeployqt misses style plugins — Qt6QuickControls2 styles (
Material,Fusion,Universal, etc.) are frequently not deployed. Symptoms: controls appear but with wrong/default styling, or style-dependent layouts break. -
Missing
platforms\qwindows.dll— Complete silent failure. No error message, the process exits immediately. Always verify this file explicitly. -
Debug/Release mismatch — Using
--releasewith windeployqt on a debug build (or vice versa) deploys incompatible DLLs. Error:Plugin uses incompatible Qt library. -
Hardcoded Qt path — Always use
$env:QT_DIRwith a fallback. Different machines have different Qt versions and installation paths. -
Forgetting
opengl32sw.dll— VMs, RDP sessions, and some CI runners lack GPU acceleration. Without the software OpenGL fallback, the app crashes in headless environments. -
QML syntax errors crash silently — A read-only property assignment (e.g.,
verticalCenter: parent.verticalCenteron a Row) prevents the entire QML component tree from loading. The engine reports the error to stderr but shows no window.
File Structure
my-qml-app/
├── build-my-app.ps1 # Build script (this skill's focus)
├── Cargo.toml # Rust project config
├── build.rs # CxxQt build config with QmlModule
├── qml/
│ ├── main.qml # Root QML file
│ └── components/ # Reusable QML components
├── src/
│ ├── main.rs # Entry point
│ └── cxxqt_bridge/ # CxxQt bridge module
└── target/
└── release/
├── my-app.exe # Built executable
├── Qt6Core.dll # ← Deployed by windeployqt + verify script
├── Qt6Quick.dll
├── ...
├── platforms/
│ └── qwindows.dll # ← Critical platform plugin
└── qml/
├── QtQuick/ # ← QML module directories
├── QtQml/
└── ...