#!/usr/bin/env python3 import os import glob import contextlib 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 # Captured at import, while cwd is still the repo root: before Python 3.9 the main script's __file__ # stays relative (bpo-20443), so abspath() re-resolves it against the cwd -- and the ubuntu18.04 # packaging container runs 3.6 and chdir's into flutter/ before it reaches the libdrmtap code. REPO_ROOT = os.path.dirname(os.path.abspath(__file__)) 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( '--print-features', action='store_true', help='Print the cargo feature list these flags select, and exit without building. For a ' 'caller that runs its own cargo line and then packages with --skip-cargo: it can ask ' 'for the list rather than repeat it, so the two cannot drift.' ) 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 linux_packaging_branch(): """Which packaging path `main()` will take on THIS host. MUST mirror the elif chain in main() (pacman / yum / zypper / else), and exists so `--drm` can refuse a branch that is not drm-aware instead of silently producing a stock-named package with the capture backend compiled in. Only the final `deb` branch reaches `build_flutter_deb`, which is what bundles libdrmtap, renames the package, adds Conflicts/Provides and asserts the staged binary really is a drm build. """ if os.path.isfile('/usr/bin/pacman'): return 'pacman' if os.path.isfile('/usr/bin/yum'): return 'yum' if os.path.isfile('/usr/bin/zypper'): return 'zypper' return 'deb' 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') # And only on the deb branch. The other three Linux paths (pacman/yum/zypper) package # straight from `target/release` without bundling libdrmtap, without the rename, without # Conflicts/Provides and without assert_staged_binary_is_drm() -- so they would emit a # package NAMED `rustdesk` carrying the consent-bypass backend and the root-side uinput # injection. The separate package name is the informed consent this feature rests on, so # refuse rather than ship a stock-named build of it. branch = linux_packaging_branch() if branch != 'deb': raise Exception( f'--drm is only supported on the deb packaging path; this host would package via ' f'{branch}, which cannot bundle libdrmtap or name the package distinctly') features.append('drm') # The display wake is its own compile gate on top of `drm`, and the unattended package is # exactly where it belongs: that variant exists to reach a machine nobody is sitting at, # and a machine whose screen went dark is the case it is for. Dropping `drm-wake` from # this line builds the same capture backend with no wake code in the binary at all. # It is ALSO switchable at runtime; see OPTION_ENABLE_DRM_DISPLAY_WAKE. features.append('drm-wake') 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 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, 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= floor if not accepted: raise Exception( f'the libdrmtap being packaged is {so_ver[0]}.{so_ver[1]}.{so_ver[2]}, which the ' f'runtime loader would REFUSE: drmtap_dl.rs accepts exactly major {major}, minor ' f'{minor}, patch >= {floor[1]}. Shipping it produces a deb whose DRM capture can never ' 'start. Move the build pin and the gate together, or fix whichever one is wrong.') print(f'[drm] libdrmtap {so_ver[0]}.{so_ver[1]}.{so_ver[2]} satisfies the runtime ABI gate ' f'(major {major}, minor {minor}, patch >= {floor[1]})') 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. assert_so_satisfies_the_runtime_abi_gate(so_path) so_basename = os.path.basename(so_path) system2('mkdir -p tmpdeb/usr/lib/rustdesk') # Quoted: so_path comes from the repo root or from DRMTAP_PREBUILT_DIR, either of which can # contain a space, and an unquoted interpolation would split the argument and fail obscurely. system2(f'cp "{so_path}" tmpdeb/usr/lib/rustdesk/') system2(f'ln -sf "{so_basename}" tmpdeb/usr/lib/rustdesk/libdrmtap.so.0') def _max_glibc_minor(path): # Read from .dynstr rather than via objdump so packaging needs no binutils; chunked because # librustdesk.so is ~45 MB. best = 0 with open(path, 'rb') as f: tail = b'' while True: chunk = f.read(1 << 20) if not chunk: return best for m in re.finditer(rb'GLIBC_2\.(\d+)', tail + chunk): best = max(best, int(m.group(1))) tail = chunk[-16:] def measured_glibc_floor(): # libdrmtap is built on a newer base than the rest of the deb, so the floor is whichever staged # object is higher -- and it moves whenever either base does. paths = [p for p in glob.glob('tmpdeb/usr/lib/rustdesk/libdrmtap.so.0.*') + glob.glob('tmpdeb/usr/share/rustdesk/lib/librustdesk.so') + glob.glob('tmpdeb/usr/share/rustdesk/rustdesk') if os.path.isfile(p) and not os.path.islink(p)] minor = max((_max_glibc_minor(p) for p in paths), default=0) if not minor: raise Exception( f'could not measure a GLIBC_2.x floor from any staged object ({paths or "none found"}); ' 'refusing to ship the unattended-wayland variant with an undeclared libc6 floor, which ' 'is what lets it install on a host where libdrmtap can never load') return f'2.{minor}' 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' floor = measured_glibc_floor() print(f'[drm] {DRM_PACKAGE_NAME} libc6 floor measured at {floor}') 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:'): # 2.4.101 is where drmModeGetFB2 landed; below it libdrmtap loads and can never capture. out.append(line.rstrip('\n') + ', libdrm2 (>= 2.4.101), libegl1, libgles2, ' f'libc6 (>= {floor})\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/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( "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: # Same artifact assertion as the --package path. Under --skip-cargo nothing here rebuilt the # binary, so `features` says what was ASKED for while the staged bundle can be anything. assert_staged_binary_is_drm() 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("..") DRMTAP_DLOPEN_MARKER = b'/usr/lib/rustdesk/libdrmtap.so.0' # Present only when `drm-wake` is compiled in: the runtime option constant is itself # #[cfg(feature = "drm-wake")] (src/ipc/drm.rs). The dlopen marker above cannot stand in for it - # `--features drm` alone produces a binary that carries the dlopen path and NO wake code, and that # is exactly the deb this assertion is here to refuse. DRMTAP_WAKE_MARKER = b'enable-drm-display-wake' def _carries_drmtap_marker(path, marker=DRMTAP_DLOPEN_MARKER): # Chunked, with an overlap of len(marker)-1 so the marker cannot be missed at a chunk boundary: # librustdesk.so is ~45 MB and there is no reason to hold it all in memory, and the `with` # closes deterministically instead of relying on refcounting. with open(path, 'rb') as f: tail = b'' while True: chunk = f.read(1 << 20) if not chunk: return False if marker in tail + chunk: return True tail = chunk[-(len(marker) - 1):] def assert_staged_binary_is_drm(): """The staged BINARY must really be a drm build before it is named the unattended-wayland variant. That package conflicts with and replaces the stock one, so shipping a stock binary under that name produces something that can never capture and cannot be installed alongside what it replaced. The marker is the absolute dlopen path from drmtap_dl.rs, present only when the feature is compiled in -- assert what was produced, not what was asked for. Called from BOTH packaging paths. It used to guard only one of them, and `--skip-cargo` (which is how CI packages) reaches the other, where nothing had rebuilt the binary at all. """ binaries = [p for p in glob.glob('tmpdeb/usr/share/rustdesk/lib/librustdesk.so') + glob.glob('tmpdeb/usr/share/rustdesk/rustdesk') if os.path.isfile(p)] if not any(_carries_drmtap_marker(p) for p in binaries): raise Exception( f'--drm was requested but the staged bundle does not look like a drm build (no ' f'{DRMTAP_DLOPEN_MARKER.decode()} dlopen path in {binaries or "any staged binary"}); ' 'refusing to package it as the unattended-wayland variant, which conflicts with and ' 'replaces the stock package but could never capture') # And the WAKE half. `--drm` enables `drm-wake` too (see get_features), and the deb is named and # documented as the variant that can reach a machine whose screen has gone dark. The dlopen # marker above does not distinguish them: `--features drm` alone carries it and has no wake code # at all. Asserting only the first half is how a deb can be named for a feature it does not have. if not any(_carries_drmtap_marker(p, DRMTAP_WAKE_MARKER) for p in binaries): raise Exception( f'--drm was requested but the staged binary has no {DRMTAP_WAKE_MARKER.decode()} ' f'marker in {binaries or "any staged binary"}, so it was built without `drm-wake`; ' 'refusing to package it as the unattended-wayland variant, which is named and ' 'documented as the build that can wake an idle-disabled display. If this fired under ' '--skip-cargo, the cargo line that produced the bundle is missing the feature: ' '--features ...,drm,drm-wake') 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") # Where the capture library comes from for a `--package --drm` build. Two shapes are # supported, because two exist in practice: a bundle that already carries libdrmtap.so.0.* # (someone staged it, e.g. a CI artifact), and a plain bundle, which is what every build path # here actually produces -- the flutter deb builds the library straight into the staged deb, so # nothing ever puts it inside the bundle folder. Demanding it in the bundle made this flag # combination impossible to satisfy. bundled_glob = glob.glob('tmpdeb/usr/share/rustdesk/libdrmtap.so.0.*') bundle_carries_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: a bundle that carries the .so must NOT be shipped as the consent-bypass variant when # --drm was never passed. if bundle_carries_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 want_drm: # Whichever shape we are in, the staged BINARY must really be a drm build. This is the # property the old presence-of-the-.so test stood in for, badly: a stock binary packaged as # the unattended-wayland variant would carry the consent-bypass name, conflict with and # replace the stock package, and never be able to capture. The marker is the absolute # dlopen path from drmtap_dl.rs, present only when the feature is compiled in -- the same # kind of artifact assertion as _assert_so_has_egl, and for the same reason: assert what # was produced, not what was asked for. assert_staged_binary_is_drm() if bundle_carries_so: so = _single_real_so(bundled_glob, 'the staged --drm bundle') # The THIRD artifact source, and the last one that was missing the check: --package # takes the .so straight out of a bundle somebody else produced, so it has the same # exposure as DRMTAP_PREBUILT_DIR (see the comment on that branch). A CPU-only stub # would ship, the loader would accept it, and capture would degrade to PipeWire # without a word. _assert_so_has_egl(so) 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') else: # Build it here, exactly as the flutter deb path does (build_libdrmtap_so asserts the # EGL backend itself). The library is independent of the staged binary. stage_libdrmtap_into_deb(build_libdrmtap_so()) system2('mkdir -p tmpdeb/DEBIAN') generate_control_file(version) # Keyed on the EXPLICIT request, not on what happened to be staged: by here a --drm build has # its library in tmpdeb whichever of the two shapes it came from. if want_drm: 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 want_drm: 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() # Before anything with a side effect: this is a query, and a caller uses it to build the very # binary it will then package. `get_features` stays the single definition of what a flag # combination means; a caller that hardcodes the list instead is one edit away from compiling # something other than what it ships. if args.print_features: # stdout carries the list and nothing else, so a caller can use it directly in a command # substitution. `get_features` prints a human-readable line of its own; send that to stderr # for this call rather than silencing it, which would change what every other path prints. with contextlib.redirect_stdout(sys.stderr): feats = ','.join(get_features(args)) print(feats) return 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('cp -a DEBIAN/* tmpdeb/DEBIAN/') 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()