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