This PR improves the MSI installer by removing the Lancer QET.bat wrapper and handling everything natively in QElectroTech.wxs.

**`build-aux/windows/QElectroTech.wxs`**
- Desktop and Start Menu shortcuts now point directly to `bin\qelectrotech.exe` with all required arguments (`--common-elements-dir`, `--common-tbt-dir`, `--lang-dir`, `-style windowsvista`) — no `.bat` wrapper needed
- Added a deferred `CustomAction` that runs after `InstallFiles` and recursively sets all files in `elements\` to read-only using an inline PowerShell command

**`.github/workflows/windows-msi.yml`**
- Replaced the step that created `Lancer QET.bat` with a step that removes it from the artifact before the WiX build, so it is not embedded in the MSI
- The `.bat` file remains untouched in the ZIP portable build (managed by `windows-build.yml`)

- No console window flashing when launching QElectroTech from the MSI shortcuts
- The `elements\` directory is properly set to read-only after installation, as required
- Cleaner MSI package — no `.bat` file shipped to end users installing via MSI
This commit is contained in:
Laurent Trinques
2026-05-21 02:34:38 +02:00
parent fa334d34a4
commit eeaa059a77
2 changed files with 67 additions and 78 deletions

View File

@@ -18,16 +18,19 @@ jobs:
build-msi: build-msi:
name: Build MSI with WiX v7 name: Build MSI with WiX v7
runs-on: windows-latest runs-on: windows-latest
# Only runs if Windows Build succeeded (or triggered manually) # Only runs if Windows Build succeeded (or triggered manually)
if: > if: >
github.event_name == 'workflow_dispatch' || github.event_name == 'workflow_dispatch' ||
github.event.workflow_run.conclusion == 'success' github.event.workflow_run.conclusion == 'success'
permissions: permissions:
contents: write contents: write
pages: write pages: write
id-token: write id-token: write
steps: steps:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 1. Checkout (to retrieve QElectroTech.wxs and sources) # 1. Checkout (to retrieve QElectroTech.wxs and sources)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
@@ -38,16 +41,12 @@ jobs:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 2. Download the portable artifact from the main build # 2. Download the portable artifact from the main build
# Requires windows-build.yml to upload an artifact named
# "qelectrotech-windows-portable" (fixed name)
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Download portable artifact - name: Download portable artifact
uses: actions/download-artifact@v4 uses: actions/download-artifact@v4
with: with:
name: qelectrotech-windows-portable name: qelectrotech-windows-portable
path: artifact\files path: artifact\files
# workflow_run => use the triggering run's ID
# workflow_dispatch => use input run_id if provided, otherwise current run
run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }} run-id: ${{ github.event.workflow_run.id || github.event.inputs.run_id || github.run_id }}
github-token: ${{ secrets.GITHUB_TOKEN }} github-token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }} repository: ${{ github.repository }}
@@ -59,12 +58,10 @@ jobs:
id: version id: version
shell: pwsh shell: pwsh
run: | run: |
# Version from qetversion.cpp (same logic as windows-build.yml)
$src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue $src = Get-Content "sources\qetversion.cpp" -Raw -ErrorAction SilentlyContinue
if ($src -match 'return QVersionNumber\{([^}]+)\}') { if ($src -match 'return QVersionNumber\{([^}]+)\}') {
$ver = $Matches[1] -replace '\s','' -replace ',','.' $ver = $Matches[1] -replace '\s','' -replace ',','.'
} else { } else {
# Fallback: CMakeLists.txt
$cmake = Get-Content "CMakeLists.txt" -Raw $cmake = Get-Content "CMakeLists.txt" -Raw
if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') { if ($cmake -match 'project\s*\([^)]*VERSION\s+([\d]+\.[\d]+\.[\d]+)') {
$ver = $Matches[1] $ver = $Matches[1]
@@ -72,20 +69,12 @@ jobs:
$ver = "0.0.0" $ver = "0.0.0"
} }
} }
# Numeric MSI version: 4 digits required (e.g. 0.100.1.0)
$verMsi = "$ver.0" $verMsi = "$ver.0"
# Short SHA for the display version
$sha = git rev-parse --short HEAD 2>$null $sha = git rev-parse --short HEAD 2>$null
if (-not $sha) { $sha = "unknown" } if (-not $sha) { $sha = "unknown" }
# Cumulative revision number (same calculation as windows-build.yml)
$count = git rev-list HEAD --count 2>$null $count = git rev-list HEAD --count 2>$null
$rev = [int]$count + 473 $rev = [int]$count + 473
$verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64" $verDisplay = "${ver}-r${rev}-${sha}_x86_64-win64"
echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT echo "VERSION_MSI=$verMsi" >> $env:GITHUB_OUTPUT
echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT echo "VERSION_DISPLAY=$verDisplay" >> $env:GITHUB_OUTPUT
Write-Host "Version MSI : $verMsi" Write-Host "Version MSI : $verMsi"
@@ -93,21 +82,15 @@ jobs:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 4. Install WiX v7, accept EULA and install WixUI extension # 4. Install WiX v7, accept EULA and install WixUI extension
# All done in one step: PATH is updated within the same step
# so wix is immediately available for eula and extension commands
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Install WiX v7 - name: Install WiX v7
shell: pwsh shell: pwsh
run: | run: |
dotnet tool install --global wix --version 7.0.0 dotnet tool install --global wix --version 7.0.0
# Update PATH immediately for the rest of this step
$toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools') $toolsPath = [System.IO.Path]::Combine($env:USERPROFILE, '.dotnet', 'tools')
$env:PATH = "$toolsPath;$env:PATH" $env:PATH = "$toolsPath;$env:PATH"
# Also export for subsequent steps
echo $toolsPath >> $env:GITHUB_PATH echo $toolsPath >> $env:GITHUB_PATH
# Accept OSMF EULA (official CI/CD method: writes a sentinel file)
wix eula accept wix7 wix eula accept wix7
# Install WixUI extension
wix extension add WixToolset.UI.wixext/7.0.0 wix extension add WixToolset.UI.wixext/7.0.0
Write-Host "WiX v7 installed, EULA accepted, UI extension added." Write-Host "WiX v7 installed, EULA accepted, UI extension added."
@@ -120,8 +103,6 @@ jobs:
$wxs = "build-aux\windows\QElectroTech.wxs" $wxs = "build-aux\windows\QElectroTech.wxs"
if (-not (Test-Path $wxs)) { if (-not (Test-Path $wxs)) {
Write-Error "WXS file not found: $wxs" Write-Error "WXS file not found: $wxs"
Write-Host "Contents of build-aux\windows\ :"
Get-ChildItem "build-aux\windows\" -ErrorAction SilentlyContinue
exit 1 exit 1
} }
Write-Host "WXS found: $wxs" Write-Host "WXS found: $wxs"
@@ -136,10 +117,8 @@ jobs:
Get-ChildItem -Path "artifact\files" -Depth 2 | Get-ChildItem -Path "artifact\files" -Depth 2 |
Select-Object FullName | Format-Table -AutoSize Select-Object FullName | Format-Table -AutoSize
# Search for qelectrotech.exe in the artifact
$exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1 $exe = Get-ChildItem -Path "artifact\files" -Filter "qelectrotech.exe" -Recurse | Select-Object -First 1
if (-not $exe) { if (-not $exe) {
# Also try QElectroTech.exe (capital Q)
$exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1 $exe = Get-ChildItem -Path "artifact\files" -Filter "QElectroTech.exe" -Recurse | Select-Object -First 1
} }
if (-not $exe) { if (-not $exe) {
@@ -147,69 +126,58 @@ jobs:
exit 1 exit 1
} }
Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)" Write-Host "Executable: $($exe.FullName) ($([math]::Round($exe.Length/1MB,1)) MB)"
# FilesDir = folder containing bin\
$binDir = $exe.Directory.FullName $binDir = $exe.Directory.FullName
$filesDir = Split-Path $binDir -Parent $filesDir = Split-Path $binDir -Parent
echo "FILES_DIR=$filesDir" >> $env:GITHUB_ENV echo "FILES_DIR=$filesDir" >> $env:GITHUB_ENV
Write-Host "FILES_DIR: $filesDir" Write-Host "FILES_DIR: $filesDir"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 7. Convert LICENSE (GPL-2) to RTF for the WixUI licence screen # 7. Convert LICENSE (GPL-2) to RTF for the WixUI licence screen
# RTF is the only format accepted by Windows Installer.
# The conversion wraps plain text lines in basic RTF markup.
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Convert LICENSE to RTF - name: Convert LICENSE to RTF
shell: pwsh shell: pwsh
run: | run: |
$licSrc = "LICENSE" $licSrc = "LICENSE"
$licRtf = "$env:TEMP\License.rtf" $licRtf = "$env:TEMP\License.rtf"
if (-not (Test-Path $licSrc)) { if (-not (Test-Path $licSrc)) {
Write-Error "LICENSE file not found in repository root" Write-Error "LICENSE file not found in repository root"
exit 1 exit 1
} }
$lines = Get-Content $licSrc -Encoding UTF8 $lines = Get-Content $licSrc -Encoding UTF8
# RTF header — Courier New, 9pt, black
$rtf = New-Object System.Text.StringBuilder $rtf = New-Object System.Text.StringBuilder
[void]$rtf.AppendLine('{\rtf1\ansi\ansicpg1252\deff0') [void]$rtf.AppendLine('{\rtf1\ansi\ansicpg1252\deff0')
[void]$rtf.AppendLine('{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}') [void]$rtf.AppendLine('{\fonttbl{\f0\fmodern\fprq1\fcharset0 Courier New;}}')
[void]$rtf.AppendLine('{\colortbl;\red0\green0\blue0;}') [void]$rtf.AppendLine('{\colortbl;\red0\green0\blue0;}')
[void]$rtf.AppendLine('\f0\fs18\cf1') [void]$rtf.AppendLine('\f0\fs18\cf1')
foreach ($line in $lines) { foreach ($line in $lines) {
# Escape RTF special characters
$escaped = $line ` $escaped = $line `
-replace '\\', '\\\\' ` -replace '\\', '\\\\' `
-replace '\{', '\{' ` -replace '\{', '\{' `
-replace '\}', '\}' -replace '\}', '\}'
[void]$rtf.AppendLine("$escaped\par") [void]$rtf.AppendLine("$escaped\par")
} }
[void]$rtf.AppendLine('}') [void]$rtf.AppendLine('}')
[System.IO.File]::WriteAllText($licRtf, $rtf.ToString(), [System.Text.Encoding]::ASCII) [System.IO.File]::WriteAllText($licRtf, $rtf.ToString(), [System.Text.Encoding]::ASCII)
echo "LICENSE_RTF=$licRtf" >> $env:GITHUB_ENV echo "LICENSE_RTF=$licRtf" >> $env:GITHUB_ENV
Write-Host "License.rtf generated: $licRtf ($([math]::Round((Get-Item $licRtf).Length/1KB,1)) KB)" Write-Host "License.rtf generated: $licRtf ($([math]::Round((Get-Item $licRtf).Length/1KB,1)) KB)"
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 8. Replace Lancer QET.bat with the MSI-specific version # 8. Remove Lancer QET.bat from the artifact
# The portable version uses relative paths suited for the zip. # The MSI does not use the .bat: shortcuts point directly to
# The MSI version uses %~dp0 to resolve paths relative to # qelectrotech.exe, and elements\ is set read-only via a
# the installation directory in Program Files. # CustomAction in QElectroTech.wxs.
# The .bat is kept as-is in the ZIP portable build.
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Replace Lancer QET.bat for MSI - name: Remove Lancer QET.bat from artifact
shell: pwsh shell: pwsh
run: | run: |
$bat = "$env:FILES_DIR\Lancer QET.bat" $bat = "$env:FILES_DIR\Lancer QET.bat"
$content = "@echo off`r`nstart `"`" `"%~dp0bin\qelectrotech.exe`" --common-elements-dir=`"%~dp0elements/`" --common-tbt-dir=`"%~dp0titleblocks/`" --lang-dir=`"%~dp0lang/`" -style windowsvista`r`n" if (Test-Path $bat) {
[System.IO.File]::WriteAllText($bat, $content, [System.Text.Encoding]::ASCII) Remove-Item $bat -Force
Write-Host "Lancer QET.bat replaced for MSI installation." Write-Host "Lancer QET.bat removed from artifact (MSI uses direct exe shortcut)."
Write-Host "=== Content of new Lancer QET.bat ===" } else {
Get-Content $bat Write-Host "Lancer QET.bat not found in artifact (already absent)."
}
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 9. Build the MSI # 9. Build the MSI
@@ -246,7 +214,6 @@ jobs:
Write-Error "MSI not generated: dist\$outputName" Write-Error "MSI not generated: dist\$outputName"
exit 1 exit 1
} }
$size = [math]::Round((Get-Item "dist\$outputName").Length / 1MB, 1) $size = [math]::Round((Get-Item "dist\$outputName").Length / 1MB, 1)
Write-Host "MSI generated: dist\$outputName ($size MB) ✓" Write-Host "MSI generated: dist\$outputName ($size MB) ✓"
echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV echo "MSI_NAME=$outputName" >> $env:GITHUB_ENV
@@ -308,7 +275,6 @@ jobs:
# ---------------------------------------------------------------- # ----------------------------------------------------------------
# 13. Generate and deploy the GitHub Pages download page # 13. Generate and deploy the GitHub Pages download page
# Toutes les URLs sont connues ici (exe, zip, msi).
# ---------------------------------------------------------------- # ----------------------------------------------------------------
- name: Checkout (for generate-page.py) - name: Checkout (for generate-page.py)
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -325,29 +291,22 @@ jobs:
run: | run: |
set -euo pipefail set -euo pipefail
REPO="${{ github.repository }}" REPO="${{ github.repository }}"
# Fetch asset names from the nightly release (source of truth)
ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name') ASSETS=$(gh release view nightly --repo "$REPO" --json assets --jq '.assets[].name')
EXE_NAME=$(echo "$ASSETS" | grep '\.exe$' | head -1) EXE_NAME=$(echo "$ASSETS" | grep '\.exe$' | head -1)
ZIP_NAME=$(echo "$ASSETS" | grep '\.zip$' | head -1) ZIP_NAME=$(echo "$ASSETS" | grep '\.zip$' | head -1)
MSI_NAME=$(echo "$ASSETS" | grep '\.msi$' | head -1 || echo "") MSI_NAME=$(echo "$ASSETS" | grep '\.msi$' | head -1 || echo "")
BASE="https://github.com/$REPO/releases/download/nightly" BASE="https://github.com/$REPO/releases/download/nightly"
INSTALLER_URL="$BASE/$EXE_NAME" INSTALLER_URL="$BASE/$EXE_NAME"
PORTABLE_URL="$BASE/$ZIP_NAME" PORTABLE_URL="$BASE/$ZIP_NAME"
MSI_URL="" MSI_URL=""
[ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME" [ -n "$MSI_NAME" ] && MSI_URL="$BASE/$MSI_NAME"
SHA="${{ github.event.workflow_run.head_sha || github.sha }}" SHA="${{ github.event.workflow_run.head_sha || github.sha }}"
SHORT="${SHA:0:7}" SHORT="${SHA:0:7}"
DATE=$(date -u '+%Y-%m-%d %H:%M UTC') DATE=$(date -u '+%Y-%m-%d %H:%M UTC')
RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}" RUN_URL="https://github.com/$REPO/actions/runs/${{ github.run_id }}"
RUN_NUMBER="${{ github.run_number }}" RUN_NUMBER="${{ github.run_number }}"
export DATE SHORT REPO SHA RUN_URL RUN_NUMBER export DATE SHORT REPO SHA RUN_URL RUN_NUMBER
export INSTALLER_URL PORTABLE_URL MSI_URL export INSTALLER_URL PORTABLE_URL MSI_URL
python3 source/build-aux/generate-page.py python3 source/build-aux/generate-page.py
- name: Add .nojekyll - name: Add .nojekyll

