mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-07-27 07:35:22 +00:00
Building and running tauri with jlink locally
This commit is contained in:
parent
aed4ccd07b
commit
6a75ecc6ae
228
JLINK-README.md
Normal file
228
JLINK-README.md
Normal file
@ -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
|
128
build-tauri-jlink.bat
Normal file
128
build-tauri-jlink.bat
Normal file
@ -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!
|
230
build-tauri-jlink.sh
Normal file
230
build-tauri-jlink.sh
Normal file
@ -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!"
|
1
frontend/src-tauri/.gitignore
vendored
1
frontend/src-tauri/.gitignore
vendored
@ -2,3 +2,4 @@
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
/gen/schemas
|
||||
/runtime/
|
22
frontend/src-tauri/runtime/launch-stirling.bat
Normal file
22
frontend/src-tauri/runtime/launch-stirling.bat
Normal file
@ -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%" ^
|
||||
%*
|
@ -27,10 +27,10 @@ async fn get_backend_logs() -> Result<Vec<String>, 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<String, String> {
|
||||
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<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
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<String, String> {
|
||||
.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<String, String> {
|
||||
});
|
||||
|
||||
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<String, String> {
|
||||
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<String, String> {
|
||||
*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<String, String> {
|
||||
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<String, String> {
|
||||
// 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<bool, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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<String, String> {
|
||||
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<String> = 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<String, String> {
|
||||
}
|
||||
}
|
||||
|
||||
// Command to check Java environment
|
||||
// Command to check Java environment (bundled version)
|
||||
#[tauri::command]
|
||||
async fn check_java_environment() -> Result<String, String> {
|
||||
println!("🔍 Checking Java environment...");
|
||||
async fn check_java_environment(app: tauri::AppHandle) -> Result<String, String> {
|
||||
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));
|
||||
|
@ -32,7 +32,8 @@
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"resources": [
|
||||
"libs/*.jar"
|
||||
"libs/*.jar",
|
||||
"runtime/jre/**/*"
|
||||
]
|
||||
},
|
||||
"plugins": {
|
||||
|
@ -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 (
|
||||
<div className="min-h-screen bg-gray-100">
|
||||
|
Loading…
x
Reference in New Issue
Block a user