mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-05 15:41:23 +03:00
Three findings from the round on the wake-gate commits, all the same shape: the gate made "what was asked for" and "what was produced" diverge, and two places still trusted the first. CI built the unattended-wayland deb with `--features ...,drm` and then packaged it with `--skip-cargo`. build.py appends `drm-wake` for `--drm`, but skipping cargo means whatever that explicit line compiled is what ships, so the deb had no wake code in it at all while being named and documented as the variant that has it. The feature list has to be complete on the line that actually builds. The marker assertion that catches exactly this class only guarded one of the two packaging paths. `build_deb_from_folder` asserts that the staged binary carries the libdrmtap dlopen path before it takes the unattended-wayland name; the flutter path did not, and `--skip-cargo` reaches that one. A stock binary could therefore be packaged under a name that conflicts with and replaces the stock package, and then never capture. Hoisted the check to module level and called it from both, before the bundle is renamed. And the security doc described the synthetic input injection as an unconditional property of a drm build. It is behind its own compile feature and a runtime option, which is exactly what an operator auditing the deb needs to know.
428 lines
21 KiB
YAML
428 lines
21 KiB
YAML
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, and the lock file is
|
|
# what every cargo invocation here resolves against (`--locked`). Without these two, a PR
|
|
# that changes what `drm` pulls in, or bumps a dependency the drm path uses, skips the
|
|
# entire verification this workflow exists for.
|
|
- "Cargo.toml"
|
|
- "Cargo.lock"
|
|
- "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, and the lock file is
|
|
# what every cargo invocation here resolves against (`--locked`). Without these two, a PR
|
|
# that changes what `drm` pulls in, or bumps a dependency the drm path uses, skips the
|
|
# entire verification this workflow exists for.
|
|
- "Cargo.toml"
|
|
- "Cargo.lock"
|
|
- "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: |
|
|
# drm-wake must be listed HERE: build.py adds it for --drm, but the next line passes
|
|
# --skip-cargo, so build.py never rebuilds and whatever this line compiled is what ships.
|
|
cargo build --locked --lib --release --features hwcodec,flutter,unix-file-copy-paste,drm,drm-wake
|
|
python3 ./build.py --flutter --drm --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
|