View File

@@ -37,6 +37,8 @@
<!-- ============================================================ <!-- ============================================================
All application files harvested in one pass from files\** All application files harvested in one pass from files\**
(Lancer QET.bat has been removed from the artifact before
this build — see windows-msi.yml step "Remove Lancer QET.bat")
============================================================ --> ============================================================ -->
<ComponentGroup Id="CG_AllFiles" Directory="INSTALLDIR"> <ComponentGroup Id="CG_AllFiles" Directory="INSTALLDIR">
<Files Include="$(var.FilesDir)\**" /> <Files Include="$(var.FilesDir)\**" />
@@ -44,13 +46,17 @@
<!-- ============================================================ <!-- ============================================================
Desktop + Start Menu shortcuts Desktop + Start Menu shortcuts
Point directly to qelectrotech.exe with all required arguments.
No .bat wrapper needed.
============================================================ --> ============================================================ -->
<ComponentGroup Id="CG_Shortcuts" Directory="INSTALLDIR"> <ComponentGroup Id="CG_Shortcuts" Directory="INSTALLDIR">
<Component Id="C_ShortcutDesktop" Guid="F1A2B3C4-D5E6-7890-5678-012345678901"> <Component Id="C_ShortcutDesktop" Guid="F1A2B3C4-D5E6-7890-5678-012345678901">
<Shortcut Id="DesktopShortcut" <Shortcut Id="DesktopShortcut"
Directory="DesktopFolder" Directory="DesktopFolder"
Name="QElectroTech" Name="QElectroTech"
Target="[INSTALLDIR]Lancer QET.bat" Target="[INSTALLDIR]bin\qelectrotech.exe"
Arguments="--common-elements-dir=&quot;[INSTALLDIR]elements/&quot; --common-tbt-dir=&quot;[INSTALLDIR]titleblocks/&quot; --lang-dir=&quot;[INSTALLDIR]lang/&quot; -style windowsvista"
Icon="qet.ico" Icon="qet.ico"
WorkingDirectory="INSTALLDIR" /> WorkingDirectory="INSTALLDIR" />
<RegistryValue Root="HKCU" <RegistryValue Root="HKCU"
@@ -59,11 +65,13 @@
Type="integer" Value="1" Type="integer" Value="1"
KeyPath="yes" /> KeyPath="yes" />
</Component> </Component>
<Component Id="C_ShortcutStartMenu" Guid="A2B3C4D5-E6F7-8901-6789-123456789012"> <Component Id="C_ShortcutStartMenu" Guid="A2B3C4D5-E6F7-8901-6789-123456789012">
<Shortcut Id="StartMenuShortcut" <Shortcut Id="StartMenuShortcut"
Directory="ProgramMenuFolder" Directory="ProgramMenuFolder"
Name="QElectroTech" Name="QElectroTech"
Target="[INSTALLDIR]Lancer QET.bat" Target="[INSTALLDIR]bin\qelectrotech.exe"
Arguments="--common-elements-dir=&quot;[INSTALLDIR]elements/&quot; --common-tbt-dir=&quot;[INSTALLDIR]titleblocks/&quot; --lang-dir=&quot;[INSTALLDIR]lang/&quot; -style windowsvista"
Icon="qet.ico" Icon="qet.ico"
WorkingDirectory="INSTALLDIR" /> WorkingDirectory="INSTALLDIR" />
<RegistryValue Root="HKCU" <RegistryValue Root="HKCU"
@@ -72,6 +80,7 @@
Type="integer" Value="1" Type="integer" Value="1"
KeyPath="yes" /> KeyPath="yes" />
</Component> </Component>
</ComponentGroup> </ComponentGroup>
<!-- ============================================================ <!-- ============================================================
@@ -93,6 +102,27 @@
============================================================ --> ============================================================ -->
<Icon Id="qet.ico" SourceFile="$(var.FilesDir)\ico\qelectrotech.ico" /> <Icon Id="qet.ico" SourceFile="$(var.FilesDir)\ico\qelectrotech.ico" />
<!-- ============================================================
CustomAction: set elements\ subtree read-only after install
Runs an inline PowerShell script via WiX's QtExec pattern.
Executes after all files are committed to disk (afterInstallFinalize).
============================================================ -->
<Property Id="SetElementsReadOnly"
Value="powershell.exe -NonInteractive -NoProfile -WindowStyle Hidden -Command &quot;Get-ChildItem -Path '[INSTALLDIR]elements' -Recurse -File | ForEach-Object { $_.IsReadOnly = $true }&quot;" />
<CustomAction Id="CA_SetElementsReadOnly"
Property="SetElementsReadOnly"
ExeCommand=""
Execute="deferred"
Impersonate="no"
Return="ignore" />
<InstallExecuteSequence>
<Custom Action="CA_SetElementsReadOnly" After="InstallFiles">
NOT Installed AND NOT REMOVE
</Custom>
</InstallExecuteSequence>
<!-- ============================================================ <!-- ============================================================
Main feature (everything included) Main feature (everything included)
============================================================ --> ============================================================ -->