diff --git a/CLAUDE.md b/CLAUDE.md index c74a0f1..bedf7d9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,7 +58,7 @@ All in `Shared/Model/`: ### Platforms, Tooling & Entitlements - iOS 26 / watchOS 26 / macOS 26; Swift 6 with `SWIFT_STRICT_CONCURRENCY: complete` on all targets. -- Build number (git commit count), `BuildDate`, and `BuildHash` are stamped by the `app-versioning` skill's `update_build_info.sh` (referenced in place from `../indie-skills`, a post-build phase; release tagging is opt-in via `RELEASE_TAGGING=1`). `Scripts/` holds the App Store / TestFlight release pipeline. +- Build number, `BuildDate`, and `BuildHash` are stamped by the vendored `Scripts/update_build_info.sh` (a post-build phase; release tagging is opt-in via `RELEASE_TAGGING=1`). Build numbers are **platform-partitioned** from the git commit count — iOS/watchOS get even (2×commits), macOS gets odd (2×commits+1) — because both platforms share one ASC build-number space and the embedded watch app must match the iOS app's `CFBundleVersion`. `Scripts/` also holds the App Store / TestFlight release pipeline. - Entitlements: iOS uses CloudDocuments + HealthKit; watch uses HealthKit only; macOS uses app sandbox + CloudDocuments (same bundle ID as iOS — universal purchase). **No CloudKit service, no App Group.** - SPM packages: `IndieSync` (file-side iCloud sync core) and `IndieAbout` (in-app About / changelog / license UI). diff --git a/Scripts/update_build_info.sh b/Scripts/update_build_info.sh new file mode 100755 index 0000000..0a75d3a --- /dev/null +++ b/Scripts/update_build_info.sh @@ -0,0 +1,92 @@ +#!/bin/sh + +## app-versioning: platform-partitioned Build Info Script (vendored for Workouts) +## +## A vendored variant of the shared app-versioning script (see +## ../indie-skills/skills/app-versioning/scripts/update_build_info.sh). Workouts' +## iOS app and Mac app share ONE App Store Connect record (same bundle id, +## dev.rzen.indie.Workouts — universal purchase), and therefore one build-number +## sequence: stamping both with the raw commit count would make the second +## upload collide ("bundle version already used"). Partition the space instead: +## - iOS + watchOS -> EVEN build numbers (commit-count * 2) +## - macOS -> ODD build numbers (commit-count * 2 + 1) +## watchOS is grouped with iOS (not given its own parity) because the embedded +## watch app's CFBundleVersion must match its host iOS app's exactly. The +## platform is read from PLATFORM_NAME, so this one script serves every +## code-bearing target (iOS app, watch app, watch widget, iOS widget, Mac app) +## via a single postBuildScripts anchor — no per-target parameters needed. +## +## Sets, on every build, in both the target Info.plist and its dSYM: +## - CFBundleVersion = commit-count*2 (iOS/watchOS) or commit-count*2+1 (macOS) +## - BuildDate = today's date (for IndieAbout's "Built …" line) +## - BuildHash = short git commit hash +## +## Wire as a POST-BUILD Run Script phase with these Input Files: +## $(TARGET_BUILD_DIR)/$(INFOPLIST_PATH) +## $(DWARF_DSYM_FOLDER_PATH)/$(DWARF_DSYM_FILE_NAME)/Contents/Info.plist +## +## Release tagging is opt-in, not automatic. Set RELEASE_TAGGING=1 in the +## build environment (e.g. `RELEASE_TAGGING=1 xcodebuild ...`) to have this +## script run `git tag `. Default builds never tag. +set -eu + +git=$(sh /etc/profile; which git || true) +if [ -z "$git" ] || [ ! -x "$git" ]; then + echo "error: update_build_info.sh: could not resolve a 'git' executable — refusing to stamp a build number." >&2 + exit 1 +fi + +number_of_commits=$("$git" rev-list HEAD --count) +case "$number_of_commits" in + ''|*[!0-9]*) + echo "error: update_build_info.sh: 'git rev-list HEAD --count' returned '$number_of_commits', not a positive integer — refusing to stamp a build number." >&2 + exit 1 + ;; +esac +if [ "$number_of_commits" -le 0 ]; then + echo "error: update_build_info.sh: 'git rev-list HEAD --count' returned $number_of_commits — refusing to stamp a build number." >&2 + exit 1 +fi + +git_commit=$("$git" rev-parse --short HEAD) + +# Platform-aware build number (see header): iOS/watchOS even, macOS odd. +case "${PLATFORM_NAME:-}" in + macosx*) build_number=$((number_of_commits * 2 + 1)) ;; + *) build_number=$((number_of_commits * 2)) ;; +esac + +target_plist="$TARGET_BUILD_DIR/$INFOPLIST_PATH" +dsym_plist="$DWARF_DSYM_FOLDER_PATH/$DWARF_DSYM_FILE_NAME/Contents/Info.plist" + +bundle_version=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$target_plist" 2>/dev/null || true) +build_date=$(date +%F) + +echo "app-versioning: v$bundle_version-$git_commit b$build_number (${PLATFORM_NAME:-}) $build_date" + +if [ "${RELEASE_TAGGING:-}" = "1" ]; then + if [ -n "$bundle_version" ]; then + "$git" tag "$bundle_version" + echo "app-versioning: tagged release $bundle_version" + else + echo "warning: app-versioning: RELEASE_TAGGING=1 but CFBundleShortVersionString could not be read from $target_plist; skipping git tag" + fi +fi + +if [ ! -f "$target_plist" ] && [ ! -f "$dsym_plist" ]; then + echo "warning: update_build_info.sh: neither the target Info.plist ($target_plist) nor the dSYM Info.plist ($dsym_plist) exists — check that both are wired as Input Files on this Run Script build phase" +fi + +for plist in "$target_plist" "$dsym_plist"; do + if [ -f "$plist" ]; then + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $build_number" "$plist" + + /usr/libexec/PlistBuddy -c "Set :BuildDate $build_date" "$plist" 2>/dev/null || \ + /usr/libexec/PlistBuddy -c "Add :BuildDate string $build_date" "$plist" + + /usr/libexec/PlistBuddy -c "Set :BuildHash $git_commit" "$plist" 2>/dev/null || \ + /usr/libexec/PlistBuddy -c "Add :BuildHash string $git_commit" "$plist" + + echo "app-versioning: updated $plist" + fi +done diff --git a/project.yml b/project.yml index d7a5fe9..c07d15c 100644 --- a/project.yml +++ b/project.yml @@ -28,12 +28,19 @@ packages: url: https://git.rzen.dev/rzen/indie-backup.git from: "2.0.0" -# Shared post-build phase — stamps CFBundleVersion (git commit count), BuildDate, and -# BuildHash into each target's (and dSYM's) Info.plist. Aliased into every code-bearing -# target below so the app, watch app, and both widget extensions stamp identical metadata. +# Shared post-build phase — stamps CFBundleVersion, BuildDate, and BuildHash into each +# target's (and dSYM's) Info.plist. Vendored at Scripts/update_build_info.sh (not the +# shared indie-skills copy) because the iOS app and Workouts Mac share ONE App Store +# Connect record (dev.rzen.indie.Workouts, universal purchase), so both stamping the +# raw commit count would collide on upload. The script partitions the space instead: +# iOS + watchOS targets get an EVEN CFBundleVersion (commit-count*2), the Mac target +# gets ODD (commit-count*2+1) — derived from PLATFORM_NAME inside the script, so this +# one anchor still serves all five code-bearing targets unmodified. watchOS is grouped +# with iOS (not given its own parity) because the embedded watch app's CFBundleVersion +# must match its host iOS app's exactly. scriptAnchors: updateBuildInfo: &updateBuildInfo - - script: '"${SRCROOT}/../indie-skills/skills/app-versioning/scripts/update_build_info.sh"' + - script: '"${SRCROOT}/Scripts/update_build_info.sh"' name: Update Build Info shell: /bin/sh basedOnDependencyAnalysis: false