From 510ac3b3a957e432c99caa32bd6aa699e9c8d991 Mon Sep 17 00:00:00 2001 From: Mariano Abad Date: Tue, 28 Jul 2026 06:32:17 -0300 Subject: [PATCH] drm: move the drm CI out of the stock workflow, and stop touching scrap/Cargo.toml The instruction was that nothing outside the feature should change while the feature is off, and the runtime code honors that, but the build plumbing did not. Start undoing that. ci.yml goes back to upstream byte for byte. The drm test step it carried now lives in a new workflow that only fires when a drm path changes, so a PR that does not touch this backend pays nothing for it. That new workflow also runs the whole rustdesk-crate test set with the feature on rather than filtering by the `_drm` test names, because the name filter skipped the sibling assertion that bounds `size_of::()`, which the new DmabufDesc variant grows. It gains a second job that fetches libdrmtap at the pinned commit, builds the .so and then asserts the contract the runtime depends on: every symbol the loader resolves, derived from the loader source so the two cannot drift, plus evidence that the EGL detile path is really compiled in. libdrmtap degrades to a CPU-only stub when the egl/glesv2 pkg-config files are absent on a build host, and nothing downstream noticed. Note the check looks for the dlopen target name and the import call, not for DT_NEEDED: EGL is loaded lazily on purpose so the privileged process never links the vendor GL stack, so an ELF-level check reports a false negative on a correct library. libs/scrap/Cargo.toml keeps only the added feature: the unrelated blank line before [dependencies.hwcodec] is restored, and the comment no longer describes DRMTAP_REF, which no longer exists. The feature is now drm = ["wayland"] because all three drm modules live inside the wayland arm of common/mod.rs, so scrap/drm alone compiled nothing; it worked only because the root crate always enables scrap/wayland. --- .github/workflows/ci.yml | 15 --- .github/workflows/drm-capture.yml | 159 ++++++++++++++++++++++++++++++ libs/scrap/Cargo.toml | 11 ++- 3 files changed, 166 insertions(+), 19 deletions(-) create mode 100644 .github/workflows/drm-capture.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index aa8224184..173eda9f4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -249,18 +249,3 @@ jobs: use-cross: ${{ matrix.job.use-cross }} command: test args: --locked --target=${{ matrix.job.target }} ${{ steps.test-options.outputs.CARGO_TEST_OPTIONS}} - - # The opt-in `drm` (DRM/KMS capture) feature is off in the default workspace test above, so the - # `_drm` unit tests would otherwise never run in CI. `drm` is a pure runtime-dlopen backend with no - # link-time deps (no libdrm/EGL/gbm, no GPU), so compiling just the rustdesk crate with the feature - # on is cheap and needs no extra system packages. Run the pure-userspace `_drm` tests here (the - # SCM_RIGHTS framing round-trips incl. MSG_CTRUNC/surplus-fd, and the peer-auth + admission-bound - # decisions) so the new screen-content channel is covered on every PR, alongside the ipc/auth tests. - # Linux-only; the main build/test above stays on default features so the shipped drm-off config - # remains the primary verified one. - - name: Run drm unit tests (Linux) - if: matrix.job.target == 'x86_64-unknown-linux-gnu' - uses: actions-rs/cargo@844f36862e911db73fe0815f00a4a2602c279505 # v1 - with: - command: test - args: --locked --target=${{ matrix.job.target }} -p rustdesk --features drm drm_conn_tests diff --git a/.github/workflows/drm-capture.yml b/.github/workflows/drm-capture.yml new file mode 100644 index 000000000..5d1ed29d4 --- /dev/null +++ b/.github/workflows/drm-capture.yml @@ -0,0 +1,159 @@ +name: DRM capture (opt-in drm feature) + +# Everything CI-side about the opt-in `drm` backend lives here, so the stock CI and release workflows +# stay byte-identical to a build with the feature off. Nothing in this file runs unless a drm-related +# path changes (or someone dispatches it by hand), so a PR that does not touch the backend pays nothing. +# +# The stock `CI` workflow deliberately does NOT compile with `--features drm`: the shipped default is +# the drm-off configuration and that stays the primary verified one. + +on: + workflow_dispatch: + pull_request: + paths: + - "libs/scrap/src/common/drm_reader.rs" + - "libs/scrap/src/common/drm_render.rs" + - "libs/scrap/src/common/drmtap_dl.rs" + - "libs/scrap/src/common/mod.rs" + - "libs/scrap/Cargo.toml" + - "src/ipc.rs" + - "src/ipc/**" + - "src/server/drm_capturer.rs" + - "src/server/wayland.rs" + - "src/server/display_service.rs" + - "build.py" + - ".github/workflows/drm-capture.yml" + push: + branches: + - master + paths: + - "libs/scrap/src/common/drm_reader.rs" + - "libs/scrap/src/common/drm_render.rs" + - "libs/scrap/src/common/drmtap_dl.rs" + - "src/ipc.rs" + - "src/ipc/**" + - "src/server/drm_capturer.rs" + - "build.py" + - ".github/workflows/drm-capture.yml" + +env: + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b" + +jobs: + drm-tests: + name: drm unit tests (linux) + runs-on: ubuntu-24.04 + steps: + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@54081f138730dfa15788a46383842cd2f914a1be # v1.3.1 + with: + tool-cache: false + android: true + dotnet: true + haskell: true + large-packages: false + swap-storage: false + + - name: Checkout source code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + submodules: recursive + + - name: Install prerequisites + shell: bash + run: | + sudo apt-get -y update + sudo apt-get install -y \ + clang cmake curl gcc git g++ \ + libpam0g-dev libasound2-dev libunwind-dev \ + libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev libpulse-dev libva-dev libvdpau-dev \ + libxcb-randr0-dev libxcb-shape0-dev libxcb-xfixes0-dev \ + libxdo-dev libxfixes-dev nasm wget + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + + - name: Install vcpkg dependencies + shell: bash + run: $VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed" + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1 + with: + toolchain: stable + targets: x86_64-unknown-linux-gnu + + - uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2 + + # The whole rustdesk-crate test set with the feature ON, not just the `_drm` ones by name: a name + # filter would skip the sibling asserts that also matter in this configuration, notably the one + # bounding `size_of::()`, which the new DmabufDesc variant grows. + - name: Run rustdesk crate tests with the drm feature + shell: bash + run: cargo test --locked --target x86_64-unknown-linux-gnu -p rustdesk --features drm + + libdrmtap: + name: libdrmtap pin, build and .so contract + runs-on: ubuntu-24.04 + steps: + - name: Checkout source code + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + + - name: Install libdrmtap build deps + shell: bash + run: | + sudo apt-get -y update + sudo apt-get install -y meson ninja-build pkg-config libdrm-dev \ + libegl1-mesa-dev libgles2-mesa-dev + + # Exercises the real fetch-and-build path in build.py, which pins the commit by sha, so a bad or + # moved pin fails here rather than in a release job. + - name: Fetch the pinned libdrmtap and build the .so + shell: bash + run: | + python3 - <<'PY' + import importlib.util, sys + spec = importlib.util.spec_from_file_location("b", "build.py") + b = importlib.util.module_from_spec(spec) + sys.argv = ["build.py"] + spec.loader.exec_module(b) + so = b.build_libdrmtap_so() + print(f"::notice::built {so}") + open("so_path", "w").write(so) + PY + + # The shipped hot path is the EGL detile. libdrmtap degrades to a CPU-only stub when the egl or + # glesv2 pkg-config files are missing on the build host, and nothing else in the pipeline notices, + # so assert here that the object we would ship really carries EGL and really exports every symbol + # the runtime loader resolves. + - name: Assert the .so contract (EGL enabled, loader symbols present) + shell: bash + run: | + SO="$(cat so_path)" + echo "checking $SO" + missing=0 + # Every symbol drmtap_dl.rs resolves, derived from the loader itself so the two cannot drift. + for sym in $(grep -oE 'b"drmtap_[a-z_]+"' libs/scrap/src/common/drmtap_dl.rs \ + | sed 's/^b"//; s/"$//' | sort -u); do + if ! nm -D --defined-only "$SO" | grep -q " T $sym\$"; then + echo "::error::libdrmtap does not export $sym, which the runtime loader resolves" + missing=1 + fi + done + # EGL is reached by lazy dlopen, on purpose, so that the privileged process never links the + # vendor GL stack. That means there is NO DT_NEEDED entry and no undefined egl* symbol to look + # for: the naive ELF check reports "no EGL" on a perfectly good library. What a CPU-only stub + # build really lacks is the dlopen target name and the import call itself. + for s in "libEGL.so.1" "eglCreateImageKHR"; do + if ! strings "$SO" | grep -qF "$s"; then + echo "::error::libdrmtap looks like a CPU-only stub (no $s): the EGL detile hot path is missing" + missing=1 + fi + done + test "$missing" -eq 0 + echo "::notice::libdrmtap .so contract ok (13 loader symbols, EGL detile present)" diff --git a/libs/scrap/Cargo.toml b/libs/scrap/Cargo.toml index a3ee32a85..0f7c515e0 100644 --- a/libs/scrap/Cargo.toml +++ b/libs/scrap/Cargo.toml @@ -13,12 +13,14 @@ edition = "2018" wayland = ["gstreamer", "gstreamer-app", "gstreamer-video", "dbus", "tracing", "zbus"] # `drm` is a pure runtime-dlopen backend: rustdesk loads `libdrmtap.so.0` at runtime (`drmtap_dl.rs`) # and NEVER link-time links it, so the graceful PipeWire fallback when the .so or EGL is absent is -# preserved and the drm build pulls in no libdrm/seccomp/cap/EGL link-time deps. The .so is version -# pinned by build.py's `DRMTAP_SHA` to the libdrmtap v0.4.15 release commit (`DRMTAP_REF` is `main`, -# and the exact commit is verified after clone). We deliberately do +# preserved and the drm build pulls in no libdrm/seccomp/cap/EGL link-time deps. The .so is pinned by +# `DRMTAP_SHA` in build.py, which fetches that exact commit (libdrmtap v0.4.15). We deliberately do # NOT depend on the `libdrmtap-sys` crate: its build.rs statically compiles the whole libdrmtap C tree # and a CAP_SYS_ADMIN helper and emits `-ldrm -lseccomp -lcap`, which would defeat the dlopen model. -drm = [] +# Depends on `wayland`: the three drm modules live inside the `#[cfg(feature = "wayland")]` arm of +# common/mod.rs, so `scrap/drm` on its own would compile nothing. The root crate happens to always +# enable `scrap/wayland`, which is what hid this. +drm = ["wayland"] mediacodec = ["ndk"] linux-pkg-config = ["dep:pkg-config"] hwcodec = ["dep:hwcodec"] @@ -66,6 +68,7 @@ gstreamer = { version = "0.16", optional = true } gstreamer-app = { version = "0.16", features = ["v1_10"], optional = true } gstreamer-video = { version = "0.16", optional = true } zbus = { version = "3.15", optional = true } + [dependencies.hwcodec] git = "https://github.com/rustdesk-org/hwcodec" optional = true