Release v0.3

This commit is contained in:
Akshay Kolli
2026-06-24 17:51:26 -07:00
parent 3d112c677a
commit 085d7a16dc
33 changed files with 2828 additions and 428 deletions

View File

@@ -3,15 +3,22 @@ set -euo pipefail
APP_NAME="I Hate PDFs"
EXECUTABLE_NAME="IHatePDFs"
APP_VERSION="${APP_VERSION:-0.2.0}"
BUILD_NUMBER="${BUILD_NUMBER:-2}"
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/scripts/release-version.sh"
CONFIGURATION="${CONFIGURATION:-release}"
BUNDLE_ID="${BUNDLE_ID:-net.akkolli.ihatepdfs}"
STRIP_RELEASE="${STRIP_RELEASE:-1}"
SIGNING_IDENTITY="${SIGNING_IDENTITY:-}"
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-}"
PROVISIONING_PROFILE="${PROVISIONING_PROFILE:-}"
CODESIGN_TIMESTAMP="${CODESIGN_TIMESTAMP:-1}"
CODESIGN_OPTIONS="${CODESIGN_OPTIONS:-}"
PLISTBUDDY="/usr/libexec/PlistBuddy"
if [[ -z "${ARCHS+x}" && "$CONFIGURATION" == "release" ]]; then
ARCHS="arm64 x86_64"
else
ARCHS="${ARCHS:-}"
fi
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/$APP_NAME.app"
CONTENTS_DIR="$APP_DIR/Contents"
@@ -19,6 +26,29 @@ MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
ICON_SOURCE="$ROOT_DIR/ihatepdf.png"
ICON_NAME="AppIcon"
DERIVED_ENTITLEMENTS_PATH=""
PROFILE_PLIST_PATH=""
cleanup() {
if [[ -n "$DERIVED_ENTITLEMENTS_PATH" ]]; then
rm -f "$DERIVED_ENTITLEMENTS_PATH"
fi
if [[ -n "$PROFILE_PLIST_PATH" ]]; then
rm -f "$PROFILE_PLIST_PATH"
fi
}
trap cleanup EXIT
set_plist_string() {
local plist="$1"
local key="$2"
local value="$3"
if "$PLISTBUDDY" -c "Set :$key $value" "$plist" >/dev/null 2>&1; then
return
fi
"$PLISTBUDDY" -c "Add :$key string $value" "$plist"
}
cd "$ROOT_DIR"
SWIFT_BUILD_ARGS=(-c "$CONFIGURATION")
@@ -26,13 +56,26 @@ for ARCH in $ARCHS; do
SWIFT_BUILD_ARGS+=(--arch "$ARCH")
done
swift build "${SWIFT_BUILD_ARGS[@]}"
BUILD_DIR="$(swift build "${SWIFT_BUILD_ARGS[@]}" --show-bin-path)"
swift build "${SWIFT_BUILD_ARGS[@]}"
rm -rf "$APP_DIR"
mkdir -p "$MACOS_DIR" "$RESOURCES_DIR"
cp "$BUILD_DIR/$EXECUTABLE_NAME" "$MACOS_DIR/$EXECUTABLE_NAME"
if [[ "$CONFIGURATION" == "release" && "$STRIP_RELEASE" != "0" ]]; then
strip -x "$MACOS_DIR/$EXECUTABLE_NAME"
fi
if [[ -n "$PROVISIONING_PROFILE" ]]; then
if [[ ! -f "$PROVISIONING_PROFILE" ]]; then
echo "Missing provisioning profile: $PROVISIONING_PROFILE" >&2
exit 1
fi
cp "$PROVISIONING_PROFILE" "$CONTENTS_DIR/embedded.provisionprofile"
xattr -cr "$CONTENTS_DIR/embedded.provisionprofile" 2>/dev/null || true
fi
if [[ ! -f "$ICON_SOURCE" ]]; then
echo "Missing app icon source: $ICON_SOURCE" >&2
exit 1
@@ -72,7 +115,7 @@ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
<key>CFBundleExecutable</key>
<string>$EXECUTABLE_NAME</string>
<key>CFBundleIdentifier</key>
<string>org.ihatepdfs.app</string>
<string>$BUNDLE_ID</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
@@ -104,6 +147,8 @@ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
<string>$BUILD_NUMBER</string>
<key>LSMinimumSystemVersion</key>
<string>13.0</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.productivity</string>
<key>NSHighResolutionCapable</key>
<true/>
<key>NSSupportsAutomaticGraphicsSwitching</key>
@@ -116,4 +161,51 @@ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
</plist>
PLIST
if [[ -n "$SIGNING_IDENTITY" ]]; then
if [[ -n "$ENTITLEMENTS_PATH" && ! -f "$ENTITLEMENTS_PATH" ]]; then
echo "Missing entitlements file: $ENTITLEMENTS_PATH" >&2
exit 1
fi
APP_ENTITLEMENTS_PATH="$ENTITLEMENTS_PATH"
if [[ -n "$PROVISIONING_PROFILE" ]]; then
PROFILE_PLIST_PATH="$(mktemp "$DIST_DIR/profile.XXXXXX.plist")"
security cms -D -i "$PROVISIONING_PROFILE" > "$PROFILE_PLIST_PATH"
APP_IDENTIFIER="$("$PLISTBUDDY" -c "Print :Entitlements:com.apple.application-identifier" "$PROFILE_PLIST_PATH")"
TEAM_IDENTIFIER="$("$PLISTBUDDY" -c "Print :Entitlements:com.apple.developer.team-identifier" "$PROFILE_PLIST_PATH")"
DERIVED_ENTITLEMENTS_PATH="$(mktemp "$DIST_DIR/entitlements.XXXXXX.plist")"
if [[ -n "$ENTITLEMENTS_PATH" ]]; then
cp "$ENTITLEMENTS_PATH" "$DERIVED_ENTITLEMENTS_PATH"
else
cat > "$DERIVED_ENTITLEMENTS_PATH" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</plist>
PLIST
fi
set_plist_string "$DERIVED_ENTITLEMENTS_PATH" "com.apple.application-identifier" "$APP_IDENTIFIER"
set_plist_string "$DERIVED_ENTITLEMENTS_PATH" "com.apple.developer.team-identifier" "$TEAM_IDENTIFIER"
APP_ENTITLEMENTS_PATH="$DERIVED_ENTITLEMENTS_PATH"
fi
CODESIGN_ARGS=(--force --sign "$SIGNING_IDENTITY")
if [[ "$CODESIGN_TIMESTAMP" != "0" ]]; then
CODESIGN_ARGS+=(--timestamp)
fi
if [[ -n "$CODESIGN_OPTIONS" ]]; then
CODESIGN_ARGS+=(--options "$CODESIGN_OPTIONS")
fi
if [[ -n "$APP_ENTITLEMENTS_PATH" ]]; then
CODESIGN_ARGS+=(--entitlements "$APP_ENTITLEMENTS_PATH")
fi
codesign "${CODESIGN_ARGS[@]}" "$APP_DIR"
codesign --verify --strict --verbose=2 "$APP_DIR"
fi
echo "Built $APP_DIR"
du -sh "$APP_DIR" "$MACOS_DIR/$EXECUTABLE_NAME" "$RESOURCES_DIR/$ICON_NAME.icns"

