Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 8 additions & 5 deletions scripts/ci/package_windows_portable.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,12 @@
"""
TAURI_CONFIG_RELATIVE_PATH = pathlib.Path("src-tauri") / "tauri.conf.json"
CARGO_TOML_RELATIVE_PATH = pathlib.Path("src-tauri") / "Cargo.toml"
# These point to the source resource directories inside the repository checkout.
BACKEND_RESOURCE_RELATIVE_PATH = pathlib.Path("resources") / "backend"
WEBUI_RESOURCE_RELATIVE_PATH = pathlib.Path("resources") / "webui"
# These are the runtime-visible locations emitted into the portable package root.
PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH = pathlib.Path("backend")
PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH = pathlib.Path("webui")
WINDOWS_CLEANUP_SCRIPT_RELATIVE_PATH = (
pathlib.Path("src-tauri") / "windows" / "kill-backend-processes.ps1"
)
Expand Down Expand Up @@ -263,16 +267,15 @@ def populate_portable_root(
if cleanup_script.is_file():
shutil.copy2(cleanup_script, destination_root / "kill-backend-processes.ps1")

resources_root = destination_root / "resources"
backend_src = project_config.root / BACKEND_RESOURCE_RELATIVE_PATH
if not backend_src.is_dir():
raise FileNotFoundError(f"Required directory not found: {backend_src}")
shutil.copytree(backend_src, resources_root / "backend")
shutil.copytree(backend_src, destination_root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH)
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

When packaging the backend resources, it is a good practice to exclude development artifacts like __pycache__ and compiled Python files (.pyc, .pyo). These files are not necessary for the portable runtime and can bloat the package or cause issues if they were generated with a different Python version.

Suggested change
shutil.copytree(backend_src, destination_root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH)
shutil.copytree(
backend_src,
destination_root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH,
ignore=shutil.ignore_patterns("__pycache__", "*.pyc", "*.pyo")
)


webui_src = project_config.root / WEBUI_RESOURCE_RELATIVE_PATH
if not webui_src.is_dir():
raise FileNotFoundError(f"Required directory not found: {webui_src}")
shutil.copytree(webui_src, resources_root / "webui")
shutil.copytree(webui_src, destination_root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH)

add_portable_runtime_files(destination_root, project_config)
validate_portable_root(destination_root)
Expand All @@ -292,8 +295,8 @@ def add_portable_runtime_files(

def validate_portable_root(destination_root: pathlib.Path) -> None:
expected_paths = [
destination_root / "resources" / "backend" / "runtime-manifest.json",
destination_root / "resources" / "webui" / "index.html",
destination_root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH / "runtime-manifest.json",
destination_root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH / "index.html",
]
missing = [
str(path.relative_to(destination_root))
Expand Down
35 changes: 25 additions & 10 deletions scripts/ci/test_package_windows_portable.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@

from scripts.ci import package_windows_portable as MODULE

PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH = MODULE.PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH
PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH = MODULE.PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH


class PackageWindowsPortableTests(unittest.TestCase):
def make_project_layout(
Expand Down Expand Up @@ -424,12 +427,16 @@ def test_populate_portable_root_copies_release_bundle_contents(self):
self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
self.assertTrue(
(
destination_root / "resources" / "backend" / "runtime-manifest.json"
destination_root
/ PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH
/ "runtime-manifest.json"
).is_file()
)
self.assertTrue(
(destination_root / "resources" / "webui" / "index.html").is_file()
(destination_root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH / "index.html").is_file()
)
self.assertFalse((destination_root / "resources" / "backend").exists())
self.assertFalse((destination_root / "resources" / "webui").exists())
self.assertTrue((destination_root / "kill-backend-processes.ps1").is_file())
self.assertTrue((destination_root / "portable.flag").is_file())
self.assertTrue((destination_root / MODULE.PORTABLE_README_NAME).is_file())
Comment on lines 487 to 507
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): Assert that the legacy resources/backend and resources/webui paths are not created anymore

Please also have this test assert that the old resources/backend and resources/webui directories do not exist (e.g. self.assertFalse((destination_root / "resources" / "backend").exists()) and similarly for webui). This will catch any future change that mistakenly writes files to both the old and new layouts.

Suggested change
self.assertFalse((destination_root / "astrbot-desktop-tauri.exe").exists())
self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
self.assertTrue(
(
destination_root / "resources" / "backend" / "runtime-manifest.json"
).is_file()
)
self.assertTrue(
(destination_root / "resources" / "webui" / "index.html").is_file()
(destination_root / "backend" / "runtime-manifest.json").is_file()
)
self.assertTrue((destination_root / "webui" / "index.html").is_file())
self.assertTrue((destination_root / "kill-backend-processes.ps1").is_file())
self.assertTrue((destination_root / "portable.flag").is_file())
self.assertTrue((destination_root / MODULE.PORTABLE_README_NAME).is_file())
self.assertFalse((destination_root / "astrbot-desktop-tauri.exe").exists())
self.assertTrue((destination_root / "WebView2Loader.dll").is_file())
self.assertTrue(
(destination_root / "backend" / "runtime-manifest.json").is_file()
)
self.assertTrue((destination_root / "webui" / "index.html").is_file())
# Legacy layout paths must no longer be created
self.assertFalse((destination_root / "resources" / "backend").exists())
self.assertFalse((destination_root / "resources" / "webui").exists())
self.assertTrue((destination_root / "kill-backend-processes.ps1").is_file())
self.assertTrue((destination_root / "portable.flag").is_file())
self.assertTrue((destination_root / MODULE.PORTABLE_README_NAME).is_file())

Expand Down Expand Up @@ -480,10 +487,14 @@ def test_validate_portable_root_accepts_expected_layout(self):
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "AstrBot.exe").write_text("binary")
(root / "resources" / "backend").mkdir(parents=True)
(root / "resources" / "webui").mkdir(parents=True)
(root / "resources" / "backend" / "runtime-manifest.json").write_text("{}")
(root / "resources" / "webui" / "index.html").write_text("<html></html>")
(root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH).mkdir(parents=True)
(root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH).mkdir(parents=True)
(
Comment on lines 553 to +562
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 a validation test where backend/webui aliases are nested (e.g. runtime/backend)

The current test_validate_portable_root_* cases still only cover the default backend / webui layout. Since load_project_config_from now reads aliases from bundle.resources, please add an end-to-end validation test using non-trivial aliases (e.g. runtime/backend, runtime/webui). For example, call make_project_layout with tauri_resources mapping "../resources/backend" → "runtime/backend" and "../resources/webui" → "runtime/webui", build a portable root using those nested paths, and assert that MODULE.validate_portable_root(root, project_config) passes without requiring resources/backend or resources/webui directories. This will verify that validate_portable_root respects non-root aliases and protect against regressions that assume fixed backend/ / webui/ locations.

Suggested implementation:

            MODULE.add_portable_runtime_files(root, project_config)

    def test_validate_portable_root_accepts_expected_layout(self):
        layout = self.make_project_layout()
        project_config = MODULE.load_project_config_from(layout["script_path"])

        with tempfile.TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            # default layout: backend/ and webui/ at the portable root
            (root / "AstrBot.exe").write_text("binary")
            (root / "portable.flag").write_text("marker")
            (root / "backend").mkdir()
            (root / "webui").mkdir()

            MODULE.validate_portable_root(root, project_config)

    def test_validate_portable_root_accepts_nested_alias_layout(self):
        layout = self.make_project_layout(
            tauri_resources={
                "../resources/backend": "runtime/backend",
                "../resources/webui": "runtime/webui",
            }
        )
        project_config = MODULE.load_project_config_from(layout["script_path"])

        with tempfile.TemporaryDirectory() as tmpdir:
            root = Path(tmpdir)
            # binary + portable marker at root
            (root / "AstrBot.exe").write_text("binary")
            (root / "portable.flag").write_text("marker")

            # resources are placed under their aliased nested locations
            (root / "runtime" / "backend").mkdir(parents=True)
            (root / "runtime" / "webui").mkdir(parents=True)

            # Intentionally do NOT create root-level backend/ or webui/ directories.
            # validate_portable_root should honor the aliased paths instead.
            MODULE.validate_portable_root(root, project_config)

If make_project_layout uses a different keyword than tauri_resources for the resources mapping, update the argument name in test_validate_portable_root_accepts_nested_alias_layout accordingly.
If validate_portable_root requires additional files or directories beyond those shown (for example, specific backend/webui content rather than just directories), mirror whatever the other test_validate_portable_root_* tests create under the aliased runtime/backend and runtime/webui paths so this test exercises the same invariants with nested aliases instead of the default layout.

root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH / "runtime-manifest.json"
).write_text("{}")
(root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH / "index.html").write_text(
"<html></html>"
)

MODULE.validate_portable_root(root)

Expand All @@ -498,10 +509,14 @@ def test_validate_portable_root_requires_expected_files(self):
def test_validate_portable_root_requires_top_level_exe(self):
with tempfile.TemporaryDirectory() as tmpdir:
root = Path(tmpdir)
(root / "resources" / "backend").mkdir(parents=True)
(root / "resources" / "webui").mkdir(parents=True)
(root / "resources" / "backend" / "runtime-manifest.json").write_text("{}")
(root / "resources" / "webui" / "index.html").write_text("<html></html>")
(root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH).mkdir(parents=True)
(root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH).mkdir(parents=True)
(
root / PORTABLE_BACKEND_LAYOUT_RELATIVE_PATH / "runtime-manifest.json"
).write_text("{}")
(root / PORTABLE_WEBUI_LAYOUT_RELATIVE_PATH / "index.html").write_text(
"<html></html>"
)

with self.assertRaisesRegex(ValueError, r"top-level \*\.exe"):
MODULE.validate_portable_root(root)
Expand Down