Prepare v0.4 release and open source docs

This commit is contained in:
Akshay Kolli
2026-06-29 23:42:39 -07:00
parent 085d7a16dc
commit 504bd2d39a
58 changed files with 5076 additions and 923 deletions

View File

@@ -7,7 +7,9 @@ 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}"
SIZE_OPTIMIZED="${SIZE_OPTIMIZED:-0}"
STRIP_RELEASE="${STRIP_RELEASE:-1}"
ICON_MAX_SIZE="${ICON_MAX_SIZE:-1024}"
SIGNING_IDENTITY="${SIGNING_IDENTITY:-}"
ENTITLEMENTS_PATH="${ENTITLEMENTS_PATH:-}"
PROVISIONING_PROFILE="${PROVISIONING_PROFILE:-}"
@@ -20,19 +22,33 @@ else
ARCHS="${ARCHS:-}"
fi
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/$APP_NAME.app"
APP_DIR="${APP_DIR:-$DIST_DIR/$APP_NAME.app}"
CONTENTS_DIR="$APP_DIR/Contents"
MACOS_DIR="$CONTENTS_DIR/MacOS"
RESOURCES_DIR="$CONTENTS_DIR/Resources"
ICON_SOURCE="$ROOT_DIR/ihatepdf.png"
ICON_SOURCE="${ICON_SOURCE:-$ROOT_DIR/ihatepdf-profile-transparent.png}"
if [[ ! -f "$ICON_SOURCE" ]]; then
echo "Missing app icon source: $ICON_SOURCE" >&2
echo "Set ICON_SOURCE to the path of a transparent PNG icon (for example: $ROOT_DIR/ihatepdf-profile-transparent.png)." >&2
exit 1
fi
if ! sips -g hasAlpha "$ICON_SOURCE" 2>/dev/null | grep -q "hasAlpha: yes"; then
echo "App icon source must include an alpha channel for transparent rendering: $ICON_SOURCE" >&2
exit 1
fi
ICON_NAME="AppIcon"
DERIVED_ENTITLEMENTS_PATH=""
PROFILE_PLIST_PATH=""
NORMALIZED_ICON_SOURCE=""
cleanup() {
if [[ -n "$DERIVED_ENTITLEMENTS_PATH" ]]; then
rm -f "$DERIVED_ENTITLEMENTS_PATH"
fi
if [[ -n "$NORMALIZED_ICON_SOURCE" ]]; then
rm -f "$NORMALIZED_ICON_SOURCE"
fi
if [[ -n "$PROFILE_PLIST_PATH" ]]; then
rm -f "$PROFILE_PLIST_PATH"
fi
@@ -55,6 +71,13 @@ SWIFT_BUILD_ARGS=(-c "$CONFIGURATION")
for ARCH in $ARCHS; do
SWIFT_BUILD_ARGS+=(--arch "$ARCH")
done
if [[ "$CONFIGURATION" == "release" && "$SIZE_OPTIMIZED" == "1" ]]; then
SWIFT_BUILD_ARGS+=(
-Xswiftc -Osize
-Xswiftc -Xfrontend -Xswiftc -disable-reflection-metadata
-Xswiftc -Xfrontend -Xswiftc -remove-runtime-asserts
)
fi
BUILD_DIR="$(swift build "${SWIFT_BUILD_ARGS[@]}" --show-bin-path)"
swift build "${SWIFT_BUILD_ARGS[@]}"
@@ -64,9 +87,22 @@ 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"
if [[ "$SIZE_OPTIMIZED" == "1" ]]; then
strip -u -r "$MACOS_DIR/$EXECUTABLE_NAME"
else
strip -x "$MACOS_DIR/$EXECUTABLE_NAME"
fi
fi
NORMALIZED_ICON_SOURCE="$(mktemp /tmp/ihatepdf-appicon-XXXXXX.png)"
if ! sips -s format png "$ICON_SOURCE" --out "$NORMALIZED_ICON_SOURCE" >/dev/null; then
rm -f "$NORMALIZED_ICON_SOURCE"
NORMALIZED_ICON_SOURCE=""
echo "Failed to normalize icon source: $ICON_SOURCE" >&2
exit 1
fi
ICON_SOURCE="$NORMALIZED_ICON_SOURCE"
if [[ -n "$PROVISIONING_PROFILE" ]]; then
if [[ ! -f "$PROVISIONING_PROFILE" ]]; then
echo "Missing provisioning profile: $PROVISIONING_PROFILE" >&2
@@ -88,19 +124,31 @@ mkdir -p "$ICONSET_DIR"
make_icon() {
local pixels="$1"
local output="$2"
sips -s format png --resampleHeightWidth "$pixels" "$pixels" "$ICON_SOURCE" --out "$ICONSET_DIR/$output" >/dev/null
local output_path="$ICONSET_DIR/$output"
sips -s format png --resampleHeightWidth "$pixels" "$pixels" "$ICON_SOURCE" --out "$output_path" >/dev/null
}
make_icon 16 "icon_16x16.png"
make_icon 32 "icon_16x16@2x.png"
make_icon 32 "icon_32x32.png"
make_icon 64 "icon_32x32@2x.png"
make_icon 128 "icon_128x128.png"
make_icon 256 "icon_128x128@2x.png"
make_icon 256 "icon_256x256.png"
make_icon 512 "icon_256x256@2x.png"
make_icon 512 "icon_512x512.png"
make_icon 1024 "icon_512x512@2x.png"
if (( ICON_MAX_SIZE >= 128 )); then
make_icon 128 "icon_128x128.png"
fi
if (( ICON_MAX_SIZE >= 128 )); then
make_icon 256 "icon_128x128@2x.png"
fi
if (( ICON_MAX_SIZE >= 256 )); then
make_icon 256 "icon_256x256.png"
fi
if (( ICON_MAX_SIZE >= 512 )); then
make_icon 512 "icon_256x256@2x.png"
make_icon 512 "icon_512x512.png"
fi
if (( ICON_MAX_SIZE >= 1024 )); then
make_icon 1024 "icon_512x512@2x.png"
fi
iconutil -c icns "$ICONSET_DIR" -o "$RESOURCES_DIR/$ICON_NAME.icns"
rm -rf "$ICONSET_DIR"
@@ -156,7 +204,7 @@ cat > "$CONTENTS_DIR/Info.plist" <<PLIST
<key>LSSupportsOpeningDocumentsInPlace</key>
<true/>
<key>NSHumanReadableCopyright</key>
<string>MIT License</string>
<string>GNU General Public License version 2</string>
</dict>
</plist>
PLIST

View File

@@ -11,9 +11,16 @@ 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}"
STAGING_DIR=""
cleanup() {
if [[ -n "$STAGING_DIR" ]]; then
rm -rf "$STAGING_DIR"
fi
}
trap cleanup EXIT
require_value() {
local name="$1"
@@ -35,17 +42,20 @@ require_value "PROVISIONING_PROFILE" "$PROVISIONING_PROFILE" \
"Download an App Store provisioning profile for $BUNDLE_ID and pass its local path."
mkdir -p "$DIST_DIR"
rm -f "$PKG_PATH"
STAGING_DIR="$(mktemp -d "$DIST_DIR/appstore-pkg.XXXXXX")"
APP_DIR="$STAGING_DIR/$APP_NAME.app"
BUNDLE_ID="$BUNDLE_ID" \
APP_VERSION="$APP_VERSION" \
BUILD_NUMBER="$BUILD_NUMBER" \
APP_DIR="$APP_DIR" \
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" \
@@ -63,7 +73,7 @@ if [[ "$VALIDATE_WITH_ALTOOL" == "1" ]]; then
--type macos \
--file "$PKG_PATH" \
--username "$ASC_USERNAME" \
--password "$ASC_PASSWORD"
--password "@env:ASC_PASSWORD"
fi
echo "Created App Store package: $PKG_PATH"

View File

@@ -14,11 +14,19 @@ if [[ "$BUILD_APP" != "0" || ! -d "$APP_DIR" ]]; then
fi
rm -f "$DMG_PATH"
hdiutil create \
-volname "$APP_NAME" \
-srcfolder "$APP_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"
if diskutil image create from --help >/dev/null 2>&1; then
diskutil image create from \
--format UDZO \
--volumeName "$APP_NAME" \
"$APP_DIR" \
"$DMG_PATH"
else
hdiutil create \
-volname "$APP_NAME" \
-srcfolder "$APP_DIR" \
-ov \
-format UDZO \
"$DMG_PATH"
fi
echo "Created $DMG_PATH"

64
scripts/make-tiny-archives.sh Executable file
View File

@@ -0,0 +1,64 @@
#!/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"
DIST_DIR="$ROOT_DIR/dist"
STAGING_DIR="$DIST_DIR/tiny"
ARCHS_TO_BUILD="${ARCHS_TO_BUILD:-arm64 x86_64}"
if ! command -v xz >/dev/null 2>&1; then
echo "xz is required to build size-gated tiny archives with architecture filters." >&2
exit 1
fi
rm -rf "$STAGING_DIR"
mkdir -p "$STAGING_DIR"
compression_args_for_arch() {
local arch="$1"
if [[ -n "${XZ_OPT:-}" ]]; then
# Preserve explicit caller overrides.
echo "$XZ_OPT"
return
fi
case "$arch" in
arm64)
echo "--arm64 --lzma2=preset=9e"
;;
x86_64)
echo "--x86 --lzma2=preset=9e"
;;
*)
echo "--lzma2=preset=9e"
;;
esac
}
for ARCH in $ARCHS_TO_BUILD; do
APP_DIR="$STAGING_DIR/$ARCH/$APP_NAME.app"
ARCHIVE_PATH="$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-$ARCH.tar.xz"
rm -f "$ARCHIVE_PATH"
mkdir -p "$(dirname "$APP_DIR")"
ARCHS="$ARCH" \
SIZE_OPTIMIZED=1 \
ICON_MAX_SIZE="${ICON_MAX_SIZE:-32}" \
APP_VERSION="$APP_VERSION" \
BUILD_NUMBER="$BUILD_NUMBER" \
APP_DIR="$APP_DIR" \
"$ROOT_DIR/scripts/build-app.sh"
read -r -a XZ_ARGS <<< "$(compression_args_for_arch "$ARCH")"
COPYFILE_DISABLE=1 tar -C "$(dirname "$APP_DIR")" -cf - "$APP_NAME.app" \
| env XZ_OPT= xz "${XZ_ARGS[@]}" -c > "$ARCHIVE_PATH"
echo "Created $ARCHIVE_PATH"
done
"$ROOT_DIR/scripts/verify-release-size.sh"

