From 4330dc06e286d319bf2d9180c538176534d942e1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 31 Jul 2026 14:10:30 +0000 Subject: [PATCH] Fix empty CFBundleVersion app-extension install failure via committed xcconfig fallbacks Co-authored-by: premnirmal <1255689+premnirmal@users.noreply.github.com> --- .github/workflows/ios.yml | 9 +++++---- .gitignore | 3 +++ iosApp/README.md | 19 +++++++++++-------- iosApp/Version.xcconfig | 24 ++++++++++++++++++++++++ iosApp/project.yml | 34 ++++++++++++++++++++++------------ iosApp/version.sh | 39 +++++++++++++++++++++++++++++---------- 6 files changed, 94 insertions(+), 34 deletions(-) create mode 100644 iosApp/Version.xcconfig diff --git a/.github/workflows/ios.yml b/.github/workflows/ios.yml index 54946b12..742746a3 100644 --- a/.github/workflows/ios.yml +++ b/.github/workflows/ios.yml @@ -62,12 +62,13 @@ jobs: run: brew install xcodegen - name: Generate Xcode project working-directory: iosApp - # Source version.sh first so XcodeGen can expand ${MARKETING_VERSION} / - # ${CURRENT_PROJECT_VERSION} in project.yml from the latest git tag - # (mirroring the Android version derivation). fetch-depth: 0 above brings + # Run version.sh first so the git-derived MARKETING_VERSION / + # CURRENT_PROJECT_VERSION are written to Version.local.xcconfig (included + # by the committed Version.xcconfig) and stamped into the bundles, + # mirroring the Android version derivation. fetch-depth: 0 above brings # the tags this needs. run: | - source ./version.sh + ./version.sh xcodegen generate - name: Install iOS platform # The macos-26 image ships the iOS 26 SDK but not the iOS 26 platform diff --git a/.gitignore b/.gitignore index 2af5a53d..3035054b 100644 --- a/.gitignore +++ b/.gitignore @@ -31,6 +31,9 @@ DerivedData/ .swiftpm/ # Local, machine-specific JAVA_HOME override for the iOS Gradle build phase iosApp/.xcode.env.local +# Local, git-derived version override written by iosApp/version.sh (optionally +# #included by the committed iosApp/Version.xcconfig) +iosApp/Version.local.xcconfig #iOS Firebase config (optional, prod only) iosApp/GoogleService-Info.plist iosApp/iosApp/GoogleService-Info.plist diff --git a/iosApp/README.md b/iosApp/README.md index 37149922..f3267500 100644 --- a/iosApp/README.md +++ b/iosApp/README.md @@ -88,16 +88,19 @@ On a Mac: 2. Generate the project: ```sh cd iosApp - source ./version.sh # derives the version from the latest git tag + ./version.sh # stamps the version from the latest git tag xcodegen generate # produces iosApp/StockTicker.xcodeproj ``` - `version.sh` exports `MARKETING_VERSION` (`CFBundleShortVersionString`) and - `CURRENT_PROJECT_VERSION` (`CFBundleVersion`) from `git describe --tags`, - mirroring how the Android app derives its `versionName` / `versionCode` in - `app/build.gradle.kts` (falling back to `1.0` / `1` when no tag is - reachable). `project.yml` references those env vars, so source it before - generating — otherwise the bundle version is empty and an app-extension - install fails. + `version.sh` writes `MARKETING_VERSION` (`CFBundleShortVersionString`) and + `CURRENT_PROJECT_VERSION` (`CFBundleVersion`) into the git-ignored + `Version.local.xcconfig` from `git describe --tags`, mirroring how the Android + app derives its `versionName` / `versionCode` in `app/build.gradle.kts` + (falling back to `1.0` / `1` when no tag is reachable). The committed + `Version.xcconfig` (read by every target and optionally including + `Version.local.xcconfig`) already ships non-empty `1.0` / `1` fallbacks, so + even if you skip `version.sh` the bundle version is never empty and the + app-extension install still succeeds — running `version.sh` just stamps the + real git-derived version for TestFlight/App Store archives. 3. Open `iosApp/StockTicker.xcodeproj` and run, or build from the command line: ```sh xcodebuild build \ diff --git a/iosApp/Version.xcconfig b/iosApp/Version.xcconfig new file mode 100644 index 00000000..e9a2fac2 --- /dev/null +++ b/iosApp/Version.xcconfig @@ -0,0 +1,24 @@ +// App/extension version, consumed by every target's Info.plist via +// CFBundleShortVersionString ($(MARKETING_VERSION)) and CFBundleVersion +// ($(CURRENT_PROJECT_VERSION)). +// +// These committed values are deliberately NON-EMPTY fallbacks: an app extension +// whose CFBundleVersion resolves to an empty string fails to install on the +// simulator/device with +// "bundleVersion must be set in placeholder attributes for an app extension +// placeholder" (IXErrorDomain, code 17). +// Because they live in this xcconfig (read by Xcode at build time) rather than +// being substituted into the generated project at `xcodegen generate` time, +// the build is always installable even if you forget to run `version.sh`. +// +// To stamp the real, git-derived version (mirroring the Android app), run +// `iosApp/version.sh`. It writes `iosApp/Version.local.xcconfig` (git-ignored) +// with the values from `git describe --tags`, which the optional include below +// layers on top of these defaults — so a proper version is used for +// TestFlight/App Store archives without dirtying this committed file. +MARKETING_VERSION = 1.0 +CURRENT_PROJECT_VERSION = 1 + +// Optional machine-local override written by version.sh (git-ignored). The `?` +// makes the include optional, so builds still succeed when it is absent. +#include? "Version.local.xcconfig" diff --git a/iosApp/project.yml b/iosApp/project.yml index 35b5c522..1507441b 100644 --- a/iosApp/project.yml +++ b/iosApp/project.yml @@ -18,6 +18,20 @@ options: iOS: "17.0" createIntermediateGroups: true +# App/extension version, derived from the latest git tag (mirroring the Android +# app — see app/build.gradle.kts). Version.xcconfig sets CFBundleShortVersionString +# ($(MARKETING_VERSION)) and CFBundleVersion ($(CURRENT_PROJECT_VERSION)) for +# every target, with committed non-empty fallbacks (1.0 / 1). It optionally +# #includes Version.local.xcconfig, which `iosApp/version.sh` writes with the +# git-derived values. Resolving these in an xcconfig (read by Xcode at build +# time) — rather than substituting `${...}` at `xcodegen generate` time — means +# the bundle version is NEVER empty even if you forget to run version.sh, so an +# app extension no longer fails to install with "bundleVersion must be set in +# placeholder attributes for an app extension placeholder" (IXErrorDomain). +configFiles: + Debug: Version.xcconfig + Release: Version.xcconfig + settings: base: SWIFT_VERSION: "5.0" @@ -27,18 +41,14 @@ settings: # Kotlin crash frames. Debug builds default to plain `dwarf` (no dSYM); forcing dwarf-with-dsym # here ensures symbolication works for Release/Archive (TestFlight/App Store) builds. DEBUG_INFORMATION_FORMAT: "dwarf-with-dsym" - # App/extension version, derived from the latest git tag (mirroring the - # Android app — see app/build.gradle.kts). These back - # CFBundleShortVersionString ($(MARKETING_VERSION)) and CFBundleVersion - # ($(CURRENT_PROJECT_VERSION)) in every target's Info.plist. They MUST be - # non-empty: an app extension whose CFBundleVersion resolves to an empty - # string fails to install with "bundleVersion must be set in placeholder - # attributes for an app extension placeholder" (IXErrorDomain). XcodeGen - # expands the ${...} references below from the environment, so source - # `iosApp/version.sh` before `xcodegen generate` (it derives the values from - # `git describe --tags` and falls back to 1.0 / 1 when no tag is reachable). - MARKETING_VERSION: "${MARKETING_VERSION}" - CURRENT_PROJECT_VERSION: "${CURRENT_PROJECT_VERSION}" + # App/extension version comes from Version.xcconfig (see the top-level + # `configFiles` above): CFBundleShortVersionString ($(MARKETING_VERSION)) + # and CFBundleVersion ($(CURRENT_PROJECT_VERSION)). Those MUST be non-empty: + # an app extension whose CFBundleVersion resolves to an empty string fails to + # install with "bundleVersion must be set in placeholder attributes for an + # app extension placeholder" (IXErrorDomain). Version.xcconfig ships + # non-empty fallbacks and optionally includes Version.local.xcconfig (written + # by iosApp/version.sh) for the git-derived version. # The shared framework is a static Kotlin/Native framework, so it only has # to be on the linker search path — it is not embedded. FRAMEWORK_SEARCH_PATHS: diff --git a/iosApp/version.sh b/iosApp/version.sh index 306e624d..06f36cea 100755 --- a/iosApp/version.sh +++ b/iosApp/version.sh @@ -4,21 +4,29 @@ # (app/build.gradle.kts derives versionName/versionCode from # `git describe --tags --abbrev=0`). # -# Source this script before generating the Xcode project so XcodeGen can expand -# the `${MARKETING_VERSION}` / `${CURRENT_PROJECT_VERSION}` references in -# project.yml: +# Run this before building/generating so the git-derived version is stamped into +# the app + widget bundles: # # cd iosApp -# source ./version.sh +# ./version.sh # writes iosApp/Version.local.xcconfig # xcodegen generate # -# It exports two environment variables: +# It writes two Xcode build settings into `iosApp/Version.local.xcconfig` +# (git-ignored): # MARKETING_VERSION -> CFBundleShortVersionString (the git tag, e.g. 4.0.0) # CURRENT_PROJECT_VERSION -> CFBundleVersion (integer build number) # -# Both are guaranteed non-empty (falling back to 1.0 / 1 when no tag is -# reachable) because an app extension whose CFBundleVersion resolves to an empty -# string fails to install. +# `Version.xcconfig` (committed, read by every target) optionally #includes this +# file, so these values override the committed 1.0 / 1 fallbacks when present. +# Unlike the previous `${...}` XcodeGen env-substitution approach, forgetting to +# run this script no longer produces an EMPTY CFBundleVersion (which makes the +# app-extension install fail with "bundleVersion must be set in placeholder +# attributes for an app extension placeholder") — the committed fallbacks keep +# the build installable. +# +# The values are guaranteed non-empty (falling back to 1.0 / 1 when no tag is +# reachable). This script can be `source`d or executed; it exports the same two +# variables too, for backward compatibility with tooling that reads them. # Resolve this script's directory so it works regardless of the current # working directory (and whether sourced or executed). @@ -43,6 +51,17 @@ _version_code=$(( _version_major * 100000000 + _version_minor * 100000 + _versio export MARKETING_VERSION="$_version_name" export CURRENT_PROJECT_VERSION="$_version_code" -echo "iOS version from git: MARKETING_VERSION=$MARKETING_VERSION CURRENT_PROJECT_VERSION=$CURRENT_PROJECT_VERSION" +# Write the git-ignored local override that Version.xcconfig optionally includes. +_version_xcconfig="$_version_script_dir/Version.local.xcconfig" +cat > "$_version_xcconfig" <