preprocess.py 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545
  1. #!/usr/bin/env python3
  2. # -*- coding: utf-8 -*-
  3. import json
  4. import sys
  5. import uuid
  6. import argparse
  7. import datetime
  8. import subprocess
  9. import re
  10. from pathlib import Path
  11. import shutil
  12. g_indent_unit = "\t"
  13. g_version = ""
  14. g_build_date = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
  15. # Replace the following links with your own in the custom arp properties.
  16. # https://learn.microsoft.com/en-us/windows/win32/msi/property-reference
  17. g_arpsystemcomponent = {
  18. "Comments": {
  19. "msi": "ARPCOMMENTS",
  20. "t": "string",
  21. "v": "!(loc.AR_Comment)",
  22. },
  23. "Contact": {
  24. "msi": "ARPCONTACT",
  25. "v": "https://github.com/rustdesk/rustdesk",
  26. },
  27. "HelpLink": {
  28. "msi": "ARPHELPLINK",
  29. "v": "https://github.com/rustdesk/rustdesk/issues/",
  30. },
  31. "ReadMe": {
  32. "msi": "ARPREADME",
  33. "v": "https://github.com/rustdesk/rustdesk",
  34. },
  35. }
  36. def default_revision_version():
  37. return int(datetime.datetime.now().timestamp() / 60)
  38. def make_parser():
  39. parser = argparse.ArgumentParser(description="Msi preprocess script.")
  40. parser.add_argument(
  41. "-d",
  42. "--dist-dir",
  43. type=str,
  44. default="../../rustdesk",
  45. help="The dist direcotry to install.",
  46. )
  47. parser.add_argument(
  48. "--arp",
  49. action="store_true",
  50. help="Is ARPSYSTEMCOMPONENT",
  51. default=False,
  52. )
  53. parser.add_argument(
  54. "--custom-arp",
  55. type=str,
  56. default="{}",
  57. help='Custom arp properties, e.g. \'["Comments": {"msi": "ARPCOMMENTS", "v": "Remote control application."}]\'',
  58. )
  59. parser.add_argument(
  60. "-c", "--custom", action="store_true", help="Is custom client", default=False
  61. )
  62. parser.add_argument(
  63. "--conn-type",
  64. type=str,
  65. default="",
  66. help='Connection type, e.g. "incoming", "outgoing". Default is empty, means incoming-outgoing',
  67. )
  68. parser.add_argument(
  69. "--app-name", type=str, default="RustDesk", help="The app name."
  70. )
  71. parser.add_argument(
  72. "-v", "--version", type=str, default="", help="The app version."
  73. )
  74. parser.add_argument(
  75. "--revision-version", type=int, default=default_revision_version(), help="The revision version."
  76. )
  77. parser.add_argument(
  78. "-m",
  79. "--manufacturer",
  80. type=str,
  81. default="PURSLANE",
  82. help="The app manufacturer.",
  83. )
  84. return parser
  85. def read_lines_and_start_index(file_path, tag_start, tag_end):
  86. with open(file_path, "r", encoding="utf-8") as f:
  87. lines = f.readlines()
  88. index_start = -1
  89. index_end = -1
  90. for i, line in enumerate(lines):
  91. if tag_start in line:
  92. index_start = i
  93. if tag_end in line:
  94. index_end = i
  95. if index_start == -1:
  96. print(f'Error: start tag "{tag_start}" not found')
  97. return None, None
  98. if index_end == -1:
  99. print(f'Error: end tag "{tag_end}" not found')
  100. return None, None
  101. return lines, index_start
  102. def insert_components_between_tags(lines, index_start, app_name, dist_dir):
  103. indent = g_indent_unit * 3
  104. path = Path(dist_dir)
  105. idx = 1
  106. for file_path in path.glob("**/*"):
  107. if file_path.is_file():
  108. if file_path.name.lower() == f"{app_name}.exe".lower():
  109. continue
  110. subdir = str(file_path.parent.relative_to(path))
  111. dir_attr = ""
  112. if subdir != ".":
  113. dir_attr = f'Subdirectory="{subdir}"'
  114. # Don't generate Component Id and File Id like 'Component_{idx}' and 'File_{idx}'
  115. # because it will cause error
  116. # "Error WIX0130 The primary key 'xxxx' is duplicated in table 'Directory'"
  117. to_insert_lines = f"""
  118. {indent}<Component Guid="{uuid.uuid4()}" {dir_attr}>
  119. {indent}{g_indent_unit}<File Source="{file_path.as_posix()}" KeyPath="yes" Checksum="yes" />
  120. {indent}</Component>
  121. """
  122. lines.insert(index_start + 1, to_insert_lines[1:])
  123. index_start += 1
  124. idx += 1
  125. return True
  126. def gen_auto_component(app_name, dist_dir):
  127. return gen_content_between_tags(
  128. "Package/Components/RustDesk.wxs",
  129. "<!--$AutoComonentStart$-->",
  130. "<!--$AutoComponentEnd$-->",
  131. lambda lines, index_start: insert_components_between_tags(
  132. lines, index_start, app_name, dist_dir
  133. ),
  134. )
  135. def gen_pre_vars(args, dist_dir):
  136. def func(lines, index_start):
  137. upgrade_code = uuid.uuid5(uuid.NAMESPACE_OID, app_name + ".exe")
  138. indent = g_indent_unit * 1
  139. to_insert_lines = [
  140. f'{indent}<?define Version="{g_version}" ?>\n',
  141. f'{indent}<?define Manufacturer="{args.manufacturer}" ?>\n',
  142. f'{indent}<?define Product="{args.app_name}" ?>\n',
  143. f'{indent}<?define Description="{args.app_name} Installer" ?>\n',
  144. f'{indent}<?define ProductLower="{args.app_name.lower()}" ?>\n',
  145. f'{indent}<?define RegKeyRoot=".$(var.ProductLower)" ?>\n',
  146. f'{indent}<?define RegKeyInstall="$(var.RegKeyRoot)\\Install" ?>\n',
  147. f'{indent}<?define BuildDir="{dist_dir}" ?>\n',
  148. f'{indent}<?define BuildDate="{g_build_date}" ?>\n',
  149. "\n",
  150. f"{indent}<!-- The UpgradeCode must be consistent for each product. ! -->\n"
  151. f'{indent}<?define UpgradeCode = "{upgrade_code}" ?>\n',
  152. ]
  153. for i, line in enumerate(to_insert_lines):
  154. lines.insert(index_start + i + 1, line)
  155. return lines
  156. return gen_content_between_tags(
  157. "Package/Includes.wxi", "<!--$PreVarsStart$-->", "<!--$PreVarsEnd$-->", func
  158. )
  159. def replace_app_name_in_langs(app_name):
  160. langs_dir = Path(sys.argv[0]).parent.joinpath("Package/Language")
  161. for file_path in langs_dir.glob("*.wxl"):
  162. with open(file_path, "r", encoding="utf-8") as f:
  163. lines = f.readlines()
  164. for i, line in enumerate(lines):
  165. lines[i] = line.replace("RustDesk", app_name)
  166. with open(file_path, "w", encoding="utf-8") as f:
  167. f.writelines(lines)
  168. def gen_upgrade_info():
  169. def func(lines, index_start):
  170. indent = g_indent_unit * 3
  171. vs = g_version.split(".")
  172. major = vs[0]
  173. upgrade_id = uuid.uuid4()
  174. to_insert_lines = [
  175. f'{indent}<Upgrade Id="{upgrade_id}">\n',
  176. 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',
  177. f"{indent}</Upgrade>\n",
  178. ]
  179. for i, line in enumerate(to_insert_lines):
  180. lines.insert(index_start + i + 1, line)
  181. return lines
  182. return gen_content_between_tags(
  183. "Package/Fragments/Upgrades.wxs",
  184. "<!--$UpgradeStart$-->",
  185. "<!--$UpgradeEnd$-->",
  186. func,
  187. )
  188. def gen_custom_dialog_bitmaps():
  189. def func(lines, index_start):
  190. indent = g_indent_unit * 2
  191. # https://wixtoolset.org/docs/tools/wixext/wixui/#customizing-a-dialog-set
  192. vars = [
  193. "WixUIBannerBmp",
  194. "WixUIDialogBmp",
  195. "WixUIExclamationIco",
  196. "WixUIInfoIco",
  197. "WixUINewIco",
  198. "WixUIUpIco",
  199. ]
  200. to_insert_lines = []
  201. for var in vars:
  202. if Path(f"Package/Resources/{var}.bmp").exists():
  203. to_insert_lines.append(
  204. f'{indent}<WixVariable Id="{var}" Value="Resources\\{var}.bmp" />\n'
  205. )
  206. for i, line in enumerate(to_insert_lines):
  207. lines.insert(index_start + i + 1, line)
  208. return lines
  209. return gen_content_between_tags(
  210. "Package/Package.wxs",
  211. "<!--$CustomBitmapsStart$-->",
  212. "<!--$CustomBitmapsEnd$-->",
  213. func,
  214. )
  215. def gen_custom_ARPSYSTEMCOMPONENT_False(args):
  216. def func(lines, index_start):
  217. indent = g_indent_unit * 2
  218. lines_new = []
  219. lines_new.append(
  220. f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/arpsystemcomponent?redirectedfrom=MSDN-->\n"
  221. )
  222. lines_new.append(
  223. f'{indent}<!--<Property Id="ARPSYSTEMCOMPONENT" Value="1" />-->\n\n'
  224. )
  225. lines_new.append(
  226. f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
  227. )
  228. for _, v in g_arpsystemcomponent.items():
  229. if "msi" in v and "v" in v:
  230. lines_new.append(
  231. f'{indent}<Property Id="{v["msi"]}" Value="{v["v"]}" />\n'
  232. )
  233. for i, line in enumerate(lines_new):
  234. lines.insert(index_start + i + 1, line)
  235. return lines
  236. return gen_content_between_tags(
  237. "Package/Fragments/AddRemoveProperties.wxs",
  238. "<!--$ArpStart$-->",
  239. "<!--$ArpEnd$-->",
  240. func,
  241. )
  242. def get_folder_size(folder_path):
  243. total_size = 0
  244. folder = Path(folder_path)
  245. for file in folder.glob("**/*"):
  246. if file.is_file():
  247. total_size += file.stat().st_size
  248. return total_size
  249. def gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir):
  250. def func(lines, index_start):
  251. indent = g_indent_unit * 5
  252. lines_new = []
  253. lines_new.append(
  254. f"{indent}<!--https://learn.microsoft.com/en-us/windows/win32/msi/property-reference-->\n"
  255. )
  256. lines_new.append(
  257. f'{indent}<RegistryValue Type="string" Name="DisplayName" Value="{args.app_name}" />\n'
  258. )
  259. lines_new.append(
  260. f'{indent}<RegistryValue Type="string" Name="DisplayIcon" Value="[INSTALLFOLDER_INNER]{args.app_name}.exe" />\n'
  261. )
  262. lines_new.append(
  263. f'{indent}<RegistryValue Type="string" Name="DisplayVersion" Value="{g_version}" />\n'
  264. )
  265. lines_new.append(
  266. f'{indent}<RegistryValue Type="string" Name="Publisher" Value="{args.manufacturer}" />\n'
  267. )
  268. installDate = datetime.datetime.now().strftime("%Y%m%d")
  269. lines_new.append(
  270. f'{indent}<RegistryValue Type="string" Name="InstallDate" Value="{installDate}" />\n'
  271. )
  272. lines_new.append(
  273. f'{indent}<RegistryValue Type="string" Name="InstallLocation" Value="[INSTALLFOLDER_INNER]" />\n'
  274. )
  275. lines_new.append(
  276. f'{indent}<RegistryValue Type="string" Name="InstallSource" Value="[InstallSource]" />\n'
  277. )
  278. lines_new.append(
  279. f'{indent}<RegistryValue Type="integer" Name="Language" Value="[ProductLanguage]" />\n'
  280. )
  281. estimated_size = get_folder_size(dist_dir)
  282. lines_new.append(
  283. f'{indent}<RegistryValue Type="integer" Name="EstimatedSize" Value="{estimated_size}" />\n'
  284. )
  285. lines_new.append(
  286. f'{indent}<RegistryValue Type="expandable" Name="ModifyPath" Value="MsiExec.exe /X [ProductCode]" />\n'
  287. )
  288. lines_new.append(
  289. f'{indent}<RegistryValue Type="integer" Id="NoModify" Value="1" />\n'
  290. )
  291. lines_new.append(
  292. f'{indent}<RegistryValue Type="expandable" Name="UninstallString" Value="MsiExec.exe /X [ProductCode]" />\n'
  293. )
  294. lines_new.append(
  295. f'{indent}<RegistryValue Type="expandable" Name="QuietUninstallString" Value="MsiExec.exe /qn /X [ProductCode]" />\n'
  296. )
  297. vs = g_version.split(".")
  298. major, minor, build = vs[0], vs[1], vs[2]
  299. lines_new.append(
  300. f'{indent}<RegistryValue Type="string" Name="Version" Value="{g_version}" />\n'
  301. )
  302. lines_new.append(
  303. f'{indent}<RegistryValue Type="integer" Name="VersionMajor" Value="{major}" />\n'
  304. )
  305. lines_new.append(
  306. f'{indent}<RegistryValue Type="integer" Name="VersionMinor" Value="{minor}" />\n'
  307. )
  308. lines_new.append(
  309. f'{indent}<RegistryValue Type="integer" Name="VersionBuild" Value="{build}" />\n'
  310. )
  311. lines_new.append(
  312. f'{indent}<RegistryValue Type="integer" Name="WindowsInstaller" Value="1" />\n'
  313. )
  314. for k, v in g_arpsystemcomponent.items():
  315. if "v" in v:
  316. t = v["t"] if "t" in v is None else "string"
  317. lines_new.append(
  318. f'{indent}<RegistryValue Type="{t}" Name="{k}" Value="{v["v"]}" />\n'
  319. )
  320. for i, line in enumerate(lines_new):
  321. lines.insert(index_start + i + 1, line)
  322. return lines
  323. return gen_content_between_tags(
  324. "Package/Components/Regs.wxs",
  325. "<!--$ArpStart$-->",
  326. "<!--$ArpEnd$-->",
  327. func,
  328. )
  329. def gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
  330. try:
  331. custom_arp = json.loads(args.custom_arp)
  332. g_arpsystemcomponent.update(custom_arp)
  333. except json.JSONDecodeError as e:
  334. print(f"Failed to decode custom arp: {e}")
  335. return False
  336. if args.arp:
  337. return gen_custom_ARPSYSTEMCOMPONENT_True(args, dist_dir)
  338. else:
  339. return gen_custom_ARPSYSTEMCOMPONENT_False(args)
  340. def gen_conn_type(args):
  341. def func(lines, index_start):
  342. indent = g_indent_unit * 3
  343. lines_new = []
  344. if args.conn_type != "":
  345. lines_new.append(
  346. f"""{indent}<Property Id="CC_CONNECTION_TYPE" Value="{args.conn_type}" />\n"""
  347. )
  348. for i, line in enumerate(lines_new):
  349. lines.insert(index_start + i + 1, line)
  350. return lines
  351. return gen_content_between_tags(
  352. "Package/Fragments/AddRemoveProperties.wxs",
  353. "<!--$CustomClientPropsStart$-->",
  354. "<!--$CustomClientPropsEnd$-->",
  355. func,
  356. )
  357. def gen_content_between_tags(filename, tag_start, tag_end, func):
  358. target_file = Path(sys.argv[0]).parent.joinpath(filename)
  359. lines, index_start = read_lines_and_start_index(target_file, tag_start, tag_end)
  360. if lines is None:
  361. return False
  362. func(lines, index_start)
  363. with open(target_file, "w", encoding="utf-8") as f:
  364. f.writelines(lines)
  365. return True
  366. def prepare_resources():
  367. icon_src = Path(sys.argv[0]).parent.joinpath("../icon.ico")
  368. icon_dst = Path(sys.argv[0]).parent.joinpath("Package/Resources/icon.ico")
  369. if icon_src.exists():
  370. icon_dst.parent.mkdir(parents=True, exist_ok=True)
  371. shutil.copy(icon_src, icon_dst)
  372. return True
  373. else:
  374. # unreachable
  375. print(f"Error: icon.ico not found in {icon_src}")
  376. return False
  377. def init_global_vars(dist_dir, app_name, args):
  378. dist_app = dist_dir.joinpath(app_name + ".exe")
  379. def read_process_output(args):
  380. process = subprocess.Popen(
  381. f"{dist_app} {args}",
  382. stdout=subprocess.PIPE,
  383. stderr=subprocess.STDOUT,
  384. shell=True,
  385. )
  386. output, _ = process.communicate()
  387. return output.decode("utf-8").strip()
  388. global g_version
  389. global g_build_date
  390. g_version = args.version.replace("-", ".")
  391. if g_version == "":
  392. g_version = read_process_output("--version")
  393. version_pattern = re.compile(r"\d+\.\d+\.\d+.*")
  394. if not version_pattern.match(g_version):
  395. print(f"Error: version {g_version} not found in {dist_app}")
  396. return False
  397. if g_version.count(".") == 2:
  398. # https://github.com/dotnet/runtime/blob/5535e31a712343a63f5d7d796cd874e563e5ac14/src/libraries/System.Private.CoreLib/src/System/Version.cs
  399. if args.revision_version < 0 or args.revision_version > 2147483647:
  400. raise ValueError(f"Invalid revision version: {args.revision_version}")
  401. g_version = f"{g_version}.{args.revision_version}"
  402. g_build_date = read_process_output("--build-date")
  403. build_date_pattern = re.compile(r"\d{4}-\d{2}-\d{2} \d{2}:\d{2}")
  404. if not build_date_pattern.match(g_build_date):
  405. print(f"Error: build date {g_build_date} not found in {dist_app}")
  406. return False
  407. return True
  408. def update_license_file(app_name):
  409. if app_name == "RustDesk":
  410. return
  411. license_file = Path(sys.argv[0]).parent.joinpath("Package/License.rtf")
  412. with open(license_file, "r", encoding="utf-8") as f:
  413. license_content = f.read()
  414. license_content = license_content.replace("website rustdesk.com and other ", "")
  415. license_content = license_content.replace("RustDesk", app_name)
  416. license_content = re.sub("Purslane Ltd", app_name, license_content, flags=re.IGNORECASE)
  417. with open(license_file, "w", encoding="utf-8") as f:
  418. f.write(license_content)
  419. def replace_component_guids_in_wxs():
  420. langs_dir = Path(sys.argv[0]).parent.joinpath("Package")
  421. for file_path in langs_dir.glob("**/*.wxs"):
  422. with open(file_path, "r", encoding="utf-8") as f:
  423. lines = f.readlines()
  424. # <Component Id="Product.Registry.DefaultIcon" Guid="6DBF2690-0955-4C6A-940F-634DDA503F49">
  425. for i, line in enumerate(lines):
  426. match = re.search(r'Component.+Guid="([^"]+)"', line)
  427. if match:
  428. lines[i] = re.sub(r'Guid="[^"]+"', f'Guid="{uuid.uuid4()}"', line)
  429. with open(file_path, "w", encoding="utf-8") as f:
  430. f.writelines(lines)
  431. if __name__ == "__main__":
  432. parser = make_parser()
  433. args = parser.parse_args()
  434. app_name = args.app_name
  435. dist_dir = Path(sys.argv[0]).parent.joinpath(args.dist_dir).resolve()
  436. if not prepare_resources():
  437. sys.exit(-1)
  438. if not init_global_vars(dist_dir, app_name, args):
  439. sys.exit(-1)
  440. update_license_file(app_name)
  441. if not gen_pre_vars(args, dist_dir):
  442. sys.exit(-1)
  443. if app_name != "RustDesk":
  444. replace_component_guids_in_wxs()
  445. if not gen_upgrade_info():
  446. sys.exit(-1)
  447. if not gen_custom_ARPSYSTEMCOMPONENT(args, dist_dir):
  448. sys.exit(-1)
  449. if not gen_conn_type(args):
  450. sys.exit(-1)
  451. if not gen_auto_component(app_name, dist_dir):
  452. sys.exit(-1)
  453. if not gen_custom_dialog_bitmaps():
  454. sys.exit(-1)
  455. replace_app_name_in_langs(args.app_name)