Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 14 additions & 12 deletions .github/workflows/build-mac-dmg.yml
Original file line number Diff line number Diff line change
Expand Up @@ -55,29 +55,33 @@ jobs:
run: pnpm run build:frontend

- name: Build macOS app bundle (unsigned)
# Build the unpacked .app only — electron-builder signing is disabled entirely.
# Ad-hoc signing is applied in the next step via build/macos/codesign.sh.
# Build the unpacked .app with python-embed embedded via extraResources.
# Signing is disabled here; ad-hoc signing is applied in the next step
# via build/macos/codesign.sh (without --options runtime, so no hardened
# runtime and no library validation that would block Python .so loading).
id: build-app
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: |
pnpm exec electron-builder --mac --dir \
--config.mac.notarize=false \
--config.publish.owner=${{ github.repository_owner }} \
--config.publish.repo=${{ github.event.repository.name }}
--publish never

APP_PATH=$(find release/mac-* -name "*.app" -maxdepth 1 | head -1)
APP_PATH=$(find release/mac-* -maxdepth 1 -name "*.app" | head -1)
if [ -z "$APP_PATH" ]; then
echo "Error: No .app bundle found in release/"
exit 1
fi
echo "app_path=$APP_PATH" >> "$GITHUB_OUTPUT"

- name: Ad-hoc code sign the app bundle
# Signs all Mach-O binaries (Python natives, Electron frameworks, executables)
# and the app bundle using identity "-" (ad-hoc / self-signed).
# No Apple ID, no certificate, no notarization required.
# To use a real Developer ID certificate, set the MACOS_SIGNING_IDENTITY secret.
# Signs the entire bundle with ad-hoc identity ("-") WITHOUT hardened
# runtime so macOS does not enforce library validation. Python can then
# load its pip-installed .so extensions. Because the whole bundle is
# consistently signed, "Open Anyway" in Privacy & Security approves
# the entire bundle including the embedded python3 binary — users do
# not need to run any shell commands (no xattr required).
# To use a real Developer ID certificate, set MACOS_SIGNING_IDENTITY.
run: |
chmod +x build/macos/codesign.sh
./build/macos/codesign.sh "${{ steps.build-app.outputs.app_path }}"
Expand All @@ -86,16 +90,14 @@ jobs:

- name: Create DMG from signed app
# Packages the signed .app into a DMG using the electron-builder.yml layout.
# --prepackaged skips the build phase; electron-builder only creates the DMG.
# --prepackaged skips the app-build phase; electron-builder only creates the DMG.
# CSC_IDENTITY_AUTO_DISCOVERY=false prevents any re-signing attempt.
env:
CSC_IDENTITY_AUTO_DISCOVERY: "false"
run: |
pnpm exec electron-builder --mac dmg \
--prepackaged "${{ steps.build-app.outputs.app_path }}" \
--config.mac.notarize=false \
--config.publish.owner=${{ github.repository_owner }} \
--config.publish.repo=${{ github.event.repository.name }} \
--publish never

- name: Upload DMG to GitHub Release
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ In API-only mode, available resolutions/durations may be limited to what the API
2. Install and launch **LTX Desktop**
3. Complete first-run setup

> **macOS users:** The DMG is ad-hoc signed (no Apple Developer account). On first launch macOS Gatekeeper will show "cannot be opened because the developer cannot be verified." Go to **System Settings → Privacy & Security** and click **Open Anyway** — no shell commands required. See [INSTALLER.md](docs/INSTALLER.md) for step-by-step instructions.

## First run & data locations

LTX Desktop stores app data (settings, models, logs) in:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
from typing import Any, cast

import torch
from diffusers.pipelines.auto_pipeline import ZImagePipeline # type: ignore[reportUnknownVariableType]
from PIL.Image import Image as PILImage

