How to stop Xcode 11 from changing CFBundleVersion

2020-05-26 07:11发布

问题:

Since version 11, Xcode sets my CFBundleVersion value to $(CURRENT_PROJECT_VERSION) and my CFBundleShortVersionString to value $(MARKETING_VERSION) whenever I enter Version or Build values in the target settings (tab "General").

The actual version and build values that I enter are now stored in the project.pbxproj file. I do not want or like this behaviour, as I use shell scripts to modify the values at buildtime.

I can manually set the correct values in the Info.plist file, but as soon as I change Version or Build numbers in the target settings, the Info.plist file gets changed again by Xcode.

How do I stop Xcode 11 from doing this?

When I modify my build script to change the project file itself, Xcode will immediately cancel the build as soon as the project file is changed.

回答1:

The road so far

My use case was that:

  1. I'm synchronizing the version and build numbers across several targets.
  2. I'm synchronizing the version and build numbers with the target's Settigns.bundle
  3. I'm reading and modifying the the build number from a CI server.

I used to execute point 1 and 2 as a target build script and point 3 as a custom script on the CI itself.

The new way of storing the version and build within the Xcode build settings were causing issues with the scripts, because they were no longer able to effectively modify the values. At least reading was possible.

Unfortunately i was not able to discover a legit way of preventing Xcode from storing the version and build numbers into the project build settings, however i've managed to create a workaround.

It turns out that when a build or an archive is made, the value written in the Info.plist is used. This means that the value is substituted during build time, which does not allow us to modify it during the same build time.

I've also tried to modify the project using xcodeproj cli, however any changes to the project were causing any builds to stop, so this solution was not working.

Eventually, after a lot of different approaches that i tried, i've finally managed find a compromise that was not violating the Xcode's new behavior.

Short Answer:

As a target pre-action, a script is executed which writes the respective values to CFBundleShortVersionString and CFBundleVersion to the target's Info.plist

As a source of truth, i use the Xcode build settings to read the values of MARKETING_VERSION and CURRENT_PROJECT_VERSION of the desired target.

This way, when you modify the values from the project settings - upon the next build/archive - they will be written to the Info.plist, allowing any if your existing scripting logic to continue to work.

Detailed Answer

The only way to modify a resource upon a build action is using a pre-action script. If you try doing it from a build script - the changes will not take effect immediately and will not be present at the end of the build/archive.

In order to add a pre-build action - go to edit scheme.

Then expand the Build and Archive sections. Under Pre-action, click the Provide build and settings from dropdown and select the source of truth target from which you wish to read the values.

Add the following script:

# 1) 
cd ${PROJECT_DIR}

# 2) 
exec > Pruvit-Int.prebuild.sync_project_version_and_build_with_info_plists.log 2>&1

# 3) 
./sync_project_version_and_build_with_info_plists.sh $MARKETING_VERSION $CURRENT_PROJECT_VERSION

The scrip lines do the following:

  1. Go to the directory where the sync script is located in order to execute it
  2. Allows a log to be written during the pre-action, otherwise any output is silenced by default
  3. Execute the sync script by providing the MARKETING_VERSION and CURRENT_PROJECT_VERSION

The final step is to write your own sync script that reads the values of the provided MARKETING_VERSION and CURRENT_PROJECT_VERSION to the respective target/s and whenever else you want.

In my case the script is the following:

#!/bin/bash

#IMPORTANT - this script must run as pre-action of each target's Build and Archive actions

version_number=$1
build_number=$2

echo "version_number is $version_number"
echo "build_number is $build_number"

#update Pruvit/Info.plist
pruvitInfoPlist="Pruvit/Info.plist"
/usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $version_number" $pruvitInfoPlist
/usr/libexec/PlistBuddy -c "Set CFBundleVersion $build_number" $pruvitInfoPlist

#update Pruvit/Settings.bundle
settingsPlist="Pruvit/Settings.bundle/Root.plist"
/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:0:DefaultValue $version_number" $settingsPlist
/usr/libexec/PlistBuddy -c "Set PreferenceSpecifiers:1:DefaultValue $build_number" $settingsPlist

#update BadgeCounter/Info.plist
badgeCounterInfoPlist="BadgeCounter/Info.plist"
/usr/libexec/PlistBuddy -c "Set CFBundleShortVersionString $version_number" $badgeCounterInfoPlist
/usr/libexec/PlistBuddy -c "Set CFBundleVersion $build_number" $badgeCounterInfoPlist

I use shared Info.plist and Settings.bundle between both of my app targets, so i have to update this once.

Also i use a notification service extension BadgeCounter, which has to have the exact same version and build as the target into which it is embedded. So i update this as well.



回答2:

Don't.

Presumably there is a reason why this behavior changed. If later Xcode features build on this behavior, things get more and more "constructed" down the line.

Instead of trying to bend Xcode, change how the build script retrieves these values:

How to read current app version in Xcode 11 with script

If you need to manipulate the project.pbxproj file, it is a Next style plist that is well documented. You can use plistbuddy which is compatible with this old format. You can also use awk with more scripting if you have more complex manipulations.

If I understand your use case, you could write a script that gets the highest version numbers with awk and then updates all lower version numbers it can find in the file with sed.