name: build-windows on: push: branches: [pro-features] workflow_dispatch: inputs: version_suffix: description: "Version suffix (e.g. 'cst', 'beta1'). Empty = vanilla." type: string default: "cst" env: RUST_VERSION: "1.75" LLVM_VERSION: "15.0.6" # Where provision.ps1 installs LLVM. ffigen and flutter_rust_bridge_codegen need # the install root (they append bin\libclang.dll themselves). LLVM_HOME: 'C:\tools\llvm-15.0.6' FLUTTER_VERSION: "3.24.5" VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" CARGO_EXPAND_VERSION: "1.0.95" FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" # Numeric base, must match Cargo.toml's ... # MSI ProductVersion is forced to this (Windows Installer rejects non-numeric). VERSION_BASE: "1.4.6" # Default suffix on push events. workflow_dispatch can override per-run. VERSION_SUFFIX: ${{ inputs.version_suffix || 'cst' }} jobs: build-x64: name: build-windows-x64 runs-on: [self-hosted, windows-10] timeout-minutes: 240 env: VCPKG_ROOT: C:\vcpkg VCPKG_BINARY_SOURCES: "clear;files,C:\\vcpkg-cache,readwrite" steps: - name: Checkout source uses: actions/checkout@v4 with: submodules: recursive - name: Verify host toolchain shell: pwsh run: | $required = 'node','pwsh','git','bash','python','rustc','cargo','rustup','clang','flutter','nuget','cmake','ninja','dotnet' $missing = @() foreach ($tool in $required) { $cmd = Get-Command $tool -ErrorAction SilentlyContinue if (-not $cmd) { $missing += $tool; continue } $ver = & $tool --version 2>&1 | Select-Object -First 1 Write-Host ("{0,-10} {1} ({2})" -f $tool, $cmd.Source, $ver) } if ($missing.Count -gt 0) { Write-Error ("Missing tools on runner: {0}. Re-run provision.ps1 or install manually." -f ($missing -join ', ')) exit 1 } if (-not $env:VCPKG_ROOT -or -not (Test-Path "$env:VCPKG_ROOT\vcpkg.exe")) { Write-Error "VCPKG_ROOT not set or vcpkg.exe missing at $env:VCPKG_ROOT" exit 1 } Write-Host "VCPKG_ROOT $env:VCPKG_ROOT" - name: Compute version strings shell: pwsh run: | $base = "${env:VERSION_BASE}" $suffix = "${env:VERSION_SUFFIX}" if ($base -notmatch '^\d+\.\d+\.\d+$') { Write-Error "VERSION_BASE '$base' must be major.minor.patch numeric" exit 1 } if ($suffix) { $display = "$base-$suffix" } else { $display = $base } Write-Host ("Base : {0} (used for MSI ProductVersion)" -f $base) Write-Host ("Suffix : {0}" -f $suffix) Write-Host ("Display : {0} (used for exe filename + Cargo.toml)" -f $display) "VERSION_DISPLAY=$display" | Out-File -FilePath $env:GITHUB_ENV -Append - name: Patch Cargo.toml with display version shell: bash run: | # Cargo accepts SemVer-style suffix with a hyphen (e.g. 1.4.6-cst). sed -i -E "0,/^version[[:space:]]*=/{s/^version[[:space:]]*=[[:space:]]*\"${VERSION_BASE}\"/version = \"${VERSION_DISPLAY}\"/}" Cargo.toml echo "--- Cargo.toml [package] ---" awk '/^\[package\]/{f=1} f; /^\[/&&!/^\[package\]/{f=0}' Cargo.toml | head -10 - name: Ensure Rust toolchain configured for runner user shell: pwsh run: | # provision.ps1 installs rust machine-wide (CARGO_HOME=C:\cargo, # RUSTUP_HOME=C:\rustup) so this is normally a no-op verification. # Kept as a guardrail in case the runner was provisioned with an # older script or by hand. rustup --version rustup toolchain install $env:RUST_VERSION --profile minimal --component rustfmt if ($LASTEXITCODE -ne 0) { throw "rustup toolchain install failed ($LASTEXITCODE)" } rustup default $env:RUST_VERSION if ($LASTEXITCODE -ne 0) { throw "rustup default failed ($LASTEXITCODE)" } rustup target add x86_64-pc-windows-msvc rustc --version cargo --version - name: Install flutter_rust_bridge codegen tools shell: pwsh run: | # Pin the install destination with --root so the resulting binaries land # in a deterministic, machine-wide path regardless of which user the # runner service is configured to run as. $tools = 'C:\cargo-tools' New-Item -ItemType Directory -Force -Path "$tools\bin" | Out-Null cargo install --root $tools cargo-expand --version "$env:CARGO_EXPAND_VERSION" --locked if ($LASTEXITCODE -ne 0) { throw "cargo install cargo-expand failed ($LASTEXITCODE)" } cargo install --root $tools flutter_rust_bridge_codegen --version "$env:FLUTTER_RUST_BRIDGE_VERSION" --features uuid --locked if ($LASTEXITCODE -ne 0) { throw "cargo install flutter_rust_bridge_codegen failed ($LASTEXITCODE)" } Write-Host "--- $tools\bin ---" Get-ChildItem "$tools\bin" | Format-Table Name, Length, LastWriteTime $expected = Join-Path $tools 'bin\flutter_rust_bridge_codegen.exe' if (-not (Test-Path $expected)) { throw "missing: $expected" } Add-Content -Path $env:GITHUB_PATH -Value "$tools\bin" - name: Generate Rust <-> Dart bridge shell: pwsh env: LIBCLANG_PATH: '${{ env.LLVM_HOME }}\bin' run: | $ErrorActionPreference = 'Stop' # Force-prepend cargo-tools and Git so they win the lookup race even if # something earlier in PATH owns the same name. $env:PATH = "C:\cargo-tools\bin;C:\Program Files\Git\cmd;C:\Program Files\Git\bin;$env:PATH" Write-Host "== Tool resolution ==" foreach ($t in 'git','flutter','flutter_rust_bridge_codegen','dart') { $cmd = Get-Command $t -ErrorAction SilentlyContinue if ($cmd) { Write-Host ("{0,-30} {1}" -f $t, $cmd.Source) } else { Write-Host ("{0,-30} MISSING" -f $t) } } # `where git` shows ALL git.exe matches on PATH. If multiple, the first # is what CreateProcess will pick -- and it might be a broken shim. Write-Host "`n== where git ==" where.exe git 2>&1 Write-Host "`n== git --version ==" git --version # Diagnose the real reason flutter pub get says "Unable to find git in # your PATH". On a self-hosted runner where the Flutter SDK at # C:\tools\flutter was provisioned by a different user, git refuses to # touch it with "fatal: detected dubious ownership". Flutter's tool # catches that ProcessException and (misleadingly) reports it as a # missing-git-on-PATH error. Write-Host "`n== git ops on flutter SDK (probe for dubious ownership) ==" git -C 'C:\tools\flutter' rev-parse HEAD 2>&1 | Out-Host Write-Host "`n== Configuring safe.directory globally for runner user ==" git config --global --add safe.directory '*' git config --global --get-all safe.directory # Workaround: stage git.exe next to dart.exe. CreateProcessW searches # the calling exe's directory before PATH -- so dropping git.exe here # bypasses any PATH oddities the dart child may inherit from its # parents (powershell -> cmd -> flutter.bat -> dart.exe). $dartBin = 'C:\tools\flutter\bin\cache\dart-sdk\bin' $stagedGit = Join-Path $dartBin 'git.exe' $sourceGit = 'C:\Program Files\Git\cmd\git.exe' if ((Test-Path $dartBin) -and -not (Test-Path $stagedGit)) { Write-Host "`nStaging $sourceGit -> $stagedGit" Copy-Item -Force $sourceGit $stagedGit } if (-not (Test-Path "$env:LIBCLANG_PATH\libclang.dll")) { throw "libclang.dll not found at $env:LIBCLANG_PATH" } Write-Host "`n== flutter pub get ==" Push-Location flutter flutter pub get $rc = $LASTEXITCODE Pop-Location if ($rc -ne 0) { throw "flutter pub get failed ($rc)" } Write-Host "`n== flutter_rust_bridge_codegen ==" flutter_rust_bridge_codegen ` --llvm-path "$env:LLVM_HOME" ` --rust-input ./src/flutter_ffi.rs ` --dart-output ./flutter/lib/generated_bridge.dart ` --c-output ./flutter/macos/Runner/bridge_generated.h if ($LASTEXITCODE -ne 0) { throw "flutter_rust_bridge_codegen failed ($LASTEXITCODE)" } Copy-Item -Force ` ./flutter/macos/Runner/bridge_generated.h ` ./flutter/ios/Runner/bridge_generated.h - name: Replace Flutter engine with rustdesk custom engine shell: pwsh run: | flutter precache --windows Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip Expand-Archive -Force -Path windows-x64-release.zip -DestinationPath windows-x64-release $engineDir = "C:\tools\flutter\bin\cache\artifacts\engine\windows-x64-release" New-Item -ItemType Directory -Force -Path $engineDir | Out-Null Move-Item -Force windows-x64-release\* $engineDir\ - name: Patch Flutter (dropdown_menu enableFilter) shell: bash run: | patch_file=".github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff" if [ -f "$patch_file" ]; then flutter_root="$(dirname "$(dirname "$(which flutter)")")" cp "$patch_file" "$flutter_root/" (cd "$flutter_root" && git apply "$(basename "$patch_file")" || true) fi - name: vcpkg install dependencies (x64-windows-static) shell: bash env: VCPKG_DEFAULT_HOST_TRIPLET: x64-windows-static run: | mkdir -p /c/vcpkg-cache if ! "$VCPKG_ROOT/vcpkg" install \ --triplet x64-windows-static \ --x-install-root="$VCPKG_ROOT/installed"; then find "$VCPKG_ROOT/" -name "*.log" -exec sh -c 'echo "===== {} ====="; cat "{}"' \; exit 1 fi - name: Diagnose + restore flutter/windows if missing shell: bash run: | echo "== pwd ==" pwd echo echo "== flutter/ (top level) ==" ls -la flutter/ 2>/dev/null || echo "flutter/ missing" echo if [ -d flutter/windows ]; then echo "== flutter/windows/ exists, file count ==" find flutter/windows -type f | wc -l ls -la flutter/windows/ else echo "!! flutter/windows MISSING !!" echo "git status (filtered to flutter/windows):" git status --porcelain | grep -E "flutter/windows" | head -30 || echo "(no entries)" echo echo "Restoring flutter/windows from git index..." git checkout HEAD -- flutter/windows if [ -d flutter/windows ]; then echo "Restored: $(find flutter/windows -type f | wc -l) files" else echo "RESTORE FAILED -- check git ls-files:" git ls-files flutter/windows | head exit 1 fi fi # The upstream GitHub workflow runs `python build.py` which chains cargo + # `flutter build windows` in one step. We split that here so we can: # - get a checkpoint between cargo and flutter (~14 min apart) # - restore flutter/windows from git index if anything has clobbered it # (early in our self-hosted bring-up, flutter/windows occasionally # vanished during the cargo phase under LocalSystem; the restore is # defensive and a no-op once the runner is on a normal user) - name: Cargo build (virtual_display dylib + main lib) shell: pwsh run: | $features = "hwcodec,vram,flutter" Push-Location libs\virtual_display\dylib cargo build --release if ($LASTEXITCODE -ne 0) { throw "cargo build (virtual_display) failed" } Pop-Location cargo build --features $features --lib --release if ($LASTEXITCODE -ne 0) { throw "cargo build (rustdesk lib) failed" } if (-not (Test-Path target\release\librustdesk.dll)) { throw "target\release\librustdesk.dll missing after cargo build" } # Restore + build are intentionally one step. Closes any potential window # for flutter/windows to vanish between a separate restore step and the # build, regardless of root cause. - name: Flutter build windows (with last-second restore) shell: pwsh run: | $cmake = '.\flutter\windows\CMakeLists.txt' Write-Host "== Pre-flight check ==" Write-Host "pwd: $(Get-Location)" if (Test-Path .\flutter\windows) { Write-Host "flutter/windows present: $((Get-ChildItem -Recurse .\flutter\windows -File).Count) files" } else { Write-Host "flutter/windows MISSING" } if (-not (Test-Path $cmake)) { Write-Host "`n== Restoring flutter/windows from git index ==" git status --porcelain | Where-Object { $_ -match 'flutter/windows' } | Select-Object -First 10 git checkout HEAD -- flutter/windows if ($LASTEXITCODE -ne 0) { throw "git checkout HEAD -- flutter/windows failed" } if (-not (Test-Path $cmake)) { Write-Host "git ls-files flutter/windows:" git ls-files flutter/windows | Select-Object -First 5 throw "Restore did not produce $cmake" } Write-Host "Restored: $((Get-ChildItem -Recurse .\flutter\windows -File).Count) files" } # Touch a marker so we can confirm later (in logs) that this guard ran. $marker = ".\flutter\windows\.restored-at" Set-Content -Path $marker -Value (Get-Date -Format o) Write-Host "`n== Running flutter build windows --release ==" Push-Location flutter flutter build windows --release $rc = $LASTEXITCODE Pop-Location if ($rc -ne 0) { Write-Host "`n!! flutter build failed -- post-mortem state of flutter/windows: !!" if (Test-Path .\flutter\windows) { Get-ChildItem .\flutter\windows -Force } else { Write-Host "(does not exist)" } throw "flutter build windows failed ($rc)" } # Mirror what build.py's build_flutter_windows() does after flutter build. Copy-Item -Force ` target\release\deps\dylib_virtual_display.dll ` flutter\build\windows\x64\runner\Release\ Move-Item -Force flutter\build\windows\x64\runner\Release rustdesk - name: Bundle usbmmidd_v2 + printer driver shell: pwsh continue-on-error: true run: | Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip Expand-Archive -Force usbmmidd_v2.zip -DestinationPath . Remove-Item -Recurse -Force usbmmidd_v2\Win32 Remove-Item -Force usbmmidd_v2\deviceinstaller64.exe, usbmmidd_v2\deviceinstaller.exe, usbmmidd_v2\usbmmidd.bat Move-Item -Force .\usbmmidd_v2 .\rustdesk\ Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile printer.zip Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/printer_driver_adapter.zip -OutFile printer_adapter.zip Expand-Archive -Force printer.zip -DestinationPath . Expand-Archive -Force printer_adapter.zip -DestinationPath . New-Item -ItemType Directory -Force -Path .\rustdesk\drivers | Out-Null Move-Item -Force .\rustdesk_printer_driver_v4-1.4 .\rustdesk\drivers\RustDeskPrinterDriver Move-Item -Force .\printer_driver_adapter.dll .\rustdesk\ - name: Copy Runner.res for portable packer shell: bash continue-on-error: true run: | runner_res=$(find . -name Runner.res | head -1) [ -n "$runner_res" ] && cp "$runner_res" ./libs/portable/Runner.res - name: Build portable self-extracting exe shell: bash run: | sed -i '/dpiAware/d' res/manifest.xml pushd ./libs/portable pip install -r requirements.txt python ./generate.py -f ../../rustdesk/ -o . -e ../../rustdesk/rustdesk.exe popd mkdir -p ./SignOutput mv ./target/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk-${VERSION_DISPLAY}-x86_64.exe" - name: Build MSI installer shell: pwsh run: | Push-Location .\res\msi # Pass numeric VERSION_BASE explicitly: MSI ProductVersion must be numeric, # so we cannot let preprocess.py auto-detect from rustdesk.exe (which now # carries the suffixed VERSION_DISPLAY). python preprocess.py --arp -d ..\..\rustdesk -v "${env:VERSION_BASE}" # Resolve MSBuild from the installed VS Build Tools. # Resolve vswhere.exe explicitly. ${env:ProgramFiles(x86)} interpolation # has parser-version quirks; use [Environment] to avoid them. $pfx86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)') $vswhere = Join-Path $pfx86 'Microsoft Visual Studio\Installer\vswhere.exe' if (-not (Test-Path $vswhere)) { throw "vswhere.exe not found at $vswhere -- VS Installer missing?" } Write-Host "vswhere: $vswhere" # Diagnostic: show what VS installs vswhere can see. Write-Host "`n== vswhere installations ==" & $vswhere -all -prerelease -products '*' -property installationPath Write-Host "===" # Try the targeted -requires first; fall back to a broader search if the # component name doesn't match (e.g. on Build Tools the component IDs differ). $msbuild = & $vswhere -latest -products '*' ` -requires Microsoft.Component.MSBuild ` -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1 if (-not $msbuild) { Write-Host "Targeted lookup failed; trying broader search." $msbuild = & $vswhere -latest -products '*' -prerelease ` -find "MSBuild\**\Bin\MSBuild.exe" | Select-Object -First 1 } if (-not $msbuild) { # Last resort: any MSBuild.exe under any VS install path. $vsRoots = & $vswhere -all -prerelease -products '*' -property installationPath foreach ($r in $vsRoots) { $candidate = Get-ChildItem -Path $r -Recurse -Filter 'MSBuild.exe' -ErrorAction SilentlyContinue | Where-Object { $_.FullName -like '*\Bin\MSBuild.exe' -and $_.FullName -notlike '*\amd64\*' } | Select-Object -First 1 if ($candidate) { $msbuild = $candidate.FullName; break } } } if (-not $msbuild) { throw "MSBuild not found via vswhere or filesystem search" } Write-Host "msbuild: $msbuild" # Two-stage restore covers both project flavors in this solution: # - CustomActions.vcxproj uses old-style packages.config -> nuget restore # - Package.wixproj is SDK-style PackageReference -> msbuild -t:Restore nuget restore msi.sln & $msbuild msi.sln -t:Restore -p:Configuration=Release -p:Platform=x64 if ($LASTEXITCODE -ne 0) { throw "MSBuild restore failed ($LASTEXITCODE)" } # WiX's WindowsInstallerValidation target invokes ICEs through the # local Windows Installer service (msiserver). When the runner user # lacks the COM/RPC rights to that service, every ICE check fails # with WIX0217. Skip ICE validation -- it's a development-time lint, # not a functional requirement, and the produced MSI is identical. # Defensively also nudge the service in case some other validation # path (e.g. signing tools) needs it. try { Start-Service msiserver -ErrorAction SilentlyContinue } catch {} & $msbuild msi.sln ` -p:Configuration=Release -p:Platform=x64 ` /p:TargetVersion=Windows10 ` /p:SuppressValidation=true if ($LASTEXITCODE -ne 0) { throw "MSBuild build failed ($LASTEXITCODE)" } Move-Item -Force .\Package\bin\x64\Release\en-us\Package.msi "..\..\SignOutput\rustdesk-${env:VERSION_DISPLAY}-x86_64.msi" Pop-Location - name: Report signing status of build artifacts shell: pwsh run: | $artifacts = Get-ChildItem .\SignOutput -Include *.exe,*.msi -File if (-not $artifacts) { Write-Warning "No artifacts found in SignOutput\" return } $unsigned = @() foreach ($f in $artifacts) { $sig = Get-AuthenticodeSignature -FilePath $f.FullName $size = '{0,8:N0}' -f $f.Length switch ($sig.Status) { 'Valid' { Write-Host ("[ SIGNED ] {0} ({1} bytes) signed by: {2}" -f $f.Name, $size, $sig.SignerCertificate.Subject) } 'NotSigned' { Write-Host ("[UNSIGNED] {0} ({1} bytes)" -f $f.Name, $size) $unsigned += $f.Name } default { Write-Host ("[ {0,-7} ] {1} ({2} bytes) -- {3}" -f $sig.Status, $f.Name, $size, $sig.StatusMessage) $unsigned += $f.Name } } } if ($unsigned.Count -gt 0) { # Render a Gitea/GHA-style annotation so it shows up prominently in the run summary. $list = $unsigned -join ', ' Write-Host "::warning title=Unsigned artifacts::$list -- SmartScreen will warn end users. Wire up signing before distributing." } - name: Upload artifacts uses: actions/upload-artifact@v3 with: name: rustdesk-windows-x64-${{ github.sha }} path: | SignOutput/rustdesk-*.exe SignOutput/rustdesk-*.msi if-no-files-found: warn retention-days: 14