69
scripts/make-app-store-pkg.sh Executable file
View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/scripts/release-version.sh"
APP_NAME="I Hate PDFs"
BUNDLE_ID="${BUNDLE_ID:-net.akkolli.ihatepdfs}"
APP_SIGNING_IDENTITY="${APP_SIGNING_IDENTITY:-}"
INSTALLER_SIGNING_IDENTITY="${INSTALLER_SIGNING_IDENTITY:-}"
PROVISIONING_PROFILE="${PROVISIONING_PROFILE:-}"
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-$ROOT_DIR/Signing/IHatePDFs-AppStore.entitlements}"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/$APP_NAME.app"
PKG_PATH="${PKG_PATH:-$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-appstore.pkg}"
VALIDATE_WITH_ALTOOL="${VALIDATE_WITH_ALTOOL:-0}"
require_value() {
local name="$1"
local value="$2"
local hint="$3"
if [[ -z "$value" ]]; then
echo "Missing $name." >&2
echo "$hint" >&2
exit 2
fi
}
require_value "APP_SIGNING_IDENTITY" "$APP_SIGNING_IDENTITY" \
"Example: APP_SIGNING_IDENTITY=\"Apple Distribution: Your Name (TEAMID)\" or \"3rd Party Mac Developer Application: Your Name (TEAMID)\""
require_value "INSTALLER_SIGNING_IDENTITY" "$INSTALLER_SIGNING_IDENTITY" \
"Example: INSTALLER_SIGNING_IDENTITY=\"3rd Party Mac Developer Installer: Your Name (TEAMID)\""
require_value "PROVISIONING_PROFILE" "$PROVISIONING_PROFILE" \
"Download an App Store provisioning profile for $BUNDLE_ID and pass its local path."
mkdir -p "$DIST_DIR"
BUNDLE_ID="$BUNDLE_ID" \
APP_VERSION="$APP_VERSION" \
BUILD_NUMBER="$BUILD_NUMBER" \
SIGNING_IDENTITY="$APP_SIGNING_IDENTITY" \
ENTITLEMENTS_PATH="$ENTITLEMENTS_PATH" \
PROVISIONING_PROFILE="$PROVISIONING_PROFILE" \
"$ROOT_DIR/scripts/build-app.sh"
xattr -cr "$APP_DIR" 2>/dev/null || true
rm -f "$PKG_PATH"
productbuild \
--component "$APP_DIR" /Applications \
--sign "$INSTALLER_SIGNING_IDENTITY" \
"$PKG_PATH"
pkgutil --check-signature "$PKG_PATH"
if [[ "$VALIDATE_WITH_ALTOOL" == "1" ]]; then
require_value "ASC_USERNAME" "${ASC_USERNAME:-}" \
"Set ASC_USERNAME to the Apple ID or App Store Connect API key issuer format expected by altool."
require_value "ASC_PASSWORD" "${ASC_PASSWORD:-}" \
"Set ASC_PASSWORD to an app-specific password or app-store-connect API key password."
xcrun altool --validate-app \
--type macos \
--file "$PKG_PATH" \
--username "$ASC_USERNAME" \
--password "$ASC_PASSWORD"
fi
echo "Created App Store package: $PKG_PATH"

