Skip to content
Open
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
30 changes: 20 additions & 10 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: "publish"

# This will trigger the action on each push to the `release` branch.
# Triggers on push to the `release` branch.
on:
push:
branches:
Expand All @@ -11,7 +11,7 @@ jobs:
permissions:
contents: write
strategy:
fail-fast: true
fail-fast: false
matrix:
platform: [macos-latest, windows-latest]

Expand All @@ -35,7 +35,7 @@ jobs:
run: |
echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV

- uses: actions/cache@v3
- uses: actions/cache@v4
name: setup pnpm cache
with:
path: ${{ env.STORE_PATH }}
Expand All @@ -46,20 +46,30 @@ jobs:
- name: install Rust stable
uses: dtolnay/rust-toolchain@stable

- name: install dependencies (ubuntu only)
if: matrix.platform == 'ubuntu-22.04'
run: |
sudo apt-get update
sudo apt-get install -y libjavascriptcoregtk-4.1-0 libsoup-3.0-0 libglib2.0-dev libgtk-3-dev build-essential libwebkit2gtk-4.0-dev libappindicator3-dev librsvg2-dev patchelf
- name: install uv (Python package manager)
uses: astral-sh/setup-uv@v3
with:
enable-cache: true

- name: cache pyfing PyInstaller venv
uses: actions/cache@v4
with:
path: |
.venv-pyfing
build-pyinstaller
key: ${{ runner.os }}-pyfing-${{ hashFiles('scripts/pyfing-requirements.txt', 'scripts/pyfing_enhance.spec', 'scripts/pyfing_enhance.py') }}

- name: install frontend dependencies
run: pnpm install # change this to npm or pnpm depending on which one you use
run: pnpm install

- name: build pyfing sidecar
run: pnpm sidecar:build:pyfing