from services.services_utils import ImagePipelineOutputLike, PILImageType, get_device_type
Expand All @@ -30,6 +29,7 @@ def create(
return ZitImageGenerationPipeline(model_path=model_path, device=device)

def __init__(self, model_path: str, device: str | None = None) -> None:
from diffusers.pipelines.auto_pipeline import ZImagePipeline # type: ignore[reportUnknownVariableType]
self._device: str | None = None
self._cpu_offload_active = False
self.pipeline = ZImagePipeline.from_pretrained( # type: ignore[reportUnknownMemberType]
Expand Down
11 changes: 9 additions & 2 deletions build/macos/codesign.sh
Original file line number Diff line number Diff line change
Expand Up @@ -42,20 +42,27 @@ if [ ! -f "$ENTITLEMENTS_PATH" ]; then
fi

# ── Signing helper ────────────────────────────────────────────────────────────
# Ad-hoc signing ("-") does not support --timestamp (requires a CA).
# Ad-hoc signing ("-") does not support --timestamp (requires a CA) and must
# NOT use --options runtime. Hardened Runtime ("runtime" option) enables library
# validation, which prevents Python from loading unsigned pip-installed .so
# extensions. Without hardened runtime the signed bundle works correctly after
# the user clicks "Open Anyway" in Privacy & Security — no shell commands needed.
sign_target() {
local target="$1"
echo " Signing: $(basename "$target")"

if [ "$SIGNING_IDENTITY" = "-" ]; then
# Ad-hoc: no hardened runtime so macOS does not enforce library validation.
xcrun codesign \
--sign "$SIGNING_IDENTITY" \
--force \
--options runtime \
--entitlements "$ENTITLEMENTS_PATH" \
--deep \
"$target"
else
# Real Developer ID certificate: hardened runtime + timestamp required for
# notarization. The disable-library-validation entitlement in the plist
# allows Python to load unsigned pip extensions even under hardened runtime.
xcrun codesign \
--sign "$SIGNING_IDENTITY" \
--force \
Expand Down
15 changes: 10 additions & 5 deletions docs/INSTALLER.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,11 +128,16 @@ Ensure you have internet access. The script downloads Python automatically.
### Build fails with CUDA errors
The build doesn't require a GPU. CUDA packages are pre-built binaries.

### macOS: "App is damaged" or Gatekeeper warning
On unsigned builds, macOS Gatekeeper may block the app. Right-click the app and select "Open", or run:
```bash
xattr -dr com.apple.quarantine /Applications/LTX\ Desktop.app
```
### macOS: Gatekeeper "unidentified developer" warning
The distributed DMG is ad-hoc signed (no Apple Developer account), so Gatekeeper will
block the first launch. No shell commands are required:

1. Open the DMG and drag **LTX Desktop** to `/Applications`.
2. Double-click the app — macOS will show "cannot be opened because the developer cannot be verified." Click **Cancel** (or **OK**) to dismiss.
3. Open **System Settings → Privacy & Security**. Scroll down to find the blocked app and click **Open Anyway**.
4. Confirm by clicking **Open** in the follow-up dialog.

The app will launch normally from this point on.

### Installer is too large
Expected sizes:
Expand Down
2 changes: 2 additions & 0 deletions electron-builder.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ mac:
- "/python/.*\\.(py|pyc|pyi|pxd|pyx|h|hpp|cpp|c|cuh|cu|tcl|txt|json|yaml|yml|toml|cfg|md|rst|html|css|enc|msg|cmake|gif|png|jpg|svg|xml|jinja|typed|al)$"
- "/python/.*\\.dist-info/"
extraResources:
- from: resources/icon.png
to: icon.png
- from: python-embed
to: python
filter:
Expand Down
8 changes: 6 additions & 2 deletions electron/window.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,13 @@ export function createWindow(): BrowserWindow {
? path.join(getCurrentDir(), 'dist-electron', 'preload.js')
: path.join(app.getAppPath(), 'dist-electron', 'preload.js')

// App icon — use .ico on Windows, .png elsewhere
// App icon — use .ico on Windows, .png elsewhere.
// In production the icon is placed in the app's resources directory via
// extraResources; in dev it lives in the project's resources/ folder.
const iconExt = process.platform === 'win32' ? 'icon.ico' : 'icon.png'
const iconPath = path.join(getCurrentDir(), 'resources', iconExt)
const iconPath = isDev
? path.join(getCurrentDir(), 'resources', iconExt)
: path.join(process.resourcesPath, iconExt)
logger.info(`[icon] Loading app icon from: ${iconPath} | exists: ${fs.existsSync(iconPath)}`)
const appIcon = fs.existsSync(iconPath) ? nativeImage.createFromPath(iconPath) : undefined

Expand Down