Skip to content

Commit 208b5e7

Browse files
authored
fix: use Rust binary name for portable packaging (#108)
* fix(ci): use Rust binary name for portable packaging * fix(ci): parse Cargo.toml with tomllib * test(ci): cover Cargo.toml parser edge cases * refactor(ci): clarify Cargo binary loader naming
1 parent 2f32990 commit 208b5e7

2 files changed

Lines changed: 154 additions & 3 deletions

File tree

scripts/ci/package_windows_portable.py

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import re
1010
import shutil
1111
import tempfile
12+
import tomllib
1213
from typing import Iterable
1314

1415
from scripts.ci.lib.artifact_arch import normalize_arch_alias
@@ -31,6 +32,7 @@
3132
- Microsoft Edge WebView2 Runtime must already be installed on this Windows machine.
3233
"""
3334
TAURI_CONFIG_RELATIVE_PATH = pathlib.Path("src-tauri") / "tauri.conf.json"
35+
CARGO_TOML_RELATIVE_PATH = pathlib.Path("src-tauri") / "Cargo.toml"
3436
BACKEND_RESOURCE_RELATIVE_PATH = pathlib.Path("resources") / "backend"
3537
WEBUI_RESOURCE_RELATIVE_PATH = pathlib.Path("resources") / "webui"
3638
WINDOWS_CLEANUP_SCRIPT_RELATIVE_PATH = (
@@ -45,6 +47,7 @@
4547
class ProjectConfig:
4648
root: pathlib.Path
4749
product_name: str
50+
binary_name: str
4851
portable_marker_name: str
4952

5053

@@ -88,10 +91,12 @@ def load_portable_runtime_marker(project_root: pathlib.Path) -> str:
8891
def load_project_config_from(start_path: pathlib.Path) -> ProjectConfig:
8992
project_root = resolve_project_root_from(start_path)
9093
product_name = resolve_product_name(project_root)
94+
binary_name = load_binary_name_from_cargo(project_root)
9195
portable_marker_name = load_portable_runtime_marker(project_root)
9296
return ProjectConfig(
9397
root=project_root,
9498
product_name=product_name,
99+
binary_name=binary_name,
95100
portable_marker_name=portable_marker_name,
96101
)
97102

@@ -142,6 +147,33 @@ def load_tauri_config(project_root: pathlib.Path) -> dict:
142147
return json.loads(config_path.read_text(encoding="utf-8"))
143148

144149

150+
def load_binary_name_from_cargo(project_root: pathlib.Path) -> str:
151+
cargo_toml_path = project_root / CARGO_TOML_RELATIVE_PATH
152+
if not cargo_toml_path.is_file():
153+
raise FileNotFoundError(f"Cargo.toml not found: {cargo_toml_path}")
154+
155+
with cargo_toml_path.open("rb") as handle:
156+
cargo_data = tomllib.load(handle)
157+
158+
bins = cargo_data.get("bin")
159+
if isinstance(bins, list):
160+
for entry in bins:
161+
if isinstance(entry, dict):
162+
binary_name = str(entry.get("name", "")).strip()
163+
if binary_name:
164+
return binary_name
165+
166+
package_table = cargo_data.get("package")
167+
if not isinstance(package_table, dict):
168+
raise ValueError(f"Missing [package] in {cargo_toml_path}")
169+
170+
binary_name = str(package_table.get("name", "")).strip()
171+
if not binary_name:
172+
raise ValueError(f"Missing [package].name in {cargo_toml_path}")
173+
174+
return binary_name
175+
176+
145177
def resolve_product_name(project_root: pathlib.Path) -> str:
146178
config = load_tauri_config(project_root)
147179
product_name = str(config.get("productName", "")).strip()
@@ -158,7 +190,7 @@ def resolve_main_executable_path(
158190
bundle_dir: pathlib.Path, project_config: ProjectConfig
159191
) -> pathlib.Path:
160192
release_dir = resolve_release_dir(bundle_dir)
161-
main_executable_path = release_dir / f"{project_config.product_name}.exe"
193+
main_executable_path = release_dir / f"{project_config.binary_name}.exe"
162194
if not main_executable_path.is_file():
163195
raise FileNotFoundError(f"Main executable not found: {main_executable_path}")
164196
return main_executable_path

scripts/ci/test_package_windows_portable.py

Lines changed: 121 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import re
12
import tempfile
23
import unittest
34
from pathlib import Path
@@ -109,21 +110,134 @@ def test_load_project_config_from_returns_root_product_and_marker(self):
109110
project_root / "scripts" / "ci" / "package_windows_portable.py"
110111
)
111112
tauri_config_path = project_root / "src-tauri" / "tauri.conf.json"
113+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
112114
marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH
113115

114116
script_path.parent.mkdir(parents=True)
115117
script_path.write_text("# placeholder")
116118
tauri_config_path.parent.mkdir(parents=True)
117119
tauri_config_path.write_text('{"productName":"AstrBot"}')
120+
cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n')
118121
marker_path.parent.mkdir(parents=True, exist_ok=True)
119122
marker_path.write_text("portable.flag\n")
120123

121124
project_config = MODULE.load_project_config_from(script_path)
122125

123126
self.assertEqual(project_config.root, project_root.resolve())
124127
self.assertEqual(project_config.product_name, "AstrBot")
128+
self.assertEqual(project_config.binary_name, "astrbot-desktop-tauri")
125129
self.assertEqual(project_config.portable_marker_name, "portable.flag")
126130

131+
def test_load_cargo_package_name_supports_inline_comments(self):
132+
with tempfile.TemporaryDirectory() as tmpdir:
133+
project_root = Path(tmpdir)
134+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
135+
cargo_toml_path.parent.mkdir(parents=True)
136+
cargo_toml_path.write_text(
137+
'[package]\nname = "astrbot-desktop-tauri" # main binary\n'
138+
)
139+
140+
self.assertEqual(
141+
MODULE.load_binary_name_from_cargo(project_root),
142+
"astrbot-desktop-tauri",
143+
)
144+
145+
def test_load_cargo_package_name_missing_cargo_toml_raises_file_not_found(self):
146+
with tempfile.TemporaryDirectory() as tmpdir:
147+
project_root = Path(tmpdir)
148+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
149+
150+
with self.assertRaisesRegex(
151+
FileNotFoundError, re.escape(str(cargo_toml_path))
152+
):
153+
MODULE.load_binary_name_from_cargo(project_root)
154+
155+
def test_load_cargo_package_name_missing_package_table_raises_value_error(self):
156+
with tempfile.TemporaryDirectory() as tmpdir:
157+
project_root = Path(tmpdir)
158+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
159+
cargo_toml_path.parent.mkdir(parents=True)
160+
cargo_toml_path.write_text('[workspace]\nmembers = ["crates/*"]\n')
161+
162+
with self.assertRaisesRegex(ValueError, re.escape(str(cargo_toml_path))):
163+
MODULE.load_binary_name_from_cargo(project_root)
164+
165+
def test_load_cargo_package_name_missing_package_name_raises_value_error(self):
166+
with tempfile.TemporaryDirectory() as tmpdir:
167+
project_root = Path(tmpdir)
168+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
169+
cargo_toml_path.parent.mkdir(parents=True)
170+
cargo_toml_path.write_text('[package]\nversion = "0.1.0"\n')
171+
172+
with self.assertRaisesRegex(ValueError, re.escape(str(cargo_toml_path))):
173+
MODULE.load_binary_name_from_cargo(project_root)
174+
175+
def test_load_cargo_package_name_empty_package_name_raises_value_error(self):
176+
with tempfile.TemporaryDirectory() as tmpdir:
177+
project_root = Path(tmpdir)
178+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
179+
cargo_toml_path.parent.mkdir(parents=True)
180+
cargo_toml_path.write_text('[package]\nname = ""\n')
181+
182+
with self.assertRaisesRegex(ValueError, re.escape(str(cargo_toml_path))):
183+
MODULE.load_binary_name_from_cargo(project_root)
184+
185+
def test_load_cargo_package_name_falls_back_to_package_when_bin_missing_name(self):
186+
with tempfile.TemporaryDirectory() as tmpdir:
187+
project_root = Path(tmpdir)
188+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
189+
cargo_toml_path.parent.mkdir(parents=True)
190+
cargo_toml_path.write_text(
191+
"[package]\n"
192+
'name = "astrbot-desktop-tauri"\n\n'
193+
"[[bin]]\n"
194+
'path = "src/main.rs"\n'
195+
)
196+
197+
self.assertEqual(
198+
MODULE.load_binary_name_from_cargo(project_root),
199+
"astrbot-desktop-tauri",
200+
)
201+
202+
def test_load_cargo_package_name_prefers_explicit_bin_name(self):
203+
with tempfile.TemporaryDirectory() as tmpdir:
204+
project_root = Path(tmpdir)
205+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
206+
cargo_toml_path.parent.mkdir(parents=True)
207+
cargo_toml_path.write_text(
208+
"[package]\n"
209+
'name = "astrbot-desktop-tauri"\n\n'
210+
"[[bin]]\n"
211+
'name = "AstrBot"\n'
212+
)
213+
214+
self.assertEqual(
215+
MODULE.load_binary_name_from_cargo(project_root), "AstrBot"
216+
)
217+
218+
def test_resolve_main_executable_path_uses_binary_name_not_product_name(self):
219+
with tempfile.TemporaryDirectory() as tmpdir:
220+
project_root = Path(tmpdir)
221+
bundle_dir = (
222+
project_root / "src-tauri" / "target" / "release" / "bundle" / "nsis"
223+
)
224+
release_dir = project_root / "src-tauri" / "target" / "release"
225+
bundle_dir.mkdir(parents=True)
226+
release_dir.mkdir(parents=True, exist_ok=True)
227+
(release_dir / "astrbot-desktop-tauri.exe").write_text("exe")
228+
229+
project_config = MODULE.ProjectConfig(
230+
root=project_root,
231+
product_name="AstrBot",
232+
binary_name="astrbot-desktop-tauri",
233+
portable_marker_name="portable.flag",
234+
)
235+
236+
self.assertEqual(
237+
MODULE.resolve_main_executable_path(bundle_dir, project_config),
238+
release_dir / "astrbot-desktop-tauri.exe",
239+
)
240+
127241
def test_iter_installer_paths_only_returns_installer_style_executables(self):
128242
with tempfile.TemporaryDirectory() as tmpdir:
129243
bundle_dir = Path(tmpdir)
@@ -156,6 +270,7 @@ def test_populate_portable_root_copies_release_bundle_contents(self):
156270
webui_dir = project_root / "resources" / "webui"
157271
windows_dir = project_root / "src-tauri" / "windows"
158272
tauri_config_path = project_root / "src-tauri" / "tauri.conf.json"
273+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
159274
marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH
160275

161276
script_path.parent.mkdir(parents=True)
@@ -167,8 +282,9 @@ def test_populate_portable_root_copies_release_bundle_contents(self):
167282
windows_dir.mkdir(parents=True)
168283

169284
tauri_config_path.write_text('{"productName":"AstrBot"}')
285+
cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n')
170286
marker_path.write_text("portable.flag\n")
171-
(release_dir / "AstrBot.exe").write_text("exe")
287+
(release_dir / "astrbot-desktop-tauri.exe").write_text("exe")
172288
(release_dir / "WebView2Loader.dll").write_text("dll")
173289
(backend_dir / "runtime-manifest.json").write_text("{}")
174290
(backend_dir / "launch_backend.py").write_text("print('ok')")
@@ -183,7 +299,7 @@ def test_populate_portable_root_copies_release_bundle_contents(self):
183299
project_config=MODULE.load_project_config_from(script_path),
184300
)
185301

186-
self.assertTrue((destination_root / "AstrBot.exe").is_file())
302+
self.assertTrue((destination_root / "astrbot-desktop-tauri.exe").is_file())
187303
self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
188304
self.assertTrue(
189305
(
@@ -210,6 +326,7 @@ def test_populate_portable_root_rejects_missing_main_executable(self):
210326
backend_dir = project_root / "resources" / "backend"
211327
webui_dir = project_root / "resources" / "webui"
212328
tauri_config_path = project_root / "src-tauri" / "tauri.conf.json"
329+
cargo_toml_path = project_root / "src-tauri" / "Cargo.toml"
213330
marker_path = project_root / MODULE.PORTABLE_RUNTIME_MARKER_RELATIVE_PATH
214331

215332
script_path.parent.mkdir(parents=True)
@@ -219,6 +336,7 @@ def test_populate_portable_root_rejects_missing_main_executable(self):
219336
webui_dir.mkdir(parents=True)
220337
tauri_config_path.parent.mkdir(parents=True, exist_ok=True)
221338
tauri_config_path.write_text('{"productName":"AstrBot"}')
339+
cargo_toml_path.write_text('[package]\nname = "astrbot-desktop-tauri"\n')
222340
marker_path.parent.mkdir(parents=True, exist_ok=True)
223341
marker_path.write_text("portable.flag\n")
224342
(backend_dir / "runtime-manifest.json").write_text("{}")
@@ -237,6 +355,7 @@ def test_add_portable_runtime_files_writes_marker_and_readme(self):
237355
project_config = MODULE.ProjectConfig(
238356
root=Path(tmpdir),
239357
product_name="AstrBot",
358+
binary_name="astrbot-desktop-tauri",
240359
portable_marker_name="portable.flag",
241360
)
242361

0 commit comments

Comments
 (0)