Files
rustdesk/build.py
Mariano Abad 1647420993 drm: close the full-review findings (a third latched flag, and two escapees)
The one that matters: the display-cache refresh worker was the THIRD copy of
the wedged-flag hazard. catch_unwind covered only the enumeration, and
thread::spawn panics on EAGAIN after RUNNING was already swapped true, so
either path parked the flag for the process lifetime and every later refresh
- including every udev hotplug - returned early forever. Same ownership
guard as UINPUT_REFRESH_BUSY (the flag is handed back and re-taken mid-loop,
so an unconditional RAII release would clear a replacement worker's flag),
plus a fallible spawn whose failure drops the closure and releases the slot.
DRM_PROBE_IN_FLIGHT, UINPUT_REFRESH_BUSY, now this: the lesson stays
'grep for every site with the shape', and twice was not enough.

Two findings had been flagged in an earlier round and escaped the ledger:
- an unrecognized convert-output fourcc fell through to 'present as BGRA'
  with a debug log, where every sibling validation in that function is a
  hard error that lets the caller fall back to PipeWire. A 64bpp output
  passes the stride check and encodes garbage. Hard error now.
- the trust-boundary validation constants (fourccs, MAX_DIM,
  MAX_FRAME_BYTES) were declared independently on both sides of the split.
  Hoisted into drm_reader, imported by the converter, so the two halves
  cannot drift apart about what data they will touch.

The rest:
- the CI symbol extraction dropped any loader symbol containing a digit and
  degraded to a pass-with-zero-iterations no-op if the b"..." literals were
  ever refactored; digits allowed, count asserted, notice de-hardcoded.
- 'drm' in features was a substring test on the comma-joined string, so a
  future drm-lease feature would have shipped the consent-bypass deb
  without --drm. Exact membership now.
- the security doc claimed the deb is built on an ubuntu18.04 container;
  the only deb job runs on ubuntu-24.04. The 18.04 sentence now says what
  is true: 2.4.95 is an API floor, the binary floor is the build host's.
- DRM_DISPLAY_CACHE poison handling was recover-in-the-writer,
  panic-in-the-readers; both readers now recover like the writer.
- the producer prewarm ran on X11 where no consumer can connect, the same
  inconsistency just fixed for warm_availability. The listener still starts
  (the service outlives sessions; a later Wayland login must find the
  socket), only the prewarm is skipped.
2026-07-29 20:05:45 -03:00

909 lines
42 KiB
Python
Executable File

#!/usr/bin/env python3
import os
import glob
import pathlib
import platform
import zipfile
import urllib.request
import shutil
import hashlib
import re
import subprocess
import argparse
import sys
from pathlib import Path
windows = platform.platform().startswith('Windows')
osx = platform.platform().startswith(
'Darwin') or platform.platform().startswith("macOS")
hbb_name = 'rustdesk' + ('.exe' if windows else '')
exe_path = 'target/release/' + hbb_name
if windows:
win_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x64'
flutter_build_dir = f'build/windows/{win_arch}/runner/Release/'
elif osx:
flutter_build_dir = 'build/macos/Build/Products/Release/'
else:
flutter_build_dir = 'build/linux/x64/release/bundle/'
flutter_build_dir_2 = f'flutter/{flutter_build_dir}'
skip_cargo = False
def get_deb_arch() -> str:
custom_arch = os.environ.get("DEB_ARCH")
if custom_arch is None:
return "amd64"
return custom_arch
def get_deb_extra_depends() -> str:
custom_arch = os.environ.get("DEB_ARCH")
if custom_arch == "armhf": # for arm32v7 libsciter-gtk.so
return ", libatomic1"
return ""
def system2(cmd):
exit_code = os.system(cmd)
if exit_code != 0:
sys.stderr.write(f"Error occurred when executing: `{cmd}`. Exiting.\n")
sys.exit(-1)
def get_version():
with open("Cargo.toml", encoding="utf-8") as fh:
for line in fh:
if line.startswith("version"):
return line.replace("version", "").replace("=", "").replace('"', '').strip()
return ''
def parse_rc_features(feature):
available_features = {}
apply_features = {}
if not feature:
feature = []
def platform_check(platforms):
if windows:
return 'windows' in platforms
elif osx:
return 'osx' in platforms
else:
return 'linux' in platforms
def get_all_features():
features = []
for (feat, feat_info) in available_features.items():
if platform_check(feat_info['platform']):
features.append(feat)
return features
if isinstance(feature, str) and feature.upper() == 'ALL':
return get_all_features()
elif isinstance(feature, list):
if windows:
# download third party is deprecated, we use github ci instead.
# feature.append('PrivacyMode')
pass
for feat in feature:
if isinstance(feat, str) and feat.upper() == 'ALL':
return get_all_features()
if feat in available_features:
if platform_check(available_features[feat]['platform']):
apply_features[feat] = available_features[feat]
else:
print(f'Unrecognized feature {feat}')
return apply_features
else:
raise Exception(f'Unsupported features param {feature}')
def make_parser():
parser = argparse.ArgumentParser(description='Build script.')
parser.add_argument(
'-f',
'--feature',
dest='feature',
metavar='N',
type=str,
nargs='+',
default='',
help='Integrate features, windows only.'
'Available: [Not used for now]. Special value is "ALL" and empty "". Default is empty.')
parser.add_argument('--flutter', action='store_true',
help='Build flutter package', default=False)
parser.add_argument(
'--hwcodec',
action='store_true',
help='Enable feature hwcodec' + (
'' if windows or osx else ', need libva-dev.')
)
parser.add_argument(
'--vram',
action='store_true',
help='Enable feature vram, only available on windows now.'
)
parser.add_argument(
'--portable',
action='store_true',
help='Build windows portable'
)
parser.add_argument(
'--unix-file-copy-paste',
action='store_true',
help='Build with unix file copy paste feature'
)
parser.add_argument(
'--drm',
action='store_true',
help='Linux only: build the DRM/KMS capture backend (bundles libdrmtap.so, '
'dlopen-ed in-process by the root service). Off by default.'
)
parser.add_argument(
'--skip-cargo',
action='store_true',
help='Skip cargo build process, only flutter version + Linux supported currently'
)
if windows:
parser.add_argument(
'--skip-portable-pack',
action='store_true',
help='Skip packing, only flutter version + Windows supported'
)
parser.add_argument(
"--package",
type=str
)
if osx:
parser.add_argument(
'--screencapturekit',
action='store_true',
help='Enable feature screencapturekit'
)
return parser
# Generate build script for docker
#
# it assumes all build dependencies are installed in environments
# Note: do not use it in bare metal, or may break build environments
def generate_build_script_for_docker():
with open("/tmp/build.sh", "w") as f:
f.write('''
#!/bin/bash
# environment
export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation: " | cut -d' ' -f4-)/include"
# flutter
pushd /opt
wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_3.0.5-stable.tar.xz
tar -xvf flutter_linux_3.0.5-stable.tar.xz
export PATH=`pwd`/flutter/bin:$PATH
popd
# flutter_rust_bridge
dart pub global activate ffigen --version 5.0.1
pushd /tmp && git clone https://github.com/SoLongAndThanksForAllThePizza/flutter_rust_bridge --depth=1 && popd
pushd /tmp/flutter_rust_bridge/frb_codegen && cargo install --path . --locked && popd
pushd flutter && flutter pub get && popd
~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart
# install vcpkg
pushd /opt
export VCPKG_ROOT=`pwd`/vcpkg
git clone https://github.com/microsoft/vcpkg
vcpkg/bootstrap-vcpkg.sh
popd
$VCPKG_ROOT/vcpkg install --x-install-root="$VCPKG_ROOT/installed"
# build rustdesk
./build.py --flutter --hwcodec
''')
system2("chmod +x /tmp/build.sh")
system2("bash /tmp/build.sh")
# Downloading third party resources is deprecated.
# We can use this function in an offline build environment.
# Even in an online environment, we recommend building third-party resources yourself.
def download_extract_features(features, res_dir):
import re
proxy = ''
def req(url):
if not proxy:
return url
else:
r = urllib.request.Request(url)
r.set_proxy(proxy, 'http')
r.set_proxy(proxy, 'https')
return r
for (feat, feat_info) in features.items():
includes = feat_info['include'] if 'include' in feat_info and feat_info['include'] else []
includes = [re.compile(p) for p in includes]
excludes = feat_info['exclude'] if 'exclude' in feat_info and feat_info['exclude'] else []
excludes = [re.compile(p) for p in excludes]
print(f'{feat} download begin')
download_filename = feat_info['zip_url'].split('/')[-1]
checksum_md5_response = urllib.request.urlopen(
req(feat_info['checksum_url']))
for line in checksum_md5_response.read().decode('utf-8').splitlines():
if line.split()[1] == download_filename:
checksum_md5 = line.split()[0]
filename, _headers = urllib.request.urlretrieve(feat_info['zip_url'],
download_filename)
md5 = hashlib.md5(open(filename, 'rb').read()).hexdigest()
if checksum_md5 != md5:
raise Exception(f'{feat} download failed')
print(f'{feat} download end. extract bein')
zip_file = zipfile.ZipFile(filename)
zip_list = zip_file.namelist()
for f in zip_list:
file_exclude = False
for p in excludes:
if p.match(f) is not None:
file_exclude = True
break
if file_exclude:
continue
file_include = False if includes else True
for p in includes:
if p.match(f) is not None:
file_include = True
break
if file_include:
print(f'extract file {f}')
zip_file.extract(f, res_dir)
zip_file.close()
os.remove(download_filename)
print(f'{feat} extract end')
def external_resources(flutter, args, res_dir):
features = parse_rc_features(args.feature)
if not features:
return
print(f'Build with features {list(features.keys())}')
if os.path.isdir(res_dir) and not os.path.islink(res_dir):
shutil.rmtree(res_dir)
elif os.path.exists(res_dir):
raise Exception(f'Find file {res_dir}, not a directory')
os.makedirs(res_dir, exist_ok=True)
download_extract_features(features, res_dir)
if flutter:
os.makedirs(flutter_build_dir_2, exist_ok=True)
for f in pathlib.Path(res_dir).iterdir():
print(f'{f}')
if f.is_file():
shutil.copy2(f, flutter_build_dir_2)
else:
shutil.copytree(f, f'{flutter_build_dir_2}{f.stem}')
def get_features(args):
features = ['inline'] if not args.flutter else []
if args.hwcodec:
features.append('hwcodec')
if args.vram:
features.append('vram')
if args.flutter:
features.append('flutter')
if args.unix_file_copy_paste:
features.append('unix-file-copy-paste')
if args.drm:
# Say so rather than quietly handing back a stock build: the backend is Linux-only, so on
# any other host the flag cannot be honoured and the resulting binary would look like a
# DRM build without being one.
if windows or osx:
raise Exception('--drm is Linux only')
features.append('drm')
if osx:
if args.screencapturekit:
features.append('screencapturekit')
print("features:", features)
return features
def generate_control_file(version):
control_file_path = "../res/DEBIAN/control"
system2('/bin/rm -rf %s' % control_file_path)
content = """Package: rustdesk
Section: net
Priority: optional
Version: %s
Architecture: %s
Maintainer: rustdesk <info@rustdesk.com>
Homepage: https://rustdesk.com
Depends: libgtk-3-0t64 | libgtk-3-0, libxcb-randr0, libxdo3 | libxdo4, libxfixes3, libxcb-shape0, libxcb-xfixes0, libasound2t64 | libasound2, libsystemd0, curl, libva2, libva-drm2, libva-x11-2, libgstreamer-plugins-base1.0-0, libpam0g, gstreamer1.0-pipewire%s
Recommends: libayatana-appindicator3-1
Description: A remote control software.
""" % (version, get_deb_arch(), get_deb_extra_depends())
file = open(control_file_path, "w")
file.write(content)
file.close()
def ffi_bindgen_function_refactor():
# workaround ffigen
system2(
'sed -i "s/ffi.NativeFunction<ffi.Bool Function(DartPort/ffi.NativeFunction<ffi.Uint8 Function(DartPort/g" flutter/lib/generated_bridge.dart')
# libdrmtap is fetched at build time from the rustdesk-org fork at a pinned
# commit — the same way rustdesk sources its other native build deps (vcpkg,
# flutter_rust_bridge, ...), rather than carrying a git submodule. It is the ONLY
# pin for the drm backend: rustdesk dlopens this .so at runtime and does not depend on
# the libdrmtap-sys crate (whose build.rs would statically link the C tree, a helper and
# libdrm/seccomp/cap). DRMTAP_REPO, DRMTAP_SHA and DRMTAP_PREBUILT_DIR override it for local testing
# or another fork, and each requires DRMTAP_ALLOW_UNPINNED=1 alongside it (see below).
# The commit is fetched directly by sha, so no branch or tag name takes part in the build: see
# build_libdrmtap_so(). This is the SINGLE source of truth for the pin, deliberately not duplicated in
# any workflow, so a bump is one edit here (plus the informational version comment in
# libs/scrap/Cargo.toml). This commit is libdrmtap v0.4.15.
LIBDRMTAP_REPO_PINNED = 'https://github.com/rustdesk-org/libdrmtap'
LIBDRMTAP_SHA_PINNED = 'cbc5e6af5b353b6bc351072a27a5351d82ba66e3'
LIBDRMTAP_REPO = os.environ.get('DRMTAP_REPO', LIBDRMTAP_REPO_PINNED)
LIBDRMTAP_SHA = os.environ.get('DRMTAP_SHA', LIBDRMTAP_SHA_PINNED)
# Every way of getting a different .so than the pin needs the same explicit opt-in. Otherwise the
# claim this feature rests on -- that the privileged capture library is the reviewed object at
# LIBDRMTAP_SHA_PINNED -- would hold only as long as nobody happened to have one of these set, and a
# build that silently used something else would be indistinguishable from one that did not.
# DRMTAP_PREBUILT_DIR is in the list because it is the widest of the three: it skips both the fetch
# and the sha verification and hands over an object built from nothing this script can see.
DRMTAP_UNPINNED_OK = os.environ.get('DRMTAP_ALLOW_UNPINNED') == '1'
_overridden = [
name
for name, value, pinned in (
('DRMTAP_REPO', LIBDRMTAP_REPO, LIBDRMTAP_REPO_PINNED),
('DRMTAP_SHA', LIBDRMTAP_SHA, LIBDRMTAP_SHA_PINNED),
# `or None` so an empty value reads as unset here exactly as it does in
# build_libdrmtap_so(), which tests it for truthiness. Otherwise `DRMTAP_PREBUILT_DIR=`
# would demand the opt-in for an override that is not going to happen.
('DRMTAP_PREBUILT_DIR', os.environ.get('DRMTAP_PREBUILT_DIR') or None, None),
)
if value != pinned
]
if _overridden and not DRMTAP_UNPINNED_OK:
raise Exception(
f'{", ".join(_overridden)} would build libdrmtap from something other than the pinned '
f'{LIBDRMTAP_REPO_PINNED} at {LIBDRMTAP_SHA_PINNED}. That is supported for local work and '
'cross-builds, but it has to be deliberate: set DRMTAP_ALLOW_UNPINNED=1 as well.')
if _overridden:
print(f'WARNING: libdrmtap is NOT the pinned build ({", ".join(_overridden)} set)')
# Both are interpolated into shell commands below, and both are env-overridable, so validate their
# SHAPE before they get there. This is not only about a hostile environment: a truncated or
# abbreviated sha would otherwise reach `git fetch` and fail with something far less obvious than
# saying so here, and an abbreviated one would defeat the point of pinning.
if not re.fullmatch(r'[0-9a-f]{40}', LIBDRMTAP_SHA):
raise Exception(
f'DRMTAP_SHA must be a full 40-character commit sha, got {LIBDRMTAP_SHA!r}')
if not re.fullmatch(r'(https://|git@)[A-Za-z0-9._~:/@-]+', LIBDRMTAP_REPO):
raise Exception(f'DRMTAP_REPO does not look like a git remote url: {LIBDRMTAP_REPO!r}')
def _single_real_so(paths, where):
# Return the one real libdrmtap.so.0.* object among `paths`, failing if there are zero or several.
# glob order is arbitrary, so silently taking [0] could ship a stale or wrong-arch object left
# over from an earlier build; a mismatch should fail the build loudly instead.
real = sorted(p for p in paths if os.path.isfile(p) and not os.path.islink(p))
if len(real) != 1:
raise Exception(
f'expected exactly one real libdrmtap.so.0.* in {where}, found {len(real)}: {real}')
return real[0]
def build_libdrmtap_so():
# Build libdrmtap.so from the rustdesk-org fork, fetched at the pinned LIBDRMTAP_SHA. The
# pivot dlopen-s this .so in-process in the root service (which already holds
# CAP_SYS_ADMIN) — no setcap helper, no privileged child. Only the shared
# library target is built (the source also carries a helper binary we do not
# ship). Returns the path to the built versioned .so (e.g. libdrmtap.so.0.4.x).
repo_root = os.path.dirname(os.path.abspath(__file__))
# Allow a caller (e.g. CI) to build the .so ahead of time and hand it in via
# DRMTAP_PREBUILT_DIR (must contain the real libdrmtap.so.0.* object).
prebuilt_dir = os.environ.get('DRMTAP_PREBUILT_DIR')
if prebuilt_dir:
# DRMTAP_PREBUILT_DIR explicitly names the artifact source, so honor it strictly: fail
# (rather than silently falling back to a source build) if it holds no single real .so.
prebuilt = glob.glob(os.path.join(prebuilt_dir, 'libdrmtap.so.0.*'))
return _single_real_so(prebuilt, f'DRMTAP_PREBUILT_DIR={prebuilt_dir}')
# Fetch the pinned source if it is not already present. third_party/libdrmtap is not a submodule
# anymore; it is git-ignored. The commit is fetched BY SHA rather than by cloning a branch:
# `clone --depth 1 --branch main` only ever fetches the tip, so the moment upstream pushes to
# `main` the pinned commit is not in the shallow clone at all and the build fails on an unreachable
# object. Fetching the sha needs no branch name, so it keeps working across every upstream push and
# is immune to a ref being moved or repointed.
src = os.path.join(repo_root, 'third_party', 'libdrmtap')
if not os.path.exists(os.path.join(src, 'meson.build')):
if os.path.isdir(src):
shutil.rmtree(src)
os.makedirs(src, exist_ok=True)
system2(f'git -C {src} init -q')
system2(f'git -C {src} remote add origin {LIBDRMTAP_REPO}')
system2(f'git -C {src} fetch --depth 1 origin {LIBDRMTAP_SHA}')
system2(f'git -C {src} checkout -q FETCH_HEAD')
# Verify the pin whenever the source is a GIT checkout. A fetch by sha cannot resolve to anything
# else, so this now guards the OTHER case: a reused checkout left by an earlier build at a
# different pin, which is what a bump leaves behind. Reject and remove it so the next run re-fetches
# cleanly. A NON-git tree placed here on purpose (a developer building unreleased local libdrmtap
# source) has nothing to verify and is used as-is.
if os.path.isdir(os.path.join(src, '.git')):
got_sha = subprocess.check_output(
['git', '-C', src, 'rev-parse', 'HEAD']).decode().strip()
if got_sha != LIBDRMTAP_SHA:
shutil.rmtree(src, ignore_errors=True)
raise Exception(
f'libdrmtap at {src} is {got_sha}, expected {LIBDRMTAP_SHA} '
f'(stale checkout from a different pin; removed, re-run to re-fetch)')
build_dir = os.path.join(src, 'build-pkg')
if not os.path.exists(os.path.join(build_dir, 'build.ninja')):
system2(f'meson setup {build_dir} {src} --buildtype=release')
# Build only the shared library, not the bundled helper binary or the static archive. Since
# libdrmtap 0.4.11 the project is `both_libraries` (a version-scripted .so + a static .a), so the
# bare `drmtap` target is ambiguous ("drmtap:shared_library" vs "drmtap:static_library"); ask for
# the shared one explicitly (rustdesk dlopens the .so and never needs the archive).
system2(f'meson compile -C {build_dir} drmtap:shared_library')
sos = glob.glob(os.path.join(build_dir, 'libdrmtap.so.0.*'))
# keep the real object (libdrmtap.so.0.4.x), not the .so/.so.0 symlinks or meson's .p dir, and
# require exactly one so a stale object from an earlier build is never silently picked.
so = _single_real_so(sos, f'the libdrmtap meson build dir {build_dir}')
_assert_so_has_egl(so)
return so
def _assert_so_has_egl(so_path):
# libdrmtap treats egl/glesv2 as OPTIONAL dependencies: without their headers and pkg-config
# files, meson silently builds a CPU-only stub. That stub still exports every symbol the loader
# checks for, so nothing downstream notices -- and the split architecture depends entirely on the
# unprivileged side EGL-detiling the scanout it receives. The result is a build where DRM capture
# quietly degrades to PipeWire on every tiled-scanout host, which is most of them.
#
# Assert on the ARTIFACT rather than passing an option that demands it: `-Degl=enabled` exists
# only in libdrmtap past 0.4.15, and checking what was actually produced also catches a stale or
# hand-substituted object, which a build flag cannot.
#
# EGL is reached by lazy dlopen, on purpose, so that the privileged service never links the GPU
# stack. That means there is no DT_NEEDED to look for and an ELF-level check reports "no EGL" on a
# perfectly good library; the dlopen name and an extension symbol are what a CPU-only stub really
# lacks. Same two markers the drm-capture workflow asserts in CI.
try:
with open(so_path, 'rb') as f:
blob = f.read()
except OSError as err:
raise Exception(f'cannot read the built libdrmtap at {so_path}: {err}')
missing = [m for m in (b'libEGL.so.1', b'eglCreateImageKHR') if m not in blob]
if missing:
raise Exception(
f'{so_path} looks like a CPU-only libdrmtap stub (missing '
f'{", ".join(m.decode() for m in missing)}): the EGL detile path the split capture '
'depends on is not in it, and DRM capture would silently fall back to PipeWire. '
'Install the EGL development packages and rebuild (Debian/Ubuntu: libegl-dev '
'libgles2-mesa-dev; Arch: mesa libglvnd).')
DRM_PACKAGE_NAME = 'rustdesk-unattended-wayland'
def stage_libdrmtap_into_deb(so_path):
# Put the built libdrmtap object plus its soname symlink into the staged deb. Only the soname
# symlink is needed: libdrmtap is resolved by ABSOLUTE path (/usr/lib/rustdesk/libdrmtap.so.0) at
# the in-process dlopen site (drmtap_dl.rs), so the deb does NOT drop /usr/lib/rustdesk into the
# system-wide /etc/ld.so.conf.d search path, which would let this private library shadow a system
# library for every binary on the host (Debian Policy 10.2 forbids that). No ld.so.conf.d drop-in
# and no ldconfig trigger are shipped, so the stock postinst is used unchanged.
so_basename = os.path.basename(so_path)
system2('mkdir -p tmpdeb/usr/lib/rustdesk')
system2(f'cp {so_path} tmpdeb/usr/lib/rustdesk/')
system2(f'ln -sf {so_basename} tmpdeb/usr/lib/rustdesk/libdrmtap.so.0')
def retarget_control_to_drm_variant():
# Rewrite the control file that generate_control_file just produced, instead of parameterizing that
# function: the stock packaging path stays exactly as upstream wrote it, and everything specific to
# this variant lives here. The variant installs the same files as the stock package, so it must
# conflict with and replace it: you install one or the other, never both. It also needs libdrmtap's
# own runtime deps, which the stock package has no reason to carry.
path = '../res/DEBIAN/control'
with open(path) as f:
lines = f.readlines()
out = []
for line in lines:
if line.startswith('Package: rustdesk'):
out.append(f'Package: {DRM_PACKAGE_NAME}\n')
out.append('Conflicts: rustdesk\nReplaces: rustdesk\nProvides: rustdesk\n')
elif line.startswith('Depends:'):
out.append(line.rstrip('\n') + ', libdrm2, libegl1, libgles2\n')
else:
out.append(line)
body = ''.join(out)
# Fail loudly rather than silently shipping a package that says `rustdesk`: a stock control file
# that stopped matching either anchor would otherwise produce a variant deb wearing the stock name.
if f'Package: {DRM_PACKAGE_NAME}\n' not in body or 'libegl1' not in body:
raise Exception(f'could not retarget {path} to the drm variant; upstream control layout changed')
with open(path, 'w') as f:
f.write(body)
def build_flutter_deb(version, features):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
system2('flutter build linux --release')
system2('mkdir -p tmpdeb/usr/bin/')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/etc/rustdesk/')
system2('mkdir -p tmpdeb/etc/pam.d/')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
system2('mkdir -p tmpdeb/usr/share/applications/')
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
f'cp -r {flutter_build_dir}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
'cp ../res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png')
system2(
'cp ../res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg')
system2(
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
'cp ../res/startwm.sh tmpdeb/etc/rustdesk/')
system2(
'cp ../res/xorg.conf tmpdeb/etc/rustdesk/')
system2(
'cp ../res/pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
# Bundle libdrmtap.so only when this build actually enabled the `drm` feature, so stock packages
# stay exactly what they were. The root service dlopens it in-process by absolute path.
# `features` is the comma-joined string, so split it: a bare substring test would also match any
# future feature merely containing "drm" (drm-lease, vaapi-drm) and rename the deb to the
# consent-bypass variant without --drm ever being passed.
ships_so = 'drm' in features.split(',')
if ships_so:
stage_libdrmtap_into_deb(build_libdrmtap_so())
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
if ships_so:
retarget_control_to_drm_variant()
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
system2('/bin/rm -rf ../res/DEBIAN/control')
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
if ships_so:
# Named apart from the stock package so installing the consent-free variant is a deliberate act.
os.rename('../rustdesk-%s.deb' % version, f'../{DRM_PACKAGE_NAME}-{version}.deb')
os.chdir("..")
def build_deb_from_folder(version, binary_folder, want_drm=False):
os.chdir('flutter')
system2('mkdir -p tmpdeb/usr/bin/')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
system2('mkdir -p tmpdeb/usr/share/applications/')
system2('mkdir -p tmpdeb/usr/share/polkit-1/actions')
system2('rm tmpdeb/usr/bin/rustdesk || true')
system2(
f'cp -r ../{binary_folder}/* tmpdeb/usr/share/rustdesk/')
system2(
'cp ../res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
'cp ../res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png')
system2(
'cp ../res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg')
system2(
'cp ../res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp ../res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
system2(
"echo \"#!/bin/sh\" >> tmpdeb/usr/share/rustdesk/files/polkit && chmod a+x tmpdeb/usr/share/rustdesk/files/polkit")
# A staged bundle (binary_folder) carries its own libdrmtap.so.0* for a --drm build, so we do
# not rebuild it here; the `cp -r` above placed it under usr/share/rustdesk/. Move it to the
# private lib dir, then finalize the deb the same way build_flutter_deb does.
bundled_glob = glob.glob('tmpdeb/usr/share/rustdesk/libdrmtap.so.0.*')
ships_so = any(os.path.isfile(p) and not os.path.islink(p) for p in bundled_glob)
# The variant must be decided by the EXPLICIT --drm request, not merely by what happens
# to be staged. Cross-check the two and fail loudly on a mismatch: a drm binary staged
# WITHOUT its libdrmtap.so.0.* would otherwise be silently shipped as the stock
# `rustdesk` package (no drm deps, a dlopen that can never resolve), and a
# bundle that DOES carry the .so would be shipped as the consent-bypass variant even
# when --drm was never asked for.
if want_drm and not ships_so:
raise Exception(
'--drm was requested but no real libdrmtap.so.0.* is staged under '
'usr/share/rustdesk/ in the bundle; refusing to package a drm binary as the '
'stock rustdesk package (it would ship without the capture library or its deps)')
if ships_so and not want_drm:
raise Exception(
'the staged bundle carries libdrmtap.so.0.* but --drm was not passed; refusing '
'to silently ship the consent-bypass unattended-wayland variant (pass --drm to '
'build it deliberately)')
if ships_so:
so = _single_real_so(bundled_glob, 'the staged --drm bundle')
stage_libdrmtap_into_deb(so)
system2(f'rm -f {so}')
system2('rm -f tmpdeb/usr/share/rustdesk/libdrmtap.so tmpdeb/usr/share/rustdesk/libdrmtap.so.0')
system2('mkdir -p tmpdeb/DEBIAN')
generate_control_file(version)
if ships_so:
retarget_control_to_drm_variant()
system2('cp -a ../res/DEBIAN/* tmpdeb/DEBIAN/')
md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb;')
system2('/bin/rm -rf tmpdeb/')
system2('/bin/rm -rf ../res/DEBIAN/control')
os.rename('rustdesk.deb', '../rustdesk-%s.deb' % version)
if ships_so:
os.rename('../rustdesk-%s.deb' % version, f'../{DRM_PACKAGE_NAME}-{version}.deb')
os.chdir("..")
def build_flutter_dmg(version, features):
if not skip_cargo:
# set minimum osx build target, now is 10.14, which is the same as the flutter xcode project
system2(
f'MACOSX_DEPLOYMENT_TARGET=10.14 cargo build --locked --features {features} --release')
# copy dylib
system2(
"cp target/release/liblibrustdesk.dylib target/release/librustdesk.dylib")
os.chdir('flutter')
# cargo builds a single-arch dylib for the host; restrict Xcode to the same arch
# so the universal-by-default ARCHS_STANDARD doesn't try to link a missing slice.
# FLUTTER_XCODE_* env vars are forwarded to xcodebuild as build settings.
mac_arch = 'arm64' if platform.machine().lower() in ('arm64', 'aarch64') else 'x86_64'
system2(
f'FLUTTER_XCODE_ARCHS={mac_arch} FLUTTER_XCODE_ONLY_ACTIVE_ARCH=YES flutter build macos --release')
system2('cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/')
'''
system2(
"create-dmg --volname \"RustDesk Installer\" --window-pos 200 120 --window-size 800 400 --icon-size 100 --app-drop-link 600 185 --icon RustDesk.app 200 190 --hide-extension RustDesk.app rustdesk.dmg ./build/macos/Build/Products/Release/RustDesk.app")
os.rename("rustdesk.dmg", f"../rustdesk-{version}.dmg")
'''
os.chdir("..")
def build_flutter_arch_manjaro(version, features):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
ffi_bindgen_function_refactor()
os.chdir('flutter')
system2('flutter build linux --release')
system2(f'strip {flutter_build_dir}/lib/librustdesk.so')
os.chdir('../res')
system2('HBB=`pwd`/.. FLUTTER=1 makepkg -f')
def build_flutter_windows(version, features, skip_portable_pack):
if not skip_cargo:
system2(f'cargo build --locked --features {features} --lib --release')
if not os.path.exists("target/release/librustdesk.dll"):
print("cargo build failed, please check rust source code.")
exit(-1)
os.chdir('flutter')
system2('flutter build windows --release')
os.chdir('..')
shutil.copy2('target/release/deps/dylib_virtual_display.dll',
flutter_build_dir_2)
if skip_portable_pack:
return
os.chdir('libs/portable')
system2('pip3 install -r requirements.txt')
system2(
f'python3 ./generate.py -f ../../{flutter_build_dir_2} -o . -e ../../{flutter_build_dir_2}/rustdesk.exe')
os.chdir('../..')
if os.path.exists('./rustdesk_portable.exe'):
os.replace('./target/release/rustdesk-portable-packer.exe',
'./rustdesk_portable.exe')
else:
os.rename('./target/release/rustdesk-portable-packer.exe',
'./rustdesk_portable.exe')
print(
f'output location: {os.path.abspath(os.curdir)}/rustdesk_portable.exe')
os.rename('./rustdesk_portable.exe', f'./rustdesk-{version}-install.exe')
print(
f'output location: {os.path.abspath(os.curdir)}/rustdesk-{version}-install.exe')
def main():
global skip_cargo
parser = make_parser()
args = parser.parse_args()
if os.path.exists(exe_path):
os.unlink(exe_path)
if os.path.isfile('/usr/bin/pacman'):
system2('git checkout src/ui/common.tis')
version = get_version()
features = ','.join(get_features(args))
flutter = args.flutter
if not flutter:
system2('python3 res/inline-sciter.py')
print(args.skip_cargo)
if args.skip_cargo:
skip_cargo = True
portable = args.portable
package = args.package
if package:
build_deb_from_folder(version, package, args.drm)
return
res_dir = 'resources'
external_resources(flutter, args, res_dir)
if windows:
# build virtual display dynamic library
os.chdir('libs/virtual_display/dylib')
system2('cargo build --locked --release')
os.chdir('../../..')
if flutter:
build_flutter_windows(version, features, args.skip_portable_pack)
return
system2('cargo build --locked --release --features ' + features)
# system2('upx.exe target/release/rustdesk.exe')
system2('mv target/release/rustdesk.exe target/release/RustDesk.exe')
pa = os.environ.get('P')
if pa:
# https://certera.com/kb/tutorial-guide-for-safenet-authentication-client-for-code-signing/
system2(
f'signtool sign /a /v /p {pa} /debug /f .\\cert.pfx /t http://timestamp.digicert.com '
'target\\release\\rustdesk.exe')
else:
print('Not signed')
os.makedirs(res_dir, exist_ok=True)
system2(
f'cp -rf target/release/RustDesk.exe {res_dir}')
os.chdir('libs/portable')
system2('pip3 install -r requirements.txt')
system2(
f'python3 ./generate.py -f ../../{res_dir} -o . -e ../../{res_dir}/rustdesk-{version}-win7-install.exe')
system2(f'mv ../../{res_dir}/rustdesk-{version}-win7-install.exe ../..')
elif os.path.isfile('/usr/bin/pacman'):
# pacman -S -needed base-devel
system2("sed -i 's/pkgver=.*/pkgver=%s/g' res/PKGBUILD" % version)
if flutter:
build_flutter_arch_manjaro(version, features)
else:
system2('cargo build --locked --release --features ' + features)
system2('git checkout src/ui/common.tis')
system2('strip target/release/rustdesk')
system2('ln -s res/pacman_install && ln -s res/PKGBUILD')
system2('HBB=`pwd` makepkg -f')
system2('mv rustdesk-%s-0-x86_64.pkg.tar.zst rustdesk-%s-manjaro-arch.pkg.tar.zst' % (
version, version))
# pacman -U ./rustdesk.pkg.tar.zst
elif os.path.isfile('/usr/bin/yum'):
system2('cargo build --locked --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm.spec" % version)
system2('HBB=`pwd` rpmbuild -ba res/rpm.spec')
system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-fedora28-centos8.rpm' % (
version, version))
# yum localinstall rustdesk.rpm
elif os.path.isfile('/usr/bin/zypper'):
system2('cargo build --locked --release --features ' + features)
system2('strip target/release/rustdesk')
system2(
"sed -i 's/Version: .*/Version: %s/g' res/rpm-suse.spec" % version)
system2('HBB=`pwd` rpmbuild -ba res/rpm-suse.spec')
system2(
'mv $HOME/rpmbuild/RPMS/x86_64/rustdesk-%s-0.x86_64.rpm ./rustdesk-%s-suse.rpm' % (
version, version))
# yum localinstall rustdesk.rpm
else:
if flutter:
if osx:
build_flutter_dmg(version, features)
pass
else:
# system2(
# 'mv target/release/bundle/deb/rustdesk*.deb ./flutter/rustdesk.deb')
build_flutter_deb(version, features)
else:
system2('cargo --locked bundle --release --features ' + features)
if osx:
system2(
'strip target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk')
system2(
'cp libsciter.dylib target/release/bundle/osx/RustDesk.app/Contents/MacOS/')
# https://github.com/sindresorhus/create-dmg
system2('/bin/rm -rf *.dmg')
pa = os.environ.get('P')
if pa:
system2('''
# buggy: rcodesign sign ... path/*, have to sign one by one
# install rcodesign via cargo install apple-codesign
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/rustdesk
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/libsciter.dylib
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./target/release/bundle/osx/RustDesk.app
# goto "Keychain Access" -> "My Certificates" for below id which starts with "Developer ID Application:"
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app/Contents/MacOS/*
codesign -s "Developer ID Application: {0}" --force --options runtime ./target/release/bundle/osx/RustDesk.app
'''.format(pa))
system2(
'create-dmg "RustDesk %s.dmg" "target/release/bundle/osx/RustDesk.app"' % version)
os.rename('RustDesk %s.dmg' %
version, 'rustdesk-%s.dmg' % version)
if pa:
system2('''
# https://pyoxidizer.readthedocs.io/en/apple-codesign-0.14.0/apple_codesign.html
# https://pyoxidizer.readthedocs.io/en/stable/tugger_code_signing.html
# https://developer.apple.com/developer-id/
# goto xcode and login with apple id, manager certificates (Developer ID Application and/or Developer ID Installer) online there (only download and double click (install) cer file can not export p12 because no private key)
#rcodesign sign --p12-file ~/.p12/rustdesk-developer-id.p12 --p12-password-file ~/.p12/.cert-pass --code-signature-flags runtime ./rustdesk-{1}.dmg
codesign -s "Developer ID Application: {0}" --force --options runtime ./rustdesk-{1}.dmg
# https://appstoreconnect.apple.com/access/api
# https://gregoryszorc.com/docs/apple-codesign/stable/apple_codesign_getting_started.html#apple-codesign-app-store-connect-api-key
# p8 file is generated when you generate api key (can download only once)
rcodesign notary-submit --api-key-path ../.p12/api-key.json --staple rustdesk-{1}.dmg
# verify: spctl -a -t exec -v /Applications/RustDesk.app
'''.format(pa, version))
else:
print('Not signed')
else:
# build deb package
system2(
'mv target/release/bundle/deb/rustdesk*.deb ./rustdesk.deb')
system2('dpkg-deb -R rustdesk.deb tmpdeb')
system2('mkdir -p tmpdeb/usr/share/rustdesk/files/systemd/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/256x256/apps/')
system2('mkdir -p tmpdeb/usr/share/icons/hicolor/scalable/apps/')
system2(
'cp res/rustdesk.service tmpdeb/usr/share/rustdesk/files/systemd/')
system2(
'cp res/128x128@2x.png tmpdeb/usr/share/icons/hicolor/256x256/apps/rustdesk.png')
system2(
'cp res/scalable.svg tmpdeb/usr/share/icons/hicolor/scalable/apps/rustdesk.svg')
system2(
'cp res/rustdesk.desktop tmpdeb/usr/share/applications/rustdesk.desktop')
system2(
'cp res/rustdesk-link.desktop tmpdeb/usr/share/applications/rustdesk-link.desktop')
os.system('mkdir -p tmpdeb/etc/rustdesk/')
os.system('cp -a res/startwm.sh tmpdeb/etc/rustdesk/')
os.system('mkdir -p tmpdeb/etc/X11/rustdesk/')
os.system('cp res/xorg.conf tmpdeb/etc/X11/rustdesk/')
os.system('cp -a DEBIAN/* tmpdeb/DEBIAN/')
os.system('mkdir -p tmpdeb/etc/pam.d/')
os.system('cp pam.d/rustdesk.debian tmpdeb/etc/pam.d/rustdesk')
system2('strip tmpdeb/usr/bin/rustdesk')
system2('mkdir -p tmpdeb/usr/share/rustdesk')
system2('mv tmpdeb/usr/bin/rustdesk tmpdeb/usr/share/rustdesk/')
system2('cp libsciter-gtk.so tmpdeb/usr/share/rustdesk/')
md5_file_folder("tmpdeb/")
system2('dpkg-deb -b tmpdeb rustdesk.deb; /bin/rm -rf tmpdeb/')
os.rename('rustdesk.deb', 'rustdesk-%s.deb' % version)
def md5_file(fn):
md5 = hashlib.md5(open('tmpdeb/' + fn, 'rb').read()).hexdigest()
system2('echo "%s /%s" >> tmpdeb/DEBIAN/md5sums' % (md5, fn))
def md5_file_folder(base_dir):
base_path = Path(base_dir)
for file in base_path.rglob('*'):
if file.is_file() and 'DEBIAN' not in file.parts:
relative_path = file.relative_to(base_path)
md5_file(str(relative_path))
if __name__ == "__main__":
main()