mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 02:10:59 +03:00
Customizing a Windows client recompiled the packer for every customer, because data.bin was baked in with include_bytes!. The generic payload is identical across customers, so only the small per-customer delta needs to vary: the branded runner exe, custom.txt and the icons. The packer now also reads an RDPKG RCDATA resource holding a second blob in the same format, and folds it over the compiled-in payload. A build can then inject that resource into a prebuilt template instead of running cargo. The executable to launch comes from the package trailer, and the extraction directory follows its stem, which replaces the sed of APP_PREFIX. Where the executable itself is not customized (sciter x86) it stays in the generic payload and is only renamed, so the merge covers both shapes. custom.txt keeps being written to disk next to the app: that is what the client reads at startup and what the updater stages so a customization survives an upgrade to a stock build. Also fixes generate.py restoring os.curdir (the literal ".") instead of the previous working directory, which left it inside the source folder. CI: ship windows-aarch64 in the unsigned tarball, so ARM custom clients have a template to build from. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
141 lines
5.5 KiB
Python
Executable File
141 lines
5.5 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import os
|
|
import optparse
|
|
import subprocess
|
|
from hashlib import md5
|
|
import brotli
|
|
import datetime
|
|
|
|
# 4GB maximum
|
|
length_count = 4
|
|
# encoding
|
|
encoding = 'utf-8'
|
|
|
|
# output: {path: (compressed_data, file_md5)}
|
|
|
|
|
|
def normalize(path: str) -> str:
|
|
path = path.replace('\\', '/')
|
|
while path.startswith('./'):
|
|
path = path[2:]
|
|
return path.lower()
|
|
|
|
|
|
def generate_md5_table(folder: str, level, exclude: str = None) -> dict:
|
|
res: dict = dict()
|
|
skip = normalize(exclude) if exclude else None
|
|
# os.curdir is the literal ".", so restoring it left us inside `folder`.
|
|
curdir = os.getcwd()
|
|
os.chdir(folder)
|
|
for root, _, files in os.walk('.'):
|
|
# remove ./
|
|
for f in files:
|
|
md5_generator = md5()
|
|
full_path = os.path.join(root, f)
|
|
if skip and normalize(full_path) == skip:
|
|
print(f"Excluding {full_path}...")
|
|
continue
|
|
print(f"Processing {full_path}...")
|
|
f = open(full_path, "rb")
|
|
content = f.read()
|
|
content_compressed = brotli.compress(
|
|
content, quality=level)
|
|
md5_generator.update(content)
|
|
md5_code = md5_generator.hexdigest().encode(encoding=encoding)
|
|
res[full_path] = (content_compressed, md5_code)
|
|
os.chdir(curdir)
|
|
return res
|
|
|
|
|
|
def write_package_metadata(md5_table: dict, output_folder: str, exe: str):
|
|
write_blob(md5_table, os.path.join(output_folder, "data.bin"), exe)
|
|
|
|
|
|
def write_blob(md5_table: dict, output_path: str, exe: str):
|
|
with open(output_path, "wb") as f:
|
|
f.write("rustdesk".encode(encoding=encoding))
|
|
for path in md5_table.keys():
|
|
(compressed_data, md5_code) = md5_table[path]
|
|
data_length = len(compressed_data)
|
|
path = path.encode(encoding=encoding)
|
|
# path length & path
|
|
f.write((len(path)).to_bytes(length=length_count, byteorder='big'))
|
|
f.write(path)
|
|
# data length & compressed data
|
|
f.write(data_length.to_bytes(
|
|
length=length_count, byteorder='big'))
|
|
f.write(compressed_data)
|
|
# md5 code
|
|
f.write(md5_code)
|
|
# end
|
|
f.write("rustdesk".encode(encoding=encoding))
|
|
# executable
|
|
f.write(exe.encode(encoding='utf-8'))
|
|
print(f"Metadata has been written to {output_path}")
|
|
|
|
def write_app_metadata(output_folder: str):
|
|
output_path = os.path.join(output_folder, "app_metadata.toml")
|
|
with open(output_path, "w") as f:
|
|
f.write(f"timestamp = {int(datetime.datetime.now().timestamp() * 1000)}\n")
|
|
print(f"App metadata has been written to {output_path}")
|
|
|
|
def build_portable(output_folder: str, target: str):
|
|
current_dir = os.getcwd()
|
|
try:
|
|
os.chdir(output_folder)
|
|
cmd = ["cargo", "build", "--locked", "--release"]
|
|
if target:
|
|
cmd.extend(["--target", target])
|
|
subprocess.run(cmd, check=True)
|
|
finally:
|
|
os.chdir(current_dir)
|
|
|
|
# Linux: python3 generate.py -f ../rustdesk-portable-packer/test -o . -e ./test/main.py
|
|
# Windows: python3 .\generate.py -f ..\rustdesk\flutter\build\windows\runner\Debug\ -o . -e ..\rustdesk\flutter\build\windows\runner\Debug\rustdesk.exe
|
|
|
|
|
|
if __name__ == '__main__':
|
|
parser = optparse.OptionParser()
|
|
parser.add_option("-f", "--folder", dest="folder",
|
|
help="folder to compress")
|
|
parser.add_option("-o", "--output", dest="output_folder",
|
|
help="the root of portable packer project, default is './'")
|
|
parser.add_option("-e", "--executable", dest="executable",
|
|
help="specify startup file in --folder, default is rustdesk.exe")
|
|
parser.add_option("-t", "--target", dest="target",
|
|
help="the target used by cargo")
|
|
parser.add_option("-l", "--level", dest="level", type="int",
|
|
help="compression level, default is 11, highest", default=11)
|
|
parser.add_option("--package", dest="package",
|
|
help="write the per-customer blob to this path instead of "
|
|
"data.bin, and skip the cargo build. Injected into the "
|
|
"template's RDPKG resource so customizing needs no rebuild")
|
|
parser.add_option("--exclude-exe", dest="exclude_exe", action="store_true",
|
|
default=False,
|
|
help="omit the executable from the blob, for a template whose "
|
|
"executable ships in the package instead")
|
|
(options, args) = parser.parse_args()
|
|
folder = options.folder or './rustdesk'
|
|
output_folder = os.path.abspath(options.output_folder or './')
|
|
|
|
if not options.executable:
|
|
options.executable = 'rustdesk.exe'
|
|
if not options.executable.startswith(folder):
|
|
options.executable = folder + '/' + options.executable
|
|
exe: str = os.path.abspath(options.executable)
|
|
if not exe.startswith(os.path.abspath(folder)):
|
|
print("The executable must locate in source folder")
|
|
exit(-1)
|
|
exe = '.' + exe[len(os.path.abspath(folder)):]
|
|
print("Executable path: " + exe)
|
|
print("Compression level: " + str(options.level))
|
|
md5_table = generate_md5_table(
|
|
folder, options.level, exe if options.exclude_exe else None)
|
|
if options.package:
|
|
write_blob(md5_table, os.path.abspath(options.package), exe)
|
|
else:
|
|
write_package_metadata(md5_table, output_folder, exe)
|
|
write_app_metadata(output_folder)
|
|
build_portable(output_folder, options.target)
|