V2 Tauri Build Actions (#3899)
# Description of Changes <!-- Please provide a summary of the changes, including: - What was changed - Why the change was made - Any challenges encountered Closes #(issue_number) --> --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com>
182
.github/workflows/README-tauri.md
vendored
Normal file
@ -0,0 +1,182 @@
|
||||
# Tauri Build Workflows
|
||||
|
||||
This directory contains GitHub Actions workflows for building Tauri desktop applications for Stirling-PDF.
|
||||
|
||||
## Workflow
|
||||
|
||||
### `tauri-build.yml` - Unified Build Workflow
|
||||
|
||||
**Purpose**: Build Tauri applications for all platforms (Windows, macOS, Linux) with comprehensive testing and validation.
|
||||
|
||||
**Triggers**:
|
||||
- Manual dispatch with platform selection (windows, macos, linux, or all)
|
||||
- Pull requests affecting Tauri-related files
|
||||
- Pushes to main branch affecting Tauri-related files
|
||||
|
||||
**Platforms**:
|
||||
- **Windows**: x86_64 (exe and msi)
|
||||
- **macOS**: Apple Silicon (aarch64) and Intel (x86_64) (dmg)
|
||||
- **Linux**: x86_64 (deb and AppImage)
|
||||
|
||||
**Features**:
|
||||
- **Dynamic Platform Selection**: Choose specific platforms or build all
|
||||
- **Smart JRE Bundling**: Uses JLink to create optimized custom JRE
|
||||
- **Apple Code Signing**: Full macOS notarization and signing support
|
||||
- **Comprehensive Validation**: Artifact verification and size checks
|
||||
- **Self-Contained**: No Java installation required for end users
|
||||
- **Cross-Platform**: Builds on actual target platforms for compatibility
|
||||
- **Detailed Logging**: Complete build process visibility
|
||||
|
||||
## Usage
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. **Test All Platforms**:
|
||||
```bash
|
||||
# Go to Actions tab in GitHub
|
||||
# Run "Build Tauri Applications" workflow
|
||||
# Select "all" for platform
|
||||
```
|
||||
|
||||
2. **Test Specific Platform**:
|
||||
```bash
|
||||
# Go to Actions tab in GitHub
|
||||
# Run "Build Tauri Applications" workflow
|
||||
# Select specific platform (windows/macos/linux)
|
||||
```
|
||||
|
||||
3. **Automatic Testing**:
|
||||
- Builds are automatically triggered on PRs and pushes
|
||||
- All platforms are tested by default
|
||||
- Artifacts are uploaded for download and testing
|
||||
|
||||
## Configuration
|
||||
|
||||
### Required Secrets
|
||||
|
||||
#### For macOS Code Signing (Required for distribution)
|
||||
|
||||
Configure these secrets in your repository for macOS app signing:
|
||||
|
||||
- `APPLE_CERTIFICATE`: Base64-encoded .p12 certificate file
|
||||
- `APPLE_CERTIFICATE_PASSWORD`: Password for the .p12 certificate
|
||||
- `APPLE_SIGNING_IDENTITY`: Certificate name (e.g., "Developer ID Application: Your Name")
|
||||
- `APPLE_ID`: Your Apple ID email
|
||||
- `APPLE_PASSWORD`: App-specific password for your Apple ID
|
||||
- `APPLE_TEAM_ID`: Your Apple Developer Team ID
|
||||
|
||||
#### Setting Up Apple Code Signing
|
||||
|
||||
1. **Get a Developer ID Certificate**:
|
||||
- Join the Apple Developer Program ($99/year)
|
||||
- Create a "Developer ID Application" certificate in Apple Developer portal
|
||||
- Download the certificate as a .p12 file
|
||||
|
||||
2. **Convert Certificate to Base64**:
|
||||
```bash
|
||||
base64 -i certificate.p12 | pbcopy
|
||||
```
|
||||
|
||||
3. **Create App-Specific Password**:
|
||||
- Go to appleid.apple.com → Sign-In and Security → App-Specific Passwords
|
||||
- Generate a new password for "Tauri CI"
|
||||
|
||||
4. **Find Your Team ID**:
|
||||
- Apple Developer portal → Membership → Team ID
|
||||
|
||||
5. **Add to GitHub Secrets**:
|
||||
- Repository → Settings → Secrets and variables → Actions
|
||||
- Add each secret with the exact names listed above
|
||||
|
||||
#### For General Tauri Signing (Optional)
|
||||
|
||||
- `TAURI_SIGNING_PRIVATE_KEY`: Private key for signing Tauri applications
|
||||
- `TAURI_SIGNING_PRIVATE_KEY_PASSWORD`: Password for the signing private key
|
||||
|
||||
### File Structure
|
||||
|
||||
The workflows expect this structure:
|
||||
```
|
||||
├── frontend/
|
||||
│ ├── src-tauri/
|
||||
│ │ ├── Cargo.toml
|
||||
│ │ ├── tauri.conf.json
|
||||
│ │ └── src/
|
||||
│ ├── package.json
|
||||
│ └── src/
|
||||
├── gradlew
|
||||
└── stirling-pdf/
|
||||
└── build/libs/
|
||||
```
|
||||
|
||||
## Validation
|
||||
|
||||
Both workflows include comprehensive validation:
|
||||
|
||||
1. **Build Validation**: Ensures all expected artifacts are created
|
||||
2. **Size Validation**: Checks artifacts aren't suspiciously small
|
||||
3. **Platform Validation**: Verifies platform-specific requirements
|
||||
4. **Integration Testing**: Tests that Java backend builds correctly
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Missing Dependencies**:
|
||||
- Ubuntu: Ensure system dependencies are installed
|
||||
- macOS: Check Rust toolchain targets
|
||||
- Windows: Verify MSVC tools are available
|
||||
|
||||
2. **Java Backend Build Fails**:
|
||||
- Check Gradle permissions (`chmod +x ./gradlew`)
|
||||
- Verify JDK 21 is properly configured
|
||||
|
||||
3. **Artifact Size Issues**:
|
||||
- Small artifacts usually indicate build failures
|
||||
- Check that backend JAR is properly copied to Tauri resources
|
||||
|
||||
4. **Signing Issues**:
|
||||
- Ensure signing secrets are configured if needed
|
||||
- Check that signing keys are valid
|
||||
|
||||
### Debugging
|
||||
|
||||
1. **Check Logs**: Each step provides detailed logging
|
||||
2. **Artifact Inspection**: Download artifacts to verify contents
|
||||
3. **Local Testing**: Test builds locally before running workflows
|
||||
|
||||
## JLink Integration Benefits
|
||||
|
||||
The workflows now use JLink to create custom Java runtimes:
|
||||
|
||||
### **Self-Contained Applications**
|
||||
- **No Java Required**: Users don't need Java installed
|
||||
- **Consistent Runtime**: Same Java version across all deployments
|
||||
- **Smaller Size**: Only includes needed Java modules (~30-50MB vs full JRE)
|
||||
|
||||
### **Security & Performance**
|
||||
- **Minimal Attack Surface**: Only required modules included
|
||||
- **Faster Startup**: Optimized runtime with stripped debug info
|
||||
- **Better Compression**: JLink level 2 compression reduces size
|
||||
|
||||
### **Module Analysis**
|
||||
- **Automatic Detection**: Uses `jdeps` to analyze JAR dependencies
|
||||
- **Fallback Safety**: Predefined module list if analysis fails
|
||||
- **Platform Optimized**: Different modules per platform if needed
|
||||
|
||||
## Integration with Existing Workflows
|
||||
|
||||
These workflows are designed to complement the existing build system:
|
||||
|
||||
- Uses same JDK and Gradle setup as `build.yml`
|
||||
- Follows same security practices as `multiOSReleases.yml`
|
||||
- Compatible with existing release processes
|
||||
- Integrates JLink logic from `build-tauri-jlink.sh/bat` scripts
|
||||
|
||||
## Next Steps
|
||||
|
||||
1. Test the workflows on your branch
|
||||
2. Verify all platforms build successfully
|
||||
3. Check artifact quality and sizes
|
||||
4. Configure signing if needed
|
||||
5. Merge when all tests pass
|
329
.github/workflows/tauri-build.yml
vendored
Normal file
@ -0,0 +1,329 @@
|
||||
name: Build Tauri Applications
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
platform:
|
||||
description: "Platform to build (windows, macos, linux, or all)"
|
||||
required: true
|
||||
default: "all"
|
||||
type: choice
|
||||
options:
|
||||
- all
|
||||
- windows
|
||||
- macos
|
||||
- linux
|
||||
pull_request:
|
||||
branches: [main]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
push:
|
||||
branches: [main, V2, "infra/tauri-actions"]
|
||||
paths:
|
||||
- 'frontend/src-tauri/**'
|
||||
- 'frontend/src/**'
|
||||
- 'frontend/package.json'
|
||||
- 'frontend/package-lock.json'
|
||||
- '.github/workflows/tauri-build.yml'
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
determine-matrix:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
matrix: ${{ steps.set-matrix.outputs.matrix }}
|
||||
steps:
|
||||
- name: Determine build matrix
|
||||
id: set-matrix
|
||||
run: |
|
||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
||||
case "${{ github.event.inputs.platform }}" in
|
||||
"windows")
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"macos")
|
||||
echo 'matrix={"include":[{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
"linux")
|
||||
echo 'matrix={"include":[{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
*)
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
;;
|
||||
esac
|
||||
else
|
||||
# For PR/push events, build all platforms
|
||||
echo 'matrix={"include":[{"platform":"windows-latest","args":"--target x86_64-pc-windows-msvc","name":"windows-x86_64"},{"platform":"macos-latest","args":"--target aarch64-apple-darwin","name":"macos-aarch64"},{"platform":"macos-13","args":"--target x86_64-apple-darwin","name":"macos-x86_64"},{"platform":"ubuntu-22.04","args":"","name":"linux-x86_64"}]}' >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
build:
|
||||
needs: determine-matrix
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix: ${{ fromJson(needs.determine-matrix.outputs.matrix) }}
|
||||
runs-on: ${{ matrix.platform }}
|
||||
steps:
|
||||
- name: Harden Runner
|
||||
uses: step-security/harden-runner@002fdce3c6a235733a90a27c80493a3241e56863 # v2.12.1
|
||||
with:
|
||||
egress-policy: audit
|
||||
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
|
||||
|
||||
- name: Install dependencies (ubuntu only)
|
||||
if: matrix.platform == 'ubuntu-22.04'
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libjavascriptcoregtk-4.0-dev libsoup2.4-dev libjavascriptcoregtk-4.1-dev libsoup-3.0-dev
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 20
|
||||
cache: 'npm'
|
||||
cache-dependency-path: frontend/package-lock.json
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
toolchain: stable
|
||||
targets: ${{ (matrix.platform == 'macos-latest' || matrix.platform == 'macos-13') && 'aarch64-apple-darwin,x86_64-apple-darwin' || '' }}
|
||||
|
||||
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@c5195efecf7bdfc987ee8bae7a71cb8b11521c00 # v4.7.1
|
||||
with:
|
||||
java-version: "21"
|
||||
distribution: "temurin"
|
||||
|
||||
- name: Build Java backend with JLink
|
||||
working-directory: ./
|
||||
shell: bash
|
||||
run: |
|
||||
chmod +x ./gradlew
|
||||
echo "🔧 Building Stirling-PDF JAR..."
|
||||
# STIRLING_PDF_DESKTOP_UI=false ./gradlew clean bootJar --no-daemon
|
||||
./gradlew clean build -x spotlessApply -x spotlessCheck -x test -x sonarqube
|
||||
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls app/core/build/libs/stirling-pdf-*.jar | head -n 1)
|
||||
echo "✅ Built JAR: $STIRLING_JAR"
|
||||
|
||||
# Create Tauri directories
|
||||
mkdir -p ./frontend/src-tauri/libs
|
||||
mkdir -p ./frontend/src-tauri/runtime
|
||||
|
||||
# Copy JAR to Tauri libs
|
||||
cp "$STIRLING_JAR" ./frontend/src-tauri/libs/
|
||||
echo "✅ JAR copied to Tauri libs"
|
||||
|
||||
# Analyze JAR dependencies for jlink modules
|
||||
echo "🔍 Analyzing JAR dependencies..."
|
||||
if command -v jdeps &> /dev/null; then
|
||||
DETECTED_MODULES=$(jdeps --print-module-deps --ignore-missing-deps "$STIRLING_JAR" 2>/dev/null || echo "")
|
||||
if [ -n "$DETECTED_MODULES" ]; then
|
||||
echo "📋 jdeps detected modules: $DETECTED_MODULES"
|
||||
MODULES="$DETECTED_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.sql,java.transaction.xa,java.xml.crypto,jdk.crypto.ec,jdk.crypto.cryptoki,jdk.unsupported"
|
||||
else
|
||||
echo "⚠️ jdeps analysis failed, using predefined modules"
|
||||
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
|
||||
echo "⚠️ jdeps not available, using predefined modules"
|
||||
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
|
||||
|
||||
# Create custom JRE with jlink (always rebuild)
|
||||
echo "🔧 Creating custom JRE with jlink..."
|
||||
echo "📋 Using modules: $MODULES"
|
||||
|
||||
# Remove any existing JRE
|
||||
rm -rf ./frontend/src-tauri/runtime/jre
|
||||
|
||||
# Create the custom JRE
|
||||
jlink \
|
||||
--add-modules "$MODULES" \
|
||||
--strip-debug \
|
||||
--compress=2 \
|
||||
--no-header-files \
|
||||
--no-man-pages \
|
||||
--output ./frontend/src-tauri/runtime/jre
|
||||
|
||||
if [ ! -d "./frontend/src-tauri/runtime/jre" ]; then
|
||||
echo "❌ Failed to create JLink runtime"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Test the bundled runtime
|
||||
if [ -f "./frontend/src-tauri/runtime/jre/bin/java" ]; then
|
||||
RUNTIME_VERSION=$(./frontend/src-tauri/runtime/jre/bin/java --version 2>&1 | head -n 1)
|
||||
echo "✅ Custom JRE created successfully: $RUNTIME_VERSION"
|
||||
else
|
||||
echo "❌ Custom JRE executable not found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Calculate runtime size
|
||||
RUNTIME_SIZE=$(du -sh ./frontend/src-tauri/runtime/jre | cut -f1)
|
||||
echo "📊 Custom JRE size: $RUNTIME_SIZE"
|
||||
env:
|
||||
DISABLE_ADDITIONAL_FEATURES: true
|
||||
|
||||
- name: Install frontend dependencies
|
||||
working-directory: ./frontend
|
||||
run: npm install
|
||||
|
||||
- name: Import Apple Developer Certificate
|
||||
if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
env:
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
|
||||
run: |
|
||||
echo "Importing Apple Developer Certificate..."
|
||||
echo $APPLE_CERTIFICATE | base64 --decode > certificate.p12
|
||||
security create-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security set-keychain-settings -t 3600 -u build.keychain
|
||||
security import certificate.p12 -k build.keychain -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign
|
||||
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" build.keychain
|
||||
security find-identity -v -p codesigning build.keychain
|
||||
- name: Verify Certificate
|
||||
if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
run: |
|
||||
echo "Verifying Apple Developer Certificate..."
|
||||
CERT_INFO=$(security find-identity -v -p codesigning build.keychain | grep "Developer ID Application")
|
||||
echo "Certificate Info: $CERT_INFO"
|
||||
CERT_ID=$(echo "$CERT_INFO" | awk -F'"' '{print $2}')
|
||||
echo "Certificate ID: $CERT_ID"
|
||||
echo "CERT_ID=$CERT_ID" >> $GITHUB_ENV
|
||||
echo "Certificate imported."
|
||||
|
||||
- name: Check DMG creation dependencies (macOS only)
|
||||
if: matrix.platform == 'macos-latest' || matrix.platform == 'macos-13'
|
||||
run: |
|
||||
echo "🔍 Checking DMG creation dependencies on ${{ matrix.platform }}..."
|
||||
echo "hdiutil version: $(hdiutil --version || echo 'NOT FOUND')"
|
||||
echo "create-dmg availability: $(which create-dmg || echo 'NOT FOUND')"
|
||||
echo "Available disk space: $(df -h /tmp | tail -1)"
|
||||
echo "macOS version: $(sw_vers -productVersion)"
|
||||
echo "Available tools:"
|
||||
ls -la /usr/bin/hd* || echo "No hd* tools found"
|
||||
|
||||
- name: Build Tauri app
|
||||
uses: tauri-apps/tauri-action@v0
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
|
||||
APPLE_SIGNING_IDENTITY: ${{ env.CERT_ID }}
|
||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||
APPLE_ID_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_PASSWORD: ${{ secrets.APPLE_ID_PASSWORD }}
|
||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||
APPIMAGETOOL_SIGN_PASSPHRASE: ${{ secrets.APPIMAGETOOL_SIGN_PASSPHRASE }}
|
||||
SIGN: 1
|
||||
CI: true
|
||||
with:
|
||||
projectPath: ./frontend
|
||||
tauriScript: npx tauri
|
||||
args: ${{ matrix.args }}
|
||||
|
||||
- name: Rename artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
mkdir -p ./dist
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
# Find and rename artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
find . -name "*.exe" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.exe" \;
|
||||
find . -name "*.msi" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.msi" \;
|
||||
elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
find . -name "*.dmg" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.dmg" \;
|
||||
find . -name "*.app" -exec cp -r {} "../../../dist/Stirling-PDF-${{ matrix.name }}.app" \;
|
||||
else
|
||||
find . -name "*.deb" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.deb" \;
|
||||
find . -name "*.AppImage" -exec cp {} "../../../dist/Stirling-PDF-${{ matrix.name }}.AppImage" \;
|
||||
fi
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
|
||||
with:
|
||||
name: Stirling-PDF-${{ matrix.name }}
|
||||
path: ./dist/*
|
||||
retention-days: 7
|
||||
|
||||
- name: Verify build artifacts
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
|
||||
# Check for expected artifacts based on platform
|
||||
if [ "${{ matrix.platform }}" = "windows-latest" ]; then
|
||||
echo "Checking for Windows artifacts..."
|
||||
find . -name "*.exe" -o -name "*.msi" | head -5
|
||||
if [ $(find . -name "*.exe" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No Windows executable found"
|
||||
exit 1
|
||||
fi
|
||||
elif [ "${{ matrix.platform }}" = "macos-latest" ] || [ "${{ matrix.platform }}" = "macos-13" ]; then
|
||||
echo "Checking for macOS artifacts..."
|
||||
find . -name "*.dmg" -o -name "*.app" | head -5
|
||||
if [ $(find . -name "*.dmg" -o -name "*.app" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No macOS artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Checking for Linux artifacts..."
|
||||
find . -name "*.deb" -o -name "*.AppImage" | head -5
|
||||
if [ $(find . -name "*.deb" -o -name "*.AppImage" | wc -l) -eq 0 ]; then
|
||||
echo "❌ No Linux artifacts found"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "✅ Build artifacts found for ${{ matrix.name }}"
|
||||
|
||||
- name: Test artifact sizes
|
||||
shell: bash
|
||||
run: |
|
||||
cd ./frontend/src-tauri/target
|
||||
echo "Artifact sizes for ${{ matrix.name }}:"
|
||||
find . -name "*.exe" -o -name "*.dmg" -o -name "*.deb" -o -name "*.AppImage" -o -name "*.msi" | while read file; do
|
||||
if [ -f "$file" ]; then
|
||||
size=$(stat -c%s "$file" 2>/dev/null || stat -f%z "$file" 2>/dev/null || echo "unknown")
|
||||
echo "$file: $size bytes"
|
||||
# Check if file is suspiciously small (less than 1MB)
|
||||
if [ "$size" != "unknown" ] && [ "$size" -lt 1048576 ]; then
|
||||
echo "⚠️ Warning: $file is smaller than 1MB"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
report:
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
if: always()
|
||||
steps:
|
||||
- name: Report build results
|
||||
run: |
|
||||
if [ "${{ needs.build.result }}" = "success" ]; then
|
||||
echo "✅ All Tauri builds completed successfully!"
|
||||
echo "Artifacts are ready for distribution."
|
||||
else
|
||||
echo "❌ Some Tauri builds failed."
|
||||
echo "Please check the logs and fix any issues."
|
||||
exit 1
|
||||
fi
|
@ -29,7 +29,6 @@ dependencies {
|
||||
if (System.getenv('STIRLING_PDF_DESKTOP_UI') != 'false'
|
||||
|| (project.hasProperty('STIRLING_PDF_DESKTOP_UI')
|
||||
&& project.getProperty('STIRLING_PDF_DESKTOP_UI') != 'false')) {
|
||||
implementation 'me.friwi:jcefmaven:132.3.1'
|
||||
implementation 'org.openjfx:javafx-controls:21'
|
||||
implementation 'org.openjfx:javafx-swing:21'
|
||||
}
|
||||
|
@ -1,497 +0,0 @@
|
||||
package stirling.software.SPDF.UI.impl;
|
||||
|
||||
import java.awt.AWTException;
|
||||
import java.awt.BorderLayout;
|
||||
import java.awt.Frame;
|
||||
import java.awt.Image;
|
||||
import java.awt.MenuItem;
|
||||
import java.awt.PopupMenu;
|
||||
import java.awt.SystemTray;
|
||||
import java.awt.TrayIcon;
|
||||
import java.awt.event.WindowEvent;
|
||||
import java.awt.event.WindowStateListener;
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.swing.JFrame;
|
||||
import javax.swing.JPanel;
|
||||
import javax.swing.SwingUtilities;
|
||||
import javax.swing.Timer;
|
||||
|
||||
import org.cef.CefApp;
|
||||
import org.cef.CefClient;
|
||||
import org.cef.CefSettings;
|
||||
import org.cef.browser.CefBrowser;
|
||||
import org.cef.callback.CefBeforeDownloadCallback;
|
||||
import org.cef.callback.CefDownloadItem;
|
||||
import org.cef.callback.CefDownloadItemCallback;
|
||||
import org.cef.handler.CefDownloadHandlerAdapter;
|
||||
import org.cef.handler.CefLoadHandlerAdapter;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import jakarta.annotation.PreDestroy;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import me.friwi.jcefmaven.CefAppBuilder;
|
||||
import me.friwi.jcefmaven.EnumProgress;
|
||||
import me.friwi.jcefmaven.MavenCefAppHandlerAdapter;
|
||||
import me.friwi.jcefmaven.impl.progress.ConsoleProgressHandler;
|
||||
|
||||
import stirling.software.SPDF.UI.WebBrowser;
|
||||
import stirling.software.common.configuration.InstallationPathConfig;
|
||||
import stirling.software.common.util.UIScaling;
|
||||
|
||||
@Component
|
||||
@Slf4j
|
||||
@ConditionalOnProperty(
|
||||
name = "STIRLING_PDF_DESKTOP_UI",
|
||||
havingValue = "true",
|
||||
matchIfMissing = false)
|
||||
public class DesktopBrowser implements WebBrowser {
|
||||
private static CefApp cefApp;
|
||||
private static CefClient client;
|
||||
private static CefBrowser browser;
|
||||
private static JFrame frame;
|
||||
private static LoadingWindow loadingWindow;
|
||||
private static volatile boolean browserInitialized = false;
|
||||
private static TrayIcon trayIcon;
|
||||
private static SystemTray systemTray;
|
||||
|
||||
public DesktopBrowser() {
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
loadingWindow = new LoadingWindow(null, "Initializing...");
|
||||
loadingWindow.setVisible(true);
|
||||
});
|
||||
}
|
||||
|
||||
public void initWebUI(String url) {
|
||||
CompletableFuture.runAsync(
|
||||
() -> {
|
||||
try {
|
||||
CefAppBuilder builder = new CefAppBuilder();
|
||||
configureCefSettings(builder);
|
||||
builder.setProgressHandler(createProgressHandler());
|
||||
builder.setInstallDir(
|
||||
new File(InstallationPathConfig.getClientWebUIPath()));
|
||||
// Build and initialize CEF
|
||||
cefApp = builder.build();
|
||||
client = cefApp.createClient();
|
||||
|
||||
// Set up download handler
|
||||
setupDownloadHandler();
|
||||
|
||||
// Create browser and frame on EDT
|
||||
SwingUtilities.invokeAndWait(
|
||||
() -> {
|
||||
browser = client.createBrowser(url, false, false);
|
||||
setupMainFrame();
|
||||
setupLoadHandler();
|
||||
|
||||
// Force initialize UI after 7 seconds if not already done
|
||||
Timer timeoutTimer =
|
||||
new Timer(
|
||||
2500,
|
||||
e -> {
|
||||
log.warn(
|
||||
"Loading timeout reached. Forcing"
|
||||
+ " UI transition.");
|
||||
if (!browserInitialized) {
|
||||
// Force UI initialization
|
||||
forceInitializeUI();
|
||||
}
|
||||
});
|
||||
timeoutTimer.setRepeats(false);
|
||||
timeoutTimer.start();
|
||||
});
|
||||
} catch (Exception e) {
|
||||
log.error("Error initializing JCEF browser: ", e);
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void configureCefSettings(CefAppBuilder builder) {
|
||||
CefSettings settings = builder.getCefSettings();
|
||||
String basePath = InstallationPathConfig.getClientWebUIPath();
|
||||
log.info("basePath " + basePath);
|
||||
settings.cache_path = new File(basePath + "cache").getAbsolutePath();
|
||||
settings.root_cache_path = new File(basePath + "root_cache").getAbsolutePath();
|
||||
// settings.browser_subprocess_path = new File(basePath +
|
||||
// "subprocess").getAbsolutePath();
|
||||
// settings.resources_dir_path = new File(basePath + "resources").getAbsolutePath();
|
||||
// settings.locales_dir_path = new File(basePath + "locales").getAbsolutePath();
|
||||
settings.log_file = new File(basePath, "debug.log").getAbsolutePath();
|
||||
|
||||
settings.persist_session_cookies = true;
|
||||
settings.windowless_rendering_enabled = false;
|
||||
settings.log_severity = CefSettings.LogSeverity.LOGSEVERITY_INFO;
|
||||
|
||||
builder.setAppHandler(
|
||||
new MavenCefAppHandlerAdapter() {
|
||||
@Override
|
||||
public void stateHasChanged(org.cef.CefApp.CefAppState state) {
|
||||
log.info("CEF state changed: " + state);
|
||||
if (state == CefApp.CefAppState.TERMINATED) {
|
||||
System.exit(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupDownloadHandler() {
|
||||
client.addDownloadHandler(
|
||||
new CefDownloadHandlerAdapter() {
|
||||
@Override
|
||||
public boolean onBeforeDownload(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
String suggestedName,
|
||||
CefBeforeDownloadCallback callback) {
|
||||
callback.Continue("", true);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDownloadUpdated(
|
||||
CefBrowser browser,
|
||||
CefDownloadItem downloadItem,
|
||||
CefDownloadItemCallback callback) {
|
||||
if (downloadItem.isComplete()) {
|
||||
log.info("Download completed: " + downloadItem.getFullPath());
|
||||
} else if (downloadItem.isCanceled()) {
|
||||
log.info("Download canceled: " + downloadItem.getFullPath());
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private ConsoleProgressHandler createProgressHandler() {
|
||||
return new ConsoleProgressHandler() {
|
||||
@Override
|
||||
public void handleProgress(EnumProgress state, float percent) {
|
||||
Objects.requireNonNull(state, "state cannot be null");
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
if (loadingWindow != null) {
|
||||
switch (state) {
|
||||
case LOCATING:
|
||||
loadingWindow.setStatus("Locating Files...");
|
||||
loadingWindow.setProgress(0);
|
||||
break;
|
||||
case DOWNLOADING:
|
||||
if (percent >= 0) {
|
||||
loadingWindow.setStatus(
|
||||
String.format(
|
||||
"Downloading additional files: %.0f%%",
|
||||
percent));
|
||||
loadingWindow.setProgress((int) percent);
|
||||
}
|
||||
break;
|
||||
case EXTRACTING:
|
||||
loadingWindow.setStatus("Extracting files...");
|
||||
loadingWindow.setProgress(60);
|
||||
break;
|
||||
case INITIALIZING:
|
||||
loadingWindow.setStatus("Initializing UI...");
|
||||
loadingWindow.setProgress(80);
|
||||
break;
|
||||
case INITIALIZED:
|
||||
loadingWindow.setStatus("Finalising startup...");
|
||||
loadingWindow.setProgress(90);
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private void setupMainFrame() {
|
||||
frame = new JFrame("Stirling-PDF");
|
||||
frame.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
|
||||
frame.setUndecorated(true);
|
||||
frame.setOpacity(0.0f);
|
||||
|
||||
JPanel contentPane = new JPanel(new BorderLayout());
|
||||
contentPane.setDoubleBuffered(true);
|
||||
contentPane.add(browser.getUIComponent(), BorderLayout.CENTER);
|
||||
frame.setContentPane(contentPane);
|
||||
|
||||
frame.addWindowListener(
|
||||
new java.awt.event.WindowAdapter() {
|
||||
@Override
|
||||
public void windowClosing(java.awt.event.WindowEvent windowEvent) {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
}
|
||||
});
|
||||
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
|
||||
loadIcon();
|
||||
}
|
||||
|
||||
private void setupLoadHandler() {
|
||||
final long initStartTime = System.currentTimeMillis();
|
||||
log.info("Setting up load handler at: {}", initStartTime);
|
||||
|
||||
client.addLoadHandler(
|
||||
new CefLoadHandlerAdapter() {
|
||||
@Override
|
||||
public void onLoadingStateChange(
|
||||
CefBrowser browser,
|
||||
boolean isLoading,
|
||||
boolean canGoBack,
|
||||
boolean canGoForward) {
|
||||
log.debug(
|
||||
"Loading state change - isLoading: {}, canGoBack: {}, canGoForward:"
|
||||
+ " {}, browserInitialized: {}, Time elapsed: {}ms",
|
||||
isLoading,
|
||||
canGoBack,
|
||||
canGoForward,
|
||||
browserInitialized,
|
||||
System.currentTimeMillis() - initStartTime);
|
||||
|
||||
if (!isLoading && !browserInitialized) {
|
||||
log.info(
|
||||
"Browser finished loading, preparing to initialize UI"
|
||||
+ " components");
|
||||
browserInitialized = true;
|
||||
SwingUtilities.invokeLater(
|
||||
() -> {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Starting UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(
|
||||
UIScaling.scaleWidth(1280),
|
||||
UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent()
|
||||
.requestFocus();
|
||||
log.info(
|
||||
"Browser component"
|
||||
+ " focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing"
|
||||
+ " browser",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during UI initialization", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void setupTrayIcon(Image icon) {
|
||||
if (!SystemTray.isSupported()) {
|
||||
log.warn("System tray is not supported");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
systemTray = SystemTray.getSystemTray();
|
||||
|
||||
// Create popup menu
|
||||
PopupMenu popup = new PopupMenu();
|
||||
|
||||
// Create menu items
|
||||
MenuItem showItem = new MenuItem("Show");
|
||||
showItem.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
MenuItem exitItem = new MenuItem("Exit");
|
||||
exitItem.addActionListener(
|
||||
e -> {
|
||||
cleanup();
|
||||
System.exit(0);
|
||||
});
|
||||
|
||||
// Add menu items to popup menu
|
||||
popup.add(showItem);
|
||||
popup.addSeparator();
|
||||
popup.add(exitItem);
|
||||
|
||||
// Create tray icon
|
||||
trayIcon = new TrayIcon(icon, "Stirling-PDF", popup);
|
||||
trayIcon.setImageAutoSize(true);
|
||||
|
||||
// Add double-click behavior
|
||||
trayIcon.addActionListener(
|
||||
e -> {
|
||||
frame.setVisible(true);
|
||||
frame.setState(Frame.NORMAL);
|
||||
});
|
||||
|
||||
// Add tray icon to system tray
|
||||
systemTray.add(trayIcon);
|
||||
|
||||
// Modify frame behavior to minimize to tray
|
||||
frame.addWindowStateListener(
|
||||
new WindowStateListener() {
|
||||
public void windowStateChanged(WindowEvent e) {
|
||||
if (e.getNewState() == Frame.ICONIFIED) {
|
||||
frame.setVisible(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
} catch (AWTException e) {
|
||||
log.error("Error setting up system tray icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
private void loadIcon() {
|
||||
try {
|
||||
Image icon = null;
|
||||
String[] iconPaths = {"/static/favicon.ico"};
|
||||
|
||||
for (String path : iconPaths) {
|
||||
if (icon != null) break;
|
||||
try {
|
||||
try (InputStream is = getClass().getResourceAsStream(path)) {
|
||||
if (is != null) {
|
||||
icon = ImageIO.read(is);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.debug("Could not load icon from " + path, e);
|
||||
}
|
||||
}
|
||||
|
||||
if (icon != null) {
|
||||
frame.setIconImage(icon);
|
||||
setupTrayIcon(icon);
|
||||
} else {
|
||||
log.warn("Could not load icon from any source");
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error loading icon", e);
|
||||
}
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void cleanup() {
|
||||
if (browser != null) browser.close(true);
|
||||
if (client != null) client.dispose();
|
||||
if (cefApp != null) cefApp.dispose();
|
||||
if (loadingWindow != null) loadingWindow.dispose();
|
||||
}
|
||||
|
||||
public static void forceInitializeUI() {
|
||||
try {
|
||||
if (loadingWindow != null) {
|
||||
log.info("Forcing start of UI initialization sequence");
|
||||
|
||||
// Close loading window first
|
||||
loadingWindow.setVisible(false);
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
log.info("Loading window disposed");
|
||||
|
||||
// Then setup the main frame
|
||||
frame.setVisible(false);
|
||||
frame.dispose();
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.pack();
|
||||
frame.setSize(UIScaling.scaleWidth(1280), UIScaling.scaleHeight(800));
|
||||
frame.setLocationRelativeTo(null);
|
||||
log.debug("Frame reconfigured");
|
||||
|
||||
// Show the main frame
|
||||
frame.setVisible(true);
|
||||
frame.requestFocus();
|
||||
frame.toFront();
|
||||
log.info("Main frame displayed and focused");
|
||||
|
||||
// Focus the browser component if available
|
||||
if (browser != null) {
|
||||
Timer focusTimer =
|
||||
new Timer(
|
||||
100,
|
||||
e -> {
|
||||
try {
|
||||
browser.getUIComponent().requestFocus();
|
||||
log.info("Browser component focused");
|
||||
} catch (Exception ex) {
|
||||
log.error(
|
||||
"Error focusing browser during force ui"
|
||||
+ " initialization.",
|
||||
ex);
|
||||
}
|
||||
});
|
||||
focusTimer.setRepeats(false);
|
||||
focusTimer.start();
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Error during Forced UI initialization.", e);
|
||||
// Attempt cleanup on error
|
||||
if (loadingWindow != null) {
|
||||
loadingWindow.dispose();
|
||||
loadingWindow = null;
|
||||
}
|
||||
if (frame != null) {
|
||||
frame.setVisible(true);
|
||||
frame.setOpacity(1.0f);
|
||||
frame.setUndecorated(false);
|
||||
frame.requestFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -35,7 +35,7 @@ public class WebMvcConfig implements WebMvcConfigurer {
|
||||
@ConditionalOnProperty(name = "STIRLING_PDF_TAURI_MODE", havingValue = "true")
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOrigins("http://localhost:5173", "http://tauri.localhost")
|
||||
.allowedOrigins("http://localhost:5173", "http://tauri.localhost", "tauri://localhost")
|
||||
.allowedMethods("*")
|
||||
.allowedHeaders("*");
|
||||
}
|
||||
|
87
frontend/src-tauri/Cargo.lock
generated
@ -94,7 +94,10 @@ checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
|
||||
name = "app"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cocoa",
|
||||
"log",
|
||||
"objc",
|
||||
"once_cell",
|
||||
"reqwest 0.11.27",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@ -195,6 +198,12 @@ dependencies = [
|
||||
"wyz",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0d8c1fef690941d3e7788d328517591fecc684c084084702d6ff1641e993699a"
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.4"
|
||||
@ -454,6 +463,36 @@ dependencies = [
|
||||
"windows-link",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa"
|
||||
version = "0.24.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f425db7937052c684daec3bd6375c8abe2d146dca4b8b143d6db777c39138f3a"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"cocoa-foundation",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics 0.22.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cocoa-foundation"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"block",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"libc",
|
||||
"objc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "combine"
|
||||
version = "4.6.7"
|
||||
@ -506,6 +545,19 @@ version = "0.8.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.22.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2581bbab3b8ffc6fcbd550bf46c355135d16e9ff2a6ea032ad6b9bf1d7efe4fb"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"core-graphics-types 0.1.3",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics"
|
||||
version = "0.24.0"
|
||||
@ -514,11 +566,22 @@ checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics-types",
|
||||
"core-graphics-types 0.2.0",
|
||||
"foreign-types 0.5.0",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"core-foundation 0.9.4",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-graphics-types"
|
||||
version = "0.2.0"
|
||||
@ -1978,6 +2041,15 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
|
||||
|
||||
[[package]]
|
||||
name = "malloc_buf"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62bb907fe88d54d8d9ce32a3cceab4218ed2f6b7d35617cafe9adf84e43919cb"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "markup5ever"
|
||||
version = "0.11.0"
|
||||
@ -2165,6 +2237,15 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc"
|
||||
version = "0.2.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "915b1b472bc21c53464d6c8461c9d3af805ba1ef837e1cac254428f4a77177b1"
|
||||
dependencies = [
|
||||
"malloc_buf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "objc-sys"
|
||||
version = "0.3.5"
|
||||
@ -3509,7 +3590,7 @@ checksum = "18051cdd562e792cad055119e0cdb2cfc137e44e3987532e0f9659a77931bb08"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"cfg_aliases",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"foreign-types 0.5.0",
|
||||
"js-sys",
|
||||
"log",
|
||||
@ -3687,7 +3768,7 @@ checksum = "1e59c1f38e657351a2e822eadf40d6a2ad4627b9c25557bc1180ec1b3295ef82"
|
||||
dependencies = [
|
||||
"bitflags 2.9.1",
|
||||
"core-foundation 0.10.1",
|
||||
"core-graphics",
|
||||
"core-graphics 0.24.0",
|
||||
"crossbeam-channel",
|
||||
"dispatch",
|
||||
"dlopen2",
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "app"
|
||||
name = "stirling-pdf"
|
||||
version = "0.1.0"
|
||||
description = "A Tauri App"
|
||||
authors = ["you"]
|
||||
description = "Stirling-PDF Desktop Application"
|
||||
authors = ["Stirling-PDF Contributors"]
|
||||
license = ""
|
||||
repository = ""
|
||||
edition = "2021"
|
||||
@ -27,3 +27,9 @@ tauri-plugin-shell = "2.1.0"
|
||||
tauri-plugin-fs = "2.0.0"
|
||||
tokio = { version = "1.0", features = ["time"] }
|
||||
reqwest = { version = "0.11", features = ["json"] }
|
||||
|
||||
# macOS-specific dependencies for native file opening
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
objc = "0.2"
|
||||
cocoa = "0.24"
|
||||
once_cell = "1.19"
|
||||
|
BIN
frontend/src-tauri/icons/128x128.png
Normal file
After Width: | Height: | Size: 9.2 KiB |
BIN
frontend/src-tauri/icons/128x128@2x.png
Normal file
After Width: | Height: | Size: 20 KiB |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
frontend/src-tauri/icons/64x64.png
Normal file
After Width: | Height: | Size: 4.4 KiB |
BIN
frontend/src-tauri/icons/Square107x107Logo.png
Normal file
After Width: | Height: | Size: 7.6 KiB |
BIN
frontend/src-tauri/icons/Square142x142Logo.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/src-tauri/icons/Square150x150Logo.png
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/src-tauri/icons/Square284x284Logo.png
Normal file
After Width: | Height: | Size: 22 KiB |
BIN
frontend/src-tauri/icons/Square30x30Logo.png
Normal file
After Width: | Height: | Size: 1.8 KiB |
BIN
frontend/src-tauri/icons/Square310x310Logo.png
Normal file
After Width: | Height: | Size: 25 KiB |
BIN
frontend/src-tauri/icons/Square44x44Logo.png
Normal file
After Width: | Height: | Size: 2.9 KiB |
BIN
frontend/src-tauri/icons/Square71x71Logo.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
frontend/src-tauri/icons/Square89x89Logo.png
Normal file
After Width: | Height: | Size: 6.3 KiB |
BIN
frontend/src-tauri/icons/StoreLogo.png
Normal file
After Width: | Height: | Size: 3.3 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-hdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.2 KiB |
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 3.2 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-mdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 3.1 KiB |
After Width: | Height: | Size: 7.7 KiB |
After Width: | Height: | Size: 3.1 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 6.8 KiB |
After Width: | Height: | Size: 17 KiB |
After Width: | Height: | Size: 6.8 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 11 KiB |
After Width: | Height: | Size: 26 KiB |
After Width: | Height: | Size: 11 KiB |
BIN
frontend/src-tauri/icons/android/mipmap-xxxhdpi/ic_launcher.png
Normal file
After Width: | Height: | Size: 15 KiB |
After Width: | Height: | Size: 36 KiB |
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 19 KiB |
BIN
frontend/src-tauri/icons/icon_orig.png
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@1x.png
Normal file
After Width: | Height: | Size: 1.0 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x-1.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@2x.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-20x20@3x.png
Normal file
After Width: | Height: | Size: 3.8 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@1x.png
Normal file
After Width: | Height: | Size: 1.6 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x-1.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@2x.png
Normal file
After Width: | Height: | Size: 3.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-29x29@3x.png
Normal file
After Width: | Height: | Size: 5.7 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@1x.png
Normal file
After Width: | Height: | Size: 2.4 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x-1.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@2x.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-40x40@3x.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-512@2x.png
Normal file
After Width: | Height: | Size: 111 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@2x.png
Normal file
After Width: | Height: | Size: 8.1 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-60x60@3x.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@1x.png
Normal file
After Width: | Height: | Size: 4.9 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-76x76@2x.png
Normal file
After Width: | Height: | Size: 10 KiB |
BIN
frontend/src-tauri/icons/ios/AppIcon-83.5x83.5@2x.png
Normal file
After Width: | Height: | Size: 11 KiB |
@ -112,13 +112,37 @@ fn normalize_path(path: &PathBuf) -> PathBuf {
|
||||
|
||||
// Create, configure and run the Java command to run Stirling-PDF JAR
|
||||
fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &PathBuf) -> Result<(), String> {
|
||||
// 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
|
||||
// Get platform-specific application data directory for Tauri mode
|
||||
let app_data_dir = if cfg!(target_os = "macos") {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join("Library").join("Application Support").join("Stirling-PDF")
|
||||
} else if cfg!(target_os = "windows") {
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string());
|
||||
PathBuf::from(appdata).join("Stirling-PDF")
|
||||
} else {
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join(".config").join("Stirling-PDF")
|
||||
};
|
||||
|
||||
// Define all Java options in an array
|
||||
// Create subdirectories for different purposes
|
||||
let config_dir = app_data_dir.join("configs");
|
||||
let log_dir = app_data_dir.join("logs");
|
||||
let work_dir = app_data_dir.join("workspace");
|
||||
|
||||
// Create all necessary directories
|
||||
std::fs::create_dir_all(&app_data_dir).ok();
|
||||
std::fs::create_dir_all(&log_dir).ok();
|
||||
std::fs::create_dir_all(&work_dir).ok();
|
||||
std::fs::create_dir_all(&config_dir).ok();
|
||||
|
||||
add_log(format!("📁 App data directory: {}", app_data_dir.display()));
|
||||
add_log(format!("📁 Log directory: {}", log_dir.display()));
|
||||
add_log(format!("📁 Working directory: {}", work_dir.display()));
|
||||
add_log(format!("📁 Config directory: {}", config_dir.display()));
|
||||
|
||||
// Define all Java options with Tauri-specific paths
|
||||
let log_path_option = format!("-Dlogging.file.path={}", log_dir.display());
|
||||
|
||||
let java_options = vec![
|
||||
"-Xmx2g",
|
||||
"-DBROWSER_OPEN=false",
|
||||
@ -132,18 +156,49 @@ fn run_stirling_pdf_jar(app: &tauri::AppHandle, java_path: &PathBuf, jar_path: &
|
||||
|
||||
// Log the equivalent command for external testing
|
||||
let java_command = format!(
|
||||
"TAURI_PARENT_PID={} && \"{}\" {}",
|
||||
"TAURI_PARENT_PID={} \"{}\" {}",
|
||||
std::process::id(),
|
||||
java_path.display(),
|
||||
java_options.join(" ")
|
||||
);
|
||||
add_log(format!("🔧 Equivalent command: {}", java_command));
|
||||
add_log(format!("📁 Backend logs will be in: {}", log_dir.display()));
|
||||
|
||||
// Additional macOS-specific checks
|
||||
if cfg!(target_os = "macos") {
|
||||
// Check if java executable has execute permissions
|
||||
if let Ok(metadata) = std::fs::metadata(java_path) {
|
||||
let permissions = metadata.permissions();
|
||||
add_log(format!("🔍 Java executable permissions: {:?}", permissions));
|
||||
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let mode = permissions.mode();
|
||||
add_log(format!("🔍 Java executable mode: 0o{:o}", mode));
|
||||
if mode & 0o111 == 0 {
|
||||
add_log("⚠️ Java executable may not have execute permissions".to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can read the JAR file
|
||||
if let Ok(metadata) = std::fs::metadata(jar_path) {
|
||||
add_log(format!("📦 JAR file size: {} bytes", metadata.len()));
|
||||
} else {
|
||||
add_log("⚠️ Cannot read JAR file metadata".to_string());
|
||||
}
|
||||
}
|
||||
|
||||
let sidecar_command = app
|
||||
.shell()
|
||||
.command(java_path.to_str().unwrap())
|
||||
.args(java_options)
|
||||
.env("TAURI_PARENT_PID", std::process::id().to_string());
|
||||
.current_dir(&work_dir) // Set working directory to writable location
|
||||
.env("TAURI_PARENT_PID", std::process::id().to_string())
|
||||
.env("STIRLING_PDF_CONFIG_DIR", config_dir.to_str().unwrap())
|
||||
.env("STIRLING_PDF_LOG_DIR", log_dir.to_str().unwrap())
|
||||
.env("STIRLING_PDF_WORK_DIR", work_dir.to_str().unwrap());
|
||||
|
||||
add_log("⚙️ Starting backend with bundled JRE...".to_string());
|
||||
|
||||
|
@ -1,15 +1,35 @@
|
||||
use crate::utils::add_log;
|
||||
use std::sync::Mutex;
|
||||
|
||||
// Store the opened file path globally
|
||||
static OPENED_FILE: Mutex<Option<String>> = Mutex::new(None);
|
||||
|
||||
// Set the opened file path (called by macOS file open events)
|
||||
pub fn set_opened_file(file_path: String) {
|
||||
let mut opened_file = OPENED_FILE.lock().unwrap();
|
||||
*opened_file = Some(file_path.clone());
|
||||
add_log(format!("📂 File opened via file open event: {}", file_path));
|
||||
}
|
||||
|
||||
// Command to get opened file path (if app was launched with a file)
|
||||
#[tauri::command]
|
||||
pub async fn get_opened_file() -> Result<Option<String>, String> {
|
||||
// Get command line arguments
|
||||
// First check if we have a file from macOS file open events
|
||||
{
|
||||
let opened_file = OPENED_FILE.lock().unwrap();
|
||||
if let Some(ref file_path) = *opened_file {
|
||||
add_log(format!("📂 Returning stored opened file: {}", file_path));
|
||||
return Ok(Some(file_path.clone()));
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to command line arguments (Windows/Linux)
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
|
||||
// Look for a PDF file argument (skip the first arg which is the executable)
|
||||
for arg in args.iter().skip(1) {
|
||||
if arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 PDF file opened: {}", arg));
|
||||
add_log(format!("📂 PDF file opened via command line: {}", arg));
|
||||
return Ok(Some(arg.clone()));
|
||||
}
|
||||
}
|
||||
@ -17,3 +37,12 @@ pub async fn get_opened_file() -> Result<Option<String>, String> {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
// Command to clear the opened file (after processing)
|
||||
#[tauri::command]
|
||||
pub async fn clear_opened_file() -> Result<(), String> {
|
||||
let mut opened_file = OPENED_FILE.lock().unwrap();
|
||||
*opened_file = None;
|
||||
add_log("📂 Cleared opened file".to_string());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -4,4 +4,4 @@ pub mod files;
|
||||
|
||||
pub use backend::{start_backend, cleanup_backend};
|
||||
pub use health::check_backend_health;
|
||||
pub use files::get_opened_file;
|
||||
pub use files::{get_opened_file, clear_opened_file, set_opened_file};
|
189
frontend/src-tauri/src/file_handler.rs
Normal file
@ -0,0 +1,189 @@
|
||||
/// Multi-platform file opening handler
|
||||
///
|
||||
/// This module provides unified file opening support across platforms:
|
||||
/// - macOS: Uses native NSApplication delegate (proper Apple Events)
|
||||
/// - Windows/Linux: Uses command line arguments (fallback approach)
|
||||
/// - All platforms: Runtime event handling via Tauri events
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
use tauri::AppHandle;
|
||||
|
||||
|
||||
/// Initialize file handling for the current platform
|
||||
pub fn initialize_file_handler(app: &AppHandle<tauri::Wry>) {
|
||||
add_log("🔧 Initializing file handler...".to_string());
|
||||
|
||||
// Platform-specific initialization
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🍎 Using macOS native file handler".to_string());
|
||||
macos_native::register_open_file_handler(app);
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
{
|
||||
add_log("🖥️ Using command line argument file handler".to_string());
|
||||
let _ = app; // Suppress unused variable warning
|
||||
}
|
||||
|
||||
// Universal: Check command line arguments (works on all platforms)
|
||||
check_command_line_args();
|
||||
}
|
||||
|
||||
/// Early initialization for macOS delegate registration
|
||||
pub fn early_init() {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
add_log("🔄 Early macOS initialization...".to_string());
|
||||
macos_native::register_delegate_early();
|
||||
}
|
||||
}
|
||||
|
||||
/// Check command line arguments for file paths (universal fallback)
|
||||
fn check_command_line_args() {
|
||||
let args: Vec<String> = std::env::args().collect();
|
||||
add_log(format!("🔍 DEBUG: All command line args: {:?}", args));
|
||||
|
||||
// Check command line arguments for file opening
|
||||
for (i, arg) in args.iter().enumerate() {
|
||||
add_log(format!("🔍 DEBUG: Arg {}: {}", i, arg));
|
||||
if i > 0 && arg.ends_with(".pdf") && std::path::Path::new(arg).exists() {
|
||||
add_log(format!("📂 File argument detected: {}", arg));
|
||||
set_opened_file(arg.clone());
|
||||
break; // Only handle the first PDF file
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle runtime file open events (for future single-instance support)
|
||||
#[allow(dead_code)]
|
||||
pub fn handle_runtime_file_open(file_path: String) {
|
||||
if file_path.ends_with(".pdf") && std::path::Path::new(&file_path).exists() {
|
||||
add_log(format!("📂 Runtime file open: {}", file_path));
|
||||
set_opened_file(file_path);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_native {
|
||||
use objc::{class, msg_send, sel, sel_impl};
|
||||
use objc::runtime::{Class, Object, Sel};
|
||||
use cocoa::appkit::NSApplication;
|
||||
use cocoa::base::{id, nil};
|
||||
use once_cell::sync::Lazy;
|
||||
use std::sync::Mutex;
|
||||
use tauri::{AppHandle, Emitter};
|
||||
|
||||
use crate::utils::add_log;
|
||||
use crate::commands::set_opened_file;
|
||||
|
||||
// Static app handle storage
|
||||
static APP_HANDLE: Lazy<Mutex<Option<AppHandle<tauri::Wry>>>> = Lazy::new(|| Mutex::new(None));
|
||||
|
||||
// Store files opened during launch
|
||||
static LAUNCH_FILES: Lazy<Mutex<Vec<String>>> = Lazy::new(|| Mutex::new(Vec::new()));
|
||||
|
||||
|
||||
extern "C" fn open_files(_self: &Object, _cmd: Sel, _sender: id, filenames: id) {
|
||||
unsafe {
|
||||
add_log(format!("📂 macOS native openFiles event called"));
|
||||
|
||||
// filenames is an NSArray of NSString objects
|
||||
let count: usize = msg_send![filenames, count];
|
||||
add_log(format!("📂 Number of files to open: {}", count));
|
||||
|
||||
for i in 0..count {
|
||||
let filename: id = msg_send![filenames, objectAtIndex: i];
|
||||
let cstr = {
|
||||
let bytes: *const std::os::raw::c_char = msg_send![filename, UTF8String];
|
||||
std::ffi::CStr::from_ptr(bytes)
|
||||
};
|
||||
|
||||
if let Ok(path) = cstr.to_str() {
|
||||
add_log(format!("📂 macOS file open: {}", path));
|
||||
if path.ends_with(".pdf") {
|
||||
// Always set the opened file for command-line interface
|
||||
set_opened_file(path.to_string());
|
||||
|
||||
if let Some(app) = APP_HANDLE.lock().unwrap().as_ref() {
|
||||
// App is running, emit event immediately
|
||||
add_log(format!("✅ App running, emitting file event: {}", path));
|
||||
let _ = app.emit("macos://open-file", path.to_string());
|
||||
} else {
|
||||
// App not ready yet, store for later processing
|
||||
add_log(format!("🚀 App not ready, storing file for later: {}", path));
|
||||
LAUNCH_FILES.lock().unwrap().push(path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Register the delegate immediately when the module loads
|
||||
pub fn register_delegate_early() {
|
||||
add_log("🔧 Registering macOS delegate early...".to_string());
|
||||
|
||||
unsafe {
|
||||
let ns_app = NSApplication::sharedApplication(nil);
|
||||
|
||||
// Check if there's already a delegate
|
||||
let existing_delegate: id = msg_send![ns_app, delegate];
|
||||
if existing_delegate != nil {
|
||||
add_log("⚠️ Tauri already has an NSApplication delegate, trying to extend it...".to_string());
|
||||
|
||||
// Try to add our method to the existing delegate's class
|
||||
let delegate_class: id = msg_send![existing_delegate, class];
|
||||
let class_name: *const std::os::raw::c_char = msg_send![delegate_class, name];
|
||||
let class_name_str = std::ffi::CStr::from_ptr(class_name).to_string_lossy();
|
||||
add_log(format!("🔍 Existing delegate class: {}", class_name_str));
|
||||
|
||||
// This approach won't work with existing classes, so let's try a different method
|
||||
// We'll use method swizzling or create a new delegate that forwards to the old one
|
||||
add_log("🔄 Will try alternative approach...".to_string());
|
||||
}
|
||||
|
||||
let delegate_class = Class::get("StirlingAppDelegate").unwrap_or_else(|| {
|
||||
let superclass = class!(NSObject);
|
||||
let mut decl = objc::declare::ClassDecl::new("StirlingAppDelegate", superclass).unwrap();
|
||||
|
||||
// Add file opening delegate method (modern plural version)
|
||||
decl.add_method(
|
||||
sel!(application:openFiles:),
|
||||
open_files as extern "C" fn(&Object, Sel, id, id)
|
||||
);
|
||||
|
||||
decl.register()
|
||||
});
|
||||
|
||||
let delegate: id = msg_send![delegate_class, new];
|
||||
let _: () = msg_send![ns_app, setDelegate:delegate];
|
||||
}
|
||||
|
||||
add_log("✅ macOS delegate registered early".to_string());
|
||||
}
|
||||
|
||||
pub fn register_open_file_handler(app: &AppHandle<tauri::Wry>) {
|
||||
add_log("🔧 Connecting app handle to file handler...".to_string());
|
||||
|
||||
// Store the app handle
|
||||
*APP_HANDLE.lock().unwrap() = Some(app.clone());
|
||||
|
||||
// Process any files that were opened during launch
|
||||
let launch_files = {
|
||||
let mut files = LAUNCH_FILES.lock().unwrap();
|
||||
let result = files.clone();
|
||||
files.clear();
|
||||
result
|
||||
};
|
||||
|
||||
for file_path in launch_files {
|
||||
add_log(format!("📂 Processing stored launch file: {}", file_path));
|
||||
set_opened_file(file_path.clone());
|
||||
let _ = app.emit("macos://open-file", file_path);
|
||||
}
|
||||
|
||||
add_log("✅ macOS file handler connected successfully".to_string());
|
||||
}
|
||||
}
|
@ -1,18 +1,30 @@
|
||||
use tauri::{RunEvent, WindowEvent};
|
||||
use tauri::{RunEvent, WindowEvent, Emitter};
|
||||
|
||||
mod utils;
|
||||
mod commands;
|
||||
mod file_handler;
|
||||
|
||||
use commands::{start_backend, check_backend_health, get_opened_file, cleanup_backend};
|
||||
use utils::add_log;
|
||||
use commands::{start_backend, check_backend_health, get_opened_file, clear_opened_file, cleanup_backend, set_opened_file};
|
||||
use utils::{add_log, get_tauri_logs};
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
// Initialize file handler early for macOS
|
||||
file_handler::early_init();
|
||||
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_fs::init())
|
||||
.setup(|_app| {Ok(())})
|
||||
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file])
|
||||
.setup(|app| {
|
||||
add_log("🚀 Tauri app setup started".to_string());
|
||||
|
||||
// Initialize platform-specific file handler
|
||||
file_handler::initialize_file_handler(&app.handle());
|
||||
|
||||
add_log("🔍 DEBUG: Setup completed".to_string());
|
||||
Ok(())
|
||||
})
|
||||
.invoke_handler(tauri::generate_handler![start_backend, check_backend_health, get_opened_file, clear_opened_file, get_tauri_logs])
|
||||
.build(tauri::generate_context!())
|
||||
.expect("error while building tauri application")
|
||||
.run(|app_handle, event| {
|
||||
@ -28,7 +40,26 @@ pub fn run() {
|
||||
cleanup_backend();
|
||||
// Allow the window to close
|
||||
}
|
||||
_ => {}
|
||||
#[cfg(target_os = "macos")]
|
||||
RunEvent::Opened { urls } => {
|
||||
add_log(format!("📂 Tauri file opened event: {:?}", urls));
|
||||
for url in urls {
|
||||
let url_str = url.as_str();
|
||||
if url_str.starts_with("file://") {
|
||||
let file_path = url_str.strip_prefix("file://").unwrap_or(url_str);
|
||||
if file_path.ends_with(".pdf") {
|
||||
add_log(format!("📂 Processing opened PDF: {}", file_path));
|
||||
set_opened_file(file_path.to_string());
|
||||
let _ = app_handle.emit("macos://open-file", file_path.to_string());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Only log unhandled events in debug mode to reduce noise
|
||||
// #[cfg(debug_assertions)]
|
||||
// add_log(format!("🔍 DEBUG: Unhandled event: {:?}", event));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
@ -1,20 +1,90 @@
|
||||
use std::sync::Mutex;
|
||||
use std::collections::VecDeque;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::path::PathBuf;
|
||||
|
||||
// Store backend logs globally
|
||||
static BACKEND_LOGS: Mutex<VecDeque<String>> = Mutex::new(VecDeque::new());
|
||||
|
||||
// Get platform-specific log directory
|
||||
fn get_log_directory() -> PathBuf {
|
||||
if cfg!(target_os = "macos") {
|
||||
// macOS: ~/Library/Logs/Stirling-PDF
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join("Library").join("Logs").join("Stirling-PDF")
|
||||
} else if cfg!(target_os = "windows") {
|
||||
// Windows: %APPDATA%\Stirling-PDF\logs
|
||||
let appdata = std::env::var("APPDATA").unwrap_or_else(|_| std::env::temp_dir().to_string_lossy().to_string());
|
||||
PathBuf::from(appdata).join("Stirling-PDF").join("logs")
|
||||
} else {
|
||||
// Linux: ~/.config/Stirling-PDF/logs
|
||||
let home = std::env::var("HOME").unwrap_or_else(|_| "/tmp".to_string());
|
||||
PathBuf::from(home).join(".config").join("Stirling-PDF").join("logs")
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to add log entry
|
||||
pub fn add_log(message: String) {
|
||||
let timestamp = std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap()
|
||||
.as_secs();
|
||||
|
||||
let mut logs = BACKEND_LOGS.lock().unwrap();
|
||||
logs.push_back(format!("{}: {}", std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs(), message));
|
||||
// Keep only last 100 log entries
|
||||
if logs.len() > 100 {
|
||||
logs.pop_front();
|
||||
let log_entry = format!("{}: {}", timestamp, message);
|
||||
|
||||
// Add to memory logs
|
||||
{
|
||||
let mut logs = BACKEND_LOGS.lock().unwrap();
|
||||
logs.push_back(log_entry.clone());
|
||||
// Keep only last 100 log entries
|
||||
if logs.len() > 100 {
|
||||
logs.pop_front();
|
||||
}
|
||||
}
|
||||
|
||||
// Write to file
|
||||
write_to_log_file(&log_entry);
|
||||
|
||||
// Remove trailing newline if present
|
||||
let clean_message = message.trim_end_matches('\n').to_string();
|
||||
println!("{}", clean_message); // Also print to console
|
||||
}
|
||||
|
||||
// Write log entry to file
|
||||
fn write_to_log_file(log_entry: &str) {
|
||||
let log_dir = get_log_directory();
|
||||
if let Err(e) = std::fs::create_dir_all(&log_dir) {
|
||||
eprintln!("Failed to create log directory: {}", e);
|
||||
return;
|
||||
}
|
||||
|
||||
let log_file = log_dir.join("tauri-backend.log");
|
||||
|
||||
match OpenOptions::new()
|
||||
.create(true)
|
||||
.append(true)
|
||||
.open(&log_file)
|
||||
{
|
||||
Ok(mut file) => {
|
||||
if let Err(e) = writeln!(file, "{}", log_entry) {
|
||||
eprintln!("Failed to write to log file: {}", e);
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("Failed to open log file {:?}: {}", log_file, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Get current logs for debugging
|
||||
pub fn get_logs() -> Vec<String> {
|
||||
let logs = BACKEND_LOGS.lock().unwrap();
|
||||
logs.iter().cloned().collect()
|
||||
}
|
||||
|
||||
// Command to get logs from frontend
|
||||
#[tauri::command]
|
||||
pub async fn get_tauri_logs() -> Result<Vec<String>, String> {
|
||||
Ok(get_logs())
|
||||
}
|
@ -1,3 +1,3 @@
|
||||
pub mod logging;
|
||||
|
||||
pub use logging::add_log;
|
||||
pub use logging::{add_log, get_tauri_logs};
|
14
frontend/src-tauri/stirling-pdf.desktop
Normal file
@ -0,0 +1,14 @@
|
||||
[Desktop Entry]
|
||||
Version=1.0
|
||||
Type=Application
|
||||
Name=Stirling-PDF
|
||||
Comment=Locally hosted web application that allows you to perform various operations on PDF files
|
||||
Icon={{icon}}
|
||||
Terminal=false
|
||||
MimeType=application/pdf;
|
||||
Categories=Office;Graphics;Utility;
|
||||
Actions=open-file;
|
||||
|
||||
[Desktop Action open-file]
|
||||
Name=Open PDF File
|
||||
Exec=/usr/bin/stirling-pdf %F
|
@ -13,21 +13,25 @@
|
||||
"windows": [
|
||||
{
|
||||
"title": "Stirling-PDF",
|
||||
"width": 800,
|
||||
"height": 600,
|
||||
"width": 1024,
|
||||
"height": 768,
|
||||
"resizable": true,
|
||||
"fullscreen": false
|
||||
}
|
||||
]
|
||||
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"targets": ["deb", "rpm", "dmg", "msi"],
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/icon.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
"icons/icon.ico",
|
||||
"icons/16x16.png",
|
||||
"icons/32x32.png",
|
||||
"icons/64x64.png",
|
||||
"icons/128x128.png",
|
||||
"icons/192x192.png"
|
||||
],
|
||||
"resources": [
|
||||
"libs/*.jar",
|
||||
@ -38,9 +42,15 @@
|
||||
"ext": ["pdf"],
|
||||
"name": "PDF Document",
|
||||
"description": "Open PDF files with Stirling-PDF",
|
||||
"role": "Editor"
|
||||
"role": "Editor",
|
||||
"mimeType": "application/pdf"
|
||||
}
|
||||
]
|
||||
],
|
||||
"linux": {
|
||||
"deb": {
|
||||
"desktopTemplate": "stirling-pdf.desktop"
|
||||
}
|
||||
}
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
|
@ -7,13 +7,20 @@ export function useOpenedFile() {
|
||||
|
||||
useEffect(() => {
|
||||
const checkForOpenedFile = async () => {
|
||||
console.log('🔍 Checking for opened file...');
|
||||
try {
|
||||
const filePath = await fileOpenService.getOpenedFile();
|
||||
console.log('🔍 fileOpenService.getOpenedFile() returned:', filePath);
|
||||
|
||||
if (filePath) {
|
||||
console.log('✅ App opened with file:', filePath);
|
||||
setOpenedFilePath(filePath);
|
||||
}
|
||||
|
||||
// Clear the file from service state after consuming it
|
||||
await fileOpenService.clearOpenedFile();
|
||||
} else {
|
||||
console.log('ℹ️ No file was opened with the app');
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to check for opened file:', error);
|
||||
@ -23,6 +30,17 @@ export function useOpenedFile() {
|
||||
};
|
||||
|
||||
checkForOpenedFile();
|
||||
|
||||
// Listen for runtime file open events (abstracted through service)
|
||||
const unlistenRuntimeEvents = fileOpenService.onFileOpened((filePath) => {
|
||||
console.log('📂 Runtime file open event:', filePath);
|
||||
setOpenedFilePath(filePath);
|
||||
});
|
||||
|
||||
// Cleanup function
|
||||
return () => {
|
||||
unlistenRuntimeEvents();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return { openedFilePath, loading };
|
||||
|
@ -3,15 +3,19 @@ import { invoke } from '@tauri-apps/api/core';
|
||||
export interface FileOpenService {
|
||||
getOpenedFile(): Promise<string | null>;
|
||||
readFileAsArrayBuffer(filePath: string): Promise<{ fileName: string; arrayBuffer: ArrayBuffer } | null>;
|
||||
clearOpenedFile(): Promise<void>;
|
||||
onFileOpened(callback: (filePath: string) => void): () => void; // Returns unlisten function
|
||||
}
|
||||
|
||||
class TauriFileOpenService implements FileOpenService {
|
||||
async getOpenedFile(): Promise<string | null> {
|
||||
try {
|
||||
console.log('🔍 Calling invoke(get_opened_file)...');
|
||||
const result = await invoke<string | null>('get_opened_file');
|
||||
console.log('🔍 invoke(get_opened_file) returned:', result);
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('Failed to get opened file:', error);
|
||||
console.error('❌ Failed to get opened file:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@ -32,6 +36,85 @@ class TauriFileOpenService implements FileOpenService {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async clearOpenedFile(): Promise<void> {
|
||||
try {
|
||||
console.log('🔍 Calling invoke(clear_opened_file)...');
|
||||
await invoke('clear_opened_file');
|
||||
console.log('✅ Successfully cleared opened file');
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to clear opened file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
onFileOpened(callback: (filePath: string) => void): () => void {
|
||||
let cleanup: (() => void) | null = null;
|
||||
let isCleanedUp = false;
|
||||
|
||||
const setupEventListeners = async () => {
|
||||
try {
|
||||
// Check if already cleaned up before async setup completes
|
||||
if (isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only import if in Tauri environment
|
||||
if (typeof window !== 'undefined' && ('__TAURI__' in window || '__TAURI_INTERNALS__' in window)) {
|
||||
const { listen } = await import('@tauri-apps/api/event');
|
||||
|
||||
// Check again after async import
|
||||
if (isCleanedUp) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Listen for macOS native file open events
|
||||
const unlistenMacOS = await listen('macos://open-file', (event) => {
|
||||
console.log('📂 macOS native file open event:', event.payload);
|
||||
callback(event.payload as string);
|
||||
});
|
||||
|
||||
// Listen for fallback file open events
|
||||
const unlistenFallback = await listen('file-opened', (event) => {
|
||||
console.log('📂 Fallback file open event:', event.payload);
|
||||
callback(event.payload as string);
|
||||
});
|
||||
|
||||
// Set up cleanup function only if not already cleaned up
|
||||
if (!isCleanedUp) {
|
||||
cleanup = () => {
|
||||
try {
|
||||
unlistenMacOS();
|
||||
unlistenFallback();
|
||||
console.log('✅ File event listeners cleaned up');
|
||||
} catch (error) {
|
||||
console.error('❌ Error during file event cleanup:', error);
|
||||
}
|
||||
};
|
||||
} else {
|
||||
// Clean up immediately if cleanup was called during setup
|
||||
try {
|
||||
unlistenMacOS();
|
||||
unlistenFallback();
|
||||
} catch (error) {
|
||||
console.error('❌ Error during immediate cleanup:', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ Failed to setup file event listeners:', error);
|
||||
}
|
||||
};
|
||||
|
||||
setupEventListeners();
|
||||
|
||||
// Return cleanup function
|
||||
return () => {
|
||||
isCleanedUp = true;
|
||||
if (cleanup) {
|
||||
cleanup();
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
class WebFileOpenService implements FileOpenService {
|
||||
@ -44,6 +127,18 @@ class WebFileOpenService implements FileOpenService {
|
||||
// In web mode, cannot read arbitrary file paths
|
||||
return null;
|
||||
}
|
||||
|
||||
async clearOpenedFile(): Promise<void> {
|
||||
// In web mode, no file clearing needed
|
||||
}
|
||||
|
||||
onFileOpened(callback: (filePath: string) => void): () => void {
|
||||
// In web mode, no file events - return no-op cleanup function
|
||||
console.log('ℹ️ Web mode: File event listeners not supported');
|
||||
return () => {
|
||||
// No-op cleanup for web mode
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Export the appropriate service based on environment
|
||||
|
@ -28,8 +28,8 @@ if errorlevel 1 (
|
||||
|
||||
REM Find the built JAR(s)
|
||||
echo ▶ Listing all built JAR files in app\core\build\libs:
|
||||
dir /b app\core\build\libs\Stirling-PDF-*.jar
|
||||
for %%f in (app\core\build\libs\Stirling-PDF-*.jar) do set STIRLING_JAR=%%f
|
||||
dir /b app\core\build\libs\stirling-pdf-*.jar
|
||||
for %%f in (app\core\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
|
||||
@ -47,7 +47,7 @@ echo ✅ JAR copied to frontend\src-tauri\libs\
|
||||
|
||||
REM Log out all JAR files now in the Tauri libs directory
|
||||
echo ▶ Listing all JAR files in frontend\src-tauri\libs after copy:
|
||||
dir /b frontend\src-tauri\libs\Stirling-PDF-*.jar
|
||||
dir /b frontend\src-tauri\libs\stirling-pdf-*.jar
|
||||
|
||||
echo ▶ Creating custom JRE with jlink...
|
||||
if exist "frontend\src-tauri\runtime\jre" rmdir /s /q "frontend\src-tauri\runtime\jre"
|
||||
|
@ -62,13 +62,13 @@ fi
|
||||
print_step "Building Stirling-PDF JAR..."
|
||||
./gradlew clean bootJar --no-daemon
|
||||
|
||||
if [ ! -f "stirling-pdf/build/libs/Stirling-PDF-"*.jar ]; then
|
||||
print_error "Failed to build Stirling-PDF JAR"
|
||||
if [ ! -f "app\core\build\libs\stirling-pdf-*.jar"*.jar ]; then
|
||||
print_error "Failed to build stirling-pdf JAR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Find the built JAR
|
||||
STIRLING_JAR=$(ls stirling-pdf/build/libs/Stirling-PDF-*.jar | head -n 1)
|
||||
STIRLING_JAR=$(ls app\core\build\libs\stirling-pdf-*.jar | head -n 1)
|
||||
print_success "Built JAR: $STIRLING_JAR"
|
||||
|
||||
# Create directories for Tauri
|
||||
@ -98,7 +98,7 @@ if command -v jdeps &> /dev/null; then
|
||||
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"
|
||||
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.sql,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"
|
||||
|