Skip to content

Harden GStreamer release asset downloads #309

Harden GStreamer release asset downloads

Harden GStreamer release asset downloads #309

Workflow file for this run

name: Build and Release
on:
push:
tags:
- 'v*'
workflow_dispatch:
permissions:
contents: write
jobs:
build-windows:
runs-on: windows-latest
env:
GSTREAMER_RELEASE_TAG: gstreamer-1.26.10
GSTREAMER_VERSION: 1.26.10
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Set up MinGW
uses: egor-tensin/setup-mingw@v2
with:
version: 12.2.0
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: '6.4.2'
host: 'windows'
target: 'desktop'
arch: 'win64_mingw'
modules: 'qtmultimedia qtwebsockets'
- name: Install OpenSSL 1.1 runtime for Qt TLS
shell: pwsh
run: |
$ErrorActionPreference = "Continue"
function Install-ChocoPackage([string] $package, [string] $version) {
Write-Host "Installing $package $version"
choco install $package --version=$version --allow-downgrade --no-progress -y
if ($LASTEXITCODE -ne 0) {
Write-Warning "$package $version failed with exit code $LASTEXITCODE"
return $false
}
return $true
}
function Find-OpenSsl11Bin {
$directDirs = @(
"C:\Program Files\OpenSSL-Win64\bin",
"C:\Program Files\OpenSSL\bin",
"C:\OpenSSL-Win64\bin",
"C:\OpenSSL\bin"
) | Where-Object { $_ -and (Test-Path $_) }
foreach ($dir in $directDirs) {
if ((Test-Path (Join-Path $dir "libssl-1_1-x64.dll")) -and
(Test-Path (Join-Path $dir "libcrypto-1_1-x64.dll"))) {
return $dir
}
}
$roots = @(
"C:\Program Files",
"C:\ProgramData\chocolatey",
"C:\tools"
) | Where-Object { $_ -and (Test-Path $_) }
foreach ($root in $roots) {
$sslCandidates = Get-ChildItem -Path $root -Recurse -Filter "libssl-1_1-x64.dll" -ErrorAction SilentlyContinue
foreach ($sslCandidate in $sslCandidates) {
$dir = $sslCandidate.DirectoryName
if (Test-Path (Join-Path $dir "libcrypto-1_1-x64.dll")) {
return $dir
}
}
}
return $null
}
[void](Install-ChocoPackage "openssl" "1.1.1.2000")
$opensslBin = Find-OpenSsl11Bin
if (-not $opensslBin) {
[void](Install-ChocoPackage "openssl.light" "1.1.1")
$opensslBin = Find-OpenSsl11Bin
}
if (-not $opensslBin) {
Write-Error "OpenSSL 1.1 runtime DLL pair was not found after Chocolatey install."
Get-ChildItem -Path "C:\Program Files", "C:\ProgramData\chocolatey", "C:\tools" -Recurse -Filter "libssl*.dll" -ErrorAction SilentlyContinue |
Select-Object FullName -First 80 |
Out-String |
Write-Host
exit 1
}
Write-Host "OpenSSL 1.1 runtime found at: $opensslBin"
"OPENIPC_OPENSSL11_BIN=$opensslBin" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Download GStreamer (MinGW) from GitHub Release
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$msiRuntime = "gstreamer-1.0-mingw-x86_64-$env:GSTREAMER_VERSION.msi"
$msiDevel = "gstreamer-1.0-devel-mingw-x86_64-$env:GSTREAMER_VERSION.msi"
$assets = @($msiRuntime, $msiDevel)
foreach ($asset in $assets) {
$url = "https://github.com/OpenIPC/dashboard/releases/download/$env:GSTREAMER_RELEASE_TAG/$asset"
$partial = "$asset.part"
$downloaded = $false
for ($attempt = 1; $attempt -le 5; $attempt++) {
Remove-Item -LiteralPath $partial -Force -ErrorAction SilentlyContinue
Write-Host "Downloading $asset (attempt $attempt of 5)"
& curl.exe --fail --location --silent --show-error `
--connect-timeout 30 --retry 2 --retry-delay 5 --retry-all-errors `
--output $partial $url
if ($LASTEXITCODE -eq 0 -and (Test-Path $partial) `
-and (Get-Item $partial).Length -gt 100MB) {
Move-Item -LiteralPath $partial -Destination $asset -Force
$downloaded = $true
break
}
Remove-Item -LiteralPath $partial -Force -ErrorAction SilentlyContinue
if ($attempt -lt 5) {
$delay = 15 * $attempt
Write-Warning "Download failed or produced an incomplete MSI; retrying in $delay seconds."
Start-Sleep -Seconds $delay
}
}
if (-not $downloaded) {
throw "Failed to download a complete GStreamer asset after 5 attempts: $asset"
}
}
Get-Item -LiteralPath $msiRuntime, $msiDevel |
Select-Object Name, Length |
Format-Table -AutoSize
# Extract directly into the expected MinGW root to avoid nested paths
$gstRoot = "C:\gstreamer\1.0\mingw_x86_64"
New-Item -ItemType Directory -Force -Path $gstRoot | Out-Null
Write-Host "Extracting Runtime (administrative install): $msiRuntime"
Start-Process msiexec.exe -ArgumentList '/a', $msiRuntime, '/qn', "TARGETDIR=$gstRoot" -NoNewWindow -Wait
Write-Host "Extracting Devel (administrative install): $msiDevel"
Start-Process msiexec.exe -ArgumentList '/a', $msiDevel, '/qn', "TARGETDIR=$gstRoot" -NoNewWindow -Wait
# Validate expected header exists
$gstHeaderPath = "$gstRoot\include\gstreamer-1.0\gst\gst.h"
if (-not (Test-Path $gstHeaderPath)) {
Write-Host "WARNING: Expected gst.h not found at $gstHeaderPath. Searching C:\gstreamer..."
if (-not (Test-Path "C:\gstreamer")) {
Write-Error "CRITICAL: C:\gstreamer directory does not exist after extraction!"
exit 1
}
$gstHeader = Get-ChildItem -Recurse "C:\gstreamer" -Filter "gst.h" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "gstreamer-1.0\\gst\\gst\.h" } |
Select-Object -First 1
if ($gstHeader) {
# gst.h is in <root>\include\gstreamer-1.0\gst\gst.h
# We need <root>, so go up 4 levels from gst.h
$gstRoot = Split-Path (Split-Path (Split-Path (Split-Path $gstHeader.FullName -Parent) -Parent) -Parent) -Parent
} else {
Write-Error "Could not determine GStreamer installation root."
Get-ChildItem -Recurse "C:\gstreamer" | Select-Object FullName -First 120
exit 1
}
}
if ($gstRoot -match "\\include$") {
$gstRoot = Split-Path $gstRoot -Parent
}
Write-Host "GStreamer installed at: $gstRoot"
echo "GSTREAMER_1_0_ROOT_MINGW_X86_64=$gstRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "OPENSSL_ROOT_DIR=$gstRoot" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "$gstRoot\bin" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
echo "PKG_CONFIG_PATH=$gstRoot\lib\pkgconfig" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
echo "Debug: Listing GStreamer Includes"
Get-ChildItem -Recurse "$gstRoot\include" | Select-Object FullName -First 20
if (-not (Test-Path "$gstRoot\include\openssl\ssl.h")) {
Write-Error "OpenSSL headers not found in GStreamer devel package at $gstRoot\include\openssl\ssl.h"
exit 1
}
if (-not (Test-Path "$gstRoot\lib\libssl.dll.a") -or -not (Test-Path "$gstRoot\lib\libcrypto.dll.a")) {
Write-Error "OpenSSL import libraries not found in GStreamer devel package at $gstRoot\lib"
exit 1
}
- name: Configure CMake
run: |
$qmake = (Get-Command qmake.exe -ErrorAction SilentlyContinue).Source
if (-not $qmake) { $qmake = (Get-Command qmake -ErrorAction SilentlyContinue).Source }
if (-not $qmake) { Write-Error "qmake not found in PATH"; exit 1 }
$qtDir = Split-Path (Split-Path $qmake -Parent) -Parent
$appVersion = "${{ github.ref_name }}"
if ($appVersion.StartsWith('v')) { $appVersion = $appVersion.Substring(1) }
$gstRoot = $env:GSTREAMER_1_0_ROOT_MINGW_X86_64
if (-not $gstRoot -or -not (Test-Path "$gstRoot\include\gstreamer-1.0\gst\gst.h")) {
Write-Error "GStreamer headers not found at configured root: $gstRoot"
exit 1
}
$gstCmakeRoot = $gstRoot.Replace('\', '/')
$opensslRoot = $env:OPENSSL_ROOT_DIR
if (-not $opensslRoot) { $opensslRoot = $gstRoot }
$opensslCmakeRoot = $opensslRoot.Replace('\', '/')
cmake -B build -G "MinGW Makefiles" -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DCMAKE_PREFIX_PATH="$qtDir" -DGStreamer_ROOT="$gstCmakeRoot" -DOPENSSL_ROOT_DIR="$opensslCmakeRoot" -DAPP_VERSION="$appVersion"
- name: Build
run: cmake --build build --config Release
- name: Upload CMake logs (Windows)
if: failure()
uses: actions/upload-artifact@v7
with:
name: windows-cmake-logs
path: |
build/CMakeFiles/CMakeOutput.log
build/CMakeFiles/CMakeError.log
build/CMakeCache.txt
build/compile_commands.json
- name: Prepare Distribution Folder
shell: pwsh
run: |
if (Test-Path dist) { Remove-Item -Recurse -Force dist }
New-Item -ItemType Directory -Force -Path dist
Copy-Item build/appOpenIPC-Dashboard.exe -Destination dist/
# Copy DLLs (Dahua, ONNX code in CMake might put them in build/)
if (Test-Path build/*.dll) {
Copy-Item build/*.dll -Destination dist/ -Force
}
# Resolve GStreamer root from env (set in install step), with fallback search
$gstRoot = $env:GSTREAMER_1_0_ROOT_MINGW_X86_64
if (-not $gstRoot -or -not (Test-Path $gstRoot)) {
$gstHeader = Get-ChildItem -Recurse "C:\gstreamer" -Filter "gst.h" -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -match "gstreamer-1.0\\gst\\gst\.h" } |
Select-Object -First 1
if ($gstHeader) {
$gstRoot = Split-Path (Split-Path (Split-Path (Split-Path $gstHeader.FullName -Parent) -Parent) -Parent) -Parent
}
}
if (-not $gstRoot -or -not (Test-Path $gstRoot)) {
Write-Error "GStreamer root not found. GSTREAMER_1_0_ROOT_MINGW_X86_64=$($env:GSTREAMER_1_0_ROOT_MINGW_X86_64)"
exit 1
}
$gstBin = Join-Path $gstRoot "bin"
if (-not (Test-Path $gstBin)) {
Write-Error "GStreamer bin not found at: $gstBin"
exit 1
}
# Ensure critical GStreamer/GLib runtime DLLs are present
$requiredDlls = @(
"libgstreamer-1.0-0.dll",
"libgstbase-1.0-0.dll",
"libgstaudio-1.0-0.dll",
"libgstvideo-1.0-0.dll",
"libglib-2.0-0.dll",
"libgobject-2.0-0.dll",
"libgio-2.0-0.dll"
)
foreach ($dll in $requiredDlls) {
if (Test-Path "$gstBin\$dll") {
Copy-Item "$gstBin\$dll" -Destination dist/ -Force
}
}
# Copy additional core runtime libs (exclude d3d11/dxva/mf)
$extraDlls = @(
"libgstpbutils-1.0-0.dll",
"libgsttag-1.0-0.dll",
"libgstcontroller-1.0-0.dll",
"libgstnet-1.0-0.dll",
"libgstapp-1.0-0.dll",
"libgstrtp-1.0-0.dll",
"libgstrtsp-1.0-0.dll",
"libgstsdp-1.0-0.dll",
"libgstwebrtc-1.0-0.dll",
"libgstwebrtcnice-1.0-0.dll",
"libnice-10.dll",
"libsrtp2-1.dll",
"libgstcodecparsers-1.0-0.dll",
"libgstcodecs-1.0-0.dll",
"libgstallocators-1.0-0.dll",
"libgstriff-1.0-0.dll",
"liborc-0.4-0.dll",
"libintl-8.dll",
"libiconv-2.dll",
"libffi-7.dll",
"libz-1.dll",
"libbz2.dll",
"libxml2-2.dll",
"libsqlite3-0.dll",
"libpcre2-8-0.dll",
"libssl-3-x64.dll",
"libcrypto-3-x64.dll",
"libavcodec-61.dll",
"libavformat-61.dll",
"libavutil-59.dll",
"libavfilter-10.dll",
"libswresample-5.dll",
"libswscale-8.dll",
"libjpeg-8.dll",
"libpng16.dll",
"libfreetype-6.dll",
"libfontconfig-1.dll",
"libharfbuzz-0.dll",
"libgraphene-1.0-0.dll",
"libpango-1.0-0.dll",
"libpangocairo-1.0-0.dll",
"libpangowin32-1.0-0.dll",
"libcairo-2.dll",
"libcairo-gobject-2.dll"
)
foreach ($dll in $extraDlls) {
if (Test-Path "$gstBin\$dll") {
Copy-Item "$gstBin\$dll" -Destination dist/ -Force
}
}
$missing = @()
foreach ($dll in $requiredDlls) {
if (-not (Test-Path "dist\$dll")) { $missing += $dll }
}
if ($missing.Count -gt 0) {
Write-Error "Missing required GStreamer DLLs in dist: $($missing -join ', ')"
Get-ChildItem dist | Select-Object Name | Out-String | Write-Host
exit 1
}
# Copy Plugins (minimal set)
$gstPlugins = Join-Path $gstRoot "lib\gstreamer-1.0"
New-Item -ItemType Directory -Path dist/lib/gstreamer-1.0 -Force
if (Test-Path $gstPlugins) {
$pluginList = @(
"libgstcoreelements.dll",
"libgstplayback.dll",
"libgsttypefindfunctions.dll",
"libgstapp.dll",
"libgstaudioconvert.dll",
"libgstaudioresample.dll",
"libgstaudioparsers.dll",
"libgstaacparse.dll",
"libgstfaad.dll",
"libgstvolume.dll",
"libgstautodetect.dll",
"libgstvideoconvert.dll",
"libgstvideoconvertscale.dll",
"libgstvideoscale.dll",
"libgstvideofilters.dll",
"libgstvideofilter.dll",
"libgstvideorate.dll",
"libgstvideoparsersbad.dll",
"libgstdeinterlace.dll",
"libgstrtp.dll",
"libgstrtpmanager.dll",
"libgstrtsp.dll",
"libgstudp.dll",
"libgsttcp.dll",
"libgstisomp4.dll",
"libgstmatroska.dll",
"libgstmpegtsdemux.dll",
"libgstde265.dll",
"libgstopenh264.dll",
"libgstwebrtc.dll",
"libgstnice.dll",
"libgstdtls.dll",
"libgstsrtp.dll",
"libgstlibav.dll",
"libgstwasapi.dll",
"libgstdirectsound.dll"
)
foreach ($p in $pluginList) {
if (Test-Path "$gstPlugins\$p") {
Copy-Item "$gstPlugins\$p" -Destination dist/lib/gstreamer-1.0 -Force
}
}
}
# Copy Scanner (Check libexec)
$gstLibexec = Join-Path $gstRoot "libexec\gstreamer-1.0"
$scannerCandidates = @(
"$gstLibexec\gst-plugin-scanner.exe",
"$gstRoot\bin\gst-plugin-scanner.exe",
"$gstRoot\lib\gstreamer-1.0\gst-plugin-scanner.exe"
)
$scannerCopied = $false
foreach ($scanner in $scannerCandidates) {
if (Test-Path $scanner) {
Copy-Item $scanner -Destination dist/lib/gstreamer-1.0 -Force
$scannerCopied = $true
break
}
}
if (-not $scannerCopied) {
Write-Warning "gst-plugin-scanner.exe not found in GStreamer root"
}
# Copy OpenIPC QML module (Custom module)
if (Test-Path build/OpenIPC) {
Copy-Item build/OpenIPC -Destination dist/ -Recurse -Force
}
# Run windeployqt to deploy standard Qt dependencies (including Qt.labs.folderlistmodel)
# We run this on the final dist folder to ensure everything is self-contained
$windeployqt = (Get-Command windeployqt.exe -ErrorAction SilentlyContinue).Source
if (-not $windeployqt) { $windeployqt = (Get-Command windeployqt -ErrorAction SilentlyContinue).Source }
if (-not $windeployqt) { Write-Error "windeployqt not found in PATH"; exit 1 }
& $windeployqt --qmldir src/ui --dir dist dist/appOpenIPC-Dashboard.exe
# Ensure Qt platform plugins are bundled (qwindows.dll)
$qtBin = Split-Path $windeployqt -Parent
$qtRoot = Split-Path $qtBin -Parent
$qtPlatforms = Join-Path $qtRoot "plugins\platforms"
if (Test-Path $qtPlatforms) {
New-Item -ItemType Directory -Force -Path dist/qt_plugins/platforms | Out-Null
Copy-Item "$qtPlatforms\*" -Destination dist/qt_plugins/platforms -Force
} else {
Write-Warning "Qt platforms path not found: $qtPlatforms"
}
# Move any Qt plugins generated by windeployqt to qt_plugins to avoid VLC conflict
if (Test-Path dist/plugins/platforms) {
if (Test-Path dist/qt_plugins) { Remove-Item -Recurse -Force dist/qt_plugins }
Move-Item dist/plugins dist/qt_plugins
}
# Ensure qt.conf points to correct plugin/qml folders
@"
[Paths]
Prefix=.
Plugins=qt_plugins
Imports=qml
Qml2Imports=qml
"@ | Set-Content dist/qt.conf
# Qt 6.4 MinGW TLS backend uses OpenSSL 1.1 at runtime.
# GStreamer bundles OpenSSL 3 for its own stack, but Qt HTTPS/WebSocket
# updater needs libssl-1_1-x64.dll + libcrypto-1_1-x64.dll next to the exe.
$opensslDllPairs = @(
@("libssl-1_1-x64.dll", "libcrypto-1_1-x64.dll"),
@("libssl-3-x64.dll", "libcrypto-3-x64.dll")
)
$pathSearchDirs = ($env:PATH -split [System.IO.Path]::PathSeparator) |
Where-Object { $_ -and (Test-Path $_) }
$qtToolSearchDirs = @()
if ($env:IQTA_TOOLS -and (Test-Path $env:IQTA_TOOLS)) {
$qtToolSearchDirs = Get-ChildItem -Path $env:IQTA_TOOLS -Directory -Recurse -ErrorAction SilentlyContinue |
Where-Object { $_.Name -ieq "bin" } |
Select-Object -ExpandProperty FullName
}
$opensslSearchDirs = (@(
$env:OPENIPC_OPENSSL11_BIN,
$qtBin,
(Join-Path $qtRoot "bin"),
(Join-Path $env:OPENSSL_ROOT_DIR "bin"),
(Join-Path $gstRoot "bin"),
"C:\Program Files\OpenSSL-Win64\bin",
"C:\Program Files\OpenSSL\bin",
"C:\Program Files\OpenSSL-Win64\bin\VC\x64\MD",
"C:\OpenSSL-Win64\bin",
"C:\OpenSSL\bin",
"C:\Windows\System32",
"C:\Program Files\Git\mingw64\bin",
"C:\msys64\mingw64\bin"
) + $pathSearchDirs + $qtToolSearchDirs) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
function Find-OpenSslPairByName([string[]] $pair) {
$roots = @(
$env:OPENIPC_OPENSSL11_BIN,
$env:IQTA_TOOLS,
$env:QT_ROOT_DIR,
$env:RUNNER_TOOL_CACHE,
"C:\Program Files",
"C:\ProgramData\chocolatey",
"C:\Qt",
$gstRoot
) | Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
foreach ($root in $roots) {
$sslCandidates = Get-ChildItem -Path $root -Recurse -Filter $pair[0] -ErrorAction SilentlyContinue
foreach ($sslCandidate in $sslCandidates) {
$dir = $sslCandidate.DirectoryName
$cryptoPath = Join-Path $dir $pair[1]
if (Test-Path $cryptoPath) {
return $dir
}
}
}
return $null
}
function Copy-OpenSslPair([string[]] $pair) {
foreach ($dir in $opensslSearchDirs) {
$sslPath = Join-Path $dir $pair[0]
$cryptoPath = Join-Path $dir $pair[1]
if ((Test-Path $sslPath) -and (Test-Path $cryptoPath)) {
Copy-Item $sslPath -Destination dist/ -Force
Copy-Item $cryptoPath -Destination dist/ -Force
Write-Host "Copied OpenSSL runtime pair from ${dir}: $($pair -join ', ')"
return $true
}
}
$fallbackDir = Find-OpenSslPairByName $pair
if ($fallbackDir) {
Copy-Item (Join-Path $fallbackDir $pair[0]) -Destination dist/ -Force
Copy-Item (Join-Path $fallbackDir $pair[1]) -Destination dist/ -Force
Write-Host "Copied OpenSSL runtime pair from ${fallbackDir}: $($pair -join ', ')"
return $true
}
return $false
}
$openssl11Copied = Copy-OpenSslPair $opensslDllPairs[0]
if (-not $openssl11Copied) {
Write-Error "OpenSSL 1.1 runtime DLL pair was not found. Qt TLS/updater would fail on clean machines."
Write-Host "Searched directories:"
$opensslSearchDirs | ForEach-Object { Write-Host " - $_" }
exit 1
}
[void](Copy-OpenSslPair $opensslDllPairs[1])
$distPath = (Resolve-Path dist).Path
$env:PATH = $distPath + ";" + $env:PATH
$tlsTest = Start-Process -FilePath (Join-Path $distPath "appOpenIPC-Dashboard.exe") `
-ArgumentList "--self-test-tls" `
-WorkingDirectory $distPath `
-Wait `
-PassThru `
-WindowStyle Hidden
if ($tlsTest.ExitCode -ne 0) {
Write-Error "TLS self-test failed in deployed dist. OpenSSL runtime is not loadable by Qt."
exit $tlsTest.ExitCode
}
# Remove debug symbols to reduce size
Get-ChildItem dist -Recurse -Filter "*.pdb" -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue
- name: Install UPX (optional)
shell: pwsh
run: |
$ErrorActionPreference = "Continue"
choco install upx --no-progress -y
$upx = (Get-Command upx.exe -ErrorAction SilentlyContinue).Source
if (-not $upx) {
$upx = Get-ChildItem "C:\ProgramData\chocolatey" -Recurse -Filter "upx.exe" -ErrorAction SilentlyContinue |
Select-Object -ExpandProperty FullName -First 1
}
if (-not $upx -or -not (Test-Path $upx)) {
Write-Warning "UPX is unavailable; release packaging will continue without optional compression."
"OPENIPC_UPX=" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
exit 0
}
Write-Host "UPX found at: $upx"
"OPENIPC_UPX=$upx" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: Compress binaries with UPX
shell: pwsh
run: |
$upx = $env:OPENIPC_UPX
if (-not $upx -or -not (Test-Path $upx)) {
Write-Warning "UPX is unavailable; skipping optional binary compression."
exit 0
}
# Compress EXE and DLLs in dist
# Avoid compressing Qt plugins/modules and QML imports, which can break Qt's plugin metadata loader.
$compressed = 0
$skipped = 0
Get-ChildItem dist -Recurse -Include *.exe,*.dll |
Where-Object {
$_.FullName -notmatch '\\qt_plugins\\' -and
$_.FullName -notmatch '\\platforms\\' -and
$_.FullName -notmatch '\\plugins\\' -and
$_.FullName -notmatch '\\Qt(Qml|Quick|Multimedia|Network|OpenGL|Sql|Svg|Core|Gui|Labs|QmlModels|QmlWorkerScript|QmlXmlListModel)?\\' -and
$_.FullName -notmatch '\\qml\\'
} |
ForEach-Object {
& $upx -9 --lzma --force $_.FullName
if ($LASTEXITCODE -eq 0) {
$compressed++
} else {
$skipped++
Write-Warning "UPX skipped $($_.FullName) with exit code $LASTEXITCODE"
}
}
Write-Host "UPX compression complete: $compressed compressed, $skipped skipped."
- name: Install Inno Setup
run: choco install innosetup
- name: Update Installer Version
shell: pwsh
run: |
$version = "${{ github.ref_name }}".TrimStart('v')
(Get-Content setup.iss) -replace '#define MyAppVersion ".*"', "#define MyAppVersion ""$version""" | Set-Content setup.iss
- name: Build Installer
run: iscc setup.iss
- name: Upload Installer
uses: actions/upload-artifact@v7
with:
name: windows-installer
path: OpenIPC-Dashboard-Installer.exe
build-linux:
runs-on: ubuntu-22.04
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y build-essential libgl1-mesa-dev pkg-config libfuse2 imagemagick patchelf libwayland-dev libunwind-dev squashfs-tools desktop-file-utils \
libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev \
gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl
- name: Install Qt
uses: jurplel/install-qt-action@v4
with:
version: '6.4.2'
host: 'linux'
target: 'desktop'
modules: 'qtmultimedia qtwebsockets'
- name: Install libxcb dependencies
run: sudo apt-get install -y libxcb-cursor0
- name: Configure CMake
run: cmake -B build -DCMAKE_BUILD_TYPE=Release
- name: Build
run: cmake --build build --config Release
- name: Upload CMake logs (Linux)
if: failure()
uses: actions/upload-artifact@v7
with:
name: linux-cmake-logs
path: |
build/CMakeFiles/CMakeOutput.log
build/CMakeFiles/CMakeError.log
build/CMakeCache.txt
build/compile_commands.json
- name: Helper - Create Desktop File
run: |
echo "[Desktop Entry]" > appOpenIPC-Dashboard.desktop
echo "Type=Application" >> appOpenIPC-Dashboard.desktop
echo "Name=OpenIPC Dashboard" >> appOpenIPC-Dashboard.desktop
echo "Exec=appOpenIPC-Dashboard" >> appOpenIPC-Dashboard.desktop
echo "Icon=openipc-dashboard" >> appOpenIPC-Dashboard.desktop
echo "Categories=Utility;" >> appOpenIPC-Dashboard.desktop
- name: Create AppImage
env:
VERSION: ${{ github.ref_name }}
run: |
set -euo pipefail
exec > >(tee -a create_appimage.log) 2>&1
set -x
: > linuxdeployqt.log
: > appimagetool.log
# Download linuxdeployqt
wget https://github.com/probonopd/linuxdeployqt/releases/download/continuous/linuxdeployqt-continuous-x86_64.AppImage
chmod +x linuxdeployqt-continuous-x86_64.AppImage
./linuxdeployqt-continuous-x86_64.AppImage --appimage-extract
rm -rf linuxdeployqt-root
mv squashfs-root linuxdeployqt-root
# Prepare AppDir
mkdir -p AppDir/usr/bin
cp build/appOpenIPC-Dashboard AppDir/usr/bin/
chmod +x AppDir/usr/bin/appOpenIPC-Dashboard
# Prepare Icon (Use existing PNG and resize to ensure 256x256)
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps/
convert resources/appicon.png -resize 256x256 AppDir/usr/share/icons/hicolor/256x256/apps/openipc-dashboard.png
cp AppDir/usr/share/icons/hicolor/256x256/apps/openipc-dashboard.png AppDir/openipc-dashboard.png
# Copy desktop file
mkdir -p AppDir/usr/share/applications/
cp appOpenIPC-Dashboard.desktop AppDir/usr/share/applications/
cp appOpenIPC-Dashboard.desktop AppDir/appOpenIPC-Dashboard.desktop
# Copy custom libs to standard location expected by AppImage (usr/lib)
mkdir -p AppDir/usr/lib
cp libs/dahua/lib/Linux64/*.so AppDir/usr/lib/
cp libs/onnxruntime/lib/Linux64/*.so* AppDir/usr/lib/
# Copy OpenIPC assets (QML modules, models) - mirror Windows behavior
if [ -d "build/OpenIPC" ]; then
cp -r build/OpenIPC AppDir/usr/bin/
fi
# Copy GStreamer plugins (minimal set to avoid missing dependency warnings)
mkdir -p AppDir/usr/lib/gstreamer-1.0
GST_PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/gstreamer-1.0"
GST_PLUGINS=(
libgstcoreelements.so
libgstplayback.so
libgsttypefindfunctions.so
libgstapp.so
libgstaudioconvert.so
libgstaudioresample.so
libgstaudioparsers.so
libgstaacparse.so
libgstfaad.so
libgstvolume.so
libgstautodetect.so
libgstvideoconvert.so
libgstvideoscale.so
libgstvideorate.so
libgstvideoparsers.so
libgstvideoparsersbad.so
libgstvideofilters.so
libgstvideofilter.so
libgstvideofiltersbad.so
libgstdeinterlace.so
libgstrtp.so
libgstrtpmanager.so
libgstrtsp.so
libgstudp.so
libgsttcp.so
libgstisomp4.so
libgstmatroska.so
libgstmpegtsdemux.so
libgstde265.so
libgstlibav.so
libgstopenh264.so
libgstwebrtc.so
libgstnice.so
libgstdtls.so
libgstsrtp.so
libgstalsa.so
libgstpulseaudio.so
)
for plugin in "${GST_PLUGINS[@]}"; do
if [ -f "$GST_PLUGIN_DIR/$plugin" ]; then
cp "$GST_PLUGIN_DIR/$plugin" AppDir/usr/lib/gstreamer-1.0/
fi
done
# Copy plugin scanner
if [ -f "/usr/lib/x86_64-linux-gnu/gstreamer-1.0/gst-plugin-scanner" ]; then
cp /usr/lib/x86_64-linux-gnu/gstreamer-1.0/gst-plugin-scanner AppDir/usr/lib/gstreamer-1.0/
elif [ -f "/usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner" ]; then
cp /usr/lib/x86_64-linux-gnu/gstreamer1.0/gstreamer-1.0/gst-plugin-scanner AppDir/usr/lib/gstreamer-1.0/
fi
# Copy GStreamer core libraries to avoid host/AppImage version mismatch
cp /usr/lib/x86_64-linux-gnu/libgstreamer-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstbase-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstaudio-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstvideo-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstapp-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstpbutils-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstrtp-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstrtsp-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstsdp-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgsttag-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libgstnet-1.0.so.* AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/liborc-0.4.so.* AppDir/usr/lib/
# Copy GStreamer bad/libav runtime libs needed for AAC/H265 and MP4 playback
for lib in \
/usr/lib/x86_64-linux-gnu/libgstcodecparsers-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstcodecs-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstbadaudio-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstadaptivedemux-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstwebrtc-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstwebrtcnice-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstsctp-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libnice.so.* \
/usr/lib/x86_64-linux-gnu/libsrtp2.so.* \
/usr/lib/x86_64-linux-gnu/libgstwayland-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstisoff-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgstmpegts-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libgsturidownloader-1.0.so.* \
/usr/lib/x86_64-linux-gnu/libpocketsphinx.so.* \
/usr/lib/x86_64-linux-gnu/libopenh264.so.* \
/usr/lib/x86_64-linux-gnu/libavcodec.so.* \
/usr/lib/x86_64-linux-gnu/libavformat.so.* \
/usr/lib/x86_64-linux-gnu/libavutil.so.* \
/usr/lib/x86_64-linux-gnu/libswresample.so.* \
/usr/lib/x86_64-linux-gnu/libswscale.so.* \
/usr/lib/x86_64-linux-gnu/libavfilter.so.* \
/usr/lib/x86_64-linux-gnu/libpostproc.so.* \
/usr/lib/x86_64-linux-gnu/libfaad.so.* \
/usr/lib/x86_64-linux-gnu/libde265.so.* \
/usr/lib/x86_64-linux-gnu/libx265.so.* \
/usr/lib/x86_64-linux-gnu/libx264.so.*; do
if [ -e "$lib" ]; then
cp $lib AppDir/usr/lib/
fi
done
# Create AppRun wrapper to force bundled GStreamer
printf '%s\n' \
'#!/bin/sh' \
'HERE="$(dirname "$(readlink -f "$0")")"' \
'export LD_LIBRARY_PATH="$HERE/usr/lib:$LD_LIBRARY_PATH"' \
'export GST_PLUGIN_PATH="$HERE/usr/lib/gstreamer-1.0"' \
'export GST_PLUGIN_SCANNER="$HERE/usr/lib/gstreamer-1.0/gst-plugin-scanner"' \
'export GST_PLUGIN_SYSTEM_PATH_1_0=""' \
'exec "$HERE/usr/bin/appOpenIPC-Dashboard" "$@"' \
> AppDir/AppRun
chmod +x AppDir/AppRun
# Fix OpenSSL: Copy system OpenSSL 1.1 libs (standard on Ubuntu 20.04)
# We need to provide libssl1.1 because Qt 6.4 binaries are linked against it.
cp /usr/lib/x86_64-linux-gnu/libssl.so.1.1 AppDir/usr/lib/
cp /usr/lib/x86_64-linux-gnu/libcrypto.so.1.1 AppDir/usr/lib/
# Patch RPATH of the main binary to find libs in ../lib relative to itself
# We DO NOT set LD_LIBRARY_PATH so we can verify if RPATH is working correctly
patchelf --set-rpath '$ORIGIN/../lib' AppDir/usr/bin/appOpenIPC-Dashboard
# Debug: check ldd after patching
echo "LDD after patching (Expect libs in .../AppDir/usr/lib):"
ldd AppDir/usr/bin/appOpenIPC-Dashboard
# HACK: Fix silent exit 1 loop in linuxdeployqt when using -unsupported-allow-new-glibc
# The tool checks for this specific file if the flag is present, otherwise it exits 1 silently
mkdir -p AppDir/usr/share/doc/libc6
touch AppDir/usr/share/doc/libc6/copyright
# Run linuxdeployqt (deploy + appimage)
# We use -no-strip to avoid potential issues with stripping failing
# We use -bundle-non-qt-libs so it attempts to include FUSE or other sys libs if needed
# UNSET QT_PLUGIN_PATH
unset QT_PLUGIN_PATH
# Explicitly set QMAKE path (linuxdeployqt uses it to find plugins)
export QMAKE="$(command -v qmake)"
# Use -verbose=0 (less output) to see if we can spot the actual error message at the end
# Sometimes verbose output hides the real error or crashes the logger
set +e
./linuxdeployqt-root/AppRun AppDir/usr/share/applications/appOpenIPC-Dashboard.desktop \
-qmldir=src \
-qmldir=build/OpenIPC \
-exclude-libs=libglib-2.0.so.0,libgthread-2.0.so.0,libgobject-2.0.so.0,libgmodule-2.0.so.0,libgio-2.0.so.0 \
-extra-plugins=iconengines,imageformats,platforms,platformthemes,wayland-decoration-client,wayland-graphics-integration-client,wayland-shell-integration \
-verbose=2 \
-no-strip \
-no-translations \
-qmake=$QMAKE 2>&1 | tee linuxdeployqt.log
LINUXDEPLOYQT_RC=${PIPESTATUS[0]}
set -e
if [ $LINUXDEPLOYQT_RC -ne 0 ]; then
echo "linuxdeployqt failed with exit code $LINUXDEPLOYQT_RC"
exit $LINUXDEPLOYQT_RC
fi
# Pack AppImage explicitly (use appimagetool bundled with linuxdeployqt)
if [ ! -f ./linuxdeployqt-root/usr/bin/appimagetool ]; then
echo "appimagetool not found in linuxdeployqt-root"
ls -la linuxdeployqt-root/usr/bin || true
exit 1
fi
chmod +x ./linuxdeployqt-root/usr/bin/appimagetool
echo "Using appimagetool: ./linuxdeployqt-root/usr/bin/appimagetool"
set +e
ARCH=x86_64 VERSION=${VERSION} ./linuxdeployqt-root/usr/bin/appimagetool -v AppDir OpenIPC-Dashboard-Linux.AppImage 2>&1 | tee appimagetool.log
APPIMAGETOOL_RC=${PIPESTATUS[0]}
set -e
if [ $APPIMAGETOOL_RC -ne 0 ]; then
echo "appimagetool failed with exit code $APPIMAGETOOL_RC"
exit $APPIMAGETOOL_RC
fi
if [ ! -f OpenIPC-Dashboard-Linux.AppImage ]; then
echo "AppImage not created"
find . -maxdepth 2 -name "*.AppImage" -type f -ls || true
ls -la AppDir || true
exit 1
fi
ls -la *.AppImage
mkdir -p tools
mv linuxdeployqt-continuous-x86_64.AppImage tools/
- name: Upload AppImage logs (Linux)
if: always()
uses: actions/upload-artifact@v7
with:
name: linux-appimage-logs
path: |
appimagetool.log
linuxdeployqt.log
create_appimage.log
- name: Rename AppImage
run: |
if [ -f OpenIPC-Dashboard-Linux.AppImage ]; then
echo "AppImage already built"
exit 0
fi
FOUND=$(find . -maxdepth 3 -name "*.AppImage" -type f ! -name "linuxdeployqt-continuous-x86_64.AppImage" | head -n 1)
if [ -n "$FOUND" ]; then
mv "$FOUND" OpenIPC-Dashboard-Linux.AppImage
echo "Moved AppImage: $FOUND"
else
echo "OpenIPC-Dashboard-Linux.AppImage not found"
ls -la
find . -maxdepth 3 -name "*.AppImage" -type f -ls || true
exit 1
fi
- name: Upload AppImage
uses: actions/upload-artifact@v7
with:
name: linux-appimage
path: OpenIPC-Dashboard-Linux.AppImage
release:
needs: [build-windows, build-linux]
runs-on: ubuntu-latest
if: startsWith(github.ref, 'refs/tags/v')
steps:
- name: Checkout repository
uses: actions/checkout@v7
- name: Download Windows Installer
uses: actions/download-artifact@v8
with:
name: windows-installer
- name: Download Linux AppImage
uses: actions/download-artifact@v8
with:
name: linux-appimage
- name: Create Release
uses: softprops/action-gh-release@v1
with:
name: OpenIPC Dashboard ${{ github.ref_name }}
body_path: RELEASE_NOTES.md
prerelease: ${{ contains(github.ref_name, '-') || contains(github.ref_name, 'pre') || contains(github.ref_name, 'rc') || contains(github.ref_name, 'beta') || contains(github.ref_name, 'alpha') }}
files: |
OpenIPC-Dashboard-Installer.exe
OpenIPC-Dashboard-Linux.AppImage