Drm deb in release workflow (#15776)

* docs(agents): add a comment-length rule

Comments were growing to document rejected alternatives, past bugs and
measurements. That belongs in the commit message, not the source.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* ci(drm): build the unattended-wayland deb in the release workflow

The deb was built by a separate drm-capture workflow on a plain runner,
so it diverged from every other Linux deb: different base, different
vcpkg/ffmpeg, different toolchain. Move it into flutter-build.yml as
build-rustdesk-linux-drm, mirroring build-rustdesk-linux's x86_64 path --
same ubuntu18.04 container, same vcpkg install, same rust and flutter.
libdrmtap is built on the runner first and handed to the container via
DRMTAP_PREBUILT_DIR, because bionic's meson is too old to build it.

The job is ungated, so the --drm packaging path is exercised on every PR;
only publishing stays gated on upload-artifact. drm-capture.yml is
deleted along with docs/DRM_CAPTURE_SECURITY.md -- the 29 drm unit tests
that workflow ran are no longer executed by CI.

Three bugs the move exposed:

- build.py anchored the libdrmtap paths on abspath(__file__), which is
  only cwd-independent on Python >= 3.9 (bpo-20443). The packaging
  container runs 3.6 and chdir's into flutter/, so the ABI-gate
  cross-check resolved one directory off and every --drm packaging run
  would have died with FileNotFoundError. Captured as REPO_ROOT at
  import instead.
- DRMTAP_PREBUILT_DIR no longer needs DRMTAP_ALLOW_UNPINNED. A prebuilt
  dir inside the repo's own third_party/libdrmtap at the pinned sha is
  the pinned object, not an override, and is now verified as such.
- The variant's Depends carried a bare libdrm2. libdrmtap needs
  drmModeGetFB2, so it is libdrm2 (>= 2.4.95); below that the package
  installed and could never capture.

The loader also logs the dlerror now instead of discarding it, so a
soname or glibc mismatch is named rather than surfacing as a generic
"libdrmtap not available".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(drm): declare the unattended-wayland deb's real libc6 and libdrm floors

libdrmtap is built on the ubuntu-22.04 runner while the rest of the deb comes
from the ubuntu18.04 container, so the package has a mixed glibc floor and
declared neither half. It installed happily on Ubuntu 20.04 / Debian 11
(glibc 2.31), then dlopen failed on GLIBC_2.34 and capture degraded to the
PipeWire portal -- the one thing this variant exists to avoid. Measure the
floor off the staged objects and put it in Depends, so apt refuses with a
reason instead of handing over a package that can never capture.

Measured rather than written down: the number moves whenever either base does,
and it lands exactly on RHEL/Rocky 9 (glibc 2.34), where one off-by-one decides
whether that whole family can install.

drmModeGetFB2 landed in libdrm 2.4.101, not 2.4.95 -- checked against the
libdrm tags, xf86drmMode.h first declares it in 2.4.101. The old floor admitted
Debian 10 (2.4.97), where the .so is linked -z now and dies on an undefined
symbol at dlopen. libdrmtap's own meson.build carries the same wrong number.

Upload the deb on always(): the run that fails the drm check is the one whose
artifact is most worth downloading. Publish stays gated on success, so an
unverified build still cannot reach a release.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
RustDesk
2026-08-07 08:31:09 +08:00
committed by GitHub
co-authored by Claude Opus 5
parent ddad47925c
commit 9a81c8a138
7 changed files with 364 additions and 723 deletions
-449
View File
@@ -1,449 +0,0 @@
name: DRM capture (opt-in drm feature)
# Least-privilege GITHUB_TOKEN. Every job here only checks out, builds and tests; the artifact
# up/download used by the deb job authenticates with the runtime token, not this one. Declared at
# the workflow level so the reusable bridge workflow called below inherits the same bound.
permissions:
contents: read
# Supersede a stale run when a PR is pushed again; never cancel a master run, whose whole job is to
# record that a given commit on master was verified.
concurrency:
group: drm-capture-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
# 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"
# The ROOT manifest is where the top-level `drm` feature is DEFINED, so a PR that changes
# what `drm` pulls in must not skip this workflow. `Cargo.lock` is deliberately NOT here:
# measured over the last 100 commits, it alone would have fired this workflow 13 times and
# the pair 24 times, each run costing about two job-hours for a full vcpkg + flutter release
# build, almost always for a dependency the drm path never touches. A lockfile bump that
# does affect it arrives with a manifest or source change, which is triggered above.
- "Cargo.toml"
- "src/ipc.rs"
- "src/ipc/**"
- "src/server/drm_capturer.rs"
- "src/server/wayland.rs"
- "src/server/display_service.rs"
# These three carry DRM wiring too (warm_availability, the cursor path in run_cursor, and the
# producer start + get_cursor/get_cursor_data), so a PR touching only them must not skip the
# whole drm verification.
- "src/server.rs"
- "src/server/input_service.rs"
- "src/platform/linux.rs"
- "build.py"
- ".github/workflows/drm-capture.yml"
push:
branches:
- master
# Deliberately the SAME list as the pull_request trigger above: a shorter one here means a push
# that touches only the missing paths (a squash merge, a direct push) skips re-verification.
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"
# The ROOT manifest is where the top-level `drm` feature is DEFINED, so a PR that changes
# what `drm` pulls in must not skip this workflow. `Cargo.lock` is deliberately NOT here:
# measured over the last 100 commits, it alone would have fired this workflow 13 times and
# the pair 24 times, each run costing about two job-hours for a full vcpkg + flutter release
# build, almost always for a dependency the drm path never touches. A lockfile bump that
# does affect it arrives with a manifest or source change, which is triggered above.
- "Cargo.toml"
- "src/ipc.rs"
- "src/ipc/**"
- "src/server/drm_capturer.rs"
- "src/server/wayland.rs"
- "src/server/display_service.rs"
# These three carry DRM wiring too (warm_availability, the cursor path in run_cursor, and the
# producer start + get_cursor/get_cursor_data), so a PR touching only them must not skip the
# whole drm verification.
- "src/server.rs"
- "src/server/input_service.rs"
- "src/platform/linux.rs"
- "build.py"
- ".github/workflows/drm-capture.yml"
env:
VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite"
VCPKG_COMMIT_ID: "120deac3062162151622ca4860575a33844ba10b"
FLUTTER_VERSION: "3.24.5"
jobs:
drm-tests:
name: drm unit tests (linux)
runs-on: ubuntu-24.04
timeout-minutes: 60
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
persist-credentials: false
- 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::<Data>()`, which the new DmabufDesc variant grows.
# The two skips are the same ones the stock CI applies: both need a real display server and fail
# on a headless runner regardless of this feature.
- 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 \
--no-fail-fast -- --skip test_get_cursor_pos --skip test_get_key_state
# The capture backend itself lives in the scrap crate, so its unit tests are a separate
# package. `--lib` keeps this to unit tests; none of them touch a device or a display server.
- name: Run scrap crate tests with the drm feature
shell: bash
run: |
cargo test --locked --target x86_64-unknown-linux-gnu -p scrap --features drm --lib
libdrmtap:
name: libdrmtap pin, build and .so contract
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- 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: |
# Strict mode is load-bearing here: without it the trailing ::notice echo would return 0
# and mask the `test "$missing" -eq 0` assertion, so the step would pass with a missing
# loader symbol or a CPU-only stub. (pipefail also keeps the grep -c pipelines honest.)
set -euo pipefail
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. The character class allows digits (a drmtap_grab_desc2 would otherwise be
# silently dropped from the loop), and the count is asserted below so a refactor of the
# loader away from b"..." literals cannot quietly turn this whole check into a no-op that
# iterates zero times and passes.
# `|| true` on the extraction pipelines: under set -e/pipefail a zero-match grep would
# abort the script before the explicit ::error guard below can say WHY it failed; the
# guard on nsyms is the intended reporter for that case.
syms=$(grep -oE 'b"drmtap_[a-z0-9_]+"' libs/scrap/src/common/drmtap_dl.rs \
| sed 's/^b"//; s/"$//' | sort -u || true)
nsyms=$(echo "$syms" | grep -c . || true)
if [ "$nsyms" -lt 13 ]; then
echo "::error::extracted only $nsyms loader symbols from drmtap_dl.rs (expected >= 13); the extraction pattern no longer matches the loader"
missing=1
fi
# Inspect the object ONCE into a variable, then match with bash's own pattern operator --
# NO PIPE ANYWHERE IN THESE CHECKS. `anything | grep -q` under `set -o pipefail` reports a
# FALSE FAILURE as soon as the producer outruns the 64 KB pipe buffer: grep -q exits at the
# first match, the producer dies on SIGPIPE (141), and pipefail makes that the pipeline's
# status, so a library that HAS the symbol is reported as missing it. Measured on a real
# EGL-enabled .so (101 KB of `strings`, both markers present): the piped form reported both
# missing and failed the step. Note the obvious repair does NOT work -- materializing the
# output and then doing `printf '%s\n' "$var" | grep -q` keeps the pipe and just swaps the
# producer, and it fails identically (measured). Today's release-sized .so happens to fit in
# the buffer, which is the only reason this has not fired yet.
exported="$(nm -D --defined-only "$SO")"
strs="$(strings "$SO")"
for sym in $syms; do
# Line-anchored: wrap in newlines so the pattern can require a whole line, the same
# thing `grep " T $sym$"` was expressing.
if [[ $'\n'"$exported"$'\n' != *$'\n'*" T $sym"$'\n'* ]]; 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 [[ "$strs" != *"$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 ($nsyms loader symbols, EGL detile present)"
# The bridge generator is a reusable workflow, so this calls the stock one instead of duplicating it.
generate-bridge:
uses: ./.github/workflows/bridge.yml
drm-deb:
name: unattended-wayland deb (verification build)
needs: generate-bridge
runs-on: ubuntu-24.04
timeout-minutes: 60
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
persist-credentials: false
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Install prerequisites
shell: bash
run: |
sudo apt-get -y update
# Same list the stock linux job needs, plus the flutter desktop toolchain and the three
# libdrmtap build deps (libdrm and the mesa-specific EGL/GLES dev packages).
sudo apt-get install -y \
clang cmake curl gcc git g++ ninja-build meson pkg-config \
libpam0g-dev libasound2-dev libunwind-dev liblzma-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 \
libdrm-dev libegl1-mesa-dev libgles2-mesa-dev
- 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
- name: Setup flutter
uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2
with:
channel: "stable"
flutter-version: ${{ env.FLUTTER_VERSION }}
- name: Patch flutter
shell: bash
run: |
cd $(dirname $(dirname $(which flutter)))
# `[[ ... ]] && cmd` as the last line makes the STEP fail once FLUTTER_VERSION moves off
# the pinned value, because the failed test becomes the script's exit status. An explicit
# if/else skips instead. Reading the values from the environment rather than interpolating
# github expressions into the script also keeps this off zizmor's template-injection list.
# (spelled out in prose: a literal expression marker here, even in a comment, is parsed by
# actionlint and breaks workflow linting.)
if [[ "$FLUTTER_VERSION" == "3.24.5" ]]; then
git apply "$GITHUB_WORKSPACE/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff"
else
echo "::notice::flutter $FLUTTER_VERSION is not 3.24.5; skipping the dropdown patch"
fi
- name: Build the unattended-wayland deb
shell: bash
run: |
set -euo pipefail
# The features have to be on the cargo line HERE, because the packaging line below passes
# --skip-cargo and never rebuilds: whatever this compiles is what ships. ASK build.py for
# the list rather than repeating it -- get_features() is the single definition of what
# these flags mean, and a hardcoded copy silently ships something other than what
# `build.py --drm` produces the moment that function changes. The flags must be the same
# on both lines for that to hold, so keep them in one variable.
DRM_BUILD_FLAGS=(--flutter --drm --hwcodec --unix-file-copy-paste)
FEATURES="$(python3 ./build.py "${DRM_BUILD_FLAGS[@]}" --print-features)"
echo "features from build.py: $FEATURES"
# Assert rather than trust: an empty or error-shaped value would otherwise become a cargo
# line that builds a stock binary, which only the staged-binary marker check would catch.
# Match whole comma-separated TOKENS, one feature at a time. A substring test would depend
# on the order get_features happens to append them (failing a correct build the day they
# are reordered) and would also match a future feature that merely contains "drm", the same
# trap build.py avoids by splitting on commas rather than testing a substring.
for want in drm drm-wake; do
case ",$FEATURES," in
*",$want,"*) ;;
*) echo "::error::build.py --print-features returned no '$want' feature: $FEATURES"; exit 1 ;;
esac
done
cargo build --locked --lib --release --features "$FEATURES"
python3 ./build.py "${DRM_BUILD_FLAGS[@]}" --skip-cargo
# build.py exits 0 on some inner failures, so assert the artifact instead of trusting the status,
# and assert the two things that make it the drm variant at all.
- name: Assert the deb is a real drm build
shell: bash
run: |
# Strict mode so the mid-script checks can fail the step (without it only the LAST
# command's status counts and the greps above it are decorative).
set -euo pipefail
# Glob into an array and assert the COUNT. `deb="$(ls ...)"` aborted on zero matches
# before its own `test -n` could report, and on several matches produced a multi-line
# value whose `mv` failed with something unrelated to the real problem.
shopt -s nullglob
debs=(rustdesk-unattended-wayland-*.deb)
if [ "${#debs[@]}" -ne 1 ]; then
echo "::error::expected exactly one rustdesk-unattended-wayland-*.deb, found ${#debs[@]}: ${debs[*]-none}"
exit 1
fi
deb="${debs[0]}"
echo "::notice::built $deb ($(stat -c %s "$deb") bytes)"
# Pipe-free for the same reason as the .so contract step above (see the comment there:
# a producer feeding a grep that can exit early is a SIGPIPE reported as a failure under
# pipefail). `grep -E` without -q reads to EOF so these two happen to be safe, but the
# shape is the hazard and the next `-q` added here would inherit it silently.
contents="$(dpkg -c "$deb")"
if [[ ! "$contents" =~ usr/lib/rustdesk/libdrmtap\.so\.0\.[0-9]+\.[0-9]+ ]]; then
echo "::error::the deb does not contain a versioned libdrmtap.so.0.x.y"
exit 1
fi
if [[ "$contents" != *"usr/lib/rustdesk/libdrmtap.so.0 ->"* ]]; then
echo "::error::the deb does not contain the libdrmtap.so.0 soname symlink"
exit 1
fi
# The library alone does not make this a drm build: build.py stages it whenever --drm is
# passed, independently of what was compiled, and the deb name is what tells a user this
# is the consent-bypass variant. Assert the BINARY too, by the absolute dlopen path that
# only exists when the feature is compiled in -- otherwise a stock binary could ship
# under the unattended-wayland name with a library it can never reach.
rm -rf /tmp/debassert && dpkg-deb -R "$deb" /tmp/debassert
if ! grep -qF /usr/lib/rustdesk/libdrmtap.so.0 /tmp/debassert/usr/share/rustdesk/lib/librustdesk.so; then
echo "::error::the packaged librustdesk.so has no libdrmtap dlopen path; this is not a drm build"
exit 1
fi
mv "$deb" "${deb%.deb}-x86_64.deb"
# MEASURE the glibc floor rather than describing it. This job builds on the runner instead of the
# ubuntu18.04 container the stock release debs use, so the artifact only runs on a host at least
# as new as the runner -- and that number belongs in the artifact NAME, because a comment in this
# file is not visible to whoever downloads it from the Actions UI.
- name: Measure the deb glibc floor
id: floor
shell: bash
run: |
# Strict mode for the same reason as the assert step above. The floor extraction gets an
# explicit rescue so a no-match grep reaches the `test -n` reporter instead of dying as a
# bare pipeline failure.
set -euo pipefail
# Same nullglob array + count assertion as the assert step above, for the same two
# reasons: under set -e a zero-match `ls` aborts before anything can report WHY, and
# several matches make `deb` multi-line so dpkg-deb fails with an unrelated error. (This
# was the sibling left behind when that one was fixed.)
shopt -s nullglob
debs=(rustdesk-unattended-wayland-*-x86_64.deb)
if [ "${#debs[@]}" -ne 1 ]; then
echo "::error::expected exactly one renamed deb to measure, found ${#debs[@]}: ${debs[*]-none}"
exit 1
fi
deb="${debs[0]}"
rm -rf /tmp/debfloor && dpkg-deb -R "$deb" /tmp/debfloor
floor="$(objdump -T /tmp/debfloor/usr/share/rustdesk/lib/librustdesk.so \
| grep -oE 'GLIBC_2\.[0-9]+' | sort -uV | tail -1 || true)"
test -n "$floor"
echo "floor=${floor#GLIBC_}" >> "$GITHUB_OUTPUT"
echo "::notice::deb requires ${floor} or newer (built on the runner, not the ubuntu18.04 release container)"
# Verification artifact, deliberately NOT a release deliverable. The consent-free variant stays
# out of the published release either way; the name states the floor so nobody installs it on an
# older distro and hits a bare loader error.
- name: Upload the deb
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: rustdesk-unattended-wayland-x86_64-verification-glibc${{ steps.floor.outputs.floor }}.deb
path: rustdesk-unattended-wayland-*-x86_64.deb
+270
View File
@@ -1749,6 +1749,276 @@ jobs:
files: |
res/rustdesk-${{ env.VERSION }}*.zst
# Same build as build-rustdesk-linux x86_64 -- same vcpkg/ffmpeg, same ubuntu18.04 container, same
# rust and flutter -- only with the drm feature on, so it ships as the separate
# rustdesk-unattended-wayland deb. libdrmtap is built on the runner because bionic's meson is too
# old for it. A separate job rather than a matrix entry of build-rustdesk-linux: appimage and
# flatpak need that job, and a failure here must not skip them.
build-rustdesk-linux-drm:
needs: [generate-bridge]
name: build rustdesk linux drm x86_64
runs-on: ubuntu-22.04
steps:
- name: Export GitHub Actions cache environment variables
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6
with:
script: |
core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || '');
core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || '');
- name: Maximize build space
run: |
sudo rm -rf /opt/ghc
sudo rm -rf /usr/local/lib/android
sudo rm -rf /usr/share/dotnet
sudo apt-get update -y
sudo apt-get install -y nasm
sudo apt-get install -y qemu-user-static
- name: Checkout source code
uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
submodules: recursive
- name: Set Swap Space
uses: pierotofy/set-swap-space@49819abfb41bd9b44fb781159c033dba90353a7c # v1.0
with:
swap-size-gb: 12
- name: Free Space
run: |
df -h
free -m
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@e97e2d8cc328f1b50210efc529dca0028893a2d9 # v1
with:
toolchain: ${{ env.RUST_VERSION }}
targets: x86_64-unknown-linux-gnu
components: "rustfmt"
- name: Save Rust toolchain version
run: |
RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}')
echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV
- name: Disable rust bridge build
run: |
# only build cdylib
sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml
- name: Restore bridge files
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: bridge-artifact
path: ./
- name: Setup vcpkg with Github Actions binary cache
uses: lukka/run-vcpkg@b1a0dd252f06b9e25b3c022a9a03bd7a427fb6a2 # v11
with:
vcpkgDirectory: /opt/artifacts/vcpkg
vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }}
doNotCache: false
- name: Install vcpkg dependencies
run: |
sudo apt install -y libva-dev && apt show libva-dev
if ! $VCPKG_ROOT/vcpkg \
install \
--triplet x64-linux \
--x-install-root="$VCPKG_ROOT/installed"; then
find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do
echo "$_1:"
echo "======"
cat "$_1"
echo "======"
echo ""
done
exit 1
fi
head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-x64-linux-rel-out.log" || true
shell: bash
# The container's meson is too old to build libdrmtap, so build it here from the pin in
# build.py and hand the .so to the container below via DRMTAP_PREBUILT_DIR.
- name: Build libdrmtap
run: |
sudo apt-get install -y meson ninja-build pkg-config \
libdrm-dev libegl1-mesa-dev libgles2-mesa-dev
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)
print(f"::notice::built {b.build_libdrmtap_so()}")
PY
shell: bash
- uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 # no release tag; commit 2026-05-26
name: Build rustdesk
id: vcpkg
with:
arch: x86_64
distro: ubuntu18.04
githubToken: ${{ github.token }}
setup: |
ls -l "${PWD}"
ls -l /opt/artifacts/vcpkg/installed
dockerRunArgs: |
--volume "${PWD}:/workspace"
--volume "/opt/artifacts:/opt/artifacts"
shell: /bin/bash
install: |
apt-get update -y
echo -e "installing deps"
apt-get install -y \
build-essential \
clang \
cmake \
curl \
gcc \
git \
g++ \
libayatana-appindicator3-dev \
libasound2-dev \
libclang-10-dev \
libgstreamer1.0-dev \
libgstreamer-plugins-base1.0-dev \
libgtk-3-dev \
libpam0g-dev \
libpulse-dev \
libva-dev \
libxcb-randr0-dev \
libxcb-shape0-dev \
libxcb-xfixes0-dev \
libxdo-dev \
libxfixes-dev \
llvm-10-dev \
nasm \
ninja-build \
pkg-config \
tree \
python3 \
rpm \
unzip \
wget \
xz-utils \
libssl-dev
# we have libopus compiled by us.
apt-get remove -y libopus-dev || true
# output devs
ls -l ./
tree -L 3 /opt/artifacts/vcpkg/installed
run: |
# disable git safe.directory
git config --global --add safe.directory "*"
# rust
pushd /opt
# do not use rustup, because memory overflow in qemu
wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu.tar.gz
tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz
cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu && ./install.sh
rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu
# edit config
mkdir -p ~/.cargo/
echo """
[source.crates-io]
registry = 'https://github.com/rust-lang/crates.io-index'
""" > ~/.cargo/config
cat ~/.cargo/config
# start build
pushd /workspace
export VCPKG_ROOT=/opt/artifacts/vcpkg
# use the .so built on the runner; build.py checks it is the pinned checkout
export DRMTAP_PREBUILT_DIR=/workspace/third_party/libdrmtap/build-pkg
# ask build.py for the features so this line and the packaging line cannot drift
FEATURES=$(python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --print-features)
# an empty or error-shaped value would silently build a stock binary
for want in drm drm-wake; do
case ",$FEATURES," in
*",$want,"*) ;;
*) echo "::error::build.py returned no '$want' feature: $FEATURES"; exit 1 ;;
esac
done
cargo build --locked --lib --features "$FEATURES" --release
rm -rf target/release/deps target/release/build
rm -rf ~/.cargo
# Setup Flutter
# disable git safe.directory
git config --global --add safe.directory "*"
export PATH=/opt/flutter/bin:$PATH
pushd /opt
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz
flutter doctor -v
if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then
pushd /opt/flutter
git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff
popd
fi
# build flutter
pushd /workspace
export CARGO_INCREMENTAL=0
export DEB_ARCH=amd64
python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --skip-cargo
for name in rustdesk*??.deb; do
mv "$name" "${name%%.deb}-x86_64.deb"
done
# build.py can exit 0 on some inner failures, so check the artifact rather than the status.
# The package name is the informed consent for consent-free capture, so a stock binary must
# never ship under it: assert the bundled library AND the dlopen path in the binary.
- name: Check the deb is a drm build
run: |
set -euo pipefail
# Resolve by glob, not from env.VERSION: build.py names the deb from Cargo.toml, so a
# hardcoded name fails with a bare exit 1 the first time those two drift.
shopt -s nullglob
debs=(rustdesk-unattended-wayland-*-x86_64.deb)
if [ "${#debs[@]}" -ne 1 ]; then
echo "::error::expected one rustdesk-unattended-wayland-*-x86_64.deb, found ${#debs[@]}: ${debs[*]-none}"
exit 1
fi
deb="${debs[0]}"
echo "DRM_DEB=$deb" >> "$GITHUB_ENV"
contents="$(dpkg -c "$deb")"
if [[ ! "$contents" =~ usr/lib/rustdesk/libdrmtap\.so\.0\.[0-9]+\.[0-9]+ ]]; then
echo "::error::$deb has no versioned libdrmtap.so.0.x.y"
exit 1
fi
if [[ "$contents" != *"usr/lib/rustdesk/libdrmtap.so.0 ->"* ]]; then
echo "::error::$deb has no libdrmtap.so.0 soname symlink"
exit 1
fi
rm -rf /tmp/deb && dpkg-deb -R "$deb" /tmp/deb
if ! grep -qF /usr/lib/rustdesk/libdrmtap.so.0 /tmp/deb/usr/share/rustdesk/lib/librustdesk.so; then
echo "::error::$deb was not built with the drm feature"
exit 1
fi
shell: bash
- name: Publish debian package
if: env.UPLOAD_ARTIFACT == 'true'
uses: softprops/action-gh-release@de2c0eb89ae2a093876385947365aca7b0e5f844 # v1
with:
prerelease: true
tag_name: ${{ env.TAG_NAME }}
files: |
${{ env.DRM_DEB }}
# No UPLOAD_ARTIFACT gate: on a PR this is the only way to get at the deb that was just built.
# always(), because a deb that failed the check above is the one most worth downloading.
- name: Upload deb
if: always() && env.DRM_DEB != ''
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: ${{ env.DRM_DEB }}
path: ${{ env.DRM_DEB }}
build-rustdesk-linux-sciter:
if: ${{ inputs.upload-artifact }}
runs-on: ${{ matrix.job.on }}