From cbd5c2fcb689dac973c9caa2e75c4525df74d8e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 1 Apr 2026 12:41:01 +0900 Subject: [PATCH 1/4] fix(ci): use Rust binary name for portable packaging --- scripts/ci/package_windows_portable.py | 29 ++++++++++++++++- scripts/ci/test_package_windows_portable.py | 35 +++++++++++++++++++-- 2 files changed, 61 insertions(+), 3 deletions(-) diff --git a/scripts/ci/package_windows_portable.py b/scripts/ci/package_windows_portable.py index 3325621f..8561c480 100644 --- a/scripts/ci/package_windows_portable.py +++ b/scripts/ci/package_windows_portable.py @@ -31,6 +31,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 +46,7 @@ class ProjectConfig: root: pathlib.Path product_name: str + binary_name: str portable_marker_name: str @@ -88,10 +90,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_cargo_package_name(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 +146,29 @@ def load_tauri_config(project_root: pathlib.Path) -> dict: return json.loads(config_path.read_text(encoding="utf-8")) +def load_cargo_package_name(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}") + + package_section = False + for raw_line in cargo_toml_path.read_text(encoding="utf-8").splitlines(): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if line.startswith("["): + package_section = line == "[package]" + continue + if package_section and line.startswith("name"): + _, _, value = line.partition("=") + binary_name = value.strip().strip('"').strip("'") + if binary_name: + return binary_name + break + + raise ValueError(f"Missing package.name in {CARGO_TOML_RELATIVE_PATH}") + + def resolve_product_name(project_root: pathlib.Path) -> str: config = load_tauri_config(project_root) product_name = str(config.get("productName", "")).strip() @@ -158,7 +185,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..f505cdbe 100644 --- a/scripts/ci/test_package_windows_portable.py +++ b/scripts/ci/test_package_windows_portable.py @@ -109,12 +109,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 +124,32 @@ 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_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 +182,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 +194,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 +211,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 +238,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 +248,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 +267,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", ) From 4c47952cb1ecab6a544929330530ccac8388e423 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 1 Apr 2026 12:57:21 +0900 Subject: [PATCH 2/4] fix(ci): parse Cargo.toml with tomllib --- scripts/ci/package_windows_portable.py | 37 ++++++++++++--------- scripts/ci/test_package_windows_portable.py | 28 ++++++++++++++++ 2 files changed, 49 insertions(+), 16 deletions(-) diff --git a/scripts/ci/package_windows_portable.py b/scripts/ci/package_windows_portable.py index 8561c480..a961d8d6 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 @@ -151,22 +152,26 @@ def load_cargo_package_name(project_root: pathlib.Path) -> str: if not cargo_toml_path.is_file(): raise FileNotFoundError(f"Cargo.toml not found: {cargo_toml_path}") - package_section = False - for raw_line in cargo_toml_path.read_text(encoding="utf-8").splitlines(): - line = raw_line.strip() - if not line or line.startswith("#"): - continue - if line.startswith("["): - package_section = line == "[package]" - continue - if package_section and line.startswith("name"): - _, _, value = line.partition("=") - binary_name = value.strip().strip('"').strip("'") - if binary_name: - return binary_name - break - - raise ValueError(f"Missing package.name in {CARGO_TOML_RELATIVE_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_RELATIVE_PATH}") + + binary_name = str(package_table.get("name", "")).strip() + if not binary_name: + raise ValueError(f"Missing [package].name in {CARGO_TOML_RELATIVE_PATH}") + + return binary_name def resolve_product_name(project_root: pathlib.Path) -> str: diff --git a/scripts/ci/test_package_windows_portable.py b/scripts/ci/test_package_windows_portable.py index f505cdbe..030ff101 100644 --- a/scripts/ci/test_package_windows_portable.py +++ b/scripts/ci/test_package_windows_portable.py @@ -127,6 +127,34 @@ def test_load_project_config_from_returns_root_product_and_marker(self): 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_cargo_package_name(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_cargo_package_name(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) From a30ed46e1a96d53f2051d3f47d0e1f4da83d366d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 1 Apr 2026 13:15:18 +0900 Subject: [PATCH 3/4] test(ci): cover Cargo.toml parser edge cases --- scripts/ci/test_package_windows_portable.py | 54 +++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/scripts/ci/test_package_windows_portable.py b/scripts/ci/test_package_windows_portable.py index 030ff101..b2b97e4b 100644 --- a/scripts/ci/test_package_windows_portable.py +++ b/scripts/ci/test_package_windows_portable.py @@ -141,6 +141,60 @@ def test_load_cargo_package_name_supports_inline_comments(self): "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) + + with self.assertRaises(FileNotFoundError): + MODULE.load_cargo_package_name(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.assertRaises(ValueError): + MODULE.load_cargo_package_name(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.assertRaises(ValueError): + MODULE.load_cargo_package_name(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.assertRaises(ValueError): + MODULE.load_cargo_package_name(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_cargo_package_name(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) From 26dcc7edd6bc31f68b7e9c132ddda8269e1a031d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=82=B9=E6=B0=B8=E8=B5=AB?= <1259085392@qq.com> Date: Wed, 1 Apr 2026 13:28:41 +0900 Subject: [PATCH 4/4] refactor(ci): clarify Cargo binary loader naming --- scripts/ci/package_windows_portable.py | 8 +++--- scripts/ci/test_package_windows_portable.py | 28 +++++++++++++-------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/scripts/ci/package_windows_portable.py b/scripts/ci/package_windows_portable.py index a961d8d6..2dd0cb0b 100644 --- a/scripts/ci/package_windows_portable.py +++ b/scripts/ci/package_windows_portable.py @@ -91,7 +91,7 @@ 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_cargo_package_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, @@ -147,7 +147,7 @@ def load_tauri_config(project_root: pathlib.Path) -> dict: return json.loads(config_path.read_text(encoding="utf-8")) -def load_cargo_package_name(project_root: pathlib.Path) -> str: +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}") @@ -165,11 +165,11 @@ def load_cargo_package_name(project_root: pathlib.Path) -> str: package_table = cargo_data.get("package") if not isinstance(package_table, dict): - raise ValueError(f"Missing [package] in {CARGO_TOML_RELATIVE_PATH}") + 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_RELATIVE_PATH}") + raise ValueError(f"Missing [package].name in {cargo_toml_path}") return binary_name diff --git a/scripts/ci/test_package_windows_portable.py b/scripts/ci/test_package_windows_portable.py index b2b97e4b..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 @@ -137,16 +138,19 @@ def test_load_cargo_package_name_supports_inline_comments(self): ) self.assertEqual( - MODULE.load_cargo_package_name(project_root), + 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.assertRaises(FileNotFoundError): - MODULE.load_cargo_package_name(project_root) + 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: @@ -155,8 +159,8 @@ def test_load_cargo_package_name_missing_package_table_raises_value_error(self): cargo_toml_path.parent.mkdir(parents=True) cargo_toml_path.write_text('[workspace]\nmembers = ["crates/*"]\n') - with self.assertRaises(ValueError): - MODULE.load_cargo_package_name(project_root) + 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: @@ -165,8 +169,8 @@ def test_load_cargo_package_name_missing_package_name_raises_value_error(self): cargo_toml_path.parent.mkdir(parents=True) cargo_toml_path.write_text('[package]\nversion = "0.1.0"\n') - with self.assertRaises(ValueError): - MODULE.load_cargo_package_name(project_root) + 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: @@ -175,8 +179,8 @@ def test_load_cargo_package_name_empty_package_name_raises_value_error(self): cargo_toml_path.parent.mkdir(parents=True) cargo_toml_path.write_text('[package]\nname = ""\n') - with self.assertRaises(ValueError): - MODULE.load_cargo_package_name(project_root) + 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: @@ -191,7 +195,7 @@ def test_load_cargo_package_name_falls_back_to_package_when_bin_missing_name(sel ) self.assertEqual( - MODULE.load_cargo_package_name(project_root), + MODULE.load_binary_name_from_cargo(project_root), "astrbot-desktop-tauri", ) @@ -207,7 +211,9 @@ def test_load_cargo_package_name_prefers_explicit_bin_name(self): 'name = "AstrBot"\n' ) - self.assertEqual(MODULE.load_cargo_package_name(project_root), "AstrBot") + 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: