Skip to content

Commit 3852988

Browse files
committed
test(ci): harden windows portable naming rules
1 parent 948d941 commit 3852988

2 files changed

Lines changed: 79 additions & 8 deletions

File tree

scripts/ci/package_windows_portable.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
PORTABLE_RUNTIME_MARKER_RELATIVE_PATH = (
4545
pathlib.Path("src-tauri") / "windows" / "portable-runtime-marker.txt"
4646
)
47+
WINDOWS_FILENAME_INVALID_CHARS_RE = re.compile(r'[<>:"/\\|?*]')
4748

4849

4950
@dataclass(frozen=True)
@@ -187,6 +188,16 @@ def resolve_product_name(project_root: pathlib.Path) -> str:
187188
product_name = str(config.get("productName", "")).strip()
188189
if not product_name:
189190
raise ValueError(f"Missing productName in {TAURI_CONFIG_RELATIVE_PATH}")
191+
if product_name.lower().endswith(".exe"):
192+
product_name = product_name[:-4].rstrip()
193+
if not product_name:
194+
raise ValueError(
195+
f"productName resolves to an empty executable name in {TAURI_CONFIG_RELATIVE_PATH}"
196+
)
197+
if WINDOWS_FILENAME_INVALID_CHARS_RE.search(product_name):
198+
raise ValueError(
199+
f"productName contains characters invalid Windows filename characters: {product_name!r}"
200+
)
190201
return product_name
191202

192203

scripts/ci/test_package_windows_portable.py

Lines changed: 68 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,23 @@ def test_installer_to_portable_name_accepts_canonical_nightly_windows_name(self)
2828
)
2929

3030
def test_installer_to_portable_name_normalizes_legacy_nightly_windows_name(self):
31-
self.assertEqual(
32-
MODULE.installer_to_portable_name(
33-
"AstrBot_4.29.0-nightly.20260401.deadbeef_aarch64-setup.exe"
34-
),
35-
"AstrBot_4.29.0_windows_arm64_portable_nightly_deadbeef.zip",
36-
)
31+
arch_cases = [
32+
("x64", "amd64"),
33+
("amd64", "amd64"),
34+
("arm64", "arm64"),
35+
("aarch64", "arm64"),
36+
]
37+
separators = [".", "_", "-"]
38+
39+
for arch_input, arch_output in arch_cases:
40+
for separator in separators:
41+
with self.subTest(arch=arch_input, separator=separator):
42+
installer_name = f"AstrBot_4.29.0-nightly{separator}20260401{separator}deadbeef_{arch_input}-setup.exe"
43+
expected_name = f"AstrBot_4.29.0_windows_{arch_output}_portable_nightly_deadbeef.zip"
44+
self.assertEqual(
45+
MODULE.installer_to_portable_name(installer_name),
46+
expected_name,
47+
)
3748

3849
def test_installer_to_portable_name_rejects_noncanonical_nightly_suffix_length(
3950
self,
@@ -136,6 +147,51 @@ def test_load_project_config_from_returns_root_product_and_marker(self):
136147
self.assertEqual(project_config.binary_name, "astrbot-desktop-tauri")
137148
self.assertEqual(project_config.portable_marker_name, "portable.flag")
138149

150+
def test_load_project_config_from_rejects_product_name_with_invalid_windows_chars(
151+
self,
152+
):
153+
with tempfile.TemporaryDirectory() as tmpdir:
154+
project_root = Path(tmpdir)
155+
script_path = (
156+
project_root / "scripts" / "ci" / "package_windows_portable.py"
157+
)
158+
tauri_config_path = project_root / "src-tauri" / "tauri.conf.json"
159+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
160+
marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH
161+
162+
script_path.parent.mkdir(parents=True)
163+
script_path.write_text("# placeholder")
164+
tauri_config_path.parent.mkdir(parents=True)
165+
tauri_config_path.write_text('{"productName":"AstrBot:Beta"}')
166+
cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n')
167+
marker_path.parent.mkdir(parents=True, exist_ok=True)
168+
marker_path.write_text("portable.flag\n")
169+
170+
with self.assertRaisesRegex(ValueError, "invalid Windows filename"):
171+
MODULE.load_project_config_from(script_path)
172+
173+
def test_load_project_config_from_strips_exe_suffix_from_product_name(self):
174+
with tempfile.TemporaryDirectory() as tmpdir:
175+
project_root = Path(tmpdir)
176+
script_path = (
177+
project_root / "scripts" / "ci" / "package_windows_portable.py"
178+
)
179+
tauri_config_path = project_root / "src-tauri" / "tauri.conf.json"
180+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
181+
marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH
182+
183+
script_path.parent.mkdir(parents=True)
184+
script_path.write_text("# placeholder")
185+
tauri_config_path.parent.mkdir(parents=True)
186+
tauri_config_path.write_text('{"productName":"AstrBot.exe"}')
187+
cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n')
188+
marker_path.parent.mkdir(parents=True, exist_ok=True)
189+
marker_path.write_text("portable.flag\n")
190+
191+
project_config = MODULE.load_project_config_from(script_path)
192+
193+
self.assertEqual(project_config.product_name, "AstrBot")
194+
139195
def test_load_cargo_package_name_supports_inline_comments(self):
140196
with tempfile.TemporaryDirectory() as tmpdir:
141197
project_root = Path(tmpdir)
@@ -301,13 +357,17 @@ def test_populate_portable_root_copies_release_bundle_contents(self):
301357
"Write-Host cleanup"
302358
)
303359

360+
project_config = MODULE.load_project_config_from(script_path)
361+
304362
MODULE.populate_portable_root(
305363
bundle_dir=bundle_dir,
306364
destination_root=destination_root,
307-
project_config=MODULE.load_project_config_from(script_path),
365+
project_config=project_config,
308366
)
309367

310-
self.assertTrue((destination_root / "AstrBot.exe").is_file())
368+
executable_name = f"{project_config.product_name}.exe"
369+
370+
self.assertTrue((destination_root / executable_name).is_file())
311371
self.assertFalse((destination_root / "astrbot-desktop-tauri.exe").exists())
312372
self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
313373
self.assertTrue(

0 commit comments

Comments
 (0)