commit a3686a49d4e4b872dc681654f5001a6f5f45f8e9 Author: naeeo Date: Tue Sep 29 19:56:02 2026 +0800 Initial commit: rdgen with GitHub/Gitea dual build backend diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..4d52cec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,27 @@ +docker-compose.yml + +# Docs +README.md +setup.md +LICENSE + +# Git stuff +.git +.github +.gitignore + +# Python caches / venv +*.pyc +__pycache__/ +.venv/ + +# Runtime data (容器内通过卷挂载) +db.sqlite3 +data/ +exe/ +png/ +temp_zips/ +static/ + +# 与 Web 服务无关的内容 +printer-adapter/ diff --git a/.gitea/actions/decrypt-secrets/action.yml b/.gitea/actions/decrypt-secrets/action.yml new file mode 100644 index 0000000..330f0bb --- /dev/null +++ b/.gitea/actions/decrypt-secrets/action.yml @@ -0,0 +1,48 @@ +name: 'Decrypt and Mask Secrets' +description: 'Decrypts a zip and masks the JSON contents as env vars' +inputs: + zip_password: + description: 'Password for the Zip' + required: true + zip_path: + description: 'Path to the encrypted zip' + required: false + default: 'secrets.zip' + +runs: + using: "composite" + steps: + - name: install python deps + shell: bash + run: | + pip install pyzipper + - name: Decrypt and Mask + shell: python + env: + PYTHONUTF8: "1" + PYTHONIOENCODING: "utf-8" + run: | + import sys + import io + import pyzipper + import json + import os + + # Force UTF-8 stdout/stderr so secrets with characters outside + # the runner codepage (CJK, emoji, etc.) do not crash this step + # on Windows runners (default cp1251) with UnicodeEncodeError + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + + with pyzipper.AESZipFile('${{ inputs.zip_path }}') as zf: + zf.setpassword('${{ inputs.zip_password }}'.encode()) + with zf.open('secrets.json') as f: + secrets = json.load(f) + + with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env_file: + for key, value in secrets.items(): + if value: + print(f"::add-mask::{value}") + env_file.write(f"{key}={value}\n") + + print(f"Successfully masked {len(secrets)} secrets.") \ No newline at end of file diff --git a/.gitea/workflows/bridge.yml b/.gitea/workflows/bridge.yml new file mode 100644 index 0000000..b0d8f86 --- /dev/null +++ b/.gitea/workflows/bridge.yml @@ -0,0 +1,134 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +# This yaml shares the build bridge steps with ci and nightly. +name: Build flutter-rust-bridge +# 2023-11-23 18:00:00+00:00 + +on: + workflow_call: + inputs: + version: + description: 'Rustdesk Version' + required: true + default: '1.3.1' + type: string + +env: + CARGO_EXPAND_VERSION: "1.0.95" + FLUTTER_VERSION: "3.22.3" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 + +jobs: + generate_bridge: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-24.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + if: ${{ inputs.version != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ inputs.version }} + submodules: recursive + + - name: Checkout source code + if: ${{ inputs.version == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install prerequisites + run: | + sudo apt-get install ca-certificates -y + sudo apt-get update -y + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libclang-dev \ + libgtk-3-dev \ + llvm-dev \ + nasm \ + ninja-build \ + pkg-config \ + wget + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + + - name: Cache cargo git + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: bridge-cargo-git-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + bridge-cargo-git- + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v4 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install flutter rust bridge deps + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked + pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd + + - name: Run flutter rust bridge + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h + cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + ./flutter/macos/Runner/bridge_generated.h + ./flutter/ios/Runner/bridge_generated.h \ No newline at end of file diff --git a/.gitea/workflows/fetch-encrypted-secrets.yml b/.gitea/workflows/fetch-encrypted-secrets.yml new file mode 100644 index 0000000..e097d83 --- /dev/null +++ b/.gitea/workflows/fetch-encrypted-secrets.yml @@ -0,0 +1,48 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Fetch Encrypted Secrets + +on: + workflow_call: + inputs: + zip_url_json: + required: true + type: string + +jobs: + download-zip: + runs-on: ubuntu-latest + steps: + - name: Download with Retry + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: python + command: | + import requests + import json + import time + + input_data = json.loads('${{ inputs.zip_url_json }}') + url = f"{input_data['url']}/get_zip?filename={input_data['file']}" + + for attempt in range(5): + try: + print(f"Downloading (Attempt {attempt + 1})...") + r = requests.get(url, timeout=20) + r.raise_for_status() + with open('secrets.zip', 'wb') as f: + f.write(r.content) + break + except Exception as e: + if attempt < 4: + time.sleep(5 * (2 ** attempt)) + else: raise e + + - name: Upload Encrypted Artifact + uses: actions/upload-artifact@v4 + with: + name: encrypted-secrets-zip + path: secrets.zip + retention-days: 1 \ No newline at end of file diff --git a/.gitea/workflows/generator-android.yml b/.gitea/workflows/generator-android.yml new file mode 100644 index 0000000..60c4cf4 --- /dev/null +++ b/.gitea/workflows/generator-android.yml @@ -0,0 +1,707 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Custom Android Client Generator +run-name: Custom Android Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;files,/opt/vcpkg-cache,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + +jobs: + setup: + uses: ./.gitea/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge-linux: + uses: ./.gitea/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-rustdesk-android: + needs: [generate-bridge-linux, setup] + name: build rustdesk android apk ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-linux-android, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + - { + arch: armv7, + target: armv7-linux-androideabi, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + - { + arch: x86_64, + target: x86_64-linux-android, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + imagemagick \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libc6-dev \ + libclang-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libvdpau-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-dev \ + nasm \ + ninja-build \ + openjdk-17-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Install dependencies + continue-on-error: true + run: | + sudo apt-get install -y potrace + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + continue-on-error: true + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: ${{ env.NDK_VERSION }} + add-to-path: true + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + case ${{ matrix.job.target }} in + aarch64-linux-android) + ANDROID_TARGET=arm64-v8a + ;; + armv7-linux-androideabi) + ANDROID_TARGET=armeabi-v7a + ;; + x86_64-linux-android) + ANDROID_TARGET=x86_64 + ;; + i686-linux-android) + ANDROID_TARGET=x86 + ;; + esac + if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + shell: bash + + - name: Restore bridge files + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated + key: ${{ matrix.job.target }} + + ###########################################################echo "${{ env.iconbase64 }}" | base64 -d > ./res/icon.png + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -T 30 -t 2 -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAEiuAABIrgHwmhA7AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAEx9JREFUeJztnXmYHMV5h9+vZnZ0rHYRum8J4/AErQlgAQbMsRIWBEFCjK2AgwTisGILMBFCIMug1QLiPgIYE/QY2QQwiMVYjoSlODxEAgLEHMY8YuUEbEsOp3Z1X7vanf7yR8/MztEz0zPTPTO7M78/tnurvqn6uuqdr6q7a7pFVelrkpaPhhAMTEaYjJHDUWsEARkODANGAfWgINEPxLb7QNtBPkdoR7Ud0T8iphUTbtXp4z8pyQH5KOntAEhL2yCCnALW6aAnIDQAI+3MqFHkGJM73BkCO93JXnQnsAl4C8MGuoIv69mj2rw9ouKq1wEgzRiO2noSlp6DoRHleISgnQkJnRpLw0sI4v9X4H2E9Yj172zf+2udOflgYUdYXPUaAOTpzxoImJkIsxG+YCfG+Z7cecWDIN5+J8hqjNXCIW3rdMqULvdHWBqVNQDS8tlwNPCPKJcjOslOjGZGt2UHQTStHZGnMPxQG8d9mOk4S6myBEBWbj0aZR7ILISBPRlZOiMlr+QQgGAhvITqg0ybsEZjhZWHygoA+VnbaSBLEaY6dgb0Vgii+h2GO2gcv7JcQCgLAOSp7ZNBlyI6sycR+igEILoRdJFOnfgCJVZJAZCf7pxETfhmlIsQjHNH9VkIAF0H1iKdetjvKJFKAoC0EODA9msQvQUYmL2j8uwMJ/uygwAL0dvZMHGJNmFRZBUdAHlix5dQfQw4IbeO6tMQgOgybZx4I0VW0QCQ5dQQ2v4DhO8Dofw6qk9DEIZwg0497H8ookwxKpEV7WOo2fES0IQSAnrmwBrXEhq/lcR5cnJasm1KWq5lx9knl5NvvW7877EPIMFZFFm+AyA/2Xk6EngbOCVtA1chsO1V/4oiyzcABERW7FiI6osoo2IZVQicy7HtwxRZQT8KlWaCjNm5AiOzY+Oe0jPuqdjjXjQttpWe8TMhT0Djxs/ktGRbCi07g4/kWW/C8afxX/htAc2elzyPAPIQ/Ri7cyXCbBfjXjUS9Nh2IeEnKLI8BUB+1DaI/jvXoJwfS6xC4FxOcr2i12vjpM0UWZ6dBsry/aOh61fAMfmfCyfllfoU0Y2P+dab6P/d+rVx11MCeQKALN8zDA1vAJlc+AWRpLw+D4Hcp9PHLqBEKngIkBXtdVjWWlQmA4XMgBPTymU4cONj3vXKvaXsfCgQAGkhRGfoOZDjgHwnP3F5FQXBvTp97HWUWHkDIM0Y2nY/C5zpwQw4Lq8SINC79azSdz4UEgGG7l4CnOfJDDglr09DcK/+dWkmfE7KaxIoD++aDmYtaMCDGbBtXxETQ7lXzx5dFt/8qHIGQB7eORENvI0w1E4pZAacZN+XIUDu1XPKq/MhRwDkp/Rn7+7XQY6xE6I5ZQ/BbrB+j8gWkC2g7cBeAtJFdA2GyqGIDkUYA0xAtAEYkrFstxAY7tIZY26gDJXbvYDd+5qRuM7XyBbBt+vjONgnl0NKvZtRXYewAfRtvjX8Q00cwV1JWraNRbqPRbURkTOAoxGRnHzE3KUzRpVl50MOEUAe2H88Yr0GBEu/esapHPkjWE+CPKOzh25ydVA5Sp5vHw3hbwIXInoSEvEgnY/C7Xru6MV++AIgL245FmMuQmhArQ7EvInK4zpt3Meuy3ADgDQT4tC9b6EclbbzSgOBgq5B9T7mDNuQz7c8X8kv2o9Auq8C5gB1ST5uQ/VKPW/MSl/qbmkNMbTun1G+69A2BxDma+OER12V5QqA+/c2Y1jSk5BQYSkgUGAlAb3Zr2+7W8na7fV0dH0To18G3YOwkfrOn2vjpA5f6mtpDTGk7jmUv8n4BYFLdOqEf81aXjYA5L49R2DMRtCa1A6iFBC8glgLdM7QNzM63gclaz/sR03/51DOdREld9PV9Rd65uFbM5WZ/UKQBG5DqbEnenHp6S7yuL8gkrmceHs7bT8Wi/jzoY0V2fktrSHMgGdRzgXcXKSqpya0hCzKGAHkngNfwVivJ052nM6z8TsSvALM1ssHb8l2QH1Rsn5zfzprnkf0bDshPhMyRIIuAqZBTxv3QbqyM0eAgHUbINkvu+JjJNDlhAefUbGd39Ia4kBNC3B2HpfUa+i2bstYfroIIPftn4HyQgnX1nchXKFXDM46kemrkvWb+9MRWgV6lp0Qzchp0qyY8MnaOOkNpzrSRwAL+1cqpVlC1YnFhRXd+Ws/7Mf+fs+hkc6HXOZL8XmCFfxB2nqcIoDcc+AroG9EPh61jDOI33oeCQ6gOkO/M3h9Oqf7uqTlowHUml8C03Nq49h+ShtbqDlSzxj7v8l1OUcAteanHZsT0iI1eBcJurBkZkV3/ppPBzLQ/BvKdCC3Nnayt7cGY33Psb7kCCD3HRhPN39AtIZIWYlb3yKBAhfrd+ufdHK0EiRrPh0IuhqYljZK5h8J9hHS8XrKhB3xdaZGgG6uBGq8WZRBLpHg/oru/OXUoKwCmZYxSuYfCWrpNN9OrjcBAGnGoPT8QLFoEOgGttaX7R2zomjUpw8C010NlflCIFyaXG1iBAh1nAqMdbiq5CcEuyA8W5voTnauUiS/+PgIYG5O86V8IFD9S/mPj4+Jrzt5CLggzQUFByfwBgJlgc4b8n9UsgKBuajYfeE3BAG9IL7qGADSTBD4RoarSg5OUCgEL3FV3QoqXSpHRbaR/0ncegmBpRdI3HSxJwLUdE4FRqQ5jXAuuDAILLrNAk20qEypdvbs+w7BYfz6oxOiSSYu88wkQ58h4An9p9p3qQqEl121sVcQBJgR/bcHAGFaltOI7A66hyBMWG+lKlsHeRyho2gQWDRGdw2ANDMY5egUQ/8geF7n15ft83OLLZ05qo0wz9j/xGf4BsGJ9kWnaAQIHjwdCBTtFzzGuo+qkqQP5dTGhUEQop91EkQBsLTR9WmEWwfTQaDSqlfXO96arGTp+aPfAXm/aBCIPQxE5wDHpjVMKMQTCCr2cm9WKc/k3Mb5QmDpCdADQEPazvMaAhN4mqqcFQ635NXG+UHQYFss2zuScM1nsdyUu1BJ6bF9dbjD52CfWM4mvbZ2MlWllTz/+WZgYl5t7GSfXE58XqBzsKEr0BCjJWKbuPUwEgjrqCqzVP7T3oLvkaCr35EG4h/t4jMEYdlAVZkl1oa0nec1BCINBmRiiqFTwV5AYOQdqsqscMC+OloMCNDDDcoIR0OngguDYKteO6Cy7/q5UlsrYL9tzHcIdIQhdgPIwdCp4HwhsPT3VJVVOnPyQZQ/9CTEb72GQIYbkBEZDZ0KzgcCkc0pR1tVGsnHRXlmkTLcoDIiq6FTwTlDwBaqcifFfkex/xAMN6B1rmhxKjgnCGQ7VblVW0obgx8QDDEoxoUhBUMgupeq3EnFfraA/xCY3NehOdm7gSAs+6jKpbQjbRsnpEGhEBhUxI1hQoVO9tkgMFKU9xP1DUWaqggQGGwIshoWDEGY/lTlTsqgrG2ckpcfBAaNrMf3GwKRAVTlUjrIVRun5OUMgRqQbWk7z0sILB1BVe6UcHXWVwh2GFTbHQv2GgLDWKpyKZ2QUxun5LmGoN0A7amF+ACBMp6q3Ellgr2N/g8+QdBuEGlPnbSlGHoBQQNVZZU8/ekwkFF5tbGTfSYILN1qCOvWrOvHvIFgjDTvGUZVmaWBKWk7z3sI2g1iPkgxdCrYCwhqQsdSVRbJ8UD6zvMSAsyfDJa1ydEwXp5BoI0OpVcVL5VpPfvgKwQW7xtM8H1XtHgDwdeoKq3kic9rUU5OjcQ+QdBNq9Hb2AZsLQ4EMkVu3zucqpwlwekg/QCH4dhzCNp05qi26PX51gyGXkIQoLvmG1SVThcBqW0c2/cUglaI3nVQeSODoYMzBUAgXEhVKZKWHYegnJN28h3b9woC3oTYbSdrfVGWINn7p8qtnYdTVaIOWBcD9v2SYkCAvUTfBmBA8L+AriJBYFCuoqqYpIUAcE1qR+MXBGGk36sQAUCb2Av6joNh5gqdHHQHwWVyF3VUZWvf9vNROdz1tZjYfp4QiLyrfzd4J8Q/IcSSDWloyVyhk4PZIains6M6GYTow7mWAqltHEvDWwgsa320iB4AjFntWKFTwV5AoIHjqArG77gCmJy2jWNpeAcBsja61wPAAF5D+cixQqeCC4cg/pMVKfnZrkMRWercbr5B8Dk6cn30ozEAtAkLaHF/GlEgBEL1d4Kd4ftBRwJp2s0HCJSf60zC0Y8lLtRUszL1w/gAgbZRV/MMFSz58Y4ZqFySvd08hgBJeJdhIgD38BuI/ITLLwhEFORanc8BKlTy4+3jMPIT9+3mGQSfsGn4q/G+JACgimLJY/6uQ5Ol2hSq2OcESQshCLRg4fybTPAPAovHI0N9TKlr9UM8itLhCwSit2pT8OaUOitEAsKOnf8CeiKQz5enEAi6CQd+lOxTCgB6G22gT2U8jcgHAtE7dWnopuT6KkrLd92JcKmrbyt4C4HynF405KNkl9L8Wsc8mFBAihPkCkGzNocWOddVGZLluxYDCz150ko+EIg+5OSXIwB6N++hvJRQQIoTuIWgSW8JLnWqpxIkIPLIrrtRluU1bjvZ5w7BW3rhiNec/AtmcL0ZVfvlRQpIZEftunu2QuyxZQl5ApbepLcFK/ah0PIQ/ajZ/SjCJWnbLfo/9LSbaqItDvbJtmQoW0g778r87uDrdDVE31QddUbj9uO3ceXYTizR280taQvv45KHto8jGGwBTnTVbhL/4Yh9sq2TfbJtctnKqzpr2Knp/Mz8i11LFgHhlNAT2yc19Nj7iyu68x/ecx6B4DsoibP92D6p7ebbcGBlfBlXxggAIAusxxC5jLhjyEw0N+rtZlnGQvuo5JFdh2KZO4C5jt/g4keCVTpr6Ncz+Zz9N/tB04RiP9whWyQQrq/EzpdmQvLD3dcQNh+gzI2kOnzbI+kpafgRCboQSfvO4Jjv2SIAgCxgDugKJOK9E9GGhXqHuSdrYXlKbjnYgCWXYfQIIIRar6Os0Kb+f/arzqw+NRNi8L4LMXoT6BftxGhm1KpEkcDoLTpr2JKsx+AGAABZwCzQBxCGJFW4Hax5eldgZfpP5y9pJoR2PoDId5LqBTQMrAJ9iJv6v6yJ3xHfJA/sG4lYl6DyPWBs2s4rFQTQyu7tX9arv9hJFrkGAEAWcQjd/C1qNSAEEfMu+1mlD+PLA6BkIbXUdq0BGjM2ov3/FuBZxDxLd807yde8C/bl3j3DCJizUP4B4UzQYNqZd4qPCX76DYGFcIpePOR1V8eVCwDFlCykloFdLwCnu2rEhMaQbaDrgZdB36W74z1tstfAua7/no7DEJ0CHI9YU4EpgHF9+pXiYxb/nezzgUB5UC8dco2bY7Q/UoYARDr/Vyin5dSImTvjE+Aj0M8w8jkW3QR0N4ogMhi0FiPDUGsCMAmJLNFOd53Dfb3u/XeyzwUC5T26O07SuaP341JlB4A0M5Cu7jUIUz17MUIujeimM/Kt118I9iDWCTpnaE7PZC6rR7cldD6kOdUBcDg1ynpBBIe8DOU41evm3ke8ivH0NY38F5Y5uXY+lBEA0sxADnavAaZmP9+FsoagUP8z1evs/x16xeDnyUNlAYA0M4jO8DqQqZ41YqVAYPEC9Yfmvc6i5ADIQmrpCK8GTvW8Efs8BPIG/TsviF/lm6tKOgmUhdQSDEfO80k/sUo+1UmxTWNfLhPDQv13tt9IwJyul9cX9BT2kgEgC6kloGtAG4vSiH0Lgj9BzVd17sBPKVAlGQKkmUGY8LrYM4OKEU77znCwGZjuRedDCQAQQdinT6JyClDcRuz9EGykq+urOveQnncKFaiiDwFyPeeCri5pOO2dw8F/Y8k5emXdNjxU8YcAy5pV8m9Sb4sEsIbAvmledz6UZA4gRwKlD6e9AwIFvYut9V/P5fp+LsqwKtg3daHYbaeQ12pj16tmsf8k2yeXg0O9CWWnqddf/3cizNF5h/yykMbOphIMAfo2UD4Tq3KMBOi7qHWcXlnna+dDKQBQ8yjRh0NUIUiuw0LlAbrqT9arvZvpZ1JJLgTJtSxDdHGZzK7L5exgI8b6tl5d3/PMxiKoNPcC7udGVK5HsdesVXYk6ASa2DloSrE7H0oUAWKVX8dE1FqGyLdwWm4V2yeXb1JviQSK6CosXawL6kr2Yu2yWBEk19KA0TuBcyoDAl5Dwot0ft0rlFhlAUBUch1ngd5AdEVQX4NA+A1Gm3R+7TrKRGUFQFSygKMJWPNQuRihfy+HoAt0FaLL9braFx0PuIQqSwCikvmMpsaaBzILdJKdGM2MbssWgo8RXUE3j+hib+7c+aGyBiBesogGwtZsDBcDo+3EaGaZQKC0Y1iLWC10DFyrTZG3spaxeg0AUcnfE+Cw7tNQcyZGp4JMAYIlgqAb0d+isoGgrqaj/6te/yLJb/U6AJIlN1CHhE9DZSpGjwUagJE+QdCG8D6qbxCQlwn2e1WvZ4/Xx1RM9XoAnCSLGQrdX0LNkYh1GCIjEB2GMhzRUYjU9xgnQLAdQztoO8o2hK0gH2BkE8Fgq34fz2/Hllr/D1DoAB9bI40ZAAAAAElFTkSuQmCC|$(echo "$b64")|' ./src/ui.rs + b64="" + sed -i '/android: true/a \ \ adaptive_icon_background: "#ffffff"' ./flutter/pubspec.yaml + sed -i '/adaptive_icon_background/a \ \ adaptive_icon_foreground: "../res/icon.png"' ./flutter/pubspec.yaml + sed -i '/adaptive_icon_foreground:/a \ \ adaptive_icon_foreground_inset: 32' ./flutter/pubspec.yaml + sed -i '/ic_launcher_background/d' ./flutter/android/app/src/main/res/values/colors.xml + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + # ./flutter/pubspec.yaml + #sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + sed -i 's|name = "RustDesk"|name = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e 's|RustDesk|${{ env.appname }}|' ./flutter/android/app/src/main/res/values/strings.xml + sed -i -e "s|title: 'RustDesk'|title: '${{ env.appname }}'|" ./flutter/lib/main.dart + sed -i -e "s|return 'RustDesk';|return '${{ env.appname }}';|" ./flutter/lib/web/bridge.dart + sed -i 's|android:label="RustDesk"|android:label="${{ env.appname }}"|' ./flutter/android/app/src/main/AndroidManifest.xml + sed -i 's|android:label="RustDesk Input"|android:label="${{ env.appname }} Input"|' ./flutter/android/app/src/main/AndroidManifest.xml + sed -i 's|RustDesk is Open|${{ env.appname }} is Open|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt + sed -i 's|Show Rustdesk|Show ${{ env.appname }}|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt + sed -i 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt + sed -i 's|"RustDesk Service|"${{ env.appname }} Service|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt + sed -i 's|RustDesk|${{ env.appname }}|' ./flutter/lib/main.dart + sed -i 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/lib/desktop/widgets/tabbar_widget.dart + sed -i 's|"RustDesk"|"${{ env.appname }}"|' ./libs/hbb_common/src/config.rs + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|child: Text('rustdesk.com',|child: Text('${{ env.urlLink }}',|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + # Android never file-reads custom_.txt — the config must be handed to the + # native side. Both the server (Kotlin FFI.startServer) and the FFI init + # (Dart main_init) pass "" by default; embed the base64 config there so + # read_custom_client() runs and presets the password + permissions. + - name: Embed custom config for Android + shell: bash + run: | + KOTLIN=./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt + DART=./flutter/lib/models/native_model.dart + sed -i "s|FFI.startServer(configPath, \"\")|FFI.startServer(configPath, \"${{ env.custom }}\")|" "$KOTLIN" + sed -i "s|customClientConfig: '',|customClientConfig: '${{ env.custom }}',|" "$DART" + echo "--- verify ---" + grep -n 'FFI.startServer(configPath' "$KOTLIN" | sed 's/\(.\{80\}\).*/\1.../' + grep -n 'customClientConfig:' "$DART" | sed 's/\(.\{80\}\).*/\1.../' + + - name: change app id to custom + if: env.androidappid != 'com.carriez.flutter_hbb' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|com.carriez.flutter_hbb|${{ env.androidappid }}|' ./flutter/android/app/build.gradle + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + + - name: allow custom.txt + continue-on-error: true + shell: bash + run: | + sed -i -e '/const KEY:/,/};/d' ./src/common.rs + sed -i -e '/let Ok(data) = sign::verify(&data, &pk)/,/};/d' ./src/common.rs + # sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: Remove scam alert on Android + shell: bash + run: | + sed -i 's/bind.mainGetLocalOption(key:\s*"show-scam-warning")/"N"/g' ./flutter/lib/mobile/pages/server_page.dart + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + continue_on_error: true + command: | + cp .rdgen-src/.github/patches/xoffline.diff . + git apply xoffline.diff + + - name: hide-cm + if: env.hidecm == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + continue_on_error: true + command: | + cp .rdgen-src/.github/patches/hidecm.diff . + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + run: | + pushd ./flutter + flutter pub get + dart run flutter_launcher_icons + popd + sed -i '/ic_launcher_background/d' ./flutter/android/app/src/main/res/values/colors.xml + ########################################################## + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + x86_64-linux-android) + ./flutter/ndk_x64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + ;; + i686-linux-android) + ./flutter/ndk_x86.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + ;; + esac + + - name: Upload Rustdesk library to Artifacts + uses: actions/upload-artifact@v4 + with: + name: librustdesk.so.${{ matrix.job.target }} + path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so + + - name: icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + convert ./res/icon.png ./flutter/assets/icon.svg + convert ./res/128x128.png -resize 200% ./flutter/assets/128x128@2x.png || true + cp ./flutter/assets/icon.svg ./res/scalable.svg + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + # Increase Gradle JVM memory for CI builds + sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + x86_64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + i686-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk ./${{ env.filename }}-${{ matrix.job.arch }}.apk + popd + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + continue-on-error: true + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + # 30.0.2 no longer ships on ubuntu-24.04 runners (34.0.0 does); + # missing dir made the sign step fail silently via continue-on-error + BUILD_TOOLS_VERSION: "34.0.0" + + - name: Prefer signed APK when available + shell: bash + run: | + # sign-android-release writes *-signed.apk next to the input but the + # send steps below use the plain name: promote the signed file so the + # server always receives the best available signature (release key + # when present, debug fallback otherwise). Server-side names unchanged. + plain="./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" + signed="./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}-signed.apk" + if [ -f "$signed" ]; then + mv "$signed" "$plain" + echo "Using release-signed APK: $plain" + else + echo "No signed APK found, keeping unsigned/debug APK: $plain" + fi + ls -l ./signed-apk/ + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" ${{ env.apiServer }}/api/save_custom_client + + deploy: + needs: [build-rustdesk-android] + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - uses: geekyeggo/delete-artifact@v4 + continue-on-error: true + with: + name: ${{ env.filename }}-*.deb + + cleanup: + needs: [build-rustdesk-android, deploy] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v4 + with: + name: encrypted-secrets-zip diff --git a/.gitea/workflows/generator-linux.yml b/.gitea/workflows/generator-linux.yml new file mode 100644 index 0000000..42d3560 --- /dev/null +++ b/.gitea/workflows/generator-linux.yml @@ -0,0 +1,1598 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Custom Linux Client Generator +run-name: Custom Linux Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;files,/opt/vcpkg-cache,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + +jobs: + setup: + uses: ./.gitea/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge-linux: + uses: ./.gitea/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-rustdesk-linux: + needs: [generate-bridge-linux, setup] + name: build rustdesk linux ${{ matrix.job.target }} + runs-on: ${{ matrix.job.on }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + distro: ubuntu18.04, + on: ubuntu-22.04, + deb_arch: amd64, + vcpkg-triplet: x64-linux, + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + distro: ubuntu18.04, + on: ubuntu-22.04-arm, + deb_arch: arm64, + vcpkg-triplet: arm64-linux, + } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt-get update -y + sudo apt-get install -y nasm + if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then + sudo apt-get install -y qemu-user-static + fi + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Set Swap Space + if: ${{ matrix.job.arch == 'x86_64' }} + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df -h + free -m + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - name: Save Rust toolchain version + run: | + RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') + echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + - name: Disable rust bridge build + run: | + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + run: | + sudo apt install -y libva-dev && apt show libva-dev + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e '/-p tmpdeb\/usr\/lib\/rustdesk/d' ./build.py + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/allowCustom.py . + python allowCustom.py + cp .rdgen-src/.github/patches/removeSetupServerTip.diff . + git apply removeSetupServerTip.diff + # sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + echo -n '${{ env.custom }}' > ./custom_.txt + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/xoffline.diff . + git apply xoffline.diff + + - name: hide-cm + if: env.hidecm == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/hidecm.diff . + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Restore bridge files + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 + name: Build rustdesk + id: vcpkg + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt-get update -y + echo -e "installing deps" + apt-get install -y \ + build-essential \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + imagemagick \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libclang-10-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + pkg-config \ + tree \ + python3 \ + rpm \ + unzip \ + wget \ + xz-utils \ + libssl-dev + # we have libopus compiled by us. + apt-get remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + # do not use rustup, because memory overflow in qemu + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh + rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + export VCPKG_ROOT=/opt/artifacts/vcpkg + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + export JOBS="--jobs 3" + else + export JOBS="" + fi + echo $JOBS + + # Make the right unit stop when the services is stopped by tray + sed -ie "s|\(systemctl\s\+\S\+\s\){app_name}|\1${{ env.appname }}|g" src/platform/linux.rs + sed -ie 's|\({home}/{p}/{app_name0}2\?.toml\)|\\"\1\\"|g' src/platform/linux.rs + sed -ie 's|\(/root/{p}/\)|\\"\1\\"|g' src/platform/linux.rs + + cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release + rm -rf target/release/deps target/release/build + rm -rf ~/.cargo + + # Setup Flutter + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + x86_64) + export PATH=/opt/flutter/bin:$PATH + ;; + esac + popd + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + case ${{ matrix.job.arch }} in + aarch64) + # clone repo and reset to flutter ${{ env.FLUTTER_VERSION }} + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + git fetch + git reset --hard ${{ env.FLUTTER_VERSION }} + bin/flutter-elinux doctor -v + bin/flutter-elinux precache --linux + popd + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + rm -rf flutter + ;; + x86_64) + flutter doctor -v + ;; + esac + + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then + case ${{ matrix.job.arch }} in + aarch64) + pushd /opt/flutter-elinux/flutter + ;; + x86_64) + pushd /opt/flutter + ;; + esac + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + + # build flutter + pushd /workspace + mkdir output + chmod 777 output -R + export CARGO_INCREMENTAL=0 + export DEB_ARCH=${{ matrix.job.deb_arch }} + mkdir -p flutter/tmpdeb/usr/share/rustdesk + cp ./custom_.txt ./flutter/tmpdeb/usr/share/rustdesk/custom_.txt + if [[ "${{ env.logolink_url }}" != "false" ]]; then + wget -O ./flutter/assets/logo.png ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} + fi + if [[ "${{ env.iconlink_url }}" != "false" ]]; then + mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + convert ./res/icon.png ./flutter/assets/icon.svg + convert ./res/128x128.png -resize 200% ./flutter/assets/128x128@2x.png || true + cp ./flutter/assets/icon.svg ./res/scalable.svg + pushd ./flutter + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + export PATH="/opt/flutter-elinux/flutter/bin:$PATH" + fi + flutter pub get + dart run flutter_launcher_icons + popd + fi + + # Only run the whitelabeling steps if the custom app name is actually different + if [ "${{ env.appname }}" != "rustdesk" ]; then + # Change files names in the output .deb + sed -ie "s|\(cp .* \)tmpdeb/usr/share/rustdesk/files/systemd/|\1tmpdeb/usr/share/rustdesk/files/systemd/${{ env.appname }}.service|g" build.py + sed -ie "s|cp -r \(.\+\)\* \([^ ]\+\)/rustdesk|mv \1rustdesk \1${{ env.appname }}; cp -r \1\* \2/${{ env.appname }}|g" build.py + sed -ie "s|\(tmpdeb/[^ ]*\)rustdesk|\1${{ env.appname }}|g" build.py + # The rename above is greedy in \1, so it rewrites only the LAST + # "rustdesk" of each path and leaves build.py's globs pointing where the + # staging tree never exists. Three corrections, all confined to the drm + # code paths (assert_staged_binary_is_drm, measured_glibc_floor); the + # stock packaging path never executes those lines. + # + # 1. Directory component: the cp -r rename above stages into + # tmpdeb/usr/share//, but the globs kept usr/share/rustdesk/. + sed -ie "s|tmpdeb/usr/share/rustdesk/|tmpdeb/usr/share/${{ env.appname }}/|g" build.py + # 2. Library filename: build.py renames only the executable and copies the + # bundle verbatim, so the cdylib stays librustdesk.so. The greedy sed + # renamed it to lib.so, and that is the file carrying the drm + # marker -- the thin Flutter launcher does not. + sed -ie "s|lib/lib${{ env.appname }}\.so|lib/librustdesk.so|g" build.py + # 3. libdrmtap must NOT be renamed: drmtap_dl.rs dlopens the absolute path + # /usr/lib/rustdesk/libdrmtap.so.0, compiled into the binary and + # rewritten by nothing here. Staging it under /usr/lib// + # packages and installs cleanly, then never loads on the user's machine. + sed -ie "s|tmpdeb/usr/lib/${{ env.appname }}|tmpdeb/usr/lib/rustdesk|g" build.py + sed -ie "s|Package: rustdesk|Package: ${{ env.appname }}|g" build.py + sed -ie "s|RustDesk|${{ env.appname }}|g" res/rustdesk.desktop + FILES=$(sed -ne "s~.*cp\s\(../\)\?\+\(.\+\.\(desktop\|service\)\)\s\+tmpdeb/.*~\2~p" build.py | sort | uniq) + FILES=$(echo -e "$FILES\n$(ls --color=never res/DEBIAN/*)") + for FILE in $(echo $FILES | sed "s/\n/ /g"); do + sed -ie "s|rustdesk|${{ env.appname }}|g" $FILE + done + for file in "./res/rpm-flutter.spec" "./res/rpm-flutter-suse.spec"; do + sed -ie "s|^\(URL:\s*\).*|\1${{ env.downloadLink }}|g" "$file" + sed -ie "s|^\(Vendor:\s*\).*|\1${{ env.compname }}|g" "$file" + sed -ie '/\${\?HBB}\?/!{/\(^Name:\|\/usr\|\/etc\|systemctl\)/s|rustdesk|${{ env.appname }}|g}' "$file" + sed -ie '/\${\?HBB}\?/{/^install/s|\(\/usr\/.*\/\)rustdesk|\1${{ env.appname }}|g}' "$file" + sed -ie '/\${\?HBB}\?/{/^mkdir/s|rustdesk|${{ env.appname }}|g}' "$file" + for extension in ".desktop" "-link.desktop" ".service"; do + sed -ie "/^%files$/i\mv %{buildroot}/usr/share/${{ env.appname }}/files/rustdesk$extension %{buildroot}/usr/share/${{ env.appname }}/files/${{ env.appname }}$extension" "$file" + sed -ie "/^%files$/i\sed -ie 's|RustDesk|${{ env.appname }}|g' %{buildroot}/usr/share/${{ env.appname }}/files/${{ env.appname }}$extension" "$file" + sed -ie "/^%files$/i\sed -ie 's|rustdesk|${{ env.appname }}|g' %{buildroot}/usr/share/${{ env.appname }}/files/${{ env.appname }}$extension" "$file" + done + sed -ie '/^%files$/i\\n' "$file" + done + fi + + python3 ./build.py --flutter --skip-cargo + for name in *.deb; do + mv "$name" /workspace/output/"${{ env.filename }}-${{ matrix.job.arch }}.deb" + done + + # rpm/suse/arch package the flutter BUNDLE (not tmpdeb), so the deb's + # custom_.txt never reaches them. Copy it into the bundle too, so the + # baked password/permissions land in every format. + BUNDLE=flutter/build/linux/x64/release/bundle + [ "${{ matrix.job.arch }}" = "aarch64" ] && BUNDLE=flutter/build/linux/arm64/release/bundle + cp ./custom_.txt "$BUNDLE/custom_.txt" || echo "WARN: could not copy custom_.txt into bundle" + ls -l "$BUNDLE/custom_.txt" || echo "WARN: custom_.txt missing from bundle ($BUNDLE)" + + # rpm package + echo -e "start packaging fedora package" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + for name in *.rpm; do + mv "$name" /workspace/output/"${{ env.filename }}-${{ matrix.job.arch }}.rpm" + done + + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + for name in *.rpm; do + mv "$name" /workspace/output/"${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" + done + + # only x86_64 for arch since we can not find newest arm64 docker image to build + # old arch image does not make sense for arch since it is "arch" which always update to date + # and failed to makepkg arm64 on x86_64 + - name: Patch archlinux PKGBUILD + continue-on-error: true + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' && env.VERSION != 'master' + run: | + sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + sed -i "s/x86_64/aarch64/g" ./res/PKGBUILD + fi + + - name: Build archlinux package + continue-on-error: true + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' && env.VERSION != 'master' + uses: rustdesk-org/arch-makepkg-action@master + with: + packages: + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Rename archlinux package + continue-on-error: true + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' && env.VERSION != 'master' + run: | + cp ./res/rustdesk-${{ env.VERSION }}-0-x86_64.pkg.tar.zst ./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" ${{ env.apiServer }}/api/save_custom_client || true + + - name: Upload deb + uses: actions/upload-artifact@v4 + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: ${{ env.filename }}-${{ matrix.job.arch }}.deb + path: ./output/${{ env.filename }}-${{ matrix.job.arch }}.deb + + build-rustdesk-linux-drm: + needs: [generate-bridge-linux, setup] + name: build rustdesk linux drm x86_64 + runs-on: ubuntu-22.04 + # Only rustdesk's master branch carries the drm/libdrmtap support this job + # drives: build.py at tag 1.4.9 has neither build_libdrmtap_so nor + # assert_staged_binary_is_drm, so a versioned build dies at + # AttributeError: module 'b' has no attribute 'build_libdrmtap_so' + # One failed job marks the whole run failed, so a client that was in fact + # built got reported as a build failure. No other job consumes this one's + # artifacts (deploy needs only build-rustdesk-linux, build-flatpak and + # build-appimage). Gated the way bridge.yml already tells the two sources + # apart. + if: ${{ inputs.version == 'master' }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt-get update -y + sudo apt-get install -y nasm qemu-user-static + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Detect DRM support + run: | + if grep -q "'--drm'" build.py; then + echo "DRM_SUPPORTED=true" >> $GITHUB_ENV + echo "::notice::DRM/KMS capture backend detected in this RustDesk version" + else + echo "DRM_SUPPORTED=false" >> $GITHUB_ENV + echo "::notice::DRM/KMS capture backend not present in this RustDesk version, skipping DRM build" + fi + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df -h + free -m + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: x86_64-unknown-linux-gnu + components: "rustfmt" + + - name: Save Rust toolchain version + run: | + RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') + echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + - name: Disable rust bridge build + run: | + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + sudo apt install -y libva-dev && apt show libva-dev + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet x64-linux \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-x64-linux-rel-out.log" || true + shell: bash + + - name: Build libdrmtap + if: env.DRM_SUPPORTED == 'true' + run: | + sudo apt-get install -y meson ninja-build pkg-config \ + libdrm-dev libegl1-mesa-dev libgles2-mesa-dev + python3 - <<'PY' + import importlib.util, sys + spec = importlib.util.spec_from_file_location("b", "build.py") + b = importlib.util.module_from_spec(spec) + sys.argv = ["build.py"] + spec.loader.exec_module(b) + # build_libdrmtap_so() exists only in recent RustDesk sources + # (master); tagged 1.4.x releases don't have it - skip gracefully + # instead of failing the whole drm job. + if hasattr(b, 'build_libdrmtap_so'): + print(f"::notice::built {b.build_libdrmtap_so()}") + else: + print("::notice::build_libdrmtap_so not present in this RustDesk version, skipping") + PY + shell: bash + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e '/-p tmpdeb\/usr\/lib\/rustdesk/d' ./build.py + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/allowCustom.py . + python allowCustom.py + cp .rdgen-src/.github/patches/removeSetupServerTip.diff . + git apply removeSetupServerTip.diff + echo -n '${{ env.custom }}' > ./custom_.txt + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/xoffline.diff . + git apply xoffline.diff + + - name: hide-cm + if: env.hidecm == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/hidecm.diff . + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Restore bridge files + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 + if: env.DRM_SUPPORTED == 'true' + name: Build rustdesk + id: vcpkg + with: + arch: x86_64 + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt-get update -y + echo -e "installing deps" + apt-get install -y \ + build-essential \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + imagemagick \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libclang-10-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + pkg-config \ + tree \ + python3 \ + rpm \ + unzip \ + wget \ + xz-utils \ + libssl-dev + # we have libopus compiled by us. + apt-get remove -y libopus-dev || true + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + git config --global --add safe.directory "*" + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu && ./install.sh + rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + """ > ~/.cargo/config + cat ~/.cargo/config + + # Start Cargo compilation with DRM features + pushd /workspace + export VCPKG_ROOT=/opt/artifacts/vcpkg + export DRMTAP_PREBUILT_DIR=/workspace/third_party/libdrmtap/build-pkg + FEATURES=$(python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --print-features) + for want in drm drm-wake; do + case ",$FEATURES," in + *",$want,"*) ;; + *) echo "::error::build.py returned no '$want' feature: $FEATURES"; exit 1 ;; + esac + done + + sed -ie "s|\(systemctl\s\+\S\+\s\){app_name}|\1${{ env.appname }}|g" src/platform/linux.rs + sed -ie 's|\({home}/{p}/{app_name0}2\?.toml\)|\\"\1\\"|g' src/platform/linux.rs + sed -ie 's|\(/root/{p}/\)|\\"\1\\"|g' src/platform/linux.rs + + cargo build --locked --lib --features "$FEATURES" --release + rm -rf target/release/deps target/release/build + rm -rf ~/.cargo + + # Setup Flutter + git config --global --add safe.directory "*" + export PATH=/opt/flutter/bin:$PATH + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + flutter doctor -v + + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then + pushd /opt/flutter + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + + # Build Flutter Bundle & Deb + pushd /workspace + mkdir -p output + chmod 777 output -R + export CARGO_INCREMENTAL=0 + export DEB_ARCH=amd64 + mkdir -p flutter/tmpdeb/usr/share/rustdesk + cp ./custom_.txt ./flutter/tmpdeb/usr/share/rustdesk/custom_.txt + if [[ "${{ env.logolink_url }}" != "false" ]]; then + wget -O ./flutter/assets/logo.png ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} + fi + if [[ "${{ env.iconlink_url }}" != "false" ]]; then + mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + convert ./res/icon.png ./flutter/assets/icon.svg + convert ./res/128x128.png -resize 200% ./flutter/assets/128x128@2x.png || true + cp ./flutter/assets/icon.svg ./res/scalable.svg + pushd ./flutter + flutter pub get + dart run flutter_launcher_icons + popd + fi + + if [ "${{ env.appname }}" != "rustdesk" ]; then + sed -ie "s|\(cp .* \)tmpdeb/usr/share/rustdesk/files/systemd/|\1tmpdeb/usr/share/rustdesk/files/systemd/${{ env.appname }}.service|g" build.py + sed -ie "s|cp -r \(.\+\)\* \([^ ]\+\)/rustdesk|mv \1rustdesk \1${{ env.appname }}; cp -r \1\* \2/${{ env.appname }}|g" build.py + sed -ie "s|\(tmpdeb/[^ ]*\)rustdesk|\1${{ env.appname }}|g" build.py + # The rename above is greedy in \1, so it rewrites only the LAST + # "rustdesk" of each path and leaves build.py's globs pointing where the + # staging tree never exists. Three corrections, all confined to the drm + # code paths (assert_staged_binary_is_drm, measured_glibc_floor); the + # stock packaging path never executes those lines. + # + # 1. Directory component: the cp -r rename above stages into + # tmpdeb/usr/share//, but the globs kept usr/share/rustdesk/. + sed -ie "s|tmpdeb/usr/share/rustdesk/|tmpdeb/usr/share/${{ env.appname }}/|g" build.py + # 2. Library filename: build.py renames only the executable and copies the + # bundle verbatim, so the cdylib stays librustdesk.so. The greedy sed + # renamed it to lib.so, and that is the file carrying the drm + # marker -- the thin Flutter launcher does not. + sed -ie "s|lib/lib${{ env.appname }}\.so|lib/librustdesk.so|g" build.py + # 3. libdrmtap must NOT be renamed: drmtap_dl.rs dlopens the absolute path + # /usr/lib/rustdesk/libdrmtap.so.0, compiled into the binary and + # rewritten by nothing here. Staging it under /usr/lib// + # packages and installs cleanly, then never loads on the user's machine. + sed -ie "s|tmpdeb/usr/lib/${{ env.appname }}|tmpdeb/usr/lib/rustdesk|g" build.py + sed -ie "s|Package: rustdesk|Package: ${{ env.appname }}|g" build.py + sed -ie "s|RustDesk|${{ env.appname }}|g" res/rustdesk.desktop + FILES=$(sed -ne "s~.*cp\s\(../\)\?\+\(.\+\.\(desktop\|service\)\)\s\+tmpdeb/.*~\2~p" build.py | sort | uniq) + FILES=$(echo -e "$FILES\n$(ls --color=never res/DEBIAN/*)") + for FILE in $(echo $FILES | sed "s/\n/ /g"); do + sed -ie "s|rustdesk|${{ env.appname }}|g" $FILE + done + fi + + python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --skip-cargo + for name in *.deb; do + mv "$name" /workspace/output/"${{ env.filename }}-unattended-wayland-x86_64.deb" + done + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' && env.DRM_SUPPORTED == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' && env.DRM_SUPPORTED == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" ${{ env.apiServer }}/api/save_custom_client + + - name: Upload deb + uses: actions/upload-artifact@v4 + if: ${{ env.UPLOAD_ARTIFACT == 'true' && env.DRM_SUPPORTED == 'true' }} + with: + name: ${{ env.filename }}-unattended-wayland-x86_64.deb + path: ./output/${{ env.filename }}-unattended-wayland-x86_64.deb + + build-appimage: + name: Build appimage ${{ matrix.job.target }} + needs: [build-rustdesk-linux] + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, arch: x86_64 } + - { target: aarch64-unknown-linux-gnu, arch: aarch64 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Download Binary + uses: actions/download-artifact@v4 + with: + name: ${{ env.filename }}-${{ matrix.job.arch }}.deb + path: . + + - name: Rename Binary + run: | + mv ${{ env.filename }}-${{ matrix.job.arch }}.deb appimage/rustdesk.deb + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + + # Embed the PNG into the SVG + cat < ./res/scalable.svg + + + + EOF + + - name: Build AppImage + shell: bash + run: | + if [ "${{ env.appname }}" != "rustdesk" ] && [ "${{ env.appname }}" != "RustDesk" ]; then + # The recipe's script does `bsdtar -zxvf rustdesk.deb`, so after the sed + # below it looks for .deb -- the deb must end up under exactly + # that name. Renaming it to .deb only matched when appname and + # filename happened to be equal. The deb is moved out of the way first + # because `appimage/*` globs it too, and sed over an ar archive rewrites + # bytes inside it. + mv appimage/rustdesk.deb "${RUNNER_TEMP}/appimage-payload.deb" + sed -ie "s|rustdesk|${{ env.appname }}|g" appimage/* + mv "${RUNNER_TEMP}/appimage-payload.deb" "appimage/${{ env.appname }}.deb" + fi + sudo apt-get update -y + sudo apt-get install -y libarchive-tools libfuse2 + sudo pip3 install "setuptools_scm<10" + sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + # recipe output is --.AppImage, where is the + # version stamped in AppImageBuilder-.yml -- NOT the workflow's VERSION + # input. They coincide for a tagged build, which is why globbing on VERSION + # worked there, but a master build passes VERSION=master while the recipe + # still carries a release number, so the glob matched nothing: + # mv: cannot stat './*-master-x86_64.AppImage': No such file or directory + # Match on the arch suffix instead, and exclude the destination name so this + # can never try to move a file onto itself. + dest="${{ env.filename }}-${{ matrix.job.arch }}.AppImage" + src=$(find . -maxdepth 1 -name "*-${{ matrix.job.arch }}.AppImage" ! -name "$dest" | head -1) + if [ -z "$src" ]; then + echo "::error::no *-${{ matrix.job.arch }}.AppImage produced by appimage-builder"; ls -l; exit 1 + fi + sudo mv "$src" "./$dest" + popd + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" ${{ env.apiServer }}/api/save_custom_client + + build-flatpak: + name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} + needs: + - build-rustdesk-linux + runs-on: ${{ matrix.job.on }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + distro: ubuntu22.04, + on: ubuntu-22.04, + arch: x86_64, + suffix: "", + } + - { + target: aarch64-unknown-linux-gnu, + # try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" + distro: ubuntu22.04, + on: ubuntu-22.04-arm, + arch: aarch64, + suffix: "", + } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Download Binary + uses: actions/download-artifact@v4 + with: + name: ${{ env.filename }}-${{ matrix.job.arch }}.deb + path: . + + - name: Rename Binary + run: | + mv ${{ env.filename }}-${{ matrix.job.arch }}.deb flatpak/rustdesk.deb + + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + continue-on-error: false + timeout-minutes: 30 + id: flatpak + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt-get update -y + apt-get install -y git flatpak flatpak-builder + run: | + # Flatpak app IDs are reverse-DNS without spaces, and the two seds + # below would re-match an inserted name containing "RustDesk" + # (e.g. MyRustDesk -> MyMyRustDesk), so rewrite via placeholders + # and keep a space-free ID for the manifest and the bundle. + BUNDLE_ID="com.rustdesk.RustDesk" + if [ "${{ env.appname }}" != "rustdesk" ] && [ "${{ env.appname }}" != "RustDesk" ]; then + # Same as the AppImage job: the manifest's source path is rewritten to + # .deb by the seds below, so the file must carry that name, and + # the deb is moved aside first because `flatpak/*` globs it too. + mv flatpak/rustdesk.deb /tmp/flatpak-payload.deb + sed -ie "s|rustdesk|@@RDGEN_FLAT_0@@|g" flatpak/* + sed -ie "s|RustDesk|@@RDGEN_FLAT_1@@|g" flatpak/* + sed -ie "s|@@RDGEN_FLAT_0@@|${{ env.appname }}|g" flatpak/* + sed -ie "s|@@RDGEN_FLAT_1@@|${{ env.appname }}|g" flatpak/* + mv /tmp/flatpak-payload.deb "flatpak/${{ env.appname }}.deb" + # The manifest's other source is the metainfo file. Those same seds rewrote + # the REFERENCE to it without renaming the file on disk, so only the name + # is left to fix; its contents are already rewritten. + FLAT="$(echo "${{ env.appname }}" | tr -d ' ')" + BUNDLE_ID="com.${FLAT}.${FLAT}" + sed -ie "s|\"id\": \"com[^\"]*\"|\"id\": \"${BUNDLE_ID}\"|" flatpak/rustdesk.json + sed -ie "s|\"path\": \"com[^\"]*\.metainfo\.xml\"|\"path\": \"${BUNDLE_ID}.metainfo.xml\"|" flatpak/rustdesk.json + sed -ie "s|com[^<]*|${BUNDLE_ID}|" flatpak/*.metainfo.xml + mv flatpak/com.rustdesk.RustDesk.metainfo.xml \ + "flatpak/${BUNDLE_ID}.metainfo.xml" + fi + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo ${{ env.filename }}-${{ matrix.job.arch }}.flatpak "$BUNDLE_ID" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" ${{ env.apiServer }}/api/save_custom_client + + deploy: + needs: [build-rustdesk-linux,build-flatpak,build-appimage] + if: always() + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - uses: geekyeggo/delete-artifact@v4 + continue-on-error: true + with: + name: ${{ env.filename }}-*.deb + + cleanup: + needs: [build-rustdesk-linux,build-flatpak,build-appimage,deploy] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v4 + with: + name: encrypted-secrets-zip diff --git a/.gitea/workflows/generator-macos.yml b/.gitea/workflows/generator-macos.yml new file mode 100644 index 0000000..321f392 --- /dev/null +++ b/.gitea/workflows/generator-macos.yml @@ -0,0 +1,775 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Custom macOS Client Generator +run-name: Custom macOS Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;files,/Users/runner/vcpkg-cache,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + +jobs: + setup: + uses: ./.gitea/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge: + uses: ./.gitea/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-for-macos: + name: ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + needs: [generate-bridge, setup] + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + vcpkg-triplet: x64-osx, + } + - { + target: aarch64-apple-darwin, + os: macos-14, + # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 + extra-build-args: "--screencapturekit", + arch: aarch64, + vcpkg-triplet: arm64-osx, + } + env: + STATUS_URL: "${{ secrets.GENURL }}/updategh" + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install imagemagick and potrace and nasm and and + shell: bash + run: | + brew install imagemagick potrace nasm cmake gcc wget ninja + echo "$(brew --prefix imagemagick)/bin" >> $GITHUB_PATH + + - name: Update macOS Info.plist and settings + continue-on-error: false + shell: bash + run: | + # MACSTUFF Update Info.plist + sed -i '' -e 's|CFBundleName.*.*|CFBundleName\n\t${{ env.appname }}|' ./flutter/macos/Runner/Info.plist + sed -i '' -e 's|CFBundleDisplayName.*.*|CFBundleDisplayName\n\t${{ env.appname }}|' ./flutter/macos/Runner/Info.plist + sed -i '' -e 's|CFBundleIdentifier.*.*|CFBundleIdentifier\n\tcom.${{ env.appname }}.app|' ./flutter/macos/Runner/Info.plist + + # MACSTUFF Update AppInfo.xcconfig + sed -i '' -e 's|PRODUCT_NAME = .*|PRODUCT_NAME = ${{ env.appname }}|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + sed -i '' -e 's|PRODUCT_BUNDLE_IDENTIFIER = .*|PRODUCT_BUNDLE_IDENTIFIER = com.${{ env.appname }}.app|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + sed -i '' -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + sed -i '' -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + # Update Xcode project settings + sed -i '' -e 's/PRODUCT_NAME = "RustDesk"/PRODUCT_NAME = "${{ env.appname }}"/' ./flutter/macos/Runner.xcodeproj/project.pbxproj + sed -i '' -e 's/PRODUCT_BUNDLE_IDENTIFIER = ".*"/PRODUCT_BUNDLE_IDENTIFIER = "com.${{ env.appname }}.app"/' ./flutter/macos/Runner.xcodeproj/project.pbxproj + + # Update CMake settings + if [ -f "./flutter/macos/CMakeLists.txt" ]; then + sed -i '' -e 's/set(BINARY_NAME ".*")/set(BINARY_NAME "${{ env.appname }}")/' ./flutter/macos/CMakeLists.txt + fi + + # Update Podfile - keep the target as 'Runner' + sed -i '' -e 's/target '"'"'Runner'"'"' do/target '"'"'Runner'"'"' do/' ./flutter/macos/Podfile + + find ./src/lang -name "*.rs" -exec sed -i '' -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i '' -e 's|RustDesk|${{ env.appname }}|' ./src/lang/nl.rs + + sed -i '' -e 's|https://rustdesk.com|${{ env.urlLink }}|' ./build.py + sed -i '' -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i '' -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i '' -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i '' -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i '' -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i '' -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + sed -i -e 's|$downloadUrl/$downloadFile|${{ env.downloadLink }}|' ./flutter/lib/desktop/widgets/update_progress.dart + + # Update slogan - About in en.rs + sed -i '' -e 's/("Slogan_tip", "Made with heart in this chaotic world!")/("Slogan_tip", "Powered by ${{ env.appname }}")/' ./src/lang/en.rs + sed -i '' -e 's/("About RustDesk", "")/("About RustDesk", "About ${{ env.appname }}")/' ./src/lang/en.rs + + + # Update slogan - About in nl.rs + sed -i '' -e 's/("Slogan_tip", "Ontwikkeld met het hart voor deze chaotische wereld!")/("Slogan_tip", "Powered by ${{ env.appname }}")/' ./src/lang/nl.rs + sed -i '' -e 's/("Your Desktop", "Uw Bureaublad")/("Your Desktop", "Uw ${{ env.appname }}")/' ./src/lang/nl.rs + sed -i '' -e 's/("About RustDesk", "Over RustDesk")/("About RustDesk", "Over ${{ env.appname }}")/' ./src/lang/nl.rs + sed -i '' -e 's/("About", "Over")/("About", "Over ${{ env.appname }}")/' ./src/lang/nl.rs + + # Update pubspec.yaml with proper YAML formatting + cp ./flutter/pubspec.yaml ./flutter/pubspec.yaml.bak + echo " archive: ^3.6.1" > ./flutter/temp_dependency.txt + awk '/intl:/{print;system("cat ./flutter/temp_dependency.txt");next}1' ./flutter/pubspec.yaml > ./flutter/pubspec.yaml.tmp + mv ./flutter/pubspec.yaml.tmp ./flutter/pubspec.yaml + rm ./flutter/temp_dependency.txt + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cp .rdgen-src/.github/patches/allowCustom.py . + python allowCustom.py + cp .rdgen-src/.github/patches/removeSetupServerTip.diff . + git apply removeSetupServerTip.diff + # sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: Install build runtime + run: | + brew install llvm create-dmg + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi + + - name: Install NASM + run: | + # Install NASM 2.16.x from official release. + # Do NOT use `brew install nasm` which installs NASM 3.x. + # NASM 3.x is a complete rewrite with incompatible CLI options and removed features. + # aom and other multimedia libraries require NASM 2.x for x86/x86_64 assembly. + wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/macosx/nasm-2.16.03-macosx.zip + unzip nasm-2.16.03-macosx.zip + sudo cp nasm-2.16.03/nasm /usr/local/bin/nasm + nasm --version + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Patch flutter + continue-on-error: true + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Workaround for flutter issue + shell: bash + run: | + cd "$(dirname "$(which flutter)")" + # https://github.com/flutter/flutter/issues/133533 + sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart + grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.MAC_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Magick stuff for macOS + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + # Create all necessary directories first + mkdir -p ./res + mkdir -p ./flutter/assets + mkdir -p ./flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset + mkdir -p ./macos/Runner/Assets.xcassets/AppIcon.appiconset + mkdir -p ./rustdesk/data/flutter_assets/assets + + # Download icon using curl with additional SSL options + curl -k -L --tlsv1.2 --proto =https --ssl-reqd \ + -H "User-Agent: Mozilla/5.0" \ + "${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}&uuid=${{ env.iconlink_uuid }}" \ + -o ./res/icon.png || wget --no-check-certificate -O ./res/icon.png "${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}&uuid=${{ env.iconlink_uuid }}" + + # Backup existing files (if they exist) + [ -f "./res/32x32.png" ] && mv ./res/32x32.png ./res/32x32.png.bak + [ -f "./res/64x64.png" ] && mv ./res/64x64.png ./res/64x64.png.bak + [ -f "./res/128x128.png" ] && mv ./res/128x128.png ./res/128x128.png.bak + [ -f "./res/mac-icon.png" ] && mv ./res/mac-icon.png ./res/mac-icon.png.bak + [ -f "./flutter/assets/icon.png" ] && mv ./flutter/assets/icon.png ./flutter/assets/icon.png.bak + [ -f "./flutter/assets/icon.svg" ] && mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + [ -f "./rustdesk/data/flutter_assets/assets/icon.svg" ] && mv ./rustdesk/data/flutter_assets/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg.bak + + # Create standard app icons + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + + # Copy icon to Flutter assets + cp ./res/icon.png ./flutter/assets/icon.png + cp ./res/icon.png ./rustdesk/data/flutter_assets/assets/icon.png + + # Convert PNG to SVG using potrace + magick ./res/icon.png -flatten ./temp_icon.pbm + potrace --svg -o ./flutter/assets/icon.svg ./temp_icon.pbm + cp ./flutter/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg + rm ./temp_icon.pbm + + # Create macOS app icons + magick ./res/icon.png -resize 16x16 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png" + magick ./res/icon.png -resize 32x32 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png" + magick ./res/icon.png -resize 64x64 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png" + magick ./res/icon.png -resize 128x128 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png" + magick ./res/icon.png -resize 256x256 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png" + magick ./res/icon.png -resize 512x512 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" + magick ./res/icon.png -resize 1024x1024 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png" + + # Create macOS specific icons + magick ./res/icon.png -resize 128x128 ./res/mac-icon.png + + # Create dark mode tray icon (optimized for macOS menu bar) + magick ./res/icon.png -resize 22x22 -colorspace gray -alpha set -background none -channel A -evaluate set 100% ./res/mac-tray-dark-x2.png + + # Create light mode tray icon (optimized for macOS menu bar) + magick ./res/icon.png -resize 22x22 -negate -colorspace gray -alpha set -background none -channel A -evaluate set 100% ./res/mac-tray-light-x2.png + + # Create AppIcon.icns (macOS native icon format) + mkdir -p ./iconset.iconset + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png" "./iconset.iconset/icon_16x16.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png" "./iconset.iconset/icon_16x16@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png" "./iconset.iconset/icon_32x32.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png" "./iconset.iconset/icon_32x32@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png" "./iconset.iconset/icon_128x128.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png" "./iconset.iconset/icon_128x128@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png" "./iconset.iconset/icon_256x256.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" "./iconset.iconset/icon_256x256@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" "./iconset.iconset/icon_512x512.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png" "./iconset.iconset/icon_512x512@2x.png" + iconutil -c icns ./iconset.iconset -o ./flutter/macos/Runner/AppIcon.icns + rm -rf ./iconset.iconset + + # Create Contents.json for macOS app icon + echo '{ + "images": [ + {"size":"16x16","idiom":"mac","filename":"app_icon_16.png","scale":"1x"}, + {"size":"16x16","idiom":"mac","filename":"app_icon_32.png","scale":"2x"}, + {"size":"32x32","idiom":"mac","filename":"app_icon_32.png","scale":"1x"}, + {"size":"32x32","idiom":"mac","filename":"app_icon_64.png","scale":"2x"}, + {"size":"128x128","idiom":"mac","filename":"app_icon_128.png","scale":"1x"}, + {"size":"128x128","idiom":"mac","filename":"app_icon_256.png","scale":"2x"}, + {"size":"256x256","idiom":"mac","filename":"app_icon_256.png","scale":"1x"}, + {"size":"256x256","idiom":"mac","filename":"app_icon_512.png","scale":"2x"}, + {"size":"512x512","idiom":"mac","filename":"app_icon_512.png","scale":"1x"}, + {"size":"512x512","idiom":"mac","filename":"app_icon_1024.png","scale":"2x"} + ], + "info": { + "version": 1, + "author": "xcode" + } + }' > "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json" + + # Copy icons and Contents.json to both locations + cp -r flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/* macos/Runner/Assets.xcassets/AppIcon.appiconset/ + + # Verify files exist and show their sizes + echo "Verifying generated files:" + ls -lh ./res/mac-tray-dark-x2.png + ls -lh ./res/mac-tray-light-x2.png + ls -lh ./res/mac-icon.png + echo "Flutter macOS app icons:" + ls -lh flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/ + echo "Flutter assets:" + ls -lh flutter/assets/icon.* + echo "RustDesk Flutter assets:" + ls -lh rustdesk/data/flutter_assets/assets/ + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + cd ./flutter + # Create required directories and files + mkdir -p web + mkdir -p assets + echo '{"name":"${{ env.appname }}","short_name":"${{ env.appname }}","start_url":"/","display":"standalone","background_color":"#ffffff","theme_color":"#ffffff","description":"A remote desktop software."}' > web/manifest.json + echo '${{ env.appname }}' > web/index.html + + # Ensure the AppIcon.appiconset directory exists + mkdir -p macos/Runner/Assets.xcassets/AppIcon.appiconset + + # Copy the processed icons to Flutter locations + cp ../res/mac-icon.png ./assets/icon.png + cp ../flutter/assets/icon.svg ./assets/icon.svg || true + + flutter pub upgrade win32 + flutter pub get + flutter pub run flutter_launcher_icons + cd .. + + - name: ui.rs + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + if [ -f "./res/icon.png" ]; then + SEARCH_STR="iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAEiuAABIrgHwmhA7AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAEx9JREFUeJztnXmYHMV5h9" + b64=$(base64 < ./res/icon.png) + sed -i '' -e "s~$SEARCH_STR.*\"~$b64\"~" ./src/ui.rs + fi + + - name: fix connection delay + continue-on-error: false + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + continue-on-error: true + if: env.xOffline == 'true' + run: | + cp .rdgen-src/.github/patches/xoffline.diff . + git apply xoffline.diff + + - name: hide-cm + continue-on-error: true + if: env.hidecm == 'true' + run: | + cp .rdgen-src/.github/patches/hidecm.diff . + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Restore bridge files + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + + - name: Create MacOS directory structure + run: | + mkdir -p ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS + + - name: Build rustdesk + run: | + if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then + MIN_MACOS_VERSION="12.3" + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py + sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile + sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj + fi + sed -i -e "s/RustDesk.app/\"${{ env.appname }}.app\"/" build.py + ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} + + # - name: Copy service file + # run: | + # cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/ + + - name: Embed custom config into the .app bundle + shell: bash + run: | + APP=$(find ./flutter/build/macos/Build/Products/Release -maxdepth 1 -name "*.app" | head -n 1) + echo "App bundle: $APP" + echo -n '${{ env.custom }}' > "$APP/Contents/MacOS/custom_.txt" + ls -l "$APP/Contents/MacOS/custom_.txt" + + - name: Install rcodesign tool + if: env.MACOS_P12_BASE64 != null + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm cmake gcc wget ninja + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: icon svg handling + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + ASSETS_DIR="build/macos/Build/Products/Release/RustDesk.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets" + mkdir -p "$ASSETS_DIR" + if [ -f "$ASSETS_DIR/icon.svg" ]; then + mv "$ASSETS_DIR/icon.svg" "$ASSETS_DIR/icon.svg.bak" + fi + # First convert PNG to PBM (bitmap) + magick convert ./res/icon.png ./temp_icon.pbm + # Then use potrace to convert to SVG + potrace --svg -o "$ASSETS_DIR/icon.svg" ./temp_icon.pbm + rm ./temp_icon.pbm + + - name: logo handling + if: ${{ env.logolink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + ASSETS_DIR="build/macos/Build/Products/Release/RustDesk.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets" + mkdir -p "$ASSETS_DIR" + curl -k -L --tlsv1.2 --proto =https --ssl-reqd \ + -H "User-Agent: Mozilla/5.0" \ + "${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}&uuid=${{ env.logolink_uuid }}" \ + -o "$ASSETS_DIR/logo.png" || \ + wget --no-check-certificate \ + -O "$ASSETS_DIR/logo.png" \ + "${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}&uuid=${{ env.logolink_uuid }}" + + - name: Sign macOS app bundle + if: env.MACOS_P12_BASE64 != '' + run: | + cd flutter/build/macos/Build/Products/Release + # Debug info + echo "Current directory contents:" + ls -la + + # Rename RustDesk.app to the custom app name first + if [ -d "RustDesk.app" ]; then + # First rename the app if it's still called RustDesk.app + mv "RustDesk.app" "${{ env.appname }}.app" + echo "Renamed RustDesk.app to ${{ env.appname }}.app" + fi + + echo "App bundle contents after rename:" + ls -la "${{ env.appname }}.app" || echo "App not found" + ls -la "${{ env.appname }}.app/Contents" || echo "Contents not found" + + # Decode the certificate + echo "${{ secrets.MACOS_P12_BASE64 }}" | base64 --decode > certificate.p12 + + # Sign the app bundle and its contents + if [ -d "${{ env.appname }}.app/Contents/MacOS" ]; then + echo "Signing main executable..." + MAIN_EXECUTABLE="${{ env.appname }}.app/Contents/MacOS/${{ env.appname }}" + if [ -f "$MAIN_EXECUTABLE" ]; then + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime "$MAIN_EXECUTABLE" + else + echo "Main executable not found at expected path: $MAIN_EXECUTABLE" + # Try to find the actual executable + echo "Available executables in MacOS directory:" + ls -la "${{ env.appname }}.app/Contents/MacOS/" + ACTUAL_EXECUTABLE=$(ls "${{ env.appname }}.app/Contents/MacOS/" | head -n 1) + if [ -n "$ACTUAL_EXECUTABLE" ]; then + echo "Found executable: $ACTUAL_EXECUTABLE" + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime "${{ env.appname }}.app/Contents/MacOS/$ACTUAL_EXECUTABLE" + fi + fi + + echo "Signing frameworks..." + find "${{ env.appname }}.app/Contents/Frameworks" -type f -not -name ".*" -exec \ + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime {} \; + + echo "Signing main bundle..." + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime "${{ env.appname }}.app" + else + echo "Error: Invalid app bundle structure" + exit 1 + fi + + # Clean up + rm certificate.p12 + + - name: Ad-hoc Sign macOS app bundle (Fallback) + if: env.MACOS_P12_BASE64 == '' || env.MACOS_P12_BASE64 == null + shell: bash + run: | + cd flutter/build/macos/Build/Products/Release + + # Renombrar RustDesk.app al nombre de la marca blanca si aplica + if [ -d "RustDesk.app" ]; then + mv "RustDesk.app" "${{ env.appname }}.app" + echo "RustDesk.app renombrado a ${{ env.appname }}.app" + fi + + TARGET_APP="${{ env.appname }}.app" + + if [ -d "$TARGET_APP" ]; then + echo "Limpiando atributos extendidos..." + xattr -cr "$TARGET_APP" || true + + echo "Re-firmando $TARGET_APP de forma Ad-Hoc y recursiva..." + codesign --force --deep --sign - "$TARGET_APP" + echo "Firma Ad-Hoc completada con exito." + else + echo "Error: No se encontro la app bundle en $TARGET_APP para firmar." + exit 1 + fi + + - name: Create DMG + run: | + cd /Users/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/flutter/build/macos/Build/Products/Release + # Print directory contents for debugging + echo "Directory contents:" + ls -la + # Find the actual .app bundle + if [ -d "RustDesk.app" ]; then + # First rename the app if it's still called RustDesk.app + mv "RustDesk.app" "${{ env.appname }}.app" + fi + if [ ! -d "${{ env.appname }}.app" ]; then + echo "Could not find .app bundle!" + exit 1 + fi + echo "Creating DMG for ${{ env.appname }}.app" + create-dmg \ + --volname "${{ env.appname }}" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --icon "${{ env.appname }}.app" 200 190 \ + --hide-extension "${{ env.appname }}.app" \ + --app-drop-link 600 185 \ + "${{ env.appname }}-${{ matrix.job.arch }}.dmg" \ + "${{ env.appname }}.app" + mv "${{ env.appname }}-${{ matrix.job.arch }}.dmg" $GITHUB_WORKSPACE/ + + - name: Rename rustdesk + if: env.UPLOAD_ARTIFACT == 'true' + run: | + cd $GITHUB_WORKSPACE + echo "Directory contents:" + ls -la + + # Find the DMG file dynamically + DMG_FILE=$(find . -name "${{ env.appname }}-${{ matrix.job.arch }}.dmg") + + if [ -n "$DMG_FILE" ]; then + echo "Found DMG file: $DMG_FILE" + mv "$DMG_FILE" "${{ env.filename }}-${{ matrix.job.arch }}.dmg" + echo "Renamed to ${{ env.filename }}-${{ matrix.job.arch }}.dmg" + else + echo "No DMG file found matching the pattern" + exit 1 + fi + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + shell: bash + run: | + curl -i -X POST \ + -H "Content-Type: multipart/form-data" \ + -H "Authorization: Bearer ${{ env.token }}" \ + -F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \ + -F "uuid=${{ env.uuid }}" \ + "${{ secrets.GENURL }}/save_custom_client" + + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + shell: bash + run: | + curl -i -X POST \ + -H "Content-Type: multipart/form-data" \ + -H "Authorization: Bearer ${{ env.token }}" \ + -F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \ + "${{ env.apiServer }}/api/save_custom_client" + + cleanup: + needs: [build-for-macos] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v4 + with: + name: encrypted-secrets-zip diff --git a/.gitea/workflows/generator-windows-x86.yml b/.gitea/workflows/generator-windows-x86.yml new file mode 100644 index 0000000..e2034ef --- /dev/null +++ b/.gitea/workflows/generator-windows-x86.yml @@ -0,0 +1,554 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Custom Windows x86 Client Generator +run-name: Custom Windows x86 Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81 + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;files,D:\vcpkg-cache,readwrite" + # vcpkg version: 2025.08.27 + # If we change the `VCPKG COMMIT_ID`, please remember: + # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. + # Or we may face build issue like + # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 + # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + +jobs: + setup: + uses: ./.gitea/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + build-for-windows-sciter: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + needs: setup + runs-on: ${{ matrix.job.os }} + # Temporarily disable this action due to additional test is needed. + # if: false + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: i686-pc-windows-msvc, + os: windows-2022, + arch: x86, + vcpkg-triplet: x86-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $env:GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $env:GITHUB_ENV + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install ImageMagick on Windows + run: | + choco install -y imagemagick.app --no-progress + Get-ChildItem -Path "${env:ProgramFiles}" | % { $_.FullName } | Select-String -Pattern "[\/\\]ImageMagick[^\/\\]*$" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + # ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + # ./libs/portable/Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + # ./libs/portable/src/main.rs + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + # ./src/lang/en.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + + - name: fix registry if appname has a space + if: contains(env.appname, ' ') + continue-on-error: true + shell: bash + run: | + #./src/platform/windows.rs + sed -i -e 's|reg add {}|reg add \\\"{}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|{subkey}|\\\"{subkey}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\.{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|
|
|" ./src/ui/index.tis + sed -i -e "s|
|
|" ./src/ui/index.tis + if [ -f ./res/setup.nsi ]; then sed -i -e "s|https://rustdesk.com/|${{env.urlLink }}|" ./res/setup.nsi; else echo "res/setup.nsi not present in this rustdesk version, skipped"; fi + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + sed -i -e 's|{translate("Ready")}, {translate("setup_server_tip")}|translate("Ready")|' ./src/ui/index.tis + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + continue_on_error: true + command: | + Copy-Item .rdgen-src/.github/patches/allowCustom.diff allowCustom.diff + git apply allowCustom.diff + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install Rust toolchain + run: | + rustup toolchain install nightly-2023-10-13-x86_64-pc-windows-msvc --component rustfmt --profile minimal --no-self-update + rustup target add ${{ matrix.job.target }} --toolchain nightly-2023-10-13-x86_64-pc-windows-msvc + rustup default nightly-2023-10-13-x86_64-pc-windows-msvc + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }}-sciter + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: C:\vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + for attempt in 1 2 3; do + echo "vcpkg install attempt $attempt/3" + if $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + break + fi + echo "vcpkg install failed (attempt $attempt/3)" + if [ "$attempt" -lt 3 ]; then + sleep 15 + else + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + done + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + + - name: magick stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + Invoke-WebRequest -Uri ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} -OutFile ./res/icon.png + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + magick ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + magick ./res/128x128.png -resize 200% ./res/128x128@2x.png + + + - name: ui.rs icon + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 -w0 < ./res/icon.png) + sed -i -e "s|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|$(echo $b64)|" ./src/ui.rs + b64="" + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + shell: bash + run: | + sed -i -e 's|{software_update_url ? : ""}||' ./src/ui/index.tis + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Build rustdesk + id: build + shell: bash + run: | + python3 res/inline-sciter.py + # Patch sciter x86 + sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml + # libsodium-sys 0.2.7 build.rs selects its prebuilt lib via HOST cfg + # (target_pointer_width): cross host-x64 -> target-x86 wrongly links + # msvc/x64 (LNK2019 x132). SODIUM_LIB_DIR is global and breaks the + # host build-script link, so scope the Win32 lib to the i686 target + # via per-target rustflags (-L from rustflags precedes build-script + # paths in the link line; host keeps the default x64 prebuilt). + # NOTE: rustdesk ships .cargo/config.toml with its own + # [target.i686-pc-windows-msvc] rustflags -> MERGE into the array. + cargo fetch + SODIUM_WIN32_DIR="$(ls -d "$HOME"/.cargo/registry/src/*/libsodium-sys-0.2.7/msvc/Win32/Release/v142 | head -n 1)" + echo "SODIUM_WIN32_DIR=$SODIUM_WIN32_DIR" + ls "$SODIUM_WIN32_DIR/libsodium.lib" + mkdir -p .cargo + SODIUM_WIN32_NATIVE="$(cygpath -m "$SODIUM_WIN32_DIR")" + SODIUM_WIN32_NATIVE="$SODIUM_WIN32_NATIVE" python3 - <<'PYEOF' + import os, re + native = os.environ['SODIUM_WIN32_NATIVE'] + entry = f'"-L", "native={native}"' + p = '.cargo/config.toml' + s = open(p, encoding='utf-8').read() if os.path.exists(p) else '' + if f'native={native}' in s: + print('sodium Win32 -L already configured') + else: + m = re.search(r'\[target\.i686-pc-windows-msvc\](.*?)(?=^\[|\Z)', s, re.S | re.M) + if m: + sec = m.group(1) + rm = re.search(r'rustflags\s*=\s*\[(.*?)\]', sec, re.S) + if rm: + inner = rm.group(1).rstrip() + sep = '' if inner.strip() == '' else ', ' + sec = sec[:rm.start(1)] + inner + sep + entry + sec[rm.end(1):] + else: + sec = sec + f'rustflags = [{entry}]\n' + s = s[:m.start(1)] + sec + s[m.end(1):] + else: + s += f'\n[target.i686-pc-windows-msvc]\nrustflags = [{entry}]\n' + open(p, 'w', encoding='utf-8').write(s) + print('sodium Win32 -L injected') + PYEOF + cat .cargo/config.toml + cargo build --target ${{ matrix.job.target }} --features inline,vram,hwcodec --release --bins + mkdir -p ./Release + mv ./target/${{ matrix.job.target }}/release/rustdesk.exe ./Release/rustdesk.exe + curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll + echo "output_folder=./Release" >> $GITHUB_OUTPUT + curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip + unzip usbmmidd_v2.zip + # Do not remove x64 files, because the user may run the 32bit version on a 64bit system. + # Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64, + # we need the x64 exe to install and uninstall the driver. + rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat + mv ./usbmmidd_v2 ./Release || true + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: zip dlls + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./Release/*.dll, ./Release/*.exe -DestinationPath ./Release/unsigned_files.zip -CompressionLevel Fastest + + - name: sign dlls + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./Release/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./Release/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./Release/unsigned_files.zip ./Release/signed_files.zip + fi + + - name: unzip dlls + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./Release/signed_files.zip -DestinationPath ./Release/ -Force + Remove-Item ./Release/unsigned_files.zip + Remove-Item ./Release/signed_files.zip + + - name: Create custom.txt file + shell: bash + run: | + echo -n "${{ env.custom }}" | cat > ./Release/custom.txt + + - name: Build self-extracted executable + shell: bash + run: | + mv "./Release/rustdesk.exe" "./Release/${{ env.appname }}.exe" || echo "rustdesk.exe" + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../Release/ -o . -e "../../Release/${{ env.appname }}.exe" + popd + # rustdesk-portable-packer is a separate workspace member (root + # `cargo build --bins` does not build it) and it embeds + # libs/portable/app_metadata.toml via include_bytes!, so compile it + # explicitly AFTER generate.py creates that file. + cargo build --target ${{ matrix.job.target }} --package rustdesk-portable-packer --release + mkdir -p ./SignOutput + mv ./target/${{ matrix.job.target }}/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk.exe" + + - name: zip exe + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./SignOutput/*.exe -DestinationPath ./SignOutput/unsigned_files.zip -CompressionLevel Fastest + + - name: sign exe + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./SignOutput/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./SignOutput/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./SignOutput/unsigned_files.zip ./SignOutput/signed_files.zip + fi + + - name: unzip exe + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./SignOutput/signed_files.zip -DestinationPath ./SignOutput/ -Force + Remove-Item ./SignOutput/unsigned_files.zip + Remove-Item ./SignOutput/signed_files.zip + + - name: rename rustdesk.exe to filename.exe + run: | + mv ./SignOutput/rustdesk.exe "./SignOutput/${{ env.filename }}.exe" || echo "rustdesk" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + shell: bash + run: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + shell: bash + run: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client + + cleanup: + needs: [build-for-windows-sciter] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v4 + with: + name: encrypted-secrets-zip diff --git a/.gitea/workflows/generator-windows.yml b/.gitea/workflows/generator-windows.yml new file mode 100644 index 0000000..ccd5955 --- /dev/null +++ b/.gitea/workflows/generator-windows.yml @@ -0,0 +1,750 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Custom Windows Client Generator +run-name: Custom Windows Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;files,D:\vcpkg-cache,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + + +jobs: + setup: + uses: ./.gitea/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge: + uses: ./.gitea/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-RustDeskTempTopMostWindow: + needs: setup + uses: ./.gitea/workflows/third-party-RustDeskTempTopMostWindow.yml + with: + upload-artifact: true + target: windows-2022 + configuration: Release + platform: x64 + target_version: Windows10 + secrets: inherit + strategy: + fail-fast: false + + build-for-windows-flutter: + name: Build Windows + needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $env:GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $env:GITHUB_ENV + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Restore bridge files + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - name: Install ImageMagick on Windows + run: | + choco install -y imagemagick.app --no-progress + Get-ChildItem -Path "${env:ProgramFiles}" | % { $_.FullName } | Select-String -Pattern "[\/\\]ImageMagick[^\/\\]*$" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + + - name: fix flutter_gpu_texture_renderer (fixed in master rustdesk) + shell: bash + run: | + sed -i -e 's|2ded7f146437a761ffe6981e2f742038f85ca68d|08a471bb8ceccdd50483c81cdfa8b81b07b14b87|' ./flutter/pubspec.lock + sed -i -e 's|2ded7f146437a761ffe6981e2f742038f85ca68d|08a471bb8ceccdd50483c81cdfa8b81b07b14b87|' ./flutter/pubspec.yaml + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + # ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + # ./libs/portable/Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + # ./libs/portable/src/main.rs + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + # ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk Remote Desktop"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|VALUE "InternalName", "rustdesk" "\0"|VALUE "InternalName", "${{ env.appname }}" "\0"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"rustdesk.exe"|"${{ env.filename }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + # ./src/lang/en.rs + # change powered by rustdek to powered by compname + if [ ! -z "${{ env.compname }}" ]; then + find ./src/lang -name "*.rs" -exec sed -i '/powered_by_me/s|RustDesk|${{ env.compname }}|g' {} \; + fi + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e 's|RustDesk|${{ env.appname }}|' ./res/msi/Package/License.rtf + + - name: fix registry if appname has a space + if: contains(env.appname, ' ') + continue-on-error: true + shell: bash + run: | + #./src/platform/windows.rs + sed -i -e 's|reg add {}|reg add \\\"{}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|{subkey}|\\\"{subkey}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\.{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|r"Purslane(?: Tech Pte\\.)? Ltd"|"${{ env.compname }}"|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|PURSLANE|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + sed -i -e "s|rustdesk.com|${{ env.urlLink }}|" ./res/msi/Package/License.rtf + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + sed -i -e 's|$downloadUrl/$downloadFile|${{ env.downloadLink }}|' ./flutter/lib/desktop/widgets/update_progress.dart + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + # ./flutter/pubspec.yaml + #sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + Copy-Item .rdgen-src/.github/patches/allowCustom.py allowCustom.py + python allowCustom.py + # Remove Setup Server Tip + Copy-Item .rdgen-src/.github/patches/removeSetupServerTip.diff removeSetupServerTip.diff + git apply removeSetupServerTip.diff + + - name: Patch remote printer port to spool file + shell: python + run: | + import os, re + + src = "." + app = os.environ.get("appname") or "RustDesk" + literal = "C:\\\\ProgramData\\\\%s\\\\printer-spool\\\\job.prn" % app + + # atch Rust remote_printer lib.rs + rs = os.path.join(src, "libs", "remote_printer", "src", "lib.rs") + if os.path.isfile(rs): + with open(rs, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("Rust printer port already points to spool file") + else: + old = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' format!("{} Printer", app_name)') + new = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' let base = std::env::var("ProgramData")\n' + ' .unwrap_or_else(|_| "C:\\\\ProgramData".to_string());\n' + ' let dir = format!("{}\\\\{}\\\\printer-spool", base, app_name);\n' + ' let _ = std::fs::create_dir_all(&dir);\n' + ' format!("{}\\\\job.prn", dir)') + if old in t: + with open(rs, "w", encoding="utf-8") as f: + f.write(t.replace(old, new, 1)) + print("Successfully patched Rust printer port") + else: + print("Warning: get_port_name() signature not matched in Rust source") + + # patch C++ MSI Custom Action + cpp = os.path.join(src, "res", "msi", "CustomActions", "RemotePrinter.cpp") + if os.path.isfile(cpp): + with open(cpp, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("MSI printer port already points to spool file") + else: + pattern = r'(RD_PRINTER_PORT\s*=\s*)L"[^"]*"' + new_t, n = re.subn(pattern, lambda m: m.group(1) + 'L"%s"' % literal, t, count=1) + if n == 1: + with open(cpp, "w", encoding="utf-8") as f: + f.write(new_t) + print("Successfully patched MSI CustomAction printer port") + else: + print("Warning: RD_PRINTER_PORT not matched in RemotePrinter.cpp") + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + # https://github.com/flutter/flutter/issues/155685 + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip + Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release + mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Patch flutter + shell: bash + run: | + cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter))) + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.SCITER_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: C:\vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: magick stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + Invoke-WebRequest -Uri ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} -OutFile ./res/iconx.png + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + mv ./res/iconx.png ./res/icon.png + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + magick ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + magick ./res/128x128.png -resize 200% ./res/128x128@2x.png + + + - name: ui.rs icon + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 -w0 < ./res/icon.png) + perl -0777 -pe "s|iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHL[A-Za-z0-9+/=]+|$b64|g" -i.bak ./src/ui.rs + b64="" + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + continue_on_error: true + command: | + Copy-Item .rdgen-src/.github/patches/xoffline.diff xoffline.diff + git apply xoffline.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + shell: bash + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + # - name: run as admin + # continue-on-error: true + # if: ${{ env.runasadmin == 'true' }} + # shell: bash + # run: | + # sed -i '/<\/compatibility>/a \ + # \ + # \ + # ' ./flutter/windows/runner/runner.exe.manifest + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + cd ./flutter + #flutter pub upgrade win32 + flutter pub get + flutter pub run flutter_launcher_icons + cd .. + + - name: Checkout printer-adapter crate + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref_name }} + path: ./temp-repo + + - name: Move printer-adapter into place + shell: pwsh + run: | + if (Test-Path "./temp-repo/printer-adapter") { + Move-Item -Path "./temp-repo/printer-adapter" -Destination "./printer-adapter" -Force + Remove-Item -Recurse -Force "./temp-repo" + Write-Host "Successfully placed printer-adapter into workspace." + } else { + Write-Host "Error: printer-adapter folder not found in repository." + } + + - name: Build rustdesk + run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk + Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip + Expand-Archive usbmmidd_v2.zip -DestinationPath . -Force + Remove-Item -Path usbmmidd_v2\Win32 -Recurse + Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" + mv -Force .\usbmmidd_v2 ./rustdesk + + # Download and install driver package + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + mkdir ./rustdesk/drivers -ErrorAction SilentlyContinue + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + } catch { + Write-Host "Warning: Printer driver download failed." + } + + # Build and install open printer adapter + if (Test-Path "./printer-adapter/Cargo.toml") { + Write-Host "Building open printer-adapter..." + pushd ./printer-adapter + cargo build --release --locked + popd + if (Test-Path "./printer-adapter/target/release/printer_driver_adapter.dll") { + cp -Force "./printer-adapter/target/release/printer_driver_adapter.dll" "./rustdesk/printer_driver_adapter.dll" + Write-Host "Replaced printer_driver_adapter.dll with open implementation." + } else { + Write-Host "Error: printer-adapter build succeeded but DLL was not found." + } + } else { + Write-Host "Warning: ./printer-adapter/Cargo.toml not found, remote printing will remain disabled." + } + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + mv ./rustdesk/data/flutter_assets/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg.bak + magick ./res/icon.png ./rustdesk/data/flutter_assets/assets/icon.svg + + - name: logo stuff + if: ${{ env.logolink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} -OutFile ./rustdesk/data/flutter_assets/assets/logo.png + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: Download RustDeskTempTopMostWindow artifacts + uses: actions/download-artifact@v4 + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: topmostwindow-artifacts + path: "./rustdesk" + + - name: zip dlls + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./rustdesk/*.dll, ./rustdesk/*.exe -DestinationPath ./rustdesk/unsigned_files.zip -CompressionLevel Fastest + + - name: sign dlls + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./rustdesk/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./rustdesk/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./rustdesk/unsigned_files.zip ./rustdesk/signed_files.zip + fi + + - name: unzip dlls + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./rustdesk/signed_files.zip -DestinationPath ./rustdesk/ -Force + Remove-Item ./rustdesk/unsigned_files.zip + Remove-Item ./rustdesk/signed_files.zip + + - name: Create custom.txt file + shell: bash + run: | + echo -n "${{ env.custom }}" | cat > ./rustdesk/custom_.txt + + - name: Build self-extracted executable + shell: bash + if: env.UPLOAD_ARTIFACT == 'true' + run: | + mv "./rustdesk/rustdesk.exe" "./rustdesk/${{ env.appname }}.exe" || echo "rustdesk.exe" + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../rustdesk/ -o . -e "../../rustdesk/${{ env.appname }}.exe" + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk.exe" + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build msi + continue-on-error: true + if: env.UPLOAD_ARTIFACT == 'true' + run: | + $myappname = "${{ env.appname }}" -replace '\s','_' + cp "rustdesk/${{ env.appname }}.exe" "rustdesk/${myappname}.exe" -ErrorAction SilentlyContinue + pushd ./res/msi + python preprocess.py --app-name "$myappname" --arp -d ../../rustdesk + nuget restore msi.sln + msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10 + cp ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-latest.msi + mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk.msi + sha256sum ../../SignOutput/rustdesk.msi + + - name: zip exe and msi + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./SignOutput/*.exe, ./SignOutput/*.msi -DestinationPath ./SignOutput/unsigned_files.zip -CompressionLevel Fastest + + - name: sign exe and msi + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./SignOutput/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./SignOutput/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./SignOutput/unsigned_files.zip ./SignOutput/signed_files.zip + fi + + - name: unzip exe and msi + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./SignOutput/signed_files.zip -DestinationPath ./SignOutput/ -Force + Remove-Item ./SignOutput/unsigned_files.zip + Remove-Item ./SignOutput/signed_files.zip + + - name: rename rustdesk.exe to filename.exe + run: | + mv ./SignOutput/rustdesk.exe "./SignOutput/${{ env.filename }}.exe" || echo "rustdesk" + + - name: rename rustdesk.msi to filename.msi + continue-on-error: true + run: | + mv ./SignOutput/rustdesk.msi "./SignOutput/${{ env.filename }}.msi" || echo "rustdesk" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" ${{ env.apiServer }}/api/save_custom_client || true + + cleanup: + needs: [build-for-windows-flutter] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v4 + with: + name: encrypted-secrets-zip diff --git a/.gitea/workflows/sh-generator-windows.yml b/.gitea/workflows/sh-generator-windows.yml new file mode 100644 index 0000000..fddcbf0 --- /dev/null +++ b/.gitea/workflows/sh-generator-windows.yml @@ -0,0 +1,668 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: Custom Windows Client Generator +run-name: Custom Windows Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;files,D:\vcpkg-cache,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + + +jobs: + setup: + uses: ./.gitea/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge: + uses: ./.gitea/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-RustDeskTempTopMostWindow: + needs: setup + uses: ./.gitea/workflows/third-party-RustDeskTempTopMostWindow.yml + with: + upload-artifact: true + target: windows-2022 + configuration: Release + platform: x64 + target_version: Windows10 + secrets: inherit + strategy: + fail-fast: false + + build-for-windows-flutter: + name: Build Windows + needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup] + runs-on: self-hosted + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + path: .rdgen-src + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $env:GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $env:GITHUB_ENV + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Restore bridge files + uses: actions/download-artifact@v4 + with: + name: bridge-artifact + path: ./ + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + with: + nuget-version: 'latest' + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + # ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + # ./libs/portable/Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + # ./libs/portable/src/main.rs + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + # ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk Remote Desktop"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|VALUE "InternalName", "rustdesk" "\0"|VALUE "InternalName", "${{ env.appname }}" "\0"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"rustdesk.exe"|"${{ env.filename }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + # ./src/lang/en.rs + # change powered by rustdek to powered by compname + if [ ! -z "${{ env.compname }}" ]; then + find ./src/lang -name "*.rs" -exec sed -i '/powered_by_me/s|RustDesk|${{ env.compname }}|g' {} \; + fi + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e 's|RustDesk|${{ env.appname }}|' ./res/msi/Package/License.rtf + + - name: fix registry if appname has a space + if: contains(env.appname, ' ') + continue-on-error: true + shell: bash + run: | + #./src/platform/windows.rs + sed -i -e 's|reg add {}|reg add \\\"{}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|{subkey}|\\\"{subkey}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\.{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|r"Purslane(?: Tech Pte\\.)? Ltd"|"${{ env.compname }}"|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|PURSLANE|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + sed -i -e "s|rustdesk.com|${{ env.urlLink }}|" ./res/msi/Package/License.rtf + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + sed -i -e 's|$downloadUrl/$downloadFile|${{ env.downloadLink }}|' ./flutter/lib/desktop/widgets/update_progress.dart + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + # ./flutter/pubspec.yaml + #sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + command: | + Copy-Item .rdgen-src/.github/patches/allowCustom.py allowCustom.py + python allowCustom.py + # Remove Setup Server Tip + Copy-Item .rdgen-src/.github/patches/removeSetupServerTip.diff removeSetupServerTip.diff + git apply removeSetupServerTip.diff + + - name: Patch remote printer port to spool file + shell: python + run: | + import os, re + + src = "." + app = os.environ.get("appname") or "RustDesk" + literal = "C:\\\\ProgramData\\\\%s\\\\printer-spool\\\\job.prn" % app + + # atch Rust remote_printer lib.rs + rs = os.path.join(src, "libs", "remote_printer", "src", "lib.rs") + if os.path.isfile(rs): + with open(rs, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("Rust printer port already points to spool file") + else: + old = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' format!("{} Printer", app_name)') + new = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' let base = std::env::var("ProgramData")\n' + ' .unwrap_or_else(|_| "C:\\\\ProgramData".to_string());\n' + ' let dir = format!("{}\\\\{}\\\\printer-spool", base, app_name);\n' + ' let _ = std::fs::create_dir_all(&dir);\n' + ' format!("{}\\\\job.prn", dir)') + if old in t: + with open(rs, "w", encoding="utf-8") as f: + f.write(t.replace(old, new, 1)) + print("Successfully patched Rust printer port") + else: + print("Warning: get_port_name() signature not matched in Rust source") + + # patch C++ MSI Custom Action + cpp = os.path.join(src, "res", "msi", "CustomActions", "RemotePrinter.cpp") + if os.path.isfile(cpp): + with open(cpp, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("MSI printer port already points to spool file") + else: + pattern = r'(RD_PRINTER_PORT\s*=\s*)L"[^"]*"' + new_t, n = re.subn(pattern, lambda m: m.group(1) + 'L"%s"' % literal, t, count=1) + if n == 1: + with open(cpp, "w", encoding="utf-8") as f: + f.write(new_t) + print("Successfully patched MSI CustomAction printer port") + else: + print("Warning: RD_PRINTER_PORT not matched in RemotePrinter.cpp") + + - name: magick stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + command: | + Invoke-WebRequest -Uri ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} -OutFile ./res/iconx.png + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + mv ./res/iconx.png ./res/icon.png + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + magick ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + magick ./res/128x128.png -resize 200% ./res/128x128@2x.png + + + - name: ui.rs icon + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 -w0 < ./res/icon.png) + perl -0777 -pe "s|iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHL[A-Za-z0-9+/=]+|$b64|g" -i.bak ./src/ui.rs + b64="" + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + continue_on_error: true + command: | + Copy-Item .rdgen-src/.github/patches/xoffline.diff xoffline.diff + git apply xoffline.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + shell: bash + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + cd ./flutter + #flutter pub upgrade win32 + flutter pub get + flutter pub run flutter_launcher_icons + cd .. + + - name: Checkout printer-adapter crate + uses: actions/checkout@v4 + with: + repository: ${{ github.repository }} + ref: ${{ github.ref_name }} + path: ./temp-repo + + - name: Move printer-adapter into place + shell: pwsh + run: | + if (Test-Path "./temp-repo/printer-adapter") { + Move-Item -Path "./temp-repo/printer-adapter" -Destination "./printer-adapter" -Force + Remove-Item -Recurse -Force "./temp-repo" + Write-Host "Successfully placed printer-adapter into workspace." + } else { + Write-Host "Error: printer-adapter folder not found in repository." + } + + - name: Build rustdesk + run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk + Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip + Expand-Archive usbmmidd_v2.zip -DestinationPath . -Force + Remove-Item -Path usbmmidd_v2\Win32 -Recurse + Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" + mv -Force .\usbmmidd_v2 ./rustdesk + + # Download and install driver package + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + mkdir ./rustdesk/drivers -ErrorAction SilentlyContinue + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + } catch { + Write-Host "Warning: Printer driver download failed." + } + + # Build and install open printer adapter + if (Test-Path "./printer-adapter/Cargo.toml") { + Write-Host "Building open printer-adapter..." + pushd ./printer-adapter + cargo build --release --locked + popd + if (Test-Path "./printer-adapter/target/release/printer_driver_adapter.dll") { + cp -Force "./printer-adapter/target/release/printer_driver_adapter.dll" "./rustdesk/printer_driver_adapter.dll" + Write-Host "Replaced printer_driver_adapter.dll with open implementation." + } else { + Write-Host "Error: printer-adapter build succeeded but DLL was not found." + } + } else { + Write-Host "Warning: ./printer-adapter/Cargo.toml not found, remote printing will remain disabled." + } + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + mv ./rustdesk/data/flutter_assets/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg.bak + magick ./res/icon.png ./rustdesk/data/flutter_assets/assets/icon.svg + + - name: logo stuff + if: ${{ env.logolink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} -OutFile ./rustdesk/data/flutter_assets/assets/logo.png + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: Download RustDeskTempTopMostWindow artifacts + uses: actions/download-artifact@v4 + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: topmostwindow-artifacts + path: "./rustdesk" + + - name: zip dlls + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./rustdesk/*.dll, ./rustdesk/*.exe -DestinationPath ./rustdesk/unsigned_files.zip -CompressionLevel Fastest + + - name: sign dlls + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./rustdesk/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./rustdesk/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./rustdesk/unsigned_files.zip ./rustdesk/signed_files.zip + fi + + - name: unzip dlls + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./rustdesk/signed_files.zip -DestinationPath ./rustdesk/ -Force + Remove-Item ./rustdesk/unsigned_files.zip + Remove-Item ./rustdesk/signed_files.zip + + - name: Create custom.txt file + shell: bash + run: | + echo -n "${{ env.custom }}" | cat > ./rustdesk/custom_.txt + + - name: Build self-extracted executable + shell: bash + if: env.UPLOAD_ARTIFACT == 'true' + run: | + mv "./rustdesk/rustdesk.exe" "./rustdesk/${{ env.appname }}.exe" || echo "rustdesk.exe" + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../rustdesk/ -o . -e "../../rustdesk/${{ env.appname }}.exe" + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk.exe" + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build msi + continue-on-error: true + if: env.UPLOAD_ARTIFACT == 'true' + run: | + $myappname = "${{ env.appname }}" -replace '\s','_' + cp "rustdesk/${{ env.appname }}.exe" "rustdesk/${myappname}.exe" -ErrorAction SilentlyContinue + pushd ./res/msi + python preprocess.py --app-name "$myappname" --arp -d ../../rustdesk + nuget restore msi.sln + msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10 + cp ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-latest.msi + mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk.msi + sha256sum ../../SignOutput/rustdesk.msi + + - name: zip exe and msi + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./SignOutput/*.exe, ./SignOutput/*.msi -DestinationPath ./SignOutput/unsigned_files.zip -CompressionLevel Fastest + + - name: sign exe and msi + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./SignOutput/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./SignOutput/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./SignOutput/unsigned_files.zip ./SignOutput/signed_files.zip + fi + + - name: unzip exe and msi + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./SignOutput/signed_files.zip -DestinationPath ./SignOutput/ -Force + Remove-Item ./SignOutput/unsigned_files.zip + Remove-Item ./SignOutput/signed_files.zip + + - name: rename rustdesk.exe to filename.exe + run: | + mv ./SignOutput/rustdesk.exe "./SignOutput/${{ env.filename }}.exe" || echo "rustdesk" + + - name: rename rustdesk.msi to filename.msi + continue-on-error: true + run: | + mv ./SignOutput/rustdesk.msi "./SignOutput/${{ env.filename }}.msi" || echo "rustdesk" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" ${{ env.apiServer }}/api/save_custom_client || true + + cleanup: + needs: [build-for-windows-flutter] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v4 + with: + name: encrypted-secrets-zip diff --git a/.gitea/workflows/third-party-RustDeskTempTopMostWindow.yml b/.gitea/workflows/third-party-RustDeskTempTopMostWindow.yml new file mode 100644 index 0000000..432f724 --- /dev/null +++ b/.gitea/workflows/third-party-RustDeskTempTopMostWindow.yml @@ -0,0 +1,96 @@ +# Gitea Actions 适配副本:由 .github/workflows 同名文件适配(本地路径/cache/artifact 差异),修改时请同步两边。差异说明见 setup.md「Gitea 部署附录」。 +name: build RustDeskTempTopMostWindow + +on: + workflow_call: + inputs: + upload-artifact: + type: boolean + default: true + target: + description: 'Target' + required: true + type: string + default: 'windows-2022' + configuration: + description: 'Configuration' + required: true + type: string + default: 'Release' + platform: + description: 'Platform' + required: true + type: string + default: 'x64' + target_version: + description: 'TargetVersion' + required: true + type: string + default: 'Windows10' + +env: + project_path: WindowInjection/WindowInjection.vcxproj + +jobs: + build-RustDeskTempTopMostWindow: + runs-on: ${{ inputs.target }} + strategy: + fail-fast: false + env: + build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }} + steps: + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v3 + + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.gitea/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Download the source code + run: | + git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow + + # Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3 + - name: Build the project + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796 + if ($env:privacylink_url-ne "false") { + Invoke-WebRequest -Uri ${{ env.privacylink_url }}/get_png?filename=${{ env.privacylink_file }}"&"uuid=${{ env.privacylink_uuid }} -OutFile privacy.png + Copy-Item ../.github/patches/privacyScreen.py privacyScreen.py + python privacyScreen.py + rm ./WindowInjection/img.cpp + mv img.cpp ./WindowInjection/img.cpp + } + msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }} + + - name: Archive build artifacts + uses: actions/upload-artifact@v4 + if: ${{ inputs.upload-artifact }} + with: + name: topmostwindow-artifacts + path: | + ./${{ env.build_output_dir }}/WindowInjection.dll diff --git a/.github/actions/decrypt-secrets/action.yml b/.github/actions/decrypt-secrets/action.yml new file mode 100644 index 0000000..330f0bb --- /dev/null +++ b/.github/actions/decrypt-secrets/action.yml @@ -0,0 +1,48 @@ +name: 'Decrypt and Mask Secrets' +description: 'Decrypts a zip and masks the JSON contents as env vars' +inputs: + zip_password: + description: 'Password for the Zip' + required: true + zip_path: + description: 'Path to the encrypted zip' + required: false + default: 'secrets.zip' + +runs: + using: "composite" + steps: + - name: install python deps + shell: bash + run: | + pip install pyzipper + - name: Decrypt and Mask + shell: python + env: + PYTHONUTF8: "1" + PYTHONIOENCODING: "utf-8" + run: | + import sys + import io + import pyzipper + import json + import os + + # Force UTF-8 stdout/stderr so secrets with characters outside + # the runner codepage (CJK, emoji, etc.) do not crash this step + # on Windows runners (default cp1251) with UnicodeEncodeError + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") + + with pyzipper.AESZipFile('${{ inputs.zip_path }}') as zf: + zf.setpassword('${{ inputs.zip_password }}'.encode()) + with zf.open('secrets.json') as f: + secrets = json.load(f) + + with open(os.environ['GITHUB_ENV'], 'a', encoding='utf-8') as env_file: + for key, value in secrets.items(): + if value: + print(f"::add-mask::{value}") + env_file.write(f"{key}={value}\n") + + print(f"Successfully masked {len(secrets)} secrets.") \ No newline at end of file diff --git a/.github/patches/allowCustom.diff b/.github/patches/allowCustom.diff new file mode 100644 index 0000000..5cf437b --- /dev/null +++ b/.github/patches/allowCustom.diff @@ -0,0 +1,29 @@ +diff --git a/src/common.rs b/src/common.rs +index 56361a5da..92b221e9a 100644 +--- a/src/common.rs ++++ b/src/common.rs +@@ -1474,15 +1474,15 @@ pub fn read_custom_client(config: &str) { + log::error!("Failed to decode custom client config"); + return; + }; +- const KEY: &str = "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM="; +- let Some(pk) = get_rs_pk(KEY) else { +- log::error!("Failed to parse public key of custom client"); +- return; +- }; +- let Ok(data) = sign::verify(&data, &pk) else { +- log::error!("Failed to dec custom client config"); +- return; +- }; ++ // const KEY: &str = "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM="; ++ // let Some(pk) = get_rs_pk(KEY) else { ++ // log::error!("Failed to parse public key of custom client"); ++ // return; ++ // }; ++ // let Ok(data) = sign::verify(&data, &pk) else { ++ // log::error!("Failed to dec custom client config"); ++ // return; ++ // }; + let Ok(mut data) = + serde_json::from_slice::>(&data) + else { diff --git a/.github/patches/allowCustom.py b/.github/patches/allowCustom.py new file mode 100644 index 0000000..83a8f90 --- /dev/null +++ b/.github/patches/allowCustom.py @@ -0,0 +1,63 @@ +import os +import shutil + +def remove_line_block(filepath, start_phrase, lines_to_remove_after_start): + """ + Removes a starting line and a fixed number of lines immediately following it. + + :param filepath: The path to the file to modify. + :param start_phrase: The unique string to identify the first line of the block. + :param lines_to_remove_after_start: The number of lines to remove after the starting line. + """ + + # 1. Configuration for the removal logic + # The starting line is: const KEY: &str = "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM="; + # The block contains this line plus 8 following lines, so we want to skip 9 lines in total. + total_lines_to_skip = 1 + lines_to_remove_after_start # 1 (start line) + 8 (following lines) = 9 + + lines_to_keep = [] + skip_count = 0 + + # 2. Read and filter the file content + try: + with open(filepath, 'r') as file: + for line in file: + + # If we are currently in the process of skipping lines, decrement the counter and continue + if skip_count > 0: + skip_count -= 1 + continue + + # Check if the line matches the start phrase (we use .strip() to ignore indentation/whitespace) + if line.strip().startswith(start_phrase.strip()): + # Start skipping the block (including the current line) + skip_count = total_lines_to_skip - 1 + # Note: We subtract 1 because the 'continue' will handle the first line removal immediately + continue + + # If we are not skipping, keep the line, but change custom.txt to custom_.txt + line = line.replace("custom.txt", "custom_.txt") + lines_to_keep.append(line) + + except FileNotFoundError: + print(f"Error: File not found at {filepath}") + return + + # 3. Write the remaining lines back to the file + try: + with open(filepath, 'w') as file: + file.writelines(lines_to_keep) + + print(f"Success! Removed the 9-line block starting with '{start_phrase.strip()}' from {filepath}.") + + except IOError as e: + print(f"An error occurred while writing to the file: {e}") + +def main(): + file_path = 'src/common.rs' + start_phrase = 'const KEY: &str = "5Qbwsde3unUcJBtrx9ZkvUmwFNoExHzpryHuPUdqlWM=";' + lines_to_remove_after_start = 8 + remove_line_block(file_path, start_phrase, lines_to_remove_after_start) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff new file mode 100644 index 0000000..ec53462 --- /dev/null +++ b/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff @@ -0,0 +1,42 @@ +diff --git a/packages/flutter/lib/src/material/dropdown_menu.dart b/packages/flutter/lib/src/material/dropdown_menu.dart +index 7e634cd2aa..c1e9acc295 100644 +--- a/packages/flutter/lib/src/material/dropdown_menu.dart ++++ b/packages/flutter/lib/src/material/dropdown_menu.dart +@@ -475,7 +475,7 @@ class _DropdownMenuState extends State> { + final GlobalKey _leadingKey = GlobalKey(); + late List buttonItemKeys; + final MenuController _controller = MenuController(); +- late bool _enableFilter; ++ bool _enableFilter = false; + late List> filteredEntries; + List? _initialMenu; + int? currentHighlight; +@@ -524,6 +524,11 @@ class _DropdownMenuState extends State> { + } + _localTextEditingController = widget.controller ?? TextEditingController(); + } ++ if (oldWidget.enableFilter != widget.enableFilter) { ++ if (!widget.enableFilter) { ++ _enableFilter = false; ++ } ++ } + if (oldWidget.enableSearch != widget.enableSearch) { + if (!widget.enableSearch) { + currentHighlight = null; +@@ -663,6 +668,7 @@ class _DropdownMenuState extends State> { + ); + currentHighlight = widget.enableSearch ? i : null; + widget.onSelected?.call(entry.value); ++ _enableFilter = false; + } + : null, + requestFocusOnHover: false, +@@ -735,6 +741,8 @@ class _DropdownMenuState extends State> { + if (_enableFilter) { + filteredEntries = widget.filterCallback?.call(filteredEntries, _localTextEditingController!.text) + ?? filter(widget.dropdownMenuEntries, _localTextEditingController!); ++ } else { ++ filteredEntries = widget.dropdownMenuEntries; + } + + if (widget.enableSearch) { \ No newline at end of file diff --git a/.github/patches/hidecm.diff b/.github/patches/hidecm.diff new file mode 100644 index 0000000..29e16a8 --- /dev/null +++ b/.github/patches/hidecm.diff @@ -0,0 +1,129 @@ +diff --git a/flutter/lib/desktop/pages/desktop_setting_page.dart b/flutter/lib/desktop/pages/desktop_setting_page.dart +index 56a99446c..5991aceed 100644 +--- a/flutter/lib/desktop/pages/desktop_setting_page.dart ++++ b/flutter/lib/desktop/pages/desktop_setting_page.dart +@@ -1101,8 +1101,8 @@ class _SafetyState extends State<_Safety> with AutomaticKeepAliveClientMixin { + if (usePassword) + _SubButton('Set permanent password', setPasswordDialog, + permEnabled && !locked), +- // if (usePassword) +- // hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), ++ if (usePassword) ++ hide_cm(!locked).marginOnly(left: _kContentHSubMargin - 6), + if (usePassword) radios[2], + ]); + }))); +diff --git a/flutter/lib/main.dart b/flutter/lib/main.dart +index 3032a2321..66d0d8c26 100644 +--- a/flutter/lib/main.dart ++++ b/flutter/lib/main.dart +@@ -245,7 +245,7 @@ void runConnectionManagerScreen() async { + MyTheme.currentThemeMode(), + ); + final hide = await bind.cmGetConfig(name: "hide_cm") == 'true'; +- gFFI.serverModel.hideCm = hide; ++ // gFFI.serverModel.hideCm = hide; + if (hide) { + await hideCmWindow(isStartup: true); + } else { +diff --git a/flutter/lib/models/server_model.dart b/flutter/lib/models/server_model.dart +index 877576461..4efd6c8b0 100644 +--- a/flutter/lib/models/server_model.dart ++++ b/flutter/lib/models/server_model.dart +@@ -32,7 +32,7 @@ class ServerModel with ChangeNotifier { + bool _fileOk = false; + bool _clipboardOk = false; + bool _showElevation = false; +- bool hideCm = false; ++ bool _hideCm = false; + int _connectStatus = 0; // Rendezvous Server status + String _verificationMethod = ""; + String _temporaryPasswordLength = ""; +@@ -62,6 +62,8 @@ class ServerModel with ChangeNotifier { + + bool get clipboardOk => _clipboardOk; + ++ bool get hideCm => _hideCm; ++ + bool get showElevation => _showElevation; + + int get connectStatus => _connectStatus; +@@ -82,12 +84,11 @@ class ServerModel with ChangeNotifier { + + setVerificationMethod(String method) async { + await bind.mainSetOption(key: kOptionVerificationMethod, value: method); +- /* ++ + if (method != kUsePermanentPassword) { + await bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', false)); + } +- */ + } + + String get temporaryPasswordLength { +@@ -104,12 +105,11 @@ class ServerModel with ChangeNotifier { + + setApproveMode(String mode) async { + await bind.mainSetOption(key: kOptionApproveMode, value: mode); +- /* ++ + if (mode != 'password') { + await bind.mainSetOption( + key: 'allow-hide-cm', value: bool2option('allow-hide-cm', false)); + } +- */ + } + + TextEditingController get serverId => _serverId; +@@ -126,7 +126,6 @@ class ServerModel with ChangeNotifier { + _emptyIdShow = translate("Generating ..."); + _serverId = IDTextEditingController(text: _emptyIdShow); + +- /* + // initital _hideCm at startup + final verificationMethod = + bind.mainGetOptionSync(key: kOptionVerificationMethod); +@@ -137,7 +136,6 @@ class ServerModel with ChangeNotifier { + verificationMethod == kUsePermanentPassword)) { + _hideCm = false; + } +- */ + + timerCallback() async { + final connectionStatus = +@@ -227,14 +225,14 @@ class ServerModel with ChangeNotifier { + final temporaryPasswordLength = + await bind.mainGetOption(key: "temporary-password-length"); + final approveMode = await bind.mainGetOption(key: kOptionApproveMode); +- /* ++ + var hideCm = option2bool( + 'allow-hide-cm', await bind.mainGetOption(key: 'allow-hide-cm')); + if (!(approveMode == 'password' && + verificationMethod == kUsePermanentPassword)) { + hideCm = false; + } +- */ ++ + if (_approveMode != approveMode) { + _approveMode = approveMode; + update = true; +@@ -265,7 +263,7 @@ class ServerModel with ChangeNotifier { + _temporaryPasswordLength = temporaryPasswordLength; + update = true; + } +- /* ++ + if (_hideCm != hideCm) { + _hideCm = hideCm; + if (desktopType == DesktopType.cm) { +@@ -277,7 +275,7 @@ class ServerModel with ChangeNotifier { + } + update = true; + } +- */ ++ + if (update) { + notifyListeners(); + } diff --git a/.github/patches/privacyScreen.py b/.github/patches/privacyScreen.py new file mode 100644 index 0000000..5197c4e --- /dev/null +++ b/.github/patches/privacyScreen.py @@ -0,0 +1,32 @@ +import os + +def convert_png_to_cpp(input_file, output_file, array_name="g_img"): + if not os.path.exists(input_file): + print(f"Error: {input_file} not found.") + return + + with open(input_file, "rb") as f: + data = f.read() + + with open(output_file, "w") as f: + f.write('#include "pch.h"\n') + f.write('#include "./img.h"\n\n') + f.write(f"const unsigned char {array_name}[] = {{\n") + + for i in range(0, len(data), 20): + chunk = data[i : i + 20] + hex_chunk = [f"0x{b:02x}" for b in chunk] + + line = ", ".join(hex_chunk) + + if i + 20 < len(data): + f.write(f"{line},\n") + else: + f.write(f"{line}\n") + + f.write("};\n\n") + f.write(f"const long long {array_name}Len = sizeof({array_name});\n") + + #print(f"Successfully converted {input_file} to {output_file}") + +convert_png_to_cpp("privacy.png", "img.cpp") \ No newline at end of file diff --git a/.github/patches/removeNewVersionNotif.diff b/.github/patches/removeNewVersionNotif.diff new file mode 100644 index 0000000..a1899b8 --- /dev/null +++ b/.github/patches/removeNewVersionNotif.diff @@ -0,0 +1,41 @@ +diff --git a/flutter/lib/desktop/pages/desktop_home_page.dart b/flutter/lib/desktop/pages/desktop_home_page.dart +index ba724eed5..1604c429f 100644 +--- a/flutter/lib/desktop/pages/desktop_home_page.dart ++++ b/flutter/lib/desktop/pages/desktop_home_page.dart +@@ -424,21 +424,21 @@ class _DesktopHomePageState extends State + } + + Widget buildHelpCards(String updateUrl) { +- if (!bind.isCustomClient() && +- updateUrl.isNotEmpty && +- !isCardClosed && +- bind.mainUriPrefixSync().contains('rustdesk')) { +- return buildInstallCard( +- "Status", +- "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", +- "Click to download", () async { +- final Uri url = Uri.parse('https://rustdesk.com/download'); +- await launchUrl(url); +- }, closeButton: true); +- } +- if (systemError.isNotEmpty) { +- return buildInstallCard("", systemError, "", () {}); +- } ++ // if (!bind.isCustomClient() && ++ // updateUrl.isNotEmpty && ++ // !isCardClosed && ++ // bind.mainUriPrefixSync().contains('rustdesk')) { ++ // return buildInstallCard( ++ // "Status", ++ // "${translate("new-version-of-{${bind.mainGetAppNameSync()}}-tip")} (${bind.mainGetNewVersion()}).", ++ // "Click to download", () async { ++ // final Uri url = Uri.parse('https://rustdesk.com/download'); ++ // await launchUrl(url); ++ // }, closeButton: true); ++ // } ++ // if (systemError.isNotEmpty) { ++ // return buildInstallCard("", systemError, "", () {}); ++ // } + + if (isWindows && !bind.isDisableInstallation()) { + if (!bind.mainIsInstalled()) { diff --git a/.github/patches/removeSetupServerTip.diff b/.github/patches/removeSetupServerTip.diff new file mode 100644 index 0000000..044b0d0 --- /dev/null +++ b/.github/patches/removeSetupServerTip.diff @@ -0,0 +1,13 @@ +diff --git a/flutter/lib/desktop/pages/connection_page.dart b/flutter/lib/desktop/pages/connection_page.dart +index d9dc3eec4..76f386b76 100644 +--- a/flutter/lib/desktop/pages/connection_page.dart ++++ b/flutter/lib/desktop/pages/connection_page.dart +@@ -131,7 +131,7 @@ class _OnlineStatusWidgetState extends State { + if (!isIncomingOnly) startServiceWidget(), + // ready && public + // No need to show the guide if is custom client. +- if (!isIncomingOnly) setupServerWidget(), ++ //if (!isIncomingOnly) setupServerWidget(), + ], + ); + diff --git a/.github/patches/xoffline.diff b/.github/patches/xoffline.diff new file mode 100644 index 0000000..637bfea --- /dev/null +++ b/.github/patches/xoffline.diff @@ -0,0 +1,20 @@ +diff --git a/flutter/lib/common/widgets/peer_card.dart b/flutter/lib/common/widgets/peer_card.dart +index 0a15eb45b..d776dd556 100644 +--- a/flutter/lib/common/widgets/peer_card.dart ++++ b/flutter/lib/common/widgets/peer_card.dart +@@ -1328,8 +1328,13 @@ Widget getOnline(double rightPadding, bool online) { + waitDuration: const Duration(seconds: 1), + child: Padding( + padding: EdgeInsets.fromLTRB(0, 4, rightPadding, 4), +- child: CircleAvatar( +- radius: 3, backgroundColor: online ? Colors.green : kColorWarn))); ++ child: online ++ ? CircleAvatar(radius: 3, backgroundColor: Colors.green) ++ : Icon( ++ Icons.close, // Red X for offline (Material Icon) ++ color: Colors.red, ++ size: 12.0, // Adjust size as needed ++ ))); + } + + Widget build_more(BuildContext context, {bool invert = false}) { diff --git a/.github/workflows/bridge.yml b/.github/workflows/bridge.yml new file mode 100644 index 0000000..e01648c --- /dev/null +++ b/.github/workflows/bridge.yml @@ -0,0 +1,133 @@ +# This yaml shares the build bridge steps with ci and nightly. +name: Build flutter-rust-bridge +# 2023-11-23 18:00:00+00:00 + +on: + workflow_call: + inputs: + version: + description: 'Rustdesk Version' + required: true + default: '1.3.1' + type: string + +env: + CARGO_EXPAND_VERSION: "1.0.95" + FLUTTER_VERSION: "3.22.3" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503 + +jobs: + generate_bridge: + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + os: ubuntu-24.04, + extra-build-args: "", + } + steps: + - name: Checkout source code + if: ${{ inputs.version != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ inputs.version }} + submodules: recursive + + - name: Checkout source code + if: ${{ inputs.version == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install prerequisites + run: | + sudo apt-get install ca-certificates -y + sudo apt-get update -y + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + libclang-dev \ + libgtk-3-dev \ + llvm-dev \ + nasm \ + ninja-build \ + pkg-config \ + wget + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: bridge-${{ matrix.job.os }} + + - name: Cache cargo git + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: bridge-cargo-git-${{ hashFiles('**/Cargo.lock') }} + restore-keys: | + bridge-cargo-git- + + - name: Cache Bridge + id: cache-bridge + uses: actions/cache@v4 + with: + path: /tmp/flutter_rust_bridge + key: vcpkg-${{ matrix.job.arch }} + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + cache: true + + - name: Install flutter rust bridge deps + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + cargo install cargo-expand --version ${{ env.CARGO_EXPAND_VERSION }} --locked + cargo install flutter_rust_bridge_codegen --version ${{ env.FLUTTER_RUST_BRIDGE_VERSION }} --features "uuid" --locked + pushd flutter && sed -i -e 's/extended_text: 14.0.0/extended_text: 13.0.0/g' pubspec.yaml && flutter pub get && popd + + - name: Run flutter rust bridge + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + ~/.cargo/bin/flutter_rust_bridge_codegen --rust-input ./src/flutter_ffi.rs --dart-output ./flutter/lib/generated_bridge.dart --c-output ./flutter/macos/Runner/bridge_generated.h + cp ./flutter/macos/Runner/bridge_generated.h ./flutter/ios/Runner/bridge_generated.h + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: bridge-artifact + path: | + ./src/bridge_generated.rs + ./src/bridge_generated.io.rs + ./flutter/lib/generated_bridge.dart + ./flutter/lib/generated_bridge.freezed.dart + ./flutter/macos/Runner/bridge_generated.h + ./flutter/ios/Runner/bridge_generated.h \ No newline at end of file diff --git a/.github/workflows/build-web.yml b/.github/workflows/build-web.yml new file mode 100644 index 0000000..0d68ffe --- /dev/null +++ b/.github/workflows/build-web.yml @@ -0,0 +1,69 @@ +name: Build web +on: + workflow_dispatch: +env: + FLUTTER_VERSION: "3.24.5" + TAG_NAME: "nightly" + VERSION: "1.3.9" + +jobs: + build-rustdesk-web: + name: build-rustdesk-web + runs-on: ubuntu-24.04 + strategy: + fail-fast: false + env: + RELEASE_NAME: web-basic + steps: + - name: Checkout source code + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Prepare env + run: | + sudo apt-get update -y + sudo apt-get install -y wget npm + + - name: Install flutter + uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + # https://rustdesk.com/docs/en/dev/build/web/ + - name: Build web + shell: bash + run: | + pushd flutter/web/v2 + npm install yarn -g + npm install typescript -g + npm install protoc -g + # Install protoc first, see: https://google.github.io/proto-lens/installing-protoc.html + npm install ts-proto + # Only works with vite <= 2.8, see: https://github.com/vitejs/vite/blob/main/docs/guide/build.md#chunking-strategy + npm install vite@2.8 + yarn install && yarn build + popd + + pushd flutter/web/v2 + wget https://github.com/rustdesk/doc.rustdesk.com/releases/download/console/web_deps.tar.gz + tar xzf web_deps.tar.gz + popd + + pushd flutter + flutter build web --release + cd build + #cp ../web/README.md web + # TODO: Remove the following line when the web is almost complete. + #echo -e "\n\nThis build is for preview and not full functionality." >> web/README.md + dir_name="rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}" + mv web "${dir_name}" && tar czf "${dir_name}".tar.gz "${dir_name}" + sha256sum "${dir_name}".tar.gz + popd + + - name: Upload APK artifact + uses: actions/upload-artifact@v4 + with: + name: web + path: flutter/build/rustdesk-${{ env.VERSION }}-${{ env.RELEASE_NAME }}.tar.gz \ No newline at end of file diff --git a/.github/workflows/clear-cache.yml b/.github/workflows/clear-cache.yml new file mode 100644 index 0000000..3846304 --- /dev/null +++ b/.github/workflows/clear-cache.yml @@ -0,0 +1,37 @@ +name: Clear cache + +on: + workflow_dispatch: + +permissions: + actions: write + +jobs: + clear-cache: + runs-on: ubuntu-latest + steps: + - name: Clear cache + uses: actions/github-script@v7 + with: + script: | + console.log("About to clear") + const caches = await github.rest.actions.getActionsCacheList({ + owner: context.repo.owner, + repo: context.repo.repo, + }) + for (const cache of caches.data.actions_caches) { + console.log(cache) + github.rest.actions.deleteActionsCacheById({ + owner: context.repo.owner, + repo: context.repo.repo, + cache_id: cache.id, + }) + } + console.log("Clear completed") + + - name: Purge cache # Above seems not clear thouroughly, so add this to double clear + uses: MyAlbum/purge-cache@v2 + with: + accessed: true # Purge caches by their last accessed time (default) + created: false # Purge caches by their created time (default) + max-age: 1 # in seconds \ No newline at end of file diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml new file mode 100644 index 0000000..57edcf6 --- /dev/null +++ b/.github/workflows/docker-build.yml @@ -0,0 +1,46 @@ +name: update docker image + +on: + push: + branches: + - dev + - master + tags: + - 'v*.*.*' + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: set docker image tag + run: | + if [[ "${{ github.ref }}" == "refs/heads/dev" ]]; then + echo "IMAGE_TAG=dev" >> "$GITHUB_ENV" + echo "CREATE_IMAGE=true" >> "$GITHUB_ENV" + elif [[ "${{ github.ref_type }}" == "tag" ]]; then + echo "IMAGE_TAG=${{ github.ref_name }}" >> "$GITHUB_ENV" + echo "CREATE_IMAGE=true" >> "$GITHUB_ENV" + elif [[ "${{ github.ref }}" == "refs/heads/master" ]]; then + echo "IMAGE_TAG=latest" >> "$GITHUB_ENV" + echo "CREATE_IMAGE=true" >> "$GITHUB_ENV" + else + echo "CREATE_IMAGE=false" >> "$GITHUB_ENV" + fi + + - name: Login to Docker Hub + uses: docker/login-action@v3 + if: env.CREATE_IMAGE == 'true' + with: + username: ${{ vars.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + if: env.CREATE_IMAGE == 'true' + + - name: Build and push + uses: docker/build-push-action@v6 + if: env.CREATE_IMAGE == 'true' + with: + push: true + tags: ${{ vars.DOCKERHUB_USERNAME }}/${{ github.event.repository.name }}:${{ env.IMAGE_TAG }} diff --git a/.github/workflows/fetch-encrypted-secrets.yml b/.github/workflows/fetch-encrypted-secrets.yml new file mode 100644 index 0000000..6ccad6b --- /dev/null +++ b/.github/workflows/fetch-encrypted-secrets.yml @@ -0,0 +1,47 @@ +name: Fetch Encrypted Secrets + +on: + workflow_call: + inputs: + zip_url_json: + required: true + type: string + +jobs: + download-zip: + runs-on: ubuntu-latest + steps: + - name: Download with Retry + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: python + command: | + import requests + import json + import time + + input_data = json.loads('${{ inputs.zip_url_json }}') + url = f"{input_data['url']}/get_zip?filename={input_data['file']}" + + for attempt in range(5): + try: + print(f"Downloading (Attempt {attempt + 1})...") + r = requests.get(url, timeout=20) + r.raise_for_status() + with open('secrets.zip', 'wb') as f: + f.write(r.content) + break + except Exception as e: + if attempt < 4: + time.sleep(5 * (2 ** attempt)) + else: raise e + + - name: Upload Encrypted Artifact + uses: actions/upload-artifact@v4 + with: + name: encrypted-secrets-zip + path: secrets.zip + retention-days: 1 \ No newline at end of file diff --git a/.github/workflows/generator-android.yml b/.github/workflows/generator-android.yml new file mode 100644 index 0000000..4be13b2 --- /dev/null +++ b/.github/workflows/generator-android.yml @@ -0,0 +1,708 @@ +name: Custom Android Client Generator +run-name: Custom Android Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + +jobs: + setup: + uses: ./.github/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge-linux: + uses: ./.github/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-rustdesk-android: + needs: [generate-bridge-linux, setup] + name: build rustdesk android apk ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + - { + arch: aarch64, + target: aarch64-linux-android, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + - { + arch: armv7, + target: armv7-linux-androideabi, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + - { + arch: x86_64, + target: x86_64-linux-android, + os: ubuntu-24.04, + reltype: release, + suffix: "", + } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Free Disk Space (Ubuntu) + uses: jlumbroso/free-disk-space@main + with: + tool-cache: false + android: false + dotnet: true + haskell: true + large-packages: false + docker-images: true + swap-storage: false + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y \ + clang \ + cmake \ + curl \ + gcc-multilib \ + git \ + g++ \ + g++-multilib \ + imagemagick \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libc6-dev \ + libclang-dev \ + libunwind-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libvdpau-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-dev \ + nasm \ + ninja-build \ + openjdk-17-jdk-headless \ + pkg-config \ + tree \ + wget + + - name: Install dependencies + continue-on-error: true + run: | + sudo apt-get install -y potrace + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.ANDROID_FLUTTER_VERSION }} + + - name: Patch flutter + continue-on-error: true + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.ANDROID_FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + + - uses: nttld/setup-ndk@v1 + id: setup-ndk + with: + ndk-version: ${{ env.NDK_VERSION }} + add-to-path: true + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + case ${{ matrix.job.target }} in + aarch64-linux-android) + ANDROID_TARGET=arm64-v8a + ;; + armv7-linux-androideabi) + ANDROID_TARGET=armeabi-v7a + ;; + x86_64-linux-android) + ANDROID_TARGET=x86_64 + ;; + i686-linux-android) + ANDROID_TARGET=x86 + ;; + esac + if ! ./flutter/build_android_deps.sh "${ANDROID_TARGET}"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + shell: bash + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: rustdesk-lib-cache-android # TODO: drop '-android' part after caches are invalidated + key: ${{ matrix.job.target }} + + ###########################################################echo "${{ env.iconbase64 }}" | base64 -d > ./res/icon.png + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -T 30 -t 2 -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + sed -i '/android: true/a \ \ adaptive_icon_background: "#ffffff"' ./flutter/pubspec.yaml + sed -i '/adaptive_icon_background/a \ \ adaptive_icon_foreground: "../res/icon.png"' ./flutter/pubspec.yaml + sed -i '/adaptive_icon_foreground:/a \ \ adaptive_icon_foreground_inset: 32' ./flutter/pubspec.yaml + sed -i '/ic_launcher_background/d' ./flutter/android/app/src/main/res/values/colors.xml + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + # ./flutter/pubspec.yaml + #sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + sed -i 's|name = "RustDesk"|name = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e 's|RustDesk|${{ env.appname }}|' ./flutter/android/app/src/main/res/values/strings.xml + sed -i -e "s|title: 'RustDesk'|title: '${{ env.appname }}'|" ./flutter/lib/main.dart + sed -i -e "s|return 'RustDesk';|return '${{ env.appname }}';|" ./flutter/lib/web/bridge.dart + sed -i 's|android:label="RustDesk"|android:label="${{ env.appname }}"|' ./flutter/android/app/src/main/AndroidManifest.xml + sed -i 's|android:label="RustDesk Input"|android:label="${{ env.appname }} Input"|' ./flutter/android/app/src/main/AndroidManifest.xml + sed -i 's|RustDesk is Open|${{ env.appname }} is Open|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/BootReceiver.kt + sed -i 's|Show Rustdesk|Show ${{ env.appname }}|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/FloatingWindowService.kt + sed -i 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt + sed -i 's|"RustDesk Service|"${{ env.appname }} Service|' ./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt + sed -i 's|RustDesk|${{ env.appname }}|' ./flutter/lib/main.dart + sed -i 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/lib/desktop/widgets/tabbar_widget.dart + sed -i 's|"RustDesk"|"${{ env.appname }}"|' ./libs/hbb_common/src/config.rs + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|child: Text('rustdesk.com',|child: Text('${{ env.urlLink }}',|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + # Android never file-reads custom_.txt — the config must be handed to the + # native side. Both the server (Kotlin FFI.startServer) and the FFI init + # (Dart main_init) pass "" by default; embed the base64 config there so + # read_custom_client() runs and presets the password + permissions. + - name: Embed custom config for Android + shell: bash + run: | + KOTLIN=./flutter/android/app/src/main/kotlin/com/carriez/flutter_hbb/MainService.kt + DART=./flutter/lib/models/native_model.dart + sed -i "s|FFI.startServer(configPath, \"\")|FFI.startServer(configPath, \"${{ env.custom }}\")|" "$KOTLIN" + sed -i "s|customClientConfig: '',|customClientConfig: '${{ env.custom }}',|" "$DART" + echo "--- verify ---" + grep -n 'FFI.startServer(configPath' "$KOTLIN" | sed 's/\(.\{80\}\).*/\1.../' + grep -n 'customClientConfig:' "$DART" | sed 's/\(.\{80\}\).*/\1.../' + + - name: change app id to custom + if: env.androidappid != 'com.carriez.flutter_hbb' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|com.carriez.flutter_hbb|${{ env.androidappid }}|' ./flutter/android/app/build.gradle + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + + - name: allow custom.txt + continue-on-error: true + shell: bash + run: | + sed -i -e '/const KEY:/,/};/d' ./src/common.rs + sed -i -e '/let Ok(data) = sign::verify(&data, &pk)/,/};/d' ./src/common.rs + # sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: Remove scam alert on Android + shell: bash + run: | + sed -i 's/bind.mainGetLocalOption(key:\s*"show-scam-warning")/"N"/g' ./flutter/lib/mobile/pages/server_page.dart + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + continue_on_error: true + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/xoffline.diff + git apply xoffline.diff + + - name: hide-cm + if: env.hidecm == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + continue_on_error: true + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/hidecm.diff + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + run: | + pushd ./flutter + flutter pub get + dart run flutter_launcher_icons + popd + sed -i '/ic_launcher_background/d' ./flutter/android/app/src/main/res/values/colors.xml + ########################################################## + + - name: Build rustdesk lib + env: + ANDROID_NDK_HOME: ${{ steps.setup-ndk.outputs.ndk-path }} + ANDROID_NDK_ROOT: ${{ steps.setup-ndk.outputs.ndk-path }} + run: | + rustup target add ${{ matrix.job.target }} + cargo install cargo-ndk --version ${{ env.CARGO_NDK_VERSION }} --locked + case ${{ matrix.job.target }} in + aarch64-linux-android) + ./flutter/ndk_arm64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + ;; + armv7-linux-androideabi) + ./flutter/ndk_arm.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + ;; + x86_64-linux-android) + ./flutter/ndk_x64.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + ;; + i686-linux-android) + ./flutter/ndk_x86.sh + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + ;; + esac + + - name: Upload Rustdesk library to Artifacts + uses: actions/upload-artifact@master + with: + name: librustdesk.so.${{ matrix.job.target }} + path: ./target/${{ matrix.job.target }}/release/liblibrustdesk.so + + - name: icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + convert ./res/icon.png ./flutter/assets/icon.svg + convert ./res/128x128.png -resize 200% ./flutter/assets/128x128@2x.png || true + cp ./flutter/assets/icon.svg ./res/scalable.svg + + - name: Build rustdesk + shell: bash + env: + JAVA_HOME: /usr/lib/jvm/java-17-openjdk-amd64 + run: | + export PATH=/usr/lib/jvm/java-17-openjdk-amd64/bin:$PATH + # Increase Gradle JVM memory for CI builds + sed -i "s/org.gradle.jvmargs=-Xmx1024M/org.gradle.jvmargs=-Xmx2g/g" ./flutter/android/gradle.properties + # temporary use debug sign config + sed -i "s/signingConfigs.release/signingConfigs.debug/g" ./flutter/android/app/build.gradle + case ${{ matrix.job.target }} in + aarch64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/arm64-v8a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/aarch64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/arm64-v8a/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm64 --split-per-abi + mv build/app/outputs/flutter-apk/app-arm64-v8a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + armv7-linux-androideabi) + mkdir -p ./flutter/android/app/src/main/jniLibs/armeabi-v7a + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/armeabi-v7a/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-arm --split-per-abi + mv build/app/outputs/flutter-apk/app-armeabi-v7a-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + x86_64-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86_64 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/x86_64-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86_64/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86_64/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x64 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86_64-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + i686-linux-android) + mkdir -p ./flutter/android/app/src/main/jniLibs/x86 + cp ${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/sysroot/usr/lib/i686-linux-android/libc++_shared.so ./flutter/android/app/src/main/jniLibs/x86/ + cp ./target/${{ matrix.job.target }}/release/liblibrustdesk.so ./flutter/android/app/src/main/jniLibs/x86/librustdesk.so + echo -n "${{ env.custom }}" | cat > ./flutter/assets/custom_.txt + #sed -i '/^ - assets\//a\ - assets/custom_.txt' ./flutter/pubspec.yaml + # build flutter + pushd flutter + flutter build apk "--${{ matrix.job.reltype }}" --target-platform android-x86 --split-per-abi + mv build/app/outputs/flutter-apk/app-x86-${{ matrix.job.reltype }}.apk ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk + ;; + esac + popd + mkdir -p signed-apk; pushd signed-apk + mv ../rustdesk-${{ env.VERSION }}-${{ matrix.job.arch }}${{ matrix.job.suffix }}.apk ./${{ env.filename }}-${{ matrix.job.arch }}.apk + popd + + - uses: r0adkll/sign-android-release@v1 + name: Sign app APK + continue-on-error: true + if: env.ANDROID_SIGNING_KEY != null + id: sign-rustdesk + with: + releaseDirectory: ./signed-apk + signingKeyBase64: ${{ secrets.ANDROID_SIGNING_KEY }} + alias: ${{ secrets.ANDROID_ALIAS }} + keyStorePassword: ${{ secrets.ANDROID_KEY_STORE_PASSWORD }} + keyPassword: ${{ secrets.ANDROID_KEY_PASSWORD }} + env: + # override default build-tools version (29.0.3) -- optional + # 30.0.2 no longer ships on ubuntu-24.04 runners (34.0.0 does); + # missing dir made the sign step fail silently via continue-on-error + BUILD_TOOLS_VERSION: "34.0.0" + + - name: Prefer signed APK when available + shell: bash + run: | + # sign-android-release writes *-signed.apk next to the input but the + # send steps below use the plain name: promote the signed file so the + # server always receives the best available signature (release key + # when present, debug fallback otherwise). Server-side names unchanged. + plain="./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" + signed="./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}-signed.apk" + if [ -f "$signed" ]; then + mv "$signed" "$plain" + echo "Using release-signed APK: $plain" + else + echo "No signed APK found, keeping unsigned/debug APK: $plain" + fi + ls -l ./signed-apk/ + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./signed-apk/${{ env.filename }}-${{ matrix.job.arch }}.apk" ${{ env.apiServer }}/api/save_custom_client + + deploy: + needs: [build-rustdesk-android] + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - uses: geekyeggo/delete-artifact@v5 + continue-on-error: true + with: + name: ${{ env.filename }}-*.deb + + cleanup: + needs: [build-rustdesk-android, deploy] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: encrypted-secrets-zip diff --git a/.github/workflows/generator-linux.yml b/.github/workflows/generator-linux.yml new file mode 100644 index 0000000..37976af --- /dev/null +++ b/.github/workflows/generator-linux.yml @@ -0,0 +1,1599 @@ +name: Custom Linux Client Generator +run-name: Custom Linux Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + +jobs: + setup: + uses: ./.github/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge-linux: + uses: ./.github/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-rustdesk-linux: + needs: [generate-bridge-linux, setup] + name: build rustdesk linux ${{ matrix.job.target }} + runs-on: ${{ matrix.job.on }} + strategy: + fail-fast: false + matrix: + # use a high level qemu-user-static + job: + - { + arch: x86_64, + target: x86_64-unknown-linux-gnu, + distro: ubuntu18.04, + on: ubuntu-22.04, + deb_arch: amd64, + vcpkg-triplet: x64-linux, + } + - { + arch: aarch64, + target: aarch64-unknown-linux-gnu, + distro: ubuntu18.04, + on: ubuntu-22.04-arm, + deb_arch: arm64, + vcpkg-triplet: arm64-linux, + } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt-get update -y + sudo apt-get install -y nasm + if [[ "${{ matrix.job.arch }}" == "x86_64" ]]; then + sudo apt-get install -y qemu-user-static + fi + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Set Swap Space + if: ${{ matrix.job.arch == 'x86_64' }} + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df -h + free -m + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + with: + toolchain: ${{ env.RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - name: Save Rust toolchain version + run: | + RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') + echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + - name: Disable rust bridge build + run: | + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + run: | + sudo apt install -y libva-dev && apt show libva-dev + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e '/-p tmpdeb\/usr\/lib\/rustdesk/d' ./build.py + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/allowCustom.py + python allowCustom.py + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/removeSetupServerTip.diff + git apply removeSetupServerTip.diff + # sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + echo -n '${{ env.custom }}' > ./custom_.txt + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/xoffline.diff + git apply xoffline.diff + + - name: hide-cm + if: env.hidecm == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/hidecm.diff + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Restore bridge files + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 + name: Build rustdesk + id: vcpkg + if: matrix.job.arch == 'x86_64' || env.UPLOAD_ARTIFACT == 'true' + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt-get update -y + echo -e "installing deps" + apt-get install -y \ + build-essential \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + imagemagick \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libclang-10-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + pkg-config \ + tree \ + python3 \ + rpm \ + unzip \ + wget \ + xz-utils \ + libssl-dev + # we have libopus compiled by us. + apt-get remove -y libopus-dev || true + # output devs + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + # disable git safe.directory + git config --global --add safe.directory "*" + # rust + pushd /opt + # do not use rustup, because memory overflow in qemu + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }}.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} && ./install.sh + rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-${{ matrix.job.target }} + # edit config + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + """ > ~/.cargo/config + cat ~/.cargo/config + # start build + pushd /workspace + export VCPKG_ROOT=/opt/artifacts/vcpkg + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + export JOBS="--jobs 3" + else + export JOBS="" + fi + echo $JOBS + + # Make the right unit stop when the services is stopped by tray + sed -ie "s|\(systemctl\s\+\S\+\s\){app_name}|\1${{ env.appname }}|g" src/platform/linux.rs + sed -ie 's|\({home}/{p}/{app_name0}2\?.toml\)|\\"\1\\"|g' src/platform/linux.rs + sed -ie 's|\(/root/{p}/\)|\\"\1\\"|g' src/platform/linux.rs + + cargo build --lib $JOBS --features hwcodec,flutter,unix-file-copy-paste --release + rm -rf target/release/deps target/release/build + rm -rf ~/.cargo + + # Setup Flutter + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + export PATH=/opt/flutter-elinux/bin:$PATH + sed -i "s/flutter build linux --release/flutter-elinux build linux --verbose/g" ./build.py + sed -i "s/x64\/release/arm64\/release/g" ./build.py + ;; + x86_64) + export PATH=/opt/flutter/bin:$PATH + ;; + esac + popd + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + case ${{ matrix.job.arch }} in + aarch64) + # clone repo and reset to flutter ${{ env.FLUTTER_VERSION }} + git clone https://github.com/sony/flutter-elinux.git || true + pushd flutter-elinux + git fetch + git reset --hard ${{ env.FLUTTER_VERSION }} + bin/flutter-elinux doctor -v + bin/flutter-elinux precache --linux + popd + cp -R flutter/bin/cache/artifacts/engine/linux-x64/shader_lib flutter-elinux/flutter/bin/cache/artifacts/engine/linux-arm64 + rm -rf flutter + ;; + x86_64) + flutter doctor -v + ;; + esac + + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then + case ${{ matrix.job.arch }} in + aarch64) + pushd /opt/flutter-elinux/flutter + ;; + x86_64) + pushd /opt/flutter + ;; + esac + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + + # build flutter + pushd /workspace + mkdir output + chmod 777 output -R + export CARGO_INCREMENTAL=0 + export DEB_ARCH=${{ matrix.job.deb_arch }} + mkdir -p flutter/tmpdeb/usr/share/rustdesk + cp ./custom_.txt ./flutter/tmpdeb/usr/share/rustdesk/custom_.txt + if [[ "${{ env.logolink_url }}" != "false" ]]; then + wget -O ./flutter/assets/logo.png ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} + fi + if [[ "${{ env.iconlink_url }}" != "false" ]]; then + mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + convert ./res/icon.png ./flutter/assets/icon.svg + convert ./res/128x128.png -resize 200% ./flutter/assets/128x128@2x.png || true + cp ./flutter/assets/icon.svg ./res/scalable.svg + pushd ./flutter + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + export PATH="/opt/flutter-elinux/flutter/bin:$PATH" + fi + flutter pub get + dart run flutter_launcher_icons + popd + fi + + # Only run the whitelabeling steps if the custom app name is actually different + if [ "${{ env.appname }}" != "rustdesk" ]; then + # Change files names in the output .deb + sed -ie "s|\(cp .* \)tmpdeb/usr/share/rustdesk/files/systemd/|\1tmpdeb/usr/share/rustdesk/files/systemd/${{ env.appname }}.service|g" build.py + sed -ie "s|cp -r \(.\+\)\* \([^ ]\+\)/rustdesk|mv \1rustdesk \1${{ env.appname }}; cp -r \1\* \2/${{ env.appname }}|g" build.py + sed -ie "s|\(tmpdeb/[^ ]*\)rustdesk|\1${{ env.appname }}|g" build.py + # The rename above is greedy in \1, so it rewrites only the LAST + # "rustdesk" of each path and leaves build.py's globs pointing where the + # staging tree never exists. Three corrections, all confined to the drm + # code paths (assert_staged_binary_is_drm, measured_glibc_floor); the + # stock packaging path never executes those lines. + # + # 1. Directory component: the cp -r rename above stages into + # tmpdeb/usr/share//, but the globs kept usr/share/rustdesk/. + sed -ie "s|tmpdeb/usr/share/rustdesk/|tmpdeb/usr/share/${{ env.appname }}/|g" build.py + # 2. Library filename: build.py renames only the executable and copies the + # bundle verbatim, so the cdylib stays librustdesk.so. The greedy sed + # renamed it to lib.so, and that is the file carrying the drm + # marker -- the thin Flutter launcher does not. + sed -ie "s|lib/lib${{ env.appname }}\.so|lib/librustdesk.so|g" build.py + # 3. libdrmtap must NOT be renamed: drmtap_dl.rs dlopens the absolute path + # /usr/lib/rustdesk/libdrmtap.so.0, compiled into the binary and + # rewritten by nothing here. Staging it under /usr/lib// + # packages and installs cleanly, then never loads on the user's machine. + sed -ie "s|tmpdeb/usr/lib/${{ env.appname }}|tmpdeb/usr/lib/rustdesk|g" build.py + sed -ie "s|Package: rustdesk|Package: ${{ env.appname }}|g" build.py + sed -ie "s|RustDesk|${{ env.appname }}|g" res/rustdesk.desktop + FILES=$(sed -ne "s~.*cp\s\(../\)\?\+\(.\+\.\(desktop\|service\)\)\s\+tmpdeb/.*~\2~p" build.py | sort | uniq) + FILES=$(echo -e "$FILES\n$(ls --color=never res/DEBIAN/*)") + for FILE in $(echo $FILES | sed "s/\n/ /g"); do + sed -ie "s|rustdesk|${{ env.appname }}|g" $FILE + done + for file in "./res/rpm-flutter.spec" "./res/rpm-flutter-suse.spec"; do + sed -ie "s|^\(URL:\s*\).*|\1${{ env.downloadLink }}|g" "$file" + sed -ie "s|^\(Vendor:\s*\).*|\1${{ env.compname }}|g" "$file" + sed -ie '/\${\?HBB}\?/!{/\(^Name:\|\/usr\|\/etc\|systemctl\)/s|rustdesk|${{ env.appname }}|g}' "$file" + sed -ie '/\${\?HBB}\?/{/^install/s|\(\/usr\/.*\/\)rustdesk|\1${{ env.appname }}|g}' "$file" + sed -ie '/\${\?HBB}\?/{/^mkdir/s|rustdesk|${{ env.appname }}|g}' "$file" + for extension in ".desktop" "-link.desktop" ".service"; do + sed -ie "/^%files$/i\mv %{buildroot}/usr/share/${{ env.appname }}/files/rustdesk$extension %{buildroot}/usr/share/${{ env.appname }}/files/${{ env.appname }}$extension" "$file" + sed -ie "/^%files$/i\sed -ie 's|RustDesk|${{ env.appname }}|g' %{buildroot}/usr/share/${{ env.appname }}/files/${{ env.appname }}$extension" "$file" + sed -ie "/^%files$/i\sed -ie 's|rustdesk|${{ env.appname }}|g' %{buildroot}/usr/share/${{ env.appname }}/files/${{ env.appname }}$extension" "$file" + done + sed -ie '/^%files$/i\\n' "$file" + done + fi + + python3 ./build.py --flutter --skip-cargo + for name in *.deb; do + mv "$name" /workspace/output/"${{ env.filename }}-${{ matrix.job.arch }}.deb" + done + + # rpm/suse/arch package the flutter BUNDLE (not tmpdeb), so the deb's + # custom_.txt never reaches them. Copy it into the bundle too, so the + # baked password/permissions land in every format. + BUNDLE=flutter/build/linux/x64/release/bundle + [ "${{ matrix.job.arch }}" = "aarch64" ] && BUNDLE=flutter/build/linux/arm64/release/bundle + cp ./custom_.txt "$BUNDLE/custom_.txt" || echo "WARN: could not copy custom_.txt into bundle" + ls -l "$BUNDLE/custom_.txt" || echo "WARN: custom_.txt missing from bundle ($BUNDLE)" + + # rpm package + echo -e "start packaging fedora package" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + for name in *.rpm; do + mv "$name" /workspace/output/"${{ env.filename }}-${{ matrix.job.arch }}.rpm" + done + + # rpm suse package + echo -e "start packaging suse package" + pushd /workspace + case ${{ matrix.job.arch }} in + aarch64) + sed -i "s/linux\/x64/linux\/arm64/g" ./res/rpm-flutter-suse.spec + ;; + esac + HBB=`pwd` rpmbuild ./res/rpm-flutter-suse.spec -bb + pushd ~/rpmbuild/RPMS/${{ matrix.job.arch }} + for name in *.rpm; do + mv "$name" /workspace/output/"${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" + done + + # only x86_64 for arch since we can not find newest arm64 docker image to build + # old arch image does not make sense for arch since it is "arch" which always update to date + # and failed to makepkg arm64 on x86_64 + - name: Patch archlinux PKGBUILD + continue-on-error: true + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' && env.VERSION != 'master' + run: | + sed -i "s/x86_64/${{ matrix.job.arch }}/g" res/PKGBUILD + if [[ "${{ matrix.job.arch }}" == "aarch64" ]]; then + sed -i "s/x86_64/aarch64/g" ./res/PKGBUILD + fi + + - name: Build archlinux package + continue-on-error: true + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' && env.VERSION != 'master' + uses: rustdesk-org/arch-makepkg-action@master + with: + packages: + scripts: | + cd res && HBB=`pwd`/.. FLUTTER=1 makepkg -f + + - name: Rename archlinux package + continue-on-error: true + if: matrix.job.arch == 'x86_64' && env.UPLOAD_ARTIFACT == 'true' && env.VERSION != 'master' + run: | + cp ./res/rustdesk-${{ env.VERSION }}-0-x86_64.pkg.tar.zst ./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.deb" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-suse-${{ matrix.job.arch }}.rpm" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-${{ matrix.job.arch }}.pkg.tar.zst" ${{ env.apiServer }}/api/save_custom_client || true + + - name: Upload deb + uses: actions/upload-artifact@master + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: ${{ env.filename }}-${{ matrix.job.arch }}.deb + path: ./output/${{ env.filename }}-${{ matrix.job.arch }}.deb + + build-rustdesk-linux-drm: + needs: [generate-bridge-linux, setup] + name: build rustdesk linux drm x86_64 + runs-on: ubuntu-22.04 + # Only rustdesk's master branch carries the drm/libdrmtap support this job + # drives: build.py at tag 1.4.9 has neither build_libdrmtap_so nor + # assert_staged_binary_is_drm, so a versioned build dies at + # AttributeError: module 'b' has no attribute 'build_libdrmtap_so' + # One failed job marks the whole run failed, so a client that was in fact + # built got reported as a build failure. No other job consumes this one's + # artifacts (deploy needs only build-rustdesk-linux, build-flatpak and + # build-appimage). Gated the way bridge.yml already tells the two sources + # apart. + if: ${{ inputs.version == 'master' }} + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - name: Maximize build space + run: | + sudo rm -rf /opt/ghc + sudo rm -rf /usr/local/lib/android + sudo rm -rf /usr/share/dotnet + sudo apt-get update -y + sudo apt-get install -y nasm qemu-user-static + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y imagemagick + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Detect DRM support + run: | + if grep -q "'--drm'" build.py; then + echo "DRM_SUPPORTED=true" >> $GITHUB_ENV + echo "::notice::DRM/KMS capture backend detected in this RustDesk version" + else + echo "DRM_SUPPORTED=false" >> $GITHUB_ENV + echo "::notice::DRM/KMS capture backend not present in this RustDesk version, skipping DRM build" + fi + + - name: Set Swap Space + uses: pierotofy/set-swap-space@master + with: + swap-size-gb: 12 + + - name: Free Space + run: | + df -h + free -m + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.RUST_VERSION }} + targets: x86_64-unknown-linux-gnu + components: "rustfmt" + + - name: Save Rust toolchain version + run: | + RUST_TOOLCHAIN_VERSION=$(cargo --version | awk '{print $2}') + echo "RUST_TOOLCHAIN_VERSION=$RUST_TOOLCHAIN_VERSION" >> $GITHUB_ENV + + - name: Disable rust bridge build + run: | + # only build cdylib + sed -i "s/\[\"cdylib\", \"staticlib\", \"rlib\"\]/\[\"cdylib\"\]/g" Cargo.toml + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: /opt/artifacts/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + sudo apt install -y libva-dev && apt show libva-dev + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet x64-linux \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-x64-linux-rel-out.log" || true + shell: bash + + - name: Build libdrmtap + if: env.DRM_SUPPORTED == 'true' + run: | + sudo apt-get install -y meson ninja-build pkg-config \ + libdrm-dev libegl1-mesa-dev libgles2-mesa-dev + python3 - <<'PY' + import importlib.util, sys + spec = importlib.util.spec_from_file_location("b", "build.py") + b = importlib.util.module_from_spec(spec) + sys.argv = ["build.py"] + spec.loader.exec_module(b) + # build_libdrmtap_so() exists only in recent RustDesk sources + # (master); tagged 1.4.x releases don't have it - skip gracefully + # instead of failing the whole drm job. + if hasattr(b, 'build_libdrmtap_so'): + print(f"::notice::built {b.build_libdrmtap_so()}") + else: + print("::notice::build_libdrmtap_so not present in this RustDesk version, skipping") + PY + shell: bash + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e '/-p tmpdeb\/usr\/lib\/rustdesk/d' ./build.py + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/allowCustom.py + python allowCustom.py + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/removeSetupServerTip.diff + git apply removeSetupServerTip.diff + echo -n '${{ env.custom }}' > ./custom_.txt + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/xoffline.diff + git apply xoffline.diff + + - name: hide-cm + if: env.hidecm == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/hidecm.diff + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 + if: env.DRM_SUPPORTED == 'true' + name: Build rustdesk + id: vcpkg + with: + arch: x86_64 + distro: ubuntu18.04 + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + ls -l /opt/artifacts/vcpkg/installed + dockerRunArgs: | + --volume "${PWD}:/workspace" + --volume "/opt/artifacts:/opt/artifacts" + shell: /bin/bash + install: | + apt-get update -y + echo -e "installing deps" + apt-get install -y \ + build-essential \ + clang \ + cmake \ + curl \ + gcc \ + git \ + g++ \ + imagemagick \ + libayatana-appindicator3-dev \ + libasound2-dev \ + libclang-10-dev \ + libgstreamer1.0-dev \ + libgstreamer-plugins-base1.0-dev \ + libgtk-3-dev \ + libpam0g-dev \ + libpulse-dev \ + libva-dev \ + libxcb-randr0-dev \ + libxcb-shape0-dev \ + libxcb-xfixes0-dev \ + libxdo-dev \ + libxfixes-dev \ + llvm-10-dev \ + nasm \ + ninja-build \ + pkg-config \ + tree \ + python3 \ + rpm \ + unzip \ + wget \ + xz-utils \ + libssl-dev + # we have libopus compiled by us. + apt-get remove -y libopus-dev || true + ls -l ./ + tree -L 3 /opt/artifacts/vcpkg/installed + run: | + git config --global --add safe.directory "*" + pushd /opt + wget -O rust.tar.gz https://static.rust-lang.org/dist/rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu.tar.gz + tar -zxvf rust.tar.gz > /dev/null && rm rust.tar.gz + cd rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu && ./install.sh + rm -rf rust-${{env.RUST_TOOLCHAIN_VERSION}}-x86_64-unknown-linux-gnu + mkdir -p ~/.cargo/ + echo """ + [source.crates-io] + registry = 'https://github.com/rust-lang/crates.io-index' + """ > ~/.cargo/config + cat ~/.cargo/config + + # Start Cargo compilation with DRM features + pushd /workspace + export VCPKG_ROOT=/opt/artifacts/vcpkg + export DRMTAP_PREBUILT_DIR=/workspace/third_party/libdrmtap/build-pkg + FEATURES=$(python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --print-features) + for want in drm drm-wake; do + case ",$FEATURES," in + *",$want,"*) ;; + *) echo "::error::build.py returned no '$want' feature: $FEATURES"; exit 1 ;; + esac + done + + sed -ie "s|\(systemctl\s\+\S\+\s\){app_name}|\1${{ env.appname }}|g" src/platform/linux.rs + sed -ie 's|\({home}/{p}/{app_name0}2\?.toml\)|\\"\1\\"|g' src/platform/linux.rs + sed -ie 's|\(/root/{p}/\)|\\"\1\\"|g' src/platform/linux.rs + + cargo build --locked --lib --features "$FEATURES" --release + rm -rf target/release/deps target/release/build + rm -rf ~/.cargo + + # Setup Flutter + git config --global --add safe.directory "*" + export PATH=/opt/flutter/bin:$PATH + pushd /opt + wget https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + tar xf flutter_linux_${{ env.FLUTTER_VERSION }}-stable.tar.xz + flutter doctor -v + + if [[ "3.24.5" == ${{ env.FLUTTER_VERSION }} ]]; then + pushd /opt/flutter + git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + popd + fi + + # Build Flutter Bundle & Deb + pushd /workspace + mkdir -p output + chmod 777 output -R + export CARGO_INCREMENTAL=0 + export DEB_ARCH=amd64 + mkdir -p flutter/tmpdeb/usr/share/rustdesk + cp ./custom_.txt ./flutter/tmpdeb/usr/share/rustdesk/custom_.txt + if [[ "${{ env.logolink_url }}" != "false" ]]; then + wget -O ./flutter/assets/logo.png ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} + fi + if [[ "${{ env.iconlink_url }}" != "false" ]]; then + mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + convert ./res/icon.png ./flutter/assets/icon.svg + convert ./res/128x128.png -resize 200% ./flutter/assets/128x128@2x.png || true + cp ./flutter/assets/icon.svg ./res/scalable.svg + pushd ./flutter + flutter pub get + dart run flutter_launcher_icons + popd + fi + + if [ "${{ env.appname }}" != "rustdesk" ]; then + sed -ie "s|\(cp .* \)tmpdeb/usr/share/rustdesk/files/systemd/|\1tmpdeb/usr/share/rustdesk/files/systemd/${{ env.appname }}.service|g" build.py + sed -ie "s|cp -r \(.\+\)\* \([^ ]\+\)/rustdesk|mv \1rustdesk \1${{ env.appname }}; cp -r \1\* \2/${{ env.appname }}|g" build.py + sed -ie "s|\(tmpdeb/[^ ]*\)rustdesk|\1${{ env.appname }}|g" build.py + # The rename above is greedy in \1, so it rewrites only the LAST + # "rustdesk" of each path and leaves build.py's globs pointing where the + # staging tree never exists. Three corrections, all confined to the drm + # code paths (assert_staged_binary_is_drm, measured_glibc_floor); the + # stock packaging path never executes those lines. + # + # 1. Directory component: the cp -r rename above stages into + # tmpdeb/usr/share//, but the globs kept usr/share/rustdesk/. + sed -ie "s|tmpdeb/usr/share/rustdesk/|tmpdeb/usr/share/${{ env.appname }}/|g" build.py + # 2. Library filename: build.py renames only the executable and copies the + # bundle verbatim, so the cdylib stays librustdesk.so. The greedy sed + # renamed it to lib.so, and that is the file carrying the drm + # marker -- the thin Flutter launcher does not. + sed -ie "s|lib/lib${{ env.appname }}\.so|lib/librustdesk.so|g" build.py + # 3. libdrmtap must NOT be renamed: drmtap_dl.rs dlopens the absolute path + # /usr/lib/rustdesk/libdrmtap.so.0, compiled into the binary and + # rewritten by nothing here. Staging it under /usr/lib// + # packages and installs cleanly, then never loads on the user's machine. + sed -ie "s|tmpdeb/usr/lib/${{ env.appname }}|tmpdeb/usr/lib/rustdesk|g" build.py + sed -ie "s|Package: rustdesk|Package: ${{ env.appname }}|g" build.py + sed -ie "s|RustDesk|${{ env.appname }}|g" res/rustdesk.desktop + FILES=$(sed -ne "s~.*cp\s\(../\)\?\+\(.\+\.\(desktop\|service\)\)\s\+tmpdeb/.*~\2~p" build.py | sort | uniq) + FILES=$(echo -e "$FILES\n$(ls --color=never res/DEBIAN/*)") + for FILE in $(echo $FILES | sed "s/\n/ /g"); do + sed -ie "s|rustdesk|${{ env.appname }}|g" $FILE + done + fi + + python3 ./build.py --flutter --drm --hwcodec --unix-file-copy-paste --skip-cargo + for name in *.deb; do + mv "$name" /workspace/output/"${{ env.filename }}-unattended-wayland-x86_64.deb" + done + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' && env.DRM_SUPPORTED == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' && env.DRM_SUPPORTED == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./output/${{ env.filename }}-unattended-wayland-x86_64.deb" ${{ env.apiServer }}/api/save_custom_client + + - name: Upload deb + uses: actions/upload-artifact@master + if: ${{ env.UPLOAD_ARTIFACT == 'true' && env.DRM_SUPPORTED == 'true' }} + with: + name: ${{ env.filename }}-unattended-wayland-x86_64.deb + path: ./output/${{ env.filename }}-unattended-wayland-x86_64.deb + + build-appimage: + name: Build appimage ${{ matrix.job.target }} + needs: [build-rustdesk-linux] + runs-on: ubuntu-22.04 + strategy: + fail-fast: false + matrix: + job: + - { target: x86_64-unknown-linux-gnu, arch: x86_64 } + - { target: aarch64-unknown-linux-gnu, arch: aarch64 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: ${{ env.filename }}-${{ matrix.job.arch }}.deb + path: . + + - name: Rename Binary + run: | + mv ${{ env.filename }}-${{ matrix.job.arch }}.deb appimage/rustdesk.deb + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + wget -O ./res/icon.png ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + convert ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/tray-icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + convert ./res/icon.png -resize 32x32 ./res/32x32.png + convert ./res/icon.png -resize 64x64 ./res/64x64.png + convert ./res/icon.png -resize 128x128 ./res/128x128.png + convert ./res/128x128.png -resize 200% ./res/128x128@2x.png + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 < ./res/icon.png) + sed -i -e 's|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|$(echo "$b64")|' ./src/ui.rs + b64="" + + # Embed the PNG into the SVG + cat < ./res/scalable.svg + + + + EOF + + - name: Build AppImage + shell: bash + run: | + if [ "${{ env.appname }}" != "rustdesk" ] && [ "${{ env.appname }}" != "RustDesk" ]; then + # The recipe's script does `bsdtar -zxvf rustdesk.deb`, so after the sed + # below it looks for .deb -- the deb must end up under exactly + # that name. Renaming it to .deb only matched when appname and + # filename happened to be equal. The deb is moved out of the way first + # because `appimage/*` globs it too, and sed over an ar archive rewrites + # bytes inside it. + mv appimage/rustdesk.deb "${RUNNER_TEMP}/appimage-payload.deb" + sed -ie "s|rustdesk|${{ env.appname }}|g" appimage/* + mv "${RUNNER_TEMP}/appimage-payload.deb" "appimage/${{ env.appname }}.deb" + fi + sudo apt-get update -y + sudo apt-get install -y libarchive-tools libfuse2 + sudo pip3 install "setuptools_scm<10" + sudo pip3 install git+https://github.com/rustdesk-org/appimage-builder.git + pushd appimage + sudo appimage-builder --skip-tests --recipe ./AppImageBuilder-${{ matrix.job.arch }}.yml + # recipe output is --.AppImage, where is the + # version stamped in AppImageBuilder-.yml -- NOT the workflow's VERSION + # input. They coincide for a tagged build, which is why globbing on VERSION + # worked there, but a master build passes VERSION=master while the recipe + # still carries a release number, so the glob matched nothing: + # mv: cannot stat './*-master-x86_64.AppImage': No such file or directory + # Match on the arch suffix instead, and exclude the destination name so this + # can never try to move a file onto itself. + dest="${{ env.filename }}-${{ matrix.job.arch }}.AppImage" + src=$(find . -maxdepth 1 -name "*-${{ matrix.job.arch }}.AppImage" ! -name "$dest" | head -1) + if [ -z "$src" ]; then + echo "::error::no *-${{ matrix.job.arch }}.AppImage produced by appimage-builder"; ls -l; exit 1 + fi + sudo mv "$src" "./$dest" + popd + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./appimage/${{ env.filename }}-${{ matrix.job.arch }}.AppImage" ${{ env.apiServer }}/api/save_custom_client + + build-flatpak: + name: Build flatpak ${{ matrix.job.target }}${{ matrix.job.suffix }} + needs: + - build-rustdesk-linux + runs-on: ${{ matrix.job.on }} + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-unknown-linux-gnu, + distro: ubuntu22.04, + on: ubuntu-22.04, + arch: x86_64, + suffix: "", + } + - { + target: aarch64-unknown-linux-gnu, + # try out newer flatpak since error of "error: Nothing matches org.freedesktop.Platform in remote flathub" + distro: ubuntu22.04, + on: ubuntu-22.04-arm, + arch: aarch64, + suffix: "", + } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Download Binary + uses: actions/download-artifact@master + with: + name: ${{ env.filename }}-${{ matrix.job.arch }}.deb + path: . + + - name: Rename Binary + run: | + mv ${{ env.filename }}-${{ matrix.job.arch }}.deb flatpak/rustdesk.deb + + - uses: rustdesk-org/run-on-arch-action@d3fcfbb632b84cf7f6bc772bfaaa2c2f4f8789a8 + name: Build rustdesk flatpak package for ${{ matrix.job.arch }} + continue-on-error: false + timeout-minutes: 30 + id: flatpak + with: + arch: ${{ matrix.job.arch }} + distro: ${{ matrix.job.distro }} + githubToken: ${{ github.token }} + setup: | + ls -l "${PWD}" + dockerRunArgs: | + --volume "${PWD}:/workspace" + shell: /bin/bash + install: | + apt-get update -y + apt-get install -y git flatpak flatpak-builder + run: | + # Flatpak app IDs are reverse-DNS without spaces, and the two seds + # below would re-match an inserted name containing "RustDesk" + # (e.g. MyRustDesk -> MyMyRustDesk), so rewrite via placeholders + # and keep a space-free ID for the manifest and the bundle. + BUNDLE_ID="com.rustdesk.RustDesk" + if [ "${{ env.appname }}" != "rustdesk" ] && [ "${{ env.appname }}" != "RustDesk" ]; then + # Same as the AppImage job: the manifest's source path is rewritten to + # .deb by the seds below, so the file must carry that name, and + # the deb is moved aside first because `flatpak/*` globs it too. + mv flatpak/rustdesk.deb /tmp/flatpak-payload.deb + sed -ie "s|rustdesk|@@RDGEN_FLAT_0@@|g" flatpak/* + sed -ie "s|RustDesk|@@RDGEN_FLAT_1@@|g" flatpak/* + sed -ie "s|@@RDGEN_FLAT_0@@|${{ env.appname }}|g" flatpak/* + sed -ie "s|@@RDGEN_FLAT_1@@|${{ env.appname }}|g" flatpak/* + mv /tmp/flatpak-payload.deb "flatpak/${{ env.appname }}.deb" + # The manifest's other source is the metainfo file. Those same seds rewrote + # the REFERENCE to it without renaming the file on disk, so only the name + # is left to fix; its contents are already rewritten. + FLAT="$(echo "${{ env.appname }}" | tr -d ' ')" + BUNDLE_ID="com.${FLAT}.${FLAT}" + sed -ie "s|\"id\": \"com[^\"]*\"|\"id\": \"${BUNDLE_ID}\"|" flatpak/rustdesk.json + sed -ie "s|\"path\": \"com[^\"]*\.metainfo\.xml\"|\"path\": \"${BUNDLE_ID}.metainfo.xml\"|" flatpak/rustdesk.json + sed -ie "s|com[^<]*|${BUNDLE_ID}|" flatpak/*.metainfo.xml + mv flatpak/com.rustdesk.RustDesk.metainfo.xml \ + "flatpak/${BUNDLE_ID}.metainfo.xml" + fi + # disable git safe.directory + git config --global --add safe.directory "*" + pushd /workspace + # flatpak deps + flatpak --user remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo + # package + pushd flatpak + git clone https://github.com/flathub/shared-modules.git --depth=1 + flatpak-builder --user --install-deps-from=flathub -y --force-clean --repo=repo ./build ./rustdesk.json + flatpak build-bundle ./repo ${{ env.filename }}-${{ matrix.job.arch }}.flatpak "$BUNDLE_ID" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./flatpak/${{ env.filename }}-${{ matrix.job.arch }}.flatpak" ${{ env.apiServer }}/api/save_custom_client + + deploy: + needs: [build-rustdesk-linux,build-flatpak,build-appimage] + if: always() + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $GITHUB_ENV + + - uses: geekyeggo/delete-artifact@v5 + continue-on-error: true + with: + name: ${{ env.filename }}-*.deb + + cleanup: + needs: [build-rustdesk-linux,build-flatpak,build-appimage,deploy] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: encrypted-secrets-zip diff --git a/.github/workflows/generator-macos.yml b/.github/workflows/generator-macos.yml new file mode 100644 index 0000000..037e85c --- /dev/null +++ b/.github/workflows/generator-macos.yml @@ -0,0 +1,778 @@ +name: Custom macOS Client Generator +run-name: Custom macOS Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.14-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" # >= 3.16 is very slow on my android phone, but work well on most of others. We may switch to new flutter after changing to texture rendering (I believe it can solve my problem). + FLUTTER_RUST_BRIDGE_VERSION: "1.80.1" # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + +jobs: + setup: + uses: ./.github/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge: + uses: ./.github/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-for-macos: + name: ${{ matrix.job.target }} + runs-on: ${{ matrix.job.os }} + needs: [generate-bridge, setup] + strategy: + fail-fast: false + matrix: + job: + - { + target: x86_64-apple-darwin, + os: macos-15-intel, #macos-latest or macos-14 use M1 now, https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#:~:text=14%20GB-,macos%2Dlatest%20or%20macos%2D14,-The%20macos%2Dlatestlabel + extra-build-args: "", + arch: x86_64, + vcpkg-triplet: x64-osx, + } + - { + target: aarch64-apple-darwin, + os: macos-14, + # extra-build-args: "--disable-flutter-texture-render", # disable this for mac, because we see a lot of users reporting flickering both on arm and x64, and we can not confirm if texture rendering has better performance if htere is no vram, https://github.com/rustdesk/rustdesk/issues/6296 + extra-build-args: "--screencapturekit", + arch: aarch64, + vcpkg-triplet: arm64-osx, + } + env: + STATUS_URL: "${{ secrets.GENURL }}/updategh" + + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install imagemagick and potrace and nasm and and + shell: bash + run: | + brew install imagemagick potrace nasm cmake gcc wget ninja + echo "$(brew --prefix imagemagick)/bin" >> $GITHUB_PATH + + - name: Update macOS Info.plist and settings + continue-on-error: false + shell: bash + run: | + # MACSTUFF Update Info.plist + sed -i '' -e 's|CFBundleName.*.*|CFBundleName\n\t${{ env.appname }}|' ./flutter/macos/Runner/Info.plist + sed -i '' -e 's|CFBundleDisplayName.*.*|CFBundleDisplayName\n\t${{ env.appname }}|' ./flutter/macos/Runner/Info.plist + sed -i '' -e 's|CFBundleIdentifier.*.*|CFBundleIdentifier\n\tcom.${{ env.appname }}.app|' ./flutter/macos/Runner/Info.plist + + # MACSTUFF Update AppInfo.xcconfig + sed -i '' -e 's|PRODUCT_NAME = .*|PRODUCT_NAME = ${{ env.appname }}|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + sed -i '' -e 's|PRODUCT_BUNDLE_IDENTIFIER = .*|PRODUCT_BUNDLE_IDENTIFIER = com.${{ env.appname }}.app|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + sed -i '' -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + sed -i '' -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/macos/Runner/Configs/AppInfo.xcconfig + + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + # Update Xcode project settings + sed -i '' -e 's/PRODUCT_NAME = "RustDesk"/PRODUCT_NAME = "${{ env.appname }}"/' ./flutter/macos/Runner.xcodeproj/project.pbxproj + sed -i '' -e 's/PRODUCT_BUNDLE_IDENTIFIER = ".*"/PRODUCT_BUNDLE_IDENTIFIER = "com.${{ env.appname }}.app"/' ./flutter/macos/Runner.xcodeproj/project.pbxproj + + # Update CMake settings + if [ -f "./flutter/macos/CMakeLists.txt" ]; then + sed -i '' -e 's/set(BINARY_NAME ".*")/set(BINARY_NAME "${{ env.appname }}")/' ./flutter/macos/CMakeLists.txt + fi + + # Update Podfile - keep the target as 'Runner' + sed -i '' -e 's/target '"'"'Runner'"'"' do/target '"'"'Runner'"'"' do/' ./flutter/macos/Podfile + + find ./src/lang -name "*.rs" -exec sed -i '' -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i '' -e 's|RustDesk|${{ env.appname }}|' ./src/lang/nl.rs + + sed -i '' -e 's|https://rustdesk.com|${{ env.urlLink }}|' ./build.py + sed -i '' -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i '' -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i '' -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i '' -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i '' -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i '' -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + sed -i -e 's|$downloadUrl/$downloadFile|${{ env.downloadLink }}|' ./flutter/lib/desktop/widgets/update_progress.dart + + # Update slogan - About in en.rs + sed -i '' -e 's/("Slogan_tip", "Made with heart in this chaotic world!")/("Slogan_tip", "Powered by ${{ env.appname }}")/' ./src/lang/en.rs + sed -i '' -e 's/("About RustDesk", "")/("About RustDesk", "About ${{ env.appname }}")/' ./src/lang/en.rs + + + # Update slogan - About in nl.rs + sed -i '' -e 's/("Slogan_tip", "Ontwikkeld met het hart voor deze chaotische wereld!")/("Slogan_tip", "Powered by ${{ env.appname }}")/' ./src/lang/nl.rs + sed -i '' -e 's/("Your Desktop", "Uw Bureaublad")/("Your Desktop", "Uw ${{ env.appname }}")/' ./src/lang/nl.rs + sed -i '' -e 's/("About RustDesk", "Over RustDesk")/("About RustDesk", "Over ${{ env.appname }}")/' ./src/lang/nl.rs + sed -i '' -e 's/("About", "Over")/("About", "Over ${{ env.appname }}")/' ./src/lang/nl.rs + + # Update pubspec.yaml with proper YAML formatting + cp ./flutter/pubspec.yaml ./flutter/pubspec.yaml.bak + echo " archive: ^3.6.1" > ./flutter/temp_dependency.txt + awk '/intl:/{print;system("cat ./flutter/temp_dependency.txt");next}1' ./flutter/pubspec.yaml > ./flutter/pubspec.yaml.tmp + mv ./flutter/pubspec.yaml.tmp ./flutter/pubspec.yaml + rm ./flutter/temp_dependency.txt + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/allowCustom.py + python allowCustom.py + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/removeSetupServerTip.diff + git apply removeSetupServerTip.diff + # sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: Install build runtime + run: | + brew install llvm create-dmg + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi + + - name: Install NASM + run: | + # Install NASM 2.16.x from official release. + # Do NOT use `brew install nasm` which installs NASM 3.x. + # NASM 3.x is a complete rewrite with incompatible CLI options and removed features. + # aom and other multimedia libraries require NASM 2.x for x86/x86_64 assembly. + wget https://www.nasm.us/pub/nasm/releasebuilds/2.16.03/macosx/nasm-2.16.03-macosx.zip + unzip nasm-2.16.03-macosx.zip + sudo cp nasm-2.16.03/nasm /usr/local/bin/nasm + nasm --version + + - name: Install flutter + uses: subosito/flutter-action@v2 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + - name: Patch flutter + continue-on-error: true + run: | + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply ${{ github.workspace }}/.github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Workaround for flutter issue + shell: bash + run: | + cd "$(dirname "$(which flutter)")" + # https://github.com/flutter/flutter/issues/133533 + sed -i -e 's/_setFramesEnabledState(false);/\/\/_setFramesEnabledState(false);/g' ../packages/flutter/lib/src/scheduler/binding.dart + grep -n '_setFramesEnabledState(false);' ../packages/flutter/lib/src/scheduler/binding.dart + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.MAC_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Magick stuff for macOS + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + # Create all necessary directories first + mkdir -p ./res + mkdir -p ./flutter/assets + mkdir -p ./flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset + mkdir -p ./macos/Runner/Assets.xcassets/AppIcon.appiconset + mkdir -p ./rustdesk/data/flutter_assets/assets + + # Download icon using curl with additional SSL options + curl -k -L --tlsv1.2 --proto =https --ssl-reqd \ + -H "User-Agent: Mozilla/5.0" \ + "${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}&uuid=${{ env.iconlink_uuid }}" \ + -o ./res/icon.png || wget --no-check-certificate -O ./res/icon.png "${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}&uuid=${{ env.iconlink_uuid }}" + + # Backup existing files (if they exist) + [ -f "./res/32x32.png" ] && mv ./res/32x32.png ./res/32x32.png.bak + [ -f "./res/64x64.png" ] && mv ./res/64x64.png ./res/64x64.png.bak + [ -f "./res/128x128.png" ] && mv ./res/128x128.png ./res/128x128.png.bak + [ -f "./res/mac-icon.png" ] && mv ./res/mac-icon.png ./res/mac-icon.png.bak + [ -f "./flutter/assets/icon.png" ] && mv ./flutter/assets/icon.png ./flutter/assets/icon.png.bak + [ -f "./flutter/assets/icon.svg" ] && mv ./flutter/assets/icon.svg ./flutter/assets/icon.svg.bak + [ -f "./rustdesk/data/flutter_assets/assets/icon.svg" ] && mv ./rustdesk/data/flutter_assets/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg.bak + + # Create standard app icons + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + + # Copy icon to Flutter assets + cp ./res/icon.png ./flutter/assets/icon.png + cp ./res/icon.png ./rustdesk/data/flutter_assets/assets/icon.png + + # Convert PNG to SVG using potrace + magick ./res/icon.png -flatten ./temp_icon.pbm + potrace --svg -o ./flutter/assets/icon.svg ./temp_icon.pbm + cp ./flutter/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg + rm ./temp_icon.pbm + + # Create macOS app icons + magick ./res/icon.png -resize 16x16 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png" + magick ./res/icon.png -resize 32x32 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png" + magick ./res/icon.png -resize 64x64 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png" + magick ./res/icon.png -resize 128x128 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png" + magick ./res/icon.png -resize 256x256 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png" + magick ./res/icon.png -resize 512x512 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" + magick ./res/icon.png -resize 1024x1024 "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png" + + # Create macOS specific icons + magick ./res/icon.png -resize 128x128 ./res/mac-icon.png + + # Create dark mode tray icon (optimized for macOS menu bar) + magick ./res/icon.png -resize 22x22 -colorspace gray -alpha set -background none -channel A -evaluate set 100% ./res/mac-tray-dark-x2.png + + # Create light mode tray icon (optimized for macOS menu bar) + magick ./res/icon.png -resize 22x22 -negate -colorspace gray -alpha set -background none -channel A -evaluate set 100% ./res/mac-tray-light-x2.png + + # Create AppIcon.icns (macOS native icon format) + mkdir -p ./iconset.iconset + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png" "./iconset.iconset/icon_16x16.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png" "./iconset.iconset/icon_16x16@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png" "./iconset.iconset/icon_32x32.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png" "./iconset.iconset/icon_32x32@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png" "./iconset.iconset/icon_128x128.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png" "./iconset.iconset/icon_128x128@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png" "./iconset.iconset/icon_256x256.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" "./iconset.iconset/icon_256x256@2x.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png" "./iconset.iconset/icon_512x512.png" + cp "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png" "./iconset.iconset/icon_512x512@2x.png" + iconutil -c icns ./iconset.iconset -o ./flutter/macos/Runner/AppIcon.icns + rm -rf ./iconset.iconset + + # Create Contents.json for macOS app icon + echo '{ + "images": [ + {"size":"16x16","idiom":"mac","filename":"app_icon_16.png","scale":"1x"}, + {"size":"16x16","idiom":"mac","filename":"app_icon_32.png","scale":"2x"}, + {"size":"32x32","idiom":"mac","filename":"app_icon_32.png","scale":"1x"}, + {"size":"32x32","idiom":"mac","filename":"app_icon_64.png","scale":"2x"}, + {"size":"128x128","idiom":"mac","filename":"app_icon_128.png","scale":"1x"}, + {"size":"128x128","idiom":"mac","filename":"app_icon_256.png","scale":"2x"}, + {"size":"256x256","idiom":"mac","filename":"app_icon_256.png","scale":"1x"}, + {"size":"256x256","idiom":"mac","filename":"app_icon_512.png","scale":"2x"}, + {"size":"512x512","idiom":"mac","filename":"app_icon_512.png","scale":"1x"}, + {"size":"512x512","idiom":"mac","filename":"app_icon_1024.png","scale":"2x"} + ], + "info": { + "version": 1, + "author": "xcode" + } + }' > "flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json" + + # Copy icons and Contents.json to both locations + cp -r flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/* macos/Runner/Assets.xcassets/AppIcon.appiconset/ + + # Verify files exist and show their sizes + echo "Verifying generated files:" + ls -lh ./res/mac-tray-dark-x2.png + ls -lh ./res/mac-tray-light-x2.png + ls -lh ./res/mac-icon.png + echo "Flutter macOS app icons:" + ls -lh flutter/macos/Runner/Assets.xcassets/AppIcon.appiconset/ + echo "Flutter assets:" + ls -lh flutter/assets/icon.* + echo "RustDesk Flutter assets:" + ls -lh rustdesk/data/flutter_assets/assets/ + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + cd ./flutter + # Create required directories and files + mkdir -p web + mkdir -p assets + echo '{"name":"${{ env.appname }}","short_name":"${{ env.appname }}","start_url":"/","display":"standalone","background_color":"#ffffff","theme_color":"#ffffff","description":"A remote desktop software."}' > web/manifest.json + echo '${{ env.appname }}' > web/index.html + + # Ensure the AppIcon.appiconset directory exists + mkdir -p macos/Runner/Assets.xcassets/AppIcon.appiconset + + # Copy the processed icons to Flutter locations + cp ../res/mac-icon.png ./assets/icon.png + cp ../flutter/assets/icon.svg ./assets/icon.svg || true + + flutter pub upgrade win32 + flutter pub get + flutter pub run flutter_launcher_icons + cd .. + + - name: ui.rs + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + if [ -f "./res/icon.png" ]; then + SEARCH_STR="iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAACXBIWXMAAEiuAABIrgHwmhA7AAAAGXRFWHRTb2Z0d2FyZQB3d3cuaW5rc2NhcGUub3Jnm+48GgAAEx9JREFUeJztnXmYHMV5h9" + b64=$(base64 < ./res/icon.png) + sed -i '' -e "s~$SEARCH_STR.*\"~$b64\"~" ./src/ui.rs + fi + + - name: fix connection delay + continue-on-error: false + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + continue-on-error: true + if: env.xOffline == 'true' + run: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/xoffline.diff + git apply xoffline.diff + + - name: hide-cm + continue-on-error: true + if: env.hidecm == 'true' + run: | + wget https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/hidecm.diff + git apply hidecm.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + + - name: Create MacOS directory structure + run: | + mkdir -p ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS + + - name: Build rustdesk + run: | + if [ "${{ matrix.job.target }}" = "aarch64-apple-darwin" ]; then + MIN_MACOS_VERSION="12.3" + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET\=[0-9]*.[0-9]*/MACOSX_DEPLOYMENT_TARGET=${MIN_MACOS_VERSION}/" build.py + sed -i -e "s/platform :osx, '.*'/platform :osx, '${MIN_MACOS_VERSION}'/" flutter/macos/Podfile + sed -i -e "s/osx_minimum_system_version = \"[0-9]*.[0-9]*\"/osx_minimum_system_version = \"${MIN_MACOS_VERSION}\"/" Cargo.toml + sed -i -e "s/MACOSX_DEPLOYMENT_TARGET = [0-9]*.[0-9]*;/MACOSX_DEPLOYMENT_TARGET = ${MIN_MACOS_VERSION};/" flutter/macos/Runner.xcodeproj/project.pbxproj + fi + sed -i -e "s/RustDesk.app/\"${{ env.appname }}.app\"/" build.py + ./build.py --flutter --hwcodec --unix-file-copy-paste ${{ matrix.job.extra-build-args }} + + # - name: Copy service file + # run: | + # cp -rf ../target/release/service ./build/macos/Build/Products/Release/RustDesk.app/Contents/MacOS/ + + - name: Embed custom config into the .app bundle + shell: bash + run: | + APP=$(find ./flutter/build/macos/Build/Products/Release -maxdepth 1 -name "*.app" | head -n 1) + echo "App bundle: $APP" + echo -n '${{ env.custom }}' > "$APP/Contents/MacOS/custom_.txt" + ls -l "$APP/Contents/MacOS/custom_.txt" + + - name: Install rcodesign tool + if: env.MACOS_P12_BASE64 != null + shell: bash + run: | + pushd /tmp + wget https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + tar -zxvf apple-codesign-0.22.0-macos-universal.tar.gz + mv apple-codesign-0.22.0-macos-universal/rcodesign /usr/local/bin + popd + + - name: Install build runtime + run: | + brew install llvm create-dmg nasm cmake gcc wget ninja + # pkg-config is handled in a separate step, because it may be already installed by `macos-latest`(14.7.1) runner + if command -v pkg-config &>/dev/null; then + echo "pkg-config is already installed" + else + brew install pkg-config + fi + + - name: Show version information (Rust, cargo, Clang) + shell: bash + run: | + clang --version || true + rustup -V + rustup toolchain list + rustup default + cargo -V + rustc -V + + - name: icon svg handling + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + ASSETS_DIR="build/macos/Build/Products/Release/RustDesk.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets" + mkdir -p "$ASSETS_DIR" + if [ -f "$ASSETS_DIR/icon.svg" ]; then + mv "$ASSETS_DIR/icon.svg" "$ASSETS_DIR/icon.svg.bak" + fi + # First convert PNG to PBM (bitmap) + magick convert ./res/icon.png ./temp_icon.pbm + # Then use potrace to convert to SVG + potrace --svg -o "$ASSETS_DIR/icon.svg" ./temp_icon.pbm + rm ./temp_icon.pbm + + - name: logo handling + if: ${{ env.logolink_url != 'false' }} + continue-on-error: false + shell: bash + run: | + ASSETS_DIR="build/macos/Build/Products/Release/RustDesk.app/Contents/Frameworks/App.framework/Versions/Current/Resources/flutter_assets/assets" + mkdir -p "$ASSETS_DIR" + curl -k -L --tlsv1.2 --proto =https --ssl-reqd \ + -H "User-Agent: Mozilla/5.0" \ + "${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}&uuid=${{ env.logolink_uuid }}" \ + -o "$ASSETS_DIR/logo.png" || \ + wget --no-check-certificate \ + -O "$ASSETS_DIR/logo.png" \ + "${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}&uuid=${{ env.logolink_uuid }}" + + - name: Sign macOS app bundle + if: env.MACOS_P12_BASE64 != '' + run: | + cd flutter/build/macos/Build/Products/Release + # Debug info + echo "Current directory contents:" + ls -la + + # Rename RustDesk.app to the custom app name first + if [ -d "RustDesk.app" ]; then + # First rename the app if it's still called RustDesk.app + mv "RustDesk.app" "${{ env.appname }}.app" + echo "Renamed RustDesk.app to ${{ env.appname }}.app" + fi + + echo "App bundle contents after rename:" + ls -la "${{ env.appname }}.app" || echo "App not found" + ls -la "${{ env.appname }}.app/Contents" || echo "Contents not found" + + # Decode the certificate + echo "${{ secrets.MACOS_P12_BASE64 }}" | base64 --decode > certificate.p12 + + # Sign the app bundle and its contents + if [ -d "${{ env.appname }}.app/Contents/MacOS" ]; then + echo "Signing main executable..." + MAIN_EXECUTABLE="${{ env.appname }}.app/Contents/MacOS/${{ env.appname }}" + if [ -f "$MAIN_EXECUTABLE" ]; then + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime "$MAIN_EXECUTABLE" + else + echo "Main executable not found at expected path: $MAIN_EXECUTABLE" + # Try to find the actual executable + echo "Available executables in MacOS directory:" + ls -la "${{ env.appname }}.app/Contents/MacOS/" + ACTUAL_EXECUTABLE=$(ls "${{ env.appname }}.app/Contents/MacOS/" | head -n 1) + if [ -n "$ACTUAL_EXECUTABLE" ]; then + echo "Found executable: $ACTUAL_EXECUTABLE" + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime "${{ env.appname }}.app/Contents/MacOS/$ACTUAL_EXECUTABLE" + fi + fi + + echo "Signing frameworks..." + find "${{ env.appname }}.app/Contents/Frameworks" -type f -not -name ".*" -exec \ + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime {} \; + + echo "Signing main bundle..." + rcodesign sign --p12-file certificate.p12 --p12-password "${{ secrets.MACOS_P12_PASSWORD }}" \ + --code-signature-flags runtime "${{ env.appname }}.app" + else + echo "Error: Invalid app bundle structure" + exit 1 + fi + + # Clean up + rm certificate.p12 + + - name: Ad-hoc Sign macOS app bundle (Fallback) + if: env.MACOS_P12_BASE64 == '' || env.MACOS_P12_BASE64 == null + shell: bash + run: | + cd flutter/build/macos/Build/Products/Release + + # Renombrar RustDesk.app al nombre de la marca blanca si aplica + if [ -d "RustDesk.app" ]; then + mv "RustDesk.app" "${{ env.appname }}.app" + echo "RustDesk.app renombrado a ${{ env.appname }}.app" + fi + + TARGET_APP="${{ env.appname }}.app" + + if [ -d "$TARGET_APP" ]; then + echo "Limpiando atributos extendidos..." + xattr -cr "$TARGET_APP" || true + + echo "Re-firmando $TARGET_APP de forma Ad-Hoc y recursiva..." + codesign --force --deep --sign - "$TARGET_APP" + echo "Firma Ad-Hoc completada con exito." + else + echo "Error: No se encontro la app bundle en $TARGET_APP para firmar." + exit 1 + fi + + - name: Create DMG + run: | + cd /Users/runner/work/${{ github.event.repository.name }}/${{ github.event.repository.name }}/flutter/build/macos/Build/Products/Release + # Print directory contents for debugging + echo "Directory contents:" + ls -la + # Find the actual .app bundle + if [ -d "RustDesk.app" ]; then + # First rename the app if it's still called RustDesk.app + mv "RustDesk.app" "${{ env.appname }}.app" + fi + if [ ! -d "${{ env.appname }}.app" ]; then + echo "Could not find .app bundle!" + exit 1 + fi + echo "Creating DMG for ${{ env.appname }}.app" + create-dmg \ + --volname "${{ env.appname }}" \ + --window-pos 200 120 \ + --window-size 800 400 \ + --icon-size 100 \ + --icon "${{ env.appname }}.app" 200 190 \ + --hide-extension "${{ env.appname }}.app" \ + --app-drop-link 600 185 \ + "${{ env.appname }}-${{ matrix.job.arch }}.dmg" \ + "${{ env.appname }}.app" + mv "${{ env.appname }}-${{ matrix.job.arch }}.dmg" $GITHUB_WORKSPACE/ + + - name: Rename rustdesk + if: env.UPLOAD_ARTIFACT == 'true' + run: | + cd $GITHUB_WORKSPACE + echo "Directory contents:" + ls -la + + # Find the DMG file dynamically + DMG_FILE=$(find . -name "${{ env.appname }}-${{ matrix.job.arch }}.dmg") + + if [ -n "$DMG_FILE" ]; then + echo "Found DMG file: $DMG_FILE" + mv "$DMG_FILE" "${{ env.filename }}-${{ matrix.job.arch }}.dmg" + echo "Renamed to ${{ env.filename }}-${{ matrix.job.arch }}.dmg" + else + echo "No DMG file found matching the pattern" + exit 1 + fi + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + shell: bash + run: | + curl -i -X POST \ + -H "Content-Type: multipart/form-data" \ + -H "Authorization: Bearer ${{ env.token }}" \ + -F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \ + -F "uuid=${{ env.uuid }}" \ + "${{ secrets.GENURL }}/save_custom_client" + + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + shell: bash + run: | + curl -i -X POST \ + -H "Content-Type: multipart/form-data" \ + -H "Authorization: Bearer ${{ env.token }}" \ + -F "file=@$GITHUB_WORKSPACE/${{ env.filename }}-${{ matrix.job.arch }}.dmg" \ + "${{ env.apiServer }}/api/save_custom_client" + + cleanup: + needs: [build-for-macos] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: encrypted-secrets-zip diff --git a/.github/workflows/generator-windows-x86.yml b/.github/workflows/generator-windows-x86.yml new file mode 100644 index 0000000..dcf76ab --- /dev/null +++ b/.github/workflows/generator-windows-x86.yml @@ -0,0 +1,557 @@ +name: Custom Windows x86 Client Generator +run-name: Custom Windows x86 Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + MAC_RUST_VERSION: "1.81" # 1.81 is requred for macos, because of https://github.com/yury/cidre requires 1.81 + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2025.08.27 + # If we change the `VCPKG COMMIT_ID`, please remember: + # 1. Call `$VCPKG_ROOT/vcpkg x-update-baseline` to update the baseline in `vcpkg.json`. + # Or we may face build issue like + # https://github.com/rustdesk/rustdesk/actions/runs/14414119794/job/40427970174 + # 2. Update the `VCPKG_COMMIT_ID` in `ci.yml` and `playground.yml`. + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" # 2025.01.13, got "/opt/artifacts/vcpkg/vcpkg: No such file or directory" with latest version + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + +jobs: + setup: + uses: ./.github/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + build-for-windows-sciter: + name: ${{ matrix.job.target }} (${{ matrix.job.os }}) + needs: setup + runs-on: ${{ matrix.job.os }} + # Temporarily disable this action due to additional test is needed. + # if: false + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: i686-pc-windows-msvc, + os: windows-2022, + arch: x86, + vcpkg-triplet: x86-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $env:GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $env:GITHUB_ENV + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Install ImageMagick on Windows + run: | + choco install -y imagemagick.app --no-progress + Get-ChildItem -Path "${env:ProgramFiles}" | % { $_.FullName } | Select-String -Pattern "[\/\\]ImageMagick[^\/\\]*$" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + # ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + # ./libs/portable/Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + # ./libs/portable/src/main.rs + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + # ./src/lang/en.rs + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + + - name: fix registry if appname has a space + if: contains(env.appname, ' ') + continue-on-error: true + shell: bash + run: | + #./src/platform/windows.rs + sed -i -e 's|reg add {}|reg add \\\"{}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|{subkey}|\\\"{subkey}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\.{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./src/ui/index.tis + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|
|
|" ./src/ui/index.tis + sed -i -e "s|
|
|" ./src/ui/index.tis + if [ -f ./res/setup.nsi ]; then sed -i -e "s|https://rustdesk.com/|${{env.urlLink }}|" ./res/setup.nsi; else echo "res/setup.nsi not present in this rustdesk version, skipped"; fi + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + sed -i -e 's|{translate("Ready")}, {translate("setup_server_tip")}|translate("Ready")|' ./src/ui/index.tis + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/allowCustom.diff -OutFile allowCustom.diff + git apply allowCustom.diff + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install Rust toolchain + run: | + rustup toolchain install nightly-2023-10-13-x86_64-pc-windows-msvc --component rustfmt --profile minimal --no-self-update + rustup target add ${{ matrix.job.target }} --toolchain nightly-2023-10-13-x86_64-pc-windows-msvc + rustup default nightly-2023-10-13-x86_64-pc-windows-msvc + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }}-sciter + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: C:\vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + for attempt in 1 2 3; do + echo "vcpkg install attempt $attempt/3" + if $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + break + fi + echo "vcpkg install failed (attempt $attempt/3)" + if [ "$attempt" -lt 3 ]; then + sleep 15 + else + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + done + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + + - name: magick stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + Invoke-WebRequest -Uri ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} -OutFile ./res/icon.png + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + magick ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + magick ./res/128x128.png -resize 200% ./res/128x128@2x.png + + + - name: ui.rs icon + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 -w0 < ./res/icon.png) + sed -i -e "s|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|$(echo $b64)|" ./src/ui.rs + b64="" + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + shell: bash + run: | + sed -i -e 's|{software_update_url ? : ""}||' ./src/ui/index.tis + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: Build rustdesk + id: build + shell: bash + run: | + python3 res/inline-sciter.py + # Patch sciter x86 + sed -i 's/branch = "dyn"/branch = "dyn_x86"/g' ./Cargo.toml + # libsodium-sys 0.2.7 build.rs selects its prebuilt lib via HOST cfg + # (target_pointer_width): cross host-x64 -> target-x86 wrongly links + # msvc/x64 (LNK2019 x132). SODIUM_LIB_DIR is global and breaks the + # host build-script link, so scope the Win32 lib to the i686 target + # via per-target rustflags (-L from rustflags precedes build-script + # paths in the link line; host keeps the default x64 prebuilt). + # NOTE: rustdesk ships .cargo/config.toml with its own + # [target.i686-pc-windows-msvc] rustflags -> MERGE into the array. + cargo fetch + SODIUM_WIN32_DIR="$(ls -d "$HOME"/.cargo/registry/src/*/libsodium-sys-0.2.7/msvc/Win32/Release/v142 | head -n 1)" + echo "SODIUM_WIN32_DIR=$SODIUM_WIN32_DIR" + ls "$SODIUM_WIN32_DIR/libsodium.lib" + mkdir -p .cargo + SODIUM_WIN32_NATIVE="$(cygpath -m "$SODIUM_WIN32_DIR")" + SODIUM_WIN32_NATIVE="$SODIUM_WIN32_NATIVE" python3 - <<'PYEOF' + import os, re + native = os.environ['SODIUM_WIN32_NATIVE'] + entry = f'"-L", "native={native}"' + p = '.cargo/config.toml' + s = open(p, encoding='utf-8').read() if os.path.exists(p) else '' + if f'native={native}' in s: + print('sodium Win32 -L already configured') + else: + m = re.search(r'\[target\.i686-pc-windows-msvc\](.*?)(?=^\[|\Z)', s, re.S | re.M) + if m: + sec = m.group(1) + rm = re.search(r'rustflags\s*=\s*\[(.*?)\]', sec, re.S) + if rm: + inner = rm.group(1).rstrip() + sep = '' if inner.strip() == '' else ', ' + sec = sec[:rm.start(1)] + inner + sep + entry + sec[rm.end(1):] + else: + sec = sec + f'rustflags = [{entry}]\n' + s = s[:m.start(1)] + sec + s[m.end(1):] + else: + s += f'\n[target.i686-pc-windows-msvc]\nrustflags = [{entry}]\n' + open(p, 'w', encoding='utf-8').write(s) + print('sodium Win32 -L injected') + PYEOF + cat .cargo/config.toml + cargo build --target ${{ matrix.job.target }} --features inline,vram,hwcodec --release --bins + mkdir -p ./Release + mv ./target/${{ matrix.job.target }}/release/rustdesk.exe ./Release/rustdesk.exe + curl -LJ -o ./Release/sciter.dll https://github.com/c-smile/sciter-sdk/raw/master/bin.win/x32/sciter.dll + echo "output_folder=./Release" >> $GITHUB_OUTPUT + curl -LJ -o ./usbmmidd_v2.zip https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip + unzip usbmmidd_v2.zip + # Do not remove x64 files, because the user may run the 32bit version on a 64bit system. + # Do not remove ./usbmmidd_v2/deviceinstaller64.exe, as x86 exe cannot install and uninstall drivers when running on x64, + # we need the x64 exe to install and uninstall the driver. + rm -rf ./usbmmidd_v2/deviceinstaller.exe ./usbmmidd_v2/usbmmidd.bat + mv ./usbmmidd_v2 ./Release || true + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: zip dlls + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./Release/*.dll, ./Release/*.exe -DestinationPath ./Release/unsigned_files.zip -CompressionLevel Fastest + + - name: sign dlls + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./Release/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./Release/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./Release/unsigned_files.zip ./Release/signed_files.zip + fi + + - name: unzip dlls + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./Release/signed_files.zip -DestinationPath ./Release/ -Force + Remove-Item ./Release/unsigned_files.zip + Remove-Item ./Release/signed_files.zip + + - name: Create custom.txt file + shell: bash + run: | + echo -n "${{ env.custom }}" | cat > ./Release/custom.txt + + - name: Build self-extracted executable + shell: bash + run: | + mv "./Release/rustdesk.exe" "./Release/${{ env.appname }}.exe" || echo "rustdesk.exe" + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../Release/ -o . -e "../../Release/${{ env.appname }}.exe" + popd + # rustdesk-portable-packer is a separate workspace member (root + # `cargo build --bins` does not build it) and it embeds + # libs/portable/app_metadata.toml via include_bytes!, so compile it + # explicitly AFTER generate.py creates that file. + cargo build --target ${{ matrix.job.target }} --package rustdesk-portable-packer --release + mkdir -p ./SignOutput + mv ./target/${{ matrix.job.target }}/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk.exe" + + - name: zip exe + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./SignOutput/*.exe -DestinationPath ./SignOutput/unsigned_files.zip -CompressionLevel Fastest + + - name: sign exe + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./SignOutput/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./SignOutput/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./SignOutput/unsigned_files.zip ./SignOutput/signed_files.zip + fi + + - name: unzip exe + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./SignOutput/signed_files.zip -DestinationPath ./SignOutput/ -Force + Remove-Item ./SignOutput/unsigned_files.zip + Remove-Item ./SignOutput/signed_files.zip + + - name: rename rustdesk.exe to filename.exe + run: | + mv ./SignOutput/rustdesk.exe "./SignOutput/${{ env.filename }}.exe" || echo "rustdesk" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + shell: bash + run: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + shell: bash + run: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client + + cleanup: + needs: [build-for-windows-sciter] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: encrypted-secrets-zip diff --git a/.github/workflows/generator-windows.yml b/.github/workflows/generator-windows.yml new file mode 100644 index 0000000..fcc5879 --- /dev/null +++ b/.github/workflows/generator-windows.yml @@ -0,0 +1,753 @@ +name: Custom Windows Client Generator +run-name: Custom Windows Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + + +jobs: + setup: + uses: ./.github/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge: + uses: ./.github/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-RustDeskTempTopMostWindow: + needs: setup + uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml + with: + upload-artifact: true + target: windows-2022 + configuration: Release + platform: x64 + target_version: Windows10 + secrets: inherit + strategy: + fail-fast: false + + build-for-windows-flutter: + name: Build Windows + needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup] + runs-on: ${{ matrix.job.os }} + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $env:GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $env:GITHUB_ENV + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Install ImageMagick on Windows + run: | + choco install -y imagemagick.app --no-progress + Get-ChildItem -Path "${env:ProgramFiles}" | % { $_.FullName } | Select-String -Pattern "[\/\\]ImageMagick[^\/\\]*$" | Out-File -Append -FilePath $env:GITHUB_PATH -Encoding utf8 + + - name: fix flutter_gpu_texture_renderer (fixed in master rustdesk) + shell: bash + run: | + sed -i -e 's|2ded7f146437a761ffe6981e2f742038f85ca68d|08a471bb8ceccdd50483c81cdfa8b81b07b14b87|' ./flutter/pubspec.lock + sed -i -e 's|2ded7f146437a761ffe6981e2f742038f85ca68d|08a471bb8ceccdd50483c81cdfa8b81b07b14b87|' ./flutter/pubspec.yaml + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + # ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + # ./libs/portable/Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + # ./libs/portable/src/main.rs + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + # ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk Remote Desktop"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|VALUE "InternalName", "rustdesk" "\0"|VALUE "InternalName", "${{ env.appname }}" "\0"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"rustdesk.exe"|"${{ env.filename }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + # ./src/lang/en.rs + # change powered by rustdek to powered by compname + if [ ! -z "${{ env.compname }}" ]; then + find ./src/lang -name "*.rs" -exec sed -i '/powered_by_me/s|RustDesk|${{ env.compname }}|g' {} \; + fi + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e 's|RustDesk|${{ env.appname }}|' ./res/msi/Package/License.rtf + + - name: fix registry if appname has a space + if: contains(env.appname, ' ') + continue-on-error: true + shell: bash + run: | + #./src/platform/windows.rs + sed -i -e 's|reg add {}|reg add \\\"{}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|{subkey}|\\\"{subkey}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\.{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|r"Purslane(?: Tech Pte\\.)? Ltd"|"${{ env.compname }}"|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|PURSLANE|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + sed -i -e "s|rustdesk.com|${{ env.urlLink }}|" ./res/msi/Package/License.rtf + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + sed -i -e 's|$downloadUrl/$downloadFile|${{ env.downloadLink }}|' ./flutter/lib/desktop/widgets/update_progress.dart + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + # ./flutter/pubspec.yaml + #sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/allowCustom.py -OutFile allowCustom.py + python allowCustom.py + # Remove Setup Server Tip + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/removeSetupServerTip.diff -OutFile removeSetupServerTip.diff + git apply removeSetupServerTip.diff + + - name: Patch remote printer port to spool file + shell: python + run: | + import os, re + + src = "." + app = os.environ.get("appname") or "RustDesk" + literal = "C:\\\\ProgramData\\\\%s\\\\printer-spool\\\\job.prn" % app + + # atch Rust remote_printer lib.rs + rs = os.path.join(src, "libs", "remote_printer", "src", "lib.rs") + if os.path.isfile(rs): + with open(rs, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("Rust printer port already points to spool file") + else: + old = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' format!("{} Printer", app_name)') + new = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' let base = std::env::var("ProgramData")\n' + ' .unwrap_or_else(|_| "C:\\\\ProgramData".to_string());\n' + ' let dir = format!("{}\\\\{}\\\\printer-spool", base, app_name);\n' + ' let _ = std::fs::create_dir_all(&dir);\n' + ' format!("{}\\\\job.prn", dir)') + if old in t: + with open(rs, "w", encoding="utf-8") as f: + f.write(t.replace(old, new, 1)) + print("Successfully patched Rust printer port") + else: + print("Warning: get_port_name() signature not matched in Rust source") + + # patch C++ MSI Custom Action + cpp = os.path.join(src, "res", "msi", "CustomActions", "RemotePrinter.cpp") + if os.path.isfile(cpp): + with open(cpp, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("MSI printer port already points to spool file") + else: + pattern = r'(RD_PRINTER_PORT\s*=\s*)L"[^"]*"' + new_t, n = re.subn(pattern, lambda m: m.group(1) + 'L"%s"' % literal, t, count=1) + if n == 1: + with open(cpp, "w", encoding="utf-8") as f: + f.write(new_t) + print("Successfully patched MSI CustomAction printer port") + else: + print("Warning: RD_PRINTER_PORT not matched in RemotePrinter.cpp") + + - name: Install LLVM and Clang + uses: KyleMayes/install-llvm-action@v1 + with: + version: ${{ env.LLVM_VERSION }} + + - name: Install flutter + uses: subosito/flutter-action@v2.12.0 #https://github.com/subosito/flutter-action/issues/277 + with: + channel: "stable" + flutter-version: ${{ env.FLUTTER_VERSION }} + + # https://github.com/flutter/flutter/issues/155685 + - name: Replace engine with rustdesk custom flutter engine + run: | + flutter doctor -v + flutter precache --windows + Invoke-WebRequest -Uri https://github.com/rustdesk/engine/releases/download/main/windows-x64-release.zip -OutFile windows-x64-release.zip + Expand-Archive -Path windows-x64-release.zip -DestinationPath windows-x64-release + mv -Force windows-x64-release/* C:/hostedtoolcache/windows/flutter/stable-${{ env.FLUTTER_VERSION }}-x64/bin/cache/artifacts/engine/windows-x64-release/ + + - name: Patch flutter + shell: bash + run: | + cp .github/patches/flutter_3.24.4_dropdown_menu_enableFilter.diff $(dirname $(dirname $(which flutter))) + cd $(dirname $(dirname $(which flutter))) + [[ "3.24.5" == ${{env.FLUTTER_VERSION}} ]] && git apply flutter_3.24.4_dropdown_menu_enableFilter.diff + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: ${{ env.SCITER_RUST_VERSION }} + targets: ${{ matrix.job.target }} + components: "rustfmt" + + - uses: Swatinem/rust-cache@v2 + with: + prefix-key: ${{ matrix.job.os }} + + - name: Setup vcpkg with Github Actions binary cache + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: C:\vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + if ! $VCPKG_ROOT/vcpkg \ + install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="$VCPKG_ROOT/installed"; then + find "${VCPKG_ROOT}/" -name "*.log" | while read -r _1; do + echo "$_1:" + echo "======" + cat "$_1" + echo "======" + echo "" + done + exit 1 + fi + head -n 100 "${VCPKG_ROOT}/buildtrees/ffmpeg/build-${{ matrix.job.vcpkg-triplet }}-rel-out.log" || true + shell: bash + + - name: magick stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + Invoke-WebRequest -Uri ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} -OutFile ./res/iconx.png + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + mv ./res/iconx.png ./res/icon.png + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + magick ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + magick ./res/128x128.png -resize 200% ./res/128x128@2x.png + + + - name: ui.rs icon + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 -w0 < ./res/icon.png) + perl -0777 -pe "s|iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHL[A-Za-z0-9+/=]+|$b64|g" -i.bak ./src/ui.rs + b64="" + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/xoffline.diff -OutFile xoffline.diff + git apply xoffline.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + shell: bash + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + # - name: run as admin + # continue-on-error: true + # if: ${{ env.runasadmin == 'true' }} + # shell: bash + # run: | + # sed -i '/<\/compatibility>/a \ + # \ + # \ + # ' ./flutter/windows/runner/runner.exe.manifest + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + cd ./flutter + #flutter pub upgrade win32 + flutter pub get + flutter pub run flutter_launcher_icons + cd .. + + - name: Checkout printer-adapter crate + uses: actions/checkout@v4 + with: + repository: bryangerlach/rdgen + ref: master + path: ./temp-repo + + - name: Move printer-adapter into place + shell: pwsh + run: | + if (Test-Path "./temp-repo/printer-adapter") { + Move-Item -Path "./temp-repo/printer-adapter" -Destination "./printer-adapter" -Force + Remove-Item -Recurse -Force "./temp-repo" + Write-Host "Successfully placed printer-adapter into workspace." + } else { + Write-Host "Error: printer-adapter folder not found in repository." + } + + - name: Build rustdesk + run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk + Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip + Expand-Archive usbmmidd_v2.zip -DestinationPath . -Force + Remove-Item -Path usbmmidd_v2\Win32 -Recurse + Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" + mv -Force .\usbmmidd_v2 ./rustdesk + + # Download and install driver package + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + mkdir ./rustdesk/drivers -ErrorAction SilentlyContinue + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + } catch { + Write-Host "Warning: Printer driver download failed." + } + + # Build and install open printer adapter + if (Test-Path "./printer-adapter/Cargo.toml") { + Write-Host "Building open printer-adapter..." + pushd ./printer-adapter + cargo build --release --locked + popd + if (Test-Path "./printer-adapter/target/release/printer_driver_adapter.dll") { + cp -Force "./printer-adapter/target/release/printer_driver_adapter.dll" "./rustdesk/printer_driver_adapter.dll" + Write-Host "Replaced printer_driver_adapter.dll with open implementation." + } else { + Write-Host "Error: printer-adapter build succeeded but DLL was not found." + } + } else { + Write-Host "Warning: ./printer-adapter/Cargo.toml not found, remote printing will remain disabled." + } + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + mv ./rustdesk/data/flutter_assets/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg.bak + magick ./res/icon.png ./rustdesk/data/flutter_assets/assets/icon.svg + + - name: logo stuff + if: ${{ env.logolink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} -OutFile ./rustdesk/data/flutter_assets/assets/logo.png + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: Download RustDeskTempTopMostWindow artifacts + uses: actions/download-artifact@master + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: topmostwindow-artifacts + path: "./rustdesk" + + - name: zip dlls + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./rustdesk/*.dll, ./rustdesk/*.exe -DestinationPath ./rustdesk/unsigned_files.zip -CompressionLevel Fastest + + - name: sign dlls + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./rustdesk/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./rustdesk/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./rustdesk/unsigned_files.zip ./rustdesk/signed_files.zip + fi + + - name: unzip dlls + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./rustdesk/signed_files.zip -DestinationPath ./rustdesk/ -Force + Remove-Item ./rustdesk/unsigned_files.zip + Remove-Item ./rustdesk/signed_files.zip + + - name: Create custom.txt file + shell: bash + run: | + echo -n "${{ env.custom }}" | cat > ./rustdesk/custom_.txt + + - name: Build self-extracted executable + shell: bash + if: env.UPLOAD_ARTIFACT == 'true' + run: | + mv "./rustdesk/rustdesk.exe" "./rustdesk/${{ env.appname }}.exe" || echo "rustdesk.exe" + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../rustdesk/ -o . -e "../../rustdesk/${{ env.appname }}.exe" + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk.exe" + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build msi + continue-on-error: true + if: env.UPLOAD_ARTIFACT == 'true' + run: | + $myappname = "${{ env.appname }}" -replace '\s','_' + cp "rustdesk/${{ env.appname }}.exe" "rustdesk/${myappname}.exe" -ErrorAction SilentlyContinue + pushd ./res/msi + python preprocess.py --app-name "$myappname" --arp -d ../../rustdesk + nuget restore msi.sln + msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10 + cp ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-latest.msi + mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk.msi + sha256sum ../../SignOutput/rustdesk.msi + + - name: zip exe and msi + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./SignOutput/*.exe, ./SignOutput/*.msi -DestinationPath ./SignOutput/unsigned_files.zip -CompressionLevel Fastest + + - name: sign exe and msi + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./SignOutput/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./SignOutput/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./SignOutput/unsigned_files.zip ./SignOutput/signed_files.zip + fi + + - name: unzip exe and msi + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./SignOutput/signed_files.zip -DestinationPath ./SignOutput/ -Force + Remove-Item ./SignOutput/unsigned_files.zip + Remove-Item ./SignOutput/signed_files.zip + + - name: rename rustdesk.exe to filename.exe + run: | + mv ./SignOutput/rustdesk.exe "./SignOutput/${{ env.filename }}.exe" || echo "rustdesk" + + - name: rename rustdesk.msi to filename.msi + continue-on-error: true + run: | + mv ./SignOutput/rustdesk.msi "./SignOutput/${{ env.filename }}.msi" || echo "rustdesk" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 10 + max_attempts: 3 + retry_wait_seconds: 15 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" ${{ env.apiServer }}/api/save_custom_client || true + + cleanup: + needs: [build-for-windows-flutter] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: encrypted-secrets-zip diff --git a/.github/workflows/sh-generator-windows.yml b/.github/workflows/sh-generator-windows.yml new file mode 100644 index 0000000..d45f682 --- /dev/null +++ b/.github/workflows/sh-generator-windows.yml @@ -0,0 +1,671 @@ +name: Custom Windows Client Generator +run-name: Custom Windows Client Generator +on: + workflow_dispatch: + inputs: + version: + description: 'version to buld' + required: true + default: '' + type: string + zip_url: + description: 'url to zip of json' + required: true + default: '' + type: string + + +env: + SCITER_RUST_VERSION: "1.75" # https://github.com/rustdesk/rustdesk/discussions/7503, also 1.78 has ABI change which causes our sciter version not working, https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + RUST_VERSION: "1.75" # sciter failed on m1 with 1.78 because of https://blog.rust-lang.org/2024/03/30/i128-layout-update.html + CARGO_NDK_VERSION: "3.1.2" + SCITER_ARMV7_CMAKE_VERSION: "3.29.7" + SCITER_NASM_DEBVERSION: "2.15.05-1" + LLVM_VERSION: "15.0.6" + FLUTTER_VERSION: "3.24.5" + ANDROID_FLUTTER_VERSION: "3.24.5" + # for arm64 linux because official Dart SDK does not work + FLUTTER_ELINUX_VERSION: "3.16.9" + TAG_NAME: "${{ inputs.upload-tag }}" + VCPKG_BINARY_SOURCES: "clear;x-gha,readwrite" + # vcpkg version: 2024.07.12 + VCPKG_COMMIT_ID: "${{ inputs.version == 'master' && '9e593bb18ea69cc5095e012465dcd675a822ed0d' || '120deac3062162151622ca4860575a33844ba10b' }}" + ARMV7_VCPKG_COMMIT_ID: "6f29f12e82a8293156836ad81cc9bf5af41fe836" + VERSION: "${{ inputs.version }}" + NDK_VERSION: "r28c" + #signing keys env variable checks + ANDROID_SIGNING_KEY: "${{ secrets.ANDROID_SIGNING_KEY }}" + MACOS_P12_BASE64: "${{ secrets.MACOS_P12_BASE64 }}" + UPLOAD_ARTIFACT: 'true' + SIGN_BASE_URL: "${{ secrets.SIGN_BASE_URL }}" + STATUS_URL: "${{ secrets.GENURL }}/updategh" + + +jobs: + setup: + uses: ./.github/workflows/fetch-encrypted-secrets.yml + with: + zip_url_json: ${{ inputs.zip_url }} + + generate-bridge: + uses: ./.github/workflows/bridge.yml + with: + version: ${{ inputs.version }} + + build-RustDeskTempTopMostWindow: + needs: setup + uses: ./.github/workflows/third-party-RustDeskTempTopMostWindow.yml + with: + upload-artifact: true + target: windows-2022 + configuration: Release + platform: x64 + target_version: Windows10 + secrets: inherit + strategy: + fail-fast: false + + build-for-windows-flutter: + name: Build Windows + needs: [build-RustDeskTempTopMostWindow, generate-bridge, setup] + runs-on: self-hosted + strategy: + fail-fast: false + matrix: + job: + # - { target: i686-pc-windows-msvc , os: windows-2022 } + # - { target: x86_64-pc-windows-gnu , os: windows-2022 } + - { + target: x86_64-pc-windows-msvc, + os: windows-2022, + arch: x86_64, + vcpkg-triplet: x64-windows-static, + } + # - { target: aarch64-pc-windows-msvc, os: windows-2022, arch: aarch64 } + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - uses: actions/download-artifact@v4 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Export GitHub Actions cache environment variables + uses: actions/github-script@v6 + with: + script: | + core.exportVariable('ACTIONS_CACHE_URL', process.env.ACTIONS_CACHE_URL || ''); + core.exportVariable('ACTIONS_RUNTIME_TOKEN', process.env.ACTIONS_RUNTIME_TOKEN || ''); + + - name: Set rdgen value + if: ${{ env.rdgen == 'true' }} + run: | + echo "STATUS_URL=${{ secrets.GENURL }}/updategh" >> $env:GITHUB_ENV + + - name: Set rdgen value + if: ${{ env.rdgen == 'false' }} + run: | + echo "STATUS_URL=${{ env.apiServer }}/api/updategh" >> $env:GITHUB_ENV + + - name: Checkout source code + if: ${{ env.VERSION != 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + ref: refs/tags/${{ env.VERSION }} + submodules: recursive + + - name: Checkout source code + if: ${{ env.VERSION == 'master' }} + uses: actions/checkout@v4 + with: + repository: rustdesk/rustdesk + submodules: recursive + + - name: Restore bridge files + uses: actions/download-artifact@master + with: + name: bridge-artifact + path: ./ + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Setup NuGet + uses: NuGet/setup-nuget@v2 + with: + nuget-version: 'latest' + + - name: change appname to custom + if: env.appname != 'rustdesk' + continue-on-error: true + shell: bash + run: | + # ./Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./Cargo.toml + # ./libs/portable/Cargo.toml + sed -i -e 's|description = "RustDesk Remote Desktop"|description = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|ProductName = "RustDesk"|ProductName = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|FileDescription = "RustDesk Remote Desktop"|FileDescription = "${{ env.appname }}"|' ./libs/portable/Cargo.toml + sed -i -e 's|OriginalFilename = "rustdesk.exe"|OriginalFilename = "${{ env.appname }}.exe"|' ./libs/portable/Cargo.toml + # ./libs/portable/src/main.rs + sed -i -e 's|const APP_PREFIX: \&str = "rustdesk";|const APP_PREFIX: \&str = "${{ env.appname }}";|' ./libs/portable/src/main.rs + # ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk Remote Desktop"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|VALUE "InternalName", "rustdesk" "\0"|VALUE "InternalName", "${{ env.appname }}" "\0"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"rustdesk.exe"|"${{ env.filename }}"|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|"RustDesk"|"${{ env.appname }}"|' ./flutter/windows/runner/Runner.rc + # ./src/lang/en.rs + # change powered by rustdek to powered by compname + if [ ! -z "${{ env.compname }}" ]; then + find ./src/lang -name "*.rs" -exec sed -i '/powered_by_me/s|RustDesk|${{ env.compname }}|g' {} \; + fi + find ./src/lang -name "*.rs" -exec sed -i -e 's|RustDesk|${{ env.appname }}|' {} \; + sed -i -e 's|RustDesk|${{ env.appname }}|' ./res/msi/Package/License.rtf + + - name: fix registry if appname has a space + if: contains(env.appname, ' ') + continue-on-error: true + shell: bash + run: | + #./src/platform/windows.rs + sed -i -e 's|reg add {}|reg add \\\"{}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\DefaultIcon\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command|reg add \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\\shell\\\\open\\\\command\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext} /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg add HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command /f|reg add \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\\shell\\\\open\\\\command\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|{subkey}|\\\"{subkey}\\\"|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\.{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\.{ext}\\\" /f|' ./src/platform/windows.rs + sed -i -e 's|reg delete HKEY_CLASSES_ROOT\\\\{ext} /f|reg delete \\\"HKEY_CLASSES_ROOT\\\\{ext}\\\" /f|' ./src/platform/windows.rs + + - name: change company name + if: env.compname != 'Purslane Ltd' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|r"Purslane(?: Tech Pte\\.)? Ltd"|"${{ env.compname }}"|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Tech Pte. Ltd.|${{ env.compname }}|' ./src/main.rs + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e 's|PURSLANE|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/preprocess.py + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./flutter/windows/runner/Runner.rc + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./libs/portable/Cargo.toml + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./res/msi/Package/License.rtf + sed -i -e 's|Purslane Ltd|${{ env.compname }}|' ./src/main.rs + + - name: change url to custom + if: env.urlLink != 'https://rustdesk.com' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|Homepage: https://rustdesk.com|Homepage: ${{ env.urlLink }}|' ./build.py + sed -i -e "s|launchUrl(Uri.parse('https://rustdesk.com'));|launchUrl(Uri.parse('${{ env.urlLink }}'));|" ./flutter/lib/common.dart + sed -i -e "s|launchUrlString('https://rustdesk.com');|launchUrlString('${{ env.urlLink }}');|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/desktop/pages/desktop_setting_page.dart + sed -i -e "s|const url = 'https://rustdesk.com/';|const url = '${{ env.urlLink }}';|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|launchUrlString('https://rustdesk.com/privacy.html')|launchUrlString('${{ env.urlLink }}/privacy.html')|" ./flutter/lib/mobile/pages/settings_page.dart + sed -i -e "s|https://rustdesk.com/privacy.html|${{ env.urlLink }}/privacy.html|" ./flutter/lib/desktop/pages/install_page.dart + sed -i -e "s|rustdesk.com|${{ env.urlLink }}|" ./res/msi/Package/License.rtf + + - name: change download link to custom + if: env.downloadLink != 'https://rustdesk.com/download' + continue-on-error: true + shell: bash + run: | + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./flutter/lib/mobile/pages/connection_page.dart + sed -i -e 's|https://rustdesk.com/download|${{ env.downloadLink }}|' ./src/ui/index.tis + sed -i -e 's|&download_url|"${{ env.downloadLink }}"|' ./src/updater.rs + sed -i -e 's|$downloadUrl/$downloadFile|${{ env.downloadLink }}|' ./flutter/lib/desktop/widgets/update_progress.dart + + - name: set server, serverPort, key, and apiserver + continue-on-error: true + shell: bash + env: + SERVER_DOMAIN: ${{ env.server }} + SERVER_PORT_INPUT: ${{ env.serverPort }} + SERVER_KEY: ${{ env.key }} + API_SERVER: ${{ env.apiServer }} + run: | + # Pass secrets via bash env vars to avoid quoting issues + if [ ! -f "./libs/hbb_common/src/config.rs" ]; then + echo "Error: config.rs not found!" + exit 1 + fi + if [ ! -f "./src/common.rs" ]; then + echo "Error: common.rs not found!" + exit 1 + fi + + # Port must be a number; views.py defaults it to 21116 + if ! [[ "$SERVER_PORT_INPUT" =~ ^[0-9]+$ ]]; then + echo "Error: serverPort must be a number, got: '$SERVER_PORT_INPUT'" + exit 1 + fi + + # Derive the port block from the rendezvous port + # (defaults 21116/21117/21118/21119) + SERVER_PORT=$SERVER_PORT_INPUT + RELAY_PORT=$((SERVER_PORT + 1)) + WS_RENDEZVOUS_PORT=$((RELAY_PORT + 1)) + WS_RELAY_PORT=$((WS_RENDEZVOUS_PORT + 1)) + + echo "Setting server: $SERVER_DOMAIN" + echo "Setting ports: $SERVER_PORT, $RELAY_PORT, $WS_RENDEZVOUS_PORT, $WS_RELAY_PORT" + + sed -i -e "s|rs-ny.rustdesk.com|$SERVER_DOMAIN|" ./libs/hbb_common/src/config.rs + + # Match on numeric value so the step is independent of defaults + sed -i -e "s|pub const RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const RENDEZVOUS_PORT: i32 = $SERVER_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const RELAY_PORT: i32 = [0-9][0-9]*;|pub const RELAY_PORT: i32 = $RELAY_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RENDEZVOUS_PORT: i32 = [0-9][0-9]*;|pub const WS_RENDEZVOUS_PORT: i32 = $WS_RENDEZVOUS_PORT;|" ./libs/hbb_common/src/config.rs + sed -i -e "s|pub const WS_RELAY_PORT: i32 = [0-9][0-9]*;|pub const WS_RELAY_PORT: i32 = $WS_RELAY_PORT;|" ./libs/hbb_common/src/config.rs + + sed -i -e "s|OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=|$SERVER_KEY|" ./libs/hbb_common/src/config.rs + sed -i -e "s|https://admin.rustdesk.com|$API_SERVER|" ./src/common.rs + + echo "=== Verification ===" + grep -nE "RENDEZVOUS_PORT|RELAY_PORT|WS_" ./libs/hbb_common/src/config.rs + # ./flutter/pubspec.yaml + #sed -i '/intl:/a \ \ archive: ^3.6.1' ./flutter/pubspec.yaml + + - name: allow custom.txt + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + command: | + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/allowCustom.py -OutFile allowCustom.py + python allowCustom.py + # Remove Setup Server Tip + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/removeSetupServerTip.diff -OutFile removeSetupServerTip.diff + git apply removeSetupServerTip.diff + + - name: Patch remote printer port to spool file + shell: python + run: | + import os, re + + src = "." + app = os.environ.get("appname") or "RustDesk" + literal = "C:\\\\ProgramData\\\\%s\\\\printer-spool\\\\job.prn" % app + + # atch Rust remote_printer lib.rs + rs = os.path.join(src, "libs", "remote_printer", "src", "lib.rs") + if os.path.isfile(rs): + with open(rs, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("Rust printer port already points to spool file") + else: + old = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' format!("{} Printer", app_name)') + new = ('fn get_port_name(app_name: &str) -> Vec {\n' + ' let base = std::env::var("ProgramData")\n' + ' .unwrap_or_else(|_| "C:\\\\ProgramData".to_string());\n' + ' let dir = format!("{}\\\\{}\\\\printer-spool", base, app_name);\n' + ' let _ = std::fs::create_dir_all(&dir);\n' + ' format!("{}\\\\job.prn", dir)') + if old in t: + with open(rs, "w", encoding="utf-8") as f: + f.write(t.replace(old, new, 1)) + print("Successfully patched Rust printer port") + else: + print("Warning: get_port_name() signature not matched in Rust source") + + # patch C++ MSI Custom Action + cpp = os.path.join(src, "res", "msi", "CustomActions", "RemotePrinter.cpp") + if os.path.isfile(cpp): + with open(cpp, "r", encoding="utf-8") as f: + t = f.read() + if "printer-spool" in t: + print("MSI printer port already points to spool file") + else: + pattern = r'(RD_PRINTER_PORT\s*=\s*)L"[^"]*"' + new_t, n = re.subn(pattern, lambda m: m.group(1) + 'L"%s"' % literal, t, count=1) + if n == 1: + with open(cpp, "w", encoding="utf-8") as f: + f.write(new_t) + print("Successfully patched MSI CustomAction printer port") + else: + print("Warning: RD_PRINTER_PORT not matched in RemotePrinter.cpp") + + - name: magick stuff + if: ${{ env.iconlink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + command: | + Invoke-WebRequest -Uri ${{ env.iconlink_url }}/get_png?filename=${{ env.iconlink_file }}"&"uuid=${{ env.iconlink_uuid }} -OutFile ./res/iconx.png + mv ./res/icon.ico ./res/icon.ico.bak + mv ./res/icon.png ./res/icon.png.bak + mv ./res/tray-icon.ico ./res/tray-icon.ico.bak + mv ./res/iconx.png ./res/icon.png + mv ./res/32x32.png ./res/32x32.png.bak + mv ./res/64x64.png ./res/64x64.png.bak + mv ./res/128x128.png ./res/128x128.png.bak + mv ./res/128x128@2x.png ./res/128x128@2x.png.bak + magick ./res/icon.png -define icon:auto-resize=256,64,48,32,16 ./res/icon.ico + cp ./res/icon.ico ./res/tray-icon.ico + magick ./res/icon.png -resize 32x32 ./res/32x32.png + magick ./res/icon.png -resize 64x64 ./res/64x64.png + magick ./res/icon.png -resize 128x128 ./res/128x128.png + magick ./res/128x128.png -resize 200% ./res/128x128@2x.png + + + - name: ui.rs icon + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + shell: bash + run: | + cp ./src/ui.rs ./src/ui.rs.bak + b64=$(base64 -w0 < ./res/icon.png) + perl -0777 -pe "s|iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHL[A-Za-z0-9+/=]+|$b64|g" -i.bak ./src/ui.rs + b64="" + + - name: fix connection delay + continue-on-error: true + if: ${{ env.delayFix == 'true' }} + shell: bash + run: | + # Precise fix: only extend the direct-connection condition with the + # key check, instead of globally replacing !key.is_empty() with + # false (which would also disable TCP encryption and UDP logic). + sed -i -e 's#if is_local || peer_nat_type == NatType::SYMMETRIC {#if is_local || peer_nat_type == NatType::SYMMETRIC || !key.is_empty() {#' ./src/client.rs + grep -n "key.is_empty()" ./src/client.rs || true + + - name: use X for offline display instead of orange circle + if: env.xOffline == 'true' + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/xoffline.diff -OutFile xoffline.diff + git apply xoffline.diff + + - name: removeNewVersionNotif + continue-on-error: true + if: env.removeNewVersionNotif == 'true' + shell: bash + run: | + sed -i -e 's|updateUrl.isNotEmpty|false|' ./flutter/lib/desktop/pages/desktop_home_page.dart + sed -i '/let (request, url) =/,/Ok(())/{/Ok(())/!d}' ./src/common.rs + + - name: replace flutter icons + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + cd ./flutter + #flutter pub upgrade win32 + flutter pub get + flutter pub run flutter_launcher_icons + cd .. + + - name: Checkout printer-adapter crate + uses: actions/checkout@v4 + with: + repository: bryangerlach/rdgen + ref: master + path: ./temp-repo + + - name: Move printer-adapter into place + shell: pwsh + run: | + if (Test-Path "./temp-repo/printer-adapter") { + Move-Item -Path "./temp-repo/printer-adapter" -Destination "./printer-adapter" -Force + Remove-Item -Recurse -Force "./temp-repo" + Write-Host "Successfully placed printer-adapter into workspace." + } else { + Write-Host "Error: printer-adapter folder not found in repository." + } + + - name: Build rustdesk + run: | + # Windows: build RustDesk + python3 .\build.py --portable --hwcodec --flutter --vram --skip-portable-pack + mv ./flutter/build/windows/x64/runner/Release ./rustdesk + + # Download usbmmidd_v2.zip and extract it to ./rustdesk + Invoke-WebRequest -Uri https://github.com/rustdesk-org/rdev/releases/download/usbmmidd_v2/usbmmidd_v2.zip -OutFile usbmmidd_v2.zip + Expand-Archive usbmmidd_v2.zip -DestinationPath . -Force + Remove-Item -Path usbmmidd_v2\Win32 -Recurse + Remove-Item -Path "usbmmidd_v2\deviceinstaller64.exe", "usbmmidd_v2\deviceinstaller.exe", "usbmmidd_v2\usbmmidd.bat" + mv -Force .\usbmmidd_v2 ./rustdesk + + # Download and install driver package + try { + Invoke-WebRequest -Uri https://github.com/rustdesk/hbb_common/releases/download/driver/rustdesk_printer_driver_v4-1.4.zip -OutFile rustdesk_printer_driver_v4-1.4.zip + Expand-Archive rustdesk_printer_driver_v4-1.4.zip -DestinationPath . + mkdir ./rustdesk/drivers -ErrorAction SilentlyContinue + mv -Force .\rustdesk_printer_driver_v4-1.4 ./rustdesk/drivers/RustDeskPrinterDriver + } catch { + Write-Host "Warning: Printer driver download failed." + } + + # Build and install open printer adapter + if (Test-Path "./printer-adapter/Cargo.toml") { + Write-Host "Building open printer-adapter..." + pushd ./printer-adapter + cargo build --release --locked + popd + if (Test-Path "./printer-adapter/target/release/printer_driver_adapter.dll") { + cp -Force "./printer-adapter/target/release/printer_driver_adapter.dll" "./rustdesk/printer_driver_adapter.dll" + Write-Host "Replaced printer_driver_adapter.dll with open implementation." + } else { + Write-Host "Error: printer-adapter build succeeded but DLL was not found." + } + } else { + Write-Host "Warning: ./printer-adapter/Cargo.toml not found, remote printing will remain disabled." + } + + - name: icon stuff + if: ${{ env.iconlink_url != 'false' }} + continue-on-error: true + run: | + mv ./rustdesk/data/flutter_assets/assets/icon.svg ./rustdesk/data/flutter_assets/assets/icon.svg.bak + magick ./res/icon.png ./rustdesk/data/flutter_assets/assets/icon.svg + + - name: logo stuff + if: ${{ env.logolink_url != 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: pwsh + continue_on_error: true + command: | + Invoke-WebRequest -Uri ${{ env.logolink_url }}/get_png?filename=${{ env.logolink_file }}"&"uuid=${{ env.logolink_uuid }} -OutFile ./rustdesk/data/flutter_assets/assets/logo.png + + - name: find Runner.res + # Windows: find Runner.res (compiled from ./flutter/windows/runner/Runner.rc), copy to ./Runner.res + # Runner.rc does not contain actual version, but Runner.res does + continue-on-error: true + shell: bash + run: | + runner_res=$(find . -name "Runner.res"); + if [ "$runner_res" == "" ]; then + echo "Runner.res: not found"; + else + echo "Runner.res: $runner_res"; + cp $runner_res ./libs/portable/Runner.res; + echo "list ./libs/portable/Runner.res"; + ls -l ./libs/portable/Runner.res; + fi + + - name: Download RustDeskTempTopMostWindow artifacts + uses: actions/download-artifact@master + if: env.UPLOAD_ARTIFACT == 'true' + with: + name: topmostwindow-artifacts + path: "./rustdesk" + + - name: zip dlls + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./rustdesk/*.dll, ./rustdesk/*.exe -DestinationPath ./rustdesk/unsigned_files.zip -CompressionLevel Fastest + + - name: sign dlls + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./rustdesk/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./rustdesk/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./rustdesk/unsigned_files.zip ./rustdesk/signed_files.zip + fi + + - name: unzip dlls + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./rustdesk/signed_files.zip -DestinationPath ./rustdesk/ -Force + Remove-Item ./rustdesk/unsigned_files.zip + Remove-Item ./rustdesk/signed_files.zip + + - name: Create custom.txt file + shell: bash + run: | + echo -n "${{ env.custom }}" | cat > ./rustdesk/custom_.txt + + - name: Build self-extracted executable + shell: bash + if: env.UPLOAD_ARTIFACT == 'true' + run: | + mv "./rustdesk/rustdesk.exe" "./rustdesk/${{ env.appname }}.exe" || echo "rustdesk.exe" + sed -i '/dpiAware/d' res/manifest.xml + pushd ./libs/portable + pip3 install -r requirements.txt + python3 ./generate.py -f ../../rustdesk/ -o . -e "../../rustdesk/${{ env.appname }}.exe" + popd + mkdir -p ./SignOutput + mv ./target/release/rustdesk-portable-packer.exe "./SignOutput/rustdesk.exe" + + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v2 + + - name: Build msi + continue-on-error: true + if: env.UPLOAD_ARTIFACT == 'true' + run: | + $myappname = "${{ env.appname }}" -replace '\s','_' + cp "rustdesk/${{ env.appname }}.exe" "rustdesk/${myappname}.exe" -ErrorAction SilentlyContinue + pushd ./res/msi + python preprocess.py --app-name "$myappname" --arp -d ../../rustdesk + nuget restore msi.sln + msbuild msi.sln -p:Configuration=Release -p:Platform=x64 /p:TargetVersion=Windows10 + cp ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk-latest.msi + mv ./Package/bin/x64/Release/en-us/Package.msi ../../SignOutput/rustdesk.msi + sha256sum ../../SignOutput/rustdesk.msi + + - name: zip exe and msi + continue-on-error: true + shell: pwsh + run: | + Compress-Archive -Path ./SignOutput/*.exe, ./SignOutput/*.msi -DestinationPath ./SignOutput/unsigned_files.zip -CompressionLevel Fastest + + - name: sign exe and msi + continue-on-error: true + shell: bash + run: | + if [ ! -z "${{ secrets.SIGN_BASE_URL }}" ] && [ ! -z "${{ secrets.SIGN_API_KEY }}" ]; then + curl -X POST -F "file=@./SignOutput/unsigned_files.zip" \ + -H "X-API-KEY: ${{ secrets.SIGN_API_KEY }}" \ + -m 900 \ + "${{ secrets.SIGN_BASE_URL }}/sign/" -o ./SignOutput/signed_files.zip + else + echo "Signing skipped - signing URL or API key not configured" + cp ./SignOutput/unsigned_files.zip ./SignOutput/signed_files.zip + fi + + - name: unzip exe and msi + continue-on-error: true + shell: pwsh + run: | + Expand-Archive -Path ./SignOutput/signed_files.zip -DestinationPath ./SignOutput/ -Force + Remove-Item ./SignOutput/unsigned_files.zip + Remove-Item ./SignOutput/signed_files.zip + + - name: rename rustdesk.exe to filename.exe + run: | + mv ./SignOutput/rustdesk.exe "./SignOutput/${{ env.filename }}.exe" || echo "rustdesk" + + - name: rename rustdesk.msi to filename.msi + continue-on-error: true + run: | + mv ./SignOutput/rustdesk.msi "./SignOutput/${{ env.filename }}.msi" || echo "rustdesk" + + - name: send file to rdgen server + if: ${{ env.rdgen == 'true' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" -F "uuid=${{ env.uuid }}" ${{ secrets.GENURL }}/save_custom_client || true + + - name: send file to api server + if: ${{ env.rdgen == 'false' }} + uses: nick-fields/retry@v3 + with: + timeout_minutes: 1 + max_attempts: 3 + shell: bash + command: | + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.exe" ${{ env.apiServer }}/api/save_custom_client + curl -i -X POST -H "Content-Type: multipart/form-data" -H "Authorization: Bearer ${{ env.token }}" -F "file=@./SignOutput/${{ env.filename }}.msi" ${{ env.apiServer }}/api/save_custom_client || true + + cleanup: + needs: [build-for-windows-flutter] + runs-on: ubuntu-latest + continue-on-error: true + if: always() + steps: + - name: Delete secrets artifact + uses: geekyeggo/delete-artifact@v5 + with: + name: encrypted-secrets-zip diff --git a/.github/workflows/third-party-RustDeskTempTopMostWindow.yml b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml new file mode 100644 index 0000000..9c4553f --- /dev/null +++ b/.github/workflows/third-party-RustDeskTempTopMostWindow.yml @@ -0,0 +1,95 @@ +name: build RustDeskTempTopMostWindow + +on: + workflow_call: + inputs: + upload-artifact: + type: boolean + default: true + target: + description: 'Target' + required: true + type: string + default: 'windows-2022' + configuration: + description: 'Configuration' + required: true + type: string + default: 'Release' + platform: + description: 'Platform' + required: true + type: string + default: 'x64' + target_version: + description: 'TargetVersion' + required: true + type: string + default: 'Windows10' + +env: + project_path: WindowInjection/WindowInjection.vcxproj + +jobs: + build-RustDeskTempTopMostWindow: + runs-on: ${{ inputs.target }} + strategy: + fail-fast: false + env: + build_output_dir: RustDeskTempTopMostWindow/WindowInjection/${{ inputs.platform }}/${{ inputs.configuration }} + steps: + - name: Add MSBuild to PATH + uses: microsoft/setup-msbuild@v3 + + - name: Checkout Repository + uses: actions/checkout@v5 + + - uses: actions/download-artifact@v5 + with: + name: encrypted-secrets-zip + + - name: Load Secrets + uses: ./.github/actions/decrypt-secrets + with: + zip_password: ${{ secrets.ZIP_PASSWORD }} + + - name: Finalize and Cleanup zip/json + if: always() # Run even if previous steps fail + continue-on-error: true + uses: fjogeleit/http-request-action@v1 + with: + url: "${{ secrets.GENURL }}/cleanzip" + method: 'POST' + customHeaders: '{"Content-Type": "application/json"}' + data: '{"uuid": "${{ env.uuid }}"}' + + - name: Download the source code + run: | + git clone https://github.com/rustdesk-org/RustDeskTempTopMostWindow RustDeskTempTopMostWindow + + # Build. commit 53b548a5398624f7149a382000397993542ad796 is tag v0.3 + - name: Build the project + uses: nick-fields/retry@v3 + with: + timeout_minutes: 5 + max_attempts: 3 + retry_wait_seconds: 15 + shell: pwsh + command: | + cd RustDeskTempTopMostWindow && git checkout 53b548a5398624f7149a382000397993542ad796 + if ($env:privacylink_url-ne "false") { + Invoke-WebRequest -Uri ${{ env.privacylink_url }}/get_png?filename=${{ env.privacylink_file }}"&"uuid=${{ env.privacylink_uuid }} -OutFile privacy.png + Invoke-WebRequest -Uri https://raw.githubusercontent.com/bryangerlach/rdgen/refs/heads/master/.github/patches/privacyScreen.py -OutFile privacyScreen.py + python privacyScreen.py + rm ./WindowInjection/img.cpp + mv img.cpp ./WindowInjection/img.cpp + } + msbuild ${{ env.project_path }} -p:Configuration=${{ inputs.configuration }} -p:Platform=${{ inputs.platform }} /p:TargetVersion=${{ inputs.target_version }} + + - name: Archive build artifacts + uses: actions/upload-artifact@v5 + if: ${{ inputs.upload-artifact }} + with: + name: topmostwindow-artifacts + path: | + ./${{ env.build_output_dir }}/WindowInjection.dll diff --git a/.github/workflows/vcpkg.yml b/.github/workflows/vcpkg.yml new file mode 100644 index 0000000..001a918 --- /dev/null +++ b/.github/workflows/vcpkg.yml @@ -0,0 +1,67 @@ +name: Export vcpkg Dependencies + +on: + workflow_dispatch: + inputs: + reason: + description: 'Reason for manual build' + required: false + default: 'Manual dependency update' + +env: + VCPKG_COMMIT_ID: '120deac3062162151622ca4860575a33844ba10b' + +jobs: + build-and-export: + runs-on: windows-latest + strategy: + matrix: + job: + - vcpkg-triplet: x64-windows-static + + steps: + - name: Enable Long Paths + run: git config --global core.longpaths true + + - name: Install Build Tools + run: | + choco install nasm + echo "C:\Program Files\NASM" >> $GITHUB_PATH + + - name: Checkout code + uses: actions/checkout@v4 + + - name: Clone RustDesk overlays + run: | + git clone --filter=blob:none --sparse https://github.com/rustdesk/rustdesk.git rustdesk_repo + cd rustdesk_repo + git sparse-checkout set res/vcpkg + git checkout master -- vcpkg.json || git checkout master -- vcpkg.json + cd .. + cp rustdesk_repo/vcpkg.json . + mkdir -p res + cp -r rustdesk_repo/res/vcpkg ./res/ + shell: bash + + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgDirectory: ${{ github.workspace }}/vcpkg + vcpkgGitCommitId: ${{ env.VCPKG_COMMIT_ID }} + doNotCache: false + + - name: Install vcpkg dependencies + env: + VCPKG_DEFAULT_HOST_TRIPLET: ${{ matrix.job.vcpkg-triplet }} + run: | + ./vcpkg/vcpkg install \ + --triplet ${{ matrix.job.vcpkg-triplet }} \ + --x-install-root="${{ github.workspace }}/installed" + shell: bash + + - name: Upload Installed Dependencies + uses: actions/upload-artifact@v4 + with: + name: vcpkg-export-${{ matrix.job.vcpkg-triplet }} + path: ${{ github.workspace }}/installed/ + if-no-files-found: error \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df02478 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +*.pyc +__pycache__/ +db.sqlite3 +gunicorn.conf +.venv + +# 运行时数据与产物(由 Docker 卷/绑定挂载管理,不入库) +/data/ +/exe/ +/png/ +/temp_zips/ + +# 编辑器/IDE +.trae/ +.idea/ +.vscode/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..3f3ffab --- /dev/null +++ b/Dockerfile @@ -0,0 +1,33 @@ +FROM python:3.13-alpine + +ENV PYTHONUNBUFFERED=1 \ + PYTHONDONTWRITEBYTECODE=1 \ + PIP_NO_CACHE_DIR=1 + +WORKDIR /opt/rdgen + +# 先单独安装依赖,尽量复用 Docker 层缓存 +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# 拷贝项目源码 +COPY . . + +# su-exec 用于在入口脚本中降权运行;创建非 root 用户与数据目录; +# 预收集静态文件(WhiteNoise 直接由应用进程对外提供) +RUN apk add --no-cache su-exec wget \ + && adduser -D -u 1000 appuser \ + && mkdir -p exe png temp_zips data static \ + && sed -i 's/\r$//' docker-entrypoint.sh \ + && chmod +x docker-entrypoint.sh \ + && python manage.py collectstatic --noinput \ + && chown -R appuser:appuser /opt/rdgen + +EXPOSE 8000 + +# 健康检查:业务根路径需要登录会 302,故检查专用的 /healthz +HEALTHCHECK --interval=30s --timeout=5s --retries=3 \ + CMD wget -q -O /dev/null http://127.0.0.1:8000/healthz/ || exit 1 + +ENTRYPOINT ["./docker-entrypoint.sh"] +CMD ["gunicorn", "-c", "gunicorn.conf.py", "rdgen.wsgi:application"] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..67c4871 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# RDGen, a RustDesk client generator to use with your self-hosted RustDesk server + +The client generator is currently hosted [here](https://rdgen.crayoneater.org). +If you would like to host the generator yourself, see [here](setup.md) + +## Features + +- Embed server and key into client +- Custom app name +- Custom icon/logo +- Set default settings for the client +- Support for rustdesk advanced settings (https://rustdesk.com/docs/en/self-host/client-configuration/advanced-settings/) + +## Generate RustDesk clients from command line instead of using a web browser + +Save your configuration from the rdgen web interface, or generate your own, then use that json file with [@AlekseyLapunov's rdgen-cli](https://github.com/AlekseyLapunov/rdgen-cli) to build from the command line on Windows, Linux, or MacOS like this: `python rdgen-cli -f my_config.json --set-version 1.4.5 --set-platform windows -s https://rdgen.crayoneater.org` + +## Build platforms + +Client builds run on **GitHub Actions** by default (cloud-hosted Windows/macOS/Linux runners). +A self-hosted **Gitea Actions** backend is also supported for intranet deployments where +builders and artifact uploads must stay off the public internet; you can switch between +the two in **Dashboard → 构建平台配置** (or `BUILD_PLATFORM=github|gitea`). Gitea requires +Gitea 1.27+ and your own registered `act_runner` machines. Full setup: [setup.md](setup.md). + +## Notes + +- Icons should be square (256x256 recommended) +- Avoid special characters or non-English characters in app name and file name +- Build time is currently 30 - 45 minutes + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..7fba04f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,72 @@ +services: + rdgen: + build: . + image: rdgen:latest + container_name: rdgen + restart: unless-stopped + environment: + # ===== 安全 ===== + # 生产环境务必更换:可执行 python -c "import secrets;print(secrets.token_hex(50))" 生成 + SECRET_KEY: "please-change-this-secret-key-in-production-9f3c1a7e5b2d8f06" + DEBUG: "false" + ALLOWED_HOSTS: "*" + # 经过 Nginx/Caddy 等 HTTPS 反向代理时填写实际访问地址(空格分隔多个) + CSRF_TRUSTED_ORIGINS: "" + # 配置后,GitHub Actions 回调需携带请求头 X-Webhook-Secret: <相同值>;留空则不校验 + WEBHOOK_SECRET: "" + + # ===== 初始管理员(仅首次启动、用户不存在时创建)===== + DJANGO_SUPERUSER_USERNAME: "admin" + DJANGO_SUPERUSER_PASSWORD: "Rdgen@2026" + DJANGO_SUPERUSER_EMAIL: "admin@example.com" + + # ===== GitHub 构建配置(BUILD_PLATFORM=github 时使用)===== + GHUSER: "github_username" + GHBEARER: "github_access_token" + GENURL: "accessible_url_of_server" + # 必须与构建仓库 Secret「ZIP_PASSWORD」完全一致;切勿留空(留空会导致构建提交 500)。 + # 这里给出可用的默认值 insecure,正式环境建议两边同时更换为同一随机强密码 + ZIP_PASSWORD: "insecure" + GHBRANCH: "master" + PROTOCOL: "https" + REPONAME: "rdgen" + SH_SECRET: "" + # 服务器访问 GitHub 的代理(触发构建/查询状态/拉取版本列表); + # 如 http://host:port 或 socks5://host:port(socks5 需 PySocks,镜像已内置), + # 留空直连;也可在后台「构建平台配置」中配置(优先级高于此变量) + GH_PROXY: "" + + # ===== 构建平台切换(github / gitea,默认 github)===== + # 选择 gitea 后使用下方 Gitea 配置触发自建 Gitea Actions; + # Gitea 构建机(Linux/Windows/macOS runner)需自行准备并注册。 + BUILD_PLATFORM: "github" + GITEA_SERVER_URL: "" # 如 https://gitea.example.com(建议 Gitea >= 1.27) + GITEA_TOKEN: "" # Gitea API 令牌,需仓库 Actions 读写权限 + GITEA_OWNER: "" # Gitea 仓库属主(用户/组织) + GITEA_REPO: "rdgen" + GITEA_BRANCH: "master" + GITEA_PROXY: "" # Gitea 多在同内网,通常留空直连 + + # ===== 定时维护(也可在后台「维护设置」页配置,页面配置优先级更高)===== + # 已结束构建任务保留天数(0=永不清理);是否每日自动备份数据库;备份保留份数 + BUILD_RETENTION_DAYS: "30" + DB_BACKUP_ENABLED: "1" + DB_BACKUP_KEEP: "7" + + # ===== 数据库(挂载到持久卷)===== + DB_PATH: "/opt/rdgen/data/db.sqlite3" + ports: + - "8000:8000" + volumes: + # 数据库(账号/构建记录/配置方案)使用固定名称的命名卷: + # 与部署目录解耦,重建镜像、换目录重新部署都不会丢数据; + # 切勿执行 `docker compose down -v`(会连命名卷一起删除)。 + - rdgen-data:/opt/rdgen/data + # 以下为构建产物目录,保持宿主机绑定挂载,便于直接查看/取文件 + - ./exe:/opt/rdgen/exe + - ./png:/opt/rdgen/png + - ./temp_zips:/opt/rdgen/temp_zips + +volumes: + rdgen-data: + name: rdgen-data diff --git a/docker-entrypoint.sh b/docker-entrypoint.sh new file mode 100644 index 0000000..f1f2a28 --- /dev/null +++ b/docker-entrypoint.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# rdgen 容器启动入口:修正挂载卷属主 -> 数据库迁移 -> 可选创建初始管理员 -> 降权启动 +set -e + +DATA_DIRS="/opt/rdgen/exe /opt/rdgen/png /opt/rdgen/temp_zips /opt/rdgen/data" + +# 以 root 启动时,把宿主机挂载进来的数据目录属主改为运行用户 +for d in $DATA_DIRS; do + if [ -d "$d" ]; then + chown -R appuser:appuser "$d" 2>/dev/null || true + fi +done + +# 数据库迁移(SQLite 文件位于持久化的 data 目录) +# 迁移前先冷拷贝一份兜底(防迁移异常),存放在 data/backups; +# 轮转策略统一由维护进程按后台「保留份数」设置处理,这里不删除旧备份。 +DB_FILE="/opt/rdgen/data/db.sqlite3" +BACKUP_DIR="/opt/rdgen/data/backups" +if [ -f "$DB_FILE" ]; then + mkdir -p "$BACKUP_DIR" + cp "$DB_FILE" "$BACKUP_DIR/db-$(date +%Y%m%d-%H%M%S).sqlite3" 2>/dev/null || true +fi + +su-exec appuser python manage.py migrate --noinput + +# 启动定时维护守护进程(清理过期构建任务/临时文件、每日备份数据库)。 +# 单实例后台常驻,每小时醒来检查一次;日志写入持久化数据卷。 +su-exec appuser sh -c 'python manage.py maintenance --loop >> /opt/rdgen/data/maintenance.log 2>&1 &' + +# 可选:首次启动时自动创建超级管理员(已存在同名用户则跳过) +if [ -n "$DJANGO_SUPERUSER_USERNAME" ] && [ -n "$DJANGO_SUPERUSER_PASSWORD" ]; then + export DJANGO_SUPERUSER_USERNAME DJANGO_SUPERUSER_PASSWORD DJANGO_SUPERUSER_EMAIL + su-exec appuser python manage.py shell <<'PY' +import os +from django.contrib.auth import get_user_model + +User = get_user_model() +name = os.environ.get("DJANGO_SUPERUSER_USERNAME") +password = os.environ.get("DJANGO_SUPERUSER_PASSWORD") +email = os.environ.get("DJANGO_SUPERUSER_EMAIL", "") +if name and password and not User.objects.filter(username=name).exists(): + User.objects.create_superuser(username=name, email=email, password=password) + print(f"[rdgen] 已创建初始管理员账号:{name}") +else: + print("[rdgen] 管理员账号已存在或未配置,跳过创建") +PY +fi + +# 降权运行业务进程 +exec su-exec appuser "$@" diff --git a/gunicorn.conf.py b/gunicorn.conf.py new file mode 100644 index 0000000..19d969d --- /dev/null +++ b/gunicorn.conf.py @@ -0,0 +1,10 @@ +import os + +# Adjust these values as needed +bind = "0.0.0.0:8000" # Host and port for Gunicorn to listen on +workers = 5 # The number of worker processes for concurrency (adjust based on system resources) +threads = 6 +activate_base = True # Activate your virtual environment if applicable + +# Path to your Django project's main WSGI application file (usually manage.py) +wsgi_app = "rdgen.wsgi.application" \ No newline at end of file diff --git a/manage.py b/manage.py new file mode 100644 index 0000000..4232cba --- /dev/null +++ b/manage.py @@ -0,0 +1,22 @@ +#!/usr/bin/env python +"""Django's command-line utility for administrative tasks.""" +import os +import sys + + +def main(): + """Run administrative tasks.""" + os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rdgen.settings') + try: + from django.core.management import execute_from_command_line + except ImportError as exc: + raise ImportError( + "Couldn't import Django. Are you sure it's installed and " + "available on your PYTHONPATH environment variable? Did you " + "forget to activate a virtual environment?" + ) from exc + execute_from_command_line(sys.argv) + + +if __name__ == '__main__': + main() diff --git a/printer-adapter/.gitignore b/printer-adapter/.gitignore new file mode 100644 index 0000000..2f7896d --- /dev/null +++ b/printer-adapter/.gitignore @@ -0,0 +1 @@ +target/ diff --git a/printer-adapter/Cargo.lock b/printer-adapter/Cargo.lock new file mode 100644 index 0000000..f701021 --- /dev/null +++ b/printer-adapter/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "printer_driver_adapter" +version = "0.1.0" diff --git a/printer-adapter/Cargo.toml b/printer-adapter/Cargo.toml new file mode 100644 index 0000000..7002a4d --- /dev/null +++ b/printer-adapter/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "printer_driver_adapter" +version = "0.1.0" +edition = "2021" +description = "Open drop-in replacement for RustDesk's printer_driver_adapter.dll" + +[lib] +name = "printer_driver_adapter" +crate-type = ["cdylib"] + +[profile.release] +opt-level = "z" +lto = true +strip = true + +[workspace] \ No newline at end of file diff --git a/printer-adapter/README.md b/printer-adapter/README.md new file mode 100644 index 0000000..087f699 --- /dev/null +++ b/printer-adapter/README.md @@ -0,0 +1,186 @@ +# Open `printer_driver_adapter.dll` + +A drop-in replacement for RustDesk's closed printer adapter, so remote printing +works in custom-branded builds. + +## Why this exists + +RustDesk's `printer_driver_adapter.dll` verifies the **calling executable's +Authenticode signature** inside `init()` and refuses to run for anything not +signed by RustDesk. Evidence from the shipped binary: + +- imports `WinVerifyTrust` (wintrust.dll), `CertGetNameStringW`, `GetModuleFileNameW` +- links `codesign-verify-rs`, a crate whose only purpose is Authenticode checks +- carries one hardcoded identity constant: `PURSLANEHUABINGRUSTDESK` followed by + two SHA-256 digests. Purslane Ltd is RustDesk's company; Huabing is the founder. + +An unsigned custom build fails with: + +``` +ERROR [src\server.rs:160] printer service init failed: Failed to init printer driver +``` + +which is the `fn_init() != 0` branch. Signing with *your own* certificate does +not help — the check is an allow-list of specific identities, not "is it signed". + +Everything else in RustDesk's printing chain is open source and works. Only the +capture shim is gated, and it is replaceable: this crate reimplements the same +four-function ABI with no signature check. + +## The ABI + +From `src/server/printer_service.rs`. Symbols are resolved by these exact +undecorated names, so all exports are `#[no_mangle] extern "C"`: + +```rust +pub type Init = fn(tag_name: *const i8) -> i32; // 0 = success +pub type Uninit = fn(); +pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); +pub type FreePrnData = fn(data: *mut i8); +``` + +Verified: this DLL's PE export table is byte-for-byte the same set of names as +RustDesk's own (`init`, `uninit`, `get_prn_data`, `free_prn_data`). + +## How capture works + +No custom print driver, therefore no WHQL signing: + +``` + print job + │ + ▼ + virtual printer ──► Local Port whose NAME IS A FILE PATH + (signed driver) %ProgramData%\\printer-spool\job.prn + │ + ▼ + this adapter polls the directory, + opens each file with share_mode = 0 + (fails while the spooler still holds it), + returns the bytes, deletes the file + │ + ▼ + printer_service::run() ──► on_printer_data() + │ + ▼ + existing open-source transport → controller prints +``` + +A Local Port whose name is a full file path makes the spooler write output +straight to that file with no "save as" prompt. The exclusive-open test is how +job completion is detected — no timers, no guessing. + +`init()` clears the spool directory, so a stale job can never be replayed onto +the next connection, and any file present afterwards is by definition new. + +## Build + +``` +cargo build --release +``` + +Output: `target\release\printer_driver_adapter.dll` (~140 KB, no dependencies — +std only). + +## How the build wires it in + +DVForge does all of this automatically for Windows builds — nothing here needs +to be run by hand. + +`builder/orchestrator.py` → `_install_open_printer_adapter()` +: cargo-builds this crate and copies the DLL into the Flutter `Release/` folder, + overwriting the signature-checked one that `_ensure_windows_printer_driver()` + downloaded. It runs before the MSI harvest and the portable packer, so the + replacement ships inside both installers. + +`builder/customize.py` → `_apply_printer_port()` +: repoints the printer's port at the spool file. **Two** implementations exist + and both are patched, because patching only one leaves the printer on the + wrong port: + + - `libs/remote_printer/src/lib.rs` — Rust, used by `--install-remote-printer` + and the in-app Settings button. + - `res/msi/CustomActions/RemotePrinter.cpp` — a full C++ reimplementation used + by the MSI's `InstallPrinter` custom action. This is the one that runs during + a normal installer run. + + Only the port changes. The printer keeps its name, and the driver stays + `RustDesk v4 Printer Driver`. + +It runs after `_apply_appname()`, since both files have the app name substituted +into them. + +## Verifying a build + +After installing, confirm the adapter loaded: + +```powershell +Get-ChildItem "C:\Windows\ServiceProfiles\LocalService\AppData\Roaming\\log" ` + -Recurse -Filter *.log | Select-String "printer service" +``` + +Expect `printer service initialized`. The old failure reads +`printer service init failed: Failed to init printer driver`. + +Confirm the port is the spool file, not a named port: + +```powershell +Get-Printer -Name " Printer" | Select-Object Name,DriverName,PortName +``` + +`PortName` should be `C:\ProgramData\\printer-spool\job.prn`. + +Then connect from another machine, print on the controlled one, and check +`%ProgramData%\\printer-spool\adapter.log` for a `captured` line and the +server log for `Got prn data, data len:`. + +## Which driver to render with + +The controller prints received bytes via `PrintXPSRawData()` +(`src/platform/windows.rs:4261`), so the payload must be a valid XPS package. + +- **`RustDesk v4 Printer Driver`** — already installed if you ever ran the app's + printer install, and it declares `XpsFormat=XPS` (MS-XPS), which is exactly + what the receiving end expects. Note its render filter is signed by *Microsoft* + ("Windows Hardware Compatibility Publisher"), not RustDesk — the signature gate + is only in the adapter, so this driver has no objection to who calls it. + Best format match. Check redistribution terms before shipping it yourself. +- **`Microsoft XPS Document Writer v4`** — inbox on every Windows machine and + unambiguously redistributable, but emits **OpenXPS** (`.oxps`), not MS-XPS. + May need conversion before `PrintXPSRawData` accepts it. + +**Resolved 2026-08-19 by inspecting a real captured job.** A test print through +`Microsoft XPS Document Writer v4` produced a valid OPC package whose root +relationship is: + + Type="http://schemas.openxps.org/oxps/v1.0/fixedrepresentation" + +That is **OpenXPS**, not MS-XPS — confirming the mismatch. Use +`RustDesk v4 Printer Driver` instead, which declares `XpsFormat=XPS`. If you must +stay on the inbox driver, the fallback is converting OXPS to MS-XPS, or swapping +`send_raw_data_to_printer` on the receiving end — which is your own code. + +`PrintXPSRawData` writes diagnostics to `C:\Windows\temp\test_rustdesk.log` on +failure; check there if a job arrives but does not print. + +## Status + +| Piece | State | +|---|---| +| Four-function ABI, exports match upstream | verified | +| Capture, completion detection, cleanup | verified by test suite | +| Panic safety across the FFI boundary | `catch_unwind` on every export | +| Loads and initialises inside the real server | not yet run | +| Spooler renders a job into the file port | verified — 37 KB job captured | +| End-to-end print to a remote machine | not yet run | +| XPS vs OpenXPS payload | resolved — inbox v4 driver emits OpenXPS, use RustDesk's driver | + +## Notes + +`panic = "abort"` is deliberately **not** set. A panic escaping a cdylib into the +RustDesk service would take the whole service down, so every export wraps its body +in `catch_unwind` instead. + +Licensing: this is clean-room work against an ABI declared in RustDesk's own +AGPL-3.0 source. It contains no RustDesk code and defeats no protection on their +binary — it replaces it. diff --git a/printer-adapter/src/lib.rs b/printer-adapter/src/lib.rs new file mode 100644 index 0000000..997cb7d --- /dev/null +++ b/printer-adapter/src/lib.rs @@ -0,0 +1,289 @@ +//! Open drop-in replacement for RustDesk's `printer_driver_adapter.dll`. +//! +//! RustDesk's own adapter verifies the calling executable's Authenticode +//! signature against a hardcoded vendor allow-list, so it refuses to start in +//! any custom-branded build. This crate implements the same four-function ABI +//! that `src/server/printer_service.rs` loads, with no signature check. +//! +//! The ABI, as declared upstream: +//! +//! ```ignore +//! pub type Init = fn(tag_name: *const i8) -> i32; // 0 = success +//! pub type Uninit = fn(); +//! pub type GetPrnData = fn(dur_mills: u32, data: *mut *mut i8, data_len: *mut u32); +//! pub type FreePrnData = fn(data: *mut i8); +//! ``` +//! +//! Symbols are looked up by these exact undecorated names, so every export is +//! `#[no_mangle] extern "C"`. +//! +//! # How capture works +//! +//! Instead of a custom print driver, this pairs with a printer built on an +//! inbox (already Microsoft-signed) driver whose port is a plain file path +//! under our spool directory. The spooler writes the rendered job there; we +//! pick it up, hand it to RustDesk, and delete it. +//! +//! # Safety +//! +//! Every export catches panics. Unwinding across an FFI boundary is undefined +//! behaviour, and a panic escaping into the RustDesk server process would take +//! the whole service down. + +use std::alloc::{alloc, dealloc, Layout}; +use std::ffi::CStr; +use std::fs; +use std::io::{Read, Write}; +use std::os::raw::c_char; +use std::os::windows::fs::OpenOptionsExt; +use std::path::{Path, PathBuf}; +use std::ptr; +use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Bytes reserved before each returned buffer to stash its length. +/// `free_prn_data` only receives a pointer, so the length has to travel with +/// the allocation itself. +const HEADER: usize = 16; +const ALIGN: usize = 8; + +/// Windows share mode: 0 means "no other handle may be open". Opening the +/// spool file this way is how we tell that the spooler has finished with it. +const SHARE_NONE: u32 = 0; + +struct State { + dir: PathBuf, +} + +static STATE: Mutex> = Mutex::new(None); + +// --------------------------------------------------------------------------- +// helpers +// --------------------------------------------------------------------------- + +fn now_secs() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0) +} + +/// Append a line to `/adapter.log`. Never fails the caller: if logging +/// breaks there is nothing useful to do about it from inside a print filter. +fn log(dir: &Path, msg: &str) { + let path = dir.join("adapter.log"); + if let Ok(mut f) = fs::OpenOptions::new().create(true).append(true).open(path) { + let _ = writeln!(f, "[{}] {}", now_secs(), msg); + } +} + +/// Strip characters that cannot appear in a Windows path component, so an +/// arbitrary app name can be used as a directory name. +fn sanitize(tag: &str) -> String { + let cleaned: String = tag + .chars() + .map(|c| match c { + '<' | '>' | ':' | '"' | '/' | '\\' | '|' | '?' | '*' => '_', + c if (c as u32) < 0x20 => '_', + c => c, + }) + .collect(); + let trimmed = cleaned.trim().trim_end_matches('.').to_string(); + if trimmed.is_empty() { + "RustDeskPrinter".to_string() + } else { + trimmed + } +} + +/// `%ProgramData%\\printer-spool`, the directory the printer port writes +/// into. ProgramData is used rather than a user profile because the spooler +/// runs as a service account. +fn spool_dir(tag: &str) -> PathBuf { + let base = std::env::var("ProgramData") + .or_else(|_| std::env::var("ALLUSERSPROFILE")) + .unwrap_or_else(|_| "C:\\ProgramData".to_string()); + Path::new(&base).join(sanitize(tag)).join("printer-spool") +} + +/// True when the file can be opened with no sharing, i.e. the spooler has +/// closed its handle and the job is complete. +fn read_if_complete(path: &Path) -> Option> { + let mut f = fs::OpenOptions::new() + .read(true) + .share_mode(SHARE_NONE) + .open(path) + .ok()?; + let mut buf = Vec::new(); + f.read_to_end(&mut buf).ok()?; + if buf.is_empty() { + return None; + } + Some(buf) +} + +/// Oldest candidate job in the spool directory, by modification time. +/// `adapter.log` and zero-length files are skipped. +fn oldest_job(dir: &Path) -> Option { + let mut best: Option<(SystemTime, PathBuf)> = None; + for entry in fs::read_dir(dir).ok()?.flatten() { + let path = entry.path(); + let meta = match entry.metadata() { + Ok(m) if m.is_file() && m.len() > 0 => m, + _ => continue, + }; + if path + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.eq_ignore_ascii_case("adapter.log")) + .unwrap_or(false) + { + continue; + } + let mtime = meta.modified().unwrap_or(UNIX_EPOCH); + match &best { + Some((t, _)) if *t <= mtime => {} + _ => best = Some((mtime, path)), + } + } + best.map(|(_, p)| p) +} + +/// Copy `bytes` into a buffer this crate owns, with its length recorded in a +/// header so `free_prn_data` can reconstruct the exact layout. +unsafe fn alloc_buf(bytes: &[u8]) -> *mut c_char { + let total = HEADER + bytes.len(); + let layout = match Layout::from_size_align(total, ALIGN) { + Ok(l) => l, + Err(_) => return ptr::null_mut(), + }; + let base = alloc(layout); + if base.is_null() { + return ptr::null_mut(); + } + (base as *mut u64).write_unaligned(bytes.len() as u64); + ptr::copy_nonoverlapping(bytes.as_ptr(), base.add(HEADER), bytes.len()); + base.add(HEADER) as *mut c_char +} + +unsafe fn free_buf(data: *mut c_char) { + if data.is_null() { + return; + } + let base = (data as *mut u8).sub(HEADER); + let len = (base as *mut u64).read_unaligned() as usize; + if let Ok(layout) = Layout::from_size_align(HEADER + len, ALIGN) { + dealloc(base, layout); + } +} + +// --------------------------------------------------------------------------- +// exported ABI +// --------------------------------------------------------------------------- + +/// Prepare the spool directory for `tag_name` (RustDesk passes the app name). +/// Returns 0 on success, non-zero on failure — upstream turns any non-zero +/// into "Failed to init printer driver". +#[no_mangle] +pub extern "C" fn init(tag_name: *const c_char) -> i32 { + let result = std::panic::catch_unwind(|| { + if tag_name.is_null() { + return 1; + } + let tag = match unsafe { CStr::from_ptr(tag_name) }.to_str() { + Ok(t) => t.to_string(), + Err(_) => return 2, + }; + let dir = spool_dir(&tag); + if fs::create_dir_all(&dir).is_err() { + return 3; + } + // Drop anything left from a previous run so a stale job cannot be + // replayed onto the next connection. + if let Ok(entries) = fs::read_dir(&dir) { + for entry in entries.flatten() { + let p = entry.path(); + if p.is_file() + && !p + .file_name() + .and_then(|n| n.to_str()) + .map(|n| n.eq_ignore_ascii_case("adapter.log")) + .unwrap_or(false) + { + let _ = fs::remove_file(&p); + } + } + } + log(&dir, &format!("init tag={:?} dir={}", tag, dir.display())); + match STATE.lock() { + Ok(mut g) => { + *g = Some(State { dir }); + 0 + } + Err(_) => 4, + } + }); + result.unwrap_or(5) +} + +#[no_mangle] +pub extern "C" fn uninit() { + let _ = std::panic::catch_unwind(|| { + if let Ok(mut g) = STATE.lock() { + if let Some(s) = g.as_ref() { + log(&s.dir, "uninit"); + } + *g = None; + } + }); +} + +/// Hand back one completed print job, or nothing. +/// +/// `dur_mills` is advisory upstream ("data generated in the last N ms"); the +/// spool directory is cleared on `init`, so anything present is by definition +/// new and is returned regardless of age. That avoids dropping a job when the +/// poll loop is delayed. +#[no_mangle] +pub extern "C" fn get_prn_data(_dur_mills: u32, data: *mut *mut c_char, data_len: *mut u32) { + let _ = std::panic::catch_unwind(|| { + unsafe { + if !data.is_null() { + *data = ptr::null_mut(); + } + if !data_len.is_null() { + *data_len = 0; + } + } + if data.is_null() || data_len.is_null() { + return; + } + let guard = match STATE.lock() { + Ok(g) => g, + Err(_) => return, + }; + let Some(state) = guard.as_ref() else { return }; + let Some(job) = oldest_job(&state.dir) else { return }; + // Still open by the spooler means the job is mid-render; try next tick. + let Some(bytes) = read_if_complete(&job) else { return }; + let _ = fs::remove_file(&job); + unsafe { + let buf = alloc_buf(&bytes); + if buf.is_null() { + log(&state.dir, "alloc failed, job dropped"); + return; + } + *data = buf; + *data_len = bytes.len() as u32; + } + log( + &state.dir, + &format!("captured {} ({} bytes)", job.display(), bytes.len()), + ); + }); +} + +#[no_mangle] +pub extern "C" fn free_prn_data(data: *mut c_char) { + let _ = std::panic::catch_unwind(|| unsafe { free_buf(data) }); +} diff --git a/rdgen/__init__.py b/rdgen/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdgen/asgi.py b/rdgen/asgi.py new file mode 100644 index 0000000..b69d3d1 --- /dev/null +++ b/rdgen/asgi.py @@ -0,0 +1,16 @@ +""" +ASGI config for rdgen project. + +It exposes the ASGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/asgi/ +""" + +import os + +from django.core.asgi import get_asgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rdgen.settings') + +application = get_asgi_application() diff --git a/rdgen/settings.py b/rdgen/settings.py new file mode 100644 index 0000000..1782373 --- /dev/null +++ b/rdgen/settings.py @@ -0,0 +1,152 @@ +""" +Django settings for rdgen project. +RustDesk 自定义客户端生成平台 +""" +import os +from pathlib import Path + +# Build paths inside the project like this: BASE_DIR / 'subdir'. +BASE_DIR = Path(__file__).resolve().parent.parent + + +# 快速开发配置 —— 生产环境请通过环境变量注入 SECRET_KEY +SECRET_KEY = os.environ.get( + 'SECRET_KEY', 'django-insecure-!(t-!f#6g#sr%yfded9(xha)g+=!6craeez^cp+*&bz_7vdk61' +) +GHUSER = os.environ.get("GHUSER", '') +GHBEARER = os.environ.get("GHBEARER", '') +GENURL = os.environ.get("GENURL", '') +GHBRANCH = os.environ.get("GHBRANCH", 'master') +ZIP_PASSWORD = os.environ.get("ZIP_PASSWORD", 'insecure') +PROTOCOL = os.environ.get("PROTOCOL", 'https') +REPONAME = os.environ.get("REPONAME", 'rdgen') +SH_SECRET = os.environ.get('SH_SECRET', 'secret') +# GitHub Actions 回调端点的可选共享密钥(配置后回调需携带 X-Webhook-Secret 头) +WEBHOOK_SECRET = os.environ.get('WEBHOOK_SECRET', '') + +MEDIA_URL = '/media/' +MEDIA_ROOT = os.path.join(BASE_DIR, 'media') + + +def _env_bool(name, default=False): + val = os.environ.get(name) + if val is None: + return default + return val.strip().lower() in ('1', 'true', 't', 'yes', 'on') + + +DEBUG = _env_bool("DEBUG", False) + +ALLOWED_HOSTS = [h for h in os.environ.get("ALLOWED_HOSTS", "*").split(',') if h] +# 反向代理(https)场景请配置,例如:CSRF_TRUSTED_ORIGINS="https://rdgen.example.com" +CSRF_TRUSTED_ORIGINS = [o.strip() for o in os.environ.get("CSRF_TRUSTED_ORIGINS", "").split() if o.strip()] + +# Application definition + +INSTALLED_APPS = [ + 'whitenoise.runserver_nostatic', + 'django.contrib.admin', + 'django.contrib.auth', + 'django.contrib.contenttypes', + 'django.contrib.sessions', + 'django.contrib.messages', + 'django.contrib.staticfiles', + 'rdgenerator', +] + +MIDDLEWARE = [ + 'django.middleware.security.SecurityMiddleware', + 'whitenoise.middleware.WhiteNoiseMiddleware', + 'django.contrib.sessions.middleware.SessionMiddleware', + 'django.middleware.common.CommonMiddleware', + 'django.middleware.csrf.CsrfViewMiddleware', + 'django.contrib.auth.middleware.AuthenticationMiddleware', + 'django.contrib.messages.middleware.MessageMiddleware', + 'django.middleware.clickjacking.XFrameOptionsMiddleware', +] + +ROOT_URLCONF = 'rdgen.urls' + +TEMPLATES = [ + { + 'BACKEND': 'django.template.backends.django.DjangoTemplates', + 'DIRS': [BASE_DIR / 'rdgenerator' / 'templates'], + 'APP_DIRS': True, + 'OPTIONS': { + 'context_processors': [ + 'django.template.context_processors.debug', + 'django.template.context_processors.request', + 'django.contrib.auth.context_processors.auth', + 'django.contrib.messages.context_processors.messages', + 'rdgenerator.context_processors.github_config', + ], + }, + }, +] + +WSGI_APPLICATION = 'rdgen.wsgi.application' + + +# Database +DATABASES = { + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.environ.get('DB_PATH', str(BASE_DIR / 'db.sqlite3')), + } +} + + +# Password validation +AUTH_PASSWORD_VALIDATORS = [ + {'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator'}, + {'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator'}, + {'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'}, + {'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'}, +] + + +# Internationalization +LANGUAGE_CODE = 'zh-hans' + +TIME_ZONE = 'Asia/Shanghai' + +USE_I18N = True + +USE_TZ = True + + +# Static files (CSS, JavaScript, Images) +STATIC_URL = 'static/' +STATIC_ROOT = BASE_DIR / 'static' + +STORAGES = { + "default": { + "BACKEND": "django.core.files.storage.FileSystemStorage", + }, + "staticfiles": { + "BACKEND": "whitenoise.storage.CompressedStaticFilesStorage", + }, +} + +WHITENOISE_INDEX_FILE = True + +# Default primary key field type +DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# 上传内容仅为 200px 图标/隐私图,限制 10MB 足够,避免无限制上传 +DATA_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 +FILE_UPLOAD_MAX_MEMORY_SIZE = 10 * 1024 * 1024 + +# 登录 +LOGIN_URL = '/login/' +LOGIN_REDIRECT_URL = '/' +LOGOUT_REDIRECT_URL = '/login/' + +# 反向代理(Nginx/Caddy 等)下发 X-Forwarded-Proto 时按 https 处理 +SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') +SECURE_BROWSER_XSS_FILTER = True +SECURE_CONTENT_TYPE_NOSNIFF = True +X_FRAME_OPTIONS = 'SAMEORIGIN' + +# 会话有效期默认 7 天(单位:秒) +SESSION_COOKIE_AGE = int(os.environ.get('SESSION_COOKIE_AGE', 7 * 24 * 3600)) diff --git a/rdgen/urls.py b/rdgen/urls.py new file mode 100644 index 0000000..c0ace87 --- /dev/null +++ b/rdgen/urls.py @@ -0,0 +1,62 @@ +""" +rdgen 项目 URL 路由配置。 +""" +from django.contrib import admin +from django.contrib.auth import views as auth_views +from django.urls import path + +from rdgenerator import api_views, views, web_views + +urlpatterns = [ + # 系统 + path('admin/', admin.site.urls), + path('healthz/', web_views.healthz, name='healthz'), + + # 登录 / 注销 + path('login/', auth_views.LoginView.as_view(template_name='registration/login.html'), name='login'), + path('logout/', auth_views.LogoutView.as_view(), name='logout'), + + # 业务页面 + path('', views.generator_view, name='generator'), + path('my-builds/', web_views.my_builds, name='my_builds'), + path('my-builds//retry/', web_views.retry_build, name='retry_build'), + path('my-builds//delete/', web_views.delete_my_build, name='delete_my_build'), + path('configs/', web_views.saved_configs, name='saved_configs'), + path('configs/save/', web_views.save_config, name='save_config'), + path('configs//data/', web_views.config_data, name='config_data'), + path('configs//export/', web_views.export_config, name='export_config'), + path('configs//delete/', web_views.delete_config, name='delete_config'), + path('tokens/', web_views.api_tokens, name='api_tokens'), + path('tokens//delete/', web_views.delete_token, name='delete_token'), + + # 后台管理 + path('dashboard/', web_views.dashboard, name='dashboard'), + path('dashboard/users/', web_views.user_list, name='user_list'), + path('dashboard/users//edit/', web_views.user_edit, name='user_edit'), + path('dashboard/builds//delete/', web_views.delete_build, name='delete_build'), + path('dashboard/settings/github/', web_views.github_settings, name='github_settings'), + path('dashboard/settings/build/test/', web_views.test_build, name='test_build'), + path('dashboard/settings/github/test/', web_views.test_github, name='test_github'), + path('dashboard/settings/maintenance/', web_views.maintenance_settings, name='maintenance_settings'), + path('dashboard/maintenance/run/', web_views.maintenance_run_now, name='maintenance_run_now'), + path('dashboard/backups//download/', web_views.download_backup, name='download_backup'), + + # GitHub Actions / 外部服务回调(保持历史路径兼容) + path('generator', views.generator_view), + path('check_for_file', views.check_for_file), + path('download', views.download), + path('updategh', views.update_github_run), + path('startgh', views.startgh), + path('get_png', views.get_png), + path('save_custom_client', views.save_custom_client), + path('get_zip', views.get_zip), + path('cleanzip', views.cleanup_secrets), + + # JSON API + path('api/generate', api_views.api_generate), + path('api/status', api_views.api_status), +] + +handler403 = 'rdgenerator.views_err.handler403' +handler404 = 'rdgenerator.views_err.handler404' +handler500 = 'rdgenerator.views_err.handler500' diff --git a/rdgen/wsgi.py b/rdgen/wsgi.py new file mode 100644 index 0000000..a974cfa --- /dev/null +++ b/rdgen/wsgi.py @@ -0,0 +1,16 @@ +""" +WSGI config for rdgen project. + +It exposes the WSGI callable as a module-level variable named ``application``. + +For more information on this file, see +https://docs.djangoproject.com/en/5.0/howto/deployment/wsgi/ +""" + +import os + +from django.core.wsgi import get_wsgi_application + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'rdgen.settings') + +application = get_wsgi_application() diff --git a/rdgenerator/__init__.py b/rdgenerator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdgenerator/admin.py b/rdgenerator/admin.py new file mode 100644 index 0000000..09aa7eb --- /dev/null +++ b/rdgenerator/admin.py @@ -0,0 +1,26 @@ +from django.contrib import admin + +from .models import ApiToken, GithubRun + + +@admin.register(GithubRun) +class GithubRunAdmin(admin.ModelAdmin): + list_display = ('id', 'filename', 'platform', 'status_label', 'created_by', 'github_run_id', 'created_at') + list_filter = ('status', 'platform', 'created_at') + search_fields = ('uuid', 'filename', 'created_by__username') + readonly_fields = ('uuid', 'github_run_id', 'created_at', 'updated_at') + list_per_page = 30 + + +@admin.register(ApiToken) +class ApiTokenAdmin(admin.ModelAdmin): + list_display = ('name', 'user', 'is_active', 'created_at', 'last_used_at') + list_filter = ('is_active',) + search_fields = ('name', 'user__username', 'key') + readonly_fields = ('key', 'created_at', 'last_used_at') + list_per_page = 30 + + +admin.site.site_header = 'RustDesk 客户端定制平台 · 后台管理' +admin.site.site_title = '平台管理后台' +admin.site.index_title = '管理首页' diff --git a/rdgenerator/api_views.py b/rdgenerator/api_views.py new file mode 100644 index 0000000..3521c06 --- /dev/null +++ b/rdgenerator/api_views.py @@ -0,0 +1,216 @@ +import json +import re + +from django.http import JsonResponse +from django.utils import timezone +from django.views.decorators.csrf import csrf_exempt + +from . import app_settings +from .models import ApiToken +from .versions import is_valid_version +from .views import generate_custom_client, _get_run_status + + +# 字段取值约束(与 GenerateForm 保持一致) +PLATFORM_CHOICES = ['windows', 'windows-x86', 'linux', 'android', 'macos'] +# 版本跟随 rustdesk/rustdesk 官方 tags 动态更新,这里只做格式校验 +DIRECTION_CHOICES = ['incoming', 'outgoing', 'both'] +INSTALLATION_CHOICES = ['installationY', 'installationN'] +SETTINGS_CHOICES = ['settingsY', 'settingsN'] +THEME_CHOICES = ['light', 'dark', 'system'] +THEME_DORO_CHOICES = ['default', 'override'] +PASS_APPROVE_MODE_CHOICES = ['password', 'click', 'password-click'] +PERMISSIONS_DORO_CHOICES = ['default', 'override'] +PERMISSIONS_TYPE_CHOICES = ['custom', 'full', 'view'] + +# 布尔字段 +BOOL_FIELDS = [ + 'delayFix', 'xOffline', 'hidecm', 'removeNewVersionNotif', + 'denyLan', 'enableDirectIP', 'autoClose', + 'enableKeyboard', 'enableClipboard', 'enableFileTransfer', 'enableAudio', + 'enableTCP', 'enableRemoteRestart', 'enableRecording', 'enableBlockingInput', + 'enableRemoteModi', 'removeWallpaper', 'enablePrinter', 'enableCamera', 'enableTerminal', +] + +# 可选字符串字段 +OPTIONAL_STR_FIELDS = [ + 'sh_secret_field', 'serverIP', 'serverPort', 'key', 'apiServer', 'urlLink', 'downloadLink', + 'appname', 'compname', 'androidappid', 'permanentPassword', + 'defaultManual', 'overrideManual', + 'iconbase64', 'logobase64', 'privacybase64', +] + + +def _authenticate(request): + """通过 Authorization: Token 请求头认证,返回 User 或 None。""" + auth_header = request.headers.get('Authorization', '') + if not auth_header.startswith('Token '): + return None + key = auth_header[len('Token '):].strip() + token = ( + ApiToken.objects + .filter(key=key, is_active=True) + .select_related('user') + .first() + ) + if token and token.user.is_active: + token.last_used_at = timezone.now() + token.save(update_fields=['last_used_at']) + return token.user + return None + + +def _unauthorized(): + return JsonResponse( + {"success": False, "error": "未认证或令牌无效,请在请求头中携带有效的 Token(Authorization: Token )。"}, + status=401, + headers={'WWW-Authenticate': 'Token'}, + ) + + +def validate_generate_params(data): + """ + 按与 GenerateForm 相同的约束校验 JSON API 参数。 + + Returns: + (cleaned_data, errors) + """ + errors = {} + cleaned = {} + + # 必填字符串字段 + exename = data.get('exename', '') + if not exename: + errors['exename'] = '该字段为必填项。' + else: + cleaned['exename'] = exename + + # 枚举字段 + choice_validations = { + 'platform': (PLATFORM_CHOICES, 'windows'), + 'direction': (DIRECTION_CHOICES, 'both'), + 'installation': (INSTALLATION_CHOICES, 'installationY'), + 'settings': (SETTINGS_CHOICES, 'settingsY'), + 'theme': (THEME_CHOICES, 'system'), + 'themeDorO': (THEME_DORO_CHOICES, 'default'), + 'passApproveMode': (PASS_APPROVE_MODE_CHOICES, 'password-click'), + 'permissionsDorO': (PERMISSIONS_DORO_CHOICES, 'default'), + 'permissionsType': (PERMISSIONS_TYPE_CHOICES, 'custom'), + } + for field, (choices, default) in choice_validations.items(): + value = data.get(field, default) + if value not in choices: + errors[field] = f'取值无效,必须为以下之一:{choices}' + else: + cleaned[field] = value + + # 版本:master 或形如 1.4.9 / 1.5.0-rc.1 的官方 tag(列表动态获取,不做硬枚举) + version = data.get('version', '1.4.9') + if not is_valid_version(version): + errors['version'] = "版本号无效,应为 'master' 或形如 1.4.9 的官方发布 tag。" + else: + cleaned['version'] = version + + # 布尔字段 + for field in BOOL_FIELDS: + value = data.get(field, False) + if not isinstance(value, bool): + errors[field] = '必须为布尔值(true/false)。' + else: + cleaned[field] = value + + # 可选字符串字段 + for field in OPTIONAL_STR_FIELDS: + cleaned[field] = data.get(field, '') + + # 自由文本名称会进入 shell sed 脚本(与表单校验规则一致) + for field in ('appname', 'compname'): + value = cleaned.get(field, '') + if isinstance(value, str) and re.search(r'[&\\|\'"$`\r\n]', value): + errors[field] = '包含构建脚本不支持的字符(& \\ | \' " $ ` 或换行符)。' + + # API 模式不使用文件上传,改用 base64 字段 + cleaned['iconfile'] = None + cleaned['logofile'] = None + cleaned['privacyfile'] = None + + return cleaned, errors + + +@csrf_exempt +def api_generate(request): + """ + POST /api/generate + + 接收客户端配置 JSON,触发 GitHub Actions 自定义构建。 + 需在请求头携带:Authorization: Token + """ + if request.method != 'POST': + return JsonResponse({"success": False, "error": "仅支持 POST 请求。"}, status=405) + + user = _authenticate(request) + if user is None: + return _unauthorized() + + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError) as e: + return JsonResponse({"success": False, "error": f"JSON 格式无效:{str(e)}"}, status=400) + + cleaned, errors = validate_generate_params(data) + if errors: + return JsonResponse({ + "success": False, + "error": "参数校验未通过", + "details": errors + }, status=400) + + full_url = f"{app_settings.get_value('PROTOCOL')}://{request.get_host()}" + + result = generate_custom_client(cleaned, full_url, user=user) + + if result['success']: + result['status_url'] = f"/api/status?uuid={result['uuid']}&platform={result['platform']}&filename={result['filename']}" + return JsonResponse(result) + else: + return JsonResponse({"success": False, "error": result['error']}, status=result.get('status_code', 500)) + + +@csrf_exempt +def api_status(request): + """ + GET /api/status?uuid= + + 查询指定构建任务的状态。需携带 API Token。 + """ + if request.method != 'GET': + return JsonResponse({"success": False, "error": "仅支持 GET 请求。"}, status=405) + + if _authenticate(request) is None: + return _unauthorized() + + uuid_val = request.GET.get('uuid') + if not uuid_val: + return JsonResponse({"success": False, "error": "缺少必填参数:uuid"}, status=400) + + filename = request.GET.get('filename', '') + platform = request.GET.get('platform', '') + + result = _get_run_status(uuid_val) + + if not result['found']: + return JsonResponse({"success": False, "error": "未找到对应的构建任务"}, status=404) + + response_data = { + "success": True, + "status": result['status'], + "status_label": result['gh_run'].status_label(), + "uuid": uuid_val, + "log_url": result['github_log_url'], + } + if filename: + response_data['filename'] = filename + if platform: + response_data['platform'] = platform + + return JsonResponse(response_data) diff --git a/rdgenerator/app_settings.py b/rdgenerator/app_settings.py new file mode 100644 index 0000000..e832d9a --- /dev/null +++ b/rdgenerator/app_settings.py @@ -0,0 +1,306 @@ +""" +GitHub / 构建相关站点配置的统一读取层。 + +取值优先级: + 1. 后台「GitHub 构建配置」页保存的值(SiteSetting 表) + 2. 环境变量(docker-compose / 系统环境,历史部署方式,继续支持) + 3. 内置默认值 + +进程内带 15 秒短缓存,避免每次调用都查库;后台保存后立即失效当前进程缓存, +其他 gunicorn worker 最迟 15 秒后生效。 +""" +import os +import time + +# 每项:key=配置键 / env=对应环境变量名 / default=内置默认值 / label/help/组 供后台页渲染 +SETTING_DEFS = [ + { + 'key': 'GHUSER', + 'env': 'GHUSER', + 'default': '', + 'label': 'GitHub 用户名', + 'group': 'basic', + 'sensitive': False, + 'help': 'Fork 本项目仓库所在的 GitHub 账号用户名(或组织名)。', + }, + { + 'key': 'REPONAME', + 'env': 'REPONAME', + 'default': 'rdgen', + 'label': '仓库名称', + 'group': 'basic', + 'sensitive': False, + 'help': 'Fork 后的仓库名,默认为 rdgen;若改名请如实填写。', + }, + { + 'key': 'GHBRANCH', + 'env': 'GHBRANCH', + 'default': 'master', + 'label': '工作流分支', + 'group': 'basic', + 'sensitive': False, + 'help': '触发 GitHub Actions 时使用的分支名,通常保持 master。', + }, + { + 'key': 'GHBEARER', + 'env': 'GHBEARER', + 'default': '', + 'label': 'GitHub 访问令牌(Fine-grained PAT)', + 'group': 'basic', + 'sensitive': True, + 'help': '需对该仓库授予 Actions 与 Workflows 的读写权限。页面仅显示保存状态,不会回显明文。', + }, + { + 'key': 'GENURL', + 'env': 'GENURL', + 'default': '', + 'label': '本平台外部访问地址', + 'group': 'callback', + 'sensitive': False, + 'help': '运行 Actions 的构建机可从网络访问到的本平台地址,例如 https://rdgen.example.com,用于回传构建产物。', + }, + { + 'key': 'PROTOCOL', + 'env': 'PROTOCOL', + 'default': 'https', + 'label': '平台访问协议', + 'group': 'callback', + 'sensitive': False, + 'choices': [('https', 'https'), ('http', 'http')], + 'help': '经 Nginx/Caddy 等 HTTPS 反向代理时保持 https;本地纯 HTTP 部署选 http。', + }, + { + 'key': 'ZIP_PASSWORD', + 'env': 'ZIP_PASSWORD', + 'default': 'insecure', + 'label': '配置压缩包密码', + 'group': 'callback', + 'sensitive': True, + 'help': '本平台用它加密传给 Actions 的配置包,必须与构建仓库 Secret 中的 ZIP_PASSWORD 完全一致(GitHub 与 Gitea 仓库均需配置)。默认值不安全,建议更换。', + }, + { + 'key': 'SH_SECRET', + 'env': 'SH_SECRET', + 'default': 'secret', + 'label': '自托管 Runner 密钥', + 'group': 'advanced', + 'sensitive': True, + 'help': '仅在使用自托管 GitHub Runner(rdgen-cli 的 sh_secret_field)时需要,默认值不安全。', + }, + { + 'key': 'WEBHOOK_SECRET', + 'env': 'WEBHOOK_SECRET', + 'default': '', + 'label': '回调校验密钥', + 'group': 'advanced', + 'sensitive': True, + 'help': '可选。配置后构建平台(GitHub / Gitea)Actions 回调必须携带 X-Webhook-Secret 请求头;留空则不校验(兼容旧工作流)。', + }, + { + 'key': 'GH_PROXY', + 'env': 'GH_PROXY', + 'default': '', + 'label': 'GitHub 访问代理', + 'group': 'network', + 'sensitive': True, + 'help': ( + '本服务器直连 GitHub 失败时填写,例如 http://127.0.0.1:7890;' + '支持 socks5://(需 PySocks)。可包含账密,如 http://user:pass@host:port。' + '留空则直连(仍遵循容器的 HTTPS_PROXY/HTTP_PROXY 环境变量)。' + ), + }, + # ===== 构建平台选择(GitHub / Gitea 二选一) ===== + { + 'key': 'BUILD_PLATFORM', + 'env': 'BUILD_PLATFORM', + 'default': 'github', + 'label': '构建平台', + 'group': 'platform', + 'sensitive': False, + 'choices': [('github', 'GitHub Actions'), ('gitea', 'Gitea Actions(自建)')], + 'help': ( + '选择实际执行构建任务的平台。切换后仅影响新提交的任务,' + '历史任务仍在原平台查询状态与日志。Gitea 需自建实例并自备构建机(Linux/Windows/macOS)。' + ), + }, + # ===== Gitea 连接(BUILD_PLATFORM=gitea 时生效) ===== + { + 'key': 'GITEA_SERVER_URL', + 'env': 'GITEA_SERVER_URL', + 'default': '', + 'label': 'Gitea 服务器地址', + 'group': 'gitea', + 'sensitive': False, + 'help': '自建 Gitea 的访问地址,例如 https://gitea.example.com(不带末尾斜杠);建议版本不低于 1.27。', + }, + { + 'key': 'GITEA_OWNER', + 'env': 'GITEA_OWNER', + 'default': '', + 'label': 'Gitea 仓库属主', + 'group': 'gitea', + 'sensitive': False, + 'help': '存放构建仓库的 Gitea 用户名或组织名。', + }, + { + 'key': 'GITEA_REPO', + 'env': 'GITEA_REPO', + 'default': 'rdgen', + 'label': 'Gitea 仓库名称', + 'group': 'gitea', + 'sensitive': False, + 'help': 'Gitea 上的构建仓库名,默认 rdgen;仓库需在设置中启用 Repository Actions。', + }, + { + 'key': 'GITEA_BRANCH', + 'env': 'GITEA_BRANCH', + 'default': 'master', + 'label': 'Gitea 工作流分支', + 'group': 'gitea', + 'sensitive': False, + 'help': '触发 Gitea Actions 时使用的分支名,通常保持 master。', + }, + { + 'key': 'GITEA_TOKEN', + 'env': 'GITEA_TOKEN', + 'default': '', + 'label': 'Gitea 访问令牌', + 'group': 'gitea', + 'sensitive': True, + 'help': '在 Gitea 个人设置中生成的 API 令牌,需对构建仓库授予 Actions 读写权限。页面仅显示保存状态,不会回显明文。', + }, + { + 'key': 'GITEA_PROXY', + 'env': 'GITEA_PROXY', + 'default': '', + 'label': 'Gitea 访问代理', + 'group': 'gitea', + 'sensitive': True, + 'help': ( + '服务器访问 Gitea 需要代理时填写;Gitea 通常部署在同内网,多数情况留空直连即可。' + ), + }, + # ===== 任务维护(后台「维护设置」页使用,不渲染在 GitHub 配置页) ===== + { + 'key': 'BUILD_RETENTION_DAYS', + 'env': 'BUILD_RETENTION_DAYS', + 'default': '30', + 'label': '构建任务保留天数', + 'group': 'maintenance', + 'sensitive': False, + 'help': '超过该天数的已结束任务(成功/失败/取消)及其安装包产物会被自动清理;0 表示永不自动清理。', + }, + { + 'key': 'DB_BACKUP_ENABLED', + 'env': 'DB_BACKUP_ENABLED', + 'default': '1', + 'label': '启用数据库定时备份', + 'group': 'maintenance', + 'sensitive': False, + 'help': '开启后维护进程每天自动备份一次数据库(含账号、配置方案、构建记录)。', + }, + { + 'key': 'DB_BACKUP_KEEP', + 'env': 'DB_BACKUP_KEEP', + 'default': '7', + 'label': '数据库备份保留份数', + 'group': 'maintenance', + 'sensitive': False, + 'help': '超出份数的最旧备份会被自动删除,建议 7~30。', + }, +] + +DEFS_BY_KEY = {d['key']: d for d in SETTING_DEFS} + +SOURCE_DB = 'db' # 后台配置 +SOURCE_ENV = 'env' # 环境变量 +SOURCE_DEFAULT = 'default' # 内置默认值 + +SOURCE_LABELS = { + SOURCE_DB: '后台配置', + SOURCE_ENV: '环境变量', + SOURCE_DEFAULT: '默认值', +} + +_CACHE_TTL = 15.0 +_cache = {'ts': 0.0, 'rows': {}} + + +def _load_rows(): + """一次性读取全部后台配置,带短缓存。""" + now = time.time() + if now - _cache['ts'] > _CACHE_TTL: + try: + from .models import SiteSetting + _cache['rows'] = dict(SiteSetting.objects.values_list('key', 'value')) + except Exception: + # 迁移尚未执行等异常情况下退回环境变量,避免阻断请求 + _cache['rows'] = {} + _cache['ts'] = now + return _cache['rows'] + + +def invalidate_cache(): + _cache['ts'] = 0.0 + + +def get_source(key): + """返回某项当前生效值的来源:db / env / default。""" + d = DEFS_BY_KEY[key] + rows = _load_rows() + if key in rows: + return SOURCE_DB + if os.environ.get(d['env']) is not None: + return SOURCE_ENV + return SOURCE_DEFAULT + + +def get_value(key): + """按 后台配置 → 环境变量 → 默认值 的优先级返回生效值(始终为字符串)。""" + d = DEFS_BY_KEY[key] + rows = _load_rows() + if key in rows: + return rows[key] + return os.environ.get(d['env'], d['default']) + + +def is_overridden(key): + return get_source(key) == SOURCE_DB + + +def set_value(key, value): + """写入后台配置覆盖。""" + from .models import SiteSetting + SiteSetting.objects.update_or_create(key=key, defaults={'value': value}) + invalidate_cache() + + +def delete_value(key): + """删除后台覆盖,回退到环境变量/默认值。""" + from .models import SiteSetting + SiteSetting.objects.filter(key=key).delete() + invalidate_cache() + + +def all_settings(): + """供后台页面渲染:每项定义 + 当前生效值 + 来源。""" + result = [] + for d in SETTING_DEFS: + result.append({ + **d, + 'value': get_value(d['key']), + 'source': get_source(d['key']), + }) + return result + + +def is_github_configured(): + """当前所选构建平台是否具备发起构建的最低配置(保留函数名兼容既有调用)。""" + from .build_backends import get_backend + return get_backend().configured() + + +def config_warnings(): + """返回当前所选构建平台的配置隐患列表(中文),供页面提示。""" + from .build_backends import get_backend + return get_backend().warnings() diff --git a/rdgenerator/apps.py b/rdgenerator/apps.py new file mode 100644 index 0000000..4cb2d0b --- /dev/null +++ b/rdgenerator/apps.py @@ -0,0 +1,7 @@ +from django.apps import AppConfig + + +class RdgeneratorConfig(AppConfig): + default_auto_field = 'django.db.models.BigAutoField' + name = 'rdgenerator' + verbose_name = 'RustDesk 客户端生成' diff --git a/rdgenerator/build_backends/__init__.py b/rdgenerator/build_backends/__init__.py new file mode 100644 index 0000000..2485356 --- /dev/null +++ b/rdgenerator/build_backends/__init__.py @@ -0,0 +1,39 @@ +""" +构建后端抽象层:把 GitHub Actions / Gitea Actions 的触发、状态查询、 +日志链接、连接测试收敛为统一接口,业务代码不再感知平台差异。 + +通过 app_settings 的 BUILD_PLATFORM(后台配置 → 环境变量 → 默认值)选择后端; +每个 GithubRun 记录自身 backend,切换平台后历史任务仍走原平台查询。 +""" +from .. import app_settings +from .github import GitHubBackend +from .gitea import GiteaBackend + +# 已支持的构建平台(Gitee Go 经评估不兼容,未纳入) +BACKENDS = { + 'github': GitHubBackend, + 'gitea': GiteaBackend, +} + +PLATFORM_CHOICES = [ + ('github', 'GitHub Actions'), + ('gitea', 'Gitea Actions(自建)'), +] + + +def current_platform(): + """返回当前生效的构建平台标识(非法值回退 github)。""" + name = app_settings.get_value('BUILD_PLATFORM').strip().lower() + return name if name in BACKENDS else 'github' + + +def get_backend(name=None): + """按名称获取后端实例;name 为空时取当前配置平台。 + + 后端实例无状态(每次调用实时读取设置),直接新建即可, + 这样后台修改配置后无需重启即可生效。 + """ + if not name: + name = current_platform() + cls = BACKENDS.get(name, GitHubBackend) + return cls() diff --git a/rdgenerator/build_backends/base.py b/rdgenerator/build_backends/base.py new file mode 100644 index 0000000..3c54092 --- /dev/null +++ b/rdgenerator/build_backends/base.py @@ -0,0 +1,87 @@ +"""构建后端统一接口与共享逻辑。""" +from dataclasses import dataclass + + +class BackendError(Exception): + """触发构建失败时抛出,message 为可直接展示给用户的中文提示。""" + + def __init__(self, message, *, kind='error'): + super().__init__(message) + self.message = message + # rejected=平台明确拒绝(4xx/5xx);network=连不上平台 + self.kind = kind + + +@dataclass +class DispatchResult: + run_id: object = None # 平台返回的运行编号(int 或 None) + html_url: str = '' # 平台上的运行详情页地址 + + +@dataclass +class RunInfo: + finished: bool # 是否已进入终态 + status: str = '' # 终态结论:success/failure/cancelled/skipped;未结束为进行中状态 + html_url: str = '' + + +# rdgen 目标平台 -> 工作流文件名(两平台同名) +WORKFLOW_FILES = { + 'windows': 'generator-windows.yml', + 'windows-x86': 'generator-windows-x86.yml', + 'linux': 'generator-linux.yml', + 'android': 'generator-android.yml', + 'macos': 'generator-macos.yml', +} + + +class BuildBackend: + """各构建平台实现此接口。所有配置均通过 app_settings 实时读取。""" + + name = '' + label = '' + + # ---- 配置健康度(后台页 / 全局告警条使用) ---- + def configured(self) -> bool: + raise NotImplementedError + + def warnings(self) -> list: + """配置隐患的中文提示列表;通用项(GENURL/ZIP_PASSWORD)各实现自行包含。""" + raise NotImplementedError + + # ---- 构建任务 ---- + def workflow_file_for(self, platform, selfhosted=False) -> str: + raise NotImplementedError + + def dispatch(self, workflow_file, inputs, *, timeout=20) -> DispatchResult: + """触发工作流。inputs 为完整输入字典(GitHub/Gitea 均为 {ref, inputs})。""" + raise NotImplementedError + + def get_run(self, run_id, *, timeout=12): + """查询单次运行;网络异常或非 200 返回 None(调用方按「仍在进行」处理)。""" + raise NotImplementedError + + def log_url(self, run_id) -> str: + raise NotImplementedError + + # ---- 后台连接测试 ---- + def test_connection(self) -> dict: + """返回 {'ok': bool, 'message': 中文说明},不抛异常。""" + raise NotImplementedError + + # ---- 共享的配置检查 ---- + def _shared_warnings(self, *, include_sh_secret=False): + """两平台共用的回调/加密检查。""" + from .. import app_settings + warnings = [] + if not app_settings.get_value('GENURL').strip(): + warnings.append('平台外部访问地址(GENURL)未配置,构建机将无法回传构建产物。') + zip_password = app_settings.get_value('ZIP_PASSWORD') + if zip_password in ('', 'insecure'): + warnings.append( + '配置压缩包密码为空或仍为默认值 insecure,建议设置随机强密码,' + f'并同步更新{self.label}仓库 Secret。' + ) + if include_sh_secret and app_settings.get_value('SH_SECRET') in ('', 'secret'): + warnings.append('自托管 Runner 密钥为空或仍为默认值 secret;如使用自托管 Runner,建议设置独立强密钥,避免被冒用触发。') + return warnings diff --git a/rdgenerator/build_backends/gitea.py b/rdgenerator/build_backends/gitea.py new file mode 100644 index 0000000..6ad34cd --- /dev/null +++ b/rdgenerator/build_backends/gitea.py @@ -0,0 +1,230 @@ +"""Gitea Actions 构建后端(自建 Gitea 1.23+,建议 1.27+)。 + +API 与 GitHub Actions 几乎同形(已对照 Gitea 官方 Swagger 核实): +- 触发:POST /api/v1/repos/{owner}/{repo}/actions/workflows/{file}/dispatches + ?return_run_details=true,body {ref, inputs},200 返回 + {workflow_run_id, html_url, run_url},不传该参数为 204; +- 状态:GET /api/v1/repos/{owner}/{repo}/actions/runs/{id}, + status/conclusion 字段仿 GitHub; +- 鉴权:Authorization: token 。 +""" +from .. import app_settings, ghnet +from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend + +# Gitea 未结束状态(blocked=等待审批,waiting=等待 runner) +_GITEA_ACTIVE = {'queued', 'in_progress', 'waiting', 'blocked', 'requested', 'pending'} +# Gitea 结论 -> rdgen 内部状态 +_CONCLUSION_MAP = { + 'success': 'success', + 'failure': 'failure', + 'cancelled': 'cancelled', + 'skipped': 'skipped', + 'timed_out': 'timed_out', +} + + +class GiteaBackend(BuildBackend): + name = 'gitea' + label = 'Gitea' + + # ---- 取值辅助 ---- + def _server(self): + return app_settings.get_value('GITEA_SERVER_URL').strip().rstrip('/') + + def _owner(self): + return app_settings.get_value('GITEA_OWNER').strip() + + def _repo(self): + return app_settings.get_value('GITEA_REPO').strip() or 'rdgen' + + def _branch(self): + return app_settings.get_value('GITEA_BRANCH').strip() or 'master' + + def _ref(self): + branch = self._branch() + return branch if branch.startswith('refs/') else f'refs/heads/{branch}' + + def _proxy(self): + return app_settings.get_value('GITEA_PROXY').strip() + + def _api(self, path): + return f'{self._server()}/api/v1{path}' + + # ---- 配置 ---- + def configured(self): + return bool(self._server() and self._owner() and app_settings.get_value('GITEA_TOKEN').strip()) + + def warnings(self): + warnings = [] + if not self._server(): + warnings.append('Gitea 服务器地址未配置,无法触发构建。') + if not self._owner(): + warnings.append('Gitea 仓库属主未配置,无法触发构建。') + if not app_settings.get_value('GITEA_TOKEN').strip(): + warnings.append('Gitea 访问令牌未配置,无法触发构建。') + warnings.extend(self._shared_warnings()) + return warnings + + # ---- 工作流选择 ---- + def workflow_file_for(self, platform, selfhosted=False): + # Gitea 的 runner 全部为自建注册,无需 sh- 变体,统一用标准工作流 + return WORKFLOW_FILES.get(platform, 'generator-windows.yml') + + # ---- 触发 ---- + def dispatch(self, workflow_file, inputs, *, timeout=20): + if not self.configured(): + raise BackendError('Gitea 构建服务未完成配置(服务器地址 / 属主 / 令牌缺失)。', kind='config') + url = self._api( + f'/repos/{self._owner()}/{self._repo()}' + f'/actions/workflows/{workflow_file}/dispatches?return_run_details=true' + ) + payload = {'ref': self._ref(), 'inputs': inputs} + try: + resp = ghnet.post( + url, json=payload, + token=app_settings.get_value('GITEA_TOKEN'), + timeout=timeout, flavor='gitea', proxy=self._proxy(), + ) + except Exception as e: + raise BackendError(f'无法连接 Gitea 构建服务:{e}', kind='network') + + if resp.status_code in (200, 204): + data = {} + try: + data = resp.json() + except ValueError: + pass + run_id = data.get('workflow_run_id') + html_url = data.get('html_url') or '' + # 旧版 Gitea 或未回传详情时,按工作流最新一条运行兜底取 run id + if not run_id and resp.status_code == 204: + fallback = self._latest_run_id(workflow_file) + if fallback: + run_id, html_url = fallback + return DispatchResult(run_id=run_id, html_url=html_url or self.log_url(run_id)) + + detail = '' + try: + detail = (resp.text or '')[:200] + except Exception: + pass + if resp.status_code == 404: + msg = f'Gitea 仓库或工作流 {workflow_file} 不存在(请确认仓库已启用 Repository Actions)。' + elif resp.status_code in (401, 403): + msg = 'Gitea 拒绝访问:请检查令牌是否有效且具备该仓库 Actions 读写权限。' + elif resp.status_code == 422: + msg = f'Gitea 校验失败(分支或工作流参数有误):{detail}' + else: + msg = f'Gitea 构建服务拒绝了启动请求(HTTP {resp.status_code}):{detail}' + raise BackendError(msg, kind='rejected') + + def _latest_run_id(self, workflow_file, *, timeout=10): + """204 兜底:查该工作流最新一条运行(仅在刚触发后调用)。""" + url = self._api( + f'/repos/{self._owner()}/{self._repo()}' + f'/actions/workflows/{workflow_file}/runs?limit=1' + ) + try: + resp = ghnet.get( + url, token=app_settings.get_value('GITEA_TOKEN'), + timeout=timeout, flavor='gitea', proxy=self._proxy(), + ) + if resp.status_code != 200: + return None + data = resp.json() + runs = data.get('workflow_runs') if isinstance(data, dict) else data + if runs: + run = runs[0] + return run.get('id'), run.get('html_url') or '' + except Exception as e: + print(f'查询 Gitea 最新运行失败:{e}') + return None + + # ---- 状态查询 ---- + def get_run(self, run_id, *, timeout=12): + url = self._api(f'/repos/{self._owner()}/{self._repo()}/actions/runs/{run_id}') + try: + resp = ghnet.get( + url, token=app_settings.get_value('GITEA_TOKEN'), + timeout=timeout, flavor='gitea', proxy=self._proxy(), + ) + except Exception as e: + print(f'查询 Gitea 状态出错:{e}') + return None + if resp.status_code != 200: + return None + data = resp.json() + html_url = data.get('html_url') or self.log_url(run_id) + status = (data.get('status') or '').lower() + conclusion = (data.get('conclusion') or '').lower() + if status == 'completed': + return RunInfo( + finished=True, + status=_CONCLUSION_MAP.get(conclusion, 'failure'), + html_url=html_url, + ) + # blocked(等待审批)等也算进行中 + return RunInfo(finished=False, status=status or 'in_progress', html_url=html_url) + + # ---- 日志链接 ---- + def log_url(self, run_id): + if not run_id: + return '' + return f'{self._server()}/{self._owner()}/{self._repo()}/actions/runs/{run_id}' + + # ---- 连接测试 ---- + def test_connection(self): + server = self._server() + owner = self._owner() + token = app_settings.get_value('GITEA_TOKEN').strip() + repo = self._repo() + if not server or not owner or not token: + return {'ok': False, 'message': 'Gitea 服务器地址、仓库属主或访问令牌为空,请先保存配置。'} + if not (server.startswith('http://') or server.startswith('https://')): + return {'ok': False, 'message': 'Gitea 服务器地址需以 http:// 或 https:// 开头。'} + try: + resp = ghnet.get( + self._api(f'/repos/{owner}/{repo}'), + token=token, timeout=10, flavor='gitea', proxy=self._proxy(), + ) + if resp.status_code == 401: + return {'ok': False, 'message': '令牌无效或已过期(Gitea 返回 401)。'} + if resp.status_code == 404: + return {'ok': False, + 'message': f'找不到 Gitea 仓库 {owner}/{repo},或令牌无权访问该仓库。'} + if resp.status_code != 200: + return {'ok': False, 'message': f'Gitea 返回异常状态码:{resp.status_code}。'} + + act = ghnet.get( + self._api(f'/repos/{owner}/{repo}/actions/workflows'), + token=token, timeout=10, flavor='gitea', proxy=self._proxy(), + ) + if act.status_code != 200: + return {'ok': False, + 'message': '可以访问仓库,但无法读取 Actions 工作流;请在仓库设置中启用 Repository Actions。'} + + # 版本检查:1.23 起支持 workflow_dispatch;建议 1.27+(return_run_details 与缓存兼容性更好) + version_msg = '' + ver = ghnet.get( + self._api('/version'), + token=token, timeout=10, flavor='gitea', proxy=self._proxy(), + ) + if ver.status_code == 200: + raw = (ver.json().get('version') or '').strip() + parts = raw.replace('+dev', '').split('.') + try: + major, minor = int(parts[0]), int(parts[1]) + if (major, minor) < (1, 23): + return {'ok': False, + 'message': f'Gitea 版本为 {raw},低于 1.23,不支持 workflow_dispatch 远程触发,请先升级。'} + if (major, minor) < (1, 27): + version_msg = f'当前 Gitea 版本为 {raw},可触发构建,但建议升级到 1.27+(运行详情回传与缓存兼容性更好)。' + except (ValueError, IndexError): + pass + + msg = f'连接正常:令牌可访问仓库 {owner}/{repo},Actions 已启用。' + if version_msg: + msg += version_msg + return {'ok': True, 'message': msg} + except Exception as e: + return {'ok': False, 'message': f'无法连接 Gitea API:{e}(请检查服务器地址与网络/代理配置)'} diff --git a/rdgenerator/build_backends/github.py b/rdgenerator/build_backends/github.py new file mode 100644 index 0000000..885c57d --- /dev/null +++ b/rdgenerator/build_backends/github.py @@ -0,0 +1,110 @@ +"""GitHub Actions 构建后端(迁自 views.py / web_views.py 的原有逻辑,行为保持一致)。""" +from .. import app_settings, ghnet +from .base import BackendError, DispatchResult, RunInfo, WORKFLOW_FILES, BuildBackend + +API = 'https://api.github.com' + +# 未进入 completed 的状态一律视为进行中 +_GITHUB_ACTIVE = {'queued', 'in_progress', 'waiting', 'requested', 'pending'} + + +class GitHubBackend(BuildBackend): + name = 'github' + label = 'GitHub' + + # ---- 配置 ---- + def configured(self): + return bool(app_settings.get_value('GHUSER').strip()) \ + and bool(app_settings.get_value('GHBEARER').strip()) + + def warnings(self): + warnings = [] + if not app_settings.get_value('GHUSER').strip(): + warnings.append('GitHub 用户名未配置,无法触发构建。') + if not app_settings.get_value('GHBEARER').strip(): + warnings.append('GitHub 访问令牌未配置,无法触发构建。') + warnings.extend(self._shared_warnings(include_sh_secret=True)) + return warnings + + # ---- 工作流选择(windows selfhosted 走 sh-generator-windows.yml) ---- + def workflow_file_for(self, platform, selfhosted=False): + if selfhosted and platform in ('windows',): + return 'sh-generator-windows.yml' + return WORKFLOW_FILES.get(platform, 'generator-windows.yml') + + # ---- 触发 ---- + def dispatch(self, workflow_file, inputs, *, timeout=20): + user = app_settings.get_value('GHUSER') + repo = app_settings.get_value('REPONAME') + url = f'{API}/repos/{user}/{repo}/actions/workflows/{workflow_file}/dispatches' + payload = { + 'ref': app_settings.get_value('GHBRANCH'), + 'inputs': inputs, + 'return_run_details': True, + } + token = app_settings.get_value('GHBEARER') + try: + resp = ghnet.post(url, json=payload, token=token, timeout=timeout) + except Exception as e: + raise BackendError(f'无法连接 GitHub 构建服务:{e}', kind='network') + if resp.status_code not in (200, 204): + raise BackendError('GitHub 构建服务拒绝了启动请求,请检查服务配置或稍后重试。', kind='rejected') + data = {} + try: + data = resp.json() + except ValueError: + pass + return DispatchResult(run_id=data.get('workflow_run_id'), html_url=data.get('html_url') or '') + + # ---- 状态查询 ---- + def get_run(self, run_id, *, timeout=12): + user = app_settings.get_value('GHUSER') + repo = app_settings.get_value('REPONAME') + url = f'{API}/repos/{user}/{repo}/actions/runs/{run_id}' + try: + resp = ghnet.get(url, token=app_settings.get_value('GHBEARER'), timeout=timeout) + except Exception as e: + print(f'查询 GitHub 状态出错:{e}') + return None + if resp.status_code != 200: + return None + data = resp.json() + if data.get('status') == 'completed': + return RunInfo(finished=True, status=data.get('conclusion') or 'failure', + html_url=data.get('html_url') or self.log_url(run_id)) + return RunInfo(finished=False, status=data.get('status') or 'in_progress', + html_url=data.get('html_url') or self.log_url(run_id)) + + # ---- 日志链接 ---- + def log_url(self, run_id): + if not run_id: + return '' + return ( + f"https://github.com/{app_settings.get_value('GHUSER')}" + f"/{app_settings.get_value('REPONAME')}/actions/runs/{run_id}" + ) + + # ---- 连接测试 ---- + def test_connection(self): + user = app_settings.get_value('GHUSER').strip() + repo = app_settings.get_value('REPONAME').strip() + token = app_settings.get_value('GHBEARER').strip() + if not user or not token: + return {'ok': False, 'message': 'GitHub 用户名或访问令牌为空,请先保存配置。'} + try: + resp = ghnet.get(f'{API}/repos/{user}/{repo}', token=token, timeout=10) + if resp.status_code == 401: + return {'ok': False, 'message': '令牌无效或已过期(GitHub 返回 401)。'} + if resp.status_code == 404: + return {'ok': False, + 'message': f'找不到仓库 {user}/{repo},或令牌无权访问该仓库(请确认仓库名与令牌的 Repository access)。'} + if resp.status_code != 200: + return {'ok': False, 'message': f'GitHub 返回异常状态码:{resp.status_code}。'} + act = ghnet.get(f'{API}/repos/{user}/{repo}/actions/workflows', token=token, timeout=10) + if act.status_code == 200: + return {'ok': True, 'message': f'连接正常:令牌可访问仓库 {user}/{repo},且具备 Actions 权限。'} + return {'ok': False, + 'message': '可以访问仓库,但令牌缺少 Actions 权限;请编辑令牌,授予 Actions 与 Workflows 的读写权限。'} + except Exception as e: + return {'ok': False, + 'message': f'无法连接 GitHub API:{e}(如服务器在国内网络,请在下方配置代理后重试)'} diff --git a/rdgenerator/context_processors.py b/rdgenerator/context_processors.py new file mode 100644 index 0000000..939bf38 --- /dev/null +++ b/rdgenerator/context_processors.py @@ -0,0 +1,20 @@ +"""全局模板上下文:当前构建平台的配置状态(用于未配置告警条)。""" +from .build_backends import get_backend + + +def github_config(request): + if not getattr(request.user, 'is_authenticated', False): + return {} + backend = get_backend() + ready = backend.configured() + warnings = backend.warnings() + return { + # 新变量名 + 'build_platform': backend.name, + 'build_platform_label': backend.label, + 'build_ready': ready, + 'build_warnings': warnings, + # 兼容旧模板变量名 + 'github_ready': ready, + 'github_warnings': warnings, + } diff --git a/rdgenerator/forms.py b/rdgenerator/forms.py new file mode 100644 index 0000000..b7ed4d6 --- /dev/null +++ b/rdgenerator/forms.py @@ -0,0 +1,440 @@ +import re + +from django import forms +from django.contrib.auth.models import User +from PIL import Image + +from . import versions as rdeskVersions + +# 应用名/公司名会被插入到单/双引号包裹的 bash sed 脚本中:& \ | 会导致替换 +# 静默失败,' " $ ` 会破坏 shell 引号,CR/LF 会破坏 sed 寻址。 +UNSAFE_NAME_CHARS = re.compile(r'[&\\|\'"$`\r\n]') + +UNSAFE_NAME_MESSAGE = "包含构建脚本不支持的字符(& \\ | ' \" $ ` 或换行符)。" + +# 统一的控件样式:文本/数字/下拉/文件输入框 .form-control,复选/单选 .form-check-input +# 注意:Django 5.x 中 NumberInput 直接继承 Input(而非 TextInput),必须单独列出 +_CONTROL_WIDGETS = ( + forms.TextInput, forms.NumberInput, forms.PasswordInput, forms.EmailInput, + forms.Select, forms.Textarea, forms.FileInput, +) + + +def apply_control_classes(form): + for field in form.fields.values(): + widget = field.widget + if isinstance(widget, _CONTROL_WIDGETS): + widget.attrs.setdefault('class', 'form-control') + elif isinstance(widget, forms.CheckboxInput): + widget.attrs.setdefault('class', 'form-check-input') + + +class GenerateForm(forms.Form): + sh_secret_field = forms.CharField(required=False, widget=forms.HiddenInput) + + # 平台与版本 + platform = forms.ChoiceField( + label="目标平台", + choices=[ + ('windows', 'Windows 64 位'), + ('windows-x86', 'Windows 32 位'), + ('linux', 'Linux'), + ('android', 'Android'), + ('macos', 'macOS'), + ], + initial='windows', + ) + version = forms.ChoiceField( + label="RustDesk 版本", + choices=[('master', '开发版(nightly 每夜构建)')] + [(v, v) for v in rdeskVersions.FALLBACK_VERSIONS], + initial='1.4.9', + help_text=( + "'master' 为开发版(每夜构建),包含最新特性,但稳定性可能不如正式版本。" + "正式版列表自动取自 GitHub 官方仓库 rustdesk/rustdesk,跟随官方发版更新。" + ), + ) + delayFix = forms.BooleanField(label="修复使用第三方 API 时的连接延迟问题", initial=True, required=False) + + # 常规设置 + exename = forms.CharField( + label="配置名称", + required=True, + help_text="仅限英文、数字与下划线,不含空格;将同时作为生成文件的名称。", + ) + appname = forms.CharField( + label="自定义应用名称", + required=False, + help_text="留空则使用默认名称 rustdesk。", + ) + direction = forms.ChoiceField( + label="连接方向", + widget=forms.RadioSelect, + choices=[ + ('incoming', '仅受控(只能被他人远程连接)'), + ('outgoing', '仅主控(只能主动连接他人)'), + ('both', '主控 / 受控双向(默认)'), + ], + initial='both', + ) + installation = forms.ChoiceField( + label="是否允许安装", + choices=[ + ('installationY', '允许安装(默认)'), + ('installationN', '禁止安装(绿色单文件运行)'), + ], + initial='installationY', + ) + settings = forms.ChoiceField( + label="是否允许修改设置", + choices=[ + ('settingsY', '允许修改设置(默认)'), + ('settingsN', '禁止修改设置'), + ], + initial='settingsY', + ) + androidappid = forms.CharField( + label="自定义 Android 应用 ID", + required=False, + help_text="替换默认 com.carriez.flutter_hbb,留空使用默认值。", + ) + + # 自定义服务器 + serverIP = forms.CharField( + label="ID / 中继服务器地址", + required=False, + help_text="填写自建服务器的主机名或 IP。", + ) + serverPort = forms.CharField(label="服务器端口", required=False) + apiServer = forms.CharField( + label="API 服务器地址", + required=False, + help_text="留空时默认使用「服务器地址:21114」。", + ) + key = forms.CharField(label="公钥(Key)", required=False) + urlLink = forms.CharField( + label="自定义官网链接", + required=False, + help_text="替换默认的 https://rustdesk.com。", + ) + downloadLink = forms.CharField( + label="更新下载链接", + required=False, + help_text="替换默认的 https://rustdesk.com/download。", + ) + compname = forms.CharField( + label="版权公司名称", + required=False, + help_text="替换默认的 Purslane Tech Pte. Ltd.。", + ) + + # 外观 + iconfile = forms.FileField( + label="应用图标", + required=False, + widget=forms.FileInput(attrs={'accept': 'image/png'}), + help_text="PNG 格式,建议 256×256 正方形。", + ) + logofile = forms.FileField( + label="应用 Logo", + required=False, + widget=forms.FileInput(attrs={'accept': 'image/png'}), + help_text="PNG 格式。", + ) + privacyfile = forms.FileField( + label="隐私屏图片", + required=False, + widget=forms.FileInput(attrs={'accept': 'image/png'}), + help_text="PNG 格式。", + ) + iconbase64 = forms.CharField(required=False, widget=forms.HiddenInput) + logobase64 = forms.CharField(required=False, widget=forms.HiddenInput) + privacybase64 = forms.CharField(required=False, widget=forms.HiddenInput) + theme = forms.ChoiceField( + label="主题", + choices=[('light', '浅色'), ('dark', '深色'), ('system', '跟随系统')], + initial='system', + ) + themeDorO = forms.ChoiceField( + label="主题生效方式", + choices=[('default', '默认(允许客户端更改)'), ('override', '强制锁定(客户端无法更改)')], + initial='default', + ) + + # 安全 + passApproveMode = forms.ChoiceField( + label="连接确认方式", + choices=[ + ('password', '仅通过密码接受会话'), + ('click', '仅通过点击接受会话'), + ('password-click', '密码或点击均可(默认)'), + ], + initial='password-click', + ) + permanentPassword = forms.CharField( + label="设置固定访问密码", + widget=forms.PasswordInput(), + required=False, + help_text="该密码为客户端默认密码,用户仍可在客户端自行修改。", + ) + denyLan = forms.BooleanField(label="禁止局域网发现", initial=False, required=False) + enableDirectIP = forms.BooleanField(label="允许直接 IP 直连", initial=False, required=False) + autoClose = forms.BooleanField(label="用户无操作时自动断开入站会话", initial=False, required=False) + hidecm = forms.BooleanField(label="允许在远程画面中隐藏连接窗口", initial=False, required=False) + + # 权限 + permissionsDorO = forms.ChoiceField( + label="权限生效方式", + choices=[('default', '默认(用户可更改)'), ('override', '强制锁定(用户无法更改)')], + initial='default', + ) + permissionsType = forms.ChoiceField( + label="权限预设", + choices=[('custom', '自定义'), ('full', '完全控制'), ('view', '仅查看(屏幕共享)')], + initial='custom', + ) + enableKeyboard = forms.BooleanField(label="键盘 / 鼠标控制", initial=True, required=False) + enableClipboard = forms.BooleanField(label="剪贴板同步", initial=True, required=False) + enableFileTransfer = forms.BooleanField(label="文件传输", initial=True, required=False) + enableAudio = forms.BooleanField(label="音频传输", initial=True, required=False) + enableTCP = forms.BooleanField(label="TCP 隧道", initial=True, required=False) + enableRemoteRestart = forms.BooleanField(label="远程重启", initial=True, required=False) + enableRecording = forms.BooleanField(label="会话录制", initial=True, required=False) + enableBlockingInput = forms.BooleanField(label="屏蔽被控端键鼠输入", initial=True, required=False) + enableRemoteModi = forms.BooleanField(label="允许修改远程配置", initial=False, required=False) + enablePrinter = forms.BooleanField(label="远程打印机", initial=True, required=False) + enableCamera = forms.BooleanField(label="远程摄像头", initial=True, required=False) + enableTerminal = forms.BooleanField(label="远程终端", initial=True, required=False) + + # 其他 + removeWallpaper = forms.BooleanField(label="入站会话期间移除桌面壁纸", initial=True, required=False) + defaultManual = forms.CharField( + label="默认高级设置", + widget=forms.Textarea(attrs={'rows': 4}), + required=False, + help_text="每行一项,格式:键=值。", + ) + overrideManual = forms.CharField( + label="强制高级设置", + widget=forms.Textarea(attrs={'rows': 4}), + required=False, + help_text="每行一项,格式:键=值;设置后客户端用户无法修改。", + ) + + # 定制功能 + xOffline = forms.BooleanField(label="通讯录中离线设备显示 X 标记", initial=False, required=False) + removeNewVersionNotif = forms.BooleanField(label="移除新版本更新通知", initial=False, required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apply_control_classes(self) + + def clean_version(self): + # 视图会在绑定前把 choices 刷新为官方最新列表;这里再做宽松校验, + # 兼容「打开页面后列表刷新 / 官方最新 tag 尚未进列表」等窗口期。 + value = self.cleaned_data.get('version', '') + if not rdeskVersions.is_valid_version(value): + raise forms.ValidationError("版本号无效,请从列表中选择或填写形如 1.4.9 的正式版本号。") + return value + + def clean_iconfile(self): + image = self.cleaned_data['iconfile'] + if image: + try: + img = Image.open(image) + if img.format != 'PNG': + raise forms.ValidationError("仅支持 PNG 格式的图标。") + width, height = img.size + if width != height: + raise forms.ValidationError("应用图标必须为正方形图片。") + return image + except forms.ValidationError: + raise + except OSError: + raise forms.ValidationError("图标文件无效或已损坏。") + except Exception as e: + raise forms.ValidationError(f"处理图标时出错:{e}") + + def _reject_unsafe_name_chars(self, field): + value = self.cleaned_data.get(field, '') + if value and UNSAFE_NAME_CHARS.search(value): + raise forms.ValidationError(UNSAFE_NAME_MESSAGE) + return value + + def clean_appname(self): + return self._reject_unsafe_name_chars('appname') + + def clean_compname(self): + return self._reject_unsafe_name_chars('compname') + + +class ApiTokenForm(forms.Form): + name = forms.CharField(label="令牌名称", max_length=100, required=True, + help_text="建议按用途命名,例如:办公网构建机、CI 流水线。") + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apply_control_classes(self) + + +class GitHubSettingsForm(forms.Form): + """后台「构建平台配置」表单(GitHub / Gitea)。 + + 非密钥项直接显示生效值;密钥项不回显明文,留空表示不修改, + 勾选「清除」则删除后台覆盖、回退到环境变量/默认值。 + """ + + # 构建平台 + build_platform = forms.ChoiceField( + label="构建平台", + choices=[('github', 'GitHub Actions'), ('gitea', 'Gitea Actions(自建)')], + widget=forms.RadioSelect, + required=False, + ) + + # Gitea 连接 + gitea_server_url = forms.CharField( + label="Gitea 服务器地址", + max_length=255, + required=False, + widget=forms.TextInput(attrs={'placeholder': 'https://gitea.example.com'}), + ) + gitea_owner = forms.CharField(label="Gitea 仓库属主", max_length=100, required=False) + gitea_repo = forms.CharField(label="Gitea 仓库名称", max_length=100, required=False) + gitea_branch = forms.CharField(label="Gitea 工作流分支", max_length=100, required=False) + gitea_token = forms.CharField( + label="Gitea 访问令牌", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), + required=False, + ) + clear_gitea_token = forms.BooleanField(label="清除后台保存的令牌", required=False) + gitea_proxy = forms.CharField( + label="Gitea 访问代理", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '内网直连通常留空,如 http://host:port'}), + required=False, + ) + clear_gitea_proxy = forms.BooleanField(label="清除后台保存的代理", required=False) + + # GitHub 基础连接 + ghuser = forms.CharField(label="GitHub 用户名", max_length=100, required=False) + reponame = forms.CharField(label="仓库名称", max_length=100, required=False) + ghbranch = forms.CharField(label="工作流分支", max_length=100, required=False) + ghbearer = forms.CharField( + label="GitHub 访问令牌", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), + required=False, + ) + clear_ghbearer = forms.BooleanField(label="清除后台保存的令牌", required=False) + + # 回调与加密 + genurl = forms.CharField( + label="本平台外部访问地址", + max_length=255, + required=False, + widget=forms.TextInput(attrs={'placeholder': 'https://rdgen.example.com'}), + ) + protocol = forms.ChoiceField( + label="平台访问协议", + choices=[('https', 'https'), ('http', 'http')], + required=False, + ) + zip_password = forms.CharField( + label="配置压缩包密码", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), + required=False, + ) + clear_zip_password = forms.BooleanField(label="清除后台保存的密码", required=False) + + # 高级 + sh_secret = forms.CharField( + label="自托管 Runner 密钥", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), + required=False, + ) + clear_sh_secret = forms.BooleanField(label="清除后台保存的密钥", required=False) + webhook_secret = forms.CharField( + label="回调校验密钥", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '已保存时留空表示不修改'}), + required=False, + ) + clear_webhook_secret = forms.BooleanField(label="清除后台保存的密钥", required=False) + + # 网络代理 + gh_proxy = forms.CharField( + label="GitHub 访问代理", + widget=forms.PasswordInput(attrs={'autocomplete': 'new-password', 'placeholder': '留空直连,如 http://127.0.0.1:7890'}), + required=False, + ) + clear_gh_proxy = forms.BooleanField(label="清除后台保存的代理", required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apply_control_classes(self) + + +class MaintenanceSettingsForm(forms.Form): + """后台「维护设置」:构建任务定时清理与数据库定时备份。""" + + build_retention_days = forms.IntegerField( + label="构建任务保留天数", + min_value=0, + max_value=3650, + initial=30, + help_text="超过该天数的已结束任务(成功 / 失败 / 取消)及安装包产物会被自动清理;0 表示永不自动清理。进行中的任务不会被清理。", + ) + db_backup_enabled = forms.BooleanField( + label="启用数据库定时备份(每天自动备份一次)", + required=False, + initial=True, + ) + db_backup_keep = forms.IntegerField( + label="数据库备份保留份数", + min_value=1, + max_value=100, + initial=7, + help_text="超出份数的最旧备份会被自动删除。备份文件保存在持久化数据卷中,可在下方列表下载。", + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apply_control_classes(self) + + +class AdminUserCreateForm(forms.Form): + username = forms.CharField(label="用户名", max_length=150, required=True) + email = forms.EmailField(label="邮箱", required=False) + password = forms.CharField(label="登录密码", widget=forms.PasswordInput, required=True, min_length=8, + help_text="至少 8 位。") + password_confirm = forms.CharField(label="确认密码", widget=forms.PasswordInput, required=True) + is_staff = forms.BooleanField(label="设为管理员(可访问后台管理、查看所有任务)", required=False) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apply_control_classes(self) + + def clean_username(self): + username = self.cleaned_data['username'].strip() + if User.objects.filter(username=username).exists(): + raise forms.ValidationError("该用户名已存在。") + return username + + def clean(self): + cleaned = super().clean() + if cleaned.get('password') and cleaned.get('password') != cleaned.get('password_confirm'): + self.add_error('password_confirm', "两次输入的密码不一致。") + return cleaned + + +class AdminUserEditForm(forms.Form): + email = forms.EmailField(label="邮箱", required=False) + is_active = forms.BooleanField(label="账号启用", required=False) + is_staff = forms.BooleanField(label="管理员权限(可访问后台管理)", required=False) + new_password = forms.CharField( + label="重置密码(留空表示不修改)", + widget=forms.PasswordInput, + required=False, + min_length=8, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + apply_control_classes(self) diff --git a/rdgenerator/ghnet.py b/rdgenerator/ghnet.py new file mode 100644 index 0000000..deb5823 --- /dev/null +++ b/rdgenerator/ghnet.py @@ -0,0 +1,74 @@ +""" +代码托管平台 API 的统一网络层(GitHub / Gitea)。 + +- 所有出站请求集中在此,按 flavor 注入对应平台的推荐请求头与鉴权方式; +- 代理支持显式传入(GitHub 用 GH_PROXY、Gitea 用 GITEA_PROXY); +- 未显式配置代理时 requests 仍会自动遵循容器的 HTTPS_PROXY/HTTP_PROXY 环境变量。 +""" +import requests + +from . import app_settings + +GITHUB_API = 'https://api.github.com' +API_VERSION = '2026-03-10' +DEFAULT_TIMEOUT = 15 + + +def proxies(proxy=None): + """返回 requests 使用的 proxies 字典。 + + proxy=None 时沿用 GitHub 的 GH_PROXY(历史默认); + 显式传 '' 表示直连(环境变量仍生效),传具体地址则使用该代理。 + """ + if proxy is None: + proxy = app_settings.get_value('GH_PROXY').strip() + if proxy: + return {'http': proxy, 'https': proxy} + return {} + + +def build_headers(token=None, json_content=False, flavor='github'): + """按平台风格构建请求头。 + + - github:application/vnd.github+json + 版本头 + Bearer 鉴权; + - gitea:application/json + Gitea 官方文档主推的 token 鉴权方案。 + """ + flavor = (flavor or 'github').lower() + if flavor == 'gitea': + headers = {'Accept': 'application/json'} + if token: + headers['Authorization'] = f'token {token}' + else: + headers = { + 'Accept': 'application/vnd.github+json', + 'X-GitHub-Api-Version': API_VERSION, + } + if token: + headers['Authorization'] = f'Bearer {token}' + if json_content: + headers['Content-Type'] = 'application/json' + return headers + + +def request(method, url, *, token=None, timeout=DEFAULT_TIMEOUT, headers=None, + flavor='github', proxy=None, **kwargs): + """发起平台 API 请求,自动注入代理、超时与标准请求头。""" + kwargs.setdefault('proxies', proxies(proxy)) + kwargs.setdefault('timeout', timeout) + merged_headers = build_headers( + token=token, + json_content=kwargs.get('json') is not None, + flavor=flavor, + ) + if headers: + merged_headers.update(headers) + kwargs['headers'] = merged_headers + return requests.request(method, url, **kwargs) + + +def get(url, **kwargs): + return request('GET', url, **kwargs) + + +def post(url, **kwargs): + return request('POST', url, **kwargs) diff --git a/rdgenerator/maintenance.py b/rdgenerator/maintenance.py new file mode 100644 index 0000000..1bacd03 --- /dev/null +++ b/rdgenerator/maintenance.py @@ -0,0 +1,227 @@ +""" +定时维护:清理过期构建任务(含本地产物)、清理残留临时文件、在线备份 SQLite。 + +被 management 命令(容器内常驻守护进程)与后台「维护设置」页面共用: + run_maintenance(force_backup=False) -> dict # 执行一轮 + maintenance_status() -> dict # 页面展示的状态摘要 +所有设置经 app_settings 统一读取(后台配置 → 环境变量 → 默认值)。 +""" +import os +import shutil +import sqlite3 +import time +from datetime import datetime, timedelta +from pathlib import Path + +from django.conf import settings +from django.utils import timezone + +from . import app_settings +from .models import GithubRun + +# 与 GithubRun.is_finished 保持一致的终态集合 +FINISHED_STATUSES = ('success', 'failure', 'cancelled', 'timed_out', 'skipped') + +# 临时配置包/JSON 的最长保留时间(天),失败任务遗留时兜底清理 +TEMP_FILE_DAYS = 7 + +BACKUP_NAME_PREFIX = 'db-' +BACKUP_NAME_SUFFIX = '.sqlite3' +# 自动备份最小间隔(秒):守护进程每小时醒一次,但一天只备份一次 +BACKUP_INTERVAL = 24 * 3600 + + +def _setting_int(key, default): + try: + return int(str(app_settings.get_value(key)).strip()) + except (TypeError, ValueError): + return default + + +def _setting_bool(key, default=True): + return str(app_settings.get_value(key)).strip().lower() in ('1', 'true', 'yes', 'on', '是') + + +def retention_days(): + """构建任务保留天数;0 表示永不自动清理。""" + return max(0, _setting_int('BUILD_RETENTION_DAYS', 30)) + + +def backup_enabled(): + return _setting_bool('DB_BACKUP_ENABLED', True) + + +def backup_keep(): + return max(1, min(100, _setting_int('DB_BACKUP_KEEP', 7))) + + +def db_path(): + return Path(settings.DATABASES['default']['NAME']) + + +def backup_dir(): + d = db_path().parent / 'backups' + d.mkdir(parents=True, exist_ok=True) + return d + + +def list_backups(): + """返回 [(Path, 大小字节, mtime), ...],按时间倒序。""" + items = [] + for p in backup_dir().glob(f'{BACKUP_NAME_PREFIX}*{BACKUP_NAME_SUFFIX}'): + if p.is_file(): + st = p.stat() + items.append((p, st.st_size, st.st_mtime)) + items.sort(key=lambda x: x[2], reverse=True) + return items + + +def cleanup_expired_builds(days=None): + """删除超过保留天数的终态任务,并同步清理 exe/png 产物。返回删除条数。""" + if days is None: + days = retention_days() + if days <= 0: + return 0 + + cutoff = timezone.now() - timedelta(days=days) + qs = GithubRun.objects.filter(created_at__lt=cutoff, status__in=FINISHED_STATUSES) + count = 0 + for run in qs.iterator(): + if run.uuid: + shutil.rmtree(Path('exe') / run.uuid, ignore_errors=True) + shutil.rmtree(Path('png') / run.uuid, ignore_errors=True) + run.delete() + count += 1 + return count + + +def cleanup_temp_files(days=TEMP_FILE_DAYS): + """清理失败任务遗留的加密配置包与临时 JSON。返回删除文件数。""" + cutoff = time.time() - days * 86400 + count = 0 + targets = [] + zip_dir = Path('temp_zips') + if zip_dir.is_dir(): + targets += list(zip_dir.glob('secrets_*.zip')) + # 早期版本 data_*.json 写在工作目录根下 + targets += list(Path('.').glob('data_*.json')) + for p in targets: + try: + if p.is_file() and p.stat().st_mtime < cutoff: + p.unlink() + count += 1 + except OSError: + pass + return count + + +def backup_database(): + """用 sqlite3 在线备份 API 生成一致性副本,然后按保留份数轮转。 + + 返回备份文件 Path;非 SQLite 数据库或源文件不存在时返回 None。 + """ + src = db_path() + if not src.is_file() or settings.DATABASES['default']['ENGINE'] != 'django.db.backends.sqlite3': + return None + + dest = backup_dir() / f"{BACKUP_NAME_PREFIX}{time.strftime('%Y%m%d-%H%M%S')}{BACKUP_NAME_SUFFIX}" + src_conn = sqlite3.connect(str(src)) + try: + dst_conn = sqlite3.connect(str(dest)) + try: + src_conn.backup(dst_conn) + finally: + dst_conn.close() + finally: + src_conn.close() + + rotate_backups() + return dest + + +def rotate_backups(): + """按设置的保留份数删除最旧备份(每轮维护都执行,含容器启动前的预迁移备份)。""" + deleted = 0 + for old_path, _size, _mtime in list_backups()[backup_keep():]: + try: + old_path.unlink() + deleted += 1 + except OSError: + pass + return deleted + + +def _latest_backup_age(): + backups = list_backups() + if not backups: + return None + return time.time() - backups[0][2] + + +def run_maintenance(force_backup=False): + """执行一轮维护。返回各项动作的计数/结果(供命令行与页面反馈)。""" + result = { + 'deleted_builds': cleanup_expired_builds(), + 'deleted_temp_files': cleanup_temp_files(), + 'rotated_backups': rotate_backups(), + 'backup_created': None, + 'backup_skipped': False, + } + if backup_enabled() and (force_backup or (_latest_backup_age() or BACKUP_INTERVAL) >= BACKUP_INTERVAL): + dest = backup_database() + result['backup_created'] = str(dest) if dest else None + if dest is None: + result['backup_skipped'] = True + else: + result['backup_skipped'] = True + return result + + +def _dir_size(path): + total = 0 + base = Path(path) + if not base.is_dir(): + return 0 + for root, _dirs, files in os.walk(base): + for name in files: + try: + total += (Path(root) / name).stat().st_size + except OSError: + pass + return total + + +def maintenance_status(): + """后台维护页展示用摘要。""" + days = retention_days() + total = GithubRun.objects.count() + in_progress = GithubRun.objects.exclude(status__in=FINISHED_STATUSES).count() + expired = 0 + if days > 0: + cutoff = timezone.now() - timedelta(days=days) + expired = GithubRun.objects.filter( + created_at__lt=cutoff, status__in=FINISHED_STATUSES + ).count() + + backups = [ + { + 'name': p.name, + 'size': size, + 'mtime': datetime.fromtimestamp(mtime, tz=timezone.get_current_timezone()), + } + for p, size, mtime in list_backups() + ] + return { + 'retention_days': days, + 'backup_enabled': backup_enabled(), + 'backup_keep': backup_keep(), + 'db_path': str(db_path()), + 'db_size': db_path().stat().st_size if db_path().is_file() else 0, + 'total_builds': total, + 'in_progress_builds': in_progress, + 'expired_builds': expired, + 'artifacts_size': _dir_size('exe') + _dir_size('png'), + 'temp_size': _dir_size('temp_zips'), + 'backups': backups, + 'backup_interval_hours': BACKUP_INTERVAL // 3600, + } diff --git a/rdgenerator/management/__init__.py b/rdgenerator/management/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdgenerator/management/commands/__init__.py b/rdgenerator/management/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdgenerator/management/commands/maintenance.py b/rdgenerator/management/commands/maintenance.py new file mode 100644 index 0000000..ca604df --- /dev/null +++ b/rdgenerator/management/commands/maintenance.py @@ -0,0 +1,50 @@ +"""定时维护命令。 + +用法: + python manage.py maintenance # 同 --once + python manage.py maintenance --once # 执行一轮(清理过期任务/临时文件、按需备份) + python manage.py maintenance --loop # 常驻:启动即一轮,之后每小时醒来检查 +""" +import time + +from django.core.management.base import BaseCommand + +from rdgenerator import maintenance + + +class Command(BaseCommand): + help = '清理过期构建任务与临时文件,并按设置定期备份 SQLite 数据库。' + + LOOP_INTERVAL = 3600 # 每小时醒来一次(备份内部按 24h 间隔节流) + + def add_arguments(self, parser): + parser.add_argument( + '--loop', action='store_true', + help='常驻模式:容器启动后在后台持续运行。', + ) + parser.add_argument( + '--once', action='store_true', + help='只执行一轮后退出(默认行为,也供后台「立即执行」使用)。', + ) + + def handle(self, *args, **options): + if options['loop']: + self.stdout.write('[maintenance] 守护进程已启动,每小时检查一次。') + while True: + self._run_once() + time.sleep(self.LOOP_INTERVAL) + else: + self._run_once() + + def _run_once(self): + result = maintenance.run_maintenance() + self.stdout.write( + '[maintenance] 清理过期任务 {deleted_builds} 条、临时文件 {deleted_temp_files} 个、' + '轮转旧备份 {rotated_backups} 份;备份:{backup}'.format( + deleted_builds=result['deleted_builds'], + deleted_temp_files=result['deleted_temp_files'], + rotated_backups=result['rotated_backups'], + backup=('已创建 ' + result['backup_created']) if result['backup_created'] + else ('本轮跳过' if result['backup_skipped'] else '未创建'), + ) + ) diff --git a/rdgenerator/migrations/0001_initial.py b/rdgenerator/migrations/0001_initial.py new file mode 100644 index 0000000..0f96636 --- /dev/null +++ b/rdgenerator/migrations/0001_initial.py @@ -0,0 +1,22 @@ +# Generated by Django 5.0.3 on 2024-09-26 14:48 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ] + + operations = [ + migrations.CreateModel( + name='GithubRun', + fields=[ + ('id', models.IntegerField(primary_key=True, serialize=False, verbose_name='ID')), + ('uuid', models.CharField(max_length=100, verbose_name='uuid')), + ('status', models.CharField(max_length=100, verbose_name='status')), + ], + ), + ] diff --git a/rdgenerator/migrations/0002_githubrun_github_run_id.py b/rdgenerator/migrations/0002_githubrun_github_run_id.py new file mode 100644 index 0000000..ddcb655 --- /dev/null +++ b/rdgenerator/migrations/0002_githubrun_github_run_id.py @@ -0,0 +1,18 @@ +# Generated by Django 5.0.3 on 2026-03-11 04:58 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rdgenerator', '0001_initial'), + ] + + operations = [ + migrations.AddField( + model_name='githubrun', + name='github_run_id', + field=models.BigIntegerField(blank=True, null=True), + ), + ] diff --git a/rdgenerator/migrations/0003_build_management.py b/rdgenerator/migrations/0003_build_management.py new file mode 100644 index 0000000..189dfa6 --- /dev/null +++ b/rdgenerator/migrations/0003_build_management.py @@ -0,0 +1,106 @@ +from django.conf import settings +from django.db import migrations, models + +import rdgenerator.models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("rdgenerator", "0002_githubrun_github_run_id"), + ] + + operations = [ + migrations.AlterField( + model_name="githubrun", + name="id", + field=models.BigAutoField(primary_key=True, serialize=False), + ), + migrations.AlterField( + model_name="githubrun", + name="uuid", + field=models.CharField(db_index=True, max_length=100, verbose_name="任务 UUID"), + ), + migrations.AlterField( + model_name="githubrun", + name="status", + field=models.CharField(default="queued", max_length=100, verbose_name="构建状态"), + ), + migrations.AddField( + model_name="githubrun", + name="platform", + field=models.CharField(blank=True, default="", max_length=20, verbose_name="目标平台"), + ), + migrations.AddField( + model_name="githubrun", + name="filename", + field=models.CharField(blank=True, default="", max_length=200, verbose_name="文件名"), + ), + migrations.AddField( + model_name="githubrun", + name="created_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=models.SET_NULL, + related_name="builds", + to=settings.AUTH_USER_MODEL, + verbose_name="提交用户", + ), + ), + migrations.AddField( + model_name="githubrun", + name="created_at", + field=models.DateTimeField(auto_now_add=True, verbose_name="提交时间", null=True), + ), + migrations.AddField( + model_name="githubrun", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="更新时间", null=True), + ), + migrations.AlterField( + model_name="githubrun", + name="created_at", + field=models.DateTimeField(auto_now_add=True, verbose_name="提交时间"), + ), + migrations.AlterField( + model_name="githubrun", + name="updated_at", + field=models.DateTimeField(auto_now=True, verbose_name="更新时间"), + ), + migrations.CreateModel( + name="ApiToken", + fields=[ + ( + "key", + models.CharField( + default=rdgenerator.models.generate_token_key, + editable=False, + max_length=64, + primary_key=True, + serialize=False, + verbose_name="令牌", + ), + ), + ("name", models.CharField(help_text="便于识别用途,例如:CI 构建机", max_length=100, verbose_name="令牌名称")), + ("is_active", models.BooleanField(default=True, verbose_name="启用")), + ("created_at", models.DateTimeField(auto_now_add=True, verbose_name="创建时间")), + ("last_used_at", models.DateTimeField(blank=True, null=True, verbose_name="最后使用时间")), + ( + "user", + models.ForeignKey( + on_delete=models.CASCADE, + related_name="api_tokens", + to=settings.AUTH_USER_MODEL, + verbose_name="所属用户", + ), + ), + ], + options={ + "verbose_name": "API 令牌", + "verbose_name_plural": "API 令牌", + "ordering": ["-created_at"], + }, + ), + ] diff --git a/rdgenerator/migrations/0004_sitesetting.py b/rdgenerator/migrations/0004_sitesetting.py new file mode 100644 index 0000000..04573d6 --- /dev/null +++ b/rdgenerator/migrations/0004_sitesetting.py @@ -0,0 +1,23 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rdgenerator', '0003_build_management'), + ] + + operations = [ + migrations.CreateModel( + name='SiteSetting', + fields=[ + ('key', models.CharField(max_length=50, primary_key=True, serialize=False, verbose_name='配置项')), + ('value', models.TextField(blank=True, default='', verbose_name='配置值')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '站点配置', + 'verbose_name_plural': '站点配置', + }, + ), + ] diff --git a/rdgenerator/migrations/0005_savedconfig.py b/rdgenerator/migrations/0005_savedconfig.py new file mode 100644 index 0000000..693ba92 --- /dev/null +++ b/rdgenerator/migrations/0005_savedconfig.py @@ -0,0 +1,39 @@ +# Generated for SavedConfig +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('rdgenerator', '0004_sitesetting'), + ] + + operations = [ + migrations.CreateModel( + name='SavedConfig', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100, verbose_name='方案名称')), + ('data', models.JSONField(default=dict, verbose_name='配置内容')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ('owner', models.ForeignKey( + on_delete=models.CASCADE, + related_name='saved_configs', + to=settings.AUTH_USER_MODEL, + verbose_name='所属用户', + )), + ], + options={ + 'verbose_name': '构建配置方案', + 'verbose_name_plural': '构建配置方案', + 'ordering': ['-updated_at'], + }, + ), + migrations.AlterUniqueTogether( + name='savedconfig', + unique_together={('owner', 'name')}, + ), + ] diff --git a/rdgenerator/migrations/0006_githubrun_config_data.py b/rdgenerator/migrations/0006_githubrun_config_data.py new file mode 100644 index 0000000..57430ff --- /dev/null +++ b/rdgenerator/migrations/0006_githubrun_config_data.py @@ -0,0 +1,16 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rdgenerator', '0005_savedconfig'), + ] + + operations = [ + migrations.AddField( + model_name='githubrun', + name='config_data', + field=models.JSONField(blank=True, default=dict, verbose_name='构建配置快照'), + ), + ] diff --git a/rdgenerator/migrations/0007_alter_githubrun_options_and_more.py b/rdgenerator/migrations/0007_alter_githubrun_options_and_more.py new file mode 100644 index 0000000..f03b137 --- /dev/null +++ b/rdgenerator/migrations/0007_alter_githubrun_options_and_more.py @@ -0,0 +1,27 @@ +# Generated by Django 5.2.17 on 2026-09-29 02:27 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rdgenerator', '0006_githubrun_config_data'), + ] + + operations = [ + migrations.AlterModelOptions( + name='githubrun', + options={'ordering': ['-created_at'], 'verbose_name': '构建任务', 'verbose_name_plural': '构建任务'}, + ), + migrations.AlterField( + model_name='githubrun', + name='github_run_id', + field=models.BigIntegerField(blank=True, null=True, verbose_name='GitHub 运行编号'), + ), + migrations.AlterField( + model_name='githubrun', + name='id', + field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'), + ), + ] diff --git a/rdgenerator/migrations/0008_githubrun_backend.py b/rdgenerator/migrations/0008_githubrun_backend.py new file mode 100644 index 0000000..fd3152c --- /dev/null +++ b/rdgenerator/migrations/0008_githubrun_backend.py @@ -0,0 +1,18 @@ +# Generated for 多构建平台支持(GitHub / Gitea) + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('rdgenerator', '0007_alter_githubrun_options_and_more'), + ] + + operations = [ + migrations.AddField( + model_name='githubrun', + name='backend', + field=models.CharField(default='github', max_length=20, verbose_name='构建平台'), + ), + ] diff --git a/rdgenerator/migrations/__init__.py b/rdgenerator/migrations/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/rdgenerator/models.py b/rdgenerator/models.py new file mode 100644 index 0000000..0514e5e --- /dev/null +++ b/rdgenerator/models.py @@ -0,0 +1,171 @@ +import secrets + +from django.conf import settings +from django.db import models + + + +# GitHub Actions 运行状态 -> 中文展示文案 +STATUS_LABELS = { + "Starting generator...please wait": "正在启动构建任务", + "queued": "排队中", + "in_progress": "构建中", + "success": "构建成功", + "failure": "构建失败", + "cancelled": "已取消", + "timed_out": "已超时", + "skipped": "已跳过", + "action_required": "等待人工处理", +} + +# 状态对应的徽标样式(模板中使用) +STATUS_BADGE = { + "Starting generator...please wait": "badge-info", + "queued": "badge-info", + "in_progress": "badge-info", + "success": "badge-success", + "failure": "badge-danger", + "cancelled": "badge-warning", + "timed_out": "badge-warning", + "skipped": "badge-secondary", + "action_required": "badge-warning", +} + +PLATFORM_LABELS = { + "windows": "Windows 64 位", + "windows-x86": "Windows 32 位", + "linux": "Linux", + "android": "Android", + "macos": "macOS", +} + + +def generate_token_key(): + return secrets.token_hex(20) + + +class GithubRun(models.Model): + uuid = models.CharField("任务 UUID", max_length=100, db_index=True) + status = models.CharField("构建状态", max_length=100, default="queued") + github_run_id = models.BigIntegerField("GitHub 运行编号", null=True, blank=True) + # 构建平台:github / gitea;历史任务默认 github。切换平台后旧任务仍按此值查询状态 + backend = models.CharField("构建平台", max_length=20, default="github") + platform = models.CharField("目标平台", max_length=20, blank=True, default="") + filename = models.CharField("文件名", max_length=200, blank=True, default="") + # 提交时的表单快照(纯字符串/数值,图片为 data URL),供失败后「一键重试」重新派发 + config_data = models.JSONField("构建配置快照", blank=True, default=dict) + created_by = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name="提交用户", + null=True, + blank=True, + on_delete=models.SET_NULL, + related_name="builds", + ) + created_at = models.DateTimeField("提交时间", auto_now_add=True) + updated_at = models.DateTimeField("更新时间", auto_now=True) + + class Meta: + verbose_name = "构建任务" + verbose_name_plural = "构建任务" + ordering = ["-created_at"] + + def __str__(self): + return f"{self.filename or self.uuid}({self.status_label()})" + + def status_label(self): + return STATUS_LABELS.get(self.status, self.status) + + def status_badge(self): + return STATUS_BADGE.get(self.status, "badge-secondary") + + def platform_label(self): + return PLATFORM_LABELS.get(self.platform, self.platform or "未知平台") + + def is_finished(self): + return self.status in ("success", "failure", "cancelled", "timed_out", "skipped") + + @property + def github_log_url(self): + if self.github_run_id: + # 局部导入避免循环依赖;按任务记录的平台生成对应站点的日志链接 + from .build_backends import get_backend + return get_backend(self.backend or 'github').log_url(self.github_run_id) + return "" + + def short_uuid(self): + return self.uuid[:8] if len(self.uuid) >= 8 else self.uuid + + +class SiteSetting(models.Model): + """后台可改的站点配置(键值存储)。 + + 取值优先级由 app_settings 统一处理:后台配置(本表)→ 环境变量 → 默认值, + 因此历史的环境变量部署方式继续有效。 + """ + + key = models.CharField("配置项", max_length=50, primary_key=True) + value = models.TextField("配置值", blank=True, default='') + updated_at = models.DateTimeField("更新时间", auto_now=True) + + class Meta: + verbose_name = "站点配置" + verbose_name_plural = "站点配置" + + def __str__(self): + return self.key + + +class SavedConfig(models.Model): + """用户保存的客户端构建配置方案(与账号绑定,可在定制页直接载入、可导出/导入)。""" + + owner = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name="所属用户", + on_delete=models.CASCADE, + related_name="saved_configs", + ) + name = models.CharField("方案名称", max_length=100) + data = models.JSONField("配置内容", default=dict) + created_at = models.DateTimeField("创建时间", auto_now_add=True) + updated_at = models.DateTimeField("更新时间", auto_now=True) + + class Meta: + verbose_name = "构建配置方案" + verbose_name_plural = "构建配置方案" + unique_together = [('owner', 'name')] + ordering = ["-updated_at"] + + def __str__(self): + return f"{self.name}({self.owner})" + + def platform_label(self): + return PLATFORM_LABELS.get(self.data.get('platform', ''), '未指定平台') + + @property + def version_label(self): + return self.data.get('version', '') or '未指定版本' + + +class ApiToken(models.Model): + """供 JSON API(如 rdgen-cli)使用的个人访问令牌""" + + key = models.CharField("令牌", max_length=64, primary_key=True, default=generate_token_key, editable=False) + name = models.CharField("令牌名称", max_length=100, help_text="便于识别用途,例如:CI 构建机") + user = models.ForeignKey( + settings.AUTH_USER_MODEL, + verbose_name="所属用户", + on_delete=models.CASCADE, + related_name="api_tokens", + ) + is_active = models.BooleanField("启用", default=True) + created_at = models.DateTimeField("创建时间", auto_now_add=True) + last_used_at = models.DateTimeField("最后使用时间", null=True, blank=True) + + class Meta: + verbose_name = "API 令牌" + verbose_name_plural = "API 令牌" + ordering = ["-created_at"] + + def __str__(self): + return f"{self.name}({self.user})" diff --git a/rdgenerator/static/rdgenerator/css/app.css b/rdgenerator/static/rdgenerator/css/app.css new file mode 100644 index 0000000..1a1c33e --- /dev/null +++ b/rdgenerator/static/rdgenerator/css/app.css @@ -0,0 +1,851 @@ +/* ========================================================================== + RustDesk 客户端定制平台 · 全局设计系统 + 设计原则:扁平化、留白充足、控件统一;所有页面共用同一套组件类。 + ========================================================================== */ +:root { + --primary: #2563eb; + --primary-dark: #1d4ed8; + --primary-light: #eff4ff; + --primary-border: #9db8f5; + --success: #16a34a; + --success-bg: #dcfce7; + --warning: #b45309; + --warning-bg: #fef3c7; + --danger: #dc2626; + --danger-bg: #fee2e2; + --info: #0e7490; + --info-bg: #ecfeff; + --text: #1f2937; + --text-soft: #374151; + --text-muted: #6b7280; + --border: #e5e7eb; + --control-border: #d1d5db; + --bg: #f5f7fa; + --card: #ffffff; + --radius: 10px; + --radius-sm: 8px; + --shadow: 0 1px 2px rgba(16, 24, 40, .05); + --shadow-lg: 0 16px 40px rgba(16, 24, 40, .14); + /* 统一节奏 */ + --control-h: 38px; +} + +* { box-sizing: border-box; } +html, body { height: 100%; } + +body { + margin: 0; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", + "Hiragino Sans GB", "Microsoft YaHei", "Helvetica Neue", Arial, sans-serif; + background: var(--bg); + color: var(--text); + font-size: 14px; + line-height: 1.6; + display: flex; + flex-direction: column; + min-height: 100vh; + -webkit-font-smoothing: antialiased; +} + +a { color: var(--primary); text-decoration: none; } +a:hover { text-decoration: underline; } + +/* ===== 内联 SVG 图标(统一线性/扁平单色) ===== */ +.svg-sprite { position: absolute; width: 0; height: 0; overflow: hidden; } +.icon { + width: 1em; height: 1em; + flex: none; + fill: none; + stroke: currentColor; + stroke-width: 1.8; + stroke-linecap: round; + stroke-linejoin: round; + vertical-align: -0.12em; +} +.brand-icon { + width: 1em; height: 1em; + flex: none; + fill: currentColor; + stroke: none; +} +.page-head .icon { font-size: 15px; } + +/* ===== 顶部导航 ===== */ +.topbar { + background: var(--card); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 50; +} +.topbar-inner { + max-width: 1200px; + margin: 0 auto; + padding: 0 24px; + height: 60px; + display: flex; + align-items: center; + justify-content: space-between; + gap: 20px; +} +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + font-weight: 700; + font-size: 16px; + color: var(--text); + white-space: nowrap; +} +.brand:hover { text-decoration: none; } +.brand-logo { + width: 32px; height: 32px; + border-radius: 8px; + background: var(--primary); + color: #fff; + display: inline-flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 800; + letter-spacing: .5px; +} +.topbar nav { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; } +.nav-link { + padding: 7px 13px; + border-radius: var(--radius-sm); + color: var(--text-muted); + font-weight: 500; + font-size: 13.5px; + line-height: 1.4; +} +.nav-link:hover { background: var(--primary-light); color: var(--primary); text-decoration: none; } +.nav-link.active { background: var(--primary-light); color: var(--primary); } +.nav-user { color: var(--text-muted); font-size: 13px; padding: 0 8px; white-space: nowrap; } +.btn-logout { + background: none; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 6px 13px; + color: var(--text-muted); + cursor: pointer; + font-size: 13px; + font-family: inherit; +} +.btn-logout:hover { border-color: var(--danger); color: var(--danger); } + +/* ===== 页面容器与标题 ===== */ +.page { + flex: 1; + width: 100%; + max-width: 1200px; + margin: 0 auto; + padding: 28px 24px 56px; +} +.page-narrow { max-width: 860px; } +.page-head { margin-bottom: 22px; } +.page-title { font-size: 22px; font-weight: 700; margin: 0 0 6px; } +.page-subtitle { color: var(--text-muted); margin: 0; font-size: 13.5px; line-height: 1.7; } +.breadcrumb { font-size: 13px; margin-bottom: 14px; color: var(--text-muted); } +.breadcrumb a { color: var(--text-muted); font-weight: 500; } +.breadcrumb a:hover { color: var(--primary); } +.breadcrumb .sep { margin: 0 8px; color: #c0c6d0; } + +.footer { + text-align: center; + color: #9aa3b2; + font-size: 12.5px; + padding: 20px; + border-top: 1px solid var(--border); + background: var(--card); +} + +/* ===== 卡片 ===== */ +.card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + margin-bottom: 24px; +} +.card:last-child { margin-bottom: 0; } +.card-header { + padding: 16px 24px; + border-bottom: 1px solid var(--border); + display: flex; + align-items: center; + gap: 9px; + font-weight: 600; + font-size: 15px; +} +.card-header .icon { color: #64748b; font-size: 17px; } +.card-body { padding: 22px 24px; } +.card-body.flush { padding: 0; } +.grid-2 { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + align-items: start; +} +.grid-2 .card { margin-bottom: 0; } +.form-narrow { max-width: 540px; } +@media (max-width: 920px) { .grid-2 { grid-template-columns: 1fr; } } + +/* ===== 表单 ===== */ +.field { margin-bottom: 18px; } +.field:last-child { margin-bottom: 0; } +/* 与左侧「label + 输入控件」并排的独立勾选项:下移到与控件(非 label)同一水平线 */ +.field-check-control { margin-bottom: 0; } +.field-check-control .check-item { margin-top: 33px; } +@media (max-width: 920px) { .field-check-control .check-item { margin-top: 4px; } } +.field > label, .field-label { + display: block; + font-weight: 600; + font-size: 13.5px; + margin-bottom: 7px; + color: var(--text-soft); +} +.form-control { + width: 100%; + min-height: var(--control-h); + padding: 8px 12px; + border: 1px solid var(--control-border); + border-radius: var(--radius-sm); + font-size: 14px; + color: var(--text); + background: #fff; + transition: border-color .15s, box-shadow .15s; + font-family: inherit; + line-height: 1.4; +} +.form-control:focus { + outline: none; + border-color: var(--primary); + box-shadow: 0 0 0 3px rgba(37, 99, 235, .12); +} +.form-control::placeholder { color: #9ca3af; } +select.form-control { + cursor: pointer; + padding-right: 34px; + appearance: none; + -webkit-appearance: none; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 24 24' fill='none' stroke='%236b7280' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m6 9 6 6 6-6'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right 11px center; + background-size: 16px; +} +textarea.form-control { + resize: vertical; + min-height: 96px; + font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; + font-size: 13px; + line-height: 1.6; +} +/* 文件选择框:浅灰底 + 统一内嵌按钮 */ +input[type="file"].form-control { + min-height: 0; + padding: 7px 10px; + background: #fafbfd; + cursor: pointer; +} +input[type="file"].form-control::file-selector-button { + margin-right: 12px; + padding: 5px 13px; + border: 1px solid var(--control-border); + border-radius: 6px; + background: #fff; + color: var(--text-soft); + font: 600 12.5px inherit; + cursor: pointer; + transition: border-color .15s, color .15s; +} +input[type="file"].form-control:hover::file-selector-button { + border-color: var(--primary-border); + color: var(--primary); +} +input[type="file"].form-control::-webkit-file-upload-button { + margin-right: 12px; + padding: 5px 13px; + border: 1px solid var(--control-border); + border-radius: 6px; + background: #fff; + color: var(--text-soft); + font: 600 12.5px inherit; + cursor: pointer; +} +.field-hint { color: var(--text-muted); font-size: 12.5px; margin-top: 7px; line-height: 1.6; } +.help-text { + display: block; + background: var(--info-bg); + color: var(--info); + font-size: 12.5px; + line-height: 1.6; + padding: 8px 12px; + border-radius: var(--radius-sm); + margin-top: 8px; +} +.inline-warning { + display: none; + color: var(--warning); + background: var(--warning-bg); + font-size: 12.5px; + line-height: 1.6; + padding: 8px 12px; + border-radius: var(--radius-sm); + margin: 0 0 12px; +} +.inline-error { color: var(--danger); font-size: 12.5px; margin-left: 8px; } +.errorlist { list-style: none; padding: 0; margin: 7px 0 0; color: var(--danger); font-size: 12.5px; } +.errorlist li { margin-top: 3px; } + +/* 输入框 + 按钮同一行(如令牌创建) */ +.input-row { display: flex; gap: 10px; align-items: center; } +.input-row .form-control { flex: 1 1 auto; min-width: 0; width: auto; } + +/* 复选 / 单选 */ +.form-check-input { width: 16px; height: 16px; accent-color: var(--primary); cursor: pointer; margin: 0; flex: none; } +.check-item { + display: flex; + align-items: center; + gap: 9px; + font-size: 13.5px; + cursor: pointer; + padding: 4px 0; + color: var(--text-soft); +} +.check-item:hover { color: var(--text); } +.check-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(195px, 1fr)); + gap: 4px 24px; + margin: 10px 0 4px; +} +.radio-group ul { list-style: none; padding: 0; margin: 0; display: grid; gap: 10px; } +.radio-group li { display: flex; align-items: center; gap: 9px; padding: 2px 0; } +.radio-group li label { font-weight: 400; margin: 0; font-size: 13.5px; cursor: pointer; } +.subsection-title { + display: flex; + align-items: center; + gap: 8px; + font-size: 13.5px; + font-weight: 600; + color: var(--text-soft); + margin: 22px 0 10px; +} +.subsection-title:first-child { margin-top: 0; } +.subsection-title .icon { color: #64748b; font-size: 15px; } +.subsection-title::after { + content: ""; + flex: 1; + height: 1px; + background: var(--border); +} + +/* ===== 按钮 ===== */ +.btn { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 7px; + min-height: var(--control-h); + padding: 0 18px; + border-radius: var(--radius-sm); + border: 1px solid transparent; + font-size: 14px; + font-weight: 600; + font-family: inherit; + cursor: pointer; + white-space: nowrap; + transition: background .15s, border-color .15s, color .15s, box-shadow .15s; + text-decoration: none !important; + line-height: 1.2; +} +.btn .icon { font-size: 15px; } +.btn-primary { background: var(--primary); color: #fff; } +.btn-primary:hover { background: var(--primary-dark); } +.btn-secondary { background: #fff; color: var(--text-soft); border-color: var(--control-border); } +.btn-secondary:hover { border-color: var(--primary-border); color: var(--primary); } +.btn-danger { background: #fff; color: var(--danger); border-color: #f3c2c2; } +.btn-danger:hover { background: var(--danger); color: #fff; border-color: var(--danger); } +.btn-sm { min-height: 30px; padding: 0 12px; font-size: 12.5px; border-radius: 6px; } +.btn-sm .icon { font-size: 13px; } +.btn-block { width: 100%; } +.btn:disabled { opacity: .6; cursor: not-allowed; } +.btn-row { display: flex; gap: 10px; flex-wrap: wrap; } +.inline-form { display: inline-flex; vertical-align: middle; } +.table .btn + .btn, .table .btn + .inline-form { margin-left: 8px; } + +/* ===== 平台选择卡 ===== */ +.platform-grid { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 14px; + margin: 4px 0 8px; +} +.platform-card { + border: 1.5px solid var(--border); + border-radius: var(--radius); + background: #fff; + padding: 18px 8px 16px; + text-align: center; + cursor: pointer; + transition: border-color .15s, background .15s, transform .1s; + user-select: none; +} +.platform-card:hover { border-color: var(--primary-border); } +.platform-card.active { border-color: var(--primary); background: var(--primary-light); } +.platform-card:active { transform: scale(.98); } +.platform-card .brand-icon { font-size: 30px; color: #6b728b; transition: color .15s; } +.platform-card.active .brand-icon { color: var(--primary); } +.platform-name { font-weight: 600; font-size: 13.5px; margin-top: 9px; color: var(--text); } +.platform-tag { color: var(--text-muted); font-size: 12px; margin-top: 2px; } +@media (max-width: 920px) { .platform-grid { grid-template-columns: repeat(3, 1fr); } } +@media (max-width: 560px) { .platform-grid { grid-template-columns: repeat(2, 1fr); } } + +/* ===== 后台构建平台选择卡(GitHub / Gitea 二选一) ===== */ +.build-platform-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px; + margin: 4px 0 8px; +} +.build-platform-grid .platform-card { padding: 20px 12px 18px; } +.build-platform-grid .platform-card .icon { font-size: 28px; } +.platform-desc { color: var(--text-muted); font-size: 12.5px; margin-top: 5px; line-height: 1.55; } +.panel-hidden { display: none !important; } +.settings-submit-card .settings-actions { margin-top: 0; padding: 22px 24px; border-top: none; } +@media (max-width: 560px) { .build-platform-grid { grid-template-columns: 1fr; } } +.visually-hidden { + position: absolute !important; + width: 1px; height: 1px; + padding: 0; margin: -1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); + white-space: nowrap; + border: 0; +} + +/* ===== 图片预览 ===== */ +.image-preview { margin-top: 9px; } +.image-preview img { + max-width: 100%; + max-height: 64px; + border: 1px solid var(--border); + border-radius: var(--radius-sm); + padding: 5px; + background: #fff; +} +.image-preview.photo img { max-height: 120px; } + +/* ===== 徽标 ===== */ +.badge { + display: inline-flex; + align-items: center; + padding: 3px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + line-height: 1.5; + white-space: nowrap; +} +.badge-success { background: var(--success-bg); color: #15803d; } +.badge-info { background: var(--info-bg); color: var(--info); } +.badge-warning { background: var(--warning-bg); color: var(--warning); } +.badge-danger { background: var(--danger-bg); color: #b91c1c; } +.badge-secondary { background: #f1f3f7; color: var(--text-muted); } +.badge-dot::before { + content: ""; + display: inline-block; + width: 6px; height: 6px; + border-radius: 50%; + background: currentColor; + margin-right: 6px; +} +.badge-info.badge-dot::before { animation: pulse 1.2s infinite; } +@keyframes pulse { 0%,100% { opacity: 1; } 50% { opacity: .25; } } + +/* ===== 提示消息 ===== */ +.alerts { margin-bottom: 20px; display: grid; gap: 10px; } +.alert { + padding: 12px 16px; + border-radius: var(--radius-sm); + border: 1px solid; + font-size: 13.5px; + line-height: 1.6; +} +.alert-success { background: var(--success-bg); border-color: #bbf7d0; color: #15803d; } +.alert-error, .alert-danger { background: var(--danger-bg); border-color: #fecaca; color: #b91c1c; } +.alert-warning { background: var(--warning-bg); border-color: #fde68a; color: var(--warning); } +.alert-info { background: var(--info-bg); border-color: #a5f3fc; color: var(--info); } + +/* ===== 表格 ===== */ +.table-wrap { overflow-x: auto; } +table.table { width: 100%; border-collapse: collapse; font-size: 13.5px; } +.table th, .table td { + padding: 13px 16px; + text-align: left; + border-bottom: 1px solid var(--border); + vertical-align: middle; +} +.table th { + background: #f9fafb; + color: var(--text-muted); + font-weight: 600; + font-size: 12.5px; + white-space: nowrap; +} +.table tr:last-child td { border-bottom: none; } +.table tbody tr:hover td { background: #fafbfe; } +.mono { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-size: 12.5px; } +.text-muted { color: var(--text-muted); } + +/* ===== 统计卡片 ===== */ +.stat-grid { + display: grid; + grid-template-columns: repeat(6, 1fr); + gap: 16px; + margin-bottom: 24px; +} +.stat-card { + background: var(--card); + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 18px 20px; + box-shadow: var(--shadow); +} +.stat-value { font-size: 26px; font-weight: 700; line-height: 1.15; } +.stat-label { color: var(--text-muted); font-size: 12.5px; margin-top: 5px; } +.stat-card.is-primary .stat-value { color: var(--primary); } +.stat-card.is-success .stat-value { color: var(--success); } +.stat-card.is-info .stat-value { color: #0891b2; } +.stat-card.is-danger .stat-value { color: var(--danger); } +@media (max-width: 1000px) { .stat-grid { grid-template-columns: repeat(3, 1fr); } } +@media (max-width: 560px) { .stat-grid { grid-template-columns: repeat(2, 1fr); } } + +/* ===== 空状态 ===== */ +.empty-state { text-align: center; padding: 60px 24px; color: var(--text-muted); } +.empty-state .empty-icon { font-size: 42px; color: #c3cad6; margin-bottom: 14px; } +.empty-state h3 { margin: 0 0 7px; color: var(--text); font-size: 16px; font-weight: 600; } +.empty-state p { margin: 0 0 20px; font-size: 13.5px; line-height: 1.7; } +.empty-state .btn { margin: 0 auto; } + +/* ===== 登录页 ===== */ +.login-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + background: var(--bg); +} +.login-card { + background: #fff; + border: 1px solid var(--border); + border-radius: 14px; + box-shadow: var(--shadow-lg); + width: 100%; + max-width: 400px; + padding: 36px 34px 30px; +} +.login-brand { display: flex; align-items: center; gap: 12px; margin-bottom: 4px; } +.login-brand .brand-logo { width: 42px; height: 42px; font-size: 15px; border-radius: 11px; } +.login-brand h1 { font-size: 18px; margin: 0; } +.login-brand p { margin: 0; color: var(--text-muted); font-size: 12.5px; } +.login-card form { margin-top: 24px; } +.login-card .field { margin-bottom: 16px; } +.login-card .btn { width: 100%; margin-top: 4px; } + +/* ===== 等待 / 结果页 ===== */ +.result-wrap { text-align: center; padding: 36px 20px; } +.result-icon { width: 64px; height: 64px; margin: 0 auto 16px; } +.result-icon .icon { width: 100%; height: 100%; stroke-width: 1.6; } +.result-icon.success { color: var(--success); } +.result-icon.warning { color: #d97706; } +.result-title { font-size: 22px; font-weight: 700; margin: 0 0 8px; } +.result-desc { color: var(--text-muted); margin: 0 0 24px; line-height: 1.8; } +.spinner { + width: 42px; height: 42px; + border: 4px solid #dbe5f5; + border-top-color: var(--primary); + border-radius: 50%; + margin: 0 auto 18px; + animation: spin .9s linear infinite; +} +@keyframes spin { to { transform: rotate(360deg); } } +.progress-bar { + width: 100%; + max-width: 380px; + height: 8px; + background: #e7ecf5; + border-radius: 999px; + overflow: hidden; + margin: 0 auto 20px; +} +.progress-bar-fill { + width: 30%; + height: 100%; + background: linear-gradient(90deg, #3b82f6, #0ea5e9); + border-radius: 999px; + animation: progress-slide 1.6s ease-in-out infinite; +} +@keyframes progress-slide { + 0% { width: 8%; margin-left: 0; } + 50% { width: 45%; margin-left: 20%; } + 100% { width: 8%; margin-left: 92%; } +} +.download-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(270px, 1fr)); + gap: 10px; + text-align: left; + margin-top: 8px; +} +.download-list .btn { justify-content: flex-start; padding: 0 14px; font-weight: 500; } +.info-box { + background: #f9fafb; + border: 1px solid var(--border); + border-radius: var(--radius); + padding: 16px 18px; + max-width: 540px; + margin: 0 auto 20px; + font-size: 13.5px; + color: var(--text-muted); + text-align: left; + line-height: 1.9; +} +.info-box strong { color: var(--text); } +.info-box-title { + display: flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: var(--text); + margin-bottom: 4px; +} +.info-box-title .icon { color: #64748b; } +.info-box .icon { vertical-align: -0.15em; margin-right: 4px; } +.key-box { + background: #0f172a; + color: #a7f3d0; + border-radius: var(--radius-sm); + padding: 13px 16px; + font-family: ui-monospace, Menlo, Consolas, monospace; + font-size: 13px; + word-break: break-all; + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +/* ===== 错误页 ===== */ +.error-page { + min-height: 100vh; + display: flex; + align-items: center; + justify-content: center; + background: var(--bg); + padding: 20px; +} +.error-box { text-align: center; max-width: 460px; } +.error-code { font-size: 72px; font-weight: 800; color: var(--primary); line-height: 1; } +.error-box h1 { font-size: 20px; margin: 14px 0 8px; } +.error-box p { color: var(--text-muted); margin: 0 0 24px; line-height: 1.7; } + +/* ===== 全站 GitHub 未配置告警条 ===== */ +.config-banner { + display: flex; + align-items: center; + gap: 9px; + margin-bottom: 20px; + font-size: 13.5px; + line-height: 1.6; + text-decoration: none; +} +a.config-banner:hover { text-decoration: none; filter: brightness(0.97); } +.config-banner .icon { flex-shrink: 0; } + +/* ===== GitHub 构建配置页 ===== */ +.brand-github { font-size: 1.15em; vertical-align: -0.18em; fill: currentColor; stroke: none; } +.card-desc { + margin: 0 0 18px; + font-size: 13px; + color: var(--text-muted); + line-height: 1.7; +} +.config-ok { + display: flex; + align-items: center; + gap: 9px; + color: #15803d; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: var(--radius-sm); + padding: 12px 16px; + font-size: 13.5px; +} +.config-ok .icon { color: #16a34a; flex-shrink: 0; } +.config-check-list { list-style: none; padding: 0; margin: 0; display: grid; gap: 9px; } +.config-check-list li { + display: flex; + align-items: flex-start; + gap: 9px; + font-size: 13.5px; + line-height: 1.6; +} +.config-check-list .icon { flex-shrink: 0; margin-top: 2px; } +.config-check-list .is-ok { color: #15803d; } +.config-check-list .is-ok .icon { color: #16a34a; } +.config-check-list .is-bad { color: #b91c1c; } +.config-check-list .is-bad .icon { color: var(--danger); } +.config-check-list .is-warn { color: var(--warning); } +.config-check-list .is-warn .icon { color: #d97706; } + +.setting-row { max-width: 640px; } +.setting-label { + display: flex; + align-items: center; + gap: 9px; + font-weight: 600; + font-size: 13.5px; + margin-bottom: 7px; +} +.source-badge { + font-weight: 500; + font-size: 11px; + line-height: 1; + padding: 3px 8px; + border-radius: 999px; + border: 1px solid transparent; + white-space: nowrap; +} +.source-db { background: #eff6ff; color: #1d4ed8; border-color: #bfdbfe; } +.source-env { background: #f1f5f9; color: #475569; border-color: #e2e8f0; } +.source-default { background: #fefce8; color: #a16207; border-color: #fde68a; } +.secret-meta { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 6px 14px; + margin-top: 7px; +} +.clear-check { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 12.5px; + color: var(--text-muted); + cursor: pointer; + margin: 0; +} +.clear-check input { margin: 0; } +.secret-state { + font-size: 12px; + padding: 2px 9px; + border-radius: 999px; +} +.secret-state.is-set { background: #eff6ff; color: #1d4ed8; } +.secret-state.is-env { background: #f1f5f9; color: #475569; } +.secret-state.is-empty { background: #fef2f2; color: #b91c1c; } + +/* 表单内最后一张分组卡命中 .card:last-child 被去掉下边距, + 需由表单本身补回,避免与表单后的引导卡片紧贴 */ +.settings-form { margin-bottom: 24px; } + +.settings-actions { margin-top: 22px; padding-top: 20px; border-top: 1px solid var(--border); } +.settings-actions .btn-row { margin-bottom: 0; } + +/* 文件选择:隐藏原生控件,用统一按钮样式(与客户端定制页一致) */ +.file-pick-row { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; } +.file-pick-name { font-size: 13px; color: var(--text-muted); } +.test-result { + font-size: 13px; + padding: 9px 14px; + border-radius: var(--radius-sm); + border: 1px solid var(--border); + background: #f9fafb; + color: var(--text-muted); + margin-top: 12px; +} +.test-result.is-loading { color: var(--text-muted); } +.test-result.is-ok { background: #f0fdf4; border-color: #bbf7d0; color: #15803d; } +.test-result.is-bad { background: #fef2f2; border-color: #fecaca; color: #b91c1c; } + +/* 版本来源提示 */ +.version-note { + display: flex; + align-items: center; + gap: 6px; + margin-top: 7px; + font-size: 12px; + line-height: 1.5; +} +.version-note .icon { font-size: 13px; flex-shrink: 0; } +.version-note.is-live { color: #1d4ed8; } +.version-note.is-fallback { color: #b45309; } + +/* 定制页配置方案区 */ +.profile-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-bottom: 12px; +} +.profile-row:last-child { margin-bottom: 0; } +.profile-row .form-control { width: auto; min-width: 220px; max-width: 420px; } +.profile-row #profileName { flex: 0 1 300px; } +.profile-msg { display: none; margin-top: 10px; font-size: 13px; line-height: 1.6; } +.profile-msg.is-ok { display: block; color: #15803d; } +.profile-msg.is-bad { display: block; color: #b91c1c; } + +/* 配置引导教程 */ +.guide-steps { list-style: none; padding: 0; margin: 0; counter-reset: guide; } +.guide-steps > li { + position: relative; + padding: 0 0 20px 42px; + counter-increment: guide; +} +.guide-steps > li:last-child { padding-bottom: 0; } +.guide-steps > li::before { + content: counter(guide); + position: absolute; + left: 0; + top: 0; + width: 27px; + height: 27px; + border-radius: 50%; + background: var(--primary); + color: #fff; + font-size: 13px; + font-weight: 600; + display: flex; + align-items: center; + justify-content: center; +} +.guide-steps > li:not(:last-child)::after { + content: ""; + position: absolute; + left: 13px; + top: 31px; + bottom: 2px; + width: 1.5px; + background: #dbe4f0; +} +.guide-title { font-weight: 600; font-size: 14px; margin-bottom: 5px; } +.guide-steps p { margin: 0; font-size: 13px; color: var(--text-muted); line-height: 1.75; } +.guide-steps a { color: var(--primary); font-weight: 500; } +.icon-inline { width: 13px; height: 13px; vertical-align: -0.15em; } +.guide-sublist { margin: 8px 0 0; padding-left: 18px; font-size: 13px; color: var(--text-muted); line-height: 1.85; } +.guide-sublist strong { color: var(--text); } +.guide-steps code { + background: #f1f5f9; + border-radius: 4px; + padding: 1px 6px; + font-size: 12px; + font-family: ui-monospace, Menlo, Consolas, monospace; +} +@media (max-width: 640px) { + .setting-row { max-width: none; } + .test-result { width: 100%; } +} diff --git a/rdgenerator/templates/403.html b/rdgenerator/templates/403.html new file mode 100644 index 0000000..293a5b2 --- /dev/null +++ b/rdgenerator/templates/403.html @@ -0,0 +1,19 @@ +{% load static %} + + + + + 无权访问 · RustDesk 客户端定制平台 + + + +
+
+
403
+

抱歉,你无权访问该页面

+

该资源仅对任务提交者或管理员开放。如需权限,请联系平台管理员。

+ 返回首页 +
+
+ + diff --git a/rdgenerator/templates/404.html b/rdgenerator/templates/404.html new file mode 100644 index 0000000..0785e5c --- /dev/null +++ b/rdgenerator/templates/404.html @@ -0,0 +1,19 @@ +{% load static %} + + + + + 页面不存在 · RustDesk 客户端定制平台 + + + +
+
+
404
+

页面或资源不存在

+

链接可能已失效,或对应的构建任务 / 文件尚未生成。请确认地址无误,或返回平台首页继续操作。

+ 返回首页 +
+
+ + diff --git a/rdgenerator/templates/500.html b/rdgenerator/templates/500.html new file mode 100644 index 0000000..ab5cc88 --- /dev/null +++ b/rdgenerator/templates/500.html @@ -0,0 +1,19 @@ +{% load static %} + + + + + 服务异常 · RustDesk 客户端定制平台 + + + +
+
+
500
+

服务暂时出现异常

+

服务器处理请求时发生错误,请稍后重试。如问题持续存在,请联系平台管理员。

+ 返回首页 +
+
+ + diff --git a/rdgenerator/templates/base.html b/rdgenerator/templates/base.html new file mode 100644 index 0000000..403a582 --- /dev/null +++ b/rdgenerator/templates/base.html @@ -0,0 +1,202 @@ +{% load static %} + + + + + {% block title %}RustDesk 客户端定制平台{% endblock %} + + {% block head_extra %}{% endblock %} + + +{# ===== 统一内联图标库:线性图标 stroke=currentColor,品牌图标 fill=currentColor,零外部依赖 ===== #} + + +{% if user.is_authenticated %} +
+
+ + + RustDesk 客户端定制平台 + + +
+
+{% endif %} + +
+ {% if user.is_authenticated and not build_ready %} + {% if user.is_staff %} + + + 构建服务尚未完成配置(当前平台:{{ build_platform_label }}),客户端构建暂不可用,点击进入「构建平台配置」完成设置。 + + {% elif request.resolver_match.url_name == 'generator' %} +
+ + 构建服务尚未完成配置,请联系管理员在后台「构建平台配置」中设置。 +
+ {% endif %} + {% endif %} + + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} +
+ {% endif %} + + {% block content %}{% endblock %} +
+ +
+ RustDesk 客户端定制平台 · 自定义客户端由 GitHub Actions 自动构建 +
+ + diff --git a/rdgenerator/templates/dashboard.html b/rdgenerator/templates/dashboard.html new file mode 100644 index 0000000..238f479 --- /dev/null +++ b/rdgenerator/templates/dashboard.html @@ -0,0 +1,76 @@ +{% extends "base.html" %} + +{% block title %}后台管理 · RustDesk 客户端定制平台{% endblock %} + +{% block content %} +
+

后台管理 · 构建任务总览

+

查看全部用户提交的构建任务(最近 100 条),并进行用户与任务管理。

+
+ +
+
{{ stats.total }}
全部任务
+
{{ stats.in_progress }}
进行中
+
{{ stats.success }}
构建成功
+
{{ stats.failure }}
失败 / 取消
+
{{ stats.users }}
注册用户
+
{{ stats.tokens }}
有效 API 令牌
+
+ + + +
+
最近构建任务
+ {% if builds %} +
+ + + + + + + + + + + + + + {% for run in builds %} + + + + + + + + + + {% endfor %} + +
提交时间提交用户配置名称平台状态任务编号操作
{{ run.created_at|date:"Y-m-d H:i" }}{% if run.created_by %}{{ run.created_by.username }}{% else %}—{% endif %}{{ run.filename|default:"—" }}{{ run.platform_label }}{{ run.status_label }}{{ run.short_uuid }} + 查看 +
+ {% csrf_token %} + +
+
+
+ {% else %} +
+
+

暂无构建任务

+

还没有任何用户提交过构建任务。

+
+ {% endif %} +
+{% endblock %} diff --git a/rdgenerator/templates/failure.html b/rdgenerator/templates/failure.html new file mode 100644 index 0000000..a166526 --- /dev/null +++ b/rdgenerator/templates/failure.html @@ -0,0 +1,62 @@ +{% extends "base.html" %} + +{% block title %}构建未完成 · RustDesk 客户端定制平台{% endblock %} +{% block page_class %}page-narrow{% endblock %} + +{% block content %} +
+
+
+

构建未能完成

+

+ 任务状态:{{ status_label }}
+ 配置名称:{{ filename }} +

+ +
+ 构建过程异常终止,部分安装包可能并未生成。
+ 你可以先尝试下载下方已经产出的文件(不存在的文件将提示 404),或查看 GitHub Actions 日志定位原因后重新提交构建。 +
+ + + +

+ {% if log_url %} + 查看 GitHub Actions 日志,了解具体失败原因 + {% endif %} +

+ + +
+
+{% endblock %} diff --git a/rdgenerator/templates/generated.html b/rdgenerator/templates/generated.html new file mode 100644 index 0000000..19b65cd --- /dev/null +++ b/rdgenerator/templates/generated.html @@ -0,0 +1,67 @@ +{% extends "base.html" %} + +{% block title %}构建完成 · RustDesk 客户端定制平台{% endblock %} +{% block page_class %}page-narrow{% endblock %} + +{% block content %} +
+
+
+

客户端构建完成

+

+ 配置名称:{{ filename }} · + 任务编号:{{ uuid }}
+ 请选择与目标系统对应的安装包下载。 +

+ + + +
+ {% if platform == 'windows' or platform == 'windows-x86' %} + Windows 首次运行时,可能需要在 SmartScreen 中选择「仍要运行」,或在安全软件中放行。 + {% elif platform == 'macos' %} + macOS 首次运行时,可能需要在「系统设置 → 隐私与安全性」中允许打开该应用。 + {% elif platform == 'linux' %} + 请确认系统已安装运行所需的相关依赖库。 + {% elif platform == 'android' %} + 安装时可能需要在系统设置中允许「安装未知来源应用」。 + {% endif %} +
+ + +
+
+{% endblock %} diff --git a/rdgenerator/templates/generator.html b/rdgenerator/templates/generator.html new file mode 100644 index 0000000..ba9ae48 --- /dev/null +++ b/rdgenerator/templates/generator.html @@ -0,0 +1,674 @@ +{% extends "base.html" %} + +{% block title %}客户端定制 · RustDesk 客户端定制平台{% endblock %} + +{% block content %} +
+

定制 RustDesk 客户端

+

填写以下配置并提交,系统将通过 GitHub Actions 自动构建专属客户端,通常需要 30~45 分钟,可在「我的构建」中随时查看进度。

+
+ +
+ {% csrf_token %} + + {# ---------- 平台与版本 ---------- #} +
+
目标平台与版本
+
+
+
+ +
Windows
+
64 位
+
+
+ +
Windows
+
32 位
+
+
+ +
Linux
+
deb / rpm 等
+
+
+ +
Android
+
APK
+
+
+ +
macOS
+
dmg
+
+
+ {{ form.platform }} + +
+
+ + {{ form.version }} + {% if version_source == 'github' %} +
+ + 版本列表来自 GitHub 官方仓库 rustdesk/rustdesk,跟随官方发版自动更新 +
+ {% else %} +
+ + 官方版本列表暂时获取失败,当前显示内置版本列表;保存配置或配置代理后会自动重试 +
+ {% endif %} + {% if form.version.help_text %}
{{ form.version.help_text }}
{% endif %} +
+
+ +
+
+
+
+ + {# ---------- 配置方案:账号绑定 / 载入 / 另存 / 导入导出 ---------- #} +
+
配置方案(保存 · 载入 · 导入导出)
+
+

+ 把当前配置「另存为方案」后与账号绑定,下次直接选择方案即可填表创建构建任务; + 也可导出 JSON 本地备份、从 JSON 文件导入,或前往 + 配置文件管理页统一维护。 +

+
+ + + + 管理配置文件 + +
+
+ + + + + +
+
+
+
+ +
+ {# ---------- 常规 ---------- #} +
+
常规设置
+
+
+ + {{ form.exename }} + + {{ form.exename.errors }} + {% if form.exename.help_text %}
{{ form.exename.help_text }}
{% endif %} +
+
+ + {{ form.appname }} + {{ form.appname.errors }} + {% if form.appname.help_text %}
{{ form.appname.help_text }}
{% endif %} +
+
+ + {{ form.direction }} +
+
+ + {{ form.installation }} +
+
+ + {{ form.settings }} +
+
+ + {{ form.androidappid }} + {% if form.androidappid.help_text %}
{{ form.androidappid.help_text }}
{% endif %} +
+
+
+ + {# ---------- 自定义服务器 ---------- #} +
+
自定义服务器
+
+
+
+ + {{ form.serverIP }} + {% if form.serverIP.help_text %}
{{ form.serverIP.help_text }}
{% endif %} +
+
+ + {{ form.serverPort }} +
+
+
+ + {{ form.apiServer }} + {% if form.apiServer.help_text %}
{{ form.apiServer.help_text }}
{% endif %} +
+
+ + {{ form.key }} +
+
+ + {{ form.urlLink }} + {% if form.urlLink.help_text %}
{{ form.urlLink.help_text }}
{% endif %} +
+
+ + {{ form.downloadLink }} + {% if form.downloadLink.help_text %}
{{ form.downloadLink.help_text }}
{% endif %} +
+
+ + {{ form.compname }} + {{ form.compname.errors }} + {% if form.compname.help_text %}
{{ form.compname.help_text }}
{% endif %} +
+
+
+
+ +
+ {# ---------- 安全 ---------- #} +
+
安全设置
+
+
+ + {{ form.passApproveMode }} +
+
+ + {{ form.permanentPassword }} + {% if form.permanentPassword.help_text %}
{{ form.permanentPassword.help_text }}
{% endif %} +
+
使用「隐藏连接窗口」功能时,请先设置固定访问密码。
+ + + + +
+
+ + {# ---------- 外观 ---------- #} +
+
外观定制
+
+ {{ form.iconbase64.as_hidden }} + {{ form.logobase64.as_hidden }} + {{ form.privacybase64.as_hidden }} +
+ + {{ form.iconfile }} + {{ form.iconfile.errors }} + {% if form.iconfile.help_text %}
{{ form.iconfile.help_text }}
{% endif %} +
+
+
+ + {{ form.logofile }} + {% if form.logofile.help_text %}
{{ form.logofile.help_text }}
{% endif %} +
+
+
+ + {{ form.privacyfile }} + {% if form.privacyfile.help_text %}
{{ form.privacyfile.help_text }}
{% endif %} +
+
+
+
+ + {{ form.theme }} +
+
+ + {{ form.themeDorO }} +
+
+
「默认」仅设定初始主题,客户端用户可自行更改;「强制锁定」后用户无法更改主题。
+
+
+
+ +
+ {# ---------- 权限 ---------- #} +
+
权限设置
+
+
+ + {{ form.permissionsDorO }} +
「默认」用户仍可自行修改;「强制锁定」后用户无法修改以下权限。
+
+
+ + {{ form.permissionsType }} +
+
+ + + + + + + + + + + + +
+ +
定制功能
+ + +
+
+ + {# ---------- 其他 / 高级 ---------- #} +
+
其他与高级设置
+
+ + +

+ 高级设置项参考: + RustDesk 官方文档 + + +

+
+ + {{ form.defaultManual }} + {% if form.defaultManual.help_text %}
{{ form.defaultManual.help_text }}
{% endif %} +
+
+ + {{ form.overrideManual }} + {% if form.overrideManual.help_text %}
{{ form.overrideManual.help_text }}
{% endif %} +
+
+
+
+ +
+
+ +

提交后将自动跳转到构建进度页,构建期间可关闭页面,稍后在「我的构建」中回来下载。

+
+
+
+{% endblock %} + +{% block head_extra %} + +{% endblock %} diff --git a/rdgenerator/templates/maintenance.html b/rdgenerator/templates/maintenance.html new file mode 100644 index 0000000..10b6d3b --- /dev/null +++ b/rdgenerator/templates/maintenance.html @@ -0,0 +1,39 @@ + + + + + + RustDesk 客户端定制平台 + + + +
+

RustDesk 客户端定制平台

+

平台正在维护升级中,请稍后再访问。

+
+ + diff --git a/rdgenerator/templates/maintenance_settings.html b/rdgenerator/templates/maintenance_settings.html new file mode 100644 index 0000000..018c045 --- /dev/null +++ b/rdgenerator/templates/maintenance_settings.html @@ -0,0 +1,147 @@ +{% extends "base.html" %} + +{% block title %}维护设置 · RustDesk 客户端定制平台{% endblock %} + +{% block content %} + + +
+

+ + 维护设置 +

+

+ 定时清理过期构建任务与安装包产物,并自动备份数据库。维护进程随容器启动常驻运行,每小时检查一次。 +

+
+ +
+
{{ status.total_builds }}
构建任务总数
+
{{ status.in_progress_builds }}
进行中(不清理)
+
{{ status.expired_builds }}
已到期待清理
+
{{ status.db_size|filesizeformat }}
数据库大小
+
{{ status.artifacts_size|filesizeformat }}
安装包 / 图片占用
+
{{ status.backups|length }}
现存备份份数
+
+ +
+ {# ===== 维护策略表单 ===== #} +
+
自动维护策略
+
+
+ {% csrf_token %} +
+ + {{ form.build_retention_days }} +
{{ form.build_retention_days.help_text }}
+
+
+ +
{{ form.db_backup_enabled.help_text }}
+
+
+ + {{ form.db_backup_keep }} +
{{ form.db_backup_keep.help_text }}
+
+
+
+ +
+
+
+
+
+ + {# ===== 立即执行 ===== #} +
+
手动维护
+
+

+ 立即执行一轮维护:清理超过保留期的已结束任务、7 天前的临时配置包,并立即创建一份新的数据库备份 + (不受每日一次的自动节奏限制)。 +

+
    +
  • + 当前保留策略:{% if status.retention_days > 0 %}{{ status.retention_days }} 天,预计可清理 {{ status.expired_builds }} 条已结束任务{% else %}永不自动清理{% endif %} +
  • +
  • + + 定时备份:{% if status.backup_enabled %}已开启(每 {{ status.backup_interval_hours }} 小时一次,保留 {{ status.backup_keep }} 份){% else %}已关闭{% endif %} +
  • +
  • + 数据库:{{ status.db_path }}({{ status.db_size|filesizeformat }}) +
  • +
+
+ {% csrf_token %} + +
+
+
+
+ +{# ===== 备份列表 ===== #} +
+
数据库备份
+ {% if status.backups %} +
+ + + + + + + + + + + {% for backup in status.backups %} + + + + + + + {% endfor %} + +
备份文件大小备份时间操作
{{ backup.name }}{{ backup.size|filesizeformat }}{{ backup.mtime|date:"Y-m-d H:i" }} + + 下载 + +
+
+ {% else %} +
+
+

暂无数据库备份

+

开启定时备份后会自动生成;也可以点击上方「立即执行清理与备份」马上创建一份。

+
+ {% endif %} +
+ +
+
说明
+
+

+ 备份文件是完整的 SQLite 数据库(包含账号、配置方案、构建记录与站点设置),存放在持久化数据卷 + data/backups/ 中,重建镜像与重新部署均不丢失。恢复方式:停止容器, + 用备份文件替换数据卷内的 db.sqlite3 后重新启动即可。 + 也可通过 docker-compose 环境变量 BUILD_RETENTION_DAYS、 + DB_BACKUP_ENABLED、DB_BACKUP_KEEP 配置(本页保存的优先级更高)。 +

+
+
+{% endblock %} diff --git a/rdgenerator/templates/my_builds.html b/rdgenerator/templates/my_builds.html new file mode 100644 index 0000000..2e35938 --- /dev/null +++ b/rdgenerator/templates/my_builds.html @@ -0,0 +1,78 @@ +{% extends "base.html" %} + +{% block title %}我的构建 · RustDesk 客户端定制平台{% endblock %} + +{% block content %} +
+

我的构建任务

+

这里展示你提交的全部自定义客户端构建任务,单次构建通常需要 30~45 分钟。

+
+ +{% if builds %} +
+
+ + + + + + + + + + + + + {% for run in builds %} + + + + + + + + + {% endfor %} + +
提交时间配置名称平台状态任务编号操作
{{ run.created_at|date:"Y-m-d H:i" }}{{ run.filename|default:"—" }}{{ run.platform_label }}{{ run.status_label }}{{ run.short_uuid }} + + {% if run.status == 'success' %} 下载产物{% elif run.is_finished %}查看详情{% else %}查看进度{% endif %} + + {% if run.github_log_url %} + + 日志 + + {% endif %} + {% if run.is_finished and run.config_data %} +
+ {% csrf_token %} + +
+ {% endif %} + {% if run.is_finished %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+
+{% else %} +
+
+
+

还没有构建任务

+

填写一份客户端配置,即可开始生成你的专属 RustDesk 客户端。

+ 立即定制客户端 +
+
+{% endif %} +{% endblock %} diff --git a/rdgenerator/templates/registration/login.html b/rdgenerator/templates/registration/login.html new file mode 100644 index 0000000..b426ebb --- /dev/null +++ b/rdgenerator/templates/registration/login.html @@ -0,0 +1,43 @@ +{% load static %} + + + + + 登录 · RustDesk 客户端定制平台 + + + + + + diff --git a/rdgenerator/templates/saved_configs.html b/rdgenerator/templates/saved_configs.html new file mode 100644 index 0000000..6e405a2 --- /dev/null +++ b/rdgenerator/templates/saved_configs.html @@ -0,0 +1,125 @@ +{% extends "base.html" %} + +{% block title %}配置文件管理 · RustDesk 客户端定制平台{% endblock %} + +{% block content %} + + +
+

+ + 构建配置文件 +

+

+ 在客户端定制页可把当前表单「另存为方案」,方案与你的账号绑定,下次直接选择即可创建构建任务; + 也可以在此导出为 JSON 文件备份/迁移,或从 JSON 文件导入。 +

+
+ +
+
从 JSON 文件导入
+
+
+ {% csrf_token %} +
+
+ + +
同一账号下方案名称不可重复。
+
+
+ + +
+ + 未选择任何文件 +
+
支持本平台导出的配置文件,也兼容 rdgen-cli 使用的 JSON 参数。
+
+
+ +
+
+
+ +
+
我的配置方案({{ configs|length }})
+ {% if configs %} +
+ + + + + + + + + + + + {% for cfg in configs %} + + + + + + + + {% endfor %} + +
方案名称目标平台版本更新时间操作
{{ cfg.name }}{{ cfg.platform_label }}{{ cfg.version_label }}{{ cfg.updated_at|date:"Y-m-d H:i" }} + + 载入并构建 + + + 导出 + +
+ {% csrf_token %} + +
+
+
+ {% else %} +
+
+

暂无保存的配置方案

+

在「客户端定制」页填好配置后点击「另存为方案」,下次即可一键载入,无需重复填写。

+ + 去定制页创建方案 + +
+ {% endif %} +
+ +
+
说明
+
+
    +
  • 方案内容包含「固定访问密码」等字段,仅自己可见,请勿把导出的 JSON 随意分享给他人。
  • +
  • 应用图标 / Logo / 隐私屏图片会以 base64 内嵌在方案中,无需重新上传。
  • +
  • 导出的 JSON 与定制页「导出配置为 JSON」格式一致,也可直接用于 rdgen-cli 命令行构建。
  • +
+
+
+ + +{% endblock %} diff --git a/rdgenerator/templates/site_settings.html b/rdgenerator/templates/site_settings.html new file mode 100644 index 0000000..73867cf --- /dev/null +++ b/rdgenerator/templates/site_settings.html @@ -0,0 +1,351 @@ +{% extends "base.html" %} + +{% block title %}构建平台配置 · RustDesk 客户端定制平台{% endblock %} + +{% block content %} + + +
+

+ + 构建平台配置 +

+

+ 选择实际执行构建任务的平台:GitHub Actions(云托管构建机)或自建 Gitea Actions(构建机自备、可全内网运行)。 + 支持在本页直接配置(优先级最高),也继续兼容 docker-compose 环境变量方式。 +

+
+ +{# ===== 配置状态总览 ===== #} +
+
+ 配置状态检查当前平台:{{ build_platform_label }} +
+
+ {% if build_ready and not warnings %} +
+ + {{ build_platform_label }} 构建服务配置完整,可以正常提交客户端构建任务。 +
+ {% else %} +
    + {% if build_ready %} +
  • {{ build_platform_label }} 连接信息已配置
  • + {% else %} +
  • {{ build_platform_label }} 连接信息缺失,暂无法触发构建
  • + {% endif %} + {% for w in warnings %} +
  • {{ w }}
  • + {% endfor %} +
+ {% endif %} +
+
+ +{# ===== 配置表单 ===== #} +
+ {% csrf_token %} + + {# --- 构建平台选择(自定义平台卡,单选) --- #} +
+
构建平台
+
+

切换后仅影响新提交的构建任务;历史任务仍保存在原平台并可继续查询状态与日志。

+
+ + +
+
+
+ + {# --- 各配置分组(platform 组已在上方自定义渲染) --- #} + {% for group_key, group_title, group_desc in groups %}{% if group_key != 'platform' %} +
+
{{ group_title }}
+
+

{{ group_desc }}

+ {% for row in rows %}{% if row.group == group_key %} +
+ + {{ row.field }} + {% if row.sensitive %} +
+ + {% if row.source == 'db' %} + 已在后台保存 + {% elif row.source == 'env' %} + 已通过环境变量配置 + {% else %} + 未配置(使用默认值) + {% endif %} +
+ {% endif %} + {% if row.help %}
{{ row.help }}
{% endif %} +
+ {% endif %}{% endfor %} +
+
+ {% endif %}{% endfor %} + + {# --- 保存与连接测试(始终可见,测试目标跟随当前所选平台) --- #} +
+
+
+ + +
+ +
+
+
+ +{# ===== GitHub 配置引导 ===== #} +
+
+ GitHub 首次配置引导(约 5 分钟) +
+
+
    +
  1. +
    Fork 构建仓库
    +

    + 打开上游仓库 github.com/bryangerlach/rdgen + , + 点击右上角 Fork 复制到你自己的 GitHub 账号下。 +

    +
  2. +
  3. +
    启用 GitHub Actions
    +

    + 进入 Fork 后的仓库页面,打开 + Actions 页面 + , + 如页面提示则点击绿色的 Enable Actions 按钮。 +

    +
  4. +
  5. +
    创建 Fine-grained 访问令牌(PAT)
    +

    + 打开 + + Generate new token , + 按如下设置后生成,并把令牌(github_pat_ 开头)填入上方「GitHub 访问令牌」: +

    +
      +
    • Repository access:选择 Only select repositories,勾选你 Fork 的仓库
    • +
    • Repository permissions:Actions 设为 Read and write,Workflows 设为 Read and write
    • +
    • Expiration 建议按需设置,过期后需要回到这里更新令牌
    • +
    +
  6. +
  7. +
    添加两个仓库 Secret
    +

    + 打开仓库 + Settings → Secrets and variables → Actions + , + 分别 New repository secret: +

    +
      +
    • GENURL:本平台的公网访问地址,须与上方「本平台外部访问地址」一致,例如 https://rdgen.example.com
    • +
    • ZIP_PASSWORD:一个随机强密码(可用 python3 -c 'import secrets; print(secrets.token_hex(100))' 生成), + 须与上方「配置压缩包密码」完全一致
    • +
    +
  8. +
  9. +
    保存并验证
    +

    填写上方表单后点击「保存配置」,再点击「测试 GitHub 连接」,看到「连接正常」提示即配置完成,可以去客户端定制页提交构建了。

    +
  10. +
+
+
+ +{# ===== Gitea 配置引导 ===== #} +
+
+ Gitea 首次配置引导(需自备 Gitea 与构建机) +
+
+
    +
  1. +
    部署 Gitea 1.27+ 并准备构建仓库
    +

    + 自建一个 Gitea 实例(建议 1.27 或更高版本,低版本不支持运行详情回传), + 把本项目仓库推送到该实例(仓库内含 .gitea/workflows 适配工作流)。 + + 官方 Quick Start +

    +
  2. +
  3. +
    启用仓库 Actions
    +

    + 进入 + {% if gitea_server and gitea_owner %} + + 仓库设置页 + {% else %}仓库设置页{% endif %} + ,勾选 Enable Repository Actions。 +

    +
  4. +
  5. +
    注册 act_runner 构建机
    +

    在仓库 Settings → Actions → Runners 创建注册令牌,并在构建机上安装注册 act_runner:

    +
      +
    • Linux:可用 Docker 模式(需预装 QEMU 以构建 arm64);
    • +
    • Windows:只能 host 模式直接跑在 Windows 机器上(MSVC/Flutter/vcpkg 环境);
    • +
    • macOS:只能 host 模式跑在 Apple 硬件上;
    • +
    • runner 自定义标签需与工作流一致:ubuntu-22.04、ubuntu-22.04-arm、windows-2022、macos-14、macos-15-intel。
    • +
    +
  6. +
  7. +
    配置仓库 Secrets
    +

    + 在仓库 Settings → Actions → Secrets 中添加 + GENURL(本平台地址)与 ZIP_PASSWORD(与上方完全一致); + 如需 Android/macOS 签名,再添加 ANDROID_SIGNING_KEY、MACOS_P12_BASE64、SIGN_BASE_URL、SIGN_API_KEY。 +

    +
  8. +
  9. +
    第三方 actions 的网络说明
    +

    + 工作流使用的第三方 action 默认从 github.com 拉取;纯内网环境请为 runner 配置出网白名单, + 或在 Gitea 的 app.ini 配置 [actions].DEFAULT_ACTIONS_URL 走自建 action 镜像。 + 完整部署步骤(含 vcpkg 文件缓存配置)见项目 setup.md 的「Gitea 部署附录」。 +

    +
  10. +
  11. +
    保存并验证
    +

    填写上方 Gitea 配置后点击「保存配置」,再点击「测试 Gitea 连接」,看到「连接正常」提示即可提交构建。

    +
  12. +
+
+
+ +{# ===== 环境变量方式说明 ===== #} +
+
+ 环境变量方式(可选,继续支持) +
+
+

+ 也可以在 docker-compose.yml 的 environment 中配置以下变量,容器重启后自动生效; + 本页保存的同名配置优先级更高(值与环境变量相同时会自动回退,不产生重复覆盖)。 +

+
+ + + + + + + + + + + + + + + + + + + + + +
环境变量对应配置说明
BUILD_PLATFORM构建平台github(默认)或 gitea
GHUSERGitHub 用户名Fork 仓库所在账号
GHBEARERGitHub 访问令牌Fine-grained PAT,Actions/Workflows 读写
GENURL平台外部访问地址两平台共用,供构建机回传产物,公网/内网可访问
ZIP_PASSWORD配置压缩包密码两平台共用,须与仓库 Secret 一致
REPONAMEGitHub 仓库名称默认 rdgen
GHBRANCHGitHub 工作流分支默认 master
PROTOCOL平台访问协议https(默认)或 http
SH_SECRETGitHub 自托管 Runner 密钥仅 GitHub 自托管构建时使用
WEBHOOK_SECRET回调校验密钥两平台共用,留空不校验
GH_PROXYGitHub 访问代理如 http://host:port,留空直连
GITEA_SERVER_URLGitea 服务器地址如 https://gitea.example.com
GITEA_TOKENGitea 访问令牌需仓库 Actions 读写权限
GITEA_OWNERGitea 仓库属主用户名或组织名
GITEA_REPOGitea 仓库名称默认 rdgen
GITEA_BRANCHGitea 工作流分支默认 master
GITEA_PROXYGitea 访问代理内网直连通常留空
+
+
+
+{% endblock %} + +{% block head_extra %} + +{% endblock %} diff --git a/rdgenerator/templates/tokens.html b/rdgenerator/templates/tokens.html new file mode 100644 index 0000000..b76dcea --- /dev/null +++ b/rdgenerator/templates/tokens.html @@ -0,0 +1,111 @@ +{% extends "base.html" %} + +{% block title %}接口令牌 · RustDesk 客户端定制平台{% endblock %} +{% block page_class %}page-narrow{% endblock %} + +{% block content %} +
+

接口令牌(API Token)

+

+ 通过 JSON API 或 rdgen-cli 命令行提交构建任务时,请在请求头携带令牌: + Authorization: Token <你的令牌>。请妥善保管,不要泄露给他人。 +

+
+ +{% if new_plain_key %} +
+
+ 新令牌已生成,请立即复制保存: +
+ {{ new_plain_key }} + +
+
为保障安全,离开本页后将无法再次查看完整令牌。
+
+
+{% endif %} + +
+
新建令牌
+
+
+ {% csrf_token %} +
+ +
+ {{ form.name }} + +
+ {% if form.name.help_text %}
{{ form.name.help_text }}
{% endif %} +
+
+
+
+ +
+
我的令牌
+ {% if tokens %} +
+ + + + + + + + + + + + + {% for token in tokens %} + + + + + + + + + {% endfor %} + +
名称令牌(仅显示前缀)状态创建时间最后使用操作
{{ token.name }}{{ token.key|slice:":12" }}… + {% if token.is_active %} + 启用中 + {% else %} + 已停用 + {% endif %} + {{ token.created_at|date:"Y-m-d H:i" }} + {% if token.last_used_at %}{{ token.last_used_at|date:"Y-m-d H:i" }}{% else %}从未使用{% endif %} + +
+ {% csrf_token %} + +
+
+
+ {% else %} +
+
+

暂无接口令牌

+

当你需要通过命令行或 API 自动提交构建时,在此创建一个令牌。

+
+ {% endif %} +
+{% endblock %} + +{% block head_extra %} + +{% endblock %} diff --git a/rdgenerator/templates/user_form.html b/rdgenerator/templates/user_form.html new file mode 100644 index 0000000..bfcccd6 --- /dev/null +++ b/rdgenerator/templates/user_form.html @@ -0,0 +1,44 @@ +{% extends "base.html" %} + +{% block title %}编辑用户 · 后台管理{% endblock %} +{% block page_class %}page-narrow{% endblock %} + +{% block content %} + +
+

编辑用户

+

正在编辑用户:{{ target.username }}

+
+ +
+
+
+ {% csrf_token %} +
+ + {{ form.email }} +
+ + +
+ + {{ form.new_password }} + {{ form.new_password.errors }} +
+
+ + + 返回列表 + +
+
+
+
+{% endblock %} diff --git a/rdgenerator/templates/user_list.html b/rdgenerator/templates/user_list.html new file mode 100644 index 0000000..be2f587 --- /dev/null +++ b/rdgenerator/templates/user_list.html @@ -0,0 +1,97 @@ +{% extends "base.html" %} + +{% block title %}用户管理 · 后台管理{% endblock %} + +{% block content %} + +
+

用户管理

+

平台不开放公开注册,账号由管理员在此统一创建和管理。

+
+ +
+
新建用户
+
+
+ {% csrf_token %} +
+ + {{ form.username }} + {{ form.username.errors }} +
+
+ + {{ form.email }} +
+
+
+ + {{ form.password }} + {{ form.password.errors }} + {% if form.password.help_text %}
{{ form.password.help_text }}
{% endif %} +
+
+ + {{ form.password_confirm }} + {{ form.password_confirm.errors }} +
+
+ + +
+
+
+ +
+
用户列表({{ users|length }})
+ {% if users %} +
+ + + + + + + + + + + + + {% for u in users %} + + + + + + + + + {% endfor %} + +
用户名角色状态构建数注册时间操作
+ {{ u.username }} + {% if u.email %}
{{ u.email }}
{% endif %} +
+ {% if u.is_superuser %}超级管理员 + {% elif u.is_staff %}管理员 + {% else %}普通用户{% endif %} + + {% if u.is_active %}启用 + {% else %}已停用{% endif %} + {{ u.build_count }}{{ u.date_joined|date:"Y-m-d" }} + 编辑 +
+
+ {% else %} +
+
+

暂无用户

+
+ {% endif %} +
+{% endblock %} diff --git a/rdgenerator/templates/waiting.html b/rdgenerator/templates/waiting.html new file mode 100644 index 0000000..9822fdf --- /dev/null +++ b/rdgenerator/templates/waiting.html @@ -0,0 +1,77 @@ +{% extends "base.html" %} + +{% block title %}构建进行中 · RustDesk 客户端定制平台{% endblock %} +{% block page_class %}page-narrow{% endblock %} + +{% block content %} +
+
+
+

正在构建自定义客户端

+

+ 构建通常需要 30~45 分钟,本页每 30 秒自动刷新一次状态。
+ 你也可以关闭本页,稍后到「我的构建」中查看结果。 +

+ +
+ +

+ 当前状态: + {{ status_label }} + +

+ +
+
任务信息
+
配置名称:{{ filename }}
+
目标平台:{{ platform }}
+ {% if log_url %} + + {% endif %} +
+ + +
+
+{% endblock %} + +{% block head_extra %} + +{% endblock %} diff --git a/rdgenerator/tests.py b/rdgenerator/tests.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/rdgenerator/tests.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/rdgenerator/versions.py b/rdgenerator/versions.py new file mode 100644 index 0000000..004bc09 --- /dev/null +++ b/rdgenerator/versions.py @@ -0,0 +1,85 @@ +""" +RustDesk 可选版本列表。 + +优先从 GitHub 官方仓库 rustdesk/rustdesk 的 tags 动态获取(跟随官方发版自动更新), +进程内缓存 6 小时;网络失败时回退内置版本列表,且失败结果短缓存 10 分钟, +避免每次打开页面都等待 GitHub 超时。所有请求经由 ghnet,自动使用代理配置。 +""" +import re +import time + +from . import ghnet + +TAGS_URL = 'https://api.github.com/repos/rustdesk/rustdesk/tags?per_page=100' + +# 离线兜底(截至本版本发布时的近期正式版) +FALLBACK_VERSIONS = [ + '1.4.9', '1.4.8', '1.4.7', '1.4.6', '1.4.5', + '1.4.4', '1.4.3', '1.4.2', '1.4.1', '1.4.0', +] + +# 形如 1.4.9 / 1.4.10 / 1.5.0-rc.1 的正式 tag +TAG_RE = re.compile(r'^\d+\.\d+\.\d+(?:[.-][0-9A-Za-z.]+)?$') +# 表单/API 对用户提交版本号的宽松校验,兼容动态列表刷新窗口期 +VERSION_RE = re.compile(r'^(?:master|main|\d+\.\d+\.\d+(?:[.-][0-9A-Za-z.]+)?)$') + +MAX_VERSIONS = 20 +CACHE_TTL = 6 * 3600 # 成功结果缓存 6 小时 +FALLBACK_TTL = 10 * 60 # 失败回退缓存 10 分钟 + +_memo = { + 'ts': 0.0, + 'ttl': CACHE_TTL, + 'versions': FALLBACK_VERSIONS, + 'source': 'builtin', +} + + +def _sort_versions(versions): + """按版本号数字段降序排列,预发布标识排在同正式版之后。""" + def key(v): + main, _, suffix = v.partition('-') + parts = [int(x) if x.isdigit() else 0 for x in main.split('.')] + parts += [0] * (3 - len(parts)) + # 无后缀(正式版)排在 rc/beta 之前 + return (parts, 1 if not suffix else 0, suffix) + return sorted(versions, key=key, reverse=True) + + +def _fetch_from_github(): + resp = ghnet.get(TAGS_URL, timeout=8) + resp.raise_for_status() + tags = [item['name'].lstrip('v') for item in resp.json() if item.get('name')] + versions = _sort_versions({t for t in tags if TAG_RE.match(t)}) + return versions[:MAX_VERSIONS] + + +def get_versions(force=False): + """返回 (版本号列表, 来源 source, 缓存时间戳);source: github / builtin。""" + now = time.time() + if not force and now - _memo['ts'] < _memo['ttl']: + return _memo['versions'], _memo['source'], _memo['ts'] + + try: + versions = _fetch_from_github() + if versions: + _memo.update(ts=now, ttl=CACHE_TTL, versions=versions, source='github') + else: + _memo.update(ts=now, ttl=FALLBACK_TTL, versions=FALLBACK_VERSIONS, source='builtin') + except Exception: + _memo.update(ts=now, ttl=FALLBACK_TTL, versions=FALLBACK_VERSIONS, source='builtin') + return _memo['versions'], _memo['source'], _memo['ts'] + + +def version_choices(): + """供 ChoiceField 使用的 (value, label) 列表。""" + versions, source, _ = get_versions() + label_suffix = '' if source == 'github' else '(内置列表,获取官方列表失败)' + choices = [('master', '开发版(nightly 每夜构建)')] + choices += [(v, v) for v in versions] + return choices + + +def is_valid_version(value): + """API / 表单提交的版本号合法性校验。""" + return bool(value) and bool(VERSION_RE.match(str(value))) diff --git a/rdgenerator/views.py b/rdgenerator/views.py new file mode 100644 index 0000000..d09224e --- /dev/null +++ b/rdgenerator/views.py @@ -0,0 +1,695 @@ +import io +import json +import os +import re +import uuid +from pathlib import Path +from urllib.parse import quote + +import base64 +import pyzipper +from PIL import Image + +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core.exceptions import PermissionDenied +from django.core.files.base import ContentFile +from django.db.models import Q +from django.http import ( + HttpResponse, + JsonResponse, + HttpResponseForbidden, + Http404, +) +from django.shortcuts import render +from django.views.decorators.csrf import csrf_exempt + +from . import app_settings, build_backends, versions as rdeskVersions +from .forms import GenerateForm +from .models import GithubRun, STATUS_LABELS, STATUS_BADGE + + +def _webhook_denied(request): + """配置了 WEBHOOK_SECRET 时,校验 GitHub Actions 回调请求头;未配置则放行(兼容旧工作流)。""" + secret = app_settings.get_value('WEBHOOK_SECRET') + if secret and request.headers.get('X-Webhook-Secret') != secret: + return HttpResponseForbidden('无效的 Webhook 密钥') + return None + + +def _status_extra(status_code): + return ( + STATUS_LABELS.get(status_code, status_code), + STATUS_BADGE.get(status_code, 'badge-secondary'), + ) + + +def _can_access_run(request, run): + """任务提交者本人或管理员可访问;历史任务无归属人时登录用户均可访问。""" + if request.user.is_staff: + return True + return not run.created_by_id or run.created_by_id == request.user.id + + +def generate_custom_client(params, full_url, user=None): + """ + 网页表单与 JSON API 共用的核心生成逻辑。 + + Args: + params: 包含全部配置项的字典(键名与 GenerateForm 字段一致) + full_url: 本服务的完整 URL(协议 + 主机名) + user: 提交任务的登录用户(可为 None,如匿名 API 调用) + + Returns: + 成功时包含 success/uuid/filename/platform/log_url; + 失败时包含 success=False、error(中文提示)及 status_code。 + """ + user_secret = params.get('sh_secret_field', '') + selfhosted = (app_settings.get_value('SH_SECRET') == user_secret) + platform = params.get('platform', 'windows') + version = params.get('version', '1.4.9') + delayFix = params.get('delayFix', True) + xOffline = params.get('xOffline', False) + hidecm = params.get('hidecm', False) + removeNewVersionNotif = params.get('removeNewVersionNotif', False) + server = params.get('serverIP', '') + serverPort = params.get('serverPort', '') + key = params.get('key', '') + apiServer = params.get('apiServer', '') + urlLink = params.get('urlLink', '') + downloadLink = params.get('downloadLink', '') + if not server: + server = 'rs-ny.rustdesk.com' # RustDesk 默认服务器 + if not serverPort: + serverPort = '21116' # RustDesk 默认会合端口 + if not key: + key = 'OeVuKk5nlHiXp+APNn0Y3pC1Iwpwn44JGqrQCsWqmBw=' # RustDesk 默认公钥 + if not apiServer: + apiServer = server+":21114" + if not urlLink: + urlLink = "https://rustdesk.com" + if not downloadLink: + downloadLink = "https://rustdesk.com/download" + direction = params.get('direction', 'both') + installation = params.get('installation', 'installationY') + settings = params.get('settings', 'settingsY') + appname = params.get('appname', '') + if not appname: + appname = "rustdesk" + filename = params.get('exename', 'rustdesk') + compname = params.get('compname', '') + if not compname: + compname = "Purslane Ltd" + androidappid = params.get('androidappid', '') + if not androidappid: + androidappid = "com.carriez.flutter_hbb" + compname = compname.replace("&","\\&") + permPass = params.get('permanentPassword', '') + theme = params.get('theme', 'system') + themeDorO = params.get('themeDorO', 'default') + passApproveMode = params.get('passApproveMode', 'password-click') + denyLan = params.get('denyLan', False) + enableDirectIP = params.get('enableDirectIP', False) + autoClose = params.get('autoClose', False) + permissionsDorO = params.get('permissionsDorO', 'default') + permissionsType = params.get('permissionsType', 'custom') + enableKeyboard = params.get('enableKeyboard', True) + enableClipboard = params.get('enableClipboard', True) + enableFileTransfer = params.get('enableFileTransfer', True) + enableAudio = params.get('enableAudio', True) + enableTCP = params.get('enableTCP', True) + enableRemoteRestart = params.get('enableRemoteRestart', True) + enableRecording = params.get('enableRecording', True) + enableBlockingInput = params.get('enableBlockingInput', True) + enableRemoteModi = params.get('enableRemoteModi', False) + removeWallpaper = params.get('removeWallpaper', True) + defaultManual = params.get('defaultManual', '') + overrideManual = params.get('overrideManual', '') + enablePrinter = params.get('enablePrinter', True) + enableCamera = params.get('enableCamera', True) + enableTerminal = params.get('enableTerminal', True) + + if all(char.isascii() for char in filename): + filename = re.sub(r'[^\w\s-]', '_', filename).strip() + filename = filename.replace(" ","_") + else: + filename = "rustdesk" + if not all(char.isascii() for char in appname): + appname = "rustdesk" + myuuid = str(uuid.uuid4()) + + try: + iconfile = params.get('iconfile') + if not iconfile: + iconfile = params.get('iconbase64') + iconlink_url, iconlink_uuid, iconlink_file = save_png(iconfile,myuuid,full_url,"icon.png") + except Exception: + print("获取图标失败,使用默认图标") + iconlink_url = "false" + iconlink_uuid = "false" + iconlink_file = "false" + try: + logofile = params.get('logofile') + if not logofile: + logofile = params.get('logobase64') + logolink_url, logolink_uuid, logolink_file = save_png(logofile,myuuid,full_url,"logo.png") + except Exception: + print("获取 Logo 失败") + logolink_url = "false" + logolink_uuid = "false" + logolink_file = "false" + try: + privacyfile = params.get('privacyfile') + if not privacyfile: + privacyfile = params.get('privacybase64') + privacylink_url, privacylink_uuid, privacylink_file = save_png(privacyfile,myuuid,full_url,"privacy.png") + except Exception: + print("获取隐私屏图片失败") + privacylink_url = "false" + privacylink_uuid = "false" + privacylink_file = "false" + + ### 生成 custom.txt 配置 JSON,作为工作流输入 + decodedCustom = {} + if direction != "Both": + decodedCustom['conn-type'] = direction + if installation == "installationN": + decodedCustom['disable-installation'] = 'Y' + if settings == "settingsN": + decodedCustom['disable-settings'] = 'Y' + if appname.lower() != "rustdesk" and appname != "": + decodedCustom['app-name'] = appname + decodedCustom['override-settings'] = {} + decodedCustom['default-settings'] = {} + if permPass != "": + decodedCustom['password'] = permPass + if theme != "system": + if themeDorO == "default": + if platform == "windows-x86": + decodedCustom['default-settings']['allow-darktheme'] = 'Y' if theme == "dark" else 'N' + else: + decodedCustom['default-settings']['theme'] = theme + elif themeDorO == "override": + if platform == "windows-x86": + decodedCustom['override-settings']['allow-darktheme'] = 'Y' if theme == "dark" else 'N' + else: + decodedCustom['override-settings']['theme'] = theme + decodedCustom['enable-lan-discovery'] = 'N' if denyLan else 'Y' + #decodedCustom['direct-server'] = 'Y' if enableDirectIP else 'N' + decodedCustom['allow-auto-disconnect'] = 'Y' if autoClose else 'N' + + if permissionsDorO == "default": + decodedCustom['default-settings']['access-mode'] = permissionsType + decodedCustom['default-settings']['enable-keyboard'] = 'Y' if enableKeyboard else 'N' + decodedCustom['default-settings']['enable-clipboard'] = 'Y' if enableClipboard else 'N' + decodedCustom['default-settings']['enable-file-transfer'] = 'Y' if enableFileTransfer else 'N' + decodedCustom['default-settings']['enable-audio'] = 'Y' if enableAudio else 'N' + decodedCustom['default-settings']['enable-tunnel'] = 'Y' if enableTCP else 'N' + decodedCustom['default-settings']['enable-remote-restart'] = 'Y' if enableRemoteRestart else 'N' + decodedCustom['default-settings']['enable-record-session'] = 'Y' if enableRecording else 'N' + decodedCustom['default-settings']['enable-block-input'] = 'Y' if enableBlockingInput else 'N' + decodedCustom['default-settings']['allow-remote-config-modification'] = 'Y' if enableRemoteModi else 'N' + decodedCustom['default-settings']['direct-server'] = 'Y' if enableDirectIP else 'N' + decodedCustom['default-settings']['verification-method'] = 'use-permanent-password' if hidecm else 'use-both-passwords' + decodedCustom['default-settings']['approve-mode'] = passApproveMode + decodedCustom['default-settings']['allow-hide-cm'] = 'Y' if hidecm else 'N' + decodedCustom['default-settings']['allow-remove-wallpaper'] = 'Y' if removeWallpaper else 'N' + decodedCustom['default-settings']['enable-remote-printer'] = 'Y' if enablePrinter else 'N' + decodedCustom['default-settings']['enable-camera'] = 'Y' if enableCamera else 'N' + decodedCustom['default-settings']['enable-terminal'] = 'Y' if enableTerminal else 'N' + + + else: + decodedCustom['override-settings']['access-mode'] = permissionsType + decodedCustom['override-settings']['enable-keyboard'] = 'Y' if enableKeyboard else 'N' + decodedCustom['override-settings']['enable-clipboard'] = 'Y' if enableClipboard else 'N' + decodedCustom['override-settings']['enable-file-transfer'] = 'Y' if enableFileTransfer else 'N' + decodedCustom['override-settings']['enable-audio'] = 'Y' if enableAudio else 'N' + decodedCustom['override-settings']['enable-tunnel'] = 'Y' if enableTCP else 'N' + decodedCustom['override-settings']['enable-remote-restart'] = 'Y' if enableRemoteRestart else 'N' + decodedCustom['override-settings']['enable-record-session'] = 'Y' if enableRecording else 'N' + decodedCustom['override-settings']['enable-block-input'] = 'Y' if enableBlockingInput else 'N' + decodedCustom['override-settings']['allow-remote-config-modification'] = 'Y' if enableRemoteModi else 'N' + decodedCustom['override-settings']['direct-server'] = 'Y' if enableDirectIP else 'N' + decodedCustom['override-settings']['verification-method'] = 'use-permanent-password' if hidecm else 'use-both-passwords' + decodedCustom['override-settings']['approve-mode'] = passApproveMode + decodedCustom['override-settings']['allow-hide-cm'] = 'Y' if hidecm else 'N' + decodedCustom['override-settings']['allow-remove-wallpaper'] = 'Y' if removeWallpaper else 'N' + decodedCustom['override-settings']['enable-remote-printer'] = 'Y' if enablePrinter else 'N' + decodedCustom['override-settings']['enable-camera'] = 'Y' if enableCamera else 'N' + decodedCustom['override-settings']['enable-terminal'] = 'Y' if enableTerminal else 'N' + if direction == 'incoming': + decodedCustom['override-settings']['custom-rendezvous-server'] = server + decodedCustom['override-settings']['api-server'] = apiServer + + if defaultManual: + for line in defaultManual.splitlines(): + if '=' in line: + k, value = line.split('=', 1) + decodedCustom['default-settings'][k.strip()] = value.strip() + + if overrideManual: + for line in overrideManual.splitlines(): + if '=' in line: + k, value = line.split('=', 1) + decodedCustom['override-settings'][k.strip()] = value.strip() + + decodedCustomJson = json.dumps(decodedCustom) + + string_bytes = decodedCustomJson.encode("ascii") + base64_bytes = base64.b64encode(string_bytes) + encodedCustom = base64_bytes.decode("ascii") + + #### 触发构建平台工作流(GitHub / Gitea 可在后台「构建平台配置」切换) + backend = build_backends.get_backend() + workflow_file = backend.workflow_file_for(platform, selfhosted=bool(selfhosted)) + + inputs_raw = { + "server":server, + "serverPort":serverPort, + "key":key, + "apiServer":apiServer, + "custom":encodedCustom, + "uuid":myuuid, + "iconlink_url":iconlink_url, + "iconlink_uuid":iconlink_uuid, + "iconlink_file":iconlink_file, + "logolink_url":logolink_url, + "logolink_uuid":logolink_uuid, + "logolink_file":logolink_file, + "privacylink_url":privacylink_url, + "privacylink_uuid":privacylink_uuid, + "privacylink_file":privacylink_file, + "appname":appname, + "genurl":app_settings.get_value('GENURL'), + "urlLink":urlLink, + "downloadLink":downloadLink, + "delayFix": 'true' if delayFix else 'false', + "rdgen":'true', + "xOffline": 'true' if xOffline else 'false', + "removeNewVersionNotif": 'true' if removeNewVersionNotif else 'false', + "compname": compname, + "androidappid":androidappid, + "filename":filename + } + + # ZIP_PASSWORD 为空时 pyzipper 的 AES 加密会直接抛 RuntimeError(500), + # 这里前置校验,返回可操作的中文提示(网页与 API 共用此函数)。 + zip_password = app_settings.get_value('ZIP_PASSWORD') + if not zip_password.strip(): + return { + "success": False, + "error": ( + "配置压缩包密码(ZIP_PASSWORD)未配置,无法加密构建配置包。" + "请联系管理员在后台「GitHub 构建配置 - 回调与加密」中设置," + "并确保与 GitHub 仓库 Secret「ZIP_PASSWORD」完全一致。" + ), + "status_code": 500, + } + + temp_json_path = f"data_{uuid.uuid4()}.json" + zip_filename = f"secrets_{uuid.uuid4()}.zip" + zip_path = "temp_zips/%s" % (zip_filename) + Path("temp_zips").mkdir(parents=True, exist_ok=True) + + with open(temp_json_path, "w") as f: + json.dump(inputs_raw, f) + + with pyzipper.AESZipFile(zip_path, 'w', compression=pyzipper.ZIP_LZMA, encryption=pyzipper.WZ_AES) as zf: + zf.setpassword(zip_password.encode()) + zf.write(temp_json_path, arcname="secrets.json") + + if os.path.exists(temp_json_path): + os.remove(temp_json_path) + + zipJson = {} + zipJson['url'] = full_url + zipJson['file'] = zip_filename + + zip_url = json.dumps(zipJson) + + dispatch_inputs = { + "version": version, + "zip_url": zip_url, + } + # 保存提交时的配置快照,供终态任务「一键重试」。 + # 仅保留 JSON 安全类型(上传文件对象跳过——网页提交时图片已转为 + # iconbase64/logobase64/privacybase64 data URL 字符串,不会丢失)。 + config_snapshot = {} + for _k, _v in params.items(): + if isinstance(_v, (str, int, float, bool)) or _v is None: + config_snapshot[_k] = _v + + new_github_run = GithubRun( + uuid=myuuid, + status="Starting generator...please wait", + platform=platform, + filename=filename, + config_data=config_snapshot, + backend=backend.name, + created_by=(user if user is not None and getattr(user, 'is_authenticated', False) else None), + ) + try: + dispatch_result = backend.dispatch(workflow_file, dispatch_inputs, timeout=20) + new_github_run.github_run_id = dispatch_result.run_id + new_github_run.status = "in_progress" + new_github_run.save() + + return { + "success": True, + "uuid": myuuid, + "filename": filename, + "platform": platform, + "log_url": dispatch_result.html_url, + "run": new_github_run, + } + except build_backends.base.BackendError as e: + return { + "success": False, + "error": e.message, + "status_code": 502 + } + except Exception as e: + return { + "success": False, + "error": f"触发构建服务时发生未预期的错误:{str(e)}", + "status_code": 502 + } + + +def _get_run_status(uuid_val): + """ + 查询构建状态(网页与 JSON API 共用)。 + """ + try: + gh_run = GithubRun.objects.get(uuid=uuid_val) + except GithubRun.DoesNotExist: + return {"found": False} + + # 历史任务按其记录的构建平台查询,平台切换后不影响在途任务 + backend = build_backends.get_backend(gh_run.backend or 'github') + github_log_url = backend.log_url(gh_run.github_run_id) if gh_run.github_run_id else '' + + if gh_run.status not in ['success', 'failure', 'cancelled', 'timed_out', 'skipped'] and gh_run.github_run_id: + try: + info = backend.get_run(gh_run.github_run_id) + if info is not None and info.finished: + gh_run.status = info.status + gh_run.save() + except Exception as e: + print(f"查询构建状态出错:{e}") + + return { + "found": True, + "status": gh_run.status, + "github_log_url": github_log_url, + "gh_run": gh_run + } + + +@login_required +def generator_view(request): + # 版本下拉始终刷新为官方仓库最新 tags(内部带缓存与内置兜底,不会拖慢页面) + version_choices = rdeskVersions.version_choices() + if request.method == 'POST': + form = GenerateForm(request.POST, request.FILES) + form.fields['version'].choices = version_choices + if form.is_valid(): + params = form.cleaned_data + full_url = f"{app_settings.get_value('PROTOCOL')}://{request.get_host()}" + result = generate_custom_client(params, full_url, user=request.user) + if result['success']: + status_label, status_badge = _status_extra("Starting generator...please wait") + return render(request, 'waiting.html', { + 'filename': result['filename'], + 'uuid': result['uuid'], + 'status': "Starting generator...please wait", + 'status_label': status_label, + 'status_badge': status_badge, + 'platform': result['platform'], + 'log_url': result['log_url'] + }) + else: + messages.error(request, result['error']) + else: + messages.error(request, "表单校验未通过,请根据下方提示修正后重新提交。") + else: + form = GenerateForm() + form.fields['version'].choices = version_choices + + _, version_source, version_ts = rdeskVersions.get_versions() + profiles = request.user.saved_configs.all().values('id', 'name') + return render(request, 'generator.html', { + 'form': form, + 'profiles': profiles, + 'version_source': version_source, + }) + + +@login_required +def check_for_file(request): + filename = request.GET.get('filename') + uuid_val = request.GET.get('uuid') + platform = request.GET.get('platform') + + result = _get_run_status(uuid_val) + if not result['found']: + raise Http404("未找到对应的构建任务") + + gh_run = result['gh_run'] + if not _can_access_run(request, gh_run): + raise PermissionDenied("无权查看该构建任务") + + github_log_url = result['github_log_url'] + status_label, status_badge = _status_extra(gh_run.status) + + context = { + 'filename': filename, + 'uuid': uuid_val, + 'platform': platform, + 'status': gh_run.status, + 'status_label': status_label, + 'status_badge': status_badge, + 'log_url': github_log_url, + } + + if gh_run.status == "success": + return render(request, 'generated.html', context) + elif gh_run.status in ['failure', 'cancelled', 'timed_out', 'skipped', 'action_required']: + return render(request, 'failure.html', context) + else: + return render(request, 'waiting.html', context) + + +@login_required +def download(request): + filename = request.GET.get('filename', '') + uuid_val = request.GET.get('uuid', '') + if not filename or not uuid_val: + raise Http404 + + gh_run = GithubRun.objects.filter(uuid=uuid_val).first() + if gh_run is None: + raise Http404("未找到对应的构建任务") + if not _can_access_run(request, gh_run): + raise PermissionDenied("无权下载该文件") + + # 防路径穿越:最终路径必须位于 exe// 目录内 + base_dir = os.path.abspath(os.path.join('exe', uuid_val)) + file_path = os.path.abspath(os.path.join(base_dir, os.path.basename(filename))) + if not file_path.startswith(base_dir + os.sep) or not os.path.isfile(file_path): + raise Http404("文件不存在或尚未生成完成") + + from django.http import FileResponse + return FileResponse( + open(file_path, 'rb'), + content_type='application/octet-stream', + as_attachment=True, + filename=os.path.basename(filename), + ) + + +def get_png(request): + """供 GitHub Actions 工作流下载构建所用的图片资源。""" + filename = request.GET.get('filename', '') + uuid_val = request.GET.get('uuid', '') + if not filename or not uuid_val: + raise Http404 + + base_dir = os.path.abspath(os.path.join('png', uuid_val)) + file_path = os.path.abspath(os.path.join(base_dir, os.path.basename(filename))) + if not file_path.startswith(base_dir + os.sep) or not os.path.isfile(file_path): + raise Http404("图片不存在") + + from django.http import FileResponse + return FileResponse(open(file_path, 'rb'), content_type='image/png') + + +@csrf_exempt +def update_github_run(request): + """GitHub Actions 回调:更新构建状态。""" + denied = _webhook_denied(request) + if denied: + return denied + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return HttpResponse(status=400) + myuuid = data.get('uuid') + mystatus = data.get('status') + if not myuuid or not mystatus: + return HttpResponse(status=400) + GithubRun.objects.filter(Q(uuid=myuuid)).update(status=mystatus) + return HttpResponse('') + + +def resize_and_encode_icon(imagefile): + maxWidth = 200 + try: + with io.BytesIO() as image_buffer: + for chunk in imagefile.chunks(): + image_buffer.write(chunk) + image_buffer.seek(0) + + img = Image.open(image_buffer) + imgcopy = img.copy() + except (IOError, OSError): + raise ValueError("上传的文件不是有效的图片格式。") + + # 无需缩放时直接返回 + if img.size[0] <= maxWidth: + with io.BytesIO() as image_buffer: + imgcopy.save(image_buffer, format=imagefile.content_type.split('/')[1]) + image_buffer.seek(0) + return_image = ContentFile(image_buffer.read(), name=imagefile.name) + return base64.b64encode(return_image.read()) + + # 等比例缩放 + wpercent = (maxWidth / float(img.size[0])) + hsize = int((float(img.size[1]) * float(wpercent))) + + # LANCZOS 高质量重采样 + imgcopy = imgcopy.resize((maxWidth, hsize), Image.Resampling.LANCZOS) + + with io.BytesIO() as resized_image_buffer: + imgcopy.save(resized_image_buffer, format=imagefile.content_type.split('/')[1]) + resized_image_buffer.seek(0) + + resized_imagefile = ContentFile(resized_image_buffer.read(), name=imagefile.name) + + resized64 = base64.b64encode(resized_imagefile.read()) + return resized64 + +# 以下接口供外部来源(如自建 RustDesk API 服务器)调用 +@csrf_exempt +def startgh(request): + denied = _webhook_denied(request) + if denied: + return denied + data_ = json.loads(request.body) + #### 触发构建平台工作流(GitHub / Gitea 按后台配置切换) + backend = build_backends.get_backend() + workflow_file = 'generator-' + str(data_.get('platform')) + '.yml' + inputs = { + "server":data_.get('server'), + "key":data_.get('key'), + "apiServer":data_.get('apiServer'), + "custom":data_.get('custom'), + "uuid":data_.get('uuid'), + "iconlink":data_.get('iconlink'), + "logolink":data_.get('logolink'), + "appname":data_.get('appname'), + "extras":data_.get('extras'), + "filename":data_.get('filename') + } + try: + result = backend.dispatch(workflow_file, inputs, timeout=20) + print(result) + except build_backends.base.BackendError as e: + # 历史行为:本端点无论成败都返回 204,仅记录日志 + print(f"startgh 触发失败:{e.message}") + return HttpResponse(status=204) + +def save_png(file, uuid, domain, name): + file_save_path = "png/%s/%s" % (uuid, name) + Path("png/%s" % uuid).mkdir(parents=True, exist_ok=True) + + if isinstance(file, str): # base64 字符串 + try: + header, encoded = file.split(';base64,') + decoded_img = base64.b64decode(encoded) + file = ContentFile(decoded_img, name=name) # 类文件对象 + except ValueError: + print("base64 数据无效") + return None + except Exception as e: + print(f"base64 解码出错:{e}") + return None + + with open(file_save_path, "wb+") as f: + for chunk in file.chunks(): + f.write(chunk) + return domain, uuid, name + +@csrf_exempt +def save_custom_client(request): + """GitHub Actions 回调:上传构建产物。""" + denied = _webhook_denied(request) + if denied: + return denied + file = request.FILES.get('file') + myuuid = request.POST.get('uuid') + if not file or not myuuid: + return HttpResponse("缺少文件或任务 UUID", status=400) + file_save_path = "exe/%s/%s" % (myuuid, os.path.basename(file.name)) + Path("exe/%s" % myuuid).mkdir(parents=True, exist_ok=True) + with open(file_save_path, "wb+") as f: + for chunk in file.chunks(): + f.write(chunk) + + return HttpResponse("文件保存成功") + +@csrf_exempt +def cleanup_secrets(request): + """GitHub Actions 回调:清理加密的临时配置包。""" + denied = _webhook_denied(request) + if denied: + return denied + try: + data = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return HttpResponse("请求体不是有效的 JSON", status=400) + my_uuid = data.get('uuid') + + if not my_uuid: + return HttpResponse("缺少任务 UUID", status=400) + + temp_dir = os.path.join('temp_zips') + if not os.path.isdir(temp_dir): + return HttpResponse("清理完成", status=200) + + for filename in os.listdir(temp_dir): + if my_uuid in filename and filename.endswith('.zip'): + file_path = os.path.join(temp_dir, filename) + try: + os.remove(file_path) + print(f"已删除 {file_path}") + except OSError as e: + print(f"删除文件失败:{e}") + + return HttpResponse("清理完成", status=200) + +def get_zip(request): + """供 GitHub Actions 工作流下载 AES 加密的构建配置包。""" + filename = request.GET.get('filename', '') + base_dir = os.path.abspath('temp_zips') + file_path = os.path.abspath(os.path.join(base_dir, os.path.basename(filename))) + if not file_path.startswith(base_dir + os.sep) or not os.path.isfile(file_path): + return HttpResponseForbidden("无效的文件名") + from django.http import FileResponse + return FileResponse( + open(file_path, 'rb'), + content_type='application/zip', + as_attachment=True, + filename=os.path.basename(filename), + ) diff --git a/rdgenerator/views_err.py b/rdgenerator/views_err.py new file mode 100644 index 0000000..71f74f2 --- /dev/null +++ b/rdgenerator/views_err.py @@ -0,0 +1,14 @@ +"""统一的中文错误页面。""" +from django.shortcuts import render + + +def handler403(request, exception=None): + return render(request, '403.html', status=403) + + +def handler404(request, exception=None): + return render(request, '404.html', status=404) + + +def handler500(request): + return render(request, '500.html', status=500) diff --git a/rdgenerator/web_views.py b/rdgenerator/web_views.py new file mode 100644 index 0000000..e1e0828 --- /dev/null +++ b/rdgenerator/web_views.py @@ -0,0 +1,511 @@ +"""登录后的业务页面:我的构建、接口令牌、管理仪表盘、用户管理。""" +import json +import os +import re +import shutil +from urllib.parse import quote + +from django.contrib import messages +from django.contrib.auth import update_session_auth_hash +from django.contrib.auth.decorators import login_required, user_passes_test +from django.contrib.auth.models import User +from django.db.models import Count +from django.http import ( + JsonResponse, HttpResponseRedirect, HttpResponse, Http404, FileResponse, +) +from django.shortcuts import get_object_or_404, redirect, render +from django.views.decorators.http import require_POST + +from . import app_settings, build_backends, maintenance +from .forms import ( + AdminUserCreateForm, AdminUserEditForm, ApiTokenForm, GenerateForm, GitHubSettingsForm, + MaintenanceSettingsForm, +) +from .models import ApiToken, GithubRun, SavedConfig + + +def staff_required(view_func): + """仅管理员(is_staff)可访问。""" + return user_passes_test(lambda u: u.is_authenticated and u.is_staff)(view_func) + + +@login_required +def my_builds(request): + builds = GithubRun.objects.filter(created_by=request.user) + return render(request, 'my_builds.html', {'builds': builds}) + + +def _remove_run_artifacts(run): + """删除任务对应的本地安装包与图片目录(忽略不存在的情况)。""" + if run.uuid: + shutil.rmtree(os.path.join('exe', run.uuid), ignore_errors=True) + shutil.rmtree(os.path.join('png', run.uuid), ignore_errors=True) + + +@login_required +@require_POST +def delete_my_build(request, run_id): + """用户删除自己的已结束任务;进行中的任务不允许删除(避免与 GitHub 回调竞态)。""" + run = get_object_or_404(GithubRun, pk=run_id, created_by=request.user) + if not run.is_finished(): + messages.error(request, "该任务仍在进行中,请等待结束(或先在 GitHub Actions 中取消)后再删除。") + else: + _remove_run_artifacts(run) + run.delete() + messages.success(request, f"构建任务「{run.filename or run.short_uuid()}」及本地产物已删除。") + return redirect('my_builds') + + +@login_required +@require_POST +def retry_build(request, run_id): + """用任务提交时保存的配置快照重新派发一个新构建(原任务保留作历史)。""" + qs = GithubRun.objects.all() if request.user.is_staff else GithubRun.objects.filter(created_by=request.user) + run = get_object_or_404(qs, pk=run_id) + + if not run.is_finished(): + messages.error(request, "该任务尚未结束,无需重试。") + return redirect('my_builds') + if not run.config_data: + messages.error(request, "该任务提交时未保存配置快照(较早的历史任务),无法一键重试,请重新填写配置后提交。") + return redirect('my_builds') + + # 局部导入避免与 views 模块在导入期形成耦合 + from .views import generate_custom_client + full_url = f"{app_settings.get_value('PROTOCOL')}://{request.get_host()}" + # 新任务归属原提交者(管理员代重试时也保持原归属);匿名历史任务归当前操作者 + owner = run.created_by or request.user + result = generate_custom_client(dict(run.config_data), full_url, user=owner) + if result.get('success'): + messages.success( + request, + f"已用原配置重新提交构建(新任务:{result.get('filename', '')}),可在下方查看进度。", + ) + else: + messages.error(request, f"重试失败:{result.get('error', '未知错误')}") + return redirect('my_builds') + + +@login_required +def api_tokens(request): + new_plain_key = None + if request.method == 'POST': + form = ApiTokenForm(request.POST) + if form.is_valid(): + token = ApiToken.objects.create(user=request.user, name=form.cleaned_data['name']) + new_plain_key = token.key + messages.success(request, "接口令牌创建成功,请立即复制保存(离开本页后将不再完整显示)。") + form = ApiTokenForm() + else: + form = ApiTokenForm() + + tokens = ApiToken.objects.filter(user=request.user) + return render(request, 'tokens.html', { + 'form': form, + 'tokens': tokens, + 'new_plain_key': new_plain_key, + }) + + +@login_required +@require_POST +def delete_token(request, key): + token = get_object_or_404(ApiToken, key=key, user=request.user) + token.delete() + messages.success(request, "接口令牌已删除。") + return redirect('api_tokens') + + +@staff_required +def dashboard(request): + builds = GithubRun.objects.select_related('created_by').all()[:100] + stats = { + 'total': GithubRun.objects.count(), + 'in_progress': GithubRun.objects.exclude( + status__in=['success', 'failure', 'cancelled', 'timed_out', 'skipped'] + ).count(), + 'success': GithubRun.objects.filter(status='success').count(), + 'failure': GithubRun.objects.filter(status__in=['failure', 'timed_out', 'cancelled']).count(), + 'users': User.objects.count(), + 'tokens': ApiToken.objects.filter(is_active=True).count(), + } + return render(request, 'dashboard.html', {'builds': builds, 'stats': stats}) + + +@staff_required +@require_POST +def delete_build(request, run_id): + run = get_object_or_404(GithubRun, pk=run_id) + _remove_run_artifacts(run) + run.delete() + messages.success(request, "构建任务及本地产物已删除。") + return redirect('dashboard') + + +# ===== 维护设置(定时清理 + 数据库备份) ===== + +@staff_required +def maintenance_settings(request): + if request.method == 'POST': + form = MaintenanceSettingsForm(request.POST) + if form.is_valid(): + app_settings.set_value( + 'BUILD_RETENTION_DAYS', str(form.cleaned_data['build_retention_days']) + ) + app_settings.set_value( + 'DB_BACKUP_ENABLED', '1' if form.cleaned_data['db_backup_enabled'] else '0' + ) + app_settings.set_value( + 'DB_BACKUP_KEEP', str(form.cleaned_data['db_backup_keep']) + ) + messages.success(request, "维护设置已保存,定时任务下一轮检查时自动生效。") + return redirect('maintenance_settings') + else: + form = MaintenanceSettingsForm(initial={ + 'build_retention_days': maintenance.retention_days(), + 'db_backup_enabled': maintenance.backup_enabled(), + 'db_backup_keep': maintenance.backup_keep(), + }) + + return render(request, 'maintenance_settings.html', { + 'form': form, + 'status': maintenance.maintenance_status(), + }) + + +@staff_required +@require_POST +def maintenance_run_now(request): + """手动立即执行一轮维护(清理过期任务 + 立即备份数据库)。""" + result = maintenance.run_maintenance(force_backup=True) + if result['backup_created']: + backup_name = os.path.basename(result['backup_created']) + messages.success( + request, + f"维护已执行:清理过期任务 {result['deleted_builds']} 条、" + f"临时文件 {result['deleted_temp_files']} 个,并已创建新备份 {backup_name}。", + ) + else: + messages.success( + request, + f"维护已执行:清理过期任务 {result['deleted_builds']} 条、" + f"临时文件 {result['deleted_temp_files']} 个;本轮未创建数据库备份。", + ) + return redirect('maintenance_settings') + + +@staff_required +def download_backup(request, filename): + """下载指定的数据库备份文件(文件名严格白名单,防路径穿越)。""" + if not re.fullmatch(r'db-\d{8}-\d{6}\.sqlite3', filename): + raise Http404("非法的备份文件名") + file_path = maintenance.backup_dir() / filename + if not file_path.is_file(): + raise Http404("备份文件不存在") + return FileResponse( + open(file_path, 'rb'), + content_type='application/octet-stream', + as_attachment=True, + filename=filename, + ) + + +@staff_required +def user_list(request): + if request.method == 'POST': + form = AdminUserCreateForm(request.POST) + if form.is_valid(): + User.objects.create_user( + username=form.cleaned_data['username'], + email=form.cleaned_data.get('email', ''), + password=form.cleaned_data['password'], + is_staff=form.cleaned_data.get('is_staff', False), + ) + messages.success(request, f"用户「{form.cleaned_data['username']}」创建成功。") + return redirect('user_list') + else: + form = AdminUserCreateForm() + + users = User.objects.annotate(build_count=Count('builds')).order_by('-date_joined') + return render(request, 'user_list.html', {'users': users, 'form': form}) + + +@staff_required +def user_edit(request, user_id): + target = get_object_or_404(User, pk=user_id) + if request.method == 'POST': + form = AdminUserEditForm(request.POST) + if form.is_valid(): + target.email = form.cleaned_data.get('email', '') + target.is_active = form.cleaned_data.get('is_active', False) + target.is_staff = form.cleaned_data.get('is_staff', False) + new_password = form.cleaned_data.get('new_password') + if new_password: + target.set_password(new_password) + # 管理员修改自己的密码后保持登录状态 + if request.user.pk == target.pk: + update_session_auth_hash(request, target) + target.save() + messages.success(request, f"用户「{target.username}」已更新。") + return redirect('user_list') + else: + form = AdminUserEditForm(initial={ + 'email': target.email, + 'is_active': target.is_active, + 'is_staff': target.is_staff, + }) + return render(request, 'user_form.html', {'form': form, 'target': target}) + + +# 非密钥配置项:设置键 -> 表单字段名 +_PLAIN_FIELDS = [ + ('BUILD_PLATFORM', 'build_platform'), + ('GITEA_SERVER_URL', 'gitea_server_url'), + ('GITEA_OWNER', 'gitea_owner'), + ('GITEA_REPO', 'gitea_repo'), + ('GITEA_BRANCH', 'gitea_branch'), + ('GHUSER', 'ghuser'), + ('REPONAME', 'reponame'), + ('GHBRANCH', 'ghbranch'), + ('GENURL', 'genurl'), + ('PROTOCOL', 'protocol'), +] +# 密钥配置项:设置键 -> (输入字段名, 清除复选框字段名) +_SECRET_FIELDS = [ + ('GITEA_TOKEN', ('gitea_token', 'clear_gitea_token')), + ('GITEA_PROXY', ('gitea_proxy', 'clear_gitea_proxy')), + ('GHBEARER', ('ghbearer', 'clear_ghbearer')), + ('ZIP_PASSWORD', ('zip_password', 'clear_zip_password')), + ('SH_SECRET', ('sh_secret', 'clear_sh_secret')), + ('WEBHOOK_SECRET', ('webhook_secret', 'clear_webhook_secret')), + ('GH_PROXY', ('gh_proxy', 'clear_gh_proxy')), +] + + +def _settings_rows(form): + """把配置定义与绑定后的表单字段组合成渲染行。""" + rows = [] + for item in app_settings.all_settings(): + field_name = item['key'].lower() + # 仅渲染本表单包含的设置组(maintenance 组在独立维护页处理) + if field_name not in form.fields: + continue + row = dict(item) + row['field'] = form[field_name] + row['source_label'] = app_settings.SOURCE_LABELS[row['source']] + if item['sensitive']: + row['clear_field'] = form[f'clear_{field_name}'] + rows.append(row) + return rows + + +@staff_required +def github_settings(request): + """后台 GitHub 构建配置:DB 覆盖优先,环境变量继续作为回退。""" + if request.method == 'POST': + form = GitHubSettingsForm(request.POST) + if form.is_valid(): + cleaned = form.cleaned_data + + # 非密钥项:提交值与环境变量(或默认值)一致时不写覆盖,保持来源可追溯 + for key, field_name in _PLAIN_FIELDS: + value = cleaned.get(field_name, '').strip() + env_value = os.environ.get(app_settings.DEFS_BY_KEY[key]['env']) + if env_value is not None: + fallback = env_value.strip() + else: + fallback = app_settings.DEFS_BY_KEY[key]['default'] + if value == fallback: + app_settings.delete_value(key) + else: + app_settings.set_value(key, value) + + # 密钥项:勾选清除则回退;填写新值则覆盖;留空保持不变 + for key, (field_name, clear_name) in _SECRET_FIELDS: + if cleaned.get(clear_name): + app_settings.delete_value(key) + elif cleaned.get(field_name): + app_settings.set_value(key, cleaned[field_name]) + + messages.success(request, "构建平台配置已保存。") + return redirect('github_settings') + else: + # 仅回填非密钥项;密钥绝不回显明文 + initial = { + item['key'].lower(): item['value'] + for item in app_settings.all_settings() + if not item['sensitive'] + } + form = GitHubSettingsForm(initial=initial) + + rows = _settings_rows(form) + groups = [ + ('platform', '构建平台', '选择实际执行构建任务的平台;切换后仅影响新提交的任务,历史任务仍在原平台查询。'), + ('gitea', 'Gitea 连接', '自建 Gitea(建议 1.27 或更高版本)的服务器、令牌与仓库信息;构建机(Linux/Windows/macOS runner)需自行准备。'), + ('basic', 'GitHub 基础连接', 'GitHub 仓库与访问令牌,用于触发 Actions 构建。'), + ('callback', '回调与加密', '构建产物回传地址与配置包加密,两个平台共用,需与仓库 Secret 保持一致。'), + ('advanced', '高级 / 安全', '回调签名校验两平台共用;自托管 Runner 密钥仅 GitHub 自托管构建需要,均按需配置。'), + ('network', 'GitHub 网络代理', '服务器访问 GitHub(触发构建、查询状态、拉取版本列表)的网络出口;能直连则无需配置。'), + ] + return render(request, 'site_settings.html', { + 'rows': rows, + 'groups': groups, + 'source_labels': app_settings.SOURCE_LABELS, + 'build_platform': build_backends.current_platform(), + 'github_ready': app_settings.is_github_configured(), + 'warnings': app_settings.config_warnings(), + 'gh_user': app_settings.get_value('GHUSER'), + 'gh_repo': app_settings.get_value('REPONAME'), + 'gitea_server': app_settings.get_value('GITEA_SERVER_URL').strip().rstrip('/'), + 'gitea_owner': app_settings.get_value('GITEA_OWNER'), + 'gitea_repo': app_settings.get_value('GITEA_REPO'), + }) + + +@staff_required +@require_POST +def test_build(request): + """测试当前所选(或表单中刚选的)构建平台的连接。""" + platform = (request.POST.get('platform') or '').strip().lower() + if platform not in build_backends.BACKENDS: + platform = build_backends.current_platform() + result = build_backends.get_backend(platform).test_connection() + return JsonResponse(result) + + +@staff_required +@require_POST +def test_github(request): + """兼容旧路由:直接测试 GitHub 连接。""" + return JsonResponse(build_backends.get_backend('github').test_connection()) + + +# ===== 构建配置方案(与账号绑定,可载入/导出/导入) ===== + +# 允许进入方案 JSON 的字段(即 GenerateForm 全部字段;图片以 data URL 字符串存储) +_PROFILE_KEYS = set(GenerateForm.base_fields.keys()) +_PROFILE_MAX_BYTES = 4 * 1024 * 1024 # 含三张 base64 图片,上限 4MB + + +def _sanitize_profile_data(raw): + """校验并清洗方案 JSON:仅保留表单已知字段的原始值。""" + if not isinstance(raw, dict): + raise ValueError("配置内容必须是 JSON 对象。") + cleaned = {} + for key, value in raw.items(): + if key not in _PROFILE_KEYS: + continue + if isinstance(value, bool) or value is None: + if value is not None: + cleaned[key] = value + elif isinstance(value, (str, int, float)): + cleaned[key] = value + if len(json.dumps(cleaned, ensure_ascii=False).encode('utf-8')) > _PROFILE_MAX_BYTES: + raise ValueError("配置文件过大(含图片上限 4MB)。") + return cleaned + + +def _parse_profile_upload(upload): + try: + raw_text = upload.read().decode('utf-8') + except (UnicodeDecodeError, AttributeError): + raise ValueError("配置文件编码无效,请上传本平台导出的 UTF-8 JSON 文件。") + if len(raw_text.encode('utf-8')) > _PROFILE_MAX_BYTES: + raise ValueError("配置文件过大(上限 4MB)。") + try: + raw = json.loads(raw_text) + except (json.JSONDecodeError, ValueError): + raise ValueError("配置文件不是有效的 JSON。") + return _sanitize_profile_data(raw) + + +@login_required +def saved_configs(request): + """配置方案管理页:列表、删除、导出、导入。""" + if request.method == 'POST': + upload = request.FILES.get('config_file') + name = request.POST.get('name', '').strip() + if not upload: + messages.error(request, "请选择要导入的 .json 配置文件。") + return redirect('saved_configs') + if not name: + name = os.path.splitext(upload.name)[0].strip() + name = name[:100] + if not name: + messages.error(request, "请填写方案名称。") + return redirect('saved_configs') + if SavedConfig.objects.filter(owner=request.user, name=name).exists(): + messages.error(request, f"已存在同名方案「{name}」,请改名后再导入,或先删除原方案。") + return redirect('saved_configs') + try: + data = _parse_profile_upload(upload) + except ValueError as e: + messages.error(request, str(e)) + return redirect('saved_configs') + SavedConfig.objects.create(owner=request.user, name=name, data=data) + messages.success(request, f"配置方案「{name}」导入成功,可在客户端定制页直接载入。") + return redirect('saved_configs') + + configs = request.user.saved_configs.all() + return render(request, 'saved_configs.html', {'configs': configs}) + + +@login_required +@require_POST +def save_config(request): + """定制页「另存为方案」:AJAX 提交 {name, data, overwrite?}。""" + try: + payload = json.loads(request.body) + except (json.JSONDecodeError, ValueError): + return JsonResponse({'ok': False, 'message': '请求内容不是有效的 JSON。'}, status=400) + + name = (payload.get('name') or '').strip()[:100] + if not name: + return JsonResponse({'ok': False, 'message': '请填写方案名称。'}, status=400) + try: + data = _sanitize_profile_data(payload.get('data')) + except ValueError as e: + return JsonResponse({'ok': False, 'message': str(e)}, status=400) + + existing = SavedConfig.objects.filter(owner=request.user, name=name).first() + if existing and not payload.get('overwrite'): + return JsonResponse({'ok': False, 'message': f'已存在同名方案「{name}」,是否覆盖?', 'duplicate': True}, status=409) + if existing: + existing.data = data + existing.save(update_fields=['data', 'updated_at']) + return JsonResponse({'ok': True, 'id': existing.id, 'overwritten': True}) + + cfg = SavedConfig.objects.create(owner=request.user, name=name, data=data) + return JsonResponse({'ok': True, 'id': cfg.id, 'overwritten': False}) + + +@login_required +def config_data(request, pk): + """供定制页下拉载入:返回方案完整 JSON。""" + cfg = get_object_or_404(SavedConfig, pk=pk, owner=request.user) + return JsonResponse({'name': cfg.name, 'data': cfg.data}) + + +@login_required +def export_config(request, pk): + """把方案导出为 .json 文件(格式与定制页本地导出一致,可互导/供 cli 使用)。""" + cfg = get_object_or_404(SavedConfig, pk=pk, owner=request.user) + body = json.dumps(cfg.data, ensure_ascii=False, indent=2) + response = HttpResponse(body, content_type='application/json; charset=utf-8') + response['Content-Disposition'] = f"attachment; filename*=UTF-8''{quote(cfg.name)}.json" + return response + + +@login_required +@require_POST +def delete_config(request, pk): + cfg = get_object_or_404(SavedConfig, pk=pk, owner=request.user) + name = cfg.name + cfg.delete() + messages.success(request, f"配置方案「{name}」已删除。") + return redirect('saved_configs') + + +def healthz(request): + """容器健康检查端点,无需登录。""" + return JsonResponse({"status": "ok"}) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d2a05c5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,7 @@ +Django>=4.2,<6.0 +requests>=2.31 +pysocks>=1.7.1 +pillow>=10.0 +gunicorn>=21.2 +pyzipper>=0.3.6 +whitenoise>=6.6 diff --git a/setup.md b/setup.md new file mode 100644 index 0000000..52b3e64 --- /dev/null +++ b/setup.md @@ -0,0 +1,284 @@ +## Host the rdgen server with docker + +1. First you will need to fork this repo on github +2. Next, setup a A Github fine-grained access token with permissions for your rdgen + repository: + * login to your github account + * click on your profile picture at the top right, click Settings + * at the bottom of the left panel, click Developer Settings + * click Personal access tokens + * click Fine-grained tokens + * click Generate new token + * give a token name, change expiration to whatever you want + * under Repository access, select Only select repositories, then pick your + rdgen repo + * give Read and Write access to actions and workflows + * You might have to go to: https://github.com/USERNAME/rdgen/actions and hit green Enable Actions button so it works. +3. Next, login to your Github account, go to your rdgen repo page (https://github.com/USERNAME/rdgen) + * Click on Settings + * In the left pane, click on Secrets and variables, then click Actions + * Now click New repository secret + * Set the Name to GENURL + * Set the Secret to https://rdgen.hostname.com (or whatever your server will be accessed from) + * Now click New repository secret again + * Set the Name to ZIP_PASSWORD + * Set the Secret to any password you want (use this in the next step as well) - generate a password by running: ```python3 -c 'import secrets; print(secrets.token_hex(100))'``` +4. Now download the docker-compose.yml file and fill in the environment variables: + * SECRET_KEY="your secret key" - generate a secret key by running: ```python3 -c 'import secrets; print(secrets.token_hex(100))'``` + * GHUSER="your github username" + * GHBEARER="your fine-grained access token" + * ZIP_PASSWORD="the same password that you entered as a github secret" + * PROTOCOL="https" *optional - defaults to "https", change to "http" if you need to + * REPONAME="rdgen" *optional - defaults to "rdgen", change this if you renamed the repo when you forked it +5. Now just run ```docker compose up -d``` + + +## Use a self hosted github runner for faster client generation (Windows only right now) + +1. First you need to set up a Windows computer that can build rustdesk +2. Once you can build rustdesk, follow github instructions for setting up a self hosted github runner +3. Now you need to add an environment variable SH_SECRET, which has a key/password that you will need to send to the server +4. Save a json configuration file from your rdgen web ui +5. Use the [rdgen-cli] (https://github.com/AlekseyLapunov/rdgen-cli) to submit your json configuration with the added key "sh_secret_field" with the value matching your SH_SECRET + +## Use your own Windows code signing token + +1. You will need a USB signing token plugged into a Windows computer +2. On the computer with the USB signing token, you need to make sure it is set up correctly to sign using signtool.exe +3. Run a small [signing api](https://github.com/bryangerlach/signing_api) server on the computer with the USB token connected. Follow the setup instructions for this server. +4. Now for your rdgen repo, add github secrets for + - SIGN_BASE_URL (the accesible over the internet URL for the signing api server) + - SIGN_API_KEY (the api key you have set on your signing api server) + + +## Choose a build platform: GitHub Actions or Gitea Actions + +The rdgen server itself only dispatches workflow runs and polls their status; +the actual builds run on a CI platform. Two platforms are supported and can be +switched at any time in the admin UI (or via `BUILD_PLATFORM=github|gitea`): + +| | GitHub Actions (default) | Gitea Actions (self-hosted) | +| --- | --- | --- | +| Build machines | Provided by GitHub (Windows / macOS / Linux) | You register your own `act_runner` machines | +| Network from runner back to rdgen | rdgen must be reachable from the public internet | Same LAN/VPN is enough (fully intranet capable) | +| Setup effort | Fork + token + 2 secrets, ~5 minutes | Deploy Gitea + register runners for every OS you build | +| Windows / macOS | Included | Physical/virtual Windows machines and Apple Macs required | +| Workflow files | `.github/workflows/` | `.gitea/workflows/` (adapted copy shipped in this repo) | +| Requirements | Fine-grained PAT | Gitea **1.27+ recommended** (1.23 minimum for workflow dispatch), act_runner 2.0+ | + +Switching platforms only affects newly submitted builds; historical runs stay on +the platform where they ran and their status/log links keep working. Gitee is not +supported (its pipeline uses a proprietary DSL and has no parameterized remote +trigger API). + +## Configure build platform settings from the admin UI (alternative to env vars) + +Build settings (platform selection, GHUSER, GHBEARER, GITEA_SERVER_URL, +GITEA_TOKEN, GENURL, ZIP_PASSWORD, REPONAME, GHBRANCH, PROTOCOL, SH_SECRET, +WEBHOOK_SECRET, proxies) can also be configured in the web UI after logging in +as an admin user: + +**Dashboard → 构建平台配置 (`/dashboard/settings/github/`)** + +* Pick the platform with the GitHub/Gitea radio cards; the form shows only the + fields relevant to the selected platform. +* Values saved in the admin UI take precedence over environment variables. +* Environment variables keep working as a fallback (value source is shown next + to each field: 后台配置 / 环境变量 / 默认值). +* Secrets are never displayed back; leave a secret field empty to keep the saved + value, or tick the "clear" checkbox to fall back to the env var/default. +* The page includes a step-by-step setup guide and a "Test GitHub/Gitea + connection" button (the test target follows the selected platform). You still + must add the `GENURL` and `ZIP_PASSWORD` repository secrets on the platform + itself. +* Version lists are always fetched from GitHub tags and fall back to built-in + versions when unreachable, independent of the selected build platform. + +## Gitea 部署附录(自建 Gitea Actions) + +适用场景:内网环境、构建产物回传不经过公网、希望 Windows/macOS 构建机在本地。 +Gitea Actions 的工作流语法与 API 与 GitHub Actions 高度兼容,本仓库已附带适配 +副本 `.gitea/workflows/`(6 个构建工作流 + 3 个可复用工作流 + 1 个 composite +action)。 + +### 1. Gitea 服务器与仓库 + +1. 部署 Gitea **1.27 或更高版本**(最低 1.23;1.27 随附 act_runner 2.0,对 + artifact/cache 与第三方 action 的兼容性最好)。 +2. 把本仓库整体推送到 Gitea(保留 `.gitea/` 目录与默认分支名,后台填写的 + GITEA_BRANCH 必须与实际分支一致)。 +3. 进入仓库 **Settings**,勾选 **Enable Repository Actions**。 +4. 在仓库 **Settings → Actions → Secrets** 添加与 GitHub 同名的 Secret: + * `GENURL`:rdgen 平台地址(runner 能访问到即可,例如 `http://10.0.0.5:8000`) + * `ZIP_PASSWORD`:必须与 rdgen 服务端「配置压缩包密码」完全一致 + * 可选:`ANDROID_SIGNING_KEY`、`MACOS_P12_BASE64`、`SIGN_BASE_URL`、`SIGN_API_KEY` +5. 生成一个 Gitea API 令牌(Settings → Applications → Generate New Token, + 需要对该仓库的 **Actions 读写**权限),填入 rdgen 后台「Gitea 访问令牌」。 + +### 2. 注册 act_runner 构建机 + +Gitea 不提供云构建机,每个需要构建的平台都要自行注册 runner。注册令牌在 +仓库 **Settings → Actions → Runners** 创建。runner 的自定义标签必须与工作流 +里的 `runs-on` 完全一致: + +| 标签 | 用途 | 运行方式 | +| --- | --- | --- | +| `ubuntu-22.04` | Linux x86_64、Android 构建 | Linux 主机 Docker 模式 | +| `ubuntu-22.04-arm` | Linux arm64、Android arm 构建 | arm64 Linux 主机 Docker 模式 | +| `windows-2022` | Windows x64/x86 构建 | Windows 主机 host 模式(真机/虚拟机) | +| `macos-14` | macOS arm64 构建 | Apple Silicon Mac host 模式 | +| `macos-15-intel` | macOS x64 构建 | Intel Mac host 模式 | + +**Linux(Docker 模式,推荐)**: + +```bash +docker run -d --name gitea-runner --restart always \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /opt/act-runner:/data \ + -e GITEA_INSTANCE_URL=https://gitea.example.com \ + -e GITEA_REGISTRATION_TOKEN=xxxx \ + gitea/act_runner:latest +``` + +首次启动生成 `/data/config.yaml` 后,按需把 `labels` 改为上面的标签,例如 +`- "ubuntu-22.04:docker://ghcr.io/catthehacker/ubuntu:act-22.04"`,再重启容器。 +工作流把 vcpkg 二进制缓存放在容器内 `/opt/vcpkg-cache`,如需跨任务复用,可在 +job 容器配置中把该路径挂为持久卷。 + +**Windows(host 模式)**:在准备好构建环境的 Windows 机器上 +(Git、PowerShell、Visual Studio 2022、vcpkg 位于 `C:\vcpkg`,并预创建 +`D:\vcpkg-cache`;ImageMagick 等由工作流自动安装): + +```powershell +# 下载 https://gitea.com/gitea/act_runner/releases 的 act_runner.exe +.\act_runner.exe register --instance https://gitea.example.com --token xxxx --labels "windows-2022:host" +.\act_runner.exe daemon +``` + +确认稳定后再用任务计划程序/NSSM 注册为开机服务。 + +**macOS(host 模式)**:使用 Apple 硬件(arm64 对应 `macos-14`,Intel 对应 +`macos-15-intel`,安装 Xcode 命令行工具),同样下载 act_runner 二进制后以 +`macos-14:host` 标签注册并 launchd 守护。 + +### 3. 内网网络说明 + +* 工作流中的第三方 action(`actions/checkout`、`subosito/flutter-action` 等) + 默认从 github.com 拉取。纯内网可在 Gitea 的 `app.ini` 配置 action 镜像: + + ```ini + [actions] + DEFAULT_ACTIONS_URL = https://gitea.com + ``` + + (`gitea.com` 官方镜像了主流 actions;也可搭建自有镜像。) +* 工作流仍会从 GitHub 拉取 RustDesk 源码、Flutter 引擎、cargo/pub 依赖等, + runner 主机需要可访问 github.com(直连、出网白名单或代理)。 +* `.gitea/workflows` 已把构建所需的 patch 改为从仓库内本地目录取用 + (`.rdgen-src/.github/patches/`),不依赖 raw.githubusercontent.com。 +* rdgen 后台的 `GITEA_PROXY` 仅用于「服务端 → Gitea API」的调用;Gitea 通常 + 与 rdgen 在同一内网,留空直连即可。 + +### 4. 在 rdgen 侧启用 + +在 **后台管理 → 构建平台配置** 选择 Gitea,填写服务器地址、令牌、属主、仓库、 +分支后保存,点击「测试 Gitea 连接」:连接正常即可在客户端定制页提交构建。 +也可以用环境变量配置:`BUILD_PLATFORM=gitea`、`GITEA_SERVER_URL`、 +`GITEA_TOKEN`、`GITEA_OWNER`、`GITEA_REPO`、`GITEA_BRANCH`、`GITEA_PROXY`。 + +## Host manually: + +1. A Github account with a fork of this repo +2. A Github fine-grained access token with permissions for your rdgen + repository: + * login to your github account + * click on your profile picture at the top right, click Settings + * at the bottom of the left panel, click Developer Settings + * click Personal access tokens + * click Fine-grained tokens + * click Generate new token + * give a token name, change expiration to whatever you want + * under Repository access, select Only select repositories, then pick your + rdgen repo + * give Read and Write access to actions and workflows + * You might have to go to: https://github.com/USERNAME/rdgen/actions and hit green Enable Actions button so it works. +3. Setup environment variables/secrets: + * environment variables on the server running rdgen: + * GHUSER="your github username" + * GHBEARER="your fine-grained access token" + * PROTOCOL="https" *optional - defaults to "https", change to "http" if you need to + * REPONAME="rdgen" *optional - defaults to "rdgen", change this if you renamed the repo when you forked it + * github secrets (setup on your github account for your rdgen repo): + * GENURL="example.com:8000" *this is the domain and port that you are + running rdgen on, needs to be accessible on the internet, depending + on how you have this setup the port may not be needed + +``` +# Open to the directory you want to install rdgen (change /opt to wherever you want) +cd /opt + +# Clone your rdgen repo, change bryangerlach to your github username +git clone https://github.com/bryangerlach/rdgen.git + +# Open the rdgen directory +cd rdgen + +# Setup a python virtual environment called rdgen +python -m venv .venv + +# Activate the python virtual environment +source .venv/bin/activate + +# Install the python dependencies +pip install -r requirements.txt + +# Setup the database +python manage.py migrate + +# Run the server, change 8000 with whatever you want +python manage.py runserver 0.0.0.0:8000 +``` + +open your web browser to yourdomain:8000 + +use nginx, caddy, traefik, etc. for ssl reverse proxy + +### To autostart the server on boot, you can set up a systemd service called rdgen.service + +replace user, group, and port if you need to replace /opt with wherever you +have installed rdgen save the following file as +/etc/systemd/system/rdgen.service, and make sure to change GHUSER, GHBEARER + +``` +[Unit] +Description=Rustdesk Client Generator +[Service] +Type=simple +LimitNOFILE=1000000 +Environment="GHUSER=yourgithubusername" +Environment="GHBEARER=yourgithubtoken" +PassEnvironment=GHUSER GHBEARER +ExecStart=/opt/rdgen/.venv/bin/python3 /opt/rdgen/manage.py runserver 0.0.0.0:8000 +WorkingDirectory=/opt/rdgen/ +User=root +Group=root +Restart=always +StandardOutput=file:/var/log/rdgen.log +StandardError=file:/var/log/rdgen.error +# Restart service after 10 seconds if node service crashes +RestartSec=10 +[Install] +WantedBy=multi-user.target +``` + +then run this to enable autostarting the service on boot, and then start it +manually this time: + +``` +sudo systemctl enable rdgen.service +sudo systemctl start rdgen.service +``` +and to get the status of the server, run: +``` +sudo systemctl status rdgen.service +```