Skip to content

Commit 46398de

Browse files
committed
Add Windows installer build
1 parent da48aab commit 46398de

File tree

7 files changed

+103
-9
lines changed

7 files changed

+103
-9
lines changed

README.md

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ python3 victoria.py
9595
```
9696

9797
This gives you the most control and is the same across macOS, Linux, and Windows (PowerShell).
98+
All modes store configuration and data in `~/Victoria` (or `%USERPROFILE%\Victoria` on Windows).
9899

99100
#### Customizing the launch tool
100101

@@ -137,6 +138,39 @@ After that, it’s double-click and go.
137138

138139
---
139140

141+
## 📦 Packaging for macOS and Windows
142+
143+
You can build standalone packages so Victoria can be launched without a terminal.
144+
145+
### macOS `.app`
146+
147+
1. Install [PyInstaller](https://pyinstaller.org) (`pip install pyinstaller`).
148+
2. Replace `assets/icon.icns` with your desired app icon (ICNS format).
149+
3. Run:
150+
151+
```bash
152+
./package_mac.sh
153+
```
154+
155+
The bundle will be created at `dist/Victoria.app`.
156+
157+
### Windows `.exe` and Installer
158+
159+
1. Install PyInstaller (`pip install pyinstaller`) and [Inno Setup](https://jrsoftware.org/isinfo.php) (make sure `iscc` is on your PATH).
160+
2. Replace `assets\icon.ico` with your desired app icon (ICO format).
161+
3. Run `package_win.bat` from Command Prompt or PowerShell:
162+
163+
```powershell
164+
.\package_win.bat
165+
```
166+
167+
This produces both a standalone executable (`dist/Victoria.exe`) and an installer (`dist/VictoriaSetup.exe`).
168+
169+
To upgrade Victoria on Windows, build a new installer with an updated version number and run it; Inno Setup will replace the previous installation automatically while preserving the same AppId.
170+
Both packaged versions automatically use the `~/Victoria` folder (or `%USERPROFILE%\Victoria` on Windows) for configuration and data.
171+
172+
---
173+
140174
## 🧠 Models to Try
141175

142176
| Model | Price (\$/1M tokens) | Category | Best For |

assets/icon.icns

Whitespace-only changes.

assets/icon.ico

Whitespace-only changes.

installer_win.iss

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
#define MyAppName "Victoria"
2+
#define MyAppVersion "1.0.0"
3+
#define MyAppExeName "Victoria.exe"
4+
5+
[Setup]
6+
AppId={{8F6B2C2B-21CC-4D66-877B-0E7D12FA8E3F}}
7+
AppName={#MyAppName}
8+
AppVersion={#MyAppVersion}
9+
DefaultDirName={autopf}\{#MyAppName}
10+
DefaultGroupName={#MyAppName}
11+
UninstallDisplayIcon={app}\{#MyAppExeName}
12+
OutputDir=dist
13+
OutputBaseFilename=VictoriaSetup
14+
Compression=lzma
15+
SolidCompression=yes
16+
SetupIconFile=assets\icon.ico
17+
18+
[Files]
19+
Source: "dist\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion
20+
21+
[Icons]
22+
Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
23+
Name: "{commondesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"
24+
25+
[Run]
26+
Filename: "{app}\{#MyAppExeName}"; Description: "Launch Victoria"; Flags: nowait postinstall skipifsilent

package_mac.sh

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -e
3+
pyinstaller --noconfirm --windowed --name Victoria \
4+
--icon assets/icon.icns \
5+
--add-data "crush.template.json:." \
6+
--add-data "snowflake.mcp.json:." \
7+
--add-data ".crushignore:." \
8+
victoria.py

package_win.bat

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
@echo off
2+
pyinstaller --noconfirm --onefile --name Victoria ^
3+
--icon assets\icon.ico ^
4+
--add-data "crush.template.json;." ^
5+
--add-data "snowflake.mcp.json;." ^
6+
--add-data ".crushignore;." ^
7+
victoria.py
8+
REM Build installer with Inno Setup (iscc must be on PATH)
9+
iscc installer_win.iss

victoria.py

Lines changed: 26 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,20 @@
3535
SNOWFLAKE_FRAG = "snowflake.mcp.json"
3636
OUTPUT_CONFIG = os.environ.get("VICTORIA_OUTPUT", f"{TOOL_CMD}.json")
3737

38+
APP_HOME = Path.home() / "Victoria"
39+
APP_HOME.mkdir(exist_ok=True)
40+
41+
def resource_path(name: str) -> Path:
42+
base = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent))
43+
return base / name
44+
45+
def ensure_default_files():
46+
for fname in [CONFIG_TEMPLATE, SNOWFLAKE_FRAG, ".crushignore"]:
47+
src = resource_path(fname)
48+
dst = APP_HOME / fname
49+
if src.exists() and not dst.exists():
50+
shutil.copy(src, dst)
51+
3852
SNOWFLAKE_ENV_VARS = [
3953
"SNOWFLAKE_ACCOUNT",
4054
"SNOWFLAKE_USER",
@@ -538,15 +552,15 @@ def snowflake_env_missing() -> List[str]:
538552
return [v for v in SNOWFLAKE_ENV_VARS if not os.environ.get(v)]
539553

540554
def load_base_template() -> Dict[str, Any]:
541-
path = Path(CONFIG_TEMPLATE)
555+
path = APP_HOME / CONFIG_TEMPLATE
542556
if not path.exists():
543-
raise FileNotFoundError(f"Missing {CONFIG_TEMPLATE}")
557+
raise FileNotFoundError(f"Missing {CONFIG_TEMPLATE} in {APP_HOME}")
544558
return read_json(path)
545559

546560
def load_snowflake_fragment() -> Dict[str, Any]:
547-
path = Path(SNOWFLAKE_FRAG)
561+
path = APP_HOME / SNOWFLAKE_FRAG
548562
if not path.exists():
549-
raise FileNotFoundError(f"Missing {SNOWFLAKE_FRAG}")
563+
raise FileNotFoundError(f"Missing {SNOWFLAKE_FRAG} in {APP_HOME}")
550564
return read_json(path)
551565

552566
def build_config(include_snowflake: bool, strict_env: bool) -> Dict[str, Any]:
@@ -564,8 +578,9 @@ def generate_config(include_snowflake: bool) -> bool:
564578
try:
565579
ship_loading_animation("Generating navigation configuration", 2.0)
566580
cfg = build_config(include_snowflake, strict_env=include_snowflake)
567-
write_json(Path(OUTPUT_CONFIG), cfg)
568-
success_animation(f"Configuration written to {OUTPUT_CONFIG}")
581+
out_path = APP_HOME / OUTPUT_CONFIG
582+
write_json(out_path, cfg)
583+
success_animation(f"Configuration written to {out_path}")
569584
return True
570585
except Exception as ex:
571586
err(f"Configuration generation failed: {ex}")
@@ -612,13 +627,14 @@ def launch_tool():
612627
print(f"\n{T.GREEN}{T.TARGET} Launching Victoria Data Navigator{T.NC}")
613628

614629
try:
630+
cmd = [TOOL_CMD, "-y", "-c", str(APP_HOME)]
615631
if os.name == "nt":
616-
proc = subprocess.run([TOOL_CMD])
632+
proc = subprocess.run(cmd)
617633
if proc.returncode != 0:
618634
err(f"{TOOL_CMD} exited with error code {proc.returncode}")
619635
sys.exit(proc.returncode)
620636
else:
621-
os.execvp(TOOL_CMD, [TOOL_CMD])
637+
os.execvp(TOOL_CMD, cmd)
622638
except FileNotFoundError:
623639
err(f"'{TOOL_CMD}' command not found in PATH")
624640
sys.exit(1)
@@ -659,7 +675,7 @@ def course_menu() -> str:
659675
# ------------------ Main ------------------
660676
def remove_local_duckdb():
661677
"""Remove local DuckDB file to ensure a clean start."""
662-
db_path = Path("data") / "adtech.duckdb"
678+
db_path = APP_HOME / "adtech.duckdb"
663679
try:
664680
if db_path.exists():
665681
db_path.unlink()
@@ -670,6 +686,7 @@ def remove_local_duckdb():
670686
warn(f"Could not remove {db_path}: {e}")
671687

672688
def main():
689+
ensure_default_files()
673690
clear_screen()
674691
banner()
675692
remove_local_duckdb()

0 commit comments

Comments
 (0)