#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import json
import sys
import uuid
import argparse
import datetime
import re
from pathlib import Path
g_indent_unit = "\t"
g_version = ""
g_build_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
# 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/fufesou/rustdesk",
},
}
def make_parser():
parser = argparse.ArgumentParser(description="Msi preprocess script.")
parser.add_argument(
"-d", "--dist-dir", type=str, default="../../rustdesk", help="The dist direcotry to install."
)
parser.add_argument(
"-arp",
"--arp",
action="store_true",
help="Is ARPSYSTEMCOMPONENT",
default=False,
)
parser.add_argument(
"-custom-arp",
"--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(
"-an", "--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(
"-m",
"--manufacturer",
type=str,
default="RustDesk",
help="The app manufacturer.",
)
return parser
def read_lines_and_start_index(file_path, tag_start, tag_end):
with open(file_path, "r") 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):
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
relative_file_path = file_path.relative_to(path)
guid = uuid.uuid5(
uuid.NAMESPACE_OID, app_name + "/" + str(relative_file_path)
)
subdir = str(file_path.parent.relative_to(path))
dir_attr = ""
if subdir != ".":
dir_attr = f'Subdirectory="{subdir}"'
# 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}
{indent}{g_indent_unit}
{indent}
"""
lines.insert(index_start + 1, to_insert_lines[1:])
index_start += 1
idx += 1
return True
def gen_auto_component(app_name, dist_dir):
return gen_content_between_tags(
"Package/Components/RustDesk.wxs",
"",
"",
lambda lines, index_start: insert_components_between_tags(
lines, index_start, app_name, dist_dir
),
)
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}\n',
f'{indent}\n',
f'{indent}\n',
f'{indent}\n',
f'{indent}\n',
f'{indent}\n',
f'{indent}\n',
f'{indent}\n',
f'{indent}\n',
"\n",
f"{indent}\n"
f'{indent}\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", "", "", func
)
def replace_app_name_in_lans(app_name):
langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language")
for file_path in langs_dir.glob("*.wxs"):
with open(file_path, "r") as f:
lines = f.readlines()
for i, line in enumerate(lines):
lines[i] = line.replace("RustDesk", app_name)
with open(file_path, "w") as f:
f.writelines(lines)
def gen_upgrade_info():
def func(lines, index_start):
indent = g_indent_unit * 3
major, _, _ = g_version.split(".")
upgrade_id = uuid.uuid4()
to_insert_lines = [
f'{indent}\n',
f'{indent}{g_indent_unit}\n',
f"{indent}\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",
"",
"",
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}\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",
"",
"",
func,
)
def gen_custom_ARPSYSTEMCOMPONENT_False(args):
def func(lines, index_start):
indent = g_indent_unit * 2
lines_new = []
lines_new.append(
f"{indent}\n"
)
lines_new.append(
f'{indent}\n\n'
)
lines_new.append(
f"{indent}\n"
)
for _, v in g_arpsystemcomponent.items():
if "msi" in v and "v" in v:
lines_new.append(f'{indent}\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",
"",
"",
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}\n"
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
installDate = datetime.datetime.now().strftime("%Y%m%d")
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
estimated_size = get_folder_size(dist_dir)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(f'{indent}\n')
lines_new.append(
f'{indent}\n'
)
major, minor, build = g_version.split(".")
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\n'
)
lines_new.append(
f'{indent}\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}\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",
"",
"",
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_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") as f:
f.writelines(lines)
return True
def init_global_vars(args):
var_file = "../../src/version.rs"
if not Path(var_file).exists():
print(f"Error: {var_file} not found")
return False
with open(var_file, "r") as f:
content = f.readlines()
global g_version
global g_build_date
g_version = args.version.replace("-", ".")
if g_version == "":
# pub const VERSION: &str = "1.2.4";
version_pattern = re.compile(r'.*VERSION: &str = "(.*)";.*')
for line in content:
match = version_pattern.match(line)
if match:
g_version = match.group(1)
break
if g_version == "":
print(f"Error: version not found in {var_file}")
return False
# pub const BUILD_DATE: &str = "2024-04-08 23:11";
build_date_pattern = re.compile(r'BUILD_DATE: &str = "(.*)";')
for line in content:
match = build_date_pattern.match(line)
if match:
g_build_date = match.group(1)
break
return True
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 init_global_vars(args):
sys.exit(-1)
if not gen_pre_vars(args, dist_dir):
sys.exit(-1)
if not gen_upgrade_info():
sys.exit(-1)
if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
sys.exit(-1)
if not gen_auto_component(app_name, dist_dir):
sys.exit(-1)
if not gen_custom_dialog_bitmaps():
sys.exit(-1)
replace_app_name_in_lans(args.app_name)