View File

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

View File

@@ -0,0 +1,129 @@
#!/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}"
DIST_DIR="$ROOT_DIR/dist"
APP_DIR="$DIST_DIR/$APP_NAME.app"
DMG_PATH="$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos.dmg"
PKG_PATH="${PKG_PATH:-$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-appstore.pkg}"
REQUIRE_APP_STORE_PKG="${REQUIRE_APP_STORE_PKG:-0}"
PLISTBUDDY="/usr/libexec/PlistBuddy"
TEMP_PATHS=()
cleanup() {
((${#TEMP_PATHS[@]})) || return 0
for path in "${TEMP_PATHS[@]}"; do
rm -rf "$path"
done
}
trap cleanup EXIT
fail() {
echo "release artifact verification failed: $*" >&2
exit 1
}
require_file() {
local path="$1"
[[ -e "$path" ]] || fail "missing $path"
}
plist_value() {
local plist="$1"
local key="$2"
"$PLISTBUDDY" -c "Print :$key" "$plist"
}
verify_app_bundle() {
require_file "$APP_DIR/Contents/Info.plist"
local version
local build
local bundle_id
version="$(plist_value "$APP_DIR/Contents/Info.plist" "CFBundleShortVersionString")"
build="$(plist_value "$APP_DIR/Contents/Info.plist" "CFBundleVersion")"
bundle_id="$(plist_value "$APP_DIR/Contents/Info.plist" "CFBundleIdentifier")"
[[ "$bundle_id" == "$BUNDLE_ID" ]] || fail "$APP_DIR bundle id is $bundle_id, expected $BUNDLE_ID"
[[ "$version" == "$APP_VERSION" ]] || fail "$APP_DIR version is $version, expected $APP_VERSION"
[[ "$build" == "$BUILD_NUMBER" ]] || fail "$APP_DIR build is $build, expected $BUILD_NUMBER"
[[ ! -e "$APP_DIR/Contents/embedded.provisionprofile" ]] \
|| fail "$APP_DIR contains an embedded provisioning profile; direct DMG app should not"
}
verify_dmg() {
require_file "$DMG_PATH"
if command -v diskutil >/dev/null 2>&1 &&
diskutil image info "$DMG_PATH" 2>/dev/null | grep -q "Image Format: UDZO"; then
return
fi
if command -v hdiutil >/dev/null 2>&1 &&
hdiutil imageinfo "$DMG_PATH" 2>/dev/null | grep -q "Format: UDZO"; then
return
fi
fail "$DMG_PATH is not a compressed read-only UDZO image"
}
verify_pkg() {
require_file "$PKG_PATH"
pkgutil --check-signature "$PKG_PATH" >/dev/null
local expanded_parent
local expanded
expanded_parent="$(mktemp -d "$DIST_DIR/pkg-verify.XXXXXX")"
TEMP_PATHS+=("$expanded_parent")
expanded="$expanded_parent/expanded.pkg"
pkgutil --expand-full "$PKG_PATH" "$expanded" >/dev/null
local plist
plist="$(find "$expanded" -path "*/I Hate PDFs.app/Contents/Info.plist" -print -quit)"
[[ -n "$plist" ]] || fail "$PKG_PATH does not contain I Hate PDFs.app"
local app_contents
app_contents="$(dirname "$plist")"
local version
local build
local bundle_id
version="$(plist_value "$plist" "CFBundleShortVersionString")"
build="$(plist_value "$plist" "CFBundleVersion")"
bundle_id="$(plist_value "$plist" "CFBundleIdentifier")"
[[ "$bundle_id" == "$BUNDLE_ID" ]] || fail "$PKG_PATH app bundle id is $bundle_id, expected $BUNDLE_ID"
[[ "$version" == "$APP_VERSION" ]] || fail "$PKG_PATH app version is $version, expected $APP_VERSION"
[[ "$build" == "$BUILD_NUMBER" ]] || fail "$PKG_PATH app build is $build, expected $BUILD_NUMBER"
[[ -e "$app_contents/embedded.provisionprofile" ]] \
|| fail "$PKG_PATH app is missing embedded.provisionprofile"
local app_bundle
local entitlements
app_bundle="$(dirname "$app_contents")"
entitlements="$expanded/entitlements.plist"
codesign -d --entitlements :- "$app_bundle" > "$entitlements" 2>/dev/null \
|| fail "$PKG_PATH app entitlements could not be read"
[[ "$(plist_value "$entitlements" "com.apple.security.app-sandbox")" == "true" ]] \
|| fail "$PKG_PATH app is missing com.apple.security.app-sandbox"
[[ "$(plist_value "$entitlements" "com.apple.security.files.user-selected.read-write")" == "true" ]] \
|| fail "$PKG_PATH app is missing com.apple.security.files.user-selected.read-write"
rm -rf "$expanded_parent"
}
verify_app_bundle
verify_dmg
if [[ "$REQUIRE_APP_STORE_PKG" == "1" || -e "$PKG_PATH" ]]; then
verify_pkg
elif [[ "$REQUIRE_APP_STORE_PKG" == "0" ]]; then
echo "Skipping App Store pkg verification because $PKG_PATH does not exist."
else
fail "invalid REQUIRE_APP_STORE_PKG=$REQUIRE_APP_STORE_PKG"
fi
echo "Verified release artifacts for I Hate PDFs $APP_VERSION ($BUILD_NUMBER)."

40
scripts/verify-release-size.sh Executable file
View File

@@ -0,0 +1,40 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
source "$ROOT_DIR/scripts/release-version.sh"
DIST_DIR="${DIST_DIR:-$ROOT_DIR/dist}"
PER_ARCH_INSTALLER_MAX_BYTES="${PER_ARCH_INSTALLER_MAX_BYTES:-400000}"
PER_ARCH_INSTALLER_EXTENSION="${PER_ARCH_INSTALLER_EXTENSION:-tar.xz}"
fail() {
echo "release size verification failed: $*" >&2
exit 1
}
file_size() {
stat -f '%z' "$1"
}
verify_under_budget() {
local path="$1"
[[ -f "$path" ]] || fail "missing $path"
local bytes
bytes="$(file_size "$path")"
if (( bytes >= PER_ARCH_INSTALLER_MAX_BYTES )); then
fail "$path is $bytes bytes; per-architecture installer budget is < $PER_ARCH_INSTALLER_MAX_BYTES bytes"
fi
echo "OK: $path is $bytes bytes (< $PER_ARCH_INSTALLER_MAX_BYTES)."
}
if (( $# > 0 )); then
for artifact in "$@"; do
verify_under_budget "$artifact"
done
else
verify_under_budget "$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-arm64.$PER_ARCH_INSTALLER_EXTENSION"
verify_under_budget "$DIST_DIR/IHatePDFs-v$RELEASE_VERSION-macos-x86_64.$PER_ARCH_INSTALLER_EXTENSION"
fi