diff --git a/.github/workflows/build-dmg.yml b/.github/workflows/build-dmg.yml
new file mode 100644
index 0000000..e20096a
--- /dev/null
+++ b/.github/workflows/build-dmg.yml
@@ -0,0 +1,34 @@
+name: Build DMG
+
+on:
+ workflow_dispatch:
+ pull_request:
+ push:
+ branches:
+ - main
+
+permissions:
+ contents: read
+
+jobs:
+ build-dmg:
+ runs-on: macos-15
+
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v4
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: "22"
+
+ - name: Build DMG
+ run: make dmg
+
+ - name: Upload DMG artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: CroPDF-dmg
+ path: dist/CroPDF.dmg
+ if-no-files-found: error
diff --git a/.gitignore b/.gitignore
index 0023a53..784ff9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
.DS_Store
/.build
+/dist
/Packages
xcuserdata/
DerivedData/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..8108f81
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,30 @@
+APP_NAME := CroPDF
+EXECUTABLE := CroPDFMacOS
+CONFIGURATION ?= release
+DIST_DIR := $(CURDIR)/dist
+APP_DIR := $(DIST_DIR)/$(APP_NAME).app
+CONTENTS_DIR := $(APP_DIR)/Contents
+MACOS_DIR := $(CONTENTS_DIR)/MacOS
+RESOURCES_DIR := $(CONTENTS_DIR)/Resources
+DMG_PATH := $(DIST_DIR)/$(APP_NAME).dmg
+CREATE_DMG_VERSION ?= 8.0.0
+
+.PHONY: dmg clean
+
+dmg:
+ @set -euo pipefail; \
+ swift build --disable-sandbox -c "$(CONFIGURATION)"; \
+ BIN_DIR="$$(swift build --disable-sandbox -c "$(CONFIGURATION)" --show-bin-path)"; \
+ rm -rf "$(APP_DIR)"; \
+ mkdir -p "$(MACOS_DIR)" "$(RESOURCES_DIR)"; \
+ cp "$$BIN_DIR/$(EXECUTABLE)" "$(MACOS_DIR)/$(EXECUTABLE)"; \
+ cp -R "$$BIN_DIR/$(EXECUTABLE)_$(EXECUTABLE).bundle" "$(RESOURCES_DIR)/$(EXECUTABLE)_$(EXECUTABLE).bundle"; \
+ cp "$(CURDIR)/src/Resources/$(APP_NAME).icns" "$(RESOURCES_DIR)/$(APP_NAME).icns"; \
+ cp "$(CURDIR)/scripts/Info.plist" "$(CONTENTS_DIR)/Info.plist"; \
+ rm -f "$(DMG_PATH)"; \
+ npx --yes "create-dmg@$(CREATE_DMG_VERSION)" --overwrite --no-version-in-filename --no-code-sign "$(APP_DIR)" "$(DIST_DIR)"; \
+ rm -rf "$(APP_DIR)"; \
+ echo "Built $(DMG_PATH)"
+
+clean:
+ rm -rf "$(DIST_DIR)"
diff --git a/Package.swift b/Package.swift
index cf992ff..e760fcb 100644
--- a/Package.swift
+++ b/Package.swift
@@ -9,7 +9,10 @@ let package = Package(
targets: [
.executableTarget(
name: "CroPDFMacOS",
- path: "src"
+ path: "src",
+ resources: [
+ .process("Resources"),
+ ]
),
]
)
diff --git a/README.md b/README.md
index 6d2c19a..00d9de4 100644
--- a/README.md
+++ b/README.md
@@ -41,3 +41,17 @@ swift run CroPDFMacOS /path/to/file.pdf --page 12
```
You can also open the package directly in Xcode and run it as a macOS app target.
+
+## Package As .app
+
+There is no dedicated `.app` packaging command anymore. The supported packaging output is the DMG.
+
+## Package As .dmg
+
+```bash
+make dmg
+```
+
+This builds a temporary app bundle, packages it with `create-dmg`, and leaves you with `dist/CroPDF.dmg`. The intermediate `dist/CroPDF.app` is removed automatically.
+
+`Node.js` and `npm` are required for the DMG step because the script downloads `create-dmg` on demand.
diff --git a/assets/CroPDF.svg b/assets/CroPDF.svg
new file mode 100644
index 0000000..187b25d
--- /dev/null
+++ b/assets/CroPDF.svg
@@ -0,0 +1,120 @@
+
+
diff --git a/scripts/Info.plist b/scripts/Info.plist
new file mode 100644
index 0000000..c22ae0c
--- /dev/null
+++ b/scripts/Info.plist
@@ -0,0 +1,30 @@
+
+
+
+
+ CFBundleDevelopmentRegion
+ en
+ CFBundleExecutable
+ CroPDFMacOS
+ CFBundleIconFile
+ CroPDF
+ CFBundleIdentifier
+ com.ericceglie.CroPDF
+ CFBundleInfoDictionaryVersion
+ 6.0
+ CFBundleName
+ CroPDF
+ CFBundlePackageType
+ APPL
+ CFBundleShortVersionString
+ 1.0
+ CFBundleVersion
+ 1
+ LSMinimumSystemVersion
+ 14.0
+ NSHighResolutionCapable
+
+ NSPrincipalClass
+ NSApplication
+
+
diff --git a/src/CroPDFMacOSApp.swift b/src/CroPDFMacOSApp.swift
index 13de9a2..a5d2330 100644
--- a/src/CroPDFMacOSApp.swift
+++ b/src/CroPDFMacOSApp.swift
@@ -3,10 +3,30 @@ import SwiftUI
final class CroPDFAppDelegate: NSObject, NSApplicationDelegate {
func applicationDidFinishLaunching(_ notification: Notification) {
+ if let iconImage = appIconImage() {
+ NSApp.applicationIconImage = iconImage
+ }
+
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
NSApp.windows.first?.makeKeyAndOrderFront(nil)
}
+
+ private func appIconImage() -> NSImage? {
+ let candidates = [
+ ("CroPDF", "icns"),
+ ("CroPDFIcon", "png"),
+ ]
+
+ for (name, ext) in candidates {
+ if let iconURL = Bundle.module.url(forResource: name, withExtension: ext),
+ let iconImage = NSImage(contentsOf: iconURL) {
+ return iconImage
+ }
+ }
+
+ return nil
+ }
}
@main
diff --git a/src/Resources/CroPDF.icns b/src/Resources/CroPDF.icns
new file mode 100644
index 0000000..f1318a2
Binary files /dev/null and b/src/Resources/CroPDF.icns differ
diff --git a/src/Resources/CroPDFIcon.png b/src/Resources/CroPDFIcon.png
new file mode 100644
index 0000000..5d6d1e8
Binary files /dev/null and b/src/Resources/CroPDFIcon.png differ