mirror of
https://github.com/rustdesk/rustdesk.git
synced 2026-09-18 02:10:59 +03:00
Rebranding recompressed the whole ~100MB payload because one cabinet held everything. In template mode preprocess.py puts the handful of files a custom client replaces on a second cabinet, so a patch rebuilds a few hundred KB and leaves the payload cabinet alone. The shipped msi is built without template mode and keeps its single cabinet. The branding assets need conditional components. A stock build ships none of them -- there is no icon.ico, icon.png or logo*.png, only icon.svg -- so the template has to carry placeholders for the File rows to exist, and a customer supplies whichever they want. Installing a placeholder unconditionally would give a customer with no logo a placeholder image, where today a missing asset means no logo at all: the client tries each candidate and treats the failure as absence. So each optional asset installs only when its property says the customer supplied one. CI creates those placeholders and builds the template with the new mode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q7fBdTwziR5BHTkSz7Tzcm
644 lines
22 KiB
Python
644 lines
22 KiB
Python
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
|
|
import json
|
|
import sys
|
|
import uuid
|
|
import argparse
|
|
import datetime
|
|
import subprocess
|
|
import re
|
|
import platform
|
|
from pathlib import Path
|
|
import shutil
|
|
|
|
g_indent_unit = "\t"
|
|
g_version = ""
|
|
g_build_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
|
|
|
# Replace the following links with your own in the custom arp properties.
|
|
# https://learn.microsoft.com/en-us/windows/win32/msi/property-reference
|
|
g_arpsystemcomponent = {
|
|
"Comments": {
|
|
"msi": "ARPCOMMENTS",
|
|
"t": "string",
|
|
"v": "!(loc.AR_Comment)",
|
|
},
|
|
"Contact": {
|
|
"msi": "ARPCONTACT",
|
|
"v": "https://github.com/rustdesk/rustdesk",
|
|
},
|
|
"HelpLink": {
|
|
"msi": "ARPHELPLINK",
|
|
"v": "https://github.com/rustdesk/rustdesk/issues/",
|
|
},
|
|
"ReadMe": {
|
|
"msi": "ARPREADME",
|
|
"v": "https://github.com/rustdesk/rustdesk",
|
|
},
|
|
}
|
|
|
|
def default_revision_version():
|
|
return int(datetime.datetime.now().timestamp() / 60)
|
|
|
|
def make_parser():
|
|
parser = argparse.ArgumentParser(description="Msi preprocess script.")
|
|
parser.add_argument(
|
|
"-d",
|
|
"--dist-dir",
|
|
type=str,
|
|
default="../../rustdesk",
|
|
help="The dist directory to install.",
|
|
)
|
|
parser.add_argument(
|
|
"--arp",
|
|
action="store_true",
|
|
help="Is ARPSYSTEMCOMPONENT",
|
|
default=False,
|
|
)
|
|
parser.add_argument(
|
|
"--custom-arp",
|
|
type=str,
|
|
default="{}",
|
|
help='Custom arp properties, e.g. \'["Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}]\'',
|
|
)
|
|
parser.add_argument(
|
|
"-c", "--custom", action="store_true", help="Is custom client", default=False
|
|
)
|
|
parser.add_argument(
|
|
"--template",
|
|
action="store_true",
|
|
default=False,
|
|
help="Build a template to be patched per customer rather than a finished "
|
|
"package: puts the files a custom client replaces in their own cabinet, so "
|
|
"rebranding rebuilds a few hundred KB instead of the whole payload.",
|
|
)
|
|
parser.add_argument(
|
|
"--conn-type",
|
|
type=str,
|
|
default="",
|
|
help='Connection type, e.g. "incoming", "outgoing". Default is empty, means incoming-outgoing',
|
|
)
|
|
parser.add_argument(
|
|
"--app-name", type=str, default="RustDesk", help="The app name."
|
|
)
|
|
parser.add_argument(
|
|
"-v", "--version", type=str, default="", help="The app version."
|
|
)
|
|
parser.add_argument(
|
|
"--revision-version", type=int, default=default_revision_version(), help="The revision version."
|
|
)
|
|
parser.add_argument(
|
|
"-m",
|
|
"--manufacturer",
|
|
type=str,
|
|
default="Purslane Tech Pte. Ltd.",
|
|
help="The app manufacturer.",
|
|
)
|
|
return parser
|
|
|
|
|
|
# Files a custom client replaces. Kept in their own cabinet by --template so that
|
|
# rebranding rebuilds a few hundred KB instead of recompressing the whole payload.
|
|
# The app executable is handled separately: it has its own component in RustDesk.wxs.
|
|
#
|
|
# A template has to ship a placeholder for each of these so there is a File row to
|
|
# patch, but the branding assets are optional for a customer and a stock build has
|
|
# none of them at all. So each optional one installs only when its property is set,
|
|
# which the patcher does for the files a customer actually supplied. Otherwise a
|
|
# customer without a logo would install the placeholder, where today they get no
|
|
# logo at all -- the client treats a missing asset as "no logo".
|
|
PER_CUSTOMER_DISK_ID = 2
|
|
PER_CUSTOMER_FILES = {
|
|
# relative path -> property gating installation, or None if always installed
|
|
"custom.txt": None,
|
|
"data/flutter_assets/assets/icon.ico": "CC_HAS_ICON_ICO",
|
|
"data/flutter_assets/assets/icon.png": "CC_HAS_ICON_PNG",
|
|
"data/flutter_assets/assets/logo.png": "CC_HAS_LOGO",
|
|
"data/flutter_assets/assets/logo_light.png": "CC_HAS_LOGO_LIGHT",
|
|
"data/flutter_assets/assets/logo_dark.png": "CC_HAS_LOGO_DARK",
|
|
}
|
|
|
|
|
|
def normalize_relative(relative_path):
|
|
path = relative_path.replace("\\", "/")
|
|
while path.startswith("./"):
|
|
path = path[2:]
|
|
return path.lower()
|
|
|
|
|
|
def is_per_customer(relative_path):
|
|
return normalize_relative(relative_path) in PER_CUSTOMER_FILES
|
|
|
|
|
|
def per_customer_condition(relative_path):
|
|
return PER_CUSTOMER_FILES.get(normalize_relative(relative_path))
|
|
|
|
|
|
def read_lines_and_start_index(file_path, tag_start, tag_end):
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
index_start = -1
|
|
index_end = -1
|
|
for i, line in enumerate(lines):
|
|
if tag_start in line:
|
|
index_start = i
|
|
if tag_end in line:
|
|
index_end = i
|
|
|
|
if index_start == -1:
|
|
print(f'Error: start tag "{tag_start}" not found')
|
|
return None, None
|
|
if index_end == -1:
|
|
print(f'Error: end tag "{tag_end}" not found')
|
|
return None, None
|
|
return lines, index_start
|
|
|
|
|
|
def insert_components_between_tags(lines, index_start, app_name, dist_dir, template=False):
|
|
indent = g_indent_unit * 3
|
|
path = Path(dist_dir)
|
|
idx = 1
|
|
for file_path in path.glob("**/*"):
|
|
if file_path.is_file():
|
|
if file_path.name.lower() == f"{app_name}.exe".lower():
|
|
continue
|
|
|
|
subdir = str(file_path.parent.relative_to(path))
|
|
dir_attr = ""
|
|
if subdir != ".":
|
|
dir_attr = f'Subdirectory="{subdir}"'
|
|
|
|
relative = file_path.relative_to(path).as_posix()
|
|
disk_attr = ""
|
|
condition_attr = ""
|
|
if template and is_per_customer(relative):
|
|
disk_attr = f' DiskId="{PER_CUSTOMER_DISK_ID}"'
|
|
# Branding assets are optional, and the template only carries a
|
|
# placeholder, so install one only when the customer supplied it.
|
|
condition = per_customer_condition(relative)
|
|
if condition:
|
|
condition_attr = f' Condition="{condition} = 1"'
|
|
|
|
# Don't generate Component Id and File Id like 'Component_{idx}' and 'File_{idx}'
|
|
# because it will cause error
|
|
# "Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'"
|
|
to_insert_lines = f"""
|
|
{indent}<Component Guid="{uuid.uuid4()}" {dir_attr}{condition_attr}>
|
|
{indent}{g_indent_unit}<File Source="{file_path.as_posix()}" KeyPath="yes" Checksum="yes"{disk_attr} />
|
|
{indent}</Component>
|
|
"""
|
|
lines.insert(index_start + 1, to_insert_lines[1:])
|
|
index_start += 1
|
|
idx += 1
|
|
return True
|
|
|
|
|
|
def gen_auto_component(app_name, dist_dir, template=False):
|
|
return gen_content_between_tags(
|
|
"Package/Components/RustDesk.wxs",
|
|
"<!--$AutoComonentStart$-->",
|
|
"<!--$AutoComponentEnd$-->",
|
|
lambda lines, index_start: insert_components_between_tags(
|
|
lines, index_start, app_name, dist_dir, template
|
|
),
|
|
)
|
|
|
|
|
|
def gen_media2():
|
|
"""Second cabinet holding only what a custom client replaces."""
|
|
|
|
def func(lines, index_start):
|
|
indent = g_indent_unit * 2
|
|
lines.insert(
|
|
index_start + 1,
|
|
f'{indent}<Media Id="{PER_CUSTOMER_DISK_ID}" Cabinet="cab2.cab"'
|
|
' EmbedCab="yes" CompressionLevel="high" />\n',
|
|
)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Package.wxs", "<!--$Media2Start$-->", "<!--$Media2End$-->", func
|
|
)
|
|
|
|
|
|
def put_app_exe_on_media2():
|
|
"""The app executable has its own component, so it is moved by name."""
|
|
target = Path(sys.argv[0]).parent.joinpath("Package/Components/RustDesk.wxs")
|
|
with open(target, "r", encoding="utf-8") as f:
|
|
content = f.read()
|
|
old = '<File Id="App.exe" Name="$(var.Product).exe" KeyPath="yes" Checksum="yes">'
|
|
new = (
|
|
'<File Id="App.exe" Name="$(var.Product).exe" KeyPath="yes" Checksum="yes"'
|
|
f' DiskId="{PER_CUSTOMER_DISK_ID}">'
|
|
)
|
|
if content.count(old) != 1:
|
|
print(f"Error: expected exactly one App.exe File element, found {content.count(old)}")
|
|
return False
|
|
with open(target, "w", encoding="utf-8") as f:
|
|
f.write(content.replace(old, new))
|
|
return True
|
|
|
|
|
|
def gen_pre_vars(args, dist_dir):
|
|
def func(lines, index_start):
|
|
upgrade_code = uuid.uuid5(uuid.NAMESPACE_OID, app_name + ".exe")
|
|
|
|
indent = g_indent_unit * 1
|
|
to_insert_lines = [
|
|
f'{indent}<?define Version="{g_version}" ?>\n',
|
|
f'{indent}<?define Manufacturer="{args.manufacturer}" ?>\n',
|
|
f'{indent}<?define Product="{args.app_name}" ?>\n',
|
|
f'{indent}<?define Description="{args.app_name} Installer" ?>\n',
|
|
f'{indent}<?define ProductLower="{args.app_name.lower()}" ?>\n',
|
|
f'{indent}<?define RegKeyRoot=".$(var.ProductLower)" ?>\n',
|
|
f'{indent}<?define RegKeyInstall="$(var.RegKeyRoot)\\Install" ?>\n',
|
|
f'{indent}<?define BuildDir="{dist_dir}" ?>\n',
|
|
f'{indent}<?define BuildDate="{g_build_date}" ?>\n',
|
|
"\n",
|
|
f"{indent}<!-- The UpgradeCode must be consistent for each product. ! -->\n"
|
|
f'{indent}<?define UpgradeCode = "{upgrade_code}" ?>\n',
|
|
]
|
|
|
|
for i, line in enumerate(to_insert_lines):
|
|
lines.insert(index_start + i + 1, line)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Includes.wxi", "<!--$PreVarsStart$-->", "<!--$PreVarsEnd$-->", func
|
|
)
|
|
|
|
|
|
def replace_app_name_in_langs(app_name):
|
|
langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language")
|
|
for file_path in langs_dir.glob("*.wxl"):
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
for i, line in enumerate(lines):
|
|
lines[i] = line.replace("RustDesk", app_name)
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
f.writelines(lines)
|
|
|
|
def gen_upgrade_info():
|
|
def func(lines, index_start):
|
|
indent = g_indent_unit * 3
|
|
|
|
vs = g_version.split(".")
|
|
major = vs[0]
|
|
upgrade_id = uuid.uuid4()
|
|
to_insert_lines = [
|
|
f'{indent}<Upgrade Id="{upgrade_id}">\n',
|
|
f'{indent}{g_indent_unit}<UpgradeVersion Property="OLD_VERSION_FOUND" Minimum="{major}.0.0" Maximum="{major}.99.99" IncludeMinimum="yes" IncludeMaximum="yes" OnlyDetect="no" IgnoreRemoveFailure="yes" MigrateFeatures="yes" />\n',
|
|
f"{indent}</Upgrade>\n",
|
|
]
|
|
|
|
for i, line in enumerate(to_insert_lines):
|
|
lines.insert(index_start + i + 1, line)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Fragments/Upgrades.wxs",
|
|
"<!--$UpgradeStart$-->",
|
|
"<!--$UpgradeEnd$-->",
|
|
func,
|
|
)
|
|
|
|
|
|
def gen_custom_dialog_bitmaps():
|
|
def func(lines, index_start):
|
|
indent = g_indent_unit * 2
|
|
|
|
# https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set
|
|
vars = [
|
|
"WixUIBannerBmp",
|
|
"WixUIDialogBmp",
|
|
"WixUIExclamationIco",
|
|
"WixUIInfoIco",
|
|
"WixUINewIco",
|
|
"WixUIUpIco",
|
|
]
|
|
to_insert_lines = []
|
|
for var in vars:
|
|
if Path(f"Package/Resources/{var}.bmp").exists():
|
|
to_insert_lines.append(
|
|
f'{indent}<WixVariable Id="{var}" Value="Resources\\{var}.bmp" />\n'
|
|
)
|
|
|
|
for i, line in enumerate(to_insert_lines):
|
|
lines.insert(index_start + i + 1, line)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Package.wxs",
|
|
"<!--$CustomBitmapsStart$-->",
|
|
"<!--$CustomBitmapsEnd$-->",
|
|
func,
|
|
)
|
|
|
|
|
|
def gen_custom_ARPSYSTEMCOMPONENT_False(args):
|
|
def func(lines, index_start):
|
|
indent = g_indent_unit * 2
|
|
|
|
lines_new = []
|
|
lines_new.append(
|
|
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/arpsystemcomponent?redirectedfrom=MSDN-->\n"
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<!--<Property Id="ARPSYSTEMCOMPONENT" Value="1" />-->\n\n'
|
|
)
|
|
|
|
lines_new.append(
|
|
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
|
|
)
|
|
for _, v in g_arpsystemcomponent.items():
|
|
if "msi" in v and "v" in v:
|
|
lines_new.append(
|
|
f'{indent}<Property Id="{v["msi"]}" Value="{v["v"]}" />\n'
|
|
)
|
|
|
|
for i, line in enumerate(lines_new):
|
|
lines.insert(index_start + i + 1, line)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Fragments/AddRemoveProperties.wxs",
|
|
"<!--$ArpStart$-->",
|
|
"<!--$ArpEnd$-->",
|
|
func,
|
|
)
|
|
|
|
|
|
def get_folder_size(folder_path):
|
|
total_size = 0
|
|
|
|
folder = Path(folder_path)
|
|
for file in folder.glob("**/*"):
|
|
if file.is_file():
|
|
total_size += file.stat().st_size
|
|
|
|
return total_size
|
|
|
|
|
|
def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
|
|
def func(lines, index_start):
|
|
indent = g_indent_unit * 5
|
|
|
|
lines_new = []
|
|
lines_new.append(
|
|
f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="DisplayName" Value="{args.app_name}" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="DisplayIcon" Value="[INSTALLFOLDER_INNER]{args.app_name}.exe" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="DisplayVersion" Value="{g_version}" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="Publisher" Value="{args.manufacturer}" />\n'
|
|
)
|
|
installDate = datetime.datetime.now().strftime("%Y%m%d")
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="InstallDate" Value="{installDate}" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="InstallSource" Value="[InstallSource]" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
|
|
)
|
|
|
|
# EstimatedSize in uninstall registry must be in KB.
|
|
estimated_size_bytes = get_folder_size(dist_dir)
|
|
estimated_size = max(1, (estimated_size_bytes + 1023) // 1024)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
|
|
)
|
|
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="expandable" Name="ModifyPath" Value="MsiExec.exe /X [ProductCode]" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Id="NoModify" Value="1" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="expandable" Name="UninstallString" Value="MsiExec.exe /X [ProductCode]" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="expandable" Name="QuietUninstallString" Value="MsiExec.exe /qn /X [ProductCode]" />\n'
|
|
)
|
|
|
|
vs = g_version.split(".")
|
|
major, minor, build = vs[0], vs[1], vs[2]
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="string" Name="Version" Value="{g_version}" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Name="VersionMajor" Value="{major}" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Name="VersionMinor" Value="{minor}" />\n'
|
|
)
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Name="VersionBuild" Value="{build}" />\n'
|
|
)
|
|
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />\n'
|
|
)
|
|
for k, v in g_arpsystemcomponent.items():
|
|
if "v" in v:
|
|
t = v["t"] if "t" in v is None else "string"
|
|
lines_new.append(
|
|
f'{indent}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n'
|
|
)
|
|
|
|
for i, line in enumerate(lines_new):
|
|
lines.insert(index_start + i + 1, line)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Components/Regs.wxs",
|
|
"<!--$ArpStart$-->",
|
|
"<!--$ArpEnd$-->",
|
|
func,
|
|
)
|
|
|
|
|
|
def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
|
|
try:
|
|
custom_arp = json.loads(args.custom_arp)
|
|
g_arpsystemcomponent.update(custom_arp)
|
|
except json.JSONDecodeError as e:
|
|
print(f"Failed to decode custom arp: {e}")
|
|
return False
|
|
|
|
if args.arp:
|
|
return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir)
|
|
else:
|
|
return gen_custom_ARPSYSTEMCOMPONENT_False(args)
|
|
|
|
def gen_conn_type(args):
|
|
def func(lines, index_start):
|
|
indent = g_indent_unit * 3
|
|
|
|
lines_new = []
|
|
if args.conn_type != "":
|
|
lines_new.append(
|
|
f"""{indent}<Property Id="CC_CONNECTION_TYPE" Value="{args.conn_type}" />\n"""
|
|
)
|
|
|
|
for i, line in enumerate(lines_new):
|
|
lines.insert(index_start + i + 1, line)
|
|
return lines
|
|
|
|
return gen_content_between_tags(
|
|
"Package/Fragments/AddRemoveProperties.wxs",
|
|
"<!--$CustomClientPropsStart$-->",
|
|
"<!--$CustomClientPropsEnd$-->",
|
|
func,
|
|
)
|
|
|
|
def gen_content_between_tags(filename, tag_start, tag_end, func):
|
|
target_file = Path(sys.argv[0]).parent.joinpath(filename)
|
|
lines, index_start = read_lines_and_start_index(target_file, tag_start, tag_end)
|
|
if lines is None:
|
|
return False
|
|
|
|
func(lines, index_start)
|
|
|
|
with open(target_file, "w", encoding="utf-8") as f:
|
|
f.writelines(lines)
|
|
|
|
return True
|
|
|
|
|
|
def prepare_resources():
|
|
icon_src = Path(sys.argv[0]).parent.joinpath("../icon.ico")
|
|
icon_dst = Path(sys.argv[0]).parent.joinpath("Package/Resources/icon.ico")
|
|
if icon_src.exists():
|
|
icon_dst.parent.mkdir(parents=True, exist_ok=True)
|
|
shutil.copy(icon_src, icon_dst)
|
|
return True
|
|
else:
|
|
# unreachable
|
|
print(f"Error: icon.ico not found in {icon_src}")
|
|
return False
|
|
|
|
|
|
def init_global_vars(dist_dir, app_name, args):
|
|
dist_app = dist_dir.joinpath(app_name + ".exe")
|
|
|
|
def read_process_output(args):
|
|
process = subprocess.Popen(
|
|
f"{dist_app} {args}",
|
|
stdout=subprocess.PIPE,
|
|
stderr=subprocess.STDOUT,
|
|
shell=True,
|
|
)
|
|
output, _ = process.communicate()
|
|
return output.decode("utf-8").strip()
|
|
|
|
global g_version
|
|
global g_build_date
|
|
g_version = args.version.replace("-", ".")
|
|
if g_version == "":
|
|
g_version = read_process_output("--version")
|
|
version_pattern = re.compile(r"\d+\.\d+\.\d+.*")
|
|
if not version_pattern.match(g_version):
|
|
print(f"Error: version {g_version} not found in {dist_app}")
|
|
return False
|
|
if g_version.count(".") == 2:
|
|
# https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Version.cs
|
|
if args.revision_version < 0 or args.revision_version > 2147483647:
|
|
raise ValueError(f"Invalid revision version: {args.revision_version}")
|
|
g_version = f"{g_version}.{args.revision_version}"
|
|
|
|
g_build_date = read_process_output("--build-date")
|
|
build_date_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}")
|
|
if not build_date_pattern.match(g_build_date):
|
|
print(f"Error: build date {g_build_date} not found in {dist_app}")
|
|
return False
|
|
|
|
return True
|
|
|
|
|
|
def update_license_file(app_name):
|
|
if app_name == "RustDesk":
|
|
return
|
|
license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf")
|
|
with open(license_file, "r", encoding="utf-8") as f:
|
|
license_content = f.read()
|
|
license_content = license_content.replace("website rustdesk.com and other ", "")
|
|
license_content = license_content.replace("RustDesk", app_name)
|
|
license_content = re.sub(r"Purslane(?: Tech Pte\.)? Ltd", app_name, license_content, flags=re.IGNORECASE)
|
|
with open(license_file, "w", encoding="utf-8") as f:
|
|
f.write(license_content)
|
|
|
|
|
|
def replace_component_guids_in_wxs():
|
|
langs_dir = Path(sys.argv[0]).parent.joinpath("Package")
|
|
for file_path in langs_dir.glob("**/*.wxs"):
|
|
with open(file_path, "r", encoding="utf-8") as f:
|
|
lines = f.readlines()
|
|
|
|
# <Component Id="Product.Registry.DefaultIcon" Guid="6DBF2690-0955-4C6A-940F-634DDA503F49">
|
|
for i, line in enumerate(lines):
|
|
match = re.search(r'Component.+Guid="([^"]+)"', line)
|
|
if match:
|
|
lines[i] = re.sub(r'Guid="[^"]+"', f'Guid="{uuid.uuid4()}"', line)
|
|
|
|
with open(file_path, "w", encoding="utf-8") as f:
|
|
f.writelines(lines)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = make_parser()
|
|
args = parser.parse_args()
|
|
|
|
app_name = args.app_name
|
|
dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve()
|
|
|
|
if not prepare_resources():
|
|
sys.exit(-1)
|
|
|
|
if not init_global_vars(dist_dir, app_name, args):
|
|
sys.exit(-1)
|
|
|
|
update_license_file(app_name)
|
|
|
|
if not gen_pre_vars(args, dist_dir):
|
|
sys.exit(-1)
|
|
|
|
if app_name != "RustDesk":
|
|
replace_component_guids_in_wxs()
|
|
|
|
if not gen_upgrade_info():
|
|
sys.exit(-1)
|
|
|
|
if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
|
|
sys.exit(-1)
|
|
|
|
if not gen_conn_type(args):
|
|
sys.exit(-1)
|
|
|
|
if args.template:
|
|
if not gen_media2():
|
|
sys.exit(-1)
|
|
if not put_app_exe_on_media2():
|
|
sys.exit(-1)
|
|
|
|
if not gen_auto_component(app_name, dist_dir, args.template):
|
|
sys.exit(-1)
|
|
|
|
if not gen_custom_dialog_bitmaps():
|
|
sys.exit(-1)
|
|
|
|
replace_app_name_in_langs(args.app_name)
|