From e6601ef320a20e350655560e5ab6abb5a08d396f Mon Sep 17 00:00:00 2001 From: vasyl-ks Date: Tue, 9 Jun 2026 12:41:28 +0200 Subject: [PATCH] fix: build python deps and binaries --- blcu-programming/.gitignore | 5 +- blcu-programming/api/main.py | 14 +++ blcu-programming/requirements-build.txt | 2 + blcu-programming/requirements.txt | 2 + electron-app/BUILD.md | 13 +- electron-app/README.md | 7 +- electron-app/build.mjs | 111 ++++++++++++++++++ electron-app/package.json | 8 +- electron-app/src/processes/blcuProgramming.js | 95 ++++++++++++--- 9 files changed, 236 insertions(+), 21 deletions(-) create mode 100644 blcu-programming/requirements-build.txt create mode 100644 blcu-programming/requirements.txt diff --git a/blcu-programming/.gitignore b/blcu-programming/.gitignore index 3d2760b96..ceee592ad 100644 --- a/blcu-programming/.gitignore +++ b/blcu-programming/.gitignore @@ -1,4 +1,7 @@ /.venv +/.venv-build +build/ +dist/ /.vscode *.pyc *__pycache__/ @@ -7,4 +10,4 @@ .idea/ containers/*/.venv .env -*.log \ No newline at end of file +*.log diff --git a/blcu-programming/api/main.py b/blcu-programming/api/main.py index 15d9307ca..b4df61aec 100644 --- a/blcu-programming/api/main.py +++ b/blcu-programming/api/main.py @@ -1,6 +1,8 @@ +import os from datetime import datetime, timezone from pathlib import Path +import uvicorn from fastapi import FastAPI, HTTPException, Query from pydantic import BaseModel, Field @@ -90,3 +92,15 @@ def get_logs(tail: int = Query(default=200, ge=1, le=5000)) -> dict: "lines": selected_lines, "line_count": len(selected_lines), } + + +def main() -> None: + host = os.environ.get("BLCU_API_HOST", "127.0.0.1") + port = int(os.environ.get("BLCU_API_PORT", "8000")) + log_level = os.environ.get("BLCU_API_LOG_LEVEL", "info") + + uvicorn.run(app, host=host, port=port, log_level=log_level) + + +if __name__ == "__main__": + main() diff --git a/blcu-programming/requirements-build.txt b/blcu-programming/requirements-build.txt new file mode 100644 index 000000000..946666958 --- /dev/null +++ b/blcu-programming/requirements-build.txt @@ -0,0 +1,2 @@ +-r requirements.txt +pyinstaller==6.17.0 diff --git a/blcu-programming/requirements.txt b/blcu-programming/requirements.txt new file mode 100644 index 000000000..401444863 --- /dev/null +++ b/blcu-programming/requirements.txt @@ -0,0 +1,2 @@ +fastapi==0.124.2 +uvicorn==0.38.0 diff --git a/electron-app/BUILD.md b/electron-app/BUILD.md index e001a1397..b89737980 100644 --- a/electron-app/BUILD.md +++ b/electron-app/BUILD.md @@ -1,18 +1,19 @@ # Hyperloop Control Station Build System -The project uses a unified, modular build script (`electron-app/build.mjs`) to handle building the backend (Go), and frontends (React/Vite) for the Electron application. +The project uses a unified, modular build script (`electron-app/build.mjs`) to handle building the backend (Go), the BLCU programming API (Python/PyInstaller), and frontends (React/Vite) for the Electron application. ## Prerequisites - **Node.js** & **pnpm** - **Go** (1.21+) +- **Python 3** (for building the BLCU programming API executable) ## Basic Usage Run the build script from the `electron-app` directory (or via npm scripts). ```sh -# Build EVERYTHING (Backend, Frontends) +# Build EVERYTHING (Backend, BLCU API, Frontends) pnpm build # OR @@ -31,6 +32,9 @@ You can build individual components by passing their flag. # Build only the Backend node build.mjs --backend +# Build only the BLCU programming API executable +node build.mjs --blcu-programming + # Build only the Testing View node build.mjs --testing-view ``` @@ -43,10 +47,15 @@ By default, the script builds for all defined platforms (Windows, Linux, macOS). # Build backend for Windows only node build.mjs --backend --win +# Build BLCU API for the current Windows host +node build.mjs --blcu-programming --win + # Build everything for Linux node build.mjs --linux ``` +The BLCU programming API is packaged with PyInstaller and cannot be cross-compiled. Build it on the same OS as the Electron release target. + ## Advanced: Overwriting Commands The build script allows you to override configuration properties on the fly. This is useful for CI pipelines where you might want to use different build commands or flags. diff --git a/electron-app/README.md b/electron-app/README.md index d4e30bf0d..d28d055ad 100644 --- a/electron-app/README.md +++ b/electron-app/README.md @@ -4,7 +4,7 @@ The main Electron application that provides the control interface for the Hyperl ## Overview -Desktop application built with Electron that manages the Hyperloop pod control system. Handles backend process management, configuration, and provides multiple frontend views for competition and testing. +Desktop application built with Electron that manages the Hyperloop pod control system. Handles backend process management, BLCU programming API process management, configuration, and provides multiple frontend views for competition and testing. ## Project Structure @@ -22,7 +22,7 @@ When running in development mode (unpackaged), the application creates temporary - `config.toml.backup-{timestamp}` - Automatic backup files created when importing a configuration. These timestamped backups help recover previous configurations if needed. -- `binaries/` - Directory containing compiled backend executables for your platform. These are generated during the build process, when running `pnpm run build`. +- `binaries/` - Directory containing compiled backend and BLCU programming executables for your platform. These are generated during the build process, when running `pnpm run build`. - `renderer/` - Directory containing built frontend views (control-station, ethernet-view). These are generated during the build process, when running `pnpm run build`. @@ -56,7 +56,7 @@ Typical locations: # Install dependencies pnpm install -# Build backend and frontends +# Build backend, BLCU programming API, and frontends pnpm run build # Run in development mode (you MUST run `pnpm run build` BEFORE!) @@ -99,6 +99,7 @@ sudo ifconfig lo0 alias 127.0.0.9 up ## Architecture - **Backend Process**: Go backend for data processing +- **BLCU Programming Process**: Packaged FastAPI/TFTP API for firmware transfers - **Packet Sender**: Tool for sending test packets - **Configuration**: TOML-based config management - **Views**: Multiple frontend interfaces (Competition/Testing) diff --git a/electron-app/build.mjs b/electron-app/build.mjs index 4525e24a3..f96ec320b 100644 --- a/electron-app/build.mjs +++ b/electron-app/build.mjs @@ -53,6 +53,14 @@ const CONFIG = { }, ], }, + "blcu-programming": { + type: "python", + path: join(ROOT, "blcu-programming"), + output: join(__dirname, "binaries"), + entry: join(ROOT, "blcu-programming", "api", "main.py"), + requirements: join(ROOT, "blcu-programming", "requirements-build.txt"), + venv: join(ROOT, "blcu-programming", ".venv-build"), + }, "testing-view": { type: "frontend", path: join(ROOT, "frontend/testing-view"), @@ -127,6 +135,107 @@ const buildGo = (name, config, requestedPlatforms, extraArgs = "") => { return success; }; +const getVenvPython = (venvPath) => { + const binDir = process.platform === "win32" ? "Scripts" : "bin"; + const executable = process.platform === "win32" ? "python.exe" : "python"; + return join(venvPath, binDir, executable); +}; + +const createPythonVenv = (venvPath, cwd) => { + if (existsSync(getVenvPython(venvPath))) return true; + + const python = process.env.PYTHON || "python"; + + logger.step(`Creating Python build venv with ${python}...`); + return run(`${python} -m venv "${venvPath}"`, cwd); +}; + +const getPythonTarget = () => { + const goos = + { win32: "windows", darwin: "darwin", linux: "linux" }[process.platform] || + process.platform; + const goarch = { x64: "amd64", arm64: "arm64" }[process.arch] || process.arch; + const platformTag = + { win32: "win", darwin: "mac", linux: "linux" }[process.platform] || + process.platform; + + return { + binarySuffix: `${goos}-${goarch}${process.platform === "win32" ? ".exe" : ""}`, + label: `${goos}/${goarch}`, + platformTag, + }; +}; + +const buildPython = (name, config, requestedPlatforms) => { + const target = getPythonTarget(); + const targetRequested = + requestedPlatforms.length === 0 || + requestedPlatforms.includes("all") || + requestedPlatforms.includes(target.platformTag); + + if (!targetRequested) { + logger.error( + `${name} must be built on the target OS because PyInstaller cannot cross-compile. Current host is ${target.label}.`, + ); + return false; + } + + logger.info(`Building ${name} (Python/PyInstaller)...`); + mkdirSync(config.output, { recursive: true }); + + if (!createPythonVenv(config.venv, config.path)) return false; + + const pythonBin = getVenvPython(config.venv); + + if ( + !run( + `"${pythonBin}" -m pip install -r "${config.requirements}"`, + config.path, + ) + ) { + return false; + } + + const binaryName = `${name}-${target.binarySuffix}`; + const binaryBaseName = binaryName.replace(/\.exe$/, ""); + const binaryPath = join(config.output, binaryName); + const pyinstallerWorkPath = join(config.path, "build", "pyinstaller"); + const pyinstallerSpecPath = join(config.path, "build"); + + if (existsSync(binaryPath)) { + try { + rmSync(binaryPath, { force: true }); + } catch (error) { + if (error.code === "EPERM" || error.code === "EACCES") { + logger.error( + `Could not replace ${binaryPath}. Stop Electron or the running BLCU programming process, then build again.`, + ); + return false; + } + + throw error; + } + } + + return run( + [ + `"${pythonBin}" -m PyInstaller`, + "--clean", + "--noconfirm", + "--onefile", + `--name "${binaryBaseName}"`, + `--distpath "${config.output}"`, + `--workpath "${pyinstallerWorkPath}"`, + `--specpath "${pyinstallerSpecPath}"`, + "--hidden-import uvicorn.loops.auto", + "--hidden-import uvicorn.protocols.http.auto", + "--hidden-import uvicorn.lifespan.on", + `"${config.entry}"`, + ].join(" "), + config.path, + ); +}; + const buildFrontend = (name, config, extraArgs = "") => { if (config.optional && !existsSync(join(config.path, "package.json"))) { logger.warning(`Skipping ${name} (not initialized)`); @@ -212,6 +321,8 @@ logger.header("Hyperloop Control Station Build"); if (config.type === "go") { success = buildGo(key, config, requestedPlatforms, extraArgs); + } else if (config.type === "python") { + success = buildPython(key, config, requestedPlatforms); } else if (config.type === "frontend") { success = buildFrontend(key, config, extraArgs); if (success && !config.optional) frontendBuilt = true; diff --git a/electron-app/package.json b/electron-app/package.json index 1f1270cec..083a5bd56 100644 --- a/electron-app/package.json +++ b/electron-app/package.json @@ -30,6 +30,10 @@ "build:backend:win": "node build.mjs --backend --win", "build:backend:linux": "node build.mjs --backend --linux", "build:backend:mac": "node build.mjs --backend --mac", + "build:blcu": "node build.mjs --blcu-programming", + "build:blcu:win": "node build.mjs --blcu-programming --win", + "build:blcu:linux": "node build.mjs --blcu-programming --linux", + "build:blcu:mac": "node build.mjs --blcu-programming --mac", "build:testing": "node build.mjs --testing-view", "build:competition": "node build.mjs --competition-view", "asar:win": "asar list dist/win-unpacked/resources/app.asar | findstr /V node_modules", @@ -112,7 +116,9 @@ "icon": "icons/512x512.png", "category": "Utility", "artifactName": "${productName}-${version}-linux-${arch}.${ext}", - "executableArgs": ["--no-sandbox"] + "executableArgs": [ + "--no-sandbox" + ] } } } diff --git a/electron-app/src/processes/blcuProgramming.js b/electron-app/src/processes/blcuProgramming.js index 1721db77b..aebd2a3a6 100644 --- a/electron-app/src/processes/blcuProgramming.js +++ b/electron-app/src/processes/blcuProgramming.js @@ -1,8 +1,9 @@ import { spawn } from "child_process"; +import { app, dialog } from "electron"; import fs from "fs"; import path from "path"; import { logger } from "../utils/logger.js"; -import { getAppPath } from "../utils/paths.js"; +import { getAppPath, getBinaryPath } from "../utils/paths.js"; let blcuProgrammingProcess = null; @@ -18,11 +19,32 @@ function getPythonExecutable(repoPath) { return path.join(repoPath, ".venv", "bin", "python"); } -async function startBlcuProgramming() { - if (blcuProgrammingProcess && !blcuProgrammingProcess.killed) { - return blcuProgrammingProcess; +function getBlcuProgrammingWorkingDir() { + if (!app.isPackaged) { + return getBlcuProgrammingRepoPath(); + } + + const workingDir = path.join(app.getPath("userData"), "blcu-programming"); + fs.mkdirSync(workingDir, { recursive: true }); + return workingDir; +} + +function getBinaryStartConfig() { + const binaryPath = getBinaryPath("blcu-programming"); + + if (!fs.existsSync(binaryPath)) { + return null; } + return { + command: binaryPath, + args: [], + cwd: getBlcuProgrammingWorkingDir(), + source: "binary", + }; +} + +function getPythonStartConfig() { const repoPath = getBlcuProgrammingRepoPath(); const pythonBin = getPythonExecutable(repoPath); const entrypointPath = path.join(repoPath, "api", "main.py"); @@ -43,18 +65,55 @@ async function startBlcuProgramming() { return null; } - blcuProgrammingProcess = spawn( - pythonBin, - ["-m", "uvicorn", "api.main:app"], - { - cwd: repoPath, - env: { - ...process.env, - PYTHONUNBUFFERED: "1", - }, - }, + return { + command: pythonBin, + args: ["-m", "api.main"], + cwd: repoPath, + source: "python", + }; +} + +function getStartConfig() { + return ( + getBinaryStartConfig() || (!app.isPackaged ? getPythonStartConfig() : null) + ); +} + +async function startBlcuProgramming() { + if (blcuProgrammingProcess && !blcuProgrammingProcess.killed) { + return blcuProgrammingProcess; + } + + const startConfig = getStartConfig(); + + if (!startConfig) { + const message = + "BLCU programming executable not found. Run the Electron build before packaging the release."; + + logger.process("BLCU Programming", message); + + if (app.isPackaged) { + dialog.showErrorBox("BLCU Programming Error", message); + } + + return null; + } + + logger.process( + "BLCU Programming", + `Starting ${startConfig.source}: ${startConfig.command}`, ); + blcuProgrammingProcess = spawn(startConfig.command, startConfig.args, { + cwd: startConfig.cwd, + env: { + ...process.env, + BLCU_API_HOST: process.env.BLCU_API_HOST || "127.0.0.1", + BLCU_API_PORT: process.env.BLCU_API_PORT || "8000", + PYTHONUNBUFFERED: "1", + }, + }); + blcuProgrammingProcess.stdout.on("data", (data) => { logger.process("BLCU Programming", data.toString().trim()); }); @@ -63,6 +122,14 @@ async function startBlcuProgramming() { logger.process("BLCU Programming", data.toString().trim()); }); + blcuProgrammingProcess.on("error", (error) => { + logger.process( + "BLCU Programming", + `Failed to start BLCU programming: ${error.message}`, + ); + blcuProgrammingProcess = null; + }); + blcuProgrammingProcess.on("close", (code) => { logger.process("BLCU Programming", `Process exited with code ${code}`); blcuProgrammingProcess = null;