WIP: auto update

This commit is contained in:
SoftFever
2026-01-04 21:47:07 +08:00
parent 7c91459c37
commit e35f4dbf62
17 changed files with 1141 additions and 158 deletions

View File

@@ -5,6 +5,7 @@ on:
branches:
- main
- release/*
- feature/auto-update # TODO: Remove after auto-update testing is complete
paths:
- 'deps/**'
- 'src/**'
@@ -48,25 +49,28 @@ concurrency:
jobs:
build_linux: # Separate so unit tests can wait on just Linux builds to complete.
name: Build Linux
strategy:
fail-fast: false
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
uses: ./.github/workflows/build_deps.yml
with:
os: ubuntu-24.04
build-deps-only: ${{ inputs.build-deps-only || false }}
force-build: ${{ github.event_name == 'schedule' }}
secrets: inherit
# TODO: Re-enable after auto-update testing is complete
# build_linux: # Separate so unit tests can wait on just Linux builds to complete.
# name: Build Linux
# strategy:
# fail-fast: false
# # Don't run scheduled builds on forks:
# if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
# uses: ./.github/workflows/build_deps.yml
# with:
# os: ubuntu-24.04
# build-deps-only: ${{ inputs.build-deps-only || false }}
# force-build: ${{ github.event_name == 'schedule' }}
# secrets: inherit
build_all:
name: Build Non-Linux
name: Build macOS (testing auto-update)
strategy:
fail-fast: false
matrix:
include:
- os: windows-latest
# TODO: Re-enable Windows after auto-update testing is complete
# - os: windows-latest
- os: macos-14
arch: arm64
# Don't run scheduled builds on forks:
@@ -78,109 +82,113 @@ jobs:
build-deps-only: ${{ inputs.build-deps-only || false }}
force-build: ${{ github.event_name == 'schedule' }}
secrets: inherit
unit_tests:
name: Unit Tests
runs-on: ubuntu-24.04
needs: build_linux
if: ${{ !cancelled() && success() }}
steps:
- name: Checkout
uses: actions/checkout@v6
with:
sparse-checkout: |
.github
scripts
tests
- name: Apt-Install Dependencies
uses: ./.github/actions/apt-install-deps
- name: Restore Test Artifact
uses: actions/download-artifact@v6
with:
name: ${{ github.sha }}-tests
- uses: lukka/get-cmake@latest
with:
cmakeVersion: "~3.28.0" # use most recent 3.28.x version
- name: Unpackage and Run Unit Tests
timeout-minutes: 20
run: |
tar -xvf build_tests.tar
scripts/run_unit_tests.sh
- name: Upload Test Logs
uses: actions/upload-artifact@v5
if: ${{ failure() }}
with:
name: unit-test-logs
path: build/tests/**/*.log
- name: Publish Test Results
if: always()
uses: EnricoMi/publish-unit-test-result-action/linux@v2
with:
files: "ctest_results.xml"
flatpak:
name: "Flatpak"
container:
image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48
options: --privileged
volumes:
- /usr/local/lib/android:/usr/local/lib/android
- /usr/share/dotnet:/usr/share/dotnet
- /opt/ghc:/opt/ghc1
- /usr/local/share/boost:/usr/local/share/boost1
- /opt/hostedtoolcache:/opt/hostedtoolcache1
strategy:
fail-fast: false
matrix:
variant:
- arch: x86_64
runner: ubuntu-24.04
- arch: aarch64
runner: ubuntu-24.04-arm
# Don't run scheduled builds on forks:
if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
runs-on: ${{ matrix.variant.runner }}
env:
date:
ver:
ver_pure:
steps:
- name: "Remove unneeded stuff to free disk space"
run:
rm -rf /usr/local/lib/android/* /usr/share/dotnet/* /opt/ghc1/* "/usr/local/share/boost1/*" /opt/hostedtoolcache1/*
- uses: actions/checkout@v6
- name: Get the version and date
run: |
ver_pure=$(grep 'set(SoftFever_VERSION' version.inc | cut -d '"' -f2)
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
ver="PR-${{ github.event.number }}"
git_commit_hash="${{ github.event.pull_request.head.sha }}"
else
ver=V$ver_pure
git_commit_hash=""
fi
echo "ver=$ver" >> $GITHUB_ENV
echo "ver_pure=$ver_pure" >> $GITHUB_ENV
echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
shell: bash
- uses: flatpak/flatpak-github-actions/flatpak-builder@master
with:
bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
manifest-path: scripts/flatpak/io.github.softfever.OrcaSlicer.yml
cache: true
arch: ${{ matrix.variant.arch }}
upload-artifact: false
- name: Upload artifacts Flatpak
uses: actions/upload-artifact@v5
with:
name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
- name: Deploy Flatpak to nightly release
if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
uses: WebFreak001/deploy-nightly@v3.2.0
with:
upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
release_id: 137995723
asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
asset_content_type: application/octet-stream
max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted
# TODO: Re-enable after auto-update testing is complete (depends on build_linux)
# unit_tests:
# name: Unit Tests
# runs-on: ubuntu-24.04
# needs: build_linux
# if: ${{ !cancelled() && success() }}
# steps:
# - name: Checkout
# uses: actions/checkout@v6
# with:
# sparse-checkout: |
# .github
# scripts
# tests
# - name: Apt-Install Dependencies
# uses: ./.github/actions/apt-install-deps
# - name: Restore Test Artifact
# uses: actions/download-artifact@v6
# with:
# name: ${{ github.sha }}-tests
# - uses: lukka/get-cmake@latest
# with:
# cmakeVersion: "~3.28.0" # use most recent 3.28.x version
# - name: Unpackage and Run Unit Tests
# timeout-minutes: 20
# run: |
# tar -xvf build_tests.tar
# scripts/run_unit_tests.sh
# - name: Upload Test Logs
# uses: actions/upload-artifact@v5
# if: ${{ failure() }}
# with:
# name: unit-test-logs
# path: build/tests/**/*.log
# - name: Publish Test Results
# if: always()
# uses: EnricoMi/publish-unit-test-result-action/linux@v2
# with:
# files: "ctest_results.xml"
# TODO: Re-enable after auto-update testing is complete
# flatpak:
# name: "Flatpak"
# container:
# image: ghcr.io/flathub-infra/flatpak-github-actions:gnome-48
# options: --privileged
# volumes:
# - /usr/local/lib/android:/usr/local/lib/android
# - /usr/share/dotnet:/usr/share/dotnet
# - /opt/ghc:/opt/ghc1
# - /usr/local/share/boost:/usr/local/share/boost1
# - /opt/hostedtoolcache:/opt/hostedtoolcache1
# strategy:
# fail-fast: false
# matrix:
# variant:
# - arch: x86_64
# runner: ubuntu-24.04
# - arch: aarch64
# runner: ubuntu-24.04-arm
# # Don't run scheduled builds on forks:
# if: ${{ !cancelled() && (github.event_name != 'schedule' || github.repository == 'OrcaSlicer/OrcaSlicer') }}
# runs-on: ${{ matrix.variant.runner }}
# env:
# date:
# ver:
# ver_pure:
# steps:
# - name: "Remove unneeded stuff to free disk space"
# run:
# rm -rf /usr/local/lib/android/* /usr/share/dotnet/* /opt/ghc1/* "/usr/local/share/boost1/*" /opt/hostedtoolcache1/*
# - uses: actions/checkout@v6
# - name: Get the version and date
# run: |
# ver_pure=$(grep 'set(SoftFever_VERSION' version.inc | cut -d '"' -f2)
# if [[ "${{ github.event_name }}" == "pull_request" ]]; then
# ver="PR-${{ github.event.number }}"
# git_commit_hash="${{ github.event.pull_request.head.sha }}"
# else
# ver=V$ver_pure
# git_commit_hash=""
# fi
# echo "ver=$ver" >> $GITHUB_ENV
# echo "ver_pure=$ver_pure" >> $GITHUB_ENV
# echo "date=$(date +'%Y%m%d')" >> $GITHUB_ENV
# echo "git_commit_hash=$git_commit_hash" >> $GITHUB_ENV
# shell: bash
# - uses: flatpak/flatpak-github-actions/flatpak-builder@master
# with:
# bundle: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
# manifest-path: scripts/flatpak/io.github.softfever.OrcaSlicer.yml
# cache: true
# arch: ${{ matrix.variant.arch }}
# upload-artifact: false
# - name: Upload artifacts Flatpak
# uses: actions/upload-artifact@v5
# with:
# name: OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
# path: '/__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak'
# - name: Deploy Flatpak to nightly release
# if: github.repository == 'OrcaSlicer/OrcaSlicer' && github.ref == 'refs/heads/main'
# uses: WebFreak001/deploy-nightly@v3.2.0
# with:
# upload_url: https://uploads.github.com/repos/OrcaSlicer/OrcaSlicer/releases/137995723/assets{?name,label}
# release_id: 137995723
# asset_path: /__w/OrcaSlicer/OrcaSlicer/OrcaSlicer-Linux-flatpak_${{ env.ver }}_${{ matrix.variant.arch }}.flatpak
# asset_name: OrcaSlicer-Linux-flatpak_nightly_${{ matrix.variant.arch }}.flatpak
# asset_content_type: application/octet-stream
# max_releases: 1 # optional, if there are more releases than this matching the asset_name, the oldest ones are going to be deleted

