Skip to content

fix: use Rust binary name for portable packaging#108

Merged
zouyonghe merged 4 commits intoAstrBotDevs:mainfrom
zouyonghe:codex/windows-portable-binary-name
Apr 1, 2026
Merged

fix: use Rust binary name for portable packaging#108
zouyonghe merged 4 commits intoAstrBotDevs:mainfrom
zouyonghe:codex/windows-portable-binary-name

Conversation

@zouyonghe
Copy link
Copy Markdown
Member

@zouyonghe zouyonghe commented Apr 1, 2026

Summary

  • fix the Windows portable packaging step to use the actual Rust release binary name instead of the Tauri product name
  • add regression tests covering the case where productName differs from the built .exe filename

Problem

After PR #107 merged, the Windows portable zip step still failed in GitHub Actions. The portable packager looked for src-tauri/target/release/AstrBot.exe because it derived the executable name from productName, but the Windows build actually produces src-tauri/target/release/astrbot-desktop-tauri.exe.

Root Cause

The packaging script conflated the user-facing Tauri productName with the real Rust package/binary name. On Windows those are different in this repository, so the portable packaging step could never find the built executable in CI.

Fix

This change teaches scripts/ci/package_windows_portable.py to read the Rust package name from src-tauri/Cargo.toml and use that value when locating the built release executable. The existing productName handling stays intact for display-oriented metadata, while the actual file copy now targets the real binary path.

The tests were extended to cover the exact failing case: productName = AstrBot while the built executable is astrbot-desktop-tauri.exe. That locks in the expected behavior for future packaging changes.

Validation

  • python3 -m unittest scripts.ci.test_package_windows_portable
  • python3 -m unittest discover -s scripts/ci -p 'test_*.py'
  • python3 -m compileall -q scripts
  • node --test $(find scripts -type f -name '*.test.mjs' | sort)

Summary by Sourcery

Use the Rust binary name from Cargo.toml for Windows portable packaging instead of the Tauri product name.

Bug Fixes:

  • Fix Windows portable packaging failing when the Tauri productName differs from the actual built .exe filename by resolving the binary name from Cargo.toml.

Tests:

  • Add unit tests for loading the binary name from Cargo.toml, including various error conditions and bin-table precedence, and for using the binary name when resolving the main executable path for portable packaging.

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In load_cargo_package_name, consider replacing the ad-hoc text parsing with Python's tomllib (or a TOML parser) so that Cargo.toml formatting variations (whitespace, reordering, inline comments) and more complex configs are handled robustly.
  • The current approach assumes the Windows binary name always matches [package].name; if the project later uses a different [[bin]] target name, this will fail, so it may be worth allowing an override or explicitly handling [[bin]] entries when resolving the executable name.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `load_cargo_package_name`, consider replacing the ad-hoc text parsing with Python's `tomllib` (or a TOML parser) so that `Cargo.toml` formatting variations (whitespace, reordering, inline comments) and more complex configs are handled robustly.
- The current approach assumes the Windows binary name always matches `[package].name`; if the project later uses a different `[[bin]]` target name, this will fail, so it may be worth allowing an override or explicitly handling `[[bin]]` entries when resolving the executable name.

## Individual Comments

### Comment 1
<location path="scripts/ci/package_windows_portable.py" line_range="149-158" />
<code_context>
     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
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Consider using a TOML parser instead of manual line parsing for Cargo.toml.

Manual parsing here is likely to break on valid TOML, e.g. `name = "foo" # comment`, varied spacing, or future constructs like multiline strings. Using a TOML library (`tomllib` on 3.11+ or `toml`) would be more robust: parse once and read `data["package"]["name"]` instead of iterating lines and tracking section state yourself.

Suggested implementation:

```python
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}")

    # Parse Cargo.toml using a TOML parser instead of manual line parsing
    # to robustly handle comments, spacing, and other valid TOML constructs.
    try:
        import tomllib  # Python 3.11+
    except ModuleNotFoundError:
        # Fallback to the third‑party "toml" package if tomllib is unavailable.
        import toml as tomllib  # type: ignore[import]

    with cargo_toml_path.open("rb") as f:
        cargo_data = tomllib.load(f)

    try:
        package_table = cargo_data["package"]
        name_value = package_table["name"]
    except KeyError as exc:
        raise ValueError(f"Missing [package].name in {CARGO_TOML_RELATIVE_PATH}") from exc

    binary_name = str(name_value).strip()
    if not binary_name:
        raise ValueError(f"Empty [package].name in {CARGO_TOML_RELATIVE_PATH}")

    return binary_name

```

1. If this project guarantees Python 3.11+ in CI, you can simplify the function by removing the `try/except` around the `tomllib` import and the `toml` fallback.
2. If you rely on the fallback, ensure `toml` is listed in the appropriate dependency file for your CI environment (e.g. `requirements.txt`, `pyproject.toml`, or the CI configuration’s install step).
3. If the file already has a module-level import policy (e.g. all imports at top of file), you may want to move the `tomllib`/`toml` import block up to the top-level imports to match existing style.
</issue_to_address>

### Comment 2
<location path="scripts/ci/package_windows_portable.py" line_range="162-164" />
<code_context>
+        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
</code_context>
<issue_to_address>
**issue (bug_risk):** Current value extraction will mis-handle `name` lines with inline comments.

