7

I have a custom "Run Script" build phase which generates some source files depending on files contained in folder A. The build phase executes before the "Compile Sources" build phase. As source files are generated, and I want to avoid triggering a complete project rebuild everytime when running, I'd like to take advantage of the "Input Files" and "Output Files" section, as described in this blog post.

Unfortunately, the folder A is a folder and not a file. When I provide the path to folder A as "Input File", the build phase correctly does not get executed when nothing in the folder changes. The build phase also correctly gets executed when I rename some files. But when changing something in a file within folder A, the build phase is not executed.

I tried both providing the folder name without *, and also with * or ** in the end.

How can I have the build phase executed when any file in a given input folder has changed (added, removed, content modified)?.

13
  • What have xcassets got to do with source files (or source-source-files, to be precise)? xcassets are a method of bundling files with the app and have nothing to do with the build process. Commented Apr 5, 2016 at 11:54
  • I use objc-codegenutils because I despise stringly typed references in my code Commented Apr 5, 2016 at 11:59
  • Well I don't understand any of that (and don't want to), however you didn't answer my question. Why are these source-source-files part of xcassets? They can exist anywhere in the project folders and won't be part of the app bundle, so why use xcassets? Commented Apr 5, 2016 at 12:01
  • Also I use objc-assetgen from objc-codegenutils which I redacted from the question to be simpler Commented Apr 5, 2016 at 12:02
  • 4
    Can we please stop to discuss xcassets, this is not the essence of this question. I will edit the question. Commented Apr 6, 2016 at 6:18

3 Answers 3

5

This is the answer from a developer who can afford going beyond the Xcode's IDE capabilities.

As far as I got your question you have some IN stuff which you want to do some processing on it so that that processing results in some OUT stuff. Let's call this Subproject: something that contains IN, OUT and processing: IN -> OUT. And you want this processing to be incremental: do not repeat the same work if it is already done i.e. if OUT is there and IN was not modified do not perform processing IN -> OUT because result already exists.

For this kind of task I use Make (any other build system is fine, e.g. Ninja) which is exactly the tool which does this job: you describe what you need, describe what you have and the process of transformation, and it works and gives you incremental processing.

Instead of tinkering at Xcode's build phases in your Run Script phase you can just put cd Subproject; make and delegate everything related to your IN -> OUT processing to Make by writing proper Makefile. The only thing that will remain in Xcode is Run Script phase which performs this delegation of your build rules to Make.

I have created example that addresses your question and demonstrates the integration of Make-based subproject into Xcode project: Xcode and Make.

├── README.md
├── Subproject
│   ├── Generated-Code
│   │   ├── file1.out
│   │   ├── file2.out
│   │   └── file3.out
│   ├── Makefile
│   └── Source
│       ├── file1.in
│       ├── file2.in
│       └── file3.in
└── Xcode-and-Make
    ├── Xcode-and-Make
    │   └── main.m
    └── Xcode-and-Make.xcodeproj
        └── project.pbxproj

The custom stuff is isolated in Subproject folder. This folder is included to Xcode project: Generated-Code .out files are included to the project's main target, *.in files are not included, Makefile is also not included.

In project's Run Script phase there is only call to Make:

cd ../Subproject
make

All the Source/*.in -> Generated-Code/*.out processing is done in Makefile which is written like:

IN_PATH=./Source
IN_FILES=$(wildcard $(IN_PATH)/*.in)

OUT_PATH=./Generated-Code

OUT_FILES := $(patsubst %, $(OUT_PATH)/%, $(notdir $(IN_FILES)))
OUT_FILES := $(patsubst %.in, %.out, $(OUT_FILES))

default: generate

generate: $(OUT_FILES)

clean:
    rm -rf $(OUT_PATH)

$(OUT_PATH)/%.out: $(OUT_PATH) $(IN_PATH)/%.in
    cp -v $(IN_PATH)/$*.in $(OUT_PATH)/$*.out

$(OUT_PATH):
    mkdir -p $(OUT_PATH)
    

If you are new to Make all this notation can be confusing at first but after one day of reading tutorials about Make you'll have basic understanding of how Make works.

This Makefile is written to give you incremental processing: cp is just trivial demo operation which copies *.in to *.out - in real application it can be a compiler or some other tool.

When inside Subproject folder you write make for the first time you see:

Subproject$ make
mkdir -p ./Generated-Code
cp -v ./Source/file1.in ./Generated-Code/file1.out
./Source/file1.in -> ./Generated-Code/file1.out
cp -v ./Source/file2.in ./Generated-Code/file2.out
./Source/file2.in -> ./Generated-Code/file2.out
cp -v ./Source/file3.in ./Generated-Code/file3.out
./Source/file3.in -> ./Generated-Code/file3.out

But on the second run you get:

Subproject$ make
make: Nothing to be done for `default'.

that's because Make is smart to understand that you didn't perform any modifications of any of *.in files so to make Make perform its processing again you need to actually perform some change in *.in files and this is exactly what you original question is about.

I think the example project is the best showcase so feel free to decide if this drift away from Xcode's defaults makes you feel comfortable about it and feel free to ask if you have any further questions.

Disclaimer: I have been using Make for 2 years for (almost) everything that involves building something that Xcode cannot handle so I strongly recommend you to learn basics of Make: it can be very powerful tool for build scripts in which incremental processing is nice to have.

Sign up to request clarification or add additional context in comments.

3 Comments

Plus 1 for an interesting out-of-the-box approach
Your comment profiles and discriminates people into extreme categories, which comes across as condescending. We should not need to resort to such draconian measures as using MAKEFILES; Xcode should be a self-contained solution. If it does not meet some need, it doesn't mean the need is wrong, it just means either its feature set is insufficient or there is a bug. People should simply report this issue via FeedbackAssistant to Apple, and in the meantime, there are better options than implementing an unmaintainable MAKEFILE.
Scaly, I have removed the part of my answer which you commented on as something that "profiles and discriminates people". I definitely didn't intend that part to be read this way, so I hope now it is better formulated. For the technical part, one could still argue that having a Git-controlled Makefile can be more maintainable than doing the same within the Xcode project structure.
1
+50

We can use the cksum command to check whether files were changed or not and thus we will be able to run the diff command to check for the change.

So, first of all, we need to make the list of all the files that we want to checksum and put it into a file:

find directory_to_search_in -type f -name "*.storyboard" -or -name "*.xib" > $FILE_PATHS

Then we check if we have ever run this script, if so then we will have the file with initial checksums of the files we have found and as a next step - we create the new file of the current files' checksums and compare both of them for the change. In case they are not the same - we run the script.
If this file does not exist - the we will create it and run your script. Moreover we add a check at the very beginning to delete files that we use to store paths for files and current files' checksums in case they build was interrupted and script did not end properly, because we don't need this files to hang around, we need them only for the check itself. These files are going to be deleted.

The result looks like this:

if [ -f $FILE_PATHS ]; then
rm $FILE_PATHS
fi
if [ -f $rm $CURRENT_CHECKSUMS ]; then
rm $CURRENT_CHECKSUMS
fi


find App/Source/UI/ -type f -name "*.storyboard" -or -name "*.xib" > $FILE_PATHS

if [ ! -f $INITIAL_CHECKSUMS ]; then
while read file; do
cksum $file >> $INITIAL_CHECKSUMS
done < $FILE_PATHS

your_generate_script_function_call
else
while read file; do
cksum $file >> $CURRENT_CHECKSUMS
done < $FILE_PATHS
if ! diff $INITIAL_CHECKSUMS $CURRENT_CHECKSUMS > /dev/null ; then
your_generate_script_function_call
cat $CURRENT_CHECKSUMS > $INITIAL_CHECKSUMS
fi

rm $CURRENT_CHECKSUMS
fi

rm $FILE_PATHS

Comments

0

Xcode requires the input files to be within $SRCROOT directory, and the output files to be within $DERIVED_FILE_DIR directory, or else it will run the script on every build.

Sadly, you can't use a recursive path for the input files. (Please submit a FeedbackAssistant ticket to Apple to request this.)

You have to use an .xcfilelist, and keep it updated. In order not to have to update it manually, you would need some other script that runs during a pre-build scheme script, which checks whether each target's current .xcfilelist accurately reflects all the files you want the script to target. Obviously, this is a pretty annoying kind of solution if you have lots of schemes.

So I went with a workaround, namely:

  • specify $(SRCROOT)/$(INFOPLIST_FILE) as the sole input file
  • specify $(DERIVED_FILE_DIR)/X.plist as the sole output file
  • at the beginning of the script copy the input file to the output file
  • then perform the main work of the script

E.g.:

set -ex

if [ -f "$SCRIPT_OUTPUT_FILE_0" ]; then
    echo "$SCRIPT_OUTPUT_FILE_0 exists. This script should not even be running"
    exit 0
else
    cp -a $SCRIPT_INPUT_FILE_0 $SCRIPT_OUTPUT_FILE_0
fi

This ensures the script only runs on clean builds (i.e., the first build after you clean your build folder or erase DerivedData/YourWorkspace/Build).

Unfortunately this means the script will never run on incremental builds, even if you change some of the files it uses as input. However this was the best compromise I could find (so far). It seems much better than resorting to MAKEFILEs etc.

A better option might be to use a Build Rule.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.