diff --git a/ci/runners/linux/provision-builder.sh b/ci/runners/linux/provision-builder.sh new file mode 100755 index 0000000..4c0b424 --- /dev/null +++ b/ci/runners/linux/provision-builder.sh @@ -0,0 +1,264 @@ +#!/usr/bin/env bash +# Provisions an Ubuntu 22.04 LTS or Debian 13 (Trixie) host as a Gitea Actions +# runner for RustDesk desktop (.deb) builds. Idempotent: safe to re-run. +# +# Versions are pinned to .gitea/workflows/build-linux.yml. Bump them there and +# here together. +# +# Build host vs. user host: the resulting .deb links against the host's glibc. +# Build on the OLDEST distro your users have, otherwise the .deb won't install +# on older systems. +# - Ubuntu 22.04 build -> runs on Ubuntu 22.04+, Debian 12+, derivatives +# - Debian 13 build -> runs on Debian 13+, Ubuntu 24.04+ only +# +# Usage: +# sudo ./provision.sh \ +# --gitea-url https://gitea.example.com \ +# --runner-token +# +# All toolchains land in /opt and are readable by the gitea-runner user. +# Service is installed as a systemd unit running as that user. + +set -euo pipefail + +# ---- pinned versions (mirror .gitea/workflows/build-linux.yml env block) ---- +RUST_VERSION="1.75.0" +FLUTTER_VERSION="3.24.5" # used for `flutter build linux` +FLUTTER_BRIDGE_VERSION="3.22.3" # used for `flutter pub get` + flutter_rust_bridge_codegen +LLVM_VERSION="15.0.6" +VCPKG_COMMIT="120deac3062162151622ca4860575a33844ba10b" +RUNNER_VERSION="0.2.11" + +# ---- defaults ---- +RUNNER_NAME="$(hostname)-rustdesk" +RUNNER_LABELS="" # auto-derived from /etc/os-release if empty +SERVICE_USER="gitea-runner" +GITEA_URL="" +RUNNER_TOKEN="" + +# ---- arg parse ---- +while [[ $# -gt 0 ]]; do + case "$1" in + --gitea-url) GITEA_URL="$2"; shift 2 ;; + --runner-token) RUNNER_TOKEN="$2"; shift 2 ;; + --runner-name) RUNNER_NAME="$2"; shift 2 ;; + --runner-labels) RUNNER_LABELS="$2"; shift 2 ;; + --service-user) SERVICE_USER="$2"; shift 2 ;; + -h|--help) + sed -n '2,18p' "$0" + exit 0 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + +[[ "$EUID" -eq 0 ]] || { echo "Run as root (use sudo)." >&2; exit 1; } +[[ -n "$GITEA_URL" && -n "$RUNNER_TOKEN" ]] \ + || { echo "Missing --gitea-url or --runner-token" >&2; exit 2; } + +. /etc/os-release +case "${ID}-${VERSION_ID:-}" in + ubuntu-22.04) DISTRO_LABEL="ubuntu-22.04" ;; + debian-13|debian-trixie) DISTRO_LABEL="debian-13" ;; + *) + echo "WARNING: tested only on Ubuntu 22.04 and Debian 13. You're on $PRETTY_NAME." + echo "Package names may differ; build outputs may not run on user systems." + DISTRO_LABEL="${ID}-${VERSION_ID:-unknown}" + sleep 3 ;; +esac + +# If --runner-labels wasn't passed, derive a sensible default that includes the +# detected distro so workflows can target a specific build host when needed. +if [[ -z "$RUNNER_LABELS" ]]; then + RUNNER_LABELS="${DISTRO_LABEL},self-hosted,X64,Linux" +fi + +log() { printf '\n==> %s\n' "$*"; } + +# ---- 1. apt packages ---- +log "Installing apt packages" +export DEBIAN_FRONTEND=noninteractive +apt-get update -qq +apt-get install -y --no-install-recommends \ + build-essential clang gcc g++ cmake ninja-build pkg-config nasm yasm \ + autoconf automake libtool libtool-bin \ + libclang-dev llvm-dev \ + libgtk-3-dev libayatana-appindicator3-dev \ + libasound2-dev libpulse-dev libpam0g-dev libssl-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libva-dev libxdo-dev libxfixes-dev \ + libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev \ + git curl wget zip unzip tar xz-utils ca-certificates \ + python3 python3-pip \ + rpm tree dpkg-dev sudo + +# Node.js (act_runner spawns node for JS actions like actions/checkout) +if ! command -v node >/dev/null; then + log "Installing Node.js LTS" + curl -fsSL https://deb.nodesource.com/setup_20.x | bash - + apt-get install -y --no-install-recommends nodejs +fi + +# ---- 2. LLVM (binary tarball; libclang-15-dev was dropped from Debian 13) ---- +LLVM_DIR="/opt/llvm-${LLVM_VERSION}" +if [[ ! -x "$LLVM_DIR/bin/clang" ]]; then + log "Installing LLVM $LLVM_VERSION (binary tarball)" + arch="$(uname -m)" + case "$arch" in + x86_64) llvm_arch="x86_64-linux-gnu-ubuntu-18.04" ;; + aarch64) llvm_arch="aarch64-linux-gnu" ;; + *) echo "Unsupported arch for LLVM tarball: $arch" >&2; exit 1 ;; + esac + tmp="$(mktemp -d)" + curl -fsSL -o "$tmp/llvm.tar.xz" \ + "https://github.com/llvm/llvm-project/releases/download/llvmorg-${LLVM_VERSION}/clang+llvm-${LLVM_VERSION}-${llvm_arch}.tar.xz" + mkdir -p "$LLVM_DIR" + tar --strip-components=1 -xJf "$tmp/llvm.tar.xz" -C "$LLVM_DIR" + rm -rf "$tmp" +fi + +# ---- 3. dedicated runner user ---- +if ! id -u "$SERVICE_USER" >/dev/null 2>&1; then + log "Creating user $SERVICE_USER" + useradd --system --create-home --shell /bin/bash "$SERVICE_USER" +fi +RUNNER_HOME="$(getent passwd "$SERVICE_USER" | cut -d: -f6)" + +# ---- 4. Rust (machine-wide) ---- +export RUSTUP_HOME=/opt/rustup +export CARGO_HOME=/opt/cargo +mkdir -p "$RUSTUP_HOME" "$CARGO_HOME" + +if [[ ! -x "$CARGO_HOME/bin/rustup" ]]; then + log "Installing rustup at $RUSTUP_HOME / $CARGO_HOME" + curl -fsSL https://sh.rustup.rs | RUSTUP_HOME="$RUSTUP_HOME" CARGO_HOME="$CARGO_HOME" \ + sh -s -- -y --default-toolchain none --profile minimal --no-modify-path +fi +"$CARGO_HOME/bin/rustup" toolchain install "$RUST_VERSION" --profile minimal --component rustfmt +"$CARGO_HOME/bin/rustup" target add --toolchain "$RUST_VERSION" x86_64-unknown-linux-gnu +"$CARGO_HOME/bin/rustup" default "$RUST_VERSION" + +# ---- 5. Flutter (two SDKs: 3.24.5 for build, 3.22.3 for bridge gen) ---- +# Why two: the bridge codegen (flutter_rust_bridge_codegen 1.80.1 + freezed) +# produces broken Dart output when run under newer Flutter SDKs on Linux. +# Upstream's bridge.yml uses 3.22.3 specifically; we mirror that. The .deb +# build itself uses 3.24.5. +install_flutter() { + local ver="$1" dir="$2" + if [[ ! -x "$dir/bin/flutter" ]]; then + log "Installing Flutter $ver -> $dir" + local tmp; tmp="$(mktemp -d)" + local parent; parent="$(dirname "$dir")" + curl -fsSL -o "$tmp/flutter.tar.xz" \ + "https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${ver}-stable.tar.xz" + # Tarball extracts into a top-level `flutter/` dir; rename to target. + tar -xJf "$tmp/flutter.tar.xz" -C "$tmp" + mkdir -p "$parent" + mv "$tmp/flutter" "$dir" + rm -rf "$tmp" + fi + "$dir/bin/flutter" config --no-analytics >/dev/null + "$dir/bin/flutter" precache --linux >/dev/null +} +install_flutter "$FLUTTER_VERSION" /opt/flutter +install_flutter "$FLUTTER_BRIDGE_VERSION" /opt/flutter-bridge +FLUTTER_DIR=/opt/flutter + +# ---- 6. vcpkg ---- +VCPKG_DIR=/opt/vcpkg +if [[ ! -d "$VCPKG_DIR/.git" ]]; then + log "Cloning vcpkg" + git clone https://github.com/microsoft/vcpkg.git "$VCPKG_DIR" +fi +git -C "$VCPKG_DIR" fetch --tags origin +git -C "$VCPKG_DIR" -c advice.detachedHead=false checkout "$VCPKG_COMMIT" +[[ -x "$VCPKG_DIR/vcpkg" ]] || "$VCPKG_DIR/bootstrap-vcpkg.sh" -disableMetrics + +# vcpkg binary cache (file-backed -- same scheme as build-windows.yml) +mkdir -p /var/cache/vcpkg +chown -R "$SERVICE_USER:$SERVICE_USER" /var/cache/vcpkg + +# ---- 7. Permissions ---- +log "Setting up permissions for $SERVICE_USER" +chown -R "$SERVICE_USER:$SERVICE_USER" "$CARGO_HOME" +# rustup state needs to be writable too -- toolchain installs touch it. +chown -R "$SERVICE_USER:$SERVICE_USER" "$RUSTUP_HOME" +# Flutter SDK: r/x is enough for builds, but `flutter pub get` writes to its +# own cache subdir so we make it writable as well. +chown -R "$SERVICE_USER:$SERVICE_USER" "$FLUTTER_DIR" +chown -R "$SERVICE_USER:$SERVICE_USER" /opt/flutter-bridge +# vcpkg: builds write under installed/, buildtrees/, etc. +chown -R "$SERVICE_USER:$SERVICE_USER" "$VCPKG_DIR" +# LLVM: read+execute is enough; we never write here at build time. +chown -R "$SERVICE_USER:$SERVICE_USER" "$LLVM_DIR" +# /opt/cargo-tools: workflow installs cargo-expand and flutter_rust_bridge_codegen +# here via `cargo install --root`. Pre-create with the right owner so the first +# job doesn't try to mkdir under root-owned /opt. +mkdir -p /opt/cargo-tools +chown -R "$SERVICE_USER:$SERVICE_USER" /opt/cargo-tools + +# git "dubious ownership": same fix as Windows. Trust system-wide. +git config --system --add safe.directory '*' || true + +# ---- 8. act_runner ---- +RUNNER_DIR=/var/lib/gitea-runner +mkdir -p "$RUNNER_DIR" +chown -R "$SERVICE_USER:$SERVICE_USER" "$RUNNER_DIR" + +if [[ ! -x "$RUNNER_DIR/act_runner" ]]; then + log "Downloading act_runner $RUNNER_VERSION" + curl -fsSL -o "$RUNNER_DIR/act_runner" \ + "https://gitea.com/gitea/act_runner/releases/download/v${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-linux-amd64" + chmod +x "$RUNNER_DIR/act_runner" + chown "$SERVICE_USER:$SERVICE_USER" "$RUNNER_DIR/act_runner" +fi + +if [[ ! -f "$RUNNER_DIR/.runner" ]]; then + log "Registering runner with $GITEA_URL" + sudo -u "$SERVICE_USER" -H bash -c " + cd '$RUNNER_DIR' && \ + ./act_runner register --no-interactive \ + --instance '$GITEA_URL' \ + --token '$RUNNER_TOKEN' \ + --name '$RUNNER_NAME' \ + --labels '$RUNNER_LABELS' + " +fi + +# ---- 9. systemd service ---- +log "Installing systemd unit" +cat > /etc/systemd/system/gitea-act-runner.service < Site Admin > Actions > Runners" diff --git a/ci/runners/linux/provision.sh b/ci/runners/linux/provision-signer.sh similarity index 100% rename from ci/runners/linux/provision.sh rename to ci/runners/linux/provision-signer.sh diff --git a/ci/runners/macos/provision-builder.sh b/ci/runners/macos/provision-builder.sh new file mode 100755 index 0000000..2331720 --- /dev/null +++ b/ci/runners/macos/provision-builder.sh @@ -0,0 +1,291 @@ +#!/usr/bin/env bash +# Provisions a macOS host (Apple Silicon, macOS 14+) as a Gitea Actions runner +# for RustDesk desktop (.dmg) builds. Idempotent: safe to re-run. +# +# Versions are pinned to .gitea/workflows/build-macos.yml. Bump them there and +# here together. +# +# Usage: +# sudo ./provision.sh \ +# --gitea-url https://gitea.example.com \ +# --runner-token +# +# Toolchains land in /opt/* (chowned to the runner user). Service is installed +# as a LaunchDaemon running as that user. + +set -euo pipefail + +# ---- pinned versions (mirror .gitea/workflows/build-macos.yml env block) ---- +RUST_VERSION="1.81.0" # MAC_RUST_VERSION upstream (cidre crate needs >=1.81) +FLUTTER_VERSION="3.24.5" # used for `flutter build macos` +FLUTTER_BRIDGE_VERSION="3.22.3" # used for `flutter pub get` + flutter_rust_bridge_codegen +VCPKG_COMMIT="120deac3062162151622ca4860575a33844ba10b" +NASM_VERSION="2.16.03" # 3.x has incompatible CLI; aom/dav1d need 2.x +RUNNER_VERSION="0.2.11" + +# ---- defaults ---- +RUNNER_NAME="$(hostname -s)-rustdesk" +RUNNER_LABELS="" +SERVICE_USER="gitea-runner" +GITEA_URL="" +RUNNER_TOKEN="" + +# ---- arg parse ---- +while [[ $# -gt 0 ]]; do + case "$1" in + --gitea-url) GITEA_URL="$2"; shift 2 ;; + --runner-token) RUNNER_TOKEN="$2"; shift 2 ;; + --runner-name) RUNNER_NAME="$2"; shift 2 ;; + --runner-labels) RUNNER_LABELS="$2"; shift 2 ;; + --service-user) SERVICE_USER="$2"; shift 2 ;; + -h|--help) + sed -n '2,15p' "$0" + exit 0 ;; + *) echo "Unknown arg: $1" >&2; exit 2 ;; + esac +done + +[[ "$EUID" -eq 0 ]] || { echo "Run as root (use sudo)." >&2; exit 1; } +[[ -n "$GITEA_URL" && -n "$RUNNER_TOKEN" ]] \ + || { echo "Missing --gitea-url or --runner-token" >&2; exit 2; } + +# ---- arch + macOS version detection ---- +ARCH="$(uname -m)" +case "$ARCH" in + arm64) HOMEBREW_PREFIX="/opt/homebrew"; ARCH_LABEL="ARM64" ;; + x86_64) HOMEBREW_PREFIX="/usr/local"; ARCH_LABEL="X64" ;; + *) echo "Unsupported arch: $ARCH" >&2; exit 1 ;; +esac + +OS_MAJOR="$(sw_vers -productVersion | cut -d. -f1)" +[[ "$OS_MAJOR" -ge 14 ]] || { + echo "WARNING: tested only on macOS 14+. You're on $(sw_vers -productVersion)." + sleep 3 +} +DISTRO_LABEL="macos-${OS_MAJOR}" + +if [[ -z "$RUNNER_LABELS" ]]; then + RUNNER_LABELS="${DISTRO_LABEL},self-hosted,${ARCH_LABEL},macOS" +fi + +log() { printf '\n==> %s\n' "$*"; } + +# ---- 1. Xcode Command Line Tools ---- +log "Verifying Xcode Command Line Tools" +if ! /usr/bin/xcode-select -p >/dev/null 2>&1; then + echo "Xcode Command Line Tools not installed. Run:" >&2 + echo " xcode-select --install" >&2 + echo "Then re-run this script." >&2 + exit 1 +fi +echo " $(xcode-select -p)" + +# ---- 2. Homebrew (machine-wide) ---- +# Homebrew refuses to install under root (its installer aborts with +# "Don't run this as root!"). It must be installed manually by a regular +# user before this script runs. +log "Verifying Homebrew" +if [[ ! -x "$HOMEBREW_PREFIX/bin/brew" ]]; then + echo "Homebrew not installed at $HOMEBREW_PREFIX." >&2 + echo "Install it as your regular user (NOT root), then re-run this script:" >&2 + echo " /bin/bash -c \"\$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"" >&2 + exit 1 +fi +export PATH="$HOMEBREW_PREFIX/bin:$PATH" +echo " $(brew --version | head -1)" + +# brew install must also run as a non-root user. Determine which user invoked +# sudo so we can drop privileges for brew commands below. +BREW_USER="${SUDO_USER:-}" +if [[ -z "$BREW_USER" || "$BREW_USER" == "root" ]]; then + echo "Could not determine the non-root user that ran sudo (SUDO_USER unset)." >&2 + echo "Re-run with: sudo ./provision.sh ..." >&2 + exit 1 +fi +brew_as_user() { sudo -u "$BREW_USER" -H "$HOMEBREW_PREFIX/bin/brew" "$@"; } + +# ---- 3. brew packages ---- +log "Installing brew packages" +brew_pkgs=(node cocoapods llvm create-dmg pkg-config cmake ninja yasm autoconf automake libtool wget) +for p in "${brew_pkgs[@]}"; do + if brew_as_user list --versions "$p" >/dev/null 2>&1; then + echo " $p (already installed)" + else + brew_as_user install "$p" + fi +done + +# ---- 4. NASM 2.16.x (NOT brew's nasm 3.x; aom/dav1d need 2.x) ---- +if ! /usr/local/bin/nasm --version 2>/dev/null | grep -q "version $NASM_VERSION"; then + log "Installing NASM $NASM_VERSION" + tmp="$(mktemp -d)" + curl -fsSL -o "$tmp/nasm.zip" \ + "https://www.nasm.us/pub/nasm/releasebuilds/${NASM_VERSION}/macosx/nasm-${NASM_VERSION}-macosx.zip" + unzip -q "$tmp/nasm.zip" -d "$tmp" + install -m 0755 "$tmp/nasm-${NASM_VERSION}/nasm" /usr/local/bin/nasm + rm -rf "$tmp" +fi +/usr/local/bin/nasm --version | head -1 + +# ---- 5. dedicated runner user ---- +if ! /usr/bin/id -u "$SERVICE_USER" >/dev/null 2>&1; then + log "Creating user $SERVICE_USER" + # Find an unused UID >= 600 + uid=600 + while dscl . -list /Users UniqueID | awk '{print $2}' | grep -qx "$uid"; do + uid=$((uid + 1)) + done + dscl . -create "/Users/$SERVICE_USER" + dscl . -create "/Users/$SERVICE_USER" UserShell /bin/bash + dscl . -create "/Users/$SERVICE_USER" RealName "Gitea Runner" + dscl . -create "/Users/$SERVICE_USER" UniqueID "$uid" + dscl . -create "/Users/$SERVICE_USER" PrimaryGroupID 20 + dscl . -create "/Users/$SERVICE_USER" NFSHomeDirectory "/Users/$SERVICE_USER" + mkdir -p "/Users/$SERVICE_USER" + chown "$SERVICE_USER:staff" "/Users/$SERVICE_USER" +fi +RUNNER_HOME="/Users/$SERVICE_USER" + +# ---- 6. Rust (machine-wide) ---- +export RUSTUP_HOME=/opt/rustup +export CARGO_HOME=/opt/cargo +mkdir -p "$RUSTUP_HOME" "$CARGO_HOME" + +if [[ ! -x "$CARGO_HOME/bin/rustup" ]]; then + log "Installing rustup at $RUSTUP_HOME / $CARGO_HOME" + curl -fsSL https://sh.rustup.rs | RUSTUP_HOME="$RUSTUP_HOME" CARGO_HOME="$CARGO_HOME" \ + sh -s -- -y --default-toolchain none --profile minimal --no-modify-path +fi +"$CARGO_HOME/bin/rustup" toolchain install "$RUST_VERSION" --profile minimal --component rustfmt +"$CARGO_HOME/bin/rustup" target add --toolchain "$RUST_VERSION" aarch64-apple-darwin x86_64-apple-darwin +"$CARGO_HOME/bin/rustup" default "$RUST_VERSION" + +# ---- 7. Flutter (two SDKs: 3.24.5 for build, 3.22.3 for bridge gen) ---- +# Same rationale as Linux: bridge codegen 1.80.1 + freezed produces broken Dart +# under newer Flutter. Run codegen under 3.22.3, build under 3.24.5. +install_flutter() { + local ver="$1" dir="$2" + if [[ ! -x "$dir/bin/flutter" ]]; then + log "Installing Flutter $ver -> $dir" + local tmp; tmp="$(mktemp -d)" + local parent; parent="$(dirname "$dir")" + # Flutter URL pattern differs between archs: Apple Silicon has an + # `_arm64_` infix, Intel has no arch infix at all. + local flutter_url_base="https://storage.googleapis.com/flutter_infra_release/releases/stable/macos/flutter_macos" + local flutter_url + case "$ARCH" in + arm64) flutter_url="${flutter_url_base}_arm64_${ver}-stable.zip" ;; + x86_64) flutter_url="${flutter_url_base}_${ver}-stable.zip" ;; + esac + curl -fsSL -o "$tmp/flutter.zip" "$flutter_url" + mkdir -p "$parent" + unzip -q "$tmp/flutter.zip" -d "$tmp" + mv "$tmp/flutter" "$dir" + rm -rf "$tmp" + fi + "$dir/bin/flutter" config --no-analytics >/dev/null + "$dir/bin/flutter" precache --macos >/dev/null +} +install_flutter "$FLUTTER_VERSION" /opt/flutter +install_flutter "$FLUTTER_BRIDGE_VERSION" /opt/flutter-bridge + +# ---- 8. vcpkg ---- +VCPKG_DIR=/opt/vcpkg +if [[ ! -d "$VCPKG_DIR/.git" ]]; then + log "Cloning vcpkg" + git clone https://github.com/microsoft/vcpkg.git "$VCPKG_DIR" +fi +git -C "$VCPKG_DIR" fetch --tags origin +git -C "$VCPKG_DIR" -c advice.detachedHead=false checkout "$VCPKG_COMMIT" +[[ -x "$VCPKG_DIR/vcpkg" ]] || "$VCPKG_DIR/bootstrap-vcpkg.sh" -disableMetrics + +mkdir -p /var/cache/vcpkg +chown -R "$SERVICE_USER:staff" /var/cache/vcpkg + +# ---- 9. Permissions ---- +log "Setting up permissions for $SERVICE_USER" +chown -R "$SERVICE_USER:staff" "$CARGO_HOME" "$RUSTUP_HOME" \ + /opt/flutter /opt/flutter-bridge "$VCPKG_DIR" +mkdir -p /opt/cargo-tools +chown -R "$SERVICE_USER:staff" /opt/cargo-tools + +git config --system --add safe.directory '*' || true + +# ---- 10. act_runner ---- +RUNNER_DIR="/usr/local/var/gitea-runner" +mkdir -p "$RUNNER_DIR" +chown -R "$SERVICE_USER:staff" "$RUNNER_DIR" + +if [[ ! -x "$RUNNER_DIR/act_runner" ]]; then + log "Downloading act_runner $RUNNER_VERSION" + case "$ARCH" in + arm64) rarch="arm64" ;; + x86_64) rarch="amd64" ;; + esac + curl -fsSL -o "$RUNNER_DIR/act_runner" \ + "https://gitea.com/gitea/act_runner/releases/download/v${RUNNER_VERSION}/act_runner-${RUNNER_VERSION}-darwin-${rarch}" + chmod +x "$RUNNER_DIR/act_runner" + chown "$SERVICE_USER:staff" "$RUNNER_DIR/act_runner" +fi + +if [[ ! -f "$RUNNER_DIR/.runner" ]]; then + log "Registering runner with $GITEA_URL" + sudo -u "$SERVICE_USER" -H bash -c " + cd '$RUNNER_DIR' && \ + ./act_runner register --no-interactive \ + --instance '$GITEA_URL' \ + --token '$RUNNER_TOKEN' \ + --name '$RUNNER_NAME' \ + --labels '$RUNNER_LABELS' + " +fi + +# ---- 11. launchd service ---- +log "Installing LaunchDaemon" +PLIST=/Library/LaunchDaemons/com.rustdesk.gitea-runner.plist +cat > "$PLIST" < + + + + Label + com.rustdesk.gitea-runner + UserName + ${SERVICE_USER} + WorkingDirectory + ${RUNNER_DIR} + ProgramArguments + + ${RUNNER_DIR}/act_runner + daemon + + EnvironmentVariables + + RUSTUP_HOME ${RUSTUP_HOME} + CARGO_HOME ${CARGO_HOME} + VCPKG_ROOT ${VCPKG_DIR} + HOMEBREW_PREFIX ${HOMEBREW_PREFIX} + PATH + ${CARGO_HOME}/bin:/opt/flutter/bin:/opt/cargo-tools/bin:${HOMEBREW_PREFIX}/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin + + RunAtLoad + KeepAlive + StandardOutPath${RUNNER_DIR}/stdout.log + StandardErrorPath${RUNNER_DIR}/stderr.log + SoftResourceLimits + + NumberOfFiles 65535 + + + +EOF +chmod 0644 "$PLIST" + +launchctl bootout system "$PLIST" 2>/dev/null || true +launchctl bootstrap system "$PLIST" +launchctl enable "system/com.rustdesk.gitea-runner" + +log "Done." +echo " Verify with: sudo launchctl print system/com.rustdesk.gitea-runner | head" +echo " Tail logs with: tail -F $RUNNER_DIR/stderr.log" +echo " Runner should appear (online) at $GITEA_URL > Site Admin > Actions > Runners" diff --git a/ci/runners/windows/provision-builder.ps1 b/ci/runners/windows/provision-builder.ps1 new file mode 100644 index 0000000..a82affc --- /dev/null +++ b/ci/runners/windows/provision-builder.ps1 @@ -0,0 +1,324 @@ +# 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.' +}