View File

@@ -2,14 +2,15 @@
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/scripts/release-version.sh"
APP_NAME="I Hate PDFs"
RELEASE_VERSION="${RELEASE_VERSION:-0.2}"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/$APP_NAME.app"
DMG_PATH="$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos.dmg"
BUILD_APP="${BUILD_APP:-1}"
if [[ ! -d "$APP_DIR" ]]; then
"$ROOT_DIR/scripts/build-app.sh"
if [[ "$BUILD_APP" != "0" || ! -d "$APP_DIR" ]]; then
APP_VERSION="$APP_VERSION" BUILD_NUMBER="$BUILD_NUMBER" "$ROOT_DIR/scripts/build-app.sh"
fi
rm -f "$DMG_PATH"

5
scripts/release-version.sh Executable file
View File

@@ -0,0 +1,5 @@
#!/usr/bin/env bash
APP_VERSION="${APP_VERSION:-0.3.0}"
BUILD_NUMBER="${BUILD_NUMBER:-4}"
RELEASE_VERSION="${RELEASE_VERSION:-${APP_VERSION%.0}}"

View File

@@ -18,8 +18,9 @@ enum VerificationError: Error, CustomStringConvertible {
case missingName(page: Int, index: Int, key: String)
case missingString(page: Int, index: Int, key: String)
case missingArray(page: Int, index: Int, key: String)
case missingPopup(page: Int, index: Int, subtype: String)
case missingPopupParent(page: Int, index: Int)
case unexpectedMarkupPopup(page: Int, index: Int, subtype: String)
case unexpectedPopupLink(page: Int, index: Int, subtype: String)
case missingExpectedSubtype(String)
var description: String {
@@ -36,10 +37,12 @@ enum VerificationError: Error, CustomStringConvertible {
return "Annotation \(index) on page \(page) is missing string key /\(key)"
case .missingArray(let page, let index, let key):
return "Annotation \(index) on page \(page) is missing array key /\(key)"
case .missingPopup(let page, let index, let subtype):
return "\(subtype) annotation \(index) on page \(page) is missing a /Popup dictionary"
case .missingPopupParent(let page, let index):
return "Popup annotation \(index) on page \(page) is missing a /Parent dictionary"
case .unexpectedMarkupPopup(let page, let index, let subtype):
return "Popup annotation \(index) on page \(page) points at a /\(subtype) markup annotation; markup comments should export through /Contents"
case .unexpectedPopupLink(let page, let index, let subtype):
return "\(subtype) annotation \(index) on page \(page) should store comments in /Contents, not a /Popup link"
case .missingExpectedSubtype(let subtype):
return "Expected at least one /\(subtype) annotation"
}
@@ -85,7 +88,7 @@ for pageNumber in 1...document.numberOfPages {
switch subtype {
case "Highlight":
try requireMarkupKeys(in: annotation, page: pageNumber, index: annotationIndex)
try requirePopup(in: annotation, page: pageNumber, index: annotationIndex, subtype: "Highlight")
try rejectPopupLink(in: annotation, subtype: subtype, page: pageNumber, index: annotationIndex)
if hasString(in: annotation, key: "IHatePDFsKind") {
try requireString(in: annotation, key: "IHatePDFsKind", page: pageNumber, index: annotationIndex)
summary.selectedTextComments += 1
@@ -95,7 +98,7 @@ for pageNumber in 1...document.numberOfPages {
case "Underline":
summary.underlines += 1
try requireMarkupKeys(in: annotation, page: pageNumber, index: annotationIndex)
try requirePopup(in: annotation, page: pageNumber, index: annotationIndex, subtype: "Underline")
try rejectPopupLink(in: annotation, subtype: subtype, page: pageNumber, index: annotationIndex)
case "Text":
try requireTextKeys(in: annotation, page: pageNumber, index: annotationIndex)
@@ -104,7 +107,6 @@ for pageNumber in 1...document.numberOfPages {
try requireString(in: annotation, key: "IRT", page: pageNumber, index: annotationIndex)
try requireString(in: annotation, key: "RT", page: pageNumber, index: annotationIndex)
} else {
try requirePopup(in: annotation, page: pageNumber, index: annotationIndex, subtype: "Text")
summary.textNotes += 1
}
case "FreeText":
@@ -118,9 +120,24 @@ for pageNumber in 1...document.numberOfPages {
case "Popup":
summary.popups += 1
var parentDictionary: CGPDFDictionaryRef?
guard CGPDFDictionaryGetDictionary(annotation, "Parent", &parentDictionary) else {
guard CGPDFDictionaryGetDictionary(annotation, "Parent", &parentDictionary),
let parentDictionary
else {
throw VerificationError.missingPopupParent(page: pageNumber, index: annotationIndex)
}
let parentSubtype = try nameValue(
in: parentDictionary,
key: "Subtype",
page: pageNumber,
index: annotationIndex
)
if parentSubtype == "Highlight" || parentSubtype == "Underline" {
throw VerificationError.unexpectedMarkupPopup(
page: pageNumber,
index: annotationIndex,
subtype: parentSubtype
)
}
default:
continue
}
@@ -145,10 +162,6 @@ guard summary.replies > 0 else {
guard summary.freeText > 0 else {
throw VerificationError.missingExpectedSubtype("FreeText")
}
guard summary.popups >= 4 else {
throw VerificationError.missingExpectedSubtype("Popup")
}
print("Verified raw PDF annotation dictionaries in \(inputURL.path): \(summary.highlights) highlight, \(summary.selectedTextComments) selected-text comment, \(summary.underlines) underline, \(summary.textNotes) text note, \(summary.replies) reply, \(summary.freeText) free-text, \(summary.popups) popups.")
func annotationDictionary(
@@ -251,6 +264,20 @@ func requireMarkupKeys(
try requireString(in: dictionary, key: "M", page: page, index: index)
}
func rejectPopupLink(
in dictionary: CGPDFDictionaryRef,
subtype: String,
page: Int,
index: Int
) throws {
var popupDictionary: CGPDFDictionaryRef?
guard CGPDFDictionaryGetDictionary(dictionary, "Popup", &popupDictionary) else {
return
}
throw VerificationError.unexpectedPopupLink(page: page, index: index, subtype: subtype)
}
func requireTextKeys(
in dictionary: CGPDFDictionaryRef,
page: Int,
@@ -262,22 +289,3 @@ func requireTextKeys(
try requireString(in: dictionary, key: "T", page: page, index: index)
try requireString(in: dictionary, key: "M", page: page, index: index)
}
func requirePopup(
in dictionary: CGPDFDictionaryRef,
page: Int,
index: Int,
subtype: String
) throws {
var popupDictionary: CGPDFDictionaryRef?
guard CGPDFDictionaryGetDictionary(dictionary, "Popup", &popupDictionary),
let popupDictionary
else {
throw VerificationError.missingPopup(page: page, index: index, subtype: subtype)
}
let popupSubtype = try nameValue(in: popupDictionary, key: "Subtype", page: page, index: index)
guard popupSubtype == "Popup" else {
throw VerificationError.missingPopup(page: page, index: index, subtype: subtype)
}
}

View File

@@ -29,7 +29,6 @@ standardize(
author: "Professor"
)
page.addAnnotation(highlight)
addPopup(for: highlight, bounds: CGRect(x: 352, y: 592, width: 240, height: 110))
let selectedTextComment = PDFAnnotation(
bounds: CGRect(x: 72, y: 594, width: 260, height: 22),
@@ -42,12 +41,11 @@ selectedTextComment.quadrilateralPoints = quadPoints(width: 260, height: 22)
standardize(
selectedTextComment,
name: "verify-selected-text-comment",
contents: "This selected-text comment is saved as standard PDF markup with popup contents.",
contents: "This selected-text comment is saved as standard parent annotation contents.",
author: "Professor"
)
_ = selectedTextComment.setValue("Comment", forAnnotationKey: PDFAnnotationKey(rawValue: "IHatePDFsKind"))
page.addAnnotation(selectedTextComment)
addPopup(for: selectedTextComment, bounds: CGRect(x: 352, y: 472, width: 240, height: 110))
let underline = PDFAnnotation(
bounds: CGRect(x: 72, y: 570, width: 260, height: 24),
@@ -64,7 +62,6 @@ standardize(
author: "Professor"
)
page.addAnnotation(underline)
addPopup(for: underline, bounds: CGRect(x: 352, y: 540, width: 240, height: 110))
let textNote = PDFAnnotation(
bounds: CGRect(x: 360, y: 620, width: 28, height: 28),
@@ -153,18 +150,6 @@ func standardize(
_ = annotation.setValue("Unmarked", forAnnotationKey: PDFAnnotationKey(rawValue: "State"))
}
func addPopup(for annotation: PDFAnnotation, bounds: CGRect) {
let popup = PDFAnnotation(bounds: bounds, forType: .popup, withProperties: nil)
popup.contents = annotation.contents
popup.userName = annotation.userName
popup.modificationDate = annotation.modificationDate
popup.isOpen = false
popup.shouldDisplay = true
popup.shouldPrint = true
annotation.popup = popup
page.addAnnotation(popup)
}
func quadPoints(width: CGFloat, height: CGFloat) -> [NSValue] {
[
NSValue(point: CGPoint(x: 0, y: height)),