When `name` has an inline comment (e.g. `name = "foo" # main binary`), `value.strip().strip('"').strip("'")` leaves the comment in `binary_name` (e.g. `foo" # main binary`). Consider stripping the inline comment before trimming quotes, e.g. split on `#` first, then `strip()` and remove quotes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request updates the Windows portable packaging script to use the binary name defined in Cargo.toml instead of the product name from the Tauri configuration when locating the main executable. It introduces a new function, load_cargo_package_name, to parse the package name from the project's manifest and updates the ProjectConfig structure and associated tests accordingly. Feedback was provided regarding the manual parsing logic in load_cargo_package_name, which is currently fragile and may incorrectly handle keys or trailing comments; a more robust parsing approach was suggested.

Comment on lines +162 to +167
if package_section and line.startswith("name"):
_, _, value = line.partition("=")
binary_name = value.strip().strip('"').strip("'")
if binary_name:
return binary_name
break
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current manual parsing of Cargo.toml is fragile. It can incorrectly match keys that merely start with name (e.g., namesake), and it fails to correctly extract the value if there are trailing comments on the same line (e.g., name = "foo" # comment).

Consider a more robust approach to identify the name key and strip potential comments.

Suggested change
if package_section and line.startswith("name"):
_, _, value = line.partition("=")
binary_name = value.strip().strip('"').strip("'")
if binary_name:
return binary_name
break
if package_section and "=" in line:
key, value = map(str.strip, line.split("=", 1))
if key == "name":
# Extract value, removing potential trailing comments and quotes
binary_name = value.split("#", 1)[0].strip().strip('"').strip("'")
if binary_name:
return binary_name
break

@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="scripts/ci/test_package_windows_portable.py" line_range="130-144" />
<code_context>
+            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",
</code_context>
<issue_to_address>
**suggestion (testing):** Add tests for `load_cargo_package_name` error paths and fallback behavior

The new tests cover the happy path, but the function’s error and fallback branches are still untested:

- Missing `Cargo.toml``FileNotFoundError`
- Missing `[package]` table → `ValueError`
- `[package]` present but missing/empty `name``ValueError`
- `[[bin]]` present but empty or without `name` → should fall back to `[package].name`

Please add unit tests that mirror the existing tempdir setup to assert the correct exception types (and optionally messages) for the error cases, and to confirm the fallback returns the package name when `[[bin]]` entries lack `name`.

```suggestion
    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_missing_cargo_toml_raises_file_not_found(self):
        with tempfile.TemporaryDirectory() as tmpdir:
            project_root = Path(tmpdir)
            # Intentionally DO NOT create src-tauri/Cargo.toml

            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)
            # No [package] table at all
            cargo_toml_path.write_text(
                '[workspace]\n'
                'members = ["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)
            # [package] table present but missing name
            cargo_toml_path.write_text(
                "[package]\n"
                'version = "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)
            # [package].name present but empty
            cargo_toml_path.write_text(
                "[package]\n"
                'name = ""\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)
            # [[bin]] table exists but has no name; should fall back to package.name
            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):
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +130 to +144
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):
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Add tests for load_cargo_package_name error paths and fallback behavior

The new tests cover the happy path, but the function’s error and fallback branches are still untested:

  • Missing Cargo.tomlFileNotFoundError
  • Missing [package] table → ValueError
  • [package] present but missing/empty nameValueError
  • [[bin]] present but empty or without name → should fall back to [package].name

Please add unit tests that mirror the existing tempdir setup to assert the correct exception types (and optionally messages) for the error cases, and to confirm the fallback returns the package name when [[bin]] entries lack name.

Suggested change
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):
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_missing_cargo_toml_raises_file_not_found(self):
with tempfile.TemporaryDirectory() as tmpdir:
project_root = Path(tmpdir)
# Intentionally DO NOT create src-tauri/Cargo.toml
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)
# No [package] table at all
cargo_toml_path.write_text(
'[workspace]\n'
'members = ["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)
# [package] table present but missing name
cargo_toml_path.write_text(
"[package]\n"
'version = "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)
# [package].name present but empty
cargo_toml_path.write_text(
"[package]\n"
'name = ""\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)
# [[bin]] table exists but has no name; should fall back to package.name
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):

@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • The new load_cargo_package_name function can return a [[bin]].name that differs from the package name, so the name and docstring are a bit misleading now; consider renaming it (and associated variables) to something like load_binary_name_from_cargo to reflect its behavior more accurately.
  • In load_cargo_package_name, the error messages reference CARGO_TOML_RELATIVE_PATH instead of the resolved cargo_toml_path, which could make failures harder to debug when the script is run from different locations; consider including the full path in those messages for clarity.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The new `load_cargo_package_name` function can return a `[[bin]].name` that differs from the package name, so the name and docstring are a bit misleading now; consider renaming it (and associated variables) to something like `load_binary_name_from_cargo` to reflect its behavior more accurately.
- In `load_cargo_package_name`, the error messages reference `CARGO_TOML_RELATIVE_PATH` instead of the resolved `cargo_toml_path`, which could make failures harder to debug when the script is run from different locations; consider including the full path in those messages for clarity.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe
Copy link
Copy Markdown
Member Author

@sourcery-ai review

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've reviewed your changes and they look great!


Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@zouyonghe zouyonghe merged commit 208b5e7 into AstrBotDevs:main Apr 1, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant