#!/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"