Skip to content

Commit ab97285

Browse files
committed
feat: switch PyInstaller backend from --onefile to --onedir for faster startup
Eliminates the ~5-15s extraction delay on every launch by keeping PyInstaller output as a directory (exe + _internal/) bundled as Tauri resources instead of a single-file sidecar. - Convert .spec to onedir mode (COLLECT + exclude_binaries=True) - Move backend from externalBin to resources in tauri.conf.json - Replace shell sidecar spawn with std::process::Command - Deep-codesign all .dylib/.so on macOS (inner-to-outer) - Skip splash screen on updates page
1 parent 7277385 commit ab97285

6 files changed

Lines changed: 147 additions & 46 deletions

File tree

backend/syft-space-backend.spec

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -24,20 +24,28 @@ pyz = PYZ(a.pure)
2424
exe = EXE(
2525
pyz,
2626
a.scripts,
27-
a.binaries,
28-
a.datas,
2927
[],
28+
exclude_binaries=True,
3029
name='syft-space-backend',
3130
debug=False,
3231
bootloader_ignore_signals=False,
3332
strip=False,
3433
upx=True,
3534
upx_exclude=[],
36-
runtime_tmpdir=None,
3735
console=True,
3836
disable_windowed_traceback=False,
3937
argv_emulation=False,
4038
target_arch=None,
4139
codesign_identity=None,
4240
entitlements_file=None,
4341
)
42+
43+
coll = COLLECT(
44+
exe,
45+
a.binaries,
46+
a.datas,
47+
strip=False,
48+
upx=True,
49+
upx_exclude=[],
50+
name='syft-space-backend',
51+
)

