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
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/
└── ...
References
More from ds-codi/project-memory-mcp
pyside6-mvc
Use this skill when building Python desktop applications using PySide6 with strict MVC architecture where all UI is defined by .ui files. Covers architecture patterns, controller/model/view separation, signal handling, and .ui file workflows.
95pyside6-qml-views
Use this skill when creating QML view files, designing QML component hierarchies, building layouts, styling QML controls, creating reusable QML components, implementing QML navigation / page switching, or working with QML resources. Covers QML file structure, component patterns, Material/Controls styling, resource management, and common QML idioms for desktop applications.
49pyside6-qml-architecture
Use this skill when creating a new PySide6 + QML desktop application with MVC architecture, setting up project structure, implementing the application bootstrap / DI container, or understanding how the MVC layers connect. Covers project scaffolding, entry points, singleton application class, service locator, signal registry, and lifecycle management.
47mvc-architecture
Use this skill when implementing Model-View-Controller architecture. Covers core MVC principles, layer separation, dependency injection, event-driven communication, and patterns for controllers, models, and views.
40pyside6-qml-models-services
Use this skill when creating domain models with Qt signal support, implementing the repository pattern for data persistence, building service classes for external interactions, designing the central signal registry, or working with application state management. Covers BaseModel, model serialization, database repositories, service patterns, signal definitions, and the ApplicationState singleton.
34pyside6-qml-bridge
Use this skill when exposing Python objects to QML, creating bridge classes, defining Qt properties with NOTIFY signals, implementing invokable methods / slots, or connecting QML user actions to Python controllers. Covers the QObject bridge pattern, property decorators, type conversions, context properties, and QML type registration.
32