View File

@@ -103,11 +103,13 @@ jobs:
if: inputs.os == 'macos-14'
working-directory: ${{ github.workspace }}
run: |
./build_release_macos.sh -s -n -x -a universal -t 10.15 -1
# TODO: Change back to -a universal after auto-update testing is complete
./build_release_macos.sh -s -n -x -a arm64 -t 10.15 -1
# Thanks to RaySajuuk, it's working now
- name: Sign app and notary
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/')) && inputs.os == 'macos-14'
# TODO: Remove feature/auto-update after testing is complete
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || github.ref == 'refs/heads/feature/auto-update') && inputs.os == 'macos-14'
working-directory: ${{ github.workspace }}
env:
BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
@@ -124,27 +126,28 @@ jobs:
security import $CERTIFICATE_PATH -P $P12_PASSWORD -A -t cert -f pkcs12 -k $KEYCHAIN_PATH
security list-keychain -d user -s $KEYCHAIN_PATH
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k $P12_PASSWORD $KEYCHAIN_PATH
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app
# TODO: Change build/arm64 back to build/universal after auto-update testing is complete
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" ${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer.app
# Sign OrcaSlicer_profile_validator.app if it exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app
if [ -f "${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" ${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer_profile_validator.app
fi
# Create main OrcaSlicer DMG without the profile validator helper
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
mkdir -p ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg/Applications
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
if [ -f "${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
mkdir -p ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg
rm -rf ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg/Applications
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
codesign --deep --force --verbose --options runtime --timestamp --entitlements ${{ github.workspace }}/scripts/disable_validation.entitlements --sign "$CERTIFICATE_ID" OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
@@ -159,23 +162,65 @@ jobs:
xcrun stapler staple OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
- name: Sign DMG for Sparkle auto-update
# TODO: Remove feature/auto-update after testing is complete
if: github.repository == 'OrcaSlicer/OrcaSlicer' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/heads/release/') || github.ref == 'refs/heads/feature/auto-update') && inputs.os == 'macos-14'
working-directory: ${{ github.workspace }}
env:
SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }}
run: |
# Get the Sparkle sign_update tool from deps (installed to OrcaSlicer_dep/bin)
SIGN_UPDATE="${{ github.workspace }}/deps/build/arm64/OrcaSlicer_dep/bin/sign_update"
# Fallback to x86_64 if arm64 not found
if [ ! -f "$SIGN_UPDATE" ]; then
SIGN_UPDATE="${{ github.workspace }}/deps/build/x86_64/OrcaSlicer_dep/bin/sign_update"
fi
if [ -f "$SIGN_UPDATE" ] && [ -n "$SPARKLE_PRIVATE_KEY" ]; then
# Write the private key to a temp file
echo "$SPARKLE_PRIVATE_KEY" > /tmp/sparkle_private_key
chmod 600 /tmp/sparkle_private_key
# Sign the DMG and capture the signature
SIGNATURE=$("$SIGN_UPDATE" "OrcaSlicer_Mac_universal_${{ env.ver }}.dmg" -f /tmp/sparkle_private_key)
# Clean up the key file
rm -f /tmp/sparkle_private_key
# Save signature to a file for later use in appcast generation
echo "$SIGNATURE" > OrcaSlicer_Mac_universal_${{ env.ver }}.dmg.sig
echo "Sparkle signature generated: $SIGNATURE"
# Also output as GitHub Actions output
echo "sparkle_signature=$SIGNATURE" >> $GITHUB_OUTPUT
else
echo "Warning: Sparkle sign_update tool not found at $SIGN_UPDATE or private key not set, skipping signature generation"
if [ ! -f "$SIGN_UPDATE" ]; then
echo "sign_update not found. Available files:"
ls -la "${{ github.workspace }}/deps/build/arm64/OrcaSlicer_dep/" || true
fi
fi
- name: Create DMG without notary
if: github.ref != 'refs/heads/main' && inputs.os == 'macos-14'
# TODO: Remove feature/auto-update exclusion after testing is complete
if: github.ref != 'refs/heads/main' && github.ref != 'refs/heads/feature/auto-update' && !startsWith(github.ref, 'refs/heads/release/') && inputs.os == 'macos-14'
working-directory: ${{ github.workspace }}
run: |
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_dmg/Applications
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# TODO: Change build/arm64 back to build/universal after auto-update testing is complete
mkdir -p ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg
rm -rf ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg/*
cp -R ${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer.app ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg/Applications
hdiutil create -volname "OrcaSlicer" -srcfolder ${{ github.workspace }}/build/arm64/OrcaSlicer_dmg -ov -format UDZO OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
# Create separate OrcaSlicer_profile_validator DMG if the app exists
if [ -f "${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
mkdir -p ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg
rm -rf ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/universal/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg/Applications
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/universal/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
if [ -f "${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer_profile_validator.app/Contents/MacOS/OrcaSlicer_profile_validator" ]; then
mkdir -p ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg
rm -rf ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg/*
cp -R ${{ github.workspace }}/build/arm64/OrcaSlicer/OrcaSlicer_profile_validator.app ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg/
ln -sfn /Applications ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg/Applications
hdiutil create -volname "OrcaSlicer Profile Validator" -srcfolder ${{ github.workspace }}/build/arm64/OrcaSlicer_profile_validator_dmg -ov -format UDZO OrcaSlicer_profile_validator_Mac_universal_${{ env.ver }}.dmg
fi
- name: Upload artifacts mac
@@ -185,6 +230,14 @@ jobs:
name: OrcaSlicer_Mac_universal_${{ env.ver }}
path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg
- name: Upload Sparkle signature mac
if: inputs.os == 'macos-14'
uses: actions/upload-artifact@v5
with:
name: OrcaSlicer_Mac_universal_${{ env.ver }}_sig
path: ${{ github.workspace }}/OrcaSlicer_Mac_universal_${{ env.ver }}.dmg.sig
if-no-files-found: ignore
- name: Upload OrcaSlicer_profile_validator DMG mac
if: inputs.os == 'macos-14'
uses: actions/upload-artifact@v5

138
.github/workflows/generate_appcast.yml vendored Normal file
View File

@@ -0,0 +1,138 @@
name: Generate Appcast
on:
release:
types: [published]
workflow_dispatch:
inputs:
version:
description: 'Version to generate appcast for (e.g., 2.3.2)'
required: true
jobs:
generate_appcast:
name: Generate and Deploy Appcast
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Get version from release or input
id: version
run: |
if [ "${{ github.event_name }}" == "release" ]; then
VERSION="${{ github.event.release.tag_name }}"
VERSION="${VERSION#v}" # Remove 'v' prefix if present
else
VERSION="${{ github.event.inputs.version }}"
fi
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "Version: $VERSION"
- name: Download release assets info
id: assets
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="v$VERSION"
# Get release info
RELEASE_URL="https://github.com/${{ github.repository }}/releases/tag/$TAG"
echo "release_url=$RELEASE_URL" >> $GITHUB_OUTPUT
# Get macOS DMG URL and size
# Use browser_download_url for public access (not .url which is the API endpoint)
MAC_ASSET=$(gh release view "$TAG" --json assets -q '.assets[] | select(.name | contains("Mac_universal")) | select(.name | endswith(".dmg"))')
if [ -n "$MAC_ASSET" ]; then
MAC_URL=$(echo "$MAC_ASSET" | jq -r '.browser_download_url // .url')
MAC_SIZE=$(echo "$MAC_ASSET" | jq -r '.size')
echo "mac_url=$MAC_URL" >> $GITHUB_OUTPUT
echo "mac_size=$MAC_SIZE" >> $GITHUB_OUTPUT
fi
# Get Windows installer URL and size
# Use browser_download_url for public access (not .url which is the API endpoint)
WIN_ASSET=$(gh release view "$TAG" --json assets -q '.assets[] | select(.name | contains("Windows_Installer")) | select(.name | endswith(".exe"))')
if [ -n "$WIN_ASSET" ]; then
WIN_URL=$(echo "$WIN_ASSET" | jq -r '.browser_download_url // .url')
WIN_SIZE=$(echo "$WIN_ASSET" | jq -r '.size')
echo "win_url=$WIN_URL" >> $GITHUB_OUTPUT
echo "win_size=$WIN_SIZE" >> $GITHUB_OUTPUT
fi
- name: Download signatures
id: signatures
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
VERSION="${{ steps.version.outputs.version }}"
TAG="v$VERSION"
# Try to download macOS signature artifact
MAC_SIG_ARTIFACT="OrcaSlicer_Mac_universal_V${VERSION}_sig"
if gh run download --name "$MAC_SIG_ARTIFACT" -D /tmp/mac_sig 2>/dev/null; then
MAC_SIG=$(cat /tmp/mac_sig/*.sig)
echo "mac_signature=$MAC_SIG" >> $GITHUB_OUTPUT
echo "Found macOS signature: $MAC_SIG"
else
echo "No macOS signature artifact found"
fi
# For Windows, signature would come from WinSparkle signing (if implemented)
# echo "win_signature=$WIN_SIG" >> $GITHUB_OUTPUT
- name: Generate appcast.xml
run: |
python scripts/generate_appcast.py \
--version "${{ steps.version.outputs.version }}" \
--release-notes-url "${{ steps.assets.outputs.release_url }}" \
--mac-url "${{ steps.assets.outputs.mac_url }}" \
--mac-signature "${{ steps.signatures.outputs.mac_signature }}" \
--mac-length "${{ steps.assets.outputs.mac_size }}" \
--output appcast.xml
echo "Generated appcast.xml:"
cat appcast.xml
- name: Upload appcast artifact
uses: actions/upload-artifact@v4
with:
name: appcast
path: appcast.xml
# Deploy to Cloudflare KV (for check-version.orcaslicer.com Worker)
- name: Deploy appcast to Cloudflare KV
if: github.event_name == 'release'
env:
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
CF_ACCOUNT_ID: ${{ secrets.CF_ACCOUNT_ID }}
KV_NAMESPACE_ID: ${{ secrets.CF_KV_NAMESPACE_ID }}
run: |
if [ -n "$CF_API_TOKEN" ] && [ -n "$CF_ACCOUNT_ID" ] && [ -n "$KV_NAMESPACE_ID" ]; then
# Deploy appcast.xml
curl -X PUT \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/storage/kv/namespaces/$KV_NAMESPACE_ID/values/appcast.xml" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: text/plain" \
--data-binary @appcast.xml
echo "Appcast deployed to Cloudflare KV"
# Deploy macOS signature file (for verification/auditing)
if [ -n "${{ steps.signatures.outputs.mac_signature }}" ]; then
echo "${{ steps.signatures.outputs.mac_signature }}" > mac_signature.txt
curl -X PUT \
"https://api.cloudflare.com/client/v4/accounts/$CF_ACCOUNT_ID/storage/kv/namespaces/$KV_NAMESPACE_ID/values/signatures/${{ steps.version.outputs.version }}/mac.sig" \
-H "Authorization: Bearer $CF_API_TOKEN" \
-H "Content-Type: text/plain" \
--data-binary @mac_signature.txt
echo "macOS signature deployed to Cloudflare KV"
fi
else
echo "Cloudflare credentials not configured, skipping deployment"
fi

View File

@@ -811,6 +811,11 @@ function(orcaslicer_copy_dlls target config postfix output_dlls)
${TOP_LEVEL_PROJECT_DIR}/deps/WebView2/lib/win-${_arch}/WebView2Loader.dll
DESTINATION ${_out_dir})
# WinSparkle for auto-updates
if(EXISTS "${CMAKE_PREFIX_PATH}/bin/WinSparkle.dll")
file(COPY ${CMAKE_PREFIX_PATH}/bin/WinSparkle.dll DESTINATION ${_out_dir})
endif()
file(COPY ${CMAKE_PREFIX_PATH}/bin/occt/TKBO.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKBRep.dll
${CMAKE_PREFIX_PATH}/bin/occt/TKCAF.dll

17
deps/CMakeLists.txt vendored
View File

@@ -386,6 +386,16 @@ endif ()
include(OCCT/OCCT.cmake)
include(OpenCV/OpenCV.cmake)
# WinSparkle for Windows auto-updates
if(WIN32)
include(WinSparkle/WinSparkle.cmake)
endif()
# Sparkle 2 for macOS auto-updates
if(APPLE)
include(Sparkle/Sparkle.cmake)
endif()
set(_dep_list
dep_Boost
dep_TBB
@@ -410,12 +420,19 @@ set(_dep_list
if (MSVC)
# Experimental
#list(APPEND _dep_list "dep_qhull")
# WinSparkle for auto-updates
list(APPEND _dep_list "dep_WinSparkle")
else()
list(APPEND _dep_list "dep_Qhull")
# Not working, static build has different Eigen
#list(APPEND _dep_list "dep_libigl")
endif()
if (APPLE)
# Sparkle 2 for auto-updates
list(APPEND _dep_list "dep_Sparkle")
endif()
add_custom_target(deps ALL DEPENDS ${_dep_list})
# Note: I'm not using any of the LOG_xxx options in ExternalProject_Add() commands

27
deps/Sparkle/Sparkle.cmake vendored Normal file
View File

@@ -0,0 +1,27 @@
# Sparkle 2 - Auto-update framework for macOS
# https://sparkle-project.org/
# https://github.com/sparkle-project/Sparkle
#
# Sparkle is distributed as a pre-built framework, so we just download and extract.
if(APPLE)
set(SPARKLE_VERSION "2.8.1")
ExternalProject_Add(
dep_Sparkle
EXCLUDE_FROM_ALL ON
URL "https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-${SPARKLE_VERSION}.tar.xz"
URL_HASH SHA256=5cddb7695674ef7704268f38eccaee80e3accbf19e61c1689efff5b6116d85be
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/Sparkle
# No build step needed - just install pre-built framework and tools
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ${CMAKE_COMMAND} -E make_directory ${DESTDIR}/Frameworks
COMMAND ${CMAKE_COMMAND} -E copy_directory
<SOURCE_DIR>/Sparkle.framework ${DESTDIR}/Frameworks/Sparkle.framework
# Also install the Sparkle CLI tools (sign_update, generate_appcast) for CI/CD signing
COMMAND ${CMAKE_COMMAND} -E make_directory ${DESTDIR}/bin
COMMAND ${CMAKE_COMMAND} -E copy <SOURCE_DIR>/bin/sign_update ${DESTDIR}/bin/sign_update
COMMAND ${CMAKE_COMMAND} -E copy <SOURCE_DIR>/bin/generate_appcast ${DESTDIR}/bin/generate_appcast
)
endif()

33
deps/WinSparkle/WinSparkle.cmake vendored Normal file
View File

@@ -0,0 +1,33 @@
# WinSparkle - Auto-update framework for Windows
# https://winsparkle.org/
# https://github.com/vslavik/winsparkle
#
# WinSparkle is distributed as pre-built binaries, so we just download and extract.
if(WIN32)
set(WINSPARKLE_VERSION "0.8.3")
# Determine architecture
if(CMAKE_SIZEOF_VOID_P EQUAL 8)
set(WINSPARKLE_ARCH "x64")
else()
set(WINSPARKLE_ARCH "x86")
endif()
ExternalProject_Add(
dep_WinSparkle
EXCLUDE_FROM_ALL ON
URL "https://github.com/vslavik/winsparkle/releases/download/v${WINSPARKLE_VERSION}/WinSparkle-${WINSPARKLE_VERSION}.zip"
URL_HASH SHA256=5ff4a4604c78d57e01d83e22f79f5ffea0c4969defd48b45c69ccbd6b1a71e94
DOWNLOAD_DIR ${DEP_DOWNLOAD_DIR}/WinSparkle
# No build step needed - just install pre-built binaries
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
INSTALL_COMMAND ${CMAKE_COMMAND} -E copy_directory
<SOURCE_DIR>/include ${DESTDIR}/include
COMMAND ${CMAKE_COMMAND} -E copy
<SOURCE_DIR>/${WINSPARKLE_ARCH}/Release/WinSparkle.dll ${DESTDIR}/bin/WinSparkle.dll
COMMAND ${CMAKE_COMMAND} -E copy
<SOURCE_DIR>/${WINSPARKLE_ARCH}/Release/WinSparkle.lib ${DESTDIR}/lib/WinSparkle.lib
)
endif()

208
scripts/generate_appcast.py Executable file
View File

@@ -0,0 +1,208 @@
#!/usr/bin/env python3
"""
Generate appcast.xml for Sparkle/WinSparkle auto-updates.
This script generates an appcast XML file that can be used by both
Sparkle (macOS) and WinSparkle (Windows) for auto-update functionality.
Usage:
python generate_appcast.py --version 2.1.0 \
--win-url https://github.com/.../OrcaSlicer_Windows.exe \
--win-signature "BASE64_SIGNATURE" \
--win-length 150000000 \
--mac-url https://github.com/.../OrcaSlicer_Mac.dmg \
--mac-signature "BASE64_SIGNATURE" \
--mac-length 200000000 \
--release-notes-url https://github.com/.../releases/tag/v2.1.0 \
--output appcast.xml
"""
import argparse
import os
import sys
from datetime import datetime, timezone
from xml.etree import ElementTree as ET
from xml.dom import minidom
SPARKLE_NS = "http://www.andymatuschak.org/xml-namespaces/sparkle"
DC_NS = "http://purl.org/dc/elements/1.1/"
def create_appcast(
version: str,
release_notes_url: str,
win_url: str = None,
win_signature: str = None,
win_length: int = None,
mac_url: str = None,
mac_signature: str = None,
mac_length: int = None,
title: str = "OrcaSlicer Updates",
description: str = "Most recent updates to OrcaSlicer",
link: str = "https://github.com/OrcaSlicer/OrcaSlicer",
) -> str:
"""
Create an appcast XML string.
Args:
version: Version string (e.g., "2.1.0")
release_notes_url: URL to release notes HTML page
win_url: Download URL for Windows installer
win_signature: EdDSA signature for Windows installer
win_length: File size in bytes for Windows installer
mac_url: Download URL for macOS DMG
mac_signature: EdDSA signature for macOS DMG
mac_length: File size in bytes for macOS DMG
title: Feed title
description: Feed description
link: Link to project homepage
Returns:
XML string of the appcast
"""
# Register namespaces
ET.register_namespace("sparkle", SPARKLE_NS)
ET.register_namespace("dc", DC_NS)
# Create root RSS element
rss = ET.Element("rss")
rss.set("version", "2.0")
rss.set(f"xmlns:sparkle", SPARKLE_NS)
rss.set(f"xmlns:dc", DC_NS)
# Create channel
channel = ET.SubElement(rss, "channel")
ET.SubElement(channel, "title").text = title
ET.SubElement(channel, "link").text = link
ET.SubElement(channel, "description").text = description
ET.SubElement(channel, "language").text = "en"
# Create item for this release
item = ET.SubElement(channel, "item")
ET.SubElement(item, "title").text = f"Version {version}"
# Publication date in RFC 2822 format
pub_date = datetime.now(timezone.utc).strftime("%a, %d %b %Y %H:%M:%S +0000")
ET.SubElement(item, "pubDate").text = pub_date
# Release notes link
release_notes = ET.SubElement(item, f"{{{SPARKLE_NS}}}releaseNotesLink")
release_notes.text = release_notes_url
# Windows enclosure
if win_url and win_signature:
win_enclosure = ET.SubElement(item, "enclosure")
win_enclosure.set("url", win_url)
win_enclosure.set(f"{{{SPARKLE_NS}}}version", version)
win_enclosure.set(f"{{{SPARKLE_NS}}}os", "windows")
win_enclosure.set(f"{{{SPARKLE_NS}}}edSignature", win_signature)
if win_length:
win_enclosure.set("length", str(win_length))
win_enclosure.set("type", "application/octet-stream")
# macOS enclosure
if mac_url and mac_signature:
mac_enclosure = ET.SubElement(item, "enclosure")
mac_enclosure.set("url", mac_url)
mac_enclosure.set(f"{{{SPARKLE_NS}}}version", version)
mac_enclosure.set(f"{{{SPARKLE_NS}}}os", "macos")
mac_enclosure.set(f"{{{SPARKLE_NS}}}edSignature", mac_signature)
if mac_length:
mac_enclosure.set("length", str(mac_length))
mac_enclosure.set("type", "application/octet-stream")
# Convert to pretty-printed string
rough_string = ET.tostring(rss, encoding="unicode", method="xml")
reparsed = minidom.parseString(rough_string)
pretty_xml = reparsed.toprettyxml(indent=" ", encoding=None)
# Remove extra blank lines
lines = [line for line in pretty_xml.split("\n") if line.strip()]
return "\n".join(lines)
def main():
parser = argparse.ArgumentParser(
description="Generate appcast.xml for OrcaSlicer auto-updates",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=__doc__,
)
parser.add_argument(
"--version", "-v", required=True, help="Version string (e.g., 2.1.0)"
)
parser.add_argument(
"--release-notes-url",
"-r",
required=True,
help="URL to release notes page",
)
parser.add_argument(
"--win-url", help="Download URL for Windows installer"
)
parser.add_argument(
"--win-signature", help="EdDSA signature for Windows installer"
)
parser.add_argument(
"--win-length", type=int, help="File size in bytes for Windows installer"
)
parser.add_argument(
"--mac-url", help="Download URL for macOS DMG"
)
parser.add_argument(
"--mac-signature", help="EdDSA signature for macOS DMG"
)
parser.add_argument(
"--mac-length", type=int, help="File size in bytes for macOS DMG"
)
parser.add_argument(
"--output", "-o", default="appcast.xml", help="Output file path"
)
parser.add_argument(
"--title", default="OrcaSlicer Updates", help="Feed title"
)
parser.add_argument(
"--link",
default="https://github.com/OrcaSlicer/OrcaSlicer",
help="Project homepage URL",
)
args = parser.parse_args()
# Validate that at least one platform is specified
has_windows = args.win_url and args.win_signature
has_macos = args.mac_url and args.mac_signature
if not has_windows and not has_macos:
print(
"Error: At least one platform (Windows or macOS) must have both URL and signature",
file=sys.stderr,
)
sys.exit(1)
# Generate appcast
xml_content = create_appcast(
version=args.version,
release_notes_url=args.release_notes_url,
win_url=args.win_url,
win_signature=args.win_signature,
win_length=args.win_length,
mac_url=args.mac_url,
mac_signature=args.mac_signature,
mac_length=args.mac_length,
title=args.title,
link=args.link,
)
# Write output
if args.output == "-":
print(xml_content)
else:
with open(args.output, "w", encoding="utf-8") as f:
f.write(xml_content)
print(f"Appcast written to: {args.output}")
if __name__ == "__main__":
main()

View File

@@ -131,6 +131,11 @@ if (APPLE)
# add_definitions(-DBOOST_THREAD_DONT_USE_CHRONO -DBOOST_NO_CXX11_RVALUE_REFERENCES -DBOOST_THREAD_USES_MOVE)
# -liconv: boost links to libiconv by default
target_link_libraries(OrcaSlicer "-liconv -framework IOKit" "-framework CoreFoundation" "-framework AVFoundation" "-framework AVKit" "-framework CoreMedia" "-framework VideoToolbox" -lc++)
# Set rpath for embedded frameworks (Sparkle)
set_target_properties(OrcaSlicer PROPERTIES
INSTALL_RPATH "@executable_path/../Frameworks"
BUILD_WITH_INSTALL_RPATH TRUE
)
elseif (MSVC)
# Manifest is provided through OrcaSlicer.rc, don't generate your own.
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /MANIFEST:NO")
@@ -252,6 +257,24 @@ else ()
COMMAND ln -sfn "${SLIC3R_RESOURCES_DIR}" "${BIN_RESOURCES_DIR}"
COMMENT "Symlinking the resources directory into the build tree"
VERBATIM)
# Embed Sparkle framework for auto-updates
if (CMAKE_MACOSX_BUNDLE)
find_library(SPARKLE_FRAMEWORK Sparkle PATHS ${CMAKE_PREFIX_PATH}/Frameworks)
if(SPARKLE_FRAMEWORK)
if (CMAKE_CONFIGURATION_TYPES)
set(BUNDLE_FRAMEWORKS_DIR "${CMAKE_CURRENT_BINARY_DIR}/$<CONFIG>/OrcaSlicer.app/Contents/Frameworks")
else()
set(BUNDLE_FRAMEWORKS_DIR "${CMAKE_CURRENT_BINARY_DIR}/OrcaSlicer.app/Contents/Frameworks")
endif()
add_custom_command(TARGET OrcaSlicer POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${BUNDLE_FRAMEWORKS_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${SPARKLE_FRAMEWORK}" "${BUNDLE_FRAMEWORKS_DIR}/Sparkle.framework"
COMMENT "Embedding Sparkle.framework into app bundle"
VERBATIM)
message(STATUS "Sparkle framework will be embedded: ${SPARKLE_FRAMEWORK}")
endif()
endif()
endif ()
# Slic3r binary install target. Default build type is release in case no CMAKE_BUILD_TYPE is provided.

View File

@@ -135,5 +135,14 @@
<key>ASAN_OPTIONS</key>
<string>detect_container_overflow=0</string>
</dict>
<!-- Sparkle 2 Auto-Update Configuration -->
<key>SUFeedURL</key>
<string>https://check-version.orcaslicer.com/appcast.xml</string>
<key>SUPublicEDKey</key>
<string>eLFARgt9i0VZQR4FtXiTL6jdwjkGr2RMPjfYCCfBWeM=</string>
<key>SUEnableAutomaticChecks</key>
<true/>
<key>SUAllowsAutomaticUpdates</key>
<true/>
</dict>
</plist>

View File

@@ -456,6 +456,8 @@ set(SLIC3R_GUI_SOURCES
GUI/UnsavedChangesDialog.hpp
GUI/UpdateDialogs.cpp
GUI/UpdateDialogs.hpp
GUI/UpdateManager.hpp
GUI/UpdateManager.cpp
GUI/UpgradePanel.cpp
GUI/UpgradePanel.hpp
GUI/UserManager.cpp
@@ -648,6 +650,7 @@ if (APPLE)
GUI/InstanceCheckMac.mm
GUI/InstanceCheckMac.h
GUI/GUI_UtilsMac.mm
GUI/UpdateManagerMac.mm
GUI/wxMediaCtrl2.mm
GUI/wxMediaCtrl2.h
)
@@ -672,11 +675,6 @@ string(REPLACE "\r" "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}")
string(REPLACE "\t" "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}")
string(REPLACE " " "" ORCA_UPDATER_SIG_KEY_B64 "${ORCA_UPDATER_SIG_KEY_B64}")
set(ORCA_UPDATER_SIG_KEY_AVAILABLE 0)
if(ORCA_UPDATER_SIG_KEY_B64)
set(ORCA_UPDATER_SIG_KEY_AVAILABLE 1)
endif()
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/GeneratedConfig.hpp.in
${CMAKE_CURRENT_BINARY_DIR}/GeneratedConfig.hpp
@ONLY)
@@ -707,6 +705,16 @@ target_link_libraries(libslic3r_gui libslic3r cereal::cereal imgui imguizmo mini
if (MSVC)
target_link_libraries(libslic3r_gui Setupapi.lib)
# WinSparkle for auto-updates
find_library(WINSPARKLE_LIB WinSparkle PATHS ${CMAKE_PREFIX_PATH}/lib)
if(WINSPARKLE_LIB)
target_link_libraries(libslic3r_gui ${WINSPARKLE_LIB})
target_include_directories(libslic3r_gui PRIVATE ${CMAKE_PREFIX_PATH}/include)
target_compile_definitions(libslic3r_gui PRIVATE ORCA_HAS_WINSPARKLE)
message(STATUS "WinSparkle found: ${WINSPARKLE_LIB}")
else()
message(STATUS "WinSparkle not found, auto-update disabled on Windows")
endif()
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
FIND_LIBRARY(WAYLAND_SERVER_LIBRARIES NAMES wayland-server)
FIND_LIBRARY(WAYLAND_EGL_LIBRARIES NAMES wayland-egl)
@@ -722,6 +730,15 @@ elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
)
elseif (APPLE)
target_link_libraries(libslic3r_gui ${DISKARBITRATION_LIBRARY} "-framework Security")
# Sparkle 2 for auto-updates
find_library(SPARKLE_FRAMEWORK Sparkle PATHS ${CMAKE_PREFIX_PATH}/Frameworks)
if(SPARKLE_FRAMEWORK)
target_link_libraries(libslic3r_gui ${SPARKLE_FRAMEWORK})
target_compile_definitions(libslic3r_gui PRIVATE ORCA_HAS_SPARKLE)
message(STATUS "Sparkle found: ${SPARKLE_FRAMEWORK}")
else()
message(STATUS "Sparkle not found, auto-update disabled on macOS")
endif()
endif()
if (SLIC3R_STATIC)

View File

@@ -91,6 +91,7 @@
#include "Tab.hpp"
#include "SysInfoDialog.hpp"
#include "UpdateDialogs.hpp"
#include "UpdateManager.hpp"
#include "Mouse3DController.hpp"
#include "RemovableDriveManager.hpp"
#include "InstanceCheck.hpp"
@@ -984,6 +985,16 @@ void GUI_App::post_init()
BOOST_LOG_TRIVIAL(info) << __FUNCTION__ << " sync_user_preset: false";
}
// Initialize the auto-update manager
#if defined(ORCA_HAS_SPARKLE) || defined(ORCA_HAS_WINSPARKLE)
{
// Use the appcast URL for Sparkle/WinSparkle (different from version_check_url)
std::string appcast_url = "https://check-version.orcaslicer.com/appcast.xml";
UpdateManager::init(appcast_url, "");
BOOST_LOG_TRIVIAL(info) << "UpdateManager initialized with appcast URL: " << appcast_url;
}
#endif
// The extra CallAfter() is needed because of Mac, where this is the only way
// to popup a modal dialog on start without screwing combo boxes.
// This is ugly but I honestly found no better way to do it.
@@ -998,7 +1009,12 @@ void GUI_App::post_init()
bool sys_preset = app_config->get("sync_system_preset") == "true";
this->preset_updater->sync(http_url, language, network_ver, sys_preset ? preset_bundle : nullptr);
// Check for application updates
#if defined(ORCA_HAS_SPARKLE) || defined(ORCA_HAS_WINSPARKLE)
UpdateManager::check_for_updates_background();
#else
this->check_new_version_sf();
#endif
if (is_user_login() && !app_config->get_stealth_mode()) {
// this->check_privacy_version(0);
request_user_handle(0);
@@ -2240,6 +2256,11 @@ bool GUI_App::OnInit()
int GUI_App::OnExit()
{
// Shutdown the auto-update manager
#if defined(ORCA_HAS_SPARKLE) || defined(ORCA_HAS_WINSPARKLE)
UpdateManager::shutdown();
#endif
stop_sync_user_preset();
if (m_device_manager) {
@@ -4630,7 +4651,7 @@ std::string base64url_encode(const unsigned char* data, std::size_t length)
std::optional<std::vector<unsigned char>> load_signature_key()
{
#if ORCA_UPDATER_SIG_KEY_AVAILABLE
#ifdef ORCA_UPDATER_SIG_KEY_B64
std::string key = ORCA_UPDATER_SIG_KEY_B64;
boost::algorithm::trim(key);
if (key.empty())

View File

@@ -60,6 +60,7 @@
#include "MarkdownTip.hpp"
#include "NetworkTestDialog.hpp"
#include "ConfigWizard.hpp"
#include "UpdateManager.hpp"
#include "Widgets/WebView.hpp"
#include "DailyTips.hpp"
#include "FilamentMapDialog.hpp"
@@ -2332,7 +2333,11 @@ static wxMenu* generate_help_menu()
// Check New Version
append_menu_item(helpMenu, wxID_ANY, _L("Check for Update"), _L("Check for Update"),
[](wxCommandEvent&) {
#if defined(ORCA_HAS_SPARKLE) || defined(ORCA_HAS_WINSPARKLE)
UpdateManager::check_for_updates_interactive();
#else
wxGetApp().check_new_version_sf(true, 1);
#endif
}, "", nullptr, []() {
return true;
});

View File

@@ -0,0 +1,174 @@
#include "UpdateManager.hpp"
#include "libslic3r/libslic3r.h"
#include <boost/log/trivial.hpp>
// Windows implementation uses WinSparkle
#if defined(_WIN32) && defined(ORCA_HAS_WINSPARKLE)
#include <winsparkle.h>
#include <windows.h>
#endif
namespace Slic3r {
namespace GUI {
// Static member definitions (defined in UpdateManagerMac.mm for macOS)
#if !defined(__APPLE__)
bool UpdateManager::s_initialized = false;
std::string UpdateManager::s_appcast_url;
std::string UpdateManager::s_public_key;
#endif
#if defined(_WIN32) && defined(ORCA_HAS_WINSPARKLE)
// ============================================================================
// Windows Implementation (WinSparkle)
// ============================================================================
void UpdateManager::init(const std::string& appcast_url, const std::string& public_key)
{
if (s_initialized) {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager::init called multiple times";
return;
}
s_appcast_url = appcast_url;
s_public_key = public_key;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Initializing WinSparkle with appcast URL: " << appcast_url;
// Set application details for registry storage
win_sparkle_set_app_details(L"SoftFever", L"OrcaSlicer", L"" SLIC3R_VERSION);
// Set appcast URL
win_sparkle_set_appcast_url(appcast_url.c_str());
// Set EdDSA public key for signature verification
if (!public_key.empty()) {
win_sparkle_set_dsa_pub_pem(public_key.c_str());
BOOST_LOG_TRIVIAL(info) << "UpdateManager: EdDSA public key configured";
} else {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager: No public key provided, signature verification disabled";
}
// Initialize WinSparkle (starts background thread)
win_sparkle_init();
s_initialized = true;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: WinSparkle initialized successfully";
}
void UpdateManager::check_for_updates_interactive()
{
if (!s_initialized) {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager::check_for_updates_interactive called before init";
return;
}
BOOST_LOG_TRIVIAL(info) << "UpdateManager: User-triggered update check";
win_sparkle_check_update_with_ui();
}
void UpdateManager::check_for_updates_background()
{
if (!s_initialized) {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager::check_for_updates_background called before init";
return;
}
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Background update check";
win_sparkle_check_update_without_ui();
}
void UpdateManager::shutdown()
{
if (!s_initialized) {
return;
}
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Shutting down WinSparkle";
win_sparkle_cleanup();
s_initialized = false;
}
void UpdateManager::set_automatic_check_enabled(bool enabled)
{
// WinSparkle manages automatic checks via registry settings
// The user can configure this through WinSparkle's own preferences dialog
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Automatic check enabled: " << enabled;
}
#elif defined(__linux__)
// ============================================================================
// Linux Implementation (Stub - AppImageUpdate deferred)
// ============================================================================
void UpdateManager::init(const std::string& appcast_url, const std::string& public_key)
{
s_appcast_url = appcast_url;
s_public_key = public_key;
s_initialized = true;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Linux auto-update not yet implemented (stub)";
}
void UpdateManager::check_for_updates_interactive()
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Linux interactive update check not implemented";
// TODO: Implement AppImageUpdate integration
// For now, fall back to the old behavior (handled by caller)
}
void UpdateManager::check_for_updates_background()
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Linux background update check not implemented";
// TODO: Implement AppImageUpdate integration
}
void UpdateManager::shutdown()
{
s_initialized = false;
}
void UpdateManager::set_automatic_check_enabled(bool enabled)
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Linux automatic check not implemented";
}
#elif !defined(__APPLE__)
// ============================================================================
// Fallback Implementation (No auto-update support)
// ============================================================================
void UpdateManager::init(const std::string& appcast_url, const std::string& public_key)
{
s_appcast_url = appcast_url;
s_public_key = public_key;
s_initialized = true;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: No auto-update support on this platform";
}
void UpdateManager::check_for_updates_interactive()
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Interactive update check not available";
}
void UpdateManager::check_for_updates_background()
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Background update check not available";
}
void UpdateManager::shutdown()
{
s_initialized = false;
}
void UpdateManager::set_automatic_check_enabled(bool enabled)
{
// No-op
}
#endif
// Note: macOS implementation is in UpdateManagerMac.mm
} // namespace GUI
} // namespace Slic3r

View File

@@ -0,0 +1,41 @@
#pragma once
#include <string>
namespace Slic3r {
namespace GUI {
/// Cross-platform auto-update manager abstraction.
/// Uses WinSparkle on Windows, Sparkle 2 on macOS.
/// Linux support deferred (stub implementation).
class UpdateManager {
public:
/// Initialize the platform-specific updater.
/// Must be called once during application startup (GUI_App::on_init_inner).
/// @param appcast_url URL to the appcast XML feed
/// @param public_key Base64-encoded Ed25519 public key for signature verification
static void init(const std::string& appcast_url, const std::string& public_key);
/// Manual check triggered by user (Help -> Check for Updates).
/// Shows UI regardless of whether an update is available.
static void check_for_updates_interactive();
/// Background check called on application startup.
/// Only shows UI if an update is available.
static void check_for_updates_background();
/// Cleanup on application exit.
static void shutdown();
/// Enable or disable automatic update checks.
/// @param enabled If true, automatically check for updates periodically
static void set_automatic_check_enabled(bool enabled);
private:
static bool s_initialized;
static std::string s_appcast_url;
static std::string s_public_key;
};
} // namespace GUI
} // namespace Slic3r

View File

@@ -0,0 +1,204 @@
#include "UpdateManager.hpp"
#ifdef __APPLE__
#ifdef ORCA_HAS_SPARKLE
#import <Sparkle/Sparkle.h>
#endif
#include <boost/log/trivial.hpp>
// ============================================================================
// macOS Implementation (Sparkle 2)
// ============================================================================
namespace Slic3r {
namespace GUI {
// Static member definitions (defined in UpdateManager.cpp for other platforms)
// For macOS, we need to define them here since UpdateManagerMac.mm is compiled instead
bool UpdateManager::s_initialized = false;
std::string UpdateManager::s_appcast_url;
std::string UpdateManager::s_public_key;
#ifdef ORCA_HAS_SPARKLE
// Sparkle updater delegate for custom behavior
@interface OrcaSparkleDelegate : NSObject <SPUUpdaterDelegate>
@end
@implementation OrcaSparkleDelegate
// Optional: Add custom parameters to the appcast request
- (NSArray<NSDictionary<NSString *, NSString *> *> *)feedParametersForUpdater:(SPUUpdater *)updater
sendingSystemProfile:(BOOL)sendingProfile
{
// Add OrcaSlicer-specific parameters to the update check request
NSString *version = [NSString stringWithUTF8String:SLIC3R_VERSION];
NSString *osVersion = [[NSProcessInfo processInfo] operatingSystemVersionString];
return @[
@{@"key": @"app_version", @"value": version ?: @"unknown"},
@{@"key": @"os_version", @"value": osVersion ?: @"unknown"}
];
}
// Optional: Handle update errors
- (void)updater:(SPUUpdater *)updater didAbortWithError:(NSError *)error
{
BOOST_LOG_TRIVIAL(error) << "UpdateManager: Sparkle update aborted with error: "
<< [[error localizedDescription] UTF8String];
}
// Optional: Called when an update is found
- (void)updater:(SPUUpdater *)updater didFindValidUpdate:(SUAppcastItem *)item
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Found update to version "
<< [[item displayVersionString] UTF8String];
}
// Optional: Called when no update is available
- (void)updaterDidNotFindUpdate:(SPUUpdater *)updater
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: No update available";
}
@end
// Static Sparkle controller and delegate instances
static SPUStandardUpdaterController *s_updater_controller = nil;
static OrcaSparkleDelegate *s_updater_delegate = nil;
void UpdateManager::init(const std::string& appcast_url, const std::string& public_key)
{
if (s_initialized) {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager::init called multiple times";
return;
}
s_appcast_url = appcast_url;
s_public_key = public_key;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Initializing Sparkle 2";
@autoreleasepool {
// Create the delegate
s_updater_delegate = [[OrcaSparkleDelegate alloc] init];
// Create the standard updater controller
// This reads SUFeedURL and SUPublicEDKey from Info.plist
s_updater_controller = [[SPUStandardUpdaterController alloc]
initWithStartingUpdater:YES
updaterDelegate:s_updater_delegate
userDriverDelegate:nil];
if (s_updater_controller) {
s_initialized = true;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Sparkle 2 initialized successfully";
} else {
BOOST_LOG_TRIVIAL(error) << "UpdateManager: Failed to initialize Sparkle 2";
}
}
}
void UpdateManager::check_for_updates_interactive()
{
if (!s_initialized || !s_updater_controller) {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager::check_for_updates_interactive called before init";
return;
}
BOOST_LOG_TRIVIAL(info) << "UpdateManager: User-triggered update check (Sparkle)";
@autoreleasepool {
[s_updater_controller checkForUpdates:nil];
}
}
void UpdateManager::check_for_updates_background()
{
if (!s_initialized || !s_updater_controller) {
BOOST_LOG_TRIVIAL(warning) << "UpdateManager::check_for_updates_background called before init";
return;
}
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Background update check (Sparkle)";
@autoreleasepool {
SPUUpdater *updater = s_updater_controller.updater;
if (updater) {
[updater checkForUpdatesInBackground];
}
}
}
void UpdateManager::shutdown()
{
if (!s_initialized) {
return;
}
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Shutting down Sparkle";
@autoreleasepool {
// Sparkle handles cleanup automatically when the controller is released
s_updater_controller = nil;
s_updater_delegate = nil;
}
s_initialized = false;
}
void UpdateManager::set_automatic_check_enabled(bool enabled)
{
if (!s_initialized || !s_updater_controller) {
return;
}
@autoreleasepool {
SPUUpdater *updater = s_updater_controller.updater;
if (updater) {
updater.automaticallyChecksForUpdates = enabled;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Automatic check enabled: " << enabled;
}
}
}
#else // !ORCA_HAS_SPARKLE
// Stub implementation when Sparkle is not available
void UpdateManager::init(const std::string& appcast_url, const std::string& public_key)
{
s_appcast_url = appcast_url;
s_public_key = public_key;
s_initialized = true;
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Sparkle not available (stub)";
}
void UpdateManager::check_for_updates_interactive()
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Interactive update check not available (no Sparkle)";
}
void UpdateManager::check_for_updates_background()
{
BOOST_LOG_TRIVIAL(info) << "UpdateManager: Background update check not available (no Sparkle)";
}
void UpdateManager::shutdown()
{
s_initialized = false;
}
void UpdateManager::set_automatic_check_enabled(bool enabled)
{
// No-op
}
#endif // ORCA_HAS_SPARKLE
} // namespace GUI
} // namespace Slic3r
#endif // __APPLE__

View File

@@ -1,5 +1,5 @@
#pragma once
// Ed25519 public key for verifying update signatures (used by load_signature_key)
#define ORCA_UPDATER_SIG_KEY_B64 "@ORCA_UPDATER_SIG_KEY_B64@"
#define ORCA_UPDATER_SIG_KEY_AVAILABLE @ORCA_UPDATER_SIG_KEY_AVAILABLE@