Fix empty CFBundleVersion app-extension install failure via committed xcconfig fallbacks

Co-authored-by: premnirmal <1255689+premnirmal@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-07-31 14:10:30 +00:00
committed by GitHub
co-authored by premnirmal
parent 8a74ad2428
commit 4330dc06e2
6 changed files with 94 additions and 34 deletions
+5 -4
View File
@@ -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
+3
View File
@@ -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
+11 -8
View File
@@ -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 \
+24
View File
@@ -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"
+22 -12
View File
@@ -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:
+29 -10
View File
@@ -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" <<EOF
// Generated by iosApp/version.sh — do not commit (git-ignored).
// Overrides the committed defaults in Version.xcconfig with the git-derived
// version. Re-run version.sh to refresh.
MARKETING_VERSION = $MARKETING_VERSION
CURRENT_PROJECT_VERSION = $CURRENT_PROJECT_VERSION
EOF
unset _version_script_dir _version_name _version_major _version_minor _version_patch _version_code
echo "iOS version from git: MARKETING_VERSION=$MARKETING_VERSION CURRENT_PROJECT_VERSION=$CURRENT_PROJECT_VERSION"
echo "Wrote $_version_xcconfig"
unset _version_script_dir _version_name _version_major _version_minor _version_patch _version_code _version_xcconfig