# Provisions a Windows host (Windows 10/11 or Server 2019+) as a Gitea Actions # runner for RustDesk desktop builds. Idempotent: safe to re-run. # # Versions are pinned to .gitea/workflows/build-windows.yml. Bump them there and # here together. # # Usage (Administrator PowerShell): # Set-ExecutionPolicy -Scope Process Bypass -Force # .\provision.ps1 -GiteaUrl https://gitea.example.com -RunnerToken # # By default the runner service is created under a dedicated local user # (`gitea-runner`) -- LocalSystem has been observed to break flutter pub get, # symlink creation, and git's "dubious ownership" check on this codebase. To # opt out, pass `-ServiceAccount LocalSystem` (not recommended). [CmdletBinding()] param( [Parameter(Mandatory = $true)] [string] $GiteaUrl, [Parameter(Mandatory = $true)] [string] $RunnerToken, [string] $RunnerName = "$env:COMPUTERNAME-rustdesk", [string] $RunnerLabels = "windows-10,self-hosted,X64", [string] $RunnerVersion = "0.2.11", [string] $ServiceAccount = "gitea-runner", [SecureString] $ServiceAccountPassword ) $ErrorActionPreference = 'Stop' $ProgressPreference = 'SilentlyContinue' # Must run elevated -- nearly every step writes Machine env, HKLM, or service config. $me = [Security.Principal.WindowsIdentity]::GetCurrent() if (-not (New-Object Security.Principal.WindowsPrincipal $me).IsInRole( [Security.Principal.WindowsBuiltInRole]::Administrator)) { throw 'Run this script in an elevated (Administrator) PowerShell session.' } # --- pinned versions (mirror .gitea/workflows/build-windows.yml env block) --- $RUST_VERSION = '1.75.0' $RUST_NIGHTLY = 'nightly-2023-10-13' $LLVM_VERSION = '15.0.6' $FLUTTER_VERSION = '3.24.5' $VCPKG_COMMIT = '120deac3062162151622ca4860575a33844ba10b' $ToolsRoot = 'C:\tools' New-Item -ItemType Directory -Force -Path $ToolsRoot | Out-Null # Exact-segment-match version of PATH augmentation. Substring matching would # falsely find C:\bin when C:\binaries is on PATH. function Add-MachinePath([string]$Dir) { $cur = [Environment]::GetEnvironmentVariable('Path', 'Machine') $segments = $cur -split ';' | Where-Object { $_ } if ($segments -notcontains $Dir) { [Environment]::SetEnvironmentVariable('Path', "$cur;$Dir", 'Machine') } if (($env:Path -split ';') -notcontains $Dir) { $env:Path = "$env:Path;$Dir" } } # --- 1. Chocolatey (used for git, python, nuget, 7zip, node, dotnet, ...) --- if (-not (Get-Command choco -ErrorAction SilentlyContinue)) { Write-Host '==> Installing Chocolatey' Set-ExecutionPolicy Bypass -Scope Process -Force [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Invoke-Expression ((New-Object Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1')) } Write-Host '==> Installing base packages' # nodejs-lts: act_runner spawns Node to execute JavaScript actions. # powershell-core: workflows use `shell: pwsh` (PS 7), not the OS's PS 5.1. # dotnet-sdk: WiX 4 SDK-style projects (.wixproj) need it for the MSI build. choco install -y --no-progress ` git python311 nuget.commandline 7zip cmake ninja ` nodejs-lts powershell-core dotnet-sdk Add-MachinePath 'C:\Program Files\Git\cmd' Add-MachinePath 'C:\Program Files\Git\bin' # bash.exe + posix tools (sed, find, ...) Add-MachinePath 'C:\Python311' Add-MachinePath 'C:\Python311\Scripts' Add-MachinePath 'C:\Program Files\nodejs' Add-MachinePath 'C:\Program Files\PowerShell\7' Add-MachinePath 'C:\Program Files\dotnet' # --- 2. Visual Studio 2022 Build Tools (MSVC v143 + Win10 SDK) --- # Use [Environment]::GetEnvironmentVariable to avoid the PowerShell parser quirk # that mis-tokenises `$env:ProgramFiles(x86)` as `$env:ProgramFiles` + `(x86)`. $pfx86 = [Environment]::GetEnvironmentVariable('ProgramFiles(x86)') $vsInstaller = Join-Path $pfx86 'Microsoft Visual Studio\Installer\vswhere.exe' $vsPresent = (Test-Path $vsInstaller) -and ` ((& $vsInstaller -products '*' -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath) -ne $null) if (-not $vsPresent) { Write-Host '==> Installing VS 2022 Build Tools (this takes a while)' $vsBootstrapper = "$env:TEMP\vs_buildtools.exe" Invoke-WebRequest -Uri 'https://aka.ms/vs/17/release/vs_buildtools.exe' -OutFile $vsBootstrapper $vsArgs = @( '--quiet','--wait','--norestart','--nocache', '--add','Microsoft.VisualStudio.Workload.VCTools', '--add','Microsoft.VisualStudio.Component.VC.Tools.x86.x64', '--add','Microsoft.VisualStudio.Component.VC.ATL', '--add','Microsoft.VisualStudio.Component.Windows10SDK.20348', '--add','Microsoft.VisualStudio.Component.VC.CMake.Project', '--includeRecommended' ) $p = Start-Process -FilePath $vsBootstrapper -ArgumentList $vsArgs -Wait -PassThru if ($p.ExitCode -notin 0,3010) { throw "VS Build Tools installer exit $($p.ExitCode)" } } # --- 3. Rust (stable + nightly with i686 target) --- # Install machine-wide so any user (including the dedicated runner account) # shares one toolchain registry. Without this, rustup state lives in the # installing user's profile and the service user has no default toolchain. $rustupHome = 'C:\rustup' $cargoHome = 'C:\cargo' [Environment]::SetEnvironmentVariable('RUSTUP_HOME', $rustupHome, 'Machine') [Environment]::SetEnvironmentVariable('CARGO_HOME', $cargoHome, 'Machine') $env:RUSTUP_HOME = $rustupHome $env:CARGO_HOME = $cargoHome Add-MachinePath "$cargoHome\bin" if (-not (Test-Path "$cargoHome\bin\rustup.exe")) { Write-Host '==> Installing rustup (machine-wide at C:\rustup, C:\cargo)' Invoke-WebRequest -Uri 'https://win.rustup.rs/x86_64' -OutFile "$env:TEMP\rustup-init.exe" & "$env:TEMP\rustup-init.exe" -y --default-toolchain none --profile minimal } rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt rustup target add --toolchain $RUST_VERSION x86_64-pc-windows-msvc rustup toolchain install $RUST_NIGHTLY --profile minimal --component rustfmt rustup target add --toolchain $RUST_NIGHTLY i686-pc-windows-msvc rustup default $RUST_VERSION # --- 4. LLVM/Clang (matches KyleMayes/install-llvm-action layout) --- $llvmDir = "$ToolsRoot\llvm-$LLVM_VERSION" if (-not (Test-Path "$llvmDir\bin\clang.exe")) { Write-Host "==> Installing LLVM $LLVM_VERSION" $llvmExe = "$env:TEMP\LLVM-$LLVM_VERSION-win64.exe" Invoke-WebRequest -Uri "https://github.com/llvm/llvm-project/releases/download/llvmorg-$LLVM_VERSION/LLVM-$LLVM_VERSION-win64.exe" -OutFile $llvmExe & $llvmExe /S "/D=$llvmDir" | Out-Null } [Environment]::SetEnvironmentVariable('LIBCLANG_PATH', "$llvmDir\bin", 'Machine') Add-MachinePath "$llvmDir\bin" # --- 5. Flutter (stable channel, with windows precache) --- $flutterDir = "$ToolsRoot\flutter" if (-not (Test-Path "$flutterDir\bin\flutter.bat")) { Write-Host "==> Installing Flutter $FLUTTER_VERSION" $flutterZip = "$env:TEMP\flutter.zip" Invoke-WebRequest -Uri "https://storage.googleapis.com/flutter_infra_release/releases/stable/windows/flutter_windows_$FLUTTER_VERSION-stable.zip" -OutFile $flutterZip Expand-Archive -Force -Path $flutterZip -DestinationPath $ToolsRoot } Add-MachinePath "$flutterDir\bin" & "$flutterDir\bin\flutter.bat" config --no-analytics | Out-Null & "$flutterDir\bin\flutter.bat" precache --windows | Out-Null # --- 6. vcpkg pinned to commit --- $vcpkgDir = 'C:\vcpkg' if (-not (Test-Path "$vcpkgDir\.git")) { Write-Host '==> Cloning vcpkg' git clone https://github.com/microsoft/vcpkg.git $vcpkgDir } Push-Location $vcpkgDir git fetch --tags origin git -c advice.detachedHead=false checkout $VCPKG_COMMIT if (-not (Test-Path "$vcpkgDir\vcpkg.exe")) { & "$vcpkgDir\bootstrap-vcpkg.bat" -disableMetrics } Pop-Location [Environment]::SetEnvironmentVariable('VCPKG_ROOT', $vcpkgDir, 'Machine') Add-MachinePath $vcpkgDir # --- 7. CI prerequisites that aren't tools, but environmental switches --- # git's "dubious ownership" check (>= 2.35.2) refuses to operate on a repo whose # .git directory is owned by a different user than the one running git. The # Flutter SDK at C:\tools\flutter is provisioned by this script as Administrator # but the runner service runs as a non-admin user. Trust everything system-wide. git config --system --add safe.directory '*' 2>$null # Flutter on Windows needs SeCreateSymbolicLinkPrivilege to build plugins. # Enable Developer Mode (registry) AND grant the privilege via Local Security # Policy to the built-in "Users" group (SID S-1-5-32-545). Either alone has been # observed to not take effect until logon-token refresh; doing both is # belt-and-suspenders. The privilege only reaches a long-running service after # a reboot or a fresh service-token issuance. $devKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock' if (-not (Test-Path $devKey)) { New-Item -Path $devKey -Force | Out-Null } New-ItemProperty -Path $devKey -Name 'AllowDevelopmentWithoutDevLicense' ` -PropertyType DWORD -Value 1 -Force | Out-Null $secCfg = "$env:TEMP\sec-symlink.cfg" secedit /export /cfg $secCfg | Out-Null $secContent = Get-Content $secCfg -Raw if ($secContent -match 'SeCreateSymbolicLinkPrivilege\s*=\s*([^\r\n]*)') { $cur = $matches[1] if ($cur -notmatch '\*S-1-5-32-545') { $secContent = $secContent -replace ` '(SeCreateSymbolicLinkPrivilege\s*=\s*)([^\r\n]*)', ` '$1$2,*S-1-5-32-545' } } else { $secContent = $secContent -replace ` '(\[Privilege Rights\][\r\n]+)', ` "`$1SeCreateSymbolicLinkPrivilege = *S-1-5-32-545`r`n" } $secContent | Set-Content $secCfg secedit /configure /db "$env:TEMP\sec-symlink.sdb" /cfg $secCfg /areas USER_RIGHTS /quiet Remove-Item $secCfg, "$env:TEMP\sec-symlink.sdb" -ErrorAction SilentlyContinue # --- 8. Dedicated runner user --- # Running as LocalSystem causes a cascade of issues: # - $USERPROFILE = C:\Windows\System32\config\systemprofile, which Flutter, # dart pub, and other POSIX-leaning tools mis-handle. # - cargo install lands binaries in that systemprofile path -> not on PATH. # - flutter/windows occasionally vanishes during long cargo builds. # A normal local user fixes all of these. if ($ServiceAccount -ne 'LocalSystem') { if (-not (Get-LocalUser -Name $ServiceAccount -ErrorAction SilentlyContinue)) { if (-not $ServiceAccountPassword) { # Generate a 32-byte random password using the OS RNG. Encoded as # base64 (alphanumeric + +/) and trimmed of padding -- meets local # password complexity without needing System.Web (which is missing # on Server Core). $bytes = New-Object byte[] 24 [System.Security.Cryptography.RandomNumberGenerator]::Create().GetBytes($bytes) $plain = ([Convert]::ToBase64String($bytes)).TrimEnd('=') + 'A1!' $ServiceAccountPassword = ConvertTo-SecureString $plain -AsPlainText -Force Remove-Variable plain, bytes } Write-Host "==> Creating local user '$ServiceAccount'" New-LocalUser -Name $ServiceAccount -Password $ServiceAccountPassword ` -PasswordNeverExpires -AccountNeverExpires ` -Description 'Gitea Actions runner service account' | Out-Null Add-LocalGroupMember -Group 'Users' -Member $ServiceAccount } # Grant "Log on as a service" via secedit (no PS native cmdlet for this). $sid = (Get-LocalUser $ServiceAccount).SID.Value $svcCfg = "$env:TEMP\sec-svc.cfg" secedit /export /cfg $svcCfg | Out-Null $svcContent = Get-Content $svcCfg -Raw if ($svcContent -match "SeServiceLogonRight\s*=\s*([^\r\n]*)") { if ($matches[1] -notmatch [regex]::Escape($sid)) { $svcContent = $svcContent -replace ` '(SeServiceLogonRight\s*=\s*)([^\r\n]*)', ` "`$1`$2,*$sid" } } else { $svcContent = $svcContent -replace ` '(\[Privilege Rights\][\r\n]+)', ` "`$1SeServiceLogonRight = *$sid`r`n" } $svcContent | Set-Content $svcCfg secedit /configure /db "$env:TEMP\sec-svc.sdb" /cfg $svcCfg /areas USER_RIGHTS /quiet Remove-Item $svcCfg, "$env:TEMP\sec-svc.sdb" -ErrorAction SilentlyContinue # Ensure the user can read/write everything it needs for builds. foreach ($p in @('C:\actions-runner','C:\cargo','C:\cargo-tools','C:\vcpkg','C:\vcpkg-cache')) { New-Item -ItemType Directory -Force -Path $p | Out-Null icacls $p /grant "${ServiceAccount}:(OI)(CI)F" /T 2>$null | Out-Null } foreach ($p in @('C:\rustup','C:\tools')) { if (Test-Path $p) { icacls $p /grant "${ServiceAccount}:(OI)(CI)RX" /T 2>$null | Out-Null } } } # --- 9. Gitea act_runner --- $runnerDir = 'C:\actions-runner' New-Item -ItemType Directory -Force -Path $runnerDir | Out-Null $runnerExe = "$runnerDir\act_runner.exe" if (-not (Test-Path $runnerExe)) { Write-Host "==> Downloading act_runner $RunnerVersion" Invoke-WebRequest -Uri "https://gitea.com/gitea/act_runner/releases/download/v$RunnerVersion/act_runner-$RunnerVersion-windows-amd64.exe" -OutFile $runnerExe } Push-Location $runnerDir if (-not (Test-Path "$runnerDir\.runner")) { Write-Host '==> Registering runner' & $runnerExe register --no-interactive ` --instance $GiteaUrl ` --token $RunnerToken ` --name $RunnerName ` --labels $RunnerLabels } # Reconfigure the service every run so re-running with a different # -ServiceAccount actually takes effect. $svc = Get-Service -Name 'gitea-act-runner' -ErrorAction SilentlyContinue if ($svc) { if ($svc.Status -eq 'Running') { Stop-Service gitea-act-runner } } else { Write-Host '==> Installing runner as Windows service' choco install -y --no-progress nssm nssm install gitea-act-runner $runnerExe daemon | Out-Null } nssm set gitea-act-runner AppDirectory $runnerDir | Out-Null nssm set gitea-act-runner Start SERVICE_AUTO_START | Out-Null nssm set gitea-act-runner AppStdout "$runnerDir\runner.log" | Out-Null nssm set gitea-act-runner AppStderr "$runnerDir\runner.log" | Out-Null if ($ServiceAccount -eq 'LocalSystem') { nssm set gitea-act-runner ObjectName 'LocalSystem' | Out-Null } else { $bstr = [System.Runtime.InteropServices.Marshal]::SecureStringToBSTR($ServiceAccountPassword) try { $plain = [System.Runtime.InteropServices.Marshal]::PtrToStringUni($bstr) nssm set gitea-act-runner ObjectName ".\$ServiceAccount" $plain | Out-Null } finally { [System.Runtime.InteropServices.Marshal]::ZeroFreeBSTR($bstr) Remove-Variable plain -ErrorAction SilentlyContinue } } # Start may fail before reboot if the new SeServiceLogonRight hasn't reached # SCM yet -- that's expected; the service will start cleanly after reboot. try { Start-Service gitea-act-runner } catch { Write-Warning "Could not start gitea-act-runner now ($($_.Exception.Message)). It will start on reboot." } Pop-Location Write-Host '' Write-Host '==> Done.' Write-Host ' A reboot is REQUIRED before the first build run, so:' Write-Host ' - the runner service inherits the new SeCreateSymbolicLinkPrivilege token' Write-Host ' - all PATH/env changes propagate to the SCM-launched service' Write-Host ' After reboot, verify the runner shows up in Gitea > Site Admin > Actions > Runners.' if ($ServiceAccount -eq 'LocalSystem') { Write-Warning 'Service is running as LocalSystem. RustDesk builds have been observed to fail in this configuration (Flutter pub get, symlinks, dubious ownership). Re-run with -ServiceAccount gitea-runner to switch.' }