diff --git a/.github/workflows/build-mac-dmg.yml b/.github/workflows/build-mac-dmg.yml index 665cf5b6..fd1d131d 100644 --- a/.github/workflows/build-mac-dmg.yml +++ b/.github/workflows/build-mac-dmg.yml @@ -55,18 +55,19 @@ 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 @@ -74,10 +75,13 @@ jobs: 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 }}" @@ -86,7 +90,7 @@ 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" @@ -94,8 +98,6 @@ jobs: 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 diff --git a/README.md b/README.md index ff8da4f2..a1e84a71 100644 --- a/README.md +++ b/README.md @@ -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: diff --git a/backend/services/image_generation_pipeline/zit_image_generation_pipeline.py b/backend/services/image_generation_pipeline/zit_image_generation_pipeline.py index 7e5f0bb6..db684e8d 100644 --- a/backend/services/image_generation_pipeline/zit_image_generation_pipeline.py +++ b/backend/services/image_generation_pipeline/zit_image_generation_pipeline.py @@ -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 @@ -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] diff --git a/build/macos/codesign.sh b/build/macos/codesign.sh index 735877a3..1c9b598d 100644 --- a/build/macos/codesign.sh +++ b/build/macos/codesign.sh @@ -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 \ diff --git a/docs/INSTALLER.md b/docs/INSTALLER.md index f6601e39..a91886d5 100644 --- a/docs/INSTALLER.md +++ b/docs/INSTALLER.md @@ -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: diff --git a/electron-builder.yml b/electron-builder.yml index db211187..53d121bb 100644 --- a/electron-builder.yml +++ b/electron-builder.yml @@ -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: diff --git a/electron/window.ts b/electron/window.ts index 19836520..113b1806 100644 --- a/electron/window.ts +++ b/electron/window.ts @@ -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