frontend/src/App.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ import 'vue-sonner/style.css'
1212
const route = useRoute()
1313
const serverStore = useServerAvailabilityStore()
1414
15+
const isUpdatesPage = computed(() => route.name === 'updates')
16+
1517
const showNavbar = computed(
1618
() =>
1719
route.name !== 'create' &&
1820
!route.path.startsWith('/create/') &&
1921
!route.path.startsWith('/experimental') &&
20-
route.name !== 'updates' &&
22+
!isUpdatesPage.value &&
2123
route.name !== 'onboarding',
2224
)
2325
@@ -37,7 +39,7 @@ watch(
3739
</script>
3840

3941
<template>
40-
<SplashScreen v-if="!serverStore.isReady" :is-slow="serverStore.isSlow" />
42+
<SplashScreen v-if="!serverStore.isReady && !isUpdatesPage" :is-slow="serverStore.isSlow" />
4143
<div v-else class="min-h-screen bg-background text-foreground">
4244
<AppNavbar v-if="showNavbar" />
4345

frontend/src/router/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,12 @@ router.beforeEach(async (to, _from, next) => {
145145
return
146146
}
147147

148+
// Skip server readiness and onboarding checks for the updates page
149+
if (to.name === 'updates') {
150+
next()
151+
return
152+
}
153+
148154
// Wait for backend to be available before checking onboarding
149155
const serverStore = useServerAvailabilityStore()
150156
await serverStore.waitUntilReady()

src-tauri/before_build.sh

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,24 +28,40 @@ else
2828
echo "process-wick already present"
2929
fi
3030

31-
# 2. Build backend with PyInstaller
31+
# 2. Build backend with PyInstaller (--onedir mode via .spec)
3232
cd "$PROJECT_ROOT"
33+
rm -rf dist/syft-space-backend build/syft-space-backend
3334
uv run pyinstaller backend/syft-space-backend.spec
3435

35-
# 3. On macOS, codesign the PyInstaller binary with entitlements to allow
36-
# loading the embedded Python library (which has a different Team ID).
37-
# Without this, macOS library validation blocks the Python dylib at runtime.
36+
# 3. Copy the onedir output to src-tauri/target/syft-space-backend-dist/
37+
# Tauri resources don't use the target-triple suffix convention.
38+
BACKEND_DIST="src-tauri/target/syft-space-backend-dist"
39+
rm -rf "$BACKEND_DIST"
40+
mkdir -p "$BACKEND_DIST"
41+
cp -R dist/syft-space-backend/* "$BACKEND_DIST/"
42+
43+
# 4. Ensure the main executable is executable
44+
chmod +x "$BACKEND_DIST/syft-space-backend${EXE_EXT}"
45+
46+
# 5. On macOS, codesign all shared libraries and the main executable.
47+
# PyInstaller dylibs have different Team IDs, so we need entitlements
48+
# to disable library validation. Sign inner-to-outer (libs first, then exe).
3849
if [[ "$TARGET_TRIPLE" == *"apple"* ]]; then
3950
ENTITLEMENTS="$PROJECT_ROOT/src-tauri/entitlements.plist"
4051
SIGN_IDENTITY="${APPLE_SIGNING_IDENTITY:--}"
41-
echo "Codesigning PyInstaller binary with entitlements (identity: $SIGN_IDENTITY)..."
52+
echo "Codesigning PyInstaller onedir output (identity: $SIGN_IDENTITY)..."
53+
54+
# Sign all shared libraries first (.dylib, .so)
55+
find "$BACKEND_DIST" -type f \( -name "*.dylib" -o -name "*.so" \) | while read -r lib; do
56+
codesign --force --options runtime --entitlements "$ENTITLEMENTS" \
57+
--sign "$SIGN_IDENTITY" "$lib"
58+
done
59+
60+
# Sign the main executable last
4261
codesign --force --options runtime --entitlements "$ENTITLEMENTS" \
43-
--sign "$SIGN_IDENTITY" "dist/syft-space-backend${EXE_EXT}"
62+
--sign "$SIGN_IDENTITY" "$BACKEND_DIST/syft-space-backend${EXE_EXT}"
63+
echo "Codesigning complete."
4464
fi
4565

46-
# 4. Copy backend binary with target triple suffix
47-
mkdir -p src-tauri/target
48-
cp "dist/syft-space-backend${EXE_EXT}" "src-tauri/target/syft-space-backend-${TARGET_TRIPLE}${EXE_EXT}"
49-
50-
# 5. Build frontend
66+
# 6. Build frontend
5167
VITE_API_BASE_URL=http://localhost:8080/api/v1 bun run --cwd frontend build

src-tauri/src/lib.rs

Lines changed: 94 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,28 @@ fn find_child_process_pids() -> Vec<String> {
2525
child_process_pids
2626
}
2727

28+
/// Resolve the path to the syft-space-backend executable.
29+
/// In debug builds, SYFT_BACKEND_PATH env var can override the path.
30+
fn resolve_backend_path(app: &tauri::App) -> std::path::PathBuf {
31+
if cfg!(debug_assertions) {
32+
if let Ok(override_path) = std::env::var("SYFT_BACKEND_PATH") {
33+
return std::path::PathBuf::from(override_path);
34+
}
35+
}
36+
37+
let exe_name = if cfg!(windows) {
38+
"syft-space-backend.exe"
39+
} else {
40+
"syft-space-backend"
41+
};
42+
43+
app.path()
44+
.resource_dir()
45+
.expect("failed to resolve resource directory")
46+
.join("syft-space-backend")
47+
.join(exe_name)
48+
}
49+
2850
#[cfg_attr(mobile, tauri::mobile_entry_point)]
2951
pub fn run() {
3052
tauri::Builder::default()
@@ -80,15 +102,33 @@ pub fn run() {
80102
let (host, port, token) = utils::generate_server_args();
81103
log::info!("Server args - host: {}, port: {}", host, port);
82104

83-
// Spawn the Python backend sidecar with connection params
84-
let sidecar = app
85-
.shell()
86-
.sidecar("syft-space-backend")
87-
.unwrap()
105+
// Resolve the backend executable path from resources
106+
let backend_path = resolve_backend_path(app);
107+
log::info!("Backend path: {:?}", backend_path);
108+
109+
// Ensure the backend binary is executable on Unix
110+
#[cfg(unix)]
111+
{
112+
use std::os::unix::fs::PermissionsExt;
113+
if let Ok(metadata) = std::fs::metadata(&backend_path) {
114+
let mut perms = metadata.permissions();
115+
let mode = perms.mode();
116+
if mode & 0o111 == 0 {
117+
perms.set_mode(mode | 0o755);
118+
let _ = std::fs::set_permissions(&backend_path, perms);
119+
}
120+
}
121+
}
122+
123+
// Spawn the Python backend using std::process::Command
124+
let mut child = std::process::Command::new(&backend_path)
88125
.env("SYFT_HOST", &host)
89126
.env("SYFT_PORT", &port)
90-
.env("SYFT_ADMIN_API_KEY", &token);
91-
let (mut rx, _child) = sidecar.spawn().expect("failed to spawn sidecar");
127+
.env("SYFT_ADMIN_API_KEY", &token)
128+
.stdout(std::process::Stdio::piped())
129+
.stderr(std::process::Stdio::piped())
130+
.spawn()
131+
.expect("failed to spawn backend process");
92132

93133
// Create main window with connection params in URL
94134
let url = utils::generate_main_url(&host, &port, &token);
@@ -147,39 +187,65 @@ pub fn run() {
147187
if let Some(parent) = log_path.parent() {
148188
std::fs::create_dir_all(parent).ok();
149189
}
150-
tauri::async_runtime::spawn(async move {
151-
use std::io::Write;
152-
use tauri_plugin_shell::process::CommandEvent;
190+
191+
// Spawn stdout reader thread
192+
let stdout = child.stdout.take().expect("failed to take stdout");
193+
let stdout_log_path = log_path.clone();
194+
std::thread::spawn(move || {
195+
use std::io::{BufRead, BufReader, Write};
153196

154197
let mut log_file = std::fs::OpenOptions::new()
155198
.create(true)
156199
.append(true)
157-
.open(&log_path)
200+
.open(&stdout_log_path)
158201
.expect("failed to open syft-space-backend log file");
159202

160-
while let Some(event) = rx.recv().await {
161-
match event {
162-
CommandEvent::Stdout(line) => {
163-
let msg = String::from_utf8_lossy(&line);
164-
log::info!("[backend] {}", msg);
165-
let _ = writeln!(log_file, "{}", msg);
166-
}
167-
CommandEvent::Stderr(line) => {
168-
let msg = String::from_utf8_lossy(&line);
169-
log::error!("[backend] {}", msg);
170-
let _ = writeln!(log_file, "{}", msg);
203+
let reader = BufReader::new(stdout);
204+
for line in reader.lines() {
205+
match line {
206+
Ok(line) => {
207+
log::info!("[backend] {}", line);
208+
let _ = writeln!(log_file, "{}", line);
171209
}
172-
CommandEvent::Terminated(status) => {
173-
let msg = format!("terminated with {:?}", status);
174-
log::warn!("[backend] {}", msg);
175-
let _ = writeln!(log_file, "{}", msg);
176-
break;
210+
Err(_) => break,
211+
}
212+
}
213+
});
214+
215+
// Spawn stderr reader thread
216+
let stderr = child.stderr.take().expect("failed to take stderr");
217+
let stderr_log_path = log_path.clone();
218+
std::thread::spawn(move || {
219+
use std::io::{BufRead, BufReader, Write};
220+
221+
let mut log_file = std::fs::OpenOptions::new()
222+
.create(true)
223+
.append(true)
224+
.open(&stderr_log_path)
225+
.expect("failed to open syft-space-backend log file");
226+
227+
let reader = BufReader::new(stderr);
228+
for line in reader.lines() {
229+
match line {
230+
Ok(line) => {
231+
log::error!("[backend] {}", line);
232+
let _ = writeln!(log_file, "{}", line);
177233
}
178-
_ => {}
234+
Err(_) => break,
179235
}
180236
}
181237
});
182238

239+
// Spawn thread to wait for the backend process to exit and log the status
240+
std::thread::spawn(move || match child.wait() {
241+
Ok(status) => {
242+
log::warn!("[backend] terminated with status: {:?}", status);
243+
}
244+
Err(e) => {
245+
log::error!("[backend] error waiting for process: {:?}", e);
246+
}
247+
});
248+
183249
// Start periodic update checks
184250
updates::_start_periodic_update_checks(app.handle());
185251

src-tauri/tauri.conf.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"$schema": "https://schema.tauri.app/config/2",
33
"productName": "Syft Space",
4-
"version": "0.1.2",
4+
"version": "0.1.3",
55
"identifier": "org.openmined.syft-space",
66
"build": {
77
"frontendDist": "../frontend/dist",
@@ -19,7 +19,10 @@
1919
"bundle": {
2020
"active": true,
2121
"createUpdaterArtifacts": true,
22-
"externalBin": ["target/syft-space-backend", "target/process-wick"],
22+
"externalBin": ["target/process-wick"],
23+
"resources": {
24+
"target/syft-space-backend-dist/": "syft-space-backend/"
25+
},
2326
"icon": [
2427
"icons/32x32.png",
2528
"icons/128x128.png",

0 commit comments

Comments
 (0)