diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4527fba5879..b8e82cf0d11 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,6 +58,9 @@ jobs: - 'scripts/build-linux-packages.mjs' - 'scripts/build-macos-packages*' - 'scripts/bundle-gpui*' + - 'scripts/build-macos-bundles*' + - 'scripts/build-macos-icon*' + - 'apps/macos-bundles/**' - 'scripts/finalize-linux-*' - 'scripts/finalize-macos-*' - 'scripts/linux-bundle-config*' @@ -364,7 +367,11 @@ jobs: uses: actions/checkout@v4 - name: Test desktop build preparation - run: node --test scripts/prepare-gpui-dependency.test.mjs scripts/build-macos-packages.test.mjs scripts/bundle-gpui.test.mjs scripts/finalize-macos-packages.test.mjs + run: node --test scripts/prepare-gpui-dependency.test.mjs scripts/build-macos-packages.test.mjs scripts/bundle-gpui.test.mjs scripts/build-macos-bundles.test.mjs scripts/build-macos-icon.test.mjs scripts/finalize-macos-packages.test.mjs + + - name: Compile macOS Quick Look bundles + if: ${{ runner.os == 'macOS' }} + run: swift build -c release --package-path apps/macos-bundles - name: Test Linux packaging if: ${{ runner.os == 'Linux' }} diff --git a/.github/workflows/desktop-release.yml b/.github/workflows/desktop-release.yml index f4c6d54a14a..dc354211530 100644 --- a/.github/workflows/desktop-release.yml +++ b/.github/workflows/desktop-release.yml @@ -231,7 +231,7 @@ jobs: run: node --test scripts/prepare-gpui-dependency.test.mjs - name: Test desktop packaging scripts - run: node --test scripts/build-macos-packages.test.mjs scripts/bundle-gpui.test.mjs scripts/finalize-macos-packages.test.mjs + run: node --test scripts/build-macos-packages.test.mjs scripts/bundle-gpui.test.mjs scripts/build-macos-bundles.test.mjs scripts/build-macos-icon.test.mjs scripts/finalize-macos-packages.test.mjs - name: Install Linux desktop dependencies if: ${{ runner.os == 'Linux' }} diff --git a/apps/desktop-gpui/Info.plist b/apps/desktop-gpui/Info.plist index dd740dcf952..106cfeeaaea 100644 --- a/apps/desktop-gpui/Info.plist +++ b/apps/desktop-gpui/Info.plist @@ -29,5 +29,31 @@ Cap captures your screen as part of recording your Caps NSCameraUseContinuityCameraDeviceType + UTExportedTypeDeclarations + + + UTTypeIdentifier + so.cap.desktop.project + UTTypeDescription + Cap Project + UTTypeConformsTo + + com.apple.package + + UTTypeTagSpecification + + public.filename-extension + + cap + + + + + CFBundleIconFile + icon.icns + CFBundleIconName + cap-solarium + NSAccentColorName + AccentColor diff --git a/apps/desktop-gpui/assets/app-icon.png b/apps/desktop-gpui/assets/app-icon.png new file mode 100644 index 00000000000..049b1563919 Binary files /dev/null and b/apps/desktop-gpui/assets/app-icon.png differ diff --git a/apps/desktop-gpui/assets/cap-debug-icon-legacy.png b/apps/desktop-gpui/assets/cap-debug-icon-legacy.png new file mode 100644 index 00000000000..1b55ba0d7e3 Binary files /dev/null and b/apps/desktop-gpui/assets/cap-debug-icon-legacy.png differ diff --git a/apps/desktop-gpui/assets/cap-debug-icon.png b/apps/desktop-gpui/assets/cap-debug-icon.png new file mode 100644 index 00000000000..91e1e917b90 Binary files /dev/null and b/apps/desktop-gpui/assets/cap-debug-icon.png differ diff --git a/apps/desktop-gpui/assets/dock-icon.png b/apps/desktop-gpui/assets/dock-icon.png deleted file mode 100644 index b7657009ef3..00000000000 Binary files a/apps/desktop-gpui/assets/dock-icon.png and /dev/null differ diff --git a/apps/desktop-gpui/build.rs b/apps/desktop-gpui/build.rs index 151c6bf353a..1a417c62433 100644 --- a/apps/desktop-gpui/build.rs +++ b/apps/desktop-gpui/build.rs @@ -22,7 +22,7 @@ fn main() { println!("cargo:rustc-link-arg-bin=cap-gpui=/STACK:16777216"); } - let icon = "../desktop/src-tauri/icons/icon.ico"; + let icon = "icons/icon.ico"; println!("cargo:rerun-if-changed={icon}"); tauri_winres::WindowsResource::new() .set_icon_with_id(icon, "1") diff --git a/apps/desktop-gpui/icons/128x128.png b/apps/desktop-gpui/icons/128x128.png new file mode 100644 index 00000000000..d5d3ce02baf Binary files /dev/null and b/apps/desktop-gpui/icons/128x128.png differ diff --git a/apps/desktop-gpui/icons/128x128@2x.png b/apps/desktop-gpui/icons/128x128@2x.png new file mode 100644 index 00000000000..10eaf0ba13f Binary files /dev/null and b/apps/desktop-gpui/icons/128x128@2x.png differ diff --git a/apps/desktop-gpui/icons/32x32.png b/apps/desktop-gpui/icons/32x32.png new file mode 100644 index 00000000000..8e4b70091b5 Binary files /dev/null and b/apps/desktop-gpui/icons/32x32.png differ diff --git a/apps/desktop-gpui/icons/Cap.xcassets/AccentColor.colorset/Contents.json b/apps/desktop-gpui/icons/Cap.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 00000000000..f8e6acd4869 --- /dev/null +++ b/apps/desktop-gpui/icons/Cap.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors": [ + { + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0xFF", + "green": "0x7A", + "red": "0x00" + } + }, + "idiom": "universal" + }, + { + "appearances": [ + { + "appearance": "luminosity", + "value": "dark" + } + ], + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0xFF", + "green": "0x84", + "red": "0x0A" + } + }, + "idiom": "universal" + } + ], + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/apps/desktop-gpui/icons/Cap.xcassets/Contents.json b/apps/desktop-gpui/icons/Cap.xcassets/Contents.json new file mode 100644 index 00000000000..d458f1c5928 --- /dev/null +++ b/apps/desktop-gpui/icons/Cap.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info": { + "author": "xcode", + "version": 1 + } +} diff --git a/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRing.svg b/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRing.svg new file mode 100644 index 00000000000..fd8f59ced87 --- /dev/null +++ b/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRing.svg @@ -0,0 +1,4 @@ + + + + diff --git a/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRingsSeaGlass-Shadow.png b/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRingsSeaGlass-Shadow.png new file mode 100644 index 00000000000..7c950add1dc Binary files /dev/null and b/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRingsSeaGlass-Shadow.png differ diff --git a/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRingsSeaGlass2.png b/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRingsSeaGlass2.png new file mode 100644 index 00000000000..72c4c1daadf Binary files /dev/null and b/apps/desktop-gpui/icons/cap-solarium.icon/Assets/FullRingsSeaGlass2.png differ diff --git a/apps/desktop-gpui/icons/cap-solarium.icon/icon.json b/apps/desktop-gpui/icons/cap-solarium.icon/icon.json new file mode 100644 index 00000000000..c470c9d4437 --- /dev/null +++ b/apps/desktop-gpui/icons/cap-solarium.icon/icon.json @@ -0,0 +1,251 @@ +{ + "features" : [ + "refractivity", + "specular-location" + ], + "fill-specializations" : [ + { + "value" : "automatic" + }, + { + "appearance" : "dark", + "value" : "automatic" + } + ], + "groups" : [ + { + "blend-mode-specializations" : [ + { + "appearance" : "dark", + "value" : "normal" + } + ], + "blur-material" : null, + "hidden-specializations" : [ + { + "value" : false + }, + { + "idiom" : "square", + "value" : false + } + ], + "layers" : [ + { + "blend-mode" : "normal", + "fill-specializations" : [ + { + "value" : "automatic" + }, + { + "appearance" : "dark", + "value" : "none" + } + ], + "glass" : true, + "hidden" : true, + "image-name" : "FullRing.svg", + "name" : "FullRing", + "position" : { + "scale" : 9.35, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ], + "lighting" : "individual", + "position" : { + "scale" : 1, + "translation-in-points" : [ + 0, + 0 + ] + }, + "refractivity" : { + "depth" : 0.6013671875, + "enabled" : true, + "strength" : 0.1844921875 + }, + "shadow" : { + "kind" : "layer-color", + "opacity" : 0.6 + }, + "specular" : true, + "translucency" : { + "enabled" : true, + "value" : 0.4 + } + }, + { + "blur-material" : null, + "layers" : [ + { + "image-name" : "FullRingsSeaGlass2.png", + "name" : "FullRingsSeaGlass2", + "position-specializations" : [ + { + "idiom" : "square", + "value" : { + "scale" : 0.65, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ] + } + ], + "lighting" : "individual", + "refractivity-specializations" : [ + { + "value" : { + "depth" : 0.5054296875, + "enabled" : true, + "strength" : 0.34328125 + } + }, + { + "appearance" : "dark", + "value" : { + "depth" : 0.511875, + "enabled" : true, + "strength" : 0.4355078125 + } + } + ], + "shadow-specializations" : [ + { + "value" : { + "kind" : "layer-color", + "opacity" : 0.5 + } + }, + { + "appearance" : "dark", + "value" : { + "kind" : "layer-color", + "opacity" : 0.5 + } + } + ], + "specular-specializations" : [ + { + "value" : "inside" + }, + { + "appearance" : "dark", + "value" : "inside" + } + ], + "translucency" : { + "enabled" : true, + "value" : 0.25 + } + }, + { + "blur-material" : null, + "layers" : [ + { + "blend-mode-specializations" : [ + { + "appearance" : "dark", + "value" : "normal" + } + ], + "fill-specializations" : [ + { + "appearance" : "dark", + "value" : "none" + } + ], + "glass" : false, + "hidden-specializations" : [ + { + "idiom" : "square", + "value" : false + } + ], + "image-name" : "FullRingsSeaGlass-Shadow.png", + "name" : "FullRingsSeaGlass-Shadow", + "opacity-specializations" : [ + { + "appearance" : "dark", + "value" : 0 + }, + { + "appearance" : "tinted", + "value" : 0 + } + ], + "position-specializations" : [ + { + "idiom" : "square", + "value" : { + "scale" : 0.65, + "translation-in-points" : [ + 0, + 0 + ] + } + } + ] + } + ], + "lighting" : "individual", + "refractivity-specializations" : [ + { + "value" : { + "depth" : 0.5054296875, + "enabled" : true, + "strength" : 0.34328125 + } + }, + { + "appearance" : "dark", + "value" : { + "depth" : 0.511875, + "enabled" : true, + "strength" : 0.4355078125 + } + } + ], + "shadow-specializations" : [ + { + "value" : { + "kind" : "layer-color", + "opacity" : 0.5 + } + }, + { + "appearance" : "dark", + "value" : { + "kind" : "layer-color", + "opacity" : 0.5 + } + } + ], + "specular-specializations" : [ + { + "value" : "outside" + }, + { + "appearance" : "dark", + "value" : "inside" + } + ], + "translucency" : { + "enabled" : true, + "value" : 0.3 + } + } + ], + "supported-platforms" : { + "circles" : [ + "watchOS" + ], + "squares" : "shared" + } +} \ No newline at end of file diff --git a/apps/desktop-gpui/icons/icon.icns b/apps/desktop-gpui/icons/icon.icns new file mode 100644 index 00000000000..c82c2f7ab87 Binary files /dev/null and b/apps/desktop-gpui/icons/icon.icns differ diff --git a/apps/desktop-gpui/icons/icon.ico b/apps/desktop-gpui/icons/icon.ico new file mode 100644 index 00000000000..b55f512d5b8 Binary files /dev/null and b/apps/desktop-gpui/icons/icon.ico differ diff --git a/apps/desktop-gpui/icons/icon.png b/apps/desktop-gpui/icons/icon.png new file mode 100644 index 00000000000..572a1b8d73b Binary files /dev/null and b/apps/desktop-gpui/icons/icon.png differ diff --git a/apps/desktop-gpui/src/main.rs b/apps/desktop-gpui/src/main.rs index b0a3ef392f3..b3a0342e813 100644 --- a/apps/desktop-gpui/src/main.rs +++ b/apps/desktop-gpui/src/main.rs @@ -378,9 +378,12 @@ fn main() { app.run(move |cx: &mut App| { gpui_tokio::init_from_handle(cx, runtime_handle); gpui_tokio::Tokio::spawn(cx, cap_utils::operation_diagnostics::run_checkpoints()).detach(); - // The dock icon: an unbundled dev binary shows the generic terminal - // document without it. The bytes are the shipping app's icon.png. - platform::set_dock_icon(include_bytes!("../assets/dock-icon.png")); + #[cfg(all(debug_assertions, target_os = "macos"))] + platform::set_dock_icon(if objc2::available!(macos = 26.0) { + include_bytes!("../assets/cap-debug-icon.png") + } else { + include_bytes!("../assets/cap-debug-icon-legacy.png") + }); if let Err(error) = Assets.load_fonts(cx) { tracing::error!("failed to load embedded fonts: {error:#}"); diff --git a/apps/desktop-gpui/src/permission_guide.rs b/apps/desktop-gpui/src/permission_guide.rs index 5bd32bdcb28..9a9fc15703a 100644 --- a/apps/desktop-gpui/src/permission_guide.rs +++ b/apps/desktop-gpui/src/permission_guide.rs @@ -38,7 +38,7 @@ const GIVE_UP_AFTER_CLOSE: Duration = Duration::from_secs(8); const LIFETIME: Duration = Duration::from_secs(5 * 60); const SIDEBAR_W: f64 = 230.; -const APP_ICON: &[u8] = include_bytes!("../assets/dock-icon.png"); +const APP_ICON: &[u8] = include_bytes!("../assets/app-icon.png"); /// The `.app` to drag: the bundle this binary runs from. A bare dev binary /// has none -- TCC attributes it to the launching terminal anyway -- so the diff --git a/apps/desktop-gpui/src/platform.rs b/apps/desktop-gpui/src/platform.rs index 3866f052b66..4ab3d19265c 100644 --- a/apps/desktop-gpui/src/platform.rs +++ b/apps/desktop-gpui/src/platform.rs @@ -2102,66 +2102,32 @@ mod mac { /// returned, the window stayed). Its close-button simulation is the only /// part the contract loses, so the two real steps are spelled out /// directly. Same retained-handle discipline as [`hide_native`]. - /// The dock icon. An unbundled dev binary carries no Info.plist icon, so - /// one is set at runtime with `setApplicationIconImage:`. - /// - /// The shape matters more than the bytes on macOS 26: the system - /// re-renders every *bundle* icon onto its standard squircle plate (the - /// legacy-icon treatment), which is what the shipping Cap.app's dock tile - /// shows -- but a raw `setApplicationIconImage:` bypasses that pipeline, - /// so the classic `icon.png` renders with its own pre-26 rounded-rect - /// shape and looks visibly different next to the original. NSWorkspace's - /// `iconForFile:` hands back the icon *as the system presents it*, - /// treatment included, so the installed Cap.app's is used when it exists - /// and the raw asset only as a fallback. A future .app bundle with its - /// own (Icon Composer) icon supersedes all of this. - pub fn set_dock_icon(fallback_png: &[u8]) { + /// The debug-build dock icon. An unbundled dev binary carries no + /// Info.plist icon, so the PNG is set at runtime with + /// `setApplicationIconImage:`. Release bundles never call this: they take + /// their icon from the compiled Icon Composer `Assets.car`, which macOS + /// renders as native layers, and a runtime image would replace that. + pub fn set_dock_icon(png: &[u8]) { use objc2::{class, msg_send}; - use objc2_foundation::NSString; unsafe { - let mut image: Option> = None; - - let cap_app = NSString::from_str("/Applications/Cap.app"); - let exists: bool = { - let manager: *mut AnyObject = msg_send![class!(NSFileManager), defaultManager]; - !manager.is_null() && msg_send![manager, fileExistsAtPath: &*cap_app] - }; - if exists { - let workspace: *mut AnyObject = msg_send![class!(NSWorkspace), sharedWorkspace]; - if !workspace.is_null() { - let raw: *mut AnyObject = msg_send![workspace, iconForFile: &*cap_app]; - // `iconForFile:` is autoreleased; retain for the setter. - image = Retained::retain(raw); - if let Some(image) = &image { - // The workspace icon's nominal size is small (32pt); - // the reps go up to 1024. Ask for dock resolution. - let _: () = msg_send![ - &**image, - setSize: objc2_foundation::NSSize::new(512., 512.) - ]; - } - } - } - - if image.is_none() { - let data: *mut AnyObject = msg_send![ - class!(NSData), - dataWithBytes: fallback_png.as_ptr().cast::(), - length: fallback_png.len(), - ]; - if !data.is_null() { - let alloc: *mut AnyObject = msg_send![class!(NSImage), alloc]; - let raw: *mut AnyObject = msg_send![alloc, initWithData: data]; - image = Retained::from_raw(raw); - } + let data: *mut AnyObject = msg_send![ + class!(NSData), + dataWithBytes: png.as_ptr().cast::(), + length: png.len(), + ]; + if data.is_null() { + return; } - - let Some(image) = image else { return }; + let alloc: *mut AnyObject = msg_send![class!(NSImage), alloc]; + let Some(image) = Retained::::from_raw(msg_send![alloc, initWithData: data]) + else { + return; + }; let app: *mut AnyObject = msg_send![class!(NSApplication), sharedApplication]; if !app.is_null() { let _: () = msg_send![app, setApplicationIconImage: &*image]; } - tracing::info!(from_installed_app = exists, "dock icon set"); + tracing::info!("debug dock icon set"); } } diff --git a/apps/desktop-gpui/tauri.conf.json b/apps/desktop-gpui/tauri.conf.json index 314e0f27dd9..690013c52bc 100644 --- a/apps/desktop-gpui/tauri.conf.json +++ b/apps/desktop-gpui/tauri.conf.json @@ -27,11 +27,11 @@ "../desktop/src-tauri/binaries/cap-cli" ], "icon": [ - "../desktop/src-tauri/icons/32x32.png", - "../desktop/src-tauri/icons/128x128.png", - "../desktop/src-tauri/icons/128x128@2x.png", - "../desktop/src-tauri/icons/macos/icon.icns", - "../desktop/src-tauri/icons/icon.ico" + "icons/32x32.png", + "icons/128x128.png", + "icons/128x128@2x.png", + "icons/icon.icns", + "icons/icon.png" ], "resources": { "../desktop/src-tauri/assets/backgrounds/macOS/*": "assets/backgrounds/macOS/", @@ -70,7 +70,7 @@ "nsis": { "headerImage": "../desktop/src-tauri/assets/nsis-header.bmp", "sidebarImage": "../desktop/src-tauri/assets/nsis-sidebar.bmp", - "installerIcon": "../desktop/src-tauri/icons/icon.ico", + "installerIcon": "icons/icon.ico", "installerHooks": "packaging/windows/installer-hooks.nsh", "compression": "lzma" }, diff --git a/apps/desktop/src-tauri/assets/nsis-sidebar.bmp b/apps/desktop/src-tauri/assets/nsis-sidebar.bmp index 07460e15131..454c042954a 100644 Binary files a/apps/desktop/src-tauri/assets/nsis-sidebar.bmp and b/apps/desktop/src-tauri/assets/nsis-sidebar.bmp differ diff --git a/apps/desktop/src-tauri/assets/wix-banner.bmp b/apps/desktop/src-tauri/assets/wix-banner.bmp index 3fcc7467861..045adc8acbe 100644 Binary files a/apps/desktop/src-tauri/assets/wix-banner.bmp and b/apps/desktop/src-tauri/assets/wix-banner.bmp differ diff --git a/apps/desktop/src-tauri/assets/wix-dialog.bmp b/apps/desktop/src-tauri/assets/wix-dialog.bmp index 36625dc9f46..6d0cf393d24 100644 Binary files a/apps/desktop/src-tauri/assets/wix-dialog.bmp and b/apps/desktop/src-tauri/assets/wix-dialog.bmp differ diff --git a/apps/macos-bundles/.gitignore b/apps/macos-bundles/.gitignore new file mode 100644 index 00000000000..30bcfa4ed5c --- /dev/null +++ b/apps/macos-bundles/.gitignore @@ -0,0 +1 @@ +.build/ diff --git a/apps/macos-bundles/Bundles/CapQuickLookPreview/Info.plist b/apps/macos-bundles/Bundles/CapQuickLookPreview/Info.plist new file mode 100644 index 00000000000..f833ef9e2c7 --- /dev/null +++ b/apps/macos-bundles/Bundles/CapQuickLookPreview/Info.plist @@ -0,0 +1,42 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + CapQuickLookPreview + CFBundleIdentifier + so.cap.desktop.quicklook.preview + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + CapQuickLookPreview + CFBundlePackageType + XPC! + CFBundleShortVersionString + 0.0.0 + CFBundleVersion + 0 + LSMinimumSystemVersion + 12.3 + NSExtension + + NSExtensionAttributes + + QLIsDataBasedPreview + + QLSupportedContentTypes + + so.cap.desktop.project + + QLSupportsSearchableItems + + + NSExtensionPointIdentifier + com.apple.quicklook.preview + NSExtensionPrincipalClass + PreviewProvider + + + diff --git a/apps/macos-bundles/Bundles/CapQuickLookThumbnail/Info.plist b/apps/macos-bundles/Bundles/CapQuickLookThumbnail/Info.plist new file mode 100644 index 00000000000..1f8ba80a18a --- /dev/null +++ b/apps/macos-bundles/Bundles/CapQuickLookThumbnail/Info.plist @@ -0,0 +1,40 @@ + + + + + CFBundleDevelopmentRegion + en + CFBundleExecutable + CapQuickLookThumbnail + CFBundleIdentifier + so.cap.desktop.quicklook.thumbnail + CFBundleInfoDictionaryVersion + 6.0 + CFBundleName + CapQuickLookThumbnail + CFBundlePackageType + XPC! + CFBundleShortVersionString + 0.0.0 + CFBundleVersion + 0 + LSMinimumSystemVersion + 12.3 + NSExtension + + NSExtensionAttributes + + QLSupportedContentTypes + + so.cap.desktop.project + + QLThumbnailMinimumDimension + 0 + + NSExtensionPointIdentifier + com.apple.quicklook.thumbnail + NSExtensionPrincipalClass + ThumbnailProvider + + + diff --git a/apps/macos-bundles/Bundles/Sandbox.entitlements b/apps/macos-bundles/Bundles/Sandbox.entitlements new file mode 100644 index 00000000000..18aff0ce43c --- /dev/null +++ b/apps/macos-bundles/Bundles/Sandbox.entitlements @@ -0,0 +1,10 @@ + + + + + com.apple.security.app-sandbox + + com.apple.security.files.user-selected.read-only + + + diff --git a/apps/macos-bundles/Package.swift b/apps/macos-bundles/Package.swift new file mode 100644 index 00000000000..dc0810620d1 --- /dev/null +++ b/apps/macos-bundles/Package.swift @@ -0,0 +1,33 @@ +// swift-tools-version:5.9 +import PackageDescription + +let extensionEntryPoint: [LinkerSetting] = [ + .unsafeFlags(["-Xlinker", "-e", "-Xlinker", "_NSExtensionMain"]), +] + +let package = Package( + name: "CapMacOSBundles", + platforms: [.macOS(.v12)], + products: [ + .executable(name: "CapQuickLookThumbnail", targets: ["CapQuickLookThumbnail"]), + .executable(name: "CapQuickLookPreview", targets: ["CapQuickLookPreview"]), + ], + targets: [ + .target( + name: "CapShared", + path: "Sources/CapShared" + ), + .executableTarget( + name: "CapQuickLookThumbnail", + dependencies: ["CapShared"], + path: "Sources/CapQuickLookThumbnail", + linkerSettings: [.linkedFramework("QuickLookThumbnailing")] + extensionEntryPoint + ), + .executableTarget( + name: "CapQuickLookPreview", + dependencies: ["CapShared"], + path: "Sources/CapQuickLookPreview", + linkerSettings: [.linkedFramework("Quartz")] + extensionEntryPoint + ), + ] +) diff --git a/apps/macos-bundles/Sources/CapQuickLookPreview/PreviewProvider.swift b/apps/macos-bundles/Sources/CapQuickLookPreview/PreviewProvider.swift new file mode 100644 index 00000000000..b8fd243f9c6 --- /dev/null +++ b/apps/macos-bundles/Sources/CapQuickLookPreview/PreviewProvider.swift @@ -0,0 +1,16 @@ +import Foundation +import Quartz +import CapShared + +@objc(PreviewProvider) +final class PreviewProvider: QLPreviewProvider, QLPreviewingController { + func providePreview(for request: QLFilePreviewRequest) async throws -> QLPreviewReply { + if let video = CapProject.exportedVideo(in: request.fileURL) { + return QLPreviewReply(fileURL: video) + } + if let image = CapProject.previewImage(in: request.fileURL) { + return QLPreviewReply(fileURL: image) + } + throw CocoaError(.fileReadNoSuchFile) + } +} diff --git a/apps/macos-bundles/Sources/CapQuickLookThumbnail/ThumbnailProvider.swift b/apps/macos-bundles/Sources/CapQuickLookThumbnail/ThumbnailProvider.swift new file mode 100644 index 00000000000..8b016f8c7b6 --- /dev/null +++ b/apps/macos-bundles/Sources/CapQuickLookThumbnail/ThumbnailProvider.swift @@ -0,0 +1,17 @@ +import Foundation +import QuickLookThumbnailing +import CapShared + +@objc(ThumbnailProvider) +final class ThumbnailProvider: QLThumbnailProvider { + override func provideThumbnail( + for request: QLFileThumbnailRequest, + _ handler: @escaping (QLThumbnailReply?, Error?) -> Void + ) { + guard let image = CapProject.previewImage(in: request.fileURL) else { + handler(nil, CocoaError(.fileReadNoSuchFile)) + return + } + handler(QLThumbnailReply(imageFileURL: image), nil) + } +} diff --git a/apps/macos-bundles/Sources/CapShared/CapProject.swift b/apps/macos-bundles/Sources/CapShared/CapProject.swift new file mode 100644 index 00000000000..06d6c573794 --- /dev/null +++ b/apps/macos-bundles/Sources/CapShared/CapProject.swift @@ -0,0 +1,42 @@ +import Foundation + +public enum CapProject { + public static func previewImage(in bundle: URL) -> URL? { + let screenshot = bundle.appendingPathComponent("screenshots/display.jpg") + if isReadableFile(screenshot) { + return screenshot + } + guard let path = recordingDisplayPath(in: bundle) else { + return nil + } + let original = bundle.appendingPathComponent(path) + return isReadableFile(original) ? original : nil + } + + public static func exportedVideo(in bundle: URL) -> URL? { + let video = bundle.appendingPathComponent("output/result.mp4") + return isReadableFile(video) ? video : nil + } + + private static func recordingDisplayPath(in bundle: URL) -> String? { + let meta = bundle.appendingPathComponent("recording-meta.json") + guard let data = try? Data(contentsOf: meta), + let json = try? JSONSerialization.jsonObject(with: data) as? [String: Any], + let display = json["display"] as? [String: Any], + let path = display["path"] as? String, + !path.isEmpty, + !path.hasPrefix("/"), + !path.split(separator: "/").contains("..") + else { + return nil + } + return path + } + + private static func isReadableFile(_ url: URL) -> Bool { + var isDirectory: ObjCBool = false + return FileManager.default.fileExists(atPath: url.path, isDirectory: &isDirectory) + && !isDirectory.boolValue + && FileManager.default.isReadableFile(atPath: url.path) + } +} diff --git a/biome.json b/biome.json index 189cd4a7406..b0b10f1df1a 100644 --- a/biome.json +++ b/biome.json @@ -20,7 +20,8 @@ "!capcodec/**/*.sh", "!capcodec/**/*.jsonl", "!capcodec/**/*.pyc", - "!capcodec/capcodec.bundle" + "!capcodec/capcodec.bundle", + "!**/cap-solarium.icon/**" ], "ignoreUnknown": false }, diff --git a/scripts/build-macos-bundles.mjs b/scripts/build-macos-bundles.mjs new file mode 100644 index 00000000000..144419b0b78 --- /dev/null +++ b/scripts/build-macos-bundles.mjs @@ -0,0 +1,193 @@ +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +export const macosBundlesDirectory = path.join( + repoRoot, + "apps", + "macos-bundles", +); +const gpuiManifest = path.join(repoRoot, "apps", "desktop-gpui", "Cargo.toml"); +const minimumSystemVersion = "12.3"; + +export const extensions = [ + { + product: "CapQuickLookThumbnail", + bundleId: "so.cap.desktop.quicklook.thumbnail", + }, + { + product: "CapQuickLookPreview", + bundleId: "so.cap.desktop.quicklook.preview", + }, +]; + +const swiftArchitectures = { + "aarch64-apple-darwin": "arm64", + "x86_64-apple-darwin": "x86_64", +}; + +export function swiftArchitecture(target) { + const architecture = swiftArchitectures[target]; + if (!architecture) { + throw new Error(`Unsupported macOS bundle target: ${target}`); + } + return architecture; +} + +export function signingArguments({ identity, keychain, entitlements, bundle }) { + return [ + "--force", + "--timestamp", + "--options", + "runtime", + "--entitlements", + entitlements, + "--sign", + identity, + ...(keychain ? ["--keychain", keychain] : []), + bundle, + ]; +} + +export function plistVersion(source) { + const packageStart = source.indexOf("[package]"); + if (packageStart === -1) throw new Error("[package] not found"); + const packageEnd = source.indexOf("\n[", packageStart + 1); + const section = source.slice( + packageStart, + packageEnd === -1 ? source.length : packageEnd, + ); + const match = /^version\s*=\s*"([^"]+)"$/m.exec(section); + if (!match) throw new Error("package.version not found"); + return match[1]; +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { stdio: "inherit", ...options }); + if (result.error) throw result.error; + if (result.status !== 0) { + throw new Error( + `${command} ${args.join(" ")} failed with ${result.signal ?? result.status}`, + ); + } +} + +async function assemble(extension, executable, info, version, output) { + const contents = path.join(output, `${extension.product}.appex`, "Contents"); + await fs.rm(path.dirname(contents), { recursive: true, force: true }); + await fs.mkdir(path.join(contents, "MacOS"), { recursive: true }); + await fs.copyFile( + executable, + path.join(contents, "MacOS", extension.product), + ); + await fs.chmod(path.join(contents, "MacOS", extension.product), 0o755); + const plist = (await fs.readFile(info, "utf8")) + .replace( + /(CFBundleShortVersionString<\/key>\s*)[^<]*(<\/string>)/, + `$1${version}$2`, + ) + .replace( + /(CFBundleVersion<\/key>\s*)[^<]*(<\/string>)/, + `$1${version}$2`, + ); + await fs.writeFile(path.join(contents, "Info.plist"), plist); + return path.dirname(contents); +} + +export async function buildMacosBundles( + target, + { + env = process.env, + platform = process.platform, + outputDirectory = path.join(macosBundlesDirectory, ".build", "appex"), + } = {}, +) { + if (platform !== "darwin") { + throw new Error("The macOS bundles can only be built on macOS"); + } + const architecture = swiftArchitecture(target); + const version = plistVersion(await fs.readFile(gpuiManifest, "utf8")); + const identity = env.APPLE_SIGNING_IDENTITY; + const keychain = env.APPLE_KEYCHAIN; + const ci = env.CI === "true" || env.CI === "1"; + if (ci && !identity) { + throw new Error( + "APPLE_SIGNING_IDENTITY is required to sign the macOS bundles in CI", + ); + } + + const swiftArguments = [ + "-c", + "release", + "--package-path", + macosBundlesDirectory, + "--triple", + `${architecture}-apple-macosx${minimumSystemVersion}`, + ]; + run("swift", ["build", ...swiftArguments]); + const shown = spawnSync( + "swift", + ["build", ...swiftArguments, "--show-bin-path"], + { + encoding: "utf8", + }, + ); + if (shown.status !== 0) throw new Error("swift build --show-bin-path failed"); + const binDirectory = shown.stdout.trim(); + + await fs.mkdir(outputDirectory, { recursive: true }); + const bundles = []; + for (const extension of extensions) { + const executable = path.join(binDirectory, extension.product); + const info = path.join( + macosBundlesDirectory, + "Bundles", + extension.product, + "Info.plist", + ); + const bundle = await assemble( + extension, + executable, + info, + version, + outputDirectory, + ); + if (identity) { + run("codesign", [ + ...signingArguments({ + identity, + keychain, + entitlements: path.join( + macosBundlesDirectory, + "Bundles", + "Sandbox.entitlements", + ), + bundle, + }), + ]); + run("codesign", ["--verify", "--strict", bundle]); + } else { + console.warn( + `APPLE_SIGNING_IDENTITY is not set; ${path.basename(bundle)} is unsigned`, + ); + } + bundles.push(bundle); + } + return bundles; +} + +if ( + process.argv[1] && + path.resolve(process.argv[1]) === fileURLToPath(import.meta.url) +) { + const [target] = process.argv.slice(2); + buildMacosBundles(target).catch((error) => { + console.error(error?.message ?? error); + process.exitCode = 1; + }); +} diff --git a/scripts/build-macos-bundles.test.mjs b/scripts/build-macos-bundles.test.mjs new file mode 100644 index 00000000000..674e6e9ff85 --- /dev/null +++ b/scripts/build-macos-bundles.test.mjs @@ -0,0 +1,91 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { + extensions, + plistVersion, + signingArguments, + swiftArchitecture, +} from "./build-macos-bundles.mjs"; + +const readPlist = (relative) => + readFileSync( + new URL(`../apps/macos-bundles/${relative}`, import.meta.url), + "utf8", + ); + +test("each Quick Look extension is its own bundle with its own extension point", () => { + const points = { + CapQuickLookThumbnail: "com.apple.quicklook.thumbnail", + CapQuickLookPreview: "com.apple.quicklook.preview", + }; + for (const extension of extensions) { + const plist = readPlist(`Bundles/${extension.product}/Info.plist`); + assert.match(plist, new RegExp(`${extension.bundleId}`)); + assert.match( + plist, + new RegExp(`${points[extension.product]}`), + ); + assert.match(plist, /so\.cap\.desktop\.project<\/string>/); + assert.match(plist, /XPC!<\/string>/); + } +}); + +test("each principal class is the @objc name its provider exports", () => { + for (const extension of extensions) { + const plist = readPlist(`Bundles/${extension.product}/Info.plist`); + const source = readPlist( + `Sources/${extension.product}/${extension.product.replace("CapQuickLook", "")}Provider.swift`, + ); + const principal = + /NSExtensionPrincipalClass<\/key>\s*([^<]+)<\/string>/.exec( + plist, + )[1]; + const objc = /@objc\(([A-Za-z]+)\)/.exec(source)[1]; + assert.equal(principal, objc); + } +}); + +test("signing uses hardened runtime, a timestamp and the sandbox entitlements", () => { + assert.deepEqual( + signingArguments({ + identity: "Developer ID Application: Cap", + keychain: "/tmp/build.keychain", + entitlements: "/repo/Sandbox.entitlements", + bundle: "/tmp/Thumb.appex", + }), + [ + "--force", + "--timestamp", + "--options", + "runtime", + "--entitlements", + "/repo/Sandbox.entitlements", + "--sign", + "Developer ID Application: Cap", + "--keychain", + "/tmp/build.keychain", + "/tmp/Thumb.appex", + ], + ); +}); + +test("targets map to Swift architectures and unknown targets fail", () => { + assert.equal(swiftArchitecture("aarch64-apple-darwin"), "arm64"); + assert.equal(swiftArchitecture("x86_64-apple-darwin"), "x86_64"); + assert.throws( + () => swiftArchitecture("x86_64-pc-windows-msvc"), + /Unsupported/, + ); +}); + +test("the bundle version comes from the GPUI crate", () => { + assert.equal( + plistVersion('[package]\nname = "cap-gpui"\nversion = "0.6.1"\n'), + "0.6.1", + ); + assert.throws( + () => plistVersion('[package]\nname = "x"\n'), + /package.version/, + ); +}); diff --git a/scripts/build-macos-icon.mjs b/scripts/build-macos-icon.mjs new file mode 100644 index 00000000000..a46fe409a71 --- /dev/null +++ b/scripts/build-macos-icon.mjs @@ -0,0 +1,94 @@ +import { spawnSync } from "node:child_process"; +import * as fs from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const repoRoot = path.resolve( + path.dirname(fileURLToPath(import.meta.url)), + "..", +); +export const gpuiIconsDirectory = path.join( + repoRoot, + "apps", + "desktop-gpui", + "icons", +); +export const iconComposerName = "cap-solarium"; +export const accentColorName = "AccentColor"; + +export function actoolArguments({ source, catalog, output, partialPlist }) { + return [ + source, + catalog, + "--compile", + output, + "--output-format", + "human-readable-text", + "--notices", + "--warnings", + "--output-partial-info-plist", + partialPlist, + "--app-icon", + iconComposerName, + "--accent-color", + accentColorName, + "--include-all-app-icons", + "--enable-on-demand-resources", + "NO", + "--development-region", + "en", + "--target-device", + "mac", + "--minimum-deployment-target", + "12.3", + "--platform", + "macosx", + ]; +} + +function plistString(contents, key) { + const match = new RegExp( + `${key}\\s*([^<]+)`, + ).exec(contents); + return match?.[1]; +} + +export function iconNameFromPartialPlist(contents) { + const name = plistString(contents, "CFBundleIconName"); + if (!name) throw new Error("actool did not report CFBundleIconName"); + return name; +} + +export function accentNameFromPartialPlist(contents) { + return plistString(contents, "NSAccentColorName"); +} + +export async function buildMacosIcon({ + source = path.join(gpuiIconsDirectory, `${iconComposerName}.icon`), + catalog = path.join(gpuiIconsDirectory, "Cap.xcassets"), + outputDirectory, + platform = process.platform, + run = (command, args) => spawnSync(command, args, { encoding: "utf8" }), +} = {}) { + if (platform !== "darwin") { + throw new Error("The Icon Composer icon can only be compiled on macOS"); + } + const output = path.resolve(outputDirectory); + await fs.mkdir(output, { recursive: true }); + const partialPlist = path.join(output, "partial.plist"); + const result = run("xcrun", [ + "actool", + ...actoolArguments({ source, catalog, output, partialPlist }), + ]); + if (result.status !== 0) { + throw new Error(`actool failed: ${result.stderr || result.stdout}`); + } + const assets = path.join(output, "Assets.car"); + await fs.access(assets); + const plist = await fs.readFile(partialPlist, "utf8"); + return { + assets, + iconName: iconNameFromPartialPlist(plist), + accentName: accentNameFromPartialPlist(plist), + }; +} diff --git a/scripts/build-macos-icon.test.mjs b/scripts/build-macos-icon.test.mjs new file mode 100644 index 00000000000..77ab59f38d8 --- /dev/null +++ b/scripts/build-macos-icon.test.mjs @@ -0,0 +1,59 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import test from "node:test"; +import { + accentColorName, + accentNameFromPartialPlist, + actoolArguments, + iconComposerName, + iconNameFromPartialPlist, +} from "./build-macos-icon.mjs"; + +test("actool compiles the Icon Composer file for macOS 12.3 and later", () => { + const args = actoolArguments({ + source: "/tmp/cap-solarium.icon", + output: "/tmp/out", + partialPlist: "/tmp/out/partial.plist", + }); + assert.equal(args[0], "/tmp/cap-solarium.icon"); + assert.deepEqual( + args.slice(args.indexOf("--app-icon"), args.indexOf("--app-icon") + 2), + ["--app-icon", iconComposerName], + ); + assert.equal(args[args.indexOf("--minimum-deployment-target") + 1], "12.3"); + assert.equal(args[args.indexOf("--platform") + 1], "macosx"); +}); + +test("actool also compiles the Cap asset catalog for the accent color", () => { + const args = actoolArguments({ + source: "/tmp/cap-solarium.icon", + catalog: "/tmp/Cap.xcassets", + output: "/tmp/out", + partialPlist: "/tmp/out/partial.plist", + }); + assert.equal(args[1], "/tmp/Cap.xcassets"); + assert.equal(args[args.indexOf("--accent-color") + 1], accentColorName); +}); + +test("the accent name comes from the partial plist when actool writes one", () => { + const plist = readFileSync( + new URL("./fixtures/actool-partial.plist", import.meta.url), + "utf8", + ); + assert.equal(accentNameFromPartialPlist(plist), undefined); + assert.equal( + accentNameFromPartialPlist( + "NSAccentColorName\nAccentColor", + ), + "AccentColor", + ); +}); + +test("the icon name comes from the partial plist actool writes", () => { + const plist = readFileSync( + new URL("./fixtures/actool-partial.plist", import.meta.url), + "utf8", + ); + assert.equal(iconNameFromPartialPlist(plist), "cap-solarium"); + assert.throws(() => iconNameFromPartialPlist(""), /CFBundleIconName/); +}); diff --git a/scripts/build-macos-packages.mjs b/scripts/build-macos-packages.mjs index 966a1d2dc5f..9bad3cd9226 100644 --- a/scripts/build-macos-packages.mjs +++ b/scripts/build-macos-packages.mjs @@ -81,6 +81,7 @@ function validateArguments(target, args, platform) { valid = /^--(?:config|bundles)=.+/.test(argument) || argument === "--verbose" || + argument === "--debug" || /^-v{1,2}$/.test(argument); } } diff --git a/scripts/bundle-gpui.mjs b/scripts/bundle-gpui.mjs index ffa92a6ad24..bee858e0723 100644 --- a/scripts/bundle-gpui.mjs +++ b/scripts/bundle-gpui.mjs @@ -3,6 +3,8 @@ import { createHash } from "node:crypto"; import * as fs from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { buildMacosBundles } from "./build-macos-bundles.mjs"; +import { buildMacosIcon } from "./build-macos-icon.mjs"; import { buildMacosPackages } from "./build-macos-packages.mjs"; const repoRoot = path.resolve( @@ -41,14 +43,27 @@ export function gpuiBinaryPath(target, profile = "release") { ); } -export function gpuiPlatformConfig(platform) { +export function gpuiPlatformConfig( + platform, + { appexBundles = [], icon = null } = {}, +) { if (platform === "darwin") { + const files = Object.fromEntries([ + ...appexBundles.map((bundle) => [ + `PlugIns/${path.basename(bundle)}`, + bundle, + ]), + ...(icon ? [["Resources/Assets.car", icon.assets]] : []), + ]); return { bundle: { resources: { "../../target/native-deps/onnxruntime/lib/libonnxruntime.dylib": "onnxruntime/lib/libonnxruntime.dylib", }, + ...(Object.keys(files).length > 0 && { + macOS: { files }, + }), }, }; } @@ -67,6 +82,10 @@ export function gpuiPlatformConfig(platform) { return null; } +export function profileArguments(profile) { + return profile === "debug" ? ["--debug"] : []; +} + export function bundleArguments(target, args) { return ["tauri", "bundle", "--target", target, ...args]; } @@ -79,7 +98,7 @@ export function bundleEnvironment(env) { }; } -function validate(target, args) { +function validate(target, args, profile) { const platform = targetPlatform(target ?? ""); if (!platform || platform !== process.platform) { throw new Error( @@ -89,6 +108,9 @@ function validate(target, args) { if (args.some((argument) => /^--(?:target|debug)(?:=|$)/.test(argument))) { throw new Error("The GPUI bundle is always a release build for "); } + if (profile === "debug" && process.env.CI) { + throw new Error("--profile=debug is for local bundles only"); + } return platform; } @@ -105,8 +127,8 @@ function run(command, args, options = {}) { const sha256 = (contents) => createHash("sha256").update(contents).digest("hex"); -export async function stageMainBinary(target) { - const source = gpuiBinaryPath(target); +export async function stageMainBinary(target, profile = "release") { + const source = gpuiBinaryPath(target, profile); const destination = path.join(path.dirname(source), mainBinaryName(target)); const built = await fs.readFile(source); if (built.length === 0) throw new Error(`${source} is empty`); @@ -118,8 +140,8 @@ export async function stageMainBinary(target) { return destination; } -export async function writePlatformConfig(platform) { - const config = gpuiPlatformConfig(platform); +export async function writePlatformConfig(platform, options) { + const config = gpuiPlatformConfig(platform, options); if (!config) return null; const file = path.join(gpuiDirectory, platformConfigNames[platform]); const contents = `${JSON.stringify(config, null, "\t")}\n`; @@ -129,29 +151,44 @@ export async function writePlatformConfig(platform) { } export function parseOptions(args) { - const flags = new Set(["--skip-build", "--no-bundle"]); + const flags = new Set(["--skip-build", "--no-bundle", "--profile=debug"]); return { skipBuild: args.includes("--skip-build"), noBundle: args.includes("--no-bundle"), + profile: args.includes("--profile=debug") ? "debug" : "release", bundleArgs: args.filter((argument) => !flags.has(argument)), }; } export async function bundleGpui(target, args, { env = process.env } = {}) { - const { skipBuild, noBundle, bundleArgs } = parseOptions(args); - const platform = validate(target, bundleArgs); + const { skipBuild, noBundle, profile, bundleArgs } = parseOptions(args); + const platform = validate(target, bundleArgs, profile); if (!skipBuild) { run("bash", [ path.join(repoRoot, "scripts", "build-gpui-binary.sh"), - "release", + profile, target, ]); } - const staged = await stageMainBinary(target); + const staged = await stageMainBinary(target, profile); console.log(`Staged ${staged}`); if (noBundle) return; - await writePlatformConfig(platform); + const appexBundles = + platform === "darwin" ? await buildMacosBundles(target, { env }) : []; + const icon = + platform === "darwin" + ? await buildMacosIcon({ + outputDirectory: path.join( + gpuiDirectory, + "target", + target, + profile, + "icon", + ), + }) + : null; + await writePlatformConfig(platform, { appexBundles, icon }); if (platform === "linux") { await fs .access(path.join(gpuiDirectory, platformConfigNames.linux)) @@ -162,11 +199,16 @@ export async function bundleGpui(target, args, { env = process.env } = {}) { }); } + const profileArgs = profileArguments(profile); if (platform === "darwin") { - const result = await buildMacosPackages(target, bundleArgs, { - bundleOnly: true, - env: bundleEnvironment(env), - }); + const result = await buildMacosPackages( + target, + [...bundleArgs, ...profileArgs], + { + bundleOnly: true, + env: bundleEnvironment(env), + }, + ); if (result.signal || result.code !== 0) { throw new Error( `macOS GPUI bundling failed with ${result.signal ?? result.code}`, @@ -175,10 +217,14 @@ export async function bundleGpui(target, args, { env = process.env } = {}) { return; } - run("bun", ["run", ...bundleArguments(target, bundleArgs)], { - cwd: desktopDirectory, - env: bundleEnvironment({ ...env, RUST_TARGET_TRIPLE: target }), - }); + run( + "bun", + ["run", ...bundleArguments(target, [...bundleArgs, ...profileArgs])], + { + cwd: desktopDirectory, + env: bundleEnvironment({ ...env, RUST_TARGET_TRIPLE: target }), + }, + ); } if ( diff --git a/scripts/bundle-gpui.test.mjs b/scripts/bundle-gpui.test.mjs index 367ff895e7f..d4a2dd11786 100644 --- a/scripts/bundle-gpui.test.mjs +++ b/scripts/bundle-gpui.test.mjs @@ -10,6 +10,7 @@ import { gpuiPlatformConfig, mainBinaryName, parseOptions, + profileArguments, targetPlatform, } from "./bundle-gpui.mjs"; @@ -147,6 +148,30 @@ test("platform resources point at the shared native dependencies", () => { assert.equal(gpuiPlatformConfig("linux"), null); }); +test("signed Quick Look extensions are copied into PlugIns", () => { + const appex = [ + "/tmp/build/CapQuickLookThumbnail.appex", + "/tmp/build/CapQuickLookPreview.appex", + ]; + assert.deepEqual( + gpuiPlatformConfig("darwin", { appexBundles: appex }).bundle.macOS.files, + { + "PlugIns/CapQuickLookThumbnail.appex": appex[0], + "PlugIns/CapQuickLookPreview.appex": appex[1], + }, + ); + assert.equal(gpuiPlatformConfig("darwin").bundle.macOS, undefined); +}); + +test("the GPUI app declares the Cap project type for Quick Look", () => { + const info = readFileSync( + new URL("../apps/desktop-gpui/Info.plist", import.meta.url), + "utf8", + ); + assert.match(info, /so\.cap\.desktop\.project<\/string>/); + assert.match(info, /com\.apple\.package<\/string>/); +}); + test("tauri bundle runs against the GPUI crate with its pinned toolchain", () => { assert.deepEqual(bundleArguments("x86_64-unknown-linux-gnu", ["--verbose"]), [ "tauri", @@ -165,11 +190,47 @@ test("signing builds can stop before bundling and rebundle without rebuilding", assert.deepEqual(parseOptions(["--no-bundle"]), { skipBuild: false, noBundle: true, + profile: "release", bundleArgs: [], }); assert.deepEqual(parseOptions(["--skip-build", "--bundles", "nsis"]), { skipBuild: true, noBundle: false, + profile: "release", bundleArgs: ["--bundles", "nsis"], }); }); + +test("the debug profile reaches Tauri on every platform, not only macOS", () => { + assert.deepEqual(profileArguments("debug"), ["--debug"]); + assert.deepEqual(profileArguments("release"), []); + assert.deepEqual( + bundleArguments("x86_64-pc-windows-msvc", [ + "--bundles", + "nsis", + ...profileArguments("debug"), + ]), + [ + "tauri", + "bundle", + "--target", + "x86_64-pc-windows-msvc", + "--bundles", + "nsis", + "--debug", + ], + ); +}); + +test("a local debug bundle is opt-in and never reaches the release path", () => { + assert.deepEqual(parseOptions(["--profile=debug", "--bundles", "app"]), { + skipBuild: false, + noBundle: false, + profile: "debug", + bundleArgs: ["--bundles", "app"], + }); + assert.equal( + gpuiBinaryPath("aarch64-apple-darwin", "debug"), + path.join(gpuiDirectory, "target/aarch64-apple-darwin/debug/cap-gpui"), + ); +}); diff --git a/scripts/fixtures/actool-partial.plist b/scripts/fixtures/actool-partial.plist new file mode 100644 index 00000000000..4c6bfeb6040 --- /dev/null +++ b/scripts/fixtures/actool-partial.plist @@ -0,0 +1,10 @@ + + + + + CFBundleIconFile + cap-solarium + CFBundleIconName + cap-solarium + +