add desktop build support
This commit is contained in:
8
.gitignore
vendored
8
.gitignore
vendored
@@ -1,3 +1,9 @@
|
|||||||
*__pycache__*
|
*__pycache__*
|
||||||
|
*.egg-info/
|
||||||
|
.pytest_cache/
|
||||||
|
pytest-cache-files-*/
|
||||||
|
desktop-build/
|
||||||
|
desktop-dist/
|
||||||
frontend/node_modules/
|
frontend/node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
|
.venv/
|
||||||
204
README.md
Normal file
204
README.md
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
# Argonode
|
||||||
|
|
||||||
|
Argonode is a node-based image analysis application with:
|
||||||
|
|
||||||
|
- a Python backend built on `aiohttp`
|
||||||
|
- a React + Vite frontend
|
||||||
|
- an optional desktop wrapper built with `pywebview`
|
||||||
|
|
||||||
|
The backend serves node definitions, runs workflows, manages file I/O, and streams previews/results over WebSocket. The frontend provides the graph editor and UI. The desktop build packages both together as a Windows application.
|
||||||
|
|
||||||
|
## Project Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
argonode/
|
||||||
|
backend/ Python server, execution engine, nodes
|
||||||
|
frontend/ React/Vite app
|
||||||
|
tests/ Python tests
|
||||||
|
desktop.py Local desktop launcher
|
||||||
|
scripts/ Build helpers, including Windows exe packaging
|
||||||
|
```
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python `3.10+`
|
||||||
|
- Node.js `18+`
|
||||||
|
- npm `9+`
|
||||||
|
- Windows is recommended for the desktop `.exe` packaging flow
|
||||||
|
|
||||||
|
## First-Time Setup
|
||||||
|
|
||||||
|
Create a virtual environment if you do not already have one:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
```
|
||||||
|
|
||||||
|
Install Python dependencies:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe -m pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
Install Node dependencies from the repo root:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
Optional extras:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe -m pip install -e .[dev]
|
||||||
|
.\.venv\Scripts\python.exe -m pip install -e .[spm]
|
||||||
|
.\.venv\Scripts\python.exe -m pip install -e .[desktop]
|
||||||
|
```
|
||||||
|
|
||||||
|
- `dev`: test tooling
|
||||||
|
- `spm`: optional SPM/AFM file readers like `gwyfile`, `nanonispy`, and `igor`
|
||||||
|
- `desktop`: desktop launcher and PyInstaller packaging tools
|
||||||
|
|
||||||
|
## Running the Local Web Version
|
||||||
|
|
||||||
|
This is the normal browser-based development flow.
|
||||||
|
|
||||||
|
In terminal 1, start the backend:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run backend
|
||||||
|
```
|
||||||
|
|
||||||
|
This starts the Python server at `http://127.0.0.1:8188`.
|
||||||
|
|
||||||
|
In terminal 2, start the Vite frontend:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open the Vite URL shown in the terminal, typically:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:5173
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- The frontend dev server proxies API and WebSocket requests to the backend.
|
||||||
|
- If you want the frontend accessible from other devices on your LAN, run:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev -- --host 0.0.0.0
|
||||||
|
```
|
||||||
|
|
||||||
|
## Running the Local Desktop Version
|
||||||
|
|
||||||
|
The desktop launcher starts the Python server internally and opens a native window with `pywebview`.
|
||||||
|
|
||||||
|
Build the frontend first:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Then launch the desktop app from source:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
|
||||||
|
- `npm run desktop` uses the built frontend from `frontend/dist`.
|
||||||
|
- If you change frontend code, run `npm run build` again before starting the desktop version.
|
||||||
|
|
||||||
|
## Building the Windows `.exe`
|
||||||
|
|
||||||
|
The repo includes a packaging script that:
|
||||||
|
|
||||||
|
1. builds the frontend
|
||||||
|
2. installs desktop build dependencies
|
||||||
|
3. runs PyInstaller
|
||||||
|
|
||||||
|
Build the desktop bundle:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run build:desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
Or run the script directly:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\build-desktop.ps1
|
||||||
|
```
|
||||||
|
|
||||||
|
The packaged app is written to:
|
||||||
|
|
||||||
|
```text
|
||||||
|
desktop-dist/Argonode/
|
||||||
|
```
|
||||||
|
|
||||||
|
Main executable:
|
||||||
|
|
||||||
|
```text
|
||||||
|
desktop-dist/Argonode/Argonode.exe
|
||||||
|
```
|
||||||
|
|
||||||
|
### One-File Build
|
||||||
|
|
||||||
|
The default build uses PyInstaller `--onedir`, which is more reliable for scientific Python packages like NumPy, SciPy, and Matplotlib.
|
||||||
|
|
||||||
|
If you still want to try a single-file executable:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
powershell -ExecutionPolicy Bypass -File scripts\build-desktop.ps1 -OneFile
|
||||||
|
```
|
||||||
|
|
||||||
|
## Data Directories
|
||||||
|
|
||||||
|
During normal source-based development, input/output folders live under the repo root.
|
||||||
|
|
||||||
|
In the packaged desktop app, writable data is stored under:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%LOCALAPPDATA%\Argonode\
|
||||||
|
```
|
||||||
|
|
||||||
|
Specifically:
|
||||||
|
|
||||||
|
```text
|
||||||
|
%LOCALAPPDATA%\Argonode\input
|
||||||
|
%LOCALAPPDATA%\Argonode\output
|
||||||
|
```
|
||||||
|
|
||||||
|
You can override the packaged app data directory with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
$env:ARGONODE_APPDATA="C:\path\to\custom\data"
|
||||||
|
```
|
||||||
|
|
||||||
|
## Useful Commands
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run dev
|
||||||
|
npm run build
|
||||||
|
npm run preview
|
||||||
|
npm run backend
|
||||||
|
npm run desktop
|
||||||
|
npm run build:desktop
|
||||||
|
.\.venv\Scripts\python.exe -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run the Python test suite with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\.venv\Scripts\python.exe -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
## Known Notes
|
||||||
|
|
||||||
|
- The frontend production build currently emits a large chunk warning from Vite. This does not block builds.
|
||||||
|
- The desktop app relies on WebView2 on Windows through `pywebview`.
|
||||||
|
- Optional SPM readers are not installed unless you explicitly install the `spm` extra.
|
||||||
@@ -9,10 +9,11 @@ from pathlib import Path
|
|||||||
|
|
||||||
from backend.node_registry import register_node
|
from backend.node_registry import register_node
|
||||||
from backend.data_types import DataField, encode_preview, image_to_uint8
|
from backend.data_types import DataField, encode_preview, image_to_uint8
|
||||||
|
from backend.runtime_paths import input_dir, output_dir
|
||||||
|
|
||||||
# Resolved at server startup so nodes know where to look
|
# Resolved at server startup so nodes know where to look
|
||||||
INPUT_DIR = Path(__file__).parent.parent.parent / "input"
|
INPUT_DIR = input_dir()
|
||||||
OUTPUT_DIR = Path(__file__).parent.parent.parent / "output"
|
OUTPUT_DIR = output_dir()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|||||||
54
backend/runtime_paths.py
Normal file
54
backend/runtime_paths.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
APP_NAME = "Argonode"
|
||||||
|
|
||||||
|
|
||||||
|
def project_root() -> Path:
|
||||||
|
return Path(__file__).resolve().parent.parent
|
||||||
|
|
||||||
|
|
||||||
|
def resource_root() -> Path:
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
return Path(getattr(sys, "_MEIPASS", Path(sys.executable).resolve().parent))
|
||||||
|
return project_root()
|
||||||
|
|
||||||
|
|
||||||
|
def frontend_dir() -> Path:
|
||||||
|
bundled = resource_root() / "frontend"
|
||||||
|
if bundled.exists():
|
||||||
|
return bundled
|
||||||
|
return project_root() / "frontend"
|
||||||
|
|
||||||
|
|
||||||
|
def frontend_dist_dir() -> Path:
|
||||||
|
return frontend_dir() / "dist"
|
||||||
|
|
||||||
|
|
||||||
|
def app_data_dir() -> Path:
|
||||||
|
override = os.getenv("ARGONODE_APPDATA")
|
||||||
|
if override:
|
||||||
|
return Path(override).expanduser().resolve()
|
||||||
|
|
||||||
|
if getattr(sys, "frozen", False):
|
||||||
|
local_appdata = os.getenv("LOCALAPPDATA")
|
||||||
|
base_dir = Path(local_appdata) if local_appdata else Path.home() / "AppData" / "Local"
|
||||||
|
return (base_dir / APP_NAME).resolve()
|
||||||
|
|
||||||
|
return project_root()
|
||||||
|
|
||||||
|
|
||||||
|
def input_dir() -> Path:
|
||||||
|
return app_data_dir() / "input"
|
||||||
|
|
||||||
|
|
||||||
|
def output_dir() -> Path:
|
||||||
|
return app_data_dir() / "output"
|
||||||
|
|
||||||
|
|
||||||
|
def ensure_runtime_dirs() -> None:
|
||||||
|
input_dir().mkdir(parents=True, exist_ok=True)
|
||||||
|
output_dir().mkdir(parents=True, exist_ok=True)
|
||||||
@@ -24,17 +24,23 @@ from __future__ import annotations
|
|||||||
import asyncio
|
import asyncio
|
||||||
import json
|
import json
|
||||||
import logging
|
import logging
|
||||||
import uuid
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from aiohttp import web, WSMsgType
|
from aiohttp import web, WSMsgType
|
||||||
|
from backend.runtime_paths import (
|
||||||
|
ensure_runtime_dirs,
|
||||||
|
frontend_dir,
|
||||||
|
frontend_dist_dir,
|
||||||
|
input_dir,
|
||||||
|
output_dir,
|
||||||
|
)
|
||||||
|
|
||||||
log = logging.getLogger(__name__)
|
log = logging.getLogger(__name__)
|
||||||
|
|
||||||
FRONTEND_DIR = Path(__file__).parent.parent / "frontend"
|
FRONTEND_DIR = frontend_dir()
|
||||||
DIST_DIR = FRONTEND_DIR / "dist"
|
DIST_DIR = frontend_dist_dir()
|
||||||
INPUT_DIR = Path(__file__).parent.parent / "input"
|
INPUT_DIR = input_dir()
|
||||||
OUTPUT_DIR = Path(__file__).parent.parent / "output"
|
OUTPUT_DIR = output_dir()
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -67,8 +73,7 @@ def create_app(loop: asyncio.AbstractEventLoop) -> web.Application:
|
|||||||
from backend.node_registry import get_all_node_info
|
from backend.node_registry import get_all_node_info
|
||||||
from backend.execution import ExecutionEngine, new_prompt_id
|
from backend.execution import ExecutionEngine, new_prompt_id
|
||||||
|
|
||||||
INPUT_DIR.mkdir(exist_ok=True)
|
ensure_runtime_dirs()
|
||||||
OUTPUT_DIR.mkdir(exist_ok=True)
|
|
||||||
|
|
||||||
engine = ExecutionEngine()
|
engine = ExecutionEngine()
|
||||||
websockets: set[web.WebSocketResponse] = set()
|
websockets: set[web.WebSocketResponse] = set()
|
||||||
@@ -104,7 +109,11 @@ def create_app(loop: asyncio.AbstractEventLoop) -> web.Application:
|
|||||||
# Serve Vite build output if available, else raw frontend
|
# Serve Vite build output if available, else raw frontend
|
||||||
if (DIST_DIR / "index.html").exists():
|
if (DIST_DIR / "index.html").exists():
|
||||||
return web.FileResponse(DIST_DIR / "index.html")
|
return web.FileResponse(DIST_DIR / "index.html")
|
||||||
return web.FileResponse(FRONTEND_DIR / "index.html")
|
if (FRONTEND_DIR / "index.html").exists():
|
||||||
|
return web.FileResponse(FRONTEND_DIR / "index.html")
|
||||||
|
raise web.HTTPInternalServerError(
|
||||||
|
reason="Frontend build not found. Run `npm run build` before launching the packaged app."
|
||||||
|
)
|
||||||
|
|
||||||
async def get_nodes(request: web.Request) -> web.Response:
|
async def get_nodes(request: web.Request) -> web.Response:
|
||||||
info = get_all_node_info()
|
info = get_all_node_info()
|
||||||
@@ -244,9 +253,10 @@ def create_app(loop: asyncio.AbstractEventLoop) -> web.Application:
|
|||||||
app.router.add_get("/ws", websocket_handler)
|
app.router.add_get("/ws", websocket_handler)
|
||||||
|
|
||||||
# Serve frontend static files (Vite build or raw)
|
# Serve frontend static files (Vite build or raw)
|
||||||
if DIST_DIR.exists():
|
if (DIST_DIR / "assets").exists():
|
||||||
app.router.add_static("/assets", DIST_DIR / "assets")
|
app.router.add_static("/assets", DIST_DIR / "assets")
|
||||||
app.router.add_static("/static", FRONTEND_DIR)
|
if FRONTEND_DIR.exists():
|
||||||
|
app.router.add_static("/static", FRONTEND_DIR)
|
||||||
|
|
||||||
# CORS — allow any origin (local dev only)
|
# CORS — allow any origin (local dev only)
|
||||||
async def _cors_middleware(app_, handler):
|
async def _cors_middleware(app_, handler):
|
||||||
|
|||||||
111
desktop.py
Normal file
111
desktop.py
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import logging
|
||||||
|
import socket
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
|
||||||
|
import webview
|
||||||
|
from aiohttp import web
|
||||||
|
|
||||||
|
from backend.runtime_paths import app_data_dir, ensure_runtime_dirs
|
||||||
|
from backend.server import create_app
|
||||||
|
|
||||||
|
HOST = "127.0.0.1"
|
||||||
|
WINDOW_TITLE = "Argonode"
|
||||||
|
|
||||||
|
|
||||||
|
def _pick_free_port() -> int:
|
||||||
|
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
|
||||||
|
sock.bind((HOST, 0))
|
||||||
|
return int(sock.getsockname()[1])
|
||||||
|
|
||||||
|
|
||||||
|
def _wait_for_server(url: str, timeout: float = 15.0) -> None:
|
||||||
|
deadline = time.time() + timeout
|
||||||
|
while time.time() < deadline:
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(url, timeout=0.5):
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
time.sleep(0.1)
|
||||||
|
raise RuntimeError(f"Timed out waiting for server at {url}")
|
||||||
|
|
||||||
|
|
||||||
|
def _run_server(host: str, port: int, ready: threading.Event, state: dict[str, object]) -> None:
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
state["loop"] = loop
|
||||||
|
|
||||||
|
async def start() -> None:
|
||||||
|
app = create_app(loop)
|
||||||
|
runner = web.AppRunner(app, access_log=None)
|
||||||
|
await runner.setup()
|
||||||
|
site = web.TCPSite(runner, host, port)
|
||||||
|
await site.start()
|
||||||
|
state["runner"] = runner
|
||||||
|
ready.set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
loop.run_until_complete(start())
|
||||||
|
loop.run_forever()
|
||||||
|
except Exception as exc:
|
||||||
|
state["error"] = exc
|
||||||
|
ready.set()
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
runner = state.get("runner")
|
||||||
|
if isinstance(runner, web.AppRunner):
|
||||||
|
loop.run_until_complete(runner.cleanup())
|
||||||
|
loop.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=logging.INFO,
|
||||||
|
format="%(asctime)s %(levelname)-8s %(name)s - %(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
|
ensure_runtime_dirs()
|
||||||
|
|
||||||
|
port = _pick_free_port()
|
||||||
|
base_url = f"http://{HOST}:{port}"
|
||||||
|
ready = threading.Event()
|
||||||
|
state: dict[str, object] = {}
|
||||||
|
|
||||||
|
server_thread = threading.Thread(
|
||||||
|
target=_run_server,
|
||||||
|
args=(HOST, port, ready, state),
|
||||||
|
name="argonode-server",
|
||||||
|
daemon=True,
|
||||||
|
)
|
||||||
|
server_thread.start()
|
||||||
|
ready.wait(timeout=15.0)
|
||||||
|
|
||||||
|
if "error" in state:
|
||||||
|
raise RuntimeError("Argonode server failed to start") from state["error"]
|
||||||
|
|
||||||
|
_wait_for_server(f"{base_url}/nodes")
|
||||||
|
|
||||||
|
window = webview.create_window(
|
||||||
|
WINDOW_TITLE,
|
||||||
|
base_url,
|
||||||
|
width=1600,
|
||||||
|
height=1000,
|
||||||
|
min_size=(1100, 720),
|
||||||
|
)
|
||||||
|
|
||||||
|
def _shutdown() -> None:
|
||||||
|
loop = state.get("loop")
|
||||||
|
if isinstance(loop, asyncio.AbstractEventLoop):
|
||||||
|
loop.call_soon_threadsafe(loop.stop)
|
||||||
|
|
||||||
|
window.events.closed += _shutdown
|
||||||
|
logging.getLogger(__name__).info("Using app data directory: %s", app_data_dir())
|
||||||
|
webview.start()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
43
frontend/package-lock.json
generated
43
frontend/package-lock.json
generated
@@ -14,6 +14,10 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@vitejs/plugin-react": "^4.3.0",
|
"@vitejs/plugin-react": "^4.3.0",
|
||||||
"vite": "^5.4.0"
|
"vite": "^5.4.0"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@babel/code-frame": {
|
"node_modules/@babel/code-frame": {
|
||||||
@@ -838,9 +842,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -855,9 +856,6 @@
|
|||||||
"arm"
|
"arm"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -872,9 +870,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -889,9 +884,6 @@
|
|||||||
"arm64"
|
"arm64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -906,9 +898,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -923,9 +912,6 @@
|
|||||||
"loong64"
|
"loong64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -940,9 +926,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -957,9 +940,6 @@
|
|||||||
"ppc64"
|
"ppc64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -974,9 +954,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -991,9 +968,6 @@
|
|||||||
"riscv64"
|
"riscv64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1008,9 +982,6 @@
|
|||||||
"s390x"
|
"s390x"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1025,9 +996,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"glibc"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
@@ -1042,9 +1010,6 @@
|
|||||||
"x64"
|
"x64"
|
||||||
],
|
],
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"libc": [
|
|
||||||
"musl"
|
|
||||||
],
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"optional": true,
|
"optional": true,
|
||||||
"os": [
|
"os": [
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
"name": "argonode-frontend",
|
"name": "argonode-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite build",
|
"build": "vite build",
|
||||||
|
|||||||
15
package-lock.json
generated
Normal file
15
package-lock.json
generated
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"name": "argonode",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "argonode",
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
package.json
Normal file
17
package.json
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "argonode",
|
||||||
|
"private": true,
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18.0.0",
|
||||||
|
"npm": ">=9.0.0"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"postinstall": "npm --prefix frontend install",
|
||||||
|
"dev": "npm --prefix frontend run dev",
|
||||||
|
"build": "npm --prefix frontend run build",
|
||||||
|
"preview": "npm --prefix frontend run preview",
|
||||||
|
"backend": "python -m backend.main",
|
||||||
|
"desktop": "python desktop.py",
|
||||||
|
"build:desktop": "powershell -ExecutionPolicy Bypass -File scripts\\build-desktop.ps1"
|
||||||
|
}
|
||||||
|
}
|
||||||
35
pyproject.toml
Normal file
35
pyproject.toml
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "argonode"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Node-based image analysis app with a Python backend and React frontend."
|
||||||
|
readme = "GWYDDION_FEATURE_GAP.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"aiohttp>=3.9,<4",
|
||||||
|
"matplotlib>=3.8,<4",
|
||||||
|
"numpy>=1.26,<3",
|
||||||
|
"pillow>=10,<12",
|
||||||
|
"scikit-image>=0.22,<1",
|
||||||
|
"scipy>=1.12,<2",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
spm = [
|
||||||
|
"gwyfile>=0.2",
|
||||||
|
"igor>=0.3",
|
||||||
|
"nanonispy>=1.1",
|
||||||
|
]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8,<9",
|
||||||
|
]
|
||||||
|
desktop = [
|
||||||
|
"pyinstaller>=6,<7",
|
||||||
|
"pywebview>=5,<6",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
include = ["backend*"]
|
||||||
2
pytest.ini
Normal file
2
pytest.ini
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
[pytest]
|
||||||
|
norecursedirs = .git .venv .pytest_cache frontend/node_modules frontend/dist pytest-cache-files-*
|
||||||
4
requirements.txt
Normal file
4
requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
# Core runtime dependencies are defined in pyproject.toml.
|
||||||
|
# Install them from the repo root with:
|
||||||
|
# python -m pip install -r requirements.txt
|
||||||
|
-e .
|
||||||
48
scripts/build-desktop.ps1
Normal file
48
scripts/build-desktop.ps1
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
param(
|
||||||
|
[switch]$OneFile
|
||||||
|
)
|
||||||
|
|
||||||
|
Set-StrictMode -Version Latest
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
|
||||||
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
|
Set-Location $repoRoot
|
||||||
|
|
||||||
|
$pythonExe = if (Test-Path ".\.venv\Scripts\python.exe") {
|
||||||
|
".\.venv\Scripts\python.exe"
|
||||||
|
} else {
|
||||||
|
"python"
|
||||||
|
}
|
||||||
|
$frontendDist = Join-Path $repoRoot "frontend\dist"
|
||||||
|
|
||||||
|
Write-Host "Building frontend bundle..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
Write-Host "Installing desktop build dependencies..."
|
||||||
|
& $pythonExe -m pip install -e ".[desktop]"
|
||||||
|
|
||||||
|
$mode = if ($OneFile) { "--onefile" } else { "--onedir" }
|
||||||
|
|
||||||
|
$pyInstallerArgs = @(
|
||||||
|
"-m", "PyInstaller",
|
||||||
|
"desktop.py",
|
||||||
|
"--noconfirm",
|
||||||
|
"--clean",
|
||||||
|
"--name", "Argonode",
|
||||||
|
"--windowed",
|
||||||
|
$mode,
|
||||||
|
"--distpath", "desktop-dist",
|
||||||
|
"--workpath", "desktop-build",
|
||||||
|
"--specpath", "desktop-build",
|
||||||
|
"--add-data", "${frontendDist};frontend/dist",
|
||||||
|
"--collect-all", "matplotlib",
|
||||||
|
"--collect-all", "scipy",
|
||||||
|
"--collect-all", "skimage",
|
||||||
|
"--collect-all", "webview"
|
||||||
|
)
|
||||||
|
|
||||||
|
Write-Host "Packaging desktop app..."
|
||||||
|
& $pythonExe @pyInstallerArgs
|
||||||
|
|
||||||
|
Write-Host "Desktop build complete."
|
||||||
|
Write-Host "Output folder: $repoRoot\desktop-dist\Argonode"
|
||||||
Reference in New Issue
Block a user