Harden GStreamer release asset downloads #309
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |