62

What I did:

I have a script that

  1. Read some configuration files to generate source code snippets
  2. Find relevant Objective-C source files and
  3. Replace some portions of the source code with the generated code in step 1.

and a Makefile that has a special timestamp file as a make target and the configuration files as target sources:

SRC = $(shell find ../config -iname "*.txt")
STAMP = $(PROJECT_TEMP_DIR)/$(CONFIGURATION)$(EFFECTIVE_PLATFORM_NAME).stamp
$(STAMP): $(SRC)
    python inject.py
    touch $(STAMP)

I added this Makefile as a "Run Script Build Phase" on top of the stack of build phases for the project target.

What happened:

The script build phase was run before compiling the source.

However, since the script modifies source code during its execution, I needed to build twice to get the most recent version of the build product. Here is what I imagine to be happening:

  1. 1st run: Xcode collects dependency information ---> no changes
  2. 1st run: Xcode runs "Run Script Build Phase" ---> source is changed behind Xcode's back
  3. 1st run: Xcode finishes build, thinking nothing needs to be updated
  4. 2nd run: Xcode collects dependency information ---> source has changed, needs rebuild!
  5. 2nd run: Xcode runs Run Script Build Phase" ---> everything is up-to-date
  6. 2nd run: Xcode proceeds to compilation

After reading Xcode documentation on Build Phases, I tried adding a source file which is known to be updated every time the script is run as the output of "Run Script Build Phases", but nothing changed. Since the number of configuration files may vary in my project, I don't want to specify every input and output file.

Question:

How do I make Xcode aware of source file changes made during "Run Script Build Phase"?

Edit:

  • Added that I placed the script build phase before the other build phases
user7116
  • 63,008
  • 17
  • 141
  • 172
ento
  • 5,801
  • 6
  • 51
  • 69
  • 3
    Follow up question: Is it possible to have the code rewritten just for the compiler w/o updating the actual file? – Hari Honor Sep 28 '13 at 14:27

7 Answers7

104

Every technique mentioned so far is an overkill. Reproducing steve kim's comment for visibility:

In the build phases tab, simply drag the "Run Script" step to a higher location (e.g. before "Compile Sources").

Tested on Xcode 6

shim
  • 9,289
  • 12
  • 69
  • 108
marinosb
  • 5,367
  • 2
  • 18
  • 10
  • 2
    This works well for Xcode 7. One thing to note is that I was not able to drag the phase up. I had to drag *down* the others. – Tokuriku Nov 22 '15 at 01:37
  • 1
    I was struggling with this for over an hour not getting why my build fails each time. thank you - works for xcode 6 as well. – rcat24 Dec 01 '15 at 08:42
  • 3
    @Tokuriku You can drag up, but if you try to drag on or past "Target Dependencies" it won't work. You need to drag just below it. – Rick Smith Feb 18 '16 at 18:07
  • 5
    Depending on what files you're trying to change, this solution might not work. For example, if you're updating an xcconfig file or an Info.plist file, those seems to be already consumed before any of the build phases are run, so changes that happen inside a script don't show up in the build. – plowman Mar 13 '19 at 00:04
  • 1
    Source file modifications in Xcode 11 will not work until the second run. – xi.lin Jul 20 '20 at 06:58
  • Perfect solution. – Michael Mar 25 '21 at 03:04
  • My build script was already before my "Compile Sources" build phase. I had to add the path of the generated source file to the "Output Files" section to explicitly note the dependencies. – Andrew Kirna Sep 28 '21 at 17:05
  • That doesn't work if you have Swift Packages – Grzegorz Krukowski Feb 15 '22 at 17:30
31

This solution is probably outdated. See the higher voted answer instead; I no longer actively use Xcode and am not qualified to vet a solution.


Use "External Target":

  1. Select "Project" > "New Target..." from the menu
  2. Select "Mac OS X" > "Other" > "External Target" and add it to your project
  3. Open its settings and fill in your script setup
  4. Open the "General" tab of the main target's settings and add the new target as it's direct dependency

Now the new "External Target" runs before the main target even starts gathering dependency information, so that any changes made during the script execution should be included in the build.

