name: CI on: push: branches: - main pull_request: branches: - main concurrency: group: ci-${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true permissions: contents: read # One matrix, seven platforms: the three Linux-family packaging distros # (Debian 12, Fedora 43, Arch), openKylin on x64 and arm64, macOS and # Windows. The environments match .github/workflows/cd.yml exactly (same # container images, same dependency lists, same runner sizes), so a # "passes CI, fails CD" dependency drift is caught here first. jobs: build-test: name: Build & test (${{ matrix.name }}) runs-on: ${{ matrix.runner }} # Container entries carry the container as JSON ({"image":..., # "options":...}); the empty string means "run on the host" # (actions/runner#265 allows an empty container value). container: ${{ matrix.container != '' && fromJSON(matrix.container) || '' }} # The Test step's watchdog caps a hung suite at 30 min; leave a cold # vcpkg install + full compile room beyond that. timeout-minutes: 90 strategy: fail-fast: false matrix: include: - name: Debian platform: linux distro: debian arch: x64 runner: warp-ubuntu-latest-x64-8x triplet: x64-linux container: '{"image":"debian:12","options":"--shm-size=8g"}' - name: Fedora platform: linux distro: fedora arch: x64 runner: warp-ubuntu-latest-x64-8x triplet: x64-linux container: '{"image":"fedora:43","options":"--shm-size=8g"}' - name: Arch platform: linux distro: arch arch: x64 runner: warp-ubuntu-latest-x64-8x triplet: x64-linux container: '{"image":"archlinux:latest","options":"--shm-size=8g"}' - name: openKylin x64 platform: linux distro: openkylin arch: x64 runner: warp-ubuntu-latest-x64-8x triplet: x64-linux container: '{"image":"openkylin/openkylin:latest","options":"--shm-size=8g"}' - name: openKylin arm64 platform: linux distro: openkylin arch: arm64 runner: warp-ubuntu-latest-arm64-16x triplet: arm64-linux container: '{"image":"openkylin/openkylin:latest","options":"--shm-size=8g"}' - name: macOS platform: macos distro: macos arch: arm64 runner: warp-macos-26-arm64-12x triplet: arm64-osx container: '' - name: Windows platform: windows distro: windows arch: x64 runner: warp-windows-2025-vs2026-x64-32x triplet: x64-windows container: '' steps: # The container images are bare (Fedora/Arch even lack git); # checkout and vcpkg need git/curl, and WarpCache needs wget inside # a container (its README requires it). First step of the job, so # the package lists are still fresh. - name: Bootstrap container (git, curl, wget) if: matrix.container != '' shell: bash run: | case "${{ matrix.distro }}" in fedora) dnf install -y --setopt=install_weak_deps=False --setopt=max_parallel_downloads=16 git curl wget which ;; arch) pacman -Sy --noconfirm git curl wget which ;; debian|openkylin) apt-get update && apt-get install -y git curl ca-certificates wget ;; esac - name: Checkout uses: actions/checkout@v7 with: # gpui/ is a git submodule; its crates are workspace members of # their own repo and build as path dependencies of oakapp. submodules: true # Taste the disk before the toolchains land: Defender scans every # file the vcpkg/cargo builds touch (tens of thousands of small # writes), which dominates a cold Windows build. The runner is an # ephemeral VM, so the scanner is turned off for the job # (exclusions are kept as a fallback for images where real-time # protection cannot be disabled). - name: Disable Windows Defender scanning if: matrix.platform == 'windows' shell: pwsh run: | try { Set-MpPreference -DisableRealtimeMonitoring $true -ErrorAction Stop Set-MpPreference -DisableScriptScanning $true -ErrorAction SilentlyContinue Set-MpPreference -DisableArchiveScanning $true -ErrorAction SilentlyContinue Write-Host "Windows Defender real-time scanning disabled for this job" } catch { Write-Host "Windows Defender could not be disabled (non-fatal, falling back to exclusions): $_" } foreach ($path in @( $env:GITHUB_WORKSPACE, "$env:USERPROFILE\.cargo", "$env:USERPROFILE\.rustup", "$env:LOCALAPPDATA\vcpkg" )) { Add-MpPreference -ExclusionPath $path -ErrorAction SilentlyContinue } try { Get-MpPreference | Select-Object DisableRealtimeMonitoring, DisableScriptScanning, ExclusionPath | Format-List } catch { Write-Host "Defender status unavailable: $_" } # The containers run as root but Actions sets HOME=/github/home; # rustup refuses the euid mismatch ("$HOME differs from # euid-obtained home directory") and would install a toolchain the # later steps cannot find under the Actions home. Pin the job to # root's home so rustup/cargo and the toolchain agree. - name: Pin HOME for rustup if: matrix.container != '' shell: bash run: | { echo "HOME=/root" echo "CARGO_HOME=/root/.cargo" echo "RUSTUP_HOME=/root/.rustup" } >> "$GITHUB_ENV" - name: Install Rust uses: dtolnay/rust-toolchain@stable with: # The Windows build is MSVC-ABI (the runner carries VS 2026): # vcpkg's FFmpeg and the vendored OCIO build both want it. toolchain: ${{ matrix.platform == 'windows' && 'stable-x86_64-pc-windows-msvc' || 'stable' }} # ------------------------------------------------------------------ # System dependencies — one list per distro, byte-for-byte the same # lists CD uses (see .github/workflows/cd.yml): a package a CD build # needs cannot be missing here. # ------------------------------------------------------------------ - name: Install system dependencies (Debian) if: matrix.distro == 'debian' shell: bash run: | apt-get update apt-get install -y \ build-essential clang libclang-dev cmake pkg-config nasm \ git curl zip unzip tar python3 dpkg-dev \ libpipewire-0.3-dev libspa-0.2-dev libjack-jackd2-dev \ libasound2-dev libpulse-dev libsndfile1-dev \ libgl1-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers \ libvulkan-dev libxkbcommon-dev libxkbcommon-x11-dev xvfb libdrm-dev \ icc-profiles-free gdb file librsvg2-bin patchelf \ autoconf autoconf-archive automake libtool - name: Install system dependencies (Fedora) if: matrix.distro == 'fedora' shell: bash run: | # Fedora 41 is EOL (its mirrors moved to the slow archive), so # the matrix uses the current release; weak dependencies (docs, # fonts, optional tooling) are skipped and downloads run wide. dnf install -y --setopt=install_weak_deps=False \ --setopt=max_parallel_downloads=16 \ gcc gcc-c++ clang clang-devel cmake pkgconf-pkg-config nasm \ git curl zip unzip tar python3 patch xz which \ pipewire-devel jack-audio-connection-kit-devel \ alsa-lib-devel pulseaudio-libs-devel libsndfile-devel \ mesa-libGL-devel mesa-vulkan-drivers \ vulkan-headers vulkan-loader-devel \ libxkbcommon-devel libxkbcommon-x11-devel \ rpm-build librsvg2-tools libdrm-devel \ perl-IPC-Cmd perl-FindBin perl-File-Basename perl-File-Compare \ perl-File-Copy perl-File-Path perl-File-Temp perl-Time-Piece \ xorg-x11-server-Xvfb xorg-x11-xauth gdb file \ autoconf autoconf-archive automake libtool - name: Install system dependencies (Arch) if: matrix.distro == 'arch' shell: bash run: | pacman -S --needed --noconfirm \ base-devel clang cmake pkgconf nasm \ git curl zip unzip tar python patch xz which \ pipewire jack2 alsa-lib libpulse libsndfile \ mesa vulkan-headers vulkan-icd-loader \ libxkbcommon libxkbcommon-x11 librsvg libdrm \ xorg-server-xvfb xorg-xauth gdb file \ autoconf autoconf-archive automake libtool - name: Install system dependencies (openKylin) if: matrix.distro == 'openkylin' shell: bash run: | apt-get update apt-get install -y \ build-essential clang libclang-dev cmake pkg-config nasm \ git curl zip unzip tar python3 patch xz-utils dpkg-dev \ libpipewire-0.3-dev libspa-0.2-dev libjack-jackd2-dev \ libasound2-dev libpulse-dev libsndfile1-dev \ libgl1-mesa-dev libgl1-mesa-dri mesa-vulkan-drivers \ libvulkan-dev libxkbcommon-dev libxkbcommon-x11-dev xvfb libdrm-dev \ gdb file patchelf fonts-dejavu-core \ autoconf autoconf-archive automake libtool - name: Install system dependencies (macOS) if: matrix.platform == 'macos' run: | # Homebrew's pkgconf installs a `pkg-config` symlink, which is # the name crates/oak-ffmpeg-link/build.rs invokes; nasm is what # vcpkg's ffmpeg port requires to build (FFmpeg libraries come # from the vcpkg manifest). librsvg is CD's icon renderer. brew install cmake pkg-config nasm librsvg autoconf automake libtool autoconf-archive # ------------------------------------------------------------------ # vcpkg (manifest mode) + caches # ------------------------------------------------------------------ # Bootstrap a fresh clone rather than leaning on whatever vcpkg the # image carries: `builtin-baseline`/`overrides` are only honored by # a recent vcpkg-tool, and every platform must behave alike. - name: Bootstrap vcpkg if: matrix.platform != 'windows' shell: bash run: | git clone https://github.com/microsoft/vcpkg.git .cache/vcpkg .cache/vcpkg/bootstrap-vcpkg.sh -disableMetrics echo "$PWD/.cache/vcpkg" >> "$GITHUB_PATH" echo "VCPKG_ROOT=$PWD/.cache/vcpkg" >> "$GITHUB_ENV" - name: Bootstrap vcpkg (Windows) if: matrix.platform == 'windows' shell: pwsh run: | git clone https://github.com/microsoft/vcpkg.git "$env:GITHUB_WORKSPACE\.cache\vcpkg" & "$env:GITHUB_WORKSPACE\.cache\vcpkg\bootstrap-vcpkg.bat" -disableMetrics if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } "$env:GITHUB_WORKSPACE\.cache\vcpkg" >> $env:GITHUB_PATH "VCPKG_ROOT=$env:GITHUB_WORKSPACE\.cache\vcpkg" >> $env:GITHUB_ENV # The archives dir is vcpkg's binary cache: a manifest bump then # rebuilds only what changed. Every run saves under a fresh key (so # an existing cache is updated, never a save failure) and restores # the newest matching entry through `restore-keys`. WarpBuild's # cache service backs the Linux jobs (the GitHub Actions cache # quota is full); macOS/Windows keep actions/cache. - name: Restore vcpkg artifacts (WarpCache) if: matrix.platform == 'linux' id: vcpkg-cache uses: WarpBuilds/cache/restore@v2 # A job container does not inherit the runner environment; # WarpCache authenticates with this token (README: "Running # inside a container"). env: WARPBUILD_RUNNER_VERIFICATION_TOKEN: ${{ env.WARPBUILD_RUNNER_VERIFICATION_TOKEN }} with: path: | vcpkg_installed ~/.cache/vcpkg/archives key: vcpkg-${{ matrix.distro }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json') }}-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | vcpkg-${{ matrix.distro }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json') }}- vcpkg-${{ matrix.distro }}-${{ matrix.triplet }}- - name: Restore vcpkg artifacts (GitHub) if: matrix.platform != 'linux' id: vcpkg-cache-github uses: actions/cache/restore@v6 with: path: | vcpkg_installed ~/.cache/vcpkg/archives ~/AppData/Local/vcpkg/archives key: vcpkg-${{ runner.os }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json') }}-${{ github.run_id }}-${{ github.run_attempt }} restore-keys: | vcpkg-${{ runner.os }}-${{ matrix.triplet }}-${{ hashFiles('vcpkg.json') }}- vcpkg-${{ runner.os }}-${{ matrix.triplet }}- - name: Install dependencies (vcpkg manifest) if: matrix.platform != 'windows' shell: bash run: | # Source tarballs come from third-party hosts (x264 lives on # code.videolan.org); a transient connection failure aborts the # whole install — vcpkg refuses to retry that class of curl # error — so retry here. vcpkg resumes from its archive and # download caches, so a repeat attempt is cheap. for attempt in 1 2 3; do vcpkg install --triplet ${{ matrix.triplet }} && exit 0 echo "vcpkg install failed (attempt $attempt); retrying" sleep 15 done exit 1 - name: Install dependencies (vcpkg manifest, Windows) if: matrix.platform == 'windows' shell: pwsh run: | for ($i = 1; $i -le 3; $i++) { vcpkg install --triplet ${{ matrix.triplet }} if ($LASTEXITCODE -eq 0) { exit 0 } Write-Host "vcpkg install failed (attempt $i); retrying" Start-Sleep -Seconds 15 } exit 1 # Explicit restore/save pair: the old `save-always: true` on the # combined step does not actually save on a failed job (the action # deprecation warning), so a run that failed after the install left # no binary cache and the next run rebuilt FFmpeg from source. - name: Save vcpkg artifacts (WarpCache) if: always() && matrix.platform == 'linux' uses: WarpBuilds/cache/save@v2 env: WARPBUILD_RUNNER_VERIFICATION_TOKEN: ${{ env.WARPBUILD_RUNNER_VERIFICATION_TOKEN }} with: path: | vcpkg_installed ~/.cache/vcpkg/archives key: ${{ steps.vcpkg-cache.outputs.cache-primary-key }} - name: Save vcpkg artifacts (GitHub) if: always() && matrix.platform != 'linux' uses: actions/cache/save@v6 with: path: | vcpkg_installed ~/.cache/vcpkg/archives ~/AppData/Local/vcpkg/archives key: ${{ steps.vcpkg-cache-github.outputs.cache-primary-key }} # ------------------------------------------------------------------ # Build environment # ------------------------------------------------------------------ # ocio-sys builds a stub bridge unless these are set; the oak-core # ocioutils tests need the real library. # tooling/ocio-env.sh: vendored static OCIO (the [patch.crates-io] # ocio-sys tracks shaloong/ocio-rs main, whose vendored sources build # on GCC >= 16). - name: Configure build environment (Linux) if: matrix.platform == 'linux' shell: bash run: | { echo "CC=clang" echo "CXX=clang++" } >> "$GITHUB_ENV" bash tooling/ocio-env.sh >> "$GITHUB_ENV" prefix="$PWD/vcpkg_installed/${{ matrix.triplet }}" echo "FFMPEG_DIR=$prefix" >> "$GITHUB_ENV" echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig" >> "$GITHUB_ENV" echo "$prefix/tools/pkgconf" >> "$GITHUB_PATH" # vcpkg's libva/libva-drm are shared libraries that FFmpeg links # dynamically; without this path the loader picks a system libva # that may predate symbols FFmpeg uses (undefined vaMapBuffer2). echo "LD_LIBRARY_PATH=$prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" >> "$GITHUB_ENV" # The packaging tools resolve the ELF needs through ldd as well # (dpkg-shlibdeps, linuxdeploy): register the vcpkg libs with the # dynamic linker so `libva-drm.so.2` is found when a package is # assembled. echo "$prefix/lib" > /etc/ld.so.conf.d/oak-vcpkg.conf ldconfig - name: Configure build environment (macOS) if: matrix.platform == 'macos' shell: bash run: | # Vendored static OCIO (same as every non-Windows platform via # tooling/ocio-env.sh); no OCIO_INSTALL_DIR override. bash tooling/ocio-env.sh >> "$GITHUB_ENV" prefix="$PWD/vcpkg_installed/${{ matrix.triplet }}" echo "FFMPEG_DIR=$prefix" >> "$GITHUB_ENV" echo "PKG_CONFIG_PATH=$prefix/lib/pkgconfig" >> "$GITHUB_ENV" echo "$prefix/tools/pkgconf" >> "$GITHUB_PATH" - name: Configure build environment (Windows) if: matrix.platform == 'windows' shell: pwsh run: | $prefix = "$env:GITHUB_WORKSPACE\vcpkg_installed\${{ matrix.triplet }}" "FFMPEG_DIR=$prefix" >> $env:GITHUB_ENV "PKG_CONFIG_PATH=$prefix\lib\pkgconfig" >> $env:GITHUB_ENV "$prefix\tools\pkgconf" >> $env:GITHUB_PATH # The test binaries link vcpkg's dynamic DLLs (ffmpeg and its # codecs): without this the runner reports STATUS_DLL_NOT_FOUND # when the first test executable starts. "$prefix\bin" >> $env:GITHUB_PATH # Bundled OCIO: ocio-sys' vendored sources build with the MSVC # toolchain (what they need — the MSYS2 package was the # workaround, not the preference), so no OCIO_INSTALL_DIR and # no OCIO_RS_NO_MSVC_INCLUDES anywhere. "OCIO_RS_ENABLE_REAL=1" >> $env:GITHUB_ENV "OCIO_RS_LINK=static" >> $env:GITHUB_ENV # Record the resolved versions in the build log (the manifest # pins them via overrides + builtin-baseline). vcpkg list # ------------------------------------------------------------------ # Cargo caches: whole target/ dir plus ~/.cargo, shared per # distro+arch (WarpCache for Linux, GitHub for macOS/Windows). # ------------------------------------------------------------------ - name: Cache cargo artifacts (WarpCache) if: matrix.platform == 'linux' uses: WarpBuilds/rust-cache@v2 env: WARPBUILD_RUNNER_VERIFICATION_TOKEN: ${{ env.WARPBUILD_RUNNER_VERIFICATION_TOKEN }} with: shared-key: oak-ci-${{ matrix.distro }}-${{ matrix.arch }} cache-on-failure: true - name: Cache cargo artifacts (GitHub) if: matrix.platform != 'linux' uses: Swatinem/rust-cache@v2 with: shared-key: oak-ci-${{ matrix.distro }}-${{ matrix.arch }} cache-on-failure: true # ------------------------------------------------------------------ # Build & test # ------------------------------------------------------------------ # `cargo check` (not build): the Test step links the test binaries # anyway, and a full build would codegen every workspace crate twice # (once without and once with cfg(test)). - name: Build run: cargo check --workspace --locked # xvfb + 24-bit screen: the gpui #[gpui::test] tests open real # windows and render through wgpu on Mesa's software Vulkan # (lavapipe). The watchdog bounds the step: a deadlocked test # produces no output and no failure, so after 1800 s it dumps every # hung process's thread stacks and kills the suite. - name: Test (Linux) if: matrix.platform == 'linux' timeout-minutes: 45 shell: bash env: # lavapipe is present in these images: a missing adapter must # fail the GPU acceptance tests instead of silently skipping # them (Debian is the x64 reference; the other distros keep the # historical lenient policy). OAK_REQUIRE_GPU: ${{ matrix.distro == 'debian' && '1' || '0' }} run: | run_suite() { xvfb-run -a -s "-screen 0 1920x1080x24" cargo test --workspace --locked & TEST_PID=$! # The watchdog inherits the step's stdout/stderr; detach it so # it cannot keep the runner's I/O pipes open after the step # ends ("WaitDelay expired before I/O complete" otherwise). ( sleep 1800 echo "::warning::test suite exceeded 1800s; dumping hung-process stacks" for p in $(pgrep -f 'target/debug/deps/|target/debug/oak-worker'); do echo "===== thread stacks of pid $p ($(readlink /proc/$p/exe 2>/dev/null)) =====" gdb -batch -ex 'thread apply all bt' -p "$p" || true done pkill -9 -f 'target/debug/deps/' || true pkill -9 -f 'target/debug/oak-worker' || true ) >/dev/null 2>&1 & WATCHDOG_PID=$! wait $TEST_PID rc=$? kill $WATCHDOG_PID 2>/dev/null || true # Reap the watchdog so no child holds the step's pipes. wait $WATCHDOG_PID 2>/dev/null || true return $rc } # Retry once (same policy as the Windows job): worker-pool # startup under full-suite parallelism has flaked once # (full_res_worker_outlives_a_dropped_project: the render # manager's process dispatcher failed to start while every other # test passed). A real regression fails both passes. if ! run_suite; then echo "first pass failed; retrying once for worker-pool flakes" run_suite fi # The runner's GUI session doubles as the display for the gpui # #[gpui::test] windows; wgpu renders through Metal. - name: Test (macOS) if: matrix.platform == 'macos' timeout-minutes: 40 run: | # Retry once (same policy as the other platforms). A hang trips # the in-script watchdog, which samples the test processes (the # offending test's native stack lands in the log) before killing # the suite. run_suite() { cargo test --workspace --locked & TEST_PID=$! ( sleep 900 echo "::warning::macOS test suite exceeded 900s; sampling hung processes" for p in $(pgrep -f 'target/debug/deps/' || true); do echo "===== sample of pid $p =====" sample "$p" 2 10 2>&1 | head -120 || true done pkill -9 -f 'target/debug/deps/' || true pkill -9 -f 'target/debug/oak-worker' || true ) & WATCHDOG_PID=$! wait $TEST_PID rc=$? kill $WATCHDOG_PID 2>/dev/null || true wait $WATCHDOG_PID 2>/dev/null || true return $rc } if ! run_suite; then echo "first pass failed; retrying once for worker-pool flakes" run_suite fi - name: Test (Windows) if: matrix.platform == 'windows' timeout-minutes: 40 shell: pwsh run: | cargo test --workspace --locked if ($LASTEXITCODE -ne 0) { # Retry once: a few gpui keystroke tests flake on Windows CI — # a synthetic keystroke is occasionally never delivered (the # undo/redo pair and a plain 's' toggle both failed once, # each identically to its pass state). A real regression # fails both passes. Write-Host "first pass failed; retrying once for gpui keystroke flakes" cargo test --workspace --locked if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } } # ------------------------------------------------------------------ # Failure diagnostics # ------------------------------------------------------------------ # A crashing (SIGSEGV) test gives no Rust backtrace; rerun the # crashing test binaries under gdb to capture the native stack. # `--args` is required — plain `--` makes gdb treat the test args as # a core file. The extra probes target loader-stage crashes (the # copier_test SIGSEGV happens inside ld.so's dl_main): si_addr/si_code # pin down the fault type, the dynsym dump exposes symbols the # executable exports for interposition, strace shows the last loader # syscalls, and valgrind catches a corrupting static initializer. - name: Backtrace on test failure (Debian) if: failure() && matrix.distro == 'debian' shell: bash run: | apt-get install -y gdb strace valgrind for name in node_e2e_test suites_test copier_test; do BIN=$(ls -t target/debug/deps/$name-* | grep -v '\.d$' | head -1) [ -n "$BIN" ] || continue echo "===== $BIN =====" file "$BIN" || true echo "--- exported defined dynsyms:" readelf --dyn-syms -W "$BIN" 2>/dev/null | grep -v ' UND ' | tail -n +4 | head -30 || true echo "--- strace tail:" strace -f "$BIN" --list 2>&1 | tail -15 || true echo "--- valgrind tail:" valgrind -q "$BIN" --list 2>&1 | tail -25 || true echo "--- gdb:" xvfb-run -a gdb -batch \ -ex run \ -ex 'bt' \ -ex 'p $_siginfo.si_code' \ -ex 'p/x $_siginfo._sifields._sigfault.si_addr' \ -ex 'x/6i $rip' \ --args "$BIN" --nocapture || true done # Same idea for the other distro containers (gdb is installed with # the system dependencies). - name: Backtrace on test failure (containers) if: failure() && matrix.platform == 'linux' && matrix.distro != 'debian' shell: bash run: | for name in oak_render oakapp oak_plugin; do BIN=$(ls -t target/debug/deps/$name-* 2>/dev/null | grep -v '\.d$' | head -1) [ -n "$BIN" ] || continue echo "===== $BIN =====" timeout 900 xvfb-run -a gdb -batch \ -ex run \ -ex 'thread apply all bt' \ --args "$BIN" || true done # A SIGSEGV in a test binary gives no Rust backtrace; Apple's crash # reports carry the native stack, so surface the newest ones. - name: Crash reports (macOS) if: failure() && matrix.platform == 'macos' run: | for f in $(ls -t ~/Library/Logs/DiagnosticReports/*.ips 2>/dev/null | head -3); do echo "===== $f" head -c 6000 "$f" echo done # ------------------------------------------------------------------ # OFX plugin discovery end-to-end (x64 Linux reference) # ------------------------------------------------------------------ # Build a minimal but real OFX plugin into a .ofx.bundle, point # OFX_PLUGIN_PATH at it and let the scan_probe example run the full # host path (directory scan -> dlopen -> setHost -> load -> describe # -> register). The assertion is the plugin's registration line; CI # machines have no system-wide OFX plugins, so the fixture is the # only discovery. - name: Build OFX fixture plugin if: matrix.distro == 'debian' run: crates/oak-plugin/tests/fixtures/build_fixture.sh .cache/ofx-fixture - name: Probe OFX plugin discovery if: matrix.distro == 'debian' run: | OFX_PLUGIN_PATH="$PWD/.cache/ofx-fixture" \ cargo run --locked -p oak-plugin --example scan_probe > probe.log 2>&1 grep -q 'type_id=rs.oak.CiTestPlugin' probe.log # A project carrying a plugin node must survive save/load (the # serializer resolves plugin types via the dynamic factory). OAK_OFX_FIXTURE_DIR="$PWD/.cache/ofx-fixture" \ cargo test --locked -p oak-plugin --test ofx_roundtrip