Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
44 changes: 44 additions & 0 deletions src-tauri/installer.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
; Tauri 2 NSIS installer hooks for the Windows ChaosEngineAI bundle.
;
; Tauri's default NSIS template installs the app under
; %LOCALAPPDATA%\<identifier>\ and the uninstaller removes that tree on
; uninstall. The GPU runtime bundle (torch + diffusers + transformers,
; ~2.5 GB) is intentionally written to a sibling directory:
;
; %LOCALAPPDATA%\ChaosEngineAI\extras\cp{major}{minor}\site-packages
;
; The path is namespaced by Python ABI tag (commit 24518af, v0.7.0-rc.5)
; so a runtime upgrade that changes Python minor versions cannot shadow
; the wheels from the previous tag.
;
; CRITICAL: this directory MUST survive an uninstall + reinstall cycle.
; Re-downloading 2.5 GB of CUDA wheels every time the user upgrades the
; desktop app is unacceptable, both for users on slow links and for the
; PyPI mirrors that serve the bundle.
;
; The hooks below are intentionally empty as a guardrail. If anyone
; later adds custom uninstall behaviour:
;
; 1. NEVER ``RMDir /r "$LOCALAPPDATA\ChaosEngineAI\extras"`` here.
; 2. Test that ``setup.py:_extras_site_packages()`` resolves the same
; path before AND after a clean uninstall + reinstall on Windows.
; 3. Mirror any change in ``src-tauri/src/lib.rs::chaosengine_extras_root``.

!macro NSIS_HOOK_PREINSTALL
; Reserved — currently a no-op. See contract above before adding code.
!macroend

!macro NSIS_HOOK_POSTINSTALL
; Reserved — currently a no-op. See contract above before adding code.
!macroend

!macro NSIS_HOOK_PREUNINSTALL
; Reserved — currently a no-op. See contract above before adding code.
!macroend

!macro NSIS_HOOK_POSTUNINSTALL
; Reserved — currently a no-op. The persistent GPU runtime tree at
; %LOCALAPPDATA%\ChaosEngineAI\extras MUST be left intact so an
; immediate reinstall can pick it up without re-downloading 2.5 GB.
; See contract above before adding code.
!macroend
6 changes: 6 additions & 0 deletions src-tauri/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -697,6 +697,12 @@ fn apply_embedded_runtime_env(command: &mut Command, runtime: &EmbeddedRuntime)
/// Returns ``None`` if we can't resolve a home directory at all (headless
/// environments). Callers treat that as "no extras available".
fn chaosengine_extras_root() -> Option<PathBuf> {
// The extras tree lives OUTSIDE the Tauri install directory so it
// survives uninstall + reinstall cycles — re-downloading the 2.5 GB
// GPU bundle on every desktop upgrade is unacceptable. The Windows
// NSIS installer is told to leave this path alone via the empty
// hooks in ``src-tauri/installer.nsh``; if anyone changes either
// side the other MUST be kept in sync.
let base = if cfg!(windows) {
env::var_os("LOCALAPPDATA")
.map(PathBuf::from)
Expand Down
5 changes: 5 additions & 0 deletions src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,11 @@
"hardenedRuntime": true,
"entitlements": "macos/ChaosEngineAI.entitlements"
},
"windows": {
"nsis": {
"installerHooks": "./installer.nsh"
}
},
"resources": {
"resources/": ""
}
Expand Down
9 changes: 9 additions & 0 deletions src/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -6269,6 +6269,15 @@ select.text-input {
width: 100%;
max-width: 100%;
overflow: hidden;
/* Establishes a stacking context so the streaming pip output stays
* above the Prompt + Recent Outputs cards in Image Studio and Video
* Studio. Without these the panel renders behind those siblings on
* Windows during a long GPU bundle install — the log is still alive
* but the user can't see it. ``z-index: 5`` is enough to win against
* the surrounding ``.panel`` cards (which set no z-index of their
* own) without fighting the global tooltip portal (z-index: 1000+). */
position: relative;
z-index: 5;
}
.install-log-summary {
cursor: pointer;
Expand Down
63 changes: 63 additions & 0 deletions tests/test_extras_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""Pin the persistent GPU extras path on Windows / Linux / macOS.

The desktop installer is configured to leave this directory alone on
uninstall (``src-tauri/installer.nsh`` on Windows; macOS uninstall is
``rm /Applications/ChaosEngineAI.app`` which doesn't touch
``~/Library/Application Support``). If the path computed by
``_extras_site_packages`` ever drifts from what the installer hooks
expect, the uninstall safety net breaks.

The tests below pin both halves of the contract — the parent directory
and the ABI tag layout — so any future move is loud.
"""

from __future__ import annotations

import os
import sys
import unittest
from pathlib import Path
from unittest import mock

from backend_service.routes import setup as setup_routes


class ExtrasSitePackagesTests(unittest.TestCase):
def setUp(self) -> None:
self._env_patcher = mock.patch.dict(os.environ, {}, clear=False)
self._env_patcher.start()
# Drop the override knob — the explicit env path is for tests
# that pin a custom location, not for the cross-OS shape check.
os.environ.pop("CHAOSENGINE_EXTRAS_SITE_PACKAGES", None)

def tearDown(self) -> None:
self._env_patcher.stop()

def test_path_includes_chaosengine_extras_namespace(self) -> None:
path = setup_routes._extras_site_packages()
self.assertIsNotNone(path)
assert path is not None # type narrow
parts = path.parts
# 'ChaosEngineAI/extras/cp{maj}{min}/site-packages' suffix.
# The tree above (LOCALAPPDATA / Library/Application Support /
# XDG_DATA_HOME) is platform-specific; we only assert the tail.
self.assertEqual(parts[-4], "ChaosEngineAI")
self.assertEqual(parts[-3], "extras")
self.assertTrue(parts[-2].startswith("cp"))
self.assertEqual(parts[-1], "site-packages")

def test_python_abi_tag_matches_runtime(self) -> None:
path = setup_routes._extras_site_packages()
assert path is not None
expected_tag = f"cp{sys.version_info.major}{sys.version_info.minor}"
self.assertEqual(path.parts[-2], expected_tag)

def test_env_override_wins(self) -> None:
override = "/tmp/chaosengine-extras-override"
os.environ["CHAOSENGINE_EXTRAS_SITE_PACKAGES"] = override
path = setup_routes._extras_site_packages()
self.assertEqual(path, Path(override))


if __name__ == "__main__":
unittest.main()
Loading