ento
  • 5,801
  • 6
  • 51
  • 69
  • You can also add a "Run Script" build phase from within a single target, placing that script build phase before the other target build phases. – Barry Wark Jun 10 '09 at 16:36
  • @Barry I did place the script build phase on top of the build phase stack, but I still needed to build twice. I'm editing it in to the question. Thanks. – ento Jun 11 '09 at 01:07
  • @ento Is it possible to pass build settings to your script this way? If so, I can't figure out how. The external target has a "Build Settings" section, but I want my build script to use build settings of the "outer target" (the one where the external target is embedded in). I can't see a way how, and documentation on this issue seems to be non-existent. – zmippie May 04 '11 at 12:57
  • @Abizern I think you're missing the point of Stack Overflow. Why discourage someone from adding a valid answer. Who cares if it was his own question? – Undistraction Jun 13 '13 at 13:29
  • 2
    You can drag and drop a "Run Script" before the build phase. – steve kim Jun 01 '14 at 10:07
  • This is genius! It's very useful for running scripts before Cocoapods compile (which take a long time, specially when archiving). Used 'Aggregate target for iOS build' as @respectTheCode suggested (works great on Xcode 5). – Ricardo Sanchez-Saez Aug 06 '14 at 22:47
  • Update: this solution does not appear to work anymore. – Dan Loewenherz Jul 16 '16 at 14:24
  • @Dan Does http://stackoverflow.com/a/26389328/20226 work for you? If so, I'll accept that one instead. – ento Jul 16 '16 at 21:46
  • Actually, I'm going ahead with accepting that one: trust the votes – ento Jul 16 '16 at 21:47
  • I would just like to comment that as of 2020 and XCode 11, this is still the case. In my case it was modifying the CFBundleVersion in an Info.plist file with epoch time of the build for both the app and a Notification Service Extension. Putting it in build scripts and even putting it before compile sources does absolutely nothing because I'm pretty sure XCode caches the files before the build even runs. The only way I could get this fixed was using this method. – Zane May 08 '20 at 17:19
4

There is another, slightly simpler option that doesn't require a separate target, but it's only viable if your script tends to modify the same source files every time.

First, here's a brief explanation for anyone who's confused about why Xcode sometimes requires you to build twice (or do a clean build) to see certain changes reflected in your target app. Xcode compiles a source file if the object file it produces is missing, or if the object file's last-modified date is earlier than the source file's last-modified date was at the beginning of the first build phase. If your project runs a script that modifies a source file in a pre-compilation build phase, Xcode won't notice that the source file's last-modified date has changed, so it won't bother to recompile it. It's only when you build the project a second time that Xcode will notice the date change and recompile the file.

Here's a simple solution if your script happens to modify the same source files every time. Just add a Run Script build phase at the end of your build process like this:

touch Classes/FirstModifiedFile.m Classes/SecondModifiedFile.m
exit $?

Running touch on these source files at the end of your build process guarantees that they will always have a later last-modified date than their object files, so Xcode will recompile them every time.

cduhn
  • 17,818
  • 4
  • 49
  • 65
  • Can you explain what exactly "add a Run Script build phase at the end of your build process" means? Thank you – user1244109 Dec 13 '13 at 13:33
  • This should help: https://developer.apple.com/library/ios/recipes/xcode_help-project_editor/Articles/AddingaRunScriptBuildPhase.html – cduhn Mar 20 '14 at 00:53
2

I as well struggled with this for a long time. The answer is to use ento's "External Target" solution. He is WHY this problem occurs and how we use it in practice...

Xcode 4 build steps do not execute until AFTER the plist has been compiled. This is silly, of course, because it means that any pre-build steps that modify the plist won't take effect. But if you think about it, they actually DO take effect...on the NEXT build. That's why some people have talked about "caching" of plist values or "I have to do 2 builds to make it work." What happens is the plist is built, then your script runs. Next time you build, the plist builds using your modified files, hence the second build.

ento's solution is the one way I've found to actually do a true pre-build step. Unfortunately I also found that it does not cause the plist to update without a clean build and I fixed that. Here is how we have data-driven user values in the plist:

  1. Add an External Build System project that points to a python script and passes some arguments
  2. Add user-defined build settings to the build. These are the arguments that you pass to python (more on why we do this later)
  3. The python script reads some input JSON files and builds a plist preprocessor header file AND touches the main app plist
  4. The main project has "preprocess plist files" turned on and points to this preprocessor file

Using touch on the main app plist file causes the main target to generate the plist every time. The reason we pass in build settings as parameters is so our command-line build can override settings:

  1. Add a user-defined variable "foo" to the prebuild project.
  2. In your prebuild you can use $(foo) to pass the value into the python script.
  3. On the command-line you can add foo=test to pass in a new value.

The python script uses base settings files and allows for user-defined settings files to override the defaults. You make a change and immediately it ends up in the plist. We only use this for settings that MUST be in the plist. For anything else it's a waste of effort....generate a json file or something similar instead and load it at run-time :)

I hope this helps...it's been a couple rough days figuring this out.

shim
  • 9,289
  • 12
  • 69
  • 108
mstelzer
  • 29
  • 1
2

As of Xcode 4, it looks like if you add the generated files to the output section of the build phase, it will respect that setting, and not generate the ... has been modified since the precompiled header was built error messages.

This is a good option if your script is only generating a handful of files each time.

Senseful
  • 86,719
  • 67
  • 308
  • 465
1

The External Target solution from @ento no longer works as of Xcode 11.5. The solution is to add all files that will be changed under Output Files in the Run Script.

rocky
  • 3,521
  • 1
  • 23
  • 31
  • Instead of External Target, select to use a Static Library instead and you can do the same thing. – Zane Oct 07 '20 at 18:09
0

Another option is to create a subproject framework with your scripts and just add it as a dependency to all targets. The phase scripts of this subproject should now be executed before all targets.

enter image description here

Roman Filippov
  • 2,289
  • 2
  • 11
  • 17