- uses: tauri-apps/tauri-action@v0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tagName: app-v__VERSION__ # the action automatically replaces \_\_VERSION\_\_ with the app version
tagName: app-v__VERSION__
releaseName: "App v__VERSION__"
releaseBody: "See the assets to download this version and install."
releaseDraft: false
Expand Down
15 changes: 15 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,18 @@ next-env.d.ts
/public/images/*.json
# vite cache
.vite/

# pyfing sidecar build artifacts
/.venv-pyfing/
/dist-pyinstaller/
/build-pyinstaller/
/scripts/__pycache__/

# pyfing sidecar binaries (built in CI, too large for git)
/src-tauri/bin/pyfing_enhance*.exe
/src-tauri/bin/pyfing_enhance
/src-tauri/bin/pyfing_enhance-*

# local installer / toolchain artifacts (not project files)
/Setup.msix
/rustup-init.exe
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@
"tauri": "tauri",
"tauri:dev": "node ./scripts/tauri-dev.cjs",
"sidecar:sync": "node ./scripts/sidecar-sync.cjs",
"sidecar:build:pyfing": "node ./scripts/build-pyfing-sidecar.cjs",
"sidecar:ensure:pyfing": "node ./scripts/ensure-pyfing-sidecar.cjs",
"tsc:check": "tsc --noEmit",
"tsc:watch": "tsc --noEmit --watch",
"eslint:fix": "eslint --fix .",
Expand Down
206 changes: 206 additions & 0 deletions scripts/build-pyfing-sidecar.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
// Build PyInstaller sidecar for pyfing_enhance using uv.
// Works on Windows, macOS (Intel + Apple Silicon) and Linux.
//
// Usage: pnpm sidecar:build:pyfing
//
// Steps:
// 1. Detect Rust target triple via `rustc -vV` (host: ...).
// 2. Ensure uv is available (install hint if missing).
// 3. Create venv at .venv-pyfing with Python 3.11 via `uv venv --python 3.11`.
// 4. Install requirements with `uv pip install -r ...`.
// 5. Strip unused heavy ML deps that PyInstaller would otherwise scan.
// 6. Run PyInstaller using scripts/pyfing_enhance.spec.
// 7. Copy dist/pyfing_enhance(.exe) -> src-tauri/bin/pyfing_enhance-<triple>(.exe)
// and a generic copy without triple suffix (for Command.sidecar dev fallback).
// 8. Smoke-test with `--check` (exit 0).

const { spawnSync } = require("child_process");
const fs = require("fs");
const path = require("path");

const ROOT = path.resolve(__dirname, "..");
const VENV_DIR = path.join(ROOT, ".venv-pyfing");
const REQUIREMENTS = path.join(ROOT, "scripts", "pyfing-requirements.txt");
const SPEC_FILE = path.join(ROOT, "scripts", "pyfing_enhance.spec");
const DIST_DIR = path.join(ROOT, "dist-pyinstaller");
const WORK_DIR = path.join(ROOT, "build-pyinstaller");
const BIN_DIR = path.join(ROOT, "src-tauri", "bin");
const IS_WINDOWS = process.platform === "win32";
const IS_MAC = process.platform === "darwin";
const EXE_EXT = IS_WINDOWS ? ".exe" : "";
const PYTHON_VERSION = process.env.PYFING_PYTHON_VERSION || "3.11";

function run(cmd, args, opts = {}) {
const printable = `${cmd} ${args.join(" ")}`;
console.log(`> ${printable}`);
const result = spawnSync(cmd, args, {
stdio: "inherit",
shell: false,
...opts,
});
if (result.status !== 0) {
throw new Error(`Command failed (exit ${result.status}): ${printable}`);
}
return result;
}

function captureOutput(cmd, args) {
const result = spawnSync(cmd, args, { encoding: "utf8", shell: false });
if (result.status !== 0) {
throw new Error(
`Command failed (exit ${result.status}): ${cmd} ${args.join(" ")}\n${result.stderr || ""}`
);
}
return result.stdout || "";
}

function detectTargetTriple() {
try {
const out = captureOutput("rustc", ["-vV"]);
const match = out.match(/host:\s*(\S+)/);
if (match) return match[1];
} catch (err) {
console.warn(
"WARN: rustc not found, defaulting target triple",
err.message
);
}
if (IS_WINDOWS) return "x86_64-pc-windows-msvc";
if (IS_MAC) {
return process.arch === "arm64"
? "aarch64-apple-darwin"
: "x86_64-apple-darwin";
}
return process.arch === "arm64"
? "aarch64-unknown-linux-gnu"
: "x86_64-unknown-linux-gnu";
}

function ensureUv() {
const probe = spawnSync("uv", ["--version"], {
encoding: "utf8",
shell: false,
});
if (probe.status === 0) {
console.log(`uv detected: ${(probe.stdout || "").trim()}`);
return;
}
throw new Error(
"uv is not installed or not on PATH. Install it first:\n" +
" Windows PowerShell: irm https://astral.sh/uv/install.ps1 | iex\n" +
" macOS / Linux: curl -LsSf https://astral.sh/uv/install.sh | sh\n" +
" pip: pip install uv\n" +
"Then re-run this script."
);
}

function venvPython() {
if (IS_WINDOWS) {
return path.join(VENV_DIR, "Scripts", "python.exe");
}
return path.join(VENV_DIR, "bin", "python");
}

function ensureVenv() {
if (fs.existsSync(venvPython())) {
console.log(`venv exists at ${VENV_DIR}`);
return;
}
console.log(`creating venv at ${VENV_DIR} with Python ${PYTHON_VERSION}`);
run("uv", ["venv", "--python", PYTHON_VERSION, VENV_DIR]);
}

function installDeps() {
const py = venvPython();
run("uv", ["pip", "install", "--python", py, "-r", REQUIREMENTS]);

// Strip ML deps that may have been pulled transitively but PyInstaller
// would otherwise try to bytecode-scan them (extra time + occasional bugs).
const heavyPackages = [
"torch",
"torchvision",
"torchaudio",
"jax",
"jaxlib",
];
for (const pkg of heavyPackages) {
const result = spawnSync(
"uv",
["pip", "uninstall", "--python", py, pkg],
{ stdio: "inherit", shell: false }
);
if (result.status !== 0) {
console.log(`(skipped uninstall: ${pkg} not present)`);
}
}
}

function buildExe() {
const py = venvPython();
if (fs.existsSync(DIST_DIR)) {
fs.rmSync(DIST_DIR, { recursive: true, force: true });
}
if (fs.existsSync(WORK_DIR)) {
fs.rmSync(WORK_DIR, { recursive: true, force: true });
}
run(py, [
"-m",
"PyInstaller",
"--noconfirm",
"--distpath",
DIST_DIR,
"--workpath",
WORK_DIR,
SPEC_FILE,
]);
}

function copySidecar(triple) {
if (!fs.existsSync(BIN_DIR)) {
fs.mkdirSync(BIN_DIR, { recursive: true });
}
const built = path.join(DIST_DIR, `pyfing_enhance${EXE_EXT}`);
if (!fs.existsSync(built)) {
throw new Error(`built binary not found: ${built}`);
}
const targets = [
path.join(BIN_DIR, `pyfing_enhance-${triple}${EXE_EXT}`),
path.join(BIN_DIR, `pyfing_enhance${EXE_EXT}`),
];
for (const target of targets) {
fs.copyFileSync(built, target);
if (!IS_WINDOWS) {
// Tauri requires sidecar binaries to be executable on Unix.
fs.chmodSync(target, 0o755);
}
console.log(`copied -> ${target}`);
}
}

function smokeTest() {
const exe = path.join(BIN_DIR, `pyfing_enhance${EXE_EXT}`);
console.log(`smoke test: ${exe} --check`);
const result = spawnSync(exe, ["--check"], { stdio: "inherit" });
if (result.status !== 0) {
throw new Error(`smoke test failed (exit ${result.status})`);
}
}

function main() {
const triple = detectTargetTriple();
console.log(`target triple: ${triple}`);
ensureUv();
ensureVenv();
installDeps();
buildExe();
copySidecar(triple);
smokeTest();
console.log("OK: pyfing sidecar built");
}

try {
main();
} catch (err) {
console.error(err.message || err);
process.exit(1);
}
31 changes: 31 additions & 0 deletions scripts/ensure-pyfing-sidecar.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Idempotent: ensures src-tauri/bin/pyfing_enhance.exe exists.
// If missing, runs build-pyfing-sidecar.cjs to build it.
//
// Used as a pre-build step so `pnpm tauri build` "just works" on a fresh clone.

const fs = require("fs");
const path = require("path");
const { spawnSync } = require("child_process");

const ROOT = path.resolve(__dirname, "..");
const IS_WINDOWS = process.platform === "win32";
const EXE_EXT = IS_WINDOWS ? ".exe" : "";
const SIDECAR = path.join(
ROOT,
"src-tauri",
"bin",
`pyfing_enhance${EXE_EXT}`
);

if (fs.existsSync(SIDECAR)) {
console.log(`pyfing sidecar already present: ${SIDECAR}`);
process.exit(0);
}

console.log("pyfing sidecar missing, building it now...");
const result = spawnSync(
"node",
[path.join(__dirname, "build-pyfing-sidecar.cjs")],
{ stdio: "inherit", shell: false }
);
process.exit(result.status === null ? 1 : result.status);
15 changes: 15 additions & 0 deletions scripts/pyfing-requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
pyfing==0.6
opencv-python-headless==4.10.0.84
numpy==1.26.4
keras==3.5.0

# TensorFlow varies per platform:
# - Windows (x86_64) and Linux (x86_64): tensorflow-cpu wheels are available
# - Intel macOS: tensorflow-cpu also available
# - Apple Silicon macOS (arm64): only `tensorflow` provides arm64 wheels
tensorflow-cpu==2.17.0; sys_platform == "win32"
tensorflow-cpu==2.17.0; sys_platform == "linux"
tensorflow-cpu==2.17.0; sys_platform == "darwin" and platform_machine == "x86_64"
tensorflow==2.17.0; sys_platform == "darwin" and platform_machine == "arm64"

pyinstaller==6.10.0
Loading