Restructure app-desktop as self-contained package, add PyInstaller build
Consolidates everything the desktop app depends on into app-desktop/ so it packages cleanly as a standalone Windows exe: - app-desktop-macos/ -> app-desktop/src/ (drop macOS-only naming, app now targets Windows via PyInstaller too) - remote-server/ws_server.py and app-presets/ moved into app-desktop/src/ (both were exclusive dependencies of main.py/presets.py) — fixes a PyInstaller "missing module" error for ws_server that only surfaced once packaging was attempted, since its old location wasn't on the analyzer's search path - .venv relocated into app-desktop/src/ alongside the code it serves - run.py moved into src/, path logic simplified now that it's co-located with main.py instead of bridging from the repo root - Deleted app/, server/ — stale __pycache__-only fossils from prior reorgs - .gitignore: added missing .venv/ entry (was only covering venv/, a different name — the real folder was never actually ignored) and app-desktop/build|dist/ for PyInstaller output presets.py: added a frozen-vs-source branch. Running from source is unchanged (app-presets/ next to the code). A packaged exe can't write to Program Files, so it uses %APPDATA%\VirtualController\ instead — standard Windows convention. First launch on a machine with no AppData presets yet seeds from presets.json bundled into the exe (PyInstaller --add-data, baked into VirtualController.spec), so a fresh install starts with real presets instead of empty; every launch after that only touches AppData. Verified: exe builds clean (no missing-module warnings), launches with a real window, and a clean first-launch correctly seeds AppData from the bundled presets. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,7 +41,8 @@
|
|||||||
"Bash(awk '/int REAPERAPI_LoadAPI/,0' /Users/p4piwabl0/Desktop/projects/virtual-controller-clean/extension-reaper-macos/vendor/reaper-sdk/sdk/reaper_plugin_functions.h)",
|
"Bash(awk '/int REAPERAPI_LoadAPI/,0' /Users/p4piwabl0/Desktop/projects/virtual-controller-clean/extension-reaper-macos/vendor/reaper-sdk/sdk/reaper_plugin_functions.h)",
|
||||||
"Bash(grep -n \"{NULL, NULL}\")",
|
"Bash(grep -n \"{NULL, NULL}\")",
|
||||||
"Bash(ls -la \"/Applications/REAPER.app/Contents/MacOS/\" 2>&1 | head -5 *)",
|
"Bash(ls -la \"/Applications/REAPER.app/Contents/MacOS/\" 2>&1 | head -5 *)",
|
||||||
"Read(//Applications/REAPER.app/Contents/MacOS/**)"
|
"Read(//Applications/REAPER.app/Contents/MacOS/**)",
|
||||||
|
"Bash(src/.venv/Scripts/python.exe -m PyInstaller --name VirtualController --onedir --windowed --distpath dist --workpath build --specpath . --add-data \"src/app-presets/presets.json;app-presets\" src/main.py)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
venv/
|
venv/
|
||||||
|
.venv/
|
||||||
|
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|
||||||
@@ -6,6 +7,10 @@ __pycache__/
|
|||||||
|
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
|
# PyInstaller output (app-desktop)
|
||||||
|
app-desktop/build/
|
||||||
|
app-desktop/dist/
|
||||||
|
|
||||||
# Xcode
|
# Xcode
|
||||||
*.xcuserstate
|
*.xcuserstate
|
||||||
xcuserdata/
|
xcuserdata/
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# -*- mode: python ; coding: utf-8 -*-
|
||||||
|
|
||||||
|
|
||||||
|
a = Analysis(
|
||||||
|
['src\\main.py'],
|
||||||
|
pathex=[],
|
||||||
|
binaries=[],
|
||||||
|
datas=[('src/app-presets/presets.json', 'app-presets')],
|
||||||
|
hiddenimports=[],
|
||||||
|
hookspath=[],
|
||||||
|
hooksconfig={},
|
||||||
|
runtime_hooks=[],
|
||||||
|
excludes=[],
|
||||||
|
noarchive=False,
|
||||||
|
optimize=0,
|
||||||
|
)
|
||||||
|
pyz = PYZ(a.pure)
|
||||||
|
|
||||||
|
exe = EXE(
|
||||||
|
pyz,
|
||||||
|
a.scripts,
|
||||||
|
[],
|
||||||
|
exclude_binaries=True,
|
||||||
|
name='VirtualController',
|
||||||
|
debug=False,
|
||||||
|
bootloader_ignore_signals=False,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
console=False,
|
||||||
|
disable_windowed_traceback=False,
|
||||||
|
argv_emulation=False,
|
||||||
|
target_arch=None,
|
||||||
|
codesign_identity=None,
|
||||||
|
entitlements_file=None,
|
||||||
|
)
|
||||||
|
coll = COLLECT(
|
||||||
|
exe,
|
||||||
|
a.binaries,
|
||||||
|
a.datas,
|
||||||
|
strip=False,
|
||||||
|
upx=True,
|
||||||
|
upx_exclude=[],
|
||||||
|
name='VirtualController',
|
||||||
|
)
|
||||||
@@ -9,10 +9,29 @@ from PyQt6.QtWidgets import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
import shutil
|
||||||
PRESETS_DIR = os.path.join(BASE_DIR, "app-presets")
|
import sys
|
||||||
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
|
||||||
os.makedirs(PRESETS_DIR, exist_ok=True)
|
if getattr(sys, "frozen", False):
|
||||||
|
# Packaged exe: Program Files (or wherever it's installed) is not
|
||||||
|
# writable by a normal user, so presets live in the per-user AppData
|
||||||
|
# folder instead — standard Windows convention for app-written data.
|
||||||
|
PRESETS_DIR = os.path.join(os.environ["APPDATA"], "VirtualController")
|
||||||
|
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
||||||
|
os.makedirs(PRESETS_DIR, exist_ok=True)
|
||||||
|
if not os.path.exists(PRESETS_FILE):
|
||||||
|
# First launch on this machine: seed from the presets.json bundled
|
||||||
|
# into the exe (sys._MEIPASS), so a fresh install starts with the
|
||||||
|
# real presets instead of empty. Only happens once — after this,
|
||||||
|
# AppData is the only copy that's ever read or written.
|
||||||
|
_bundled = os.path.join(sys._MEIPASS, "app-presets", "presets.json")
|
||||||
|
if os.path.exists(_bundled):
|
||||||
|
shutil.copyfile(_bundled, PRESETS_FILE)
|
||||||
|
else:
|
||||||
|
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
PRESETS_DIR = os.path.join(BASE_DIR, "app-presets")
|
||||||
|
PRESETS_FILE = os.path.join(PRESETS_DIR, "presets.json")
|
||||||
|
os.makedirs(PRESETS_DIR, exist_ok=True)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import os
|
||||||
|
import runpy
|
||||||
|
|
||||||
|
_here = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
runpy.run_path(os.path.join(_here, "main.py"), run_name="__main__")
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import sys
|
|
||||||
import os
|
|
||||||
|
|
||||||
_root = os.path.dirname(os.path.abspath(__file__))
|
|
||||||
sys.path.insert(0, os.path.join(_root, "app-desktop-macos"))
|
|
||||||
sys.path.insert(0, os.path.join(_root, "remote-server"))
|
|
||||||
|
|
||||||
import runpy
|
|
||||||
runpy.run_path(os.path.join(_root, "app-desktop-macos", "main.py"), run_name="__main__")
|
|
||||||
Reference in New Issue
Block a user