Files
OneCAD-Draft/.github/workflows/release.yml
T
Felix RiestraandClaude Sonnet 5 e767507850 macOS: add a launcher shim so a cold Finder open also works (#1039)
The previous commit's Apple Event handler only ever catches an open
sent to an already-running process. A cold double-click (app not
running yet) still failed: every registration point tried inside the
real binary — before app::run(), on the Subscription's first poll,
even the literal first statement of main() — reliably caught a warm
open but never the original cold-launch event. A raw
AEInstallEventHandler in a separate, minimal, non-AppKit process (no
winit/iced, just a manual CFRunLoopRunInMode pump) still missed it
too, ruling out "the real binary's heavy dependency graph loading
before main() wins the race" as the explanation.

What does reliably catch a cold launch, verified against real
Finder-equivalent opens: a from-scratch NSApplication with its own
delegate implementing application:openURLs: — in a process where we
own setDelegate: from the start, with no contention over the single
delegate slot winit already claims. That has to be a separate process
from the real GUI binary (only one -[NSApplication run] call can own a
process), so src/bin/ocs_launcher.rs is now the .app's actual
CFBundleExecutable: a small, dependency-light binary (no iced, wgpu,
or cadkernel) whose only job is to catch the open and relay it.

Relaying by replacing this process's image (exec, same PID) or by
spawning a child and exiting turned out to both be dead ends: Launch
Services tracks "the running instance of this bundle" by the
Application Serial Number it handed out to whichever process it
actually launched — this launcher, not whatever it execs into or
spawns. Once the launcher's own ASN stopped pointing at a live,
listening process, every later open Finder tried to deliver silently
went nowhere (measured with lsappinfo against a real second "Open
With", not assumed). So the launcher now stays running for the app's
session — invisible, via NSApplicationActivationPolicyAccessory, since
it owns no window — and relays every open (cold or warm) over the same
channel a second CLI invocation already uses: io::single_instance.

That relay only worked once single_instance's rendezvous key stopped
being computed from the running executable's own path. That was fine
when one binary always did the job, but the launcher and the real
binary are different executables and independently computed different
ports, so they could never find each other. Anchor the key on the
enclosing .app bundle instead, shared by both.

packaging/build_macos_signed.sh and .github/workflows/release.yml now
build ocs_launcher and place it at Contents/MacOS/OpenCADStudio (the
CFBundleExecutable Info.plist already names), with the real GUI binary
renamed to Contents/MacOS/OpenCADStudio-App alongside it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-07 16:45:02 +02:00

476 lines
18 KiB
YAML

name: Release
on:
release:
types: [published]
workflow_call:
inputs:
tag:
required: true
type: string
commit:
required: true
type: string
target:
default: all
type: string
workflow_dispatch:
inputs:
target:
description: Build target
required: true
default: all
type: choice
options:
- all
- snap
env:
RELEASE_TAG: ${{ inputs.tag || github.event.release.tag_name }}
jobs:
build-appimage:
runs-on: ubuntu-22.04
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || github.sha }}
- name: Validate package versions
id: version
shell: bash
run: python3 scripts/release.py versions --tag "$RELEASE_TAG"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Install dependencies
run: |
sudo apt-get update -q
sudo apt-get install -y \
libgl1-mesa-dev libx11-dev libxcursor-dev libxi-dev libxrandr-dev \
libxkbcommon-dev libwayland-dev libfontconfig1-dev libfreetype6-dev \
librsvg2-bin fuse libfuse2
- name: Build
env:
OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }}
run: cargo build --locked --release
- name: Prepare AppDir
run: |
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/share/applications
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
mkdir -p AppDir/usr/share/metainfo
cp target/release/OpenCADStudio AppDir/usr/bin/OpenCADStudio
cp packaging/OpenCADStudio.desktop AppDir/usr/share/applications/io.github.HakanSeven12.OpenCadStudio.desktop
cp packaging/io.github.HakanSeven12.OpenCadStudio.metainfo.xml AppDir/usr/share/metainfo/
rsvg-convert -w 256 -h 256 assets/logo.svg \
-o AppDir/usr/share/icons/hicolor/256x256/apps/io.github.HakanSeven12.OpenCadStudio.png
- name: Pack shared Linux payload
run: tar -C AppDir -czf linux-payload.tar.gz usr
- name: Upload shared Linux payload
uses: actions/upload-artifact@v4
with:
name: linux-payload
path: linux-payload.tar.gz
if-no-files-found: error
retention-days: 7
- name: Download linuxdeploy
run: |
wget -q https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
- name: Build AppImage
env:
APPIMAGE_EXTRACT_AND_RUN: 1
run: |
./linuxdeploy-x86_64.AppImage \
--appdir AppDir \
--desktop-file AppDir/usr/share/applications/io.github.HakanSeven12.OpenCadStudio.desktop \
--icon-file AppDir/usr/share/icons/hicolor/256x256/apps/io.github.HakanSeven12.OpenCadStudio.png \
--output appimage
mv Open*CAD*Studio*.AppImage OpenCADStudio.AppImage
- name: Upload AppImage to release
if: env.RELEASE_TAG != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ASSET=OpenCADStudio-${{ env.RELEASE_TAG }}-linux-x86_64.AppImage
mv OpenCADStudio.AppImage "$ASSET"
gh release upload ${{ env.RELEASE_TAG }} "$ASSET" --clobber --repo ${{ github.repository }}
build-snap:
needs: build-appimage
runs-on: ubuntu-22.04
timeout-minutes: 75
concurrency:
group: snap
cancel-in-progress: false
permissions:
actions: read
contents: write
env:
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || github.sha }}
- name: Validate package versions
id: version
shell: bash
run: python3 scripts/release.py versions --tag "$RELEASE_TAG"
- name: Download shared Linux payload
uses: actions/download-artifact@v4
with:
name: linux-payload
path: .
- name: Set package version
id: package
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
sed -i "s/^version: .*/version: '$VERSION'/" snap/snapcraft.yaml
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build snap
id: snapcraft
uses: snapcore/action-build@v1
- name: Normalize snap package name
id: snap_package
env:
SNAP: ${{ steps.snapcraft.outputs.snap }}
VERSION: ${{ steps.package.outputs.version }}
run: |
PACKAGE="OpenCADStudio-v${VERSION}-linux-x86_64.snap"
mv "$SNAP" "$PACKAGE"
echo "snap=$PACKAGE" >> "$GITHUB_OUTPUT"
- name: Upload snap to release
if: env.RELEASE_TAG != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
SNAP: ${{ steps.snap_package.outputs.snap }}
run: gh release upload "${{ env.RELEASE_TAG }}" "$SNAP" --clobber
- name: Upload snap as workflow artifact
if: env.RELEASE_TAG == ''
uses: actions/upload-artifact@v4
with:
name: OpenCADStudio-v${{ steps.package.outputs.version }}-linux-x86_64-snap
path: ${{ steps.snap_package.outputs.snap }}
if-no-files-found: error
- name: Publish snap to edge
if: env.RELEASE_TAG != '' && env.SNAPCRAFT_STORE_CREDENTIALS != ''
uses: snapcore/action-publish@v1
with:
snap: ${{ steps.snap_package.outputs.snap }}
release: edge
build-windows:
if: github.event_name == 'release' || inputs.tag != '' || inputs.target == 'all'
runs-on: windows-latest
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || github.sha }}
- name: Validate package versions
id: version
shell: bash
run: python3 scripts/release.py versions --tag "$RELEASE_TAG"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
- name: Convert SVG icon to ICO
shell: pwsh
run: |
magick assets/logo.svg `
-define icon:auto-resize=16,24,32,48,64,128,256 `
packaging/windows/AppIcon.ico
- name: Convert DWG/DXF file icons to ICO
shell: pwsh
run: |
# DWG/DXF Explorer file icons, from the single-source mimetype SVGs.
magick assets/mimetypes/image-vnd.dwg.svg `
-define icon:auto-resize=16,24,32,48,64,128,256 `
packaging/windows/dwg.ico
magick assets/mimetypes/image-vnd.dxf.svg `
-define icon:auto-resize=16,24,32,48,64,128,256 `
packaging/windows/dxf.ico
- name: Build
env:
OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }}
run: |
cargo build --locked --release
cargo build --locked --release -p dwg-thumbnailer-win
- name: Sign executable (Azure Trusted Signing)
uses: azure/artifact-signing-action@v2
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: ${{ secrets.AZURE_ENDPOINT }}
signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }}
files: ${{ github.workspace }}\target\release\OpenCADStudio.exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Build MSI installer
shell: pwsh
run: |
$version = "${{ steps.version.outputs.msi }}"
$candle = Join-Path $env:WIX "bin\candle.exe"
$light = Join-Path $env:WIX "bin\light.exe"
$exePath = (Resolve-Path target\release\OpenCADStudio.exe).Path
$iconPath = (Resolve-Path packaging\windows\AppIcon.ico).Path
# GPL-3 license RTF shown on the installer's EULA page.
$licensePath = (Resolve-Path packaging\windows\License.rtf).Path
# main.wxs holds the package/feature; ui.wxs holds the installer UI
# and finish-screen launch checkbox. Both compile together.
$mainWxs = (Resolve-Path packaging\windows\main.wxs).Path
$uiWxs = (Resolve-Path packaging\windows\ui.wxs).Path
# candle writes one .wixobj per source into this directory (the
# trailing slash tells it -out is a folder, not a single file).
$objDir = Join-Path $PWD "packaging\windows\"
$msiPath = Join-Path $PWD "OpenCADStudio.msi"
& $candle -arch x64 `
"-dVersion=$version" `
"-dSource=$exePath" `
"-dIcon=$iconPath" `
"-dLicense=$licensePath" `
$mainWxs $uiWxs `
-out $objDir
if ($LASTEXITCODE -ne 0) { throw "candle failed" }
$mainObj = Join-Path $objDir "main.wixobj"
$uiObj = Join-Path $objDir "ui.wixobj"
& $light $mainObj $uiObj -ext WixUIExtension -cultures:en-us -loc packaging/windows/strings.en-US.wxl -out $msiPath
if ($LASTEXITCODE -ne 0) { throw "light failed" }
- name: Sign MSI installer (Azure Trusted Signing)
# The installer itself must be signed separately — light produces a
# fresh, unsigned .msi even though the .exe inside is already signed.
uses: azure/artifact-signing-action@v2
with:
azure-tenant-id: ${{ secrets.AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.AZURE_CLIENT_SECRET }}
endpoint: ${{ secrets.AZURE_ENDPOINT }}
signing-account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
certificate-profile-name: ${{ secrets.AZURE_CERTIFICATE_PROFILE_NAME }}
files: ${{ github.workspace }}\OpenCADStudio.msi
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Upload artifacts to release
if: env.RELEASE_TAG != ''
shell: pwsh
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
$tag = "${{ env.RELEASE_TAG }}"
$exe = "OpenCADStudio-$tag-windows-x86_64-portable.exe"
$msi = "OpenCADStudio-$tag-windows-x86_64-installer.msi"
Move-Item target\release\OpenCADStudio.exe $exe
Move-Item OpenCADStudio.msi $msi
gh release upload $tag `
$exe $msi `
--clobber --repo ${{ github.repository }}
build-macos:
if: github.event_name == 'release' || inputs.tag != '' || inputs.target == 'all'
runs-on: macos-14 # Apple Silicon (arm64) only
permissions:
contents: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || github.sha }}
- name: Validate package versions
id: version
shell: bash
run: python3 scripts/release.py versions --tag "$RELEASE_TAG"
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin
- name: Install librsvg (for rsvg-convert)
env:
HOMEBREW_NO_AUTO_UPDATE: "1"
run: brew install librsvg
- name: Build
env:
OCS_PATREON_TOKEN: ${{ secrets.OCS_PATREON_TOKEN }}
run: |
cargo build --locked --release --target aarch64-apple-darwin
cargo build --locked --release --target aarch64-apple-darwin -p dwg-thumbnailer
# The launcher shim is the .app's actual CFBundleExecutable (#1039)
# — see src/bin/ocs_launcher.rs for why.
cargo build --locked --release --target aarch64-apple-darwin --bin ocs_launcher
- name: Build .icns from SVG
run: |
mkdir -p OpenCADStudio.iconset
# Render all the sizes Apple expects in an .icns.
for SIZE in 16 32 64 128 256 512 1024; do
rsvg-convert -w $SIZE -h $SIZE assets/logo.svg -o OpenCADStudio.iconset/icon_${SIZE}x${SIZE}.png
done
# @2x retina variants (half-size base name)
for BASE in 16 32 128 256 512; do
DOUBLE=$((BASE * 2))
cp OpenCADStudio.iconset/icon_${DOUBLE}x${DOUBLE}.png OpenCADStudio.iconset/icon_${BASE}x${BASE}@2x.png
done
iconutil -c icns OpenCADStudio.iconset -o AppIcon.icns
- name: Build DWG/DXF document .icns from the same SVG sources
run: |
# Finder document icons (Info.plist CFBundleTypeIconFile = DWG/DXF),
# generated from the single-source mimetype SVGs — never hand-drawn.
for T in dwg dxf; do
mkdir -p "$T.iconset"
for SIZE in 16 32 64 128 256 512 1024; do
rsvg-convert -w $SIZE -h $SIZE "assets/mimetypes/image-vnd.$T.svg" \
-o "$T.iconset/icon_${SIZE}x${SIZE}.png"
done
for BASE in 16 32 128 256 512; do
cp "$T.iconset/icon_$((BASE*2))x$((BASE*2)).png" \
"$T.iconset/icon_${BASE}x${BASE}@2x.png"
done
iconutil -c icns "$T.iconset" -o "$(echo "$T" | tr a-z A-Z).icns"
done
- name: Build DWG QuickLook thumbnail extension (.appex)
run: |
set -e
VERSION="${{ steps.version.outputs.cargo }}"
EXT=DWGThumbnail.appex
rm -rf "$EXT"; mkdir -p "$EXT/Contents/MacOS"
swiftc \
-sdk "$(xcrun --sdk macosx --show-sdk-path)" \
-target arm64-apple-macos11 \
-O -parse-as-library -application-extension \
-module-name DWGThumbnail \
-import-objc-header crates/dwg-thumbnailer/macos/dwg_thumbnailer.h \
crates/dwg-thumbnailer/macos/ThumbnailProvider.swift \
-L target/aarch64-apple-darwin/release -ldwg_thumbnailer \
-framework QuickLookThumbnailing -framework CoreGraphics \
-framework ImageIO -framework Foundation -framework Security \
-framework SystemConfiguration -liconv \
-Xlinker -e -Xlinker _NSExtensionMain \
-o "$EXT/Contents/MacOS/DWGThumbnail"
sed "s/__VERSION__/$VERSION/g" crates/dwg-thumbnailer/macos/Info.plist > "$EXT/Contents/Info.plist"
- name: Assemble .app bundle
run: |
APP=OpenCADStudio.app
rm -rf "$APP"
mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/PlugIns"
# The launcher is CFBundleExecutable (Finder always launches it,
# for both document opens and plain launches); it execs into the
# real GUI binary, renamed alongside it, over argv. See
# src/bin/ocs_launcher.rs.
cp target/aarch64-apple-darwin/release/ocs_launcher "$APP/Contents/MacOS/OpenCADStudio"
cp target/aarch64-apple-darwin/release/OpenCADStudio "$APP/Contents/MacOS/OpenCADStudio-App"
chmod +x "$APP/Contents/MacOS/OpenCADStudio" "$APP/Contents/MacOS/OpenCADStudio-App"
cp AppIcon.icns "$APP/Contents/Resources/AppIcon.icns"
cp DWG.icns DXF.icns "$APP/Contents/Resources/"
# QuickLook thumbnail extension.
cp -R DWGThumbnail.appex "$APP/Contents/PlugIns/"
# Substitute version into Info.plist.
VERSION="${{ steps.version.outputs.cargo }}"
sed "s/__VERSION__/$VERSION/g" packaging/Info.plist > "$APP/Contents/Info.plist"
- name: Ad-hoc code-sign the bundle
run: |
codesign --force --deep --sign - --timestamp=none OpenCADStudio.app
codesign --verify --strict --verbose=2 OpenCADStudio.app
- name: Create .dmg
run: |
set +e
for attempt in 1 2 3 4 5; do
hdiutil detach "/Volumes/Open CAD Studio" >/dev/null 2>&1 || true
rm -f OpenCADStudio.dmg
hdiutil create \
-volname "Open CAD Studio" \
-srcfolder OpenCADStudio.app \
-ov -format UDZO \
OpenCADStudio.dmg && break
echo "hdiutil create failed (attempt $attempt) — retrying in 5s"
sleep 5
done
set -e
test -f OpenCADStudio.dmg
- name: Upload .dmg to release
if: env.RELEASE_TAG != ''
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ASSET=OpenCADStudio-${{ env.RELEASE_TAG }}-macos-arm64.dmg
mv OpenCADStudio.dmg "$ASSET"
gh release upload ${{ env.RELEASE_TAG }} "$ASSET" --clobber --repo ${{ github.repository }}
- name: Emit Homebrew cask sha256
if: env.RELEASE_TAG != ''
run: |
VERSION="${{ env.RELEASE_TAG }}"
VERSION="${VERSION#v}"
SHA=$(shasum -a 256 OpenCADStudio-${{ env.RELEASE_TAG }}-macos-arm64.dmg | awk '{print $1}')
{
echo "## Homebrew cask bump"
echo "Update \`packaging/homebrew/open-cad-studio.rb\`:"
echo '```ruby'
echo " version \"$VERSION\""
echo " sha256 \"$SHA\""
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
verify:
needs: [build-appimage, build-snap, build-windows, build-macos]
if: github.event_name == 'release' || inputs.tag != ''
runs-on: ubuntu-22.04
permissions:
contents: read
steps:
- uses: actions/checkout@v4
with:
ref: ${{ inputs.commit || github.sha }}
- name: Verify native release assets and notes
env:
GH_TOKEN: ${{ github.token }}
run: python3 scripts/release.py verify-native --tag "$RELEASE_TAG"