diff --git a/scripts/ci/package_windows_portable.py b/scripts/ci/package_windows_portable.py index 3325621f..2dd0cb0b 100644 --- a/scripts/ci/package_windows_portable.py +++ b/scripts/ci/package_windows_portable.py @@ -9,6 +9,7 @@ import re import shutil import tempfile +import tomllib from typing import Iterable from scripts.ci.lib.artifact_arch import normalize_arch_alias @@ -31,6 +32,7 @@ - Microsoft Edge WebView2 Runtime must already be installed on this Windows machine. """ TAURI_CONFIG_RELATIVE_PATH = pathlib.Path("src-tauri") / "tauri.conf.json" +CARGO_TOML_RELATIVE_PATH = pathlib.Path("src-tauri") / "Cargo.toml" BACKEND_RESOURCE_RELATIVE_PATH = pathlib.Path("resources") / "backend" WEBUI_RESOURCE_RELATIVE_PATH = pathlib.Path("resources") / "webui" WINDOWS_CLEANUP_SCRIPT_RELATIVE_PATH = ( @@ -45,6 +47,7 @@ class ProjectConfig: root: pathlib.Path product_name: str + binary_name: str portable_marker_name: str @@ -88,10 +91,12 @@ def load_portable_runtime_marker(project_root: pathlib.Path) -> str: def load_project_config_from(start_path: pathlib.Path) -> ProjectConfig: project_root = resolve_project_root_from(start_path) product_name = resolve_product_name(project_root) + binary_name = load_binary_name_from_cargo(project_root) portable_marker_name = load_portable_runtime_marker(project_root) return ProjectConfig( root=project_root, product_name=product_name, + binary_name=binary_name, portable_marker_name=portable_marker_name, ) @@ -142,6 +147,33 @@ def load_tauri_config(project_root: pathlib.Path) -> dict: return json.loads(config_path.read_text(encoding="utf-8")) +def load_binary_name_from_cargo(project_root: pathlib.Path) -> str: + cargo_toml_path = project_root / CARGO_TOML_RELATIVE_PATH + if not cargo_toml_path.is_file(): + raise FileNotFoundError(f"Cargo.toml not found: {cargo_toml_path}") + + with cargo_toml_path.open("rb") as handle: + cargo_data = tomllib.load(handle) + + bins = cargo_data.get("bin") + if isinstance(bins, list): + for entry in bins: + if isinstance(entry, dict): + binary_name = str(entry.get("name", "")).strip() + if binary_name: + return binary_name + + package_table = cargo_data.get("package") + if not isinstance(package_table, dict): + raise ValueError(f"Missing [package] in {cargo_toml_path}") + + binary_name = str(package_table.get("name", "")).strip() + if not binary_name: + raise ValueError(f"Missing [package].name in {cargo_toml_path}") + + return binary_name + + def resolve_product_name(project_root: pathlib.Path) -> str: config = load_tauri_config(project_root) product_name = str(config.get("productName", "")).strip() @@ -158,7 +190,7 @@ def resolve_main_executable_path( bundle_dir: pathlib.Path, project_config: ProjectConfig ) -> pathlib.Path: release_dir = resolve_release_dir(bundle_dir) - main_executable_path = release_dir / f"{project_config.product_name}.exe" + main_executable_path = release_dir / f"{project_config.binary_name}.exe" if not main_executable_path.is_file(): raise FileNotFoundError(f"Main executable not found: {main_executable_path}") return main_executable_path diff --git a/scripts/ci/test_package_windows_portable.py b/scripts/ci/test_package_windows_portable.py index f68576b1..4d8bf81c 100644 --- a/scripts/ci/test_package_windows_portable.py +++ b/scripts/ci/test_package_windows_portable.py @@ -1,3 +1,4 @@ +import re import tempfile import unittest from pathlib import Path @@ -109,12 +110,14 @@ def test_load_project_config_from_returns_root_product_and_marker(self): project_root / "scripts" / "ci" / "package_windows_portable.py" ) tauri_config_path = project_root / "src-tauri" / "tauri.conf.json" + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH script_path.parent.mkdir(parents=True) script_path.write_text("# placeholder") tauri_config_path.parent.mkdir(parents=True) tauri_config_path.write_text('{"productName":"AstrBot"}') + cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n') marker_path.parent.mkdir(parents=True, exist_ok=True) marker_path.write_text("portable.flag\n") @@ -122,8 +125,119 @@ def test_load_project_config_from_returns_root_product_and_marker(self): self.assertEqual(project_config.root, project_root.resolve()) self.assertEqual(project_config.product_name, "AstrBot") + self.assertEqual(project_config.binary_name, "astrbot-desktop-tauri") self.assertEqual(project_config.portable_marker_name, "portable.flag") + def test_load_cargo_package_name_supports_inline_comments(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + cargo_toml_path.parent.mkdir(parents=True) + cargo_toml_path.write_text( + '[package]\nname = "astrbot-desktop-tauri" # main binary\n' + ) + + self.assertEqual( + MODULE.load_binary_name_from_cargo(project_root), + "astrbot-desktop-tauri", + ) + + def test_load_cargo_package_name_missing_cargo_toml_raises_file_not_found(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + + with self.assertRaisesRegex( + FileNotFoundError, re.escape(str(cargo_toml_path)) + ): + MODULE.load_binary_name_from_cargo(project_root) + + def test_load_cargo_package_name_missing_package_table_raises_value_error(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + cargo_toml_path.parent.mkdir(parents=True) + cargo_toml_path.write_text('[workspace]\nmembers = ["crates/*"]\n') + + with self.assertRaisesRegex(ValueError, re.escape(str(cargo_toml_path))): + MODULE.load_binary_name_from_cargo(project_root) + + def test_load_cargo_package_name_missing_package_name_raises_value_error(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + cargo_toml_path.parent.mkdir(parents=True) + cargo_toml_path.write_text('[package]\nversion = "0.1.0"\n') + + with self.assertRaisesRegex(ValueError, re.escape(str(cargo_toml_path))): + MODULE.load_binary_name_from_cargo(project_root) + + def test_load_cargo_package_name_empty_package_name_raises_value_error(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + cargo_toml_path.parent.mkdir(parents=True) + cargo_toml_path.write_text('[package]\nname = ""\n') + + with self.assertRaisesRegex(ValueError, re.escape(str(cargo_toml_path))): + MODULE.load_binary_name_from_cargo(project_root) + + def test_load_cargo_package_name_falls_back_to_package_when_bin_missing_name(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + cargo_toml_path.parent.mkdir(parents=True) + cargo_toml_path.write_text( + "[package]\n" + 'name = "astrbot-desktop-tauri"\n\n' + "[[bin]]\n" + 'path = "src/main.rs"\n' + ) + + self.assertEqual( + MODULE.load_binary_name_from_cargo(project_root), + "astrbot-desktop-tauri", + ) + + def test_load_cargo_package_name_prefers_explicit_bin_name(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" + cargo_toml_path.parent.mkdir(parents=True) + cargo_toml_path.write_text( + "[package]\n" + 'name = "astrbot-desktop-tauri"\n\n' + "[[bin]]\n" + 'name = "AstrBot"\n' + ) + + self.assertEqual( + MODULE.load_binary_name_from_cargo(project_root), "AstrBot" + ) + + def test_resolve_main_executable_path_uses_binary_name_not_product_name(self): + with tempfile.TemporaryDirectory() as tmpdir: + project_root = Path(tmpdir) + bundle_dir = ( + project_root / "src-tauri" / "target" / "release" / "bundle" / "nsis" + ) + release_dir = project_root / "src-tauri" / "target" / "release" + bundle_dir.mkdir(parents=True) + release_dir.mkdir(parents=True, exist_ok=True) + (release_dir / "astrbot-desktop-tauri.exe").write_text("exe") + + project_config = MODULE.ProjectConfig( + root=project_root, + product_name="AstrBot", + binary_name="astrbot-desktop-tauri", + portable_marker_name="portable.flag", + ) + + self.assertEqual( + MODULE.resolve_main_executable_path(bundle_dir, project_config), + release_dir / "astrbot-desktop-tauri.exe", + ) + def test_iter_installer_paths_only_returns_installer_style_executables(self): with tempfile.TemporaryDirectory() as tmpdir: bundle_dir = Path(tmpdir) @@ -156,6 +270,7 @@ def test_populate_portable_root_copies_release_bundle_contents(self): webui_dir = project_root / "resources" / "webui" windows_dir = project_root / "src-tauri" / "windows" tauri_config_path = project_root / "src-tauri" / "tauri.conf.json" + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH script_path.parent.mkdir(parents=True) @@ -167,8 +282,9 @@ def test_populate_portable_root_copies_release_bundle_contents(self): windows_dir.mkdir(parents=True) tauri_config_path.write_text('{"productName":"AstrBot"}') + cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n') marker_path.write_text("portable.flag\n") - (release_dir / "AstrBot.exe").write_text("exe") + (release_dir / "astrbot-desktop-tauri.exe").write_text("exe") (release_dir / "WebView2Loader.dll").write_text("dll") (backend_dir / "runtime-manifest.json").write_text("{}") (backend_dir / "launch_backend.py").write_text("print('ok')") @@ -183,7 +299,7 @@ def test_populate_portable_root_copies_release_bundle_contents(self): project_config=MODULE.load_project_config_from(script_path), ) - self.assertTrue((destination_root / "AstrBot.exe").is_file()) + self.assertTrue((destination_root / "astrbot-desktop-tauri.exe").is_file()) self.assertTrue((destination_root / "WebView2Loader.dll").is_file()) self.assertTrue( ( @@ -210,6 +326,7 @@ def test_populate_portable_root_rejects_missing_main_executable(self): backend_dir = project_root / "resources" / "backend" webui_dir = project_root / "resources" / "webui" tauri_config_path = project_root / "src-tauri" / "tauri.conf.json" + cargo_toml_path = project_root / "src-tauri" / "Cargo.toml" marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH script_path.parent.mkdir(parents=True) @@ -219,6 +336,7 @@ def test_populate_portable_root_rejects_missing_main_executable(self): webui_dir.mkdir(parents=True) tauri_config_path.parent.mkdir(parents=True, exist_ok=True) tauri_config_path.write_text('{"productName":"AstrBot"}') + cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n') marker_path.parent.mkdir(parents=True, exist_ok=True) marker_path.write_text("portable.flag\n") (backend_dir / "runtime-manifest.json").write_text("{}") @@ -237,6 +355,7 @@ def test_add_portable_runtime_files_writes_marker_and_readme(self): project_config = MODULE.ProjectConfig( root=Path(tmpdir), product_name="AstrBot", + binary_name="astrbot-desktop-tauri", portable_marker_name="portable.flag", )