From 6a75ecc6ae210e7305303deca478225b0de4aeea Mon Sep 17 00:00:00 2001 From: Connor Yoh Date: Fri, 13 Jun 2025 11:55:09 +0100 Subject: [PATCH] Building and running tauri with jlink locally --- JLINK-README.md | 228 ++++++++++++++++ build-tauri-jlink.bat | 128 +++++++++ build-tauri-jlink.sh | 230 ++++++++++++++++ frontend/src-tauri/.gitignore | 1 + .../src-tauri/runtime/launch-stirling.bat | 22 ++ frontend/src-tauri/src/lib.rs | 254 ++++++++++-------- frontend/src-tauri/tauri.conf.json | 3 +- frontend/src/App.tsx | 2 +- 8 files changed, 756 insertions(+), 112 deletions(-) create mode 100644 JLINK-README.md create mode 100644 build-tauri-jlink.bat create mode 100644 build-tauri-jlink.sh create mode 100644 frontend/src-tauri/runtime/launch-stirling.bat diff --git a/JLINK-README.md b/JLINK-README.md new file mode 100644 index 000000000..6b9798679 --- /dev/null +++ b/JLINK-README.md @@ -0,0 +1,228 @@ +# JLink Runtime Bundling for Stirling-PDF + +This guide explains how to use JLink to bundle a custom Java runtime with your Tauri application, eliminating the need for users to have Java installed. + +## Overview + +Instead of requiring users to install a JRE separately, JLink creates a minimal, custom Java runtime that includes only the modules your application needs. This approach: + +- **Eliminates JRE dependency**: Users don't need Java installed +- **Reduces size**: Only includes necessary Java modules +- **Improves security**: Minimal attack surface with fewer modules +- **Ensures consistency**: Same Java version across all deployments + +## Prerequisites + +- **JDK 17 or higher** (not just JRE - you need `jlink` command) +- **Node.js and npm** for the frontend +- **Rust and Tauri CLI** for building the desktop app + +## Quick Start + +### 1. Build with JLink + +Run the appropriate build script for your platform: + +**Linux/macOS:** +```bash +./build-tauri-jlink.sh +``` + +**Windows:** +```cmd +build-tauri-jlink.bat +``` + +### 2. Build Tauri Application + +```bash +cd frontend +npm run tauri build +``` + +The resulting application will include the bundled JRE and won't require Java to be installed on the target system. + +## What the Build Script Does + +1. **Builds the Stirling-PDF JAR** using Gradle +2. **Analyzes dependencies** using `jdeps` to determine required Java modules +3. **Creates custom JRE** using `jlink` with only necessary modules +4. **Copies files** to the correct Tauri directories: + - JAR file → `frontend/src-tauri/libs/` + - Custom JRE → `frontend/src-tauri/runtime/jre/` +5. **Creates test launchers** for standalone testing + +## Directory Structure + +After running the build script: + +``` +frontend/src-tauri/ +├── libs/ +│ └── Stirling-PDF-X.X.X.jar +├── runtime/ +│ ├── jre/ # Custom JLink runtime +│ │ ├── bin/ +│ │ │ ├── java(.exe) +│ │ │ └── ... +│ │ ├── lib/ +│ │ └── ... +│ ├── launch-stirling.sh # Test launcher (Linux/macOS) +│ └── launch-stirling.bat # Test launcher (Windows) +└── tauri.conf.json # Already configured to bundle runtime +``` + +## Testing the Bundled Runtime + +Before building the full Tauri app, you can test the bundled runtime: + +**Linux/macOS:** +```bash +./frontend/src-tauri/runtime/launch-stirling.sh +``` + +**Windows:** +```cmd +frontend\src-tauri\runtime\launch-stirling.bat +``` + +This will start Stirling-PDF using the bundled JRE, accessible at http://localhost:8080 + +## Configuration Details + +### Tauri Configuration (`tauri.conf.json`) + +The bundle resources are configured to include both the JAR and runtime: + +```json +{ + "bundle": { + "resources": [ + "libs/*.jar", + "runtime/jre/**/*" + ] + } +} +``` + +### Gradle Configuration (`build.gradle`) + +JLink options are configured in the jpackage section: + +```gradle +jLinkOptions = [ + "--strip-debug", + "--compress=2", + "--no-header-files", + "--no-man-pages" +] + +addModules = [ + "java.base", + "java.desktop", + "java.logging", + "java.management", + // ... other required modules +] +``` + +### Rust Code (`lib.rs`) + +The application automatically detects and uses the bundled JRE instead of system Java. + +## Modules Included + +The custom runtime includes these Java modules: + +- `java.base` - Core Java functionality +- `java.desktop` - AWT/Swing (for UI components) +- `java.instrument` - Java instrumentation (required by Jetty) +- `java.logging` - Logging framework +- `java.management` - JMX and monitoring +- `java.naming` - JNDI services +- `java.net.http` - HTTP client +- `java.security.jgss` - Security services +- `java.sql` - Database connectivity +- `java.xml` - XML processing +- `java.xml.crypto` - XML security +- `jdk.crypto.ec` - Elliptic curve cryptography +- `jdk.crypto.cryptoki` - PKCS#11 support +- `jdk.unsupported` - Internal APIs (used by some libraries) + +## Troubleshooting + +### JLink Not Found +``` +❌ jlink is not available +``` +**Solution**: Install a full JDK (not just JRE). JLink is included with JDK 9+. + +### Module Not Found During Runtime +If the application fails with module-related errors, you may need to add additional modules to the `addModules` list in `build.gradle`. + +### Large Runtime Size +The bundled runtime should be 50-80MB. If it's much larger: +- Ensure `--strip-debug` and `--compress=2` options are used +- Review the module list - remove unnecessary modules +- Consider using `--no-header-files` and `--no-man-pages` + +## Benefits Over Traditional JAR Approach + +| Aspect | Traditional JAR | JLink Bundle | +|--------|----------------|--------------| +| User Setup | Requires JRE installation | No Java installation needed | +| Distribution Size | Smaller JAR, but requires ~200MB JRE | Larger bundle (~80MB), but self-contained | +| Java Version | Depends on user's installed version | Consistent, controlled version | +| Security Updates | User manages JRE updates | Developer controls runtime version | +| Startup Time | May be faster (shared JRE) | Slightly slower (isolated runtime) | + +## Advanced Usage + +### Custom Module Analysis + +To analyze your specific JAR's module requirements: + +```bash +jdeps --print-module-deps --ignore-missing-deps build/libs/Stirling-PDF-*.jar +``` + +### Manual JLink Command + +If you want to create the runtime manually: + +```bash +jlink \ + --add-modules java.base,java.desktop,java.logging,java.management,java.naming,java.net.http,java.security.jgss,java.sql,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported \ + --strip-debug \ + --compress=2 \ + --no-header-files \ + --no-man-pages \ + --output frontend/src-tauri/runtime/jre +``` + +## Migration Guide + +### From JAR-based Tauri App + +1. Update your Tauri configuration to include the runtime resources +2. Update your Rust code to use the bundled JRE path +3. Run the JLink build script +4. Test the bundled runtime +5. Build and distribute the new self-contained app + +### Deployment + +The final Tauri application will be completely self-contained. Users can: +- Install the app normally (no Java installation required) +- Run the app immediately after installation +- Not worry about Java version compatibility issues + +## Support + +If you encounter issues with the JLink bundling: + +1. Ensure you have a JDK (not JRE) installed +2. Check that the Java version is 17 or higher +3. Verify that the build script completed successfully +4. Test the bundled runtime using the provided launcher scripts +5. Check the Tauri build logs for any missing resources \ No newline at end of file diff --git a/build-tauri-jlink.bat b/build-tauri-jlink.bat new file mode 100644 index 000000000..bda181233 --- /dev/null +++ b/build-tauri-jlink.bat @@ -0,0 +1,128 @@ +@echo off +REM Build script for Tauri with JLink runtime bundling +REM This script creates a self-contained Java runtime for Stirling-PDF + +echo 🔧 Building Stirling-PDF with JLink runtime for Tauri... + +echo ▶ Checking Java environment... +java -version >nul 2>&1 +if errorlevel 1 ( + echo ❌ Java is not installed or not in PATH + exit /b 1 +) + +jlink --version >nul 2>&1 +if errorlevel 1 ( + echo ❌ jlink is not available. Please ensure you have a JDK ^(not just JRE^) installed. + exit /b 1 +) + +echo ✅ Java and jlink detected + +echo ▶ Building Stirling-PDF JAR... +call gradlew.bat clean bootJar --no-daemon +if errorlevel 1 ( + echo ❌ Failed to build Stirling-PDF JAR + exit /b 1 +) + +REM Find the built JAR +for %%f in (build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f +if not exist "%STIRLING_JAR%" ( + echo ❌ No Stirling-PDF JAR found in build/libs/ + exit /b 1 +) + +echo ✅ Built JAR: %STIRLING_JAR% + +echo ▶ Creating Tauri directories... +if not exist "frontend\src-tauri\libs" mkdir "frontend\src-tauri\libs" +if not exist "frontend\src-tauri\runtime" mkdir "frontend\src-tauri\runtime" + +echo ▶ Copying JAR to Tauri libs directory... +copy "%STIRLING_JAR%" "frontend\src-tauri\libs\" +echo ✅ JAR copied to frontend\src-tauri\libs\ + +echo ▶ Creating custom JRE with jlink... +if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre" + +REM Use predefined module list for Windows (jdeps may not be available) +set MODULES=java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported + +echo ▶ Creating JLink runtime with modules: %MODULES% + +jlink ^ + --add-modules %MODULES% ^ + --strip-debug ^ + --compress=2 ^ + --no-header-files ^ + --no-man-pages ^ + --output "frontend\src-tauri\runtime\jre" + +if not exist "frontend\src-tauri\runtime\jre" ( + echo ❌ Failed to create JLink runtime + exit /b 1 +) + +echo ✅ JLink runtime created at frontend\src-tauri\runtime\jre + +echo ▶ Creating launcher scripts for testing... + +REM Create Windows launcher script +echo @echo off > "frontend\src-tauri\runtime\launch-stirling.bat" +echo REM Launcher script for Stirling-PDF with bundled JRE >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo. >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo set SCRIPT_DIR=%%~dp0 >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo set JRE_DIR=%%SCRIPT_DIR%%jre >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo set LIBS_DIR=%%SCRIPT_DIR%%..\libs >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo. >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo REM Find the Stirling-PDF JAR >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo for %%%%f in ("%%LIBS_DIR%%\Stirling-PDF-*.jar") do set STIRLING_JAR=%%%%f >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo. >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo if not exist "%%STIRLING_JAR%%" ^( >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo echo ❌ Stirling-PDF JAR not found in %%LIBS_DIR%% >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo exit /b 1 >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo ^) >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo. >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo REM Launch with bundled JRE >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo "%%JRE_DIR%%\bin\java.exe" ^^ >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo -Xmx2g ^^ >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo -DBROWSER_OPEN=true ^^ >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo -DSTIRLING_PDF_DESKTOP_UI=false ^^ >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo -jar "%%STIRLING_JAR%%" ^^ >> "frontend\src-tauri\runtime\launch-stirling.bat" +echo %%* >> "frontend\src-tauri\runtime\launch-stirling.bat" + +echo ✅ Created launcher scripts for testing + +echo ▶ Testing bundled JRE... +"frontend\src-tauri\runtime\jre\bin\java.exe" --version >nul 2>&1 +if errorlevel 1 ( + echo ❌ Bundled JRE test failed + exit /b 1 +) else ( + echo ✅ Bundled JRE works correctly +) + +echo. +echo ✅ 🎉 JLink build setup completed successfully! +echo. +echo 📊 Summary: +echo • JAR: %STIRLING_JAR% +echo • Runtime: frontend\src-tauri\runtime\jre +echo • Modules: %MODULES% +echo. +echo 📋 Next steps: +echo 1. cd frontend +echo 2. npm run tauri build +echo. +echo 💡 Testing: +echo • Test bundled runtime: frontend\src-tauri\runtime\launch-stirling.bat +echo • Tauri configuration already updated to include bundled JRE +echo. +echo 💡 Benefits: +echo • No external JRE dependency +echo • Smaller distribution size with custom runtime +echo • Better security with minimal required modules +echo • Consistent Java version across all deployments +echo. +echo ✅ The application will now run without requiring users to install Java! \ No newline at end of file diff --git a/build-tauri-jlink.sh b/build-tauri-jlink.sh new file mode 100644 index 000000000..ae4390fa6 --- /dev/null +++ b/build-tauri-jlink.sh @@ -0,0 +1,230 @@ +#!/bin/bash + +# Build script for Tauri with JLink runtime bundling +# This script creates a self-contained Java runtime for Stirling-PDF + +set -e + +echo "🔧 Building Stirling-PDF with JLink runtime for Tauri..." + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Function to print colored output +print_step() { + echo -e "${BLUE}▶ $1${NC}" +} + +print_success() { + echo -e "${GREEN}✅ $1${NC}" +} + +print_warning() { + echo -e "${YELLOW}⚠️ $1${NC}" +} + +print_error() { + echo -e "${RED}❌ $1${NC}" +} + +# Check if Java is installed and version +print_step "Checking Java environment..." +if ! command -v java &> /dev/null; then + print_error "Java is not installed or not in PATH" + exit 1 +fi + +if ! command -v jlink &> /dev/null; then + print_error "jlink is not available. Please ensure you have a JDK (not just JRE) installed." + exit 1 +fi + +JAVA_VERSION=$(java -version 2>&1 | head -n 1 | cut -d'"' -f2 | cut -d'.' -f1) +if [ "$JAVA_VERSION" -lt 17 ]; then + print_error "Java 17 or higher is required. Found Java $JAVA_VERSION" + exit 1 +fi + +print_success "Java $JAVA_VERSION detected with jlink support" + +# Check if jpackage is available (Java 14+) +if command -v jpackage &> /dev/null; then + print_success "jpackage is available for native packaging" +else + print_warning "jpackage is not available - using jlink only" +fi + +# Clean and build the Stirling-PDF JAR +print_step "Building Stirling-PDF JAR..." +./gradlew clean bootJar --no-daemon + +if [ ! -f "build/libs/Stirling-PDF-"*.jar ]; then + print_error "Failed to build Stirling-PDF JAR" + exit 1 +fi + +# Find the built JAR +STIRLING_JAR=$(ls build/libs/Stirling-PDF-*.jar | head -n 1) +print_success "Built JAR: $STIRLING_JAR" + +# Create directories for Tauri +TAURI_SRC_DIR="frontend/src-tauri" +TAURI_LIBS_DIR="$TAURI_SRC_DIR/libs" +TAURI_RUNTIME_DIR="$TAURI_SRC_DIR/runtime" + +print_step "Creating Tauri directories..." +mkdir -p "$TAURI_LIBS_DIR" +mkdir -p "$TAURI_RUNTIME_DIR" + +# Copy the JAR to Tauri libs directory +print_step "Copying JAR to Tauri libs directory..." +cp "$STIRLING_JAR" "$TAURI_LIBS_DIR/" +print_success "JAR copied to $TAURI_LIBS_DIR/" + +# Create a custom JRE using jlink +print_step "Creating custom JRE with jlink..." + +# Determine modules needed by analyzing the JAR +print_step "Analyzing JAR dependencies..." + +# Use jdeps to analyze module dependencies if available +if command -v jdeps &> /dev/null; then + print_step "Running jdeps analysis..." + REQUIRED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "") + if [ -n "$REQUIRED_MODULES" ]; then + print_success "jdeps detected modules: $REQUIRED_MODULES" + # Add additional modules we know Stirling-PDF needs + MODULES="$REQUIRED_MODULES,java.compiler,java.instrument,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" + else + print_warning "jdeps analysis failed, using predefined module list" + MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" + fi +else + print_warning "jdeps not available, using predefined module list" + MODULES="java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.net.http,java.prefs,java.rmi,java.scripting,java.security.jgss,java.security.sasl,java.sql,java.transaction.xa,java.xml,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported" +fi + +print_step "Creating JLink runtime with modules: $MODULES" + +# Remove existing runtime if present +rm -rf "$TAURI_RUNTIME_DIR/jre" + +# Create the custom JRE +jlink \ + --add-modules "$MODULES" \ + --strip-debug \ + --compress=2 \ + --no-header-files \ + --no-man-pages \ + --output "$TAURI_RUNTIME_DIR/jre" + +if [ ! -d "$TAURI_RUNTIME_DIR/jre" ]; then + print_error "Failed to create JLink runtime" + exit 1 +fi + +print_success "JLink runtime created at $TAURI_RUNTIME_DIR/jre" + +# Calculate runtime size +RUNTIME_SIZE=$(du -sh "$TAURI_RUNTIME_DIR/jre" | cut -f1) +print_success "Runtime size: $RUNTIME_SIZE" + +# Create launcher scripts for testing +print_step "Creating launcher scripts for testing..." + +LAUNCHER_SCRIPT="$TAURI_RUNTIME_DIR/launch-stirling.sh" +cat > "$LAUNCHER_SCRIPT" << 'EOF' +#!/bin/bash +# Launcher script for Stirling-PDF with bundled JRE + +# Get the directory of this script +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +JRE_DIR="$SCRIPT_DIR/jre" +LIBS_DIR="$(dirname "$SCRIPT_DIR")/libs" + +# Find the Stirling-PDF JAR +STIRLING_JAR=$(ls "$LIBS_DIR"/Stirling-PDF-*.jar | head -n 1) + +if [ ! -f "$STIRLING_JAR" ]; then + echo "❌ Stirling-PDF JAR not found in $LIBS_DIR" + exit 1 +fi + +# Launch with bundled JRE +"$JRE_DIR/bin/java" \ + -Xmx2g \ + -DBROWSER_OPEN=true \ + -DSTIRLING_PDF_DESKTOP_UI=false \ + -jar "$STIRLING_JAR" \ + "$@" +EOF + +chmod +x "$LAUNCHER_SCRIPT" + +# Create Windows launcher +LAUNCHER_BAT="$TAURI_RUNTIME_DIR/launch-stirling.bat" +cat > "$LAUNCHER_BAT" << 'EOF' +@echo off +REM Launcher script for Stirling-PDF with bundled JRE + +set SCRIPT_DIR=%~dp0 +set JRE_DIR=%SCRIPT_DIR%jre +set LIBS_DIR=%SCRIPT_DIR%..\libs + +REM Find the Stirling-PDF JAR +for %%f in ("%LIBS_DIR%\Stirling-PDF-*.jar") do set STIRLING_JAR=%%f + +if not exist "%STIRLING_JAR%" ( + echo ❌ Stirling-PDF JAR not found in %LIBS_DIR% + exit /b 1 +) + +REM Launch with bundled JRE +"%JRE_DIR%\bin\java.exe" ^ + -Xmx2g ^ + -DBROWSER_OPEN=true ^ + -DSTIRLING_PDF_DESKTOP_UI=false ^ + -jar "%STIRLING_JAR%" ^ + %* +EOF + +print_success "Created launcher scripts for testing" + +# Test the bundled runtime +print_step "Testing bundled JRE..." +if [ -f "$TAURI_RUNTIME_DIR/jre/bin/java" ]; then + JAVA_VERSION_OUTPUT=$("$TAURI_RUNTIME_DIR/jre/bin/java" --version 2>&1 | head -n 1) + print_success "Bundled JRE works: $JAVA_VERSION_OUTPUT" +else + print_error "Bundled JRE executable not found" + exit 1 +fi + +# Display summary +echo "" +print_success "🎉 JLink build setup completed successfully!" +echo "" +echo -e "${BLUE}📊 Summary:${NC}" +echo " • JAR: $STIRLING_JAR" +echo " • Runtime: $TAURI_RUNTIME_DIR/jre ($RUNTIME_SIZE)" +echo " • Modules: $MODULES" +echo "" +echo -e "${BLUE}📋 Next steps:${NC}" +echo " 1. cd frontend" +echo " 2. npm run tauri build" +echo "" +echo -e "${BLUE}💡 Testing:${NC}" +echo " • Test bundled runtime: $LAUNCHER_SCRIPT" +echo " • Tauri configuration already updated to include bundled JRE" +echo "" +echo -e "${BLUE}💡 Benefits:${NC}" +echo " • No external JRE dependency" +echo " • Smaller distribution size with custom runtime" +echo " • Better security with minimal required modules" +echo " • Consistent Java version across all deployments" +echo "" +print_success "The application will now run without requiring users to install Java!" \ No newline at end of file diff --git a/frontend/src-tauri/.gitignore b/frontend/src-tauri/.gitignore index 502406b4e..1db95a7c8 100644 --- a/frontend/src-tauri/.gitignore +++ b/frontend/src-tauri/.gitignore @@ -2,3 +2,4 @@ # will have compiled files and executables /target/ /gen/schemas +/runtime/ \ No newline at end of file diff --git a/frontend/src-tauri/runtime/launch-stirling.bat b/frontend/src-tauri/runtime/launch-stirling.bat new file mode 100644 index 000000000..b0b6aaa83 --- /dev/null +++ b/frontend/src-tauri/runtime/launch-stirling.bat @@ -0,0 +1,22 @@ +@echo off +REM Launcher script for Stirling-PDF with bundled JRE + +set SCRIPT_DIR=%~dp0 +set JRE_DIR=%SCRIPT_DIR%jre +set LIBS_DIR=%SCRIPT_DIR%..\libs + +REM Find the Stirling-PDF JAR +for %%f in ("%LIBS_DIR%\Stirling-PDF-*.jar") do set STIRLING_JAR=%%f + +if not exist "%STIRLING_JAR%" ( + echo ❌ Stirling-PDF JAR not found in %LIBS_DIR% + exit /b 1 +) + +REM Launch with bundled JRE +"%JRE_DIR%\bin\java.exe" ^ + -Xmx2g ^ + -DBROWSER_OPEN=true ^ + -DSTIRLING_PDF_DESKTOP_UI=false ^ + -jar "%STIRLING_JAR%" ^ + %* diff --git a/frontend/src-tauri/src/lib.rs b/frontend/src-tauri/src/lib.rs index 55ecb419d..2228de4a7 100644 --- a/frontend/src-tauri/src/lib.rs +++ b/frontend/src-tauri/src/lib.rs @@ -27,10 +27,10 @@ async fn get_backend_logs() -> Result, String> { Ok(logs.iter().cloned().collect()) } -// Command to start the backend sidecar +// Command to start the backend with bundled JRE #[tauri::command] async fn start_backend(app: tauri::AppHandle) -> Result { - add_log("🚀 Attempting to start backend sidecar...".to_string()); + add_log("🚀 start_backend() called - Attempting to start backend with bundled JRE...".to_string()); // Check if backend is already running { @@ -41,25 +41,36 @@ async fn start_backend(app: tauri::AppHandle) -> Result { } } - add_log("📋 Creating Java command to run JAR directly".to_string()); - - // Use Tauri's resource API to find the JAR file + // Use Tauri's resource API to find the bundled JRE and JAR let resource_dir = app.path().resource_dir().map_err(|e| { let error_msg = format!("❌ Failed to get resource directory: {}", e); add_log(error_msg.clone()); error_msg })?; - add_log(format!("🔍 Looking for JAR in Tauri resource directory: {:?}", resource_dir)); + add_log(format!("🔍 Resource directory: {:?}", resource_dir)); - // In dev mode, resources are in target/debug/libs, in production they're bundled + // Find the bundled JRE + let jre_dir = resource_dir.join("runtime").join("jre"); + let java_executable = if cfg!(windows) { + jre_dir.join("bin").join("java.exe") + } else { + jre_dir.join("bin").join("java") + }; + + if !java_executable.exists() { + let error_msg = format!("❌ Bundled JRE not found at: {:?}", java_executable); + add_log(error_msg.clone()); + return Err(error_msg); + } + + add_log(format!("✅ Found bundled JRE: {:?}", java_executable)); + + // Find the Stirling-PDF JAR let libs_dir = resource_dir.join("libs"); - add_log(format!("🔍 Checking libs directory: {:?}", libs_dir)); - - // Find all Stirling-PDF JAR files and pick the latest version let mut jar_files: Vec<_> = std::fs::read_dir(&libs_dir) .map_err(|e| { - let error_msg = format!("Failed to read libs directory: {}. Make sure the JAR is copied to frontend/src-tauri/libs/", e); + let error_msg = format!("Failed to read libs directory: {}. Make sure the JAR is copied to libs/", e); add_log(error_msg.clone()); error_msg })? @@ -72,12 +83,12 @@ async fn start_backend(app: tauri::AppHandle) -> Result { .collect(); if jar_files.is_empty() { - let error_msg = "No Stirling-PDF JAR found in Tauri resources/libs directory. Please run the build script to generate and copy the JAR.".to_string(); + let error_msg = "No Stirling-PDF JAR found in libs directory.".to_string(); add_log(error_msg.clone()); return Err(error_msg); } - // Sort by filename to get the latest version (assumes semantic versioning in filename) + // Sort by filename to get the latest version jar_files.sort_by(|a, b| { let name_a = a.file_name().to_string_lossy().to_string(); let name_b = b.file_name().to_string_lossy().to_string(); @@ -85,9 +96,20 @@ async fn start_backend(app: tauri::AppHandle) -> Result { }); let jar_path = jar_files[0].path(); - add_log(format!("📋 Selected latest JAR from {} available: {:?}", jar_files.len(), jar_path.file_name().unwrap())); + add_log(format!("📋 Selected JAR: {:?}", jar_path.file_name().unwrap())); + + // Normalize the paths to remove Windows UNC prefix \\?\ + let normalized_java_path = if cfg!(windows) { + let path_str = java_executable.to_string_lossy(); + if path_str.starts_with(r"\\?\") { + std::path::PathBuf::from(&path_str[4..]) // Remove \\?\ prefix + } else { + java_executable.clone() + } + } else { + java_executable.clone() + }; - // Normalize the path to remove Windows UNC prefix \\?\ let normalized_jar_path = if cfg!(windows) { let path_str = jar_path.to_string_lossy(); if path_str.starts_with(r"\\?\") { @@ -99,28 +121,38 @@ async fn start_backend(app: tauri::AppHandle) -> Result { jar_path.clone() }; - add_log(format!("📦 Found JAR file in resources: {:?}", jar_path)); + add_log(format!("📦 Found JAR file: {:?}", jar_path)); add_log(format!("📦 Normalized JAR path: {:?}", normalized_jar_path)); + add_log(format!("📦 Normalized Java path: {:?}", normalized_java_path)); // Log the equivalent command for external testing let java_command = format!( - "java -Xmx2g -DBROWSER_OPEN=false -DSTIRLING_PDF_DESKTOP_UI=true -jar \"{}\"", + "\"{}\" -Xmx2g -DBROWSER_OPEN=false -DSTIRLING_PDF_DESKTOP_UI=false -jar \"{}\"", + normalized_java_path.display(), normalized_jar_path.display() ); - add_log(format!("🔧 Equivalent command to run externally: {}", java_command)); + add_log(format!("🔧 Equivalent command: {}", java_command)); + + // Create Java command with bundled JRE using normalized paths + // Configure logging to write outside src-tauri to prevent dev server restarts + let temp_dir = std::env::temp_dir(); + let log_dir = temp_dir.join("stirling-pdf-logs"); + std::fs::create_dir_all(&log_dir).ok(); // Create log directory if it doesn't exist - // Create Java command directly let sidecar_command = app .shell() - .command("java") + .command(normalized_java_path.to_str().unwrap()) .args([ "-Xmx2g", "-DBROWSER_OPEN=false", + "-DSTIRLING_PDF_DESKTOP_UI=false", + &format!("-Dlogging.file.path={}", log_dir.display()), + "-Dlogging.file.name=stirling-pdf.log", "-jar", normalized_jar_path.to_str().unwrap() ]); - add_log("⚙️ Sidecar command created, attempting to spawn...".to_string()); + add_log("⚙️ Starting backend with bundled JRE...".to_string()); let (mut rx, child) = sidecar_command .spawn() @@ -136,7 +168,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result { *process_guard = Some(Arc::new(child)); } - add_log("✅ Sidecar spawned successfully, monitoring output...".to_string()); + add_log("✅ Backend started with bundled JRE, monitoring output...".to_string()); // Listen to sidecar output for debugging tokio::spawn(async move { @@ -221,7 +253,7 @@ async fn start_backend(app: tauri::AppHandle) -> Result { println!("⏳ Waiting for backend startup..."); tokio::time::sleep(std::time::Duration::from_millis(5000)).await; - Ok("Backend startup initiated successfully".to_string()) + Ok("Backend startup initiated successfully with bundled JRE".to_string()) } // Command to check if backend is healthy @@ -269,7 +301,7 @@ async fn get_backend_status() -> Result { // Try to check if process is still alive let pid = child.pid(); println!("🔍 Checking backend process status, PID: {}", pid); - Ok(format!("Backend process is running (PID: {})", pid)) + Ok(format!("Backend process is running with bundled JRE (PID: {})", pid)) }, None => Ok("Backend process is not running".to_string()), } @@ -296,77 +328,61 @@ async fn check_backend_port() -> Result { } } -// Command to check if JAR file exists +// Command to check bundled runtime and JAR #[tauri::command] async fn check_jar_exists(app: tauri::AppHandle) -> Result { - println!("🔍 Checking for JAR files in Tauri resources..."); + println!("🔍 Checking for bundled JRE and JAR files..."); - // Check in the Tauri resource directory (bundled) if let Ok(resource_dir) = app.path().resource_dir() { - let jar_path = resource_dir; - println!("Checking bundled resources: {:?}", jar_path); + let mut status_parts = Vec::new(); - if jar_path.exists() { - match std::fs::read_dir(&jar_path) { + // Check bundled JRE + let jre_dir = resource_dir.join("runtime").join("jre"); + let java_executable = if cfg!(windows) { + jre_dir.join("bin").join("java.exe") + } else { + jre_dir.join("bin").join("java") + }; + + if java_executable.exists() { + status_parts.push("✅ Bundled JRE found".to_string()); + } else { + status_parts.push("❌ Bundled JRE not found".to_string()); + } + + // Check JAR files + let libs_dir = resource_dir.join("libs"); + if libs_dir.exists() { + match std::fs::read_dir(&libs_dir) { Ok(entries) => { - let mut jar_files = Vec::new(); - for entry in entries { - if let Ok(entry) = entry { + let jar_files: Vec = entries + .filter_map(|entry| entry.ok()) + .filter(|entry| { let path = entry.path(); - if path.extension().and_then(|s| s.to_str()) == Some("jar") - && path.file_name() - .unwrap() - .to_string_lossy() - .contains("Stirling-PDF") { - jar_files.push(path.file_name().unwrap().to_string_lossy().to_string()); - } - } - } + path.extension().and_then(|s| s.to_str()) == Some("jar") + && path.file_name().unwrap().to_string_lossy().contains("Stirling-PDF") + }) + .map(|entry| entry.file_name().to_string_lossy().to_string()) + .collect(); + if !jar_files.is_empty() { - println!("✅ Found JAR files in bundled resources: {:?}", jar_files); - return Ok(format!("Found JAR files: {:?}", jar_files)); + status_parts.push(format!("✅ Found JAR files: {:?}", jar_files)); + } else { + status_parts.push("❌ No Stirling-PDF JAR files found".to_string()); } } Err(e) => { - println!("❌ Failed to read resource directory: {}", e); + status_parts.push(format!("❌ Failed to read libs directory: {}", e)); } } + } else { + status_parts.push("❌ Libs directory not found".to_string()); } + + Ok(status_parts.join("\n")) + } else { + Ok("❌ Could not access bundled resources".to_string()) } - - // Check in development mode location (libs directory) - let dev_jar_path = std::path::PathBuf::from("libs"); - println!("Checking development libs directory: {:?}", dev_jar_path); - - if dev_jar_path.exists() { - match std::fs::read_dir(&dev_jar_path) { - Ok(entries) => { - let mut jar_files = Vec::new(); - for entry in entries { - if let Ok(entry) = entry { - let path = entry.path(); - if path.extension().and_then(|s| s.to_str()) == Some("jar") - && path.file_name() - .unwrap() - .to_string_lossy() - .contains("Stirling-PDF") { - jar_files.push(path.file_name().unwrap().to_string_lossy().to_string()); - } - } - } - if !jar_files.is_empty() { - println!("✅ Found JAR files in development libs: {:?}", jar_files); - return Ok(format!("Found JAR files: {:?}", jar_files)); - } - } - Err(e) => { - println!("❌ Failed to read libs directory: {}", e); - } - } - } - - println!("❌ No Stirling-PDF JAR files found"); - Ok("No Stirling-PDF JAR files found. Please run './build-tauri.sh' or 'build-tauri.bat' to build and copy the JAR.".to_string()) } // Command to test sidecar binary directly @@ -387,33 +403,48 @@ async fn test_sidecar_binary(app: tauri::AppHandle) -> Result { } } -// Command to check Java environment +// Command to check Java environment (bundled version) #[tauri::command] -async fn check_java_environment() -> Result { - println!("🔍 Checking Java environment..."); +async fn check_java_environment(app: tauri::AppHandle) -> Result { + println!("🔍 Checking bundled Java environment..."); - let output = std::process::Command::new("java") - .arg("--version") - .output(); - - match output { - Ok(output) => { - if output.status.success() { - let stdout = String::from_utf8_lossy(&output.stdout); - let stderr = String::from_utf8_lossy(&output.stderr); - let version_info = if !stdout.is_empty() { stdout } else { stderr }; - println!("✅ Java found: {}", version_info); - Ok(format!("Java available: {}", version_info.trim())) - } else { - let stderr = String::from_utf8_lossy(&output.stderr); - println!("❌ Java command failed: {}", stderr); - Ok(format!("Java command failed: {}", stderr)) + if let Ok(resource_dir) = app.path().resource_dir() { + let jre_dir = resource_dir.join("runtime").join("jre"); + let java_executable = if cfg!(windows) { + jre_dir.join("bin").join("java.exe") + } else { + jre_dir.join("bin").join("java") + }; + + if java_executable.exists() { + let output = std::process::Command::new(&java_executable) + .arg("--version") + .output(); + + match output { + Ok(output) => { + if output.status.success() { + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + let version_info = if !stdout.is_empty() { stdout } else { stderr }; + println!("✅ Bundled Java found: {}", version_info); + Ok(format!("Bundled Java available: {}", version_info.trim())) + } else { + let stderr = String::from_utf8_lossy(&output.stderr); + println!("❌ Bundled Java command failed: {}", stderr); + Ok(format!("Bundled Java command failed: {}", stderr)) + } + } + Err(e) => { + println!("❌ Failed to execute bundled Java: {}", e); + Ok(format!("Failed to execute bundled Java: {}", e)) + } } + } else { + Ok("❌ Bundled JRE not found".to_string()) } - Err(e) => { - println!("❌ Java not found: {}", e); - Ok(format!("Java not found or not in PATH: {}", e)) - } + } else { + Ok("❌ Could not access bundled resources".to_string()) } } @@ -422,18 +453,21 @@ pub fn run() { tauri::Builder::default() .plugin(tauri_plugin_shell::init()) .setup(|app| { - if cfg!(debug_assertions) { - app.handle().plugin( - tauri_plugin_log::Builder::default() - .level(log::LevelFilter::Info) - .build(), - )?; - } + // Disable file logging in debug mode to prevent dev server restart loops + // if cfg!(debug_assertions) { + // app.handle().plugin( + // tauri_plugin_log::Builder::default() + // .level(log::LevelFilter::Info) + // .build(), + // )?; + // } // Automatically start the backend when Tauri starts let app_handle = app.handle().clone(); tauri::async_runtime::spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(1000)).await; // Small delay to ensure app is ready + add_log("🔄 Tauri app ready, starting backend...".to_string()); + match start_backend(app_handle).await { Ok(result) => { add_log(format!("🚀 Auto-started backend on Tauri startup: {}", result)); diff --git a/frontend/src-tauri/tauri.conf.json b/frontend/src-tauri/tauri.conf.json index 3b83c98cd..0adfa650e 100644 --- a/frontend/src-tauri/tauri.conf.json +++ b/frontend/src-tauri/tauri.conf.json @@ -32,7 +32,8 @@ "icons/icon.ico" ], "resources": [ - "libs/*.jar" + "libs/*.jar", + "runtime/jre/**/*" ] }, "plugins": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index db1cfc796..d6d6a5739 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -4,7 +4,7 @@ import HomePage from './pages/HomePage'; import { SidecarTest } from './components/SidecarTest'; export default function App() { - const [showTests, setShowTests] = useState(true); // Start with tests visible + const [showTests, setShowTests] = useState(false); // Start with app visible return (