From 27ea622e7b1479b5ba88d4279aed9d22b4fc001c Mon Sep 17 00:00:00 2001 From: Mike Mueller Date: Tue, 5 May 2026 23:49:00 +0200 Subject: [PATCH] ci(macos): add macos build --- .gitea/workflows/build-macos.yml | 212 ++++++++++++++++++++++ ci/runners/macos/provision.sh | 291 +++++++++++++++++++++++++++++++ 2 files changed, 503 insertions(+) create mode 100644 .gitea/workflows/build-macos.yml create mode 100755 ci/runners/macos/provision.sh diff --git a/.gitea/workflows/build-macos.yml b/.gitea/workflows/build-macos.yml new file mode 100644 index 000000000..a256be19c --- /dev/null +++ b/.gitea/workflows/build-macos.yml @@ -0,0 +1,212 @@ +name: build-macos + +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.81" # MAC_RUST_VERSION upstream (cidre needs >=1.81) + FLUTTER_VERSION: "3.24.5" + VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" + CARGO_EXPAND_VERSION: "1.0.95" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + VERSION_BASE: "1.4.6" + VERSION_SUFFIX: ${{ inputs.version_suffix || 'cst' }} + +jobs: + build-x64: + name: build-macos-x64 + # Intel Mac runner. provision.sh tags x86_64 macOS hosts with the X64 label. + runs-on: [self-hosted, macOS, X64] + timeout-minutes: 240 + env: + VCPKG_ROOT: /opt/vcpkg + VCPKG_BINARY_SOURCES: "clear;files,/var/cache/vcpkg,readwrite" + VCPKG_DEFAULT_TRIPLET: x64-osx + VCPKG_DEFAULT_HOST_TRIPLET: x64-osx + steps: + - name: Checkout source + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Verify host toolchain + shell: bash + run: | + required=(git bash python3 rustc cargo rustup clang flutter cmake ninja nasm pkg-config create-dmg) + missing=() + for t in "${required[@]}"; do + if command -v "$t" >/dev/null 2>&1; then + printf '%-15s %s\n' "$t" "$(command -v "$t")" + else + missing+=("$t") + printf '%-15s MISSING\n' "$t" + fi + done + if [[ ${#missing[@]} -gt 0 ]]; then + echo "Missing tools: ${missing[*]}. Re-run provision.sh on the runner host." + exit 1 + fi + [[ -d "$VCPKG_ROOT" && -x "$VCPKG_ROOT/vcpkg" ]] || { + echo "VCPKG_ROOT=$VCPKG_ROOT invalid"; exit 1; } + + - name: Compute version strings + shell: bash + run: | + base="${VERSION_BASE}" + suffix="${VERSION_SUFFIX}" + [[ "$base" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]] || { + echo "VERSION_BASE '$base' must be major.minor.patch"; exit 1; } + if [[ -n "$suffix" ]]; then display="${base}-${suffix}"; else display="${base}"; fi + echo "Display : $display" + echo "VERSION_DISPLAY=$display" >> "$GITHUB_ENV" + + - name: Patch Cargo.toml with display version + shell: bash + run: | + # BSD sed (macOS): -i requires an empty backup-suffix arg. + sed -i '' -E "1,/^version[[:space:]]*=/{s/^version[[:space:]]*=[[:space:]]*\"${VERSION_BASE}\"/version = \"${VERSION_DISPLAY}\"/;}" Cargo.toml + grep '^version' Cargo.toml | head -1 + + # No deployment-target patch on x86_64: build.py's build_flutter_dmg() + # already exports MACOSX_DEPLOYMENT_TARGET=10.14 internally, matching the + # Flutter Xcode project. Upstream only patches the target for arm64 + # (which needs 12.3+). + + - name: Ensure Rust toolchain configured + shell: bash + run: | + rustup toolchain install "$RUST_VERSION" --profile minimal --component rustfmt + rustup default "$RUST_VERSION" + rustup target add x86_64-apple-darwin + rustc --version + cargo --version + + - name: Install flutter_rust_bridge codegen tools + shell: bash + run: | + tools=/opt/cargo-tools + mkdir -p "$tools/bin" + cargo install --root "$tools" cargo-expand --version "$CARGO_EXPAND_VERSION" --locked + cargo install --root "$tools" flutter_rust_bridge_codegen --version "$FLUTTER_RUST_BRIDGE_VERSION" --features uuid --locked + ls -la "$tools/bin" + [[ -x "$tools/bin/flutter_rust_bridge_codegen" ]] || { echo "missing fr_bridge_codegen"; exit 1; } + echo "$tools/bin" >> "$GITHUB_PATH" + + - name: Generate Rust <-> Dart bridge (with Flutter 3.22.3) + shell: bash + run: | + set -e + # Same dual-SDK trick as Linux: bridge codegen 1.80.1 produces broken + # Dart on Flutter 3.24.5; provision.sh installs 3.22.3 at + # /opt/flutter-bridge for codegen only. Switch back to 3.24.5 after. + export PATH="/opt/cargo-tools/bin:/opt/flutter-bridge/bin:$PATH" + flutter --version + command -v flutter_rust_bridge_codegen + + # Match the Linux fix: don't pass --llvm-path, let ffigen find Xcode + # CLT's libclang via system defaults. Passing brew's llvm or a custom + # libclang path triggered fr_bridge_codegen 1.80.1's bad-emission bug + # (stray `typedef bool = NativeFunction<...>`) on Linux; same logic + # carries over here. + unset LIBCLANG_PATH + + (cd flutter && sed -i '' -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml) + (cd flutter && flutter pub get) + + flutter_rust_bridge_codegen \ + --rust-input ./src/flutter_ffi.rs \ + --dart-output ./flutter/lib/generated_bridge.dart \ + --c-output ./flutter/macos/Runner/bridge_generated.h + cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h + + (cd flutter && git checkout -- pubspec.yaml) + (cd flutter && /opt/flutter/bin/flutter pub get) + + - name: Diagnose generated bridge files + shell: bash + run: | + set +e + echo "============================================================" + echo " generated_bridge.dart (first 80 lines)" + echo "============================================================" + head -80 flutter/lib/generated_bridge.dart 2>/dev/null || echo "(missing)" + echo + echo "============================================================" + echo " Search: typedef declarations that might shadow bool/Int/Pointer" + echo "============================================================" + grep -nE 'typedef (bool|Int|Pointer|Bool)' \ + flutter/lib/generated_bridge.dart \ + flutter/lib/generated_bridge.freezed.dart 2>/dev/null || echo "(no shadowing typedefs)" + + - name: vcpkg install dependencies (x64-osx) + shell: bash + run: | + mkdir -p /var/cache/vcpkg + if ! "$VCPKG_ROOT/vcpkg" install \ + --triplet x64-osx \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "$VCPKG_ROOT/" -name '*.log' -exec sh -c 'echo "===== {} ====="; cat "{}"' \; + exit 1 + fi + + - name: Build RustDesk (.app + .dmg) + shell: bash + run: | + set -e + # build.py -> build_flutter_dmg() does: + # - MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --features --release + # - cp liblibrustdesk.dylib librustdesk.dylib + # - flutter build macos --release + # - cp -rf ../target/release/service .../RustDesk.app/Contents/MacOS/ + # It does NOT package the .dmg (the create-dmg call is commented out). + # We package below. + # + # No --screencapturekit on x86_64: upstream's matrix only enables it + # on aarch64 (cidre's ScreenCaptureKit bindings target arm64-only APIs). + python3 build.py --flutter --hwcodec --unix-file-copy-paste + + mkdir -p ./SignOutput + # Patch create-dmg's unmount-attempts upward (CI runners are flaky). + CREATE_DMG="$(command -v create-dmg)" + CREATE_DMG_REAL="$(readlink "$CREATE_DMG" 2>/dev/null || echo "$CREATE_DMG")" + # readlink may return relative; resolve. + [[ "$CREATE_DMG_REAL" == /* ]] || CREATE_DMG_REAL="$(cd "$(dirname "$CREATE_DMG")" && pwd)/$CREATE_DMG_REAL" + sed -i '' -e 's/MAXIMUM_UNMOUNTING_ATTEMPTS=3/MAXIMUM_UNMOUNTING_ATTEMPTS=7/' "$CREATE_DMG_REAL" || true + + create-dmg \ + --icon "RustDesk.app" 200 190 \ + --hide-extension "RustDesk.app" \ + --window-size 800 400 \ + --app-drop-link 600 185 \ + "./SignOutput/rustdesk-${VERSION_DISPLAY}-x86_64.dmg" \ + ./flutter/build/macos/Build/Products/Release/RustDesk.app + + - name: Report signing status of build artifacts + shell: bash + run: | + for f in ./SignOutput/*.dmg; do + [[ -f "$f" ]] || continue + size=$(stat -f%z "$f") + sig=$(codesign -dv "$f" 2>&1 | grep -E '^Authority' | head -1 || true) + if [[ -z "$sig" ]]; then + printf '[UNSIGNED] %s (%d bytes)\n' "$(basename "$f")" "$size" + else + printf '[ SIGNED ] %s (%d bytes) %s\n' "$(basename "$f")" "$size" "$sig" + fi + done + echo "::warning title=Unsigned .dmg::Wire up codesign + notarytool before distributing." + + - name: Upload artifacts + uses: actions/upload-artifact@v3 + with: + name: rustdesk-macos-x64-${{ github.sha }} + path: SignOutput/rustdesk-*.dmg + if-no-files-found: warn + retention-days: 14 diff --git a/ci/runners/macos/provision.sh b/ci/runners/macos/provision.sh new file mode 100755 index 000000000..577af3f73 --- /dev/null +++ b/ci/runners/macos/provision.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=(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"