44
55import argparse
66from dataclasses import dataclass
7+ from datetime import datetime
78import json
89import pathlib
910import re
1415
1516from scripts .ci .lib .artifact_arch import normalize_arch_alias
1617from scripts .ci .lib .release_artifacts import SHORT_SHA_PATTERN
18+ from scripts .ci .lib .windows_filenames import validate_windows_filename
1719
1820
1921WINDOWS_CANONICAL_INSTALLER_RE = re .compile (
2325WINDOWS_LEGACY_INSTALLER_RE = re .compile (
2426 r"(?P<name>.+?)_(?P<version>.+?)_(?P<arch>x64|amd64|arm64|aarch64)-setup\.exe$"
2527)
28+ LEGACY_NIGHTLY_BASE_VERSION_RE = re .compile (r"^[0-9A-Za-z.+]+(?:-[0-9A-Za-z.+]+)*$" )
2629
2730PORTABLE_README_NAME = "README-portable.txt"
2831PORTABLE_README_TEXT = """AstrBot Windows portable package
@@ -55,6 +58,14 @@ def normalize_arch(arch: str) -> str:
5558 return normalize_arch_alias (arch ) or arch
5659
5760
61+ def is_valid_nightly_date (date_value : str ) -> bool :
62+ try :
63+ datetime .strptime (date_value , "%Y%m%d" )
64+ except ValueError :
65+ return False
66+ return True
67+
68+
5869def resolve_project_root_from (start_path : pathlib .Path ) -> pathlib .Path :
5970 candidate = start_path .resolve ()
6071 if candidate .is_file ():
@@ -101,6 +112,31 @@ def load_project_config_from(start_path: pathlib.Path) -> ProjectConfig:
101112 )
102113
103114
115+ def normalize_legacy_nightly_version (version : str ) -> tuple [str , str ]:
116+ if "-nightly" not in version :
117+ return version , ""
118+
119+ base_version , separator , nightly_part = version .partition ("-nightly" )
120+ if not separator or not LEGACY_NIGHTLY_BASE_VERSION_RE .fullmatch (base_version ):
121+ return version , ""
122+
123+ nightly_part = nightly_part .lstrip ("._-" )
124+ if not nightly_part :
125+ return base_version , ""
126+
127+ parts = re .split (r"[._-]" , nightly_part , maxsplit = 2 )
128+ if len (parts ) != 2 :
129+ return base_version , ""
130+
131+ date_value , sha = parts [0 ], parts [1 ]
132+ if not is_valid_nightly_date (date_value ):
133+ return base_version , ""
134+ if not re .fullmatch (SHORT_SHA_PATTERN , sha ):
135+ return base_version , ""
136+
137+ return base_version , f"_nightly_{ sha } "
138+
139+
104140def load_project_config () -> ProjectConfig :
105141 return load_project_config_from (pathlib .Path (__file__ ))
106142
@@ -117,9 +153,11 @@ def installer_to_portable_name(installer_name: str) -> str:
117153 legacy_match = WINDOWS_LEGACY_INSTALLER_RE .fullmatch (installer_name )
118154 if legacy_match :
119155 name = legacy_match .group ("name" )
120- version = legacy_match .group ("version" )
156+ version , nightly_suffix = normalize_legacy_nightly_version (
157+ legacy_match .group ("version" )
158+ )
121159 arch = normalize_arch (legacy_match .group ("arch" ))
122- return f"{ name } _{ version } _windows_{ arch } _portable.zip"
160+ return f"{ name } _{ version } _windows_{ arch } _portable{ nightly_suffix } .zip"
123161
124162 raise ValueError (
125163 "Unexpected Windows installer name: "
@@ -179,6 +217,13 @@ def resolve_product_name(project_root: pathlib.Path) -> str:
179217 product_name = str (config .get ("productName" , "" )).strip ()
180218 if not product_name :
181219 raise ValueError (f"Missing productName in { TAURI_CONFIG_RELATIVE_PATH } " )
220+ if product_name .lower ().endswith (".exe" ):
221+ product_name = product_name [:- 4 ].rstrip ()
222+ if not product_name :
223+ raise ValueError (
224+ f"productName resolves to an empty executable name in { TAURI_CONFIG_RELATIVE_PATH } "
225+ )
226+ validate_windows_filename (product_name )
182227 return product_name
183228
184229
@@ -205,7 +250,10 @@ def populate_portable_root(
205250 main_executable_path = resolve_main_executable_path (bundle_dir , project_config )
206251
207252 destination_root .mkdir (parents = True , exist_ok = True )
208- shutil .copy2 (main_executable_path , destination_root / main_executable_path .name )
253+ shutil .copy2 (
254+ main_executable_path ,
255+ destination_root / f"{ project_config .product_name } .exe" ,
256+ )
209257
210258 webview_loader = release_dir / "WebView2Loader.dll"
211259 if webview_loader .is_file ():
0 commit comments