4
votes

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)?.

2
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. - trojanfoe
I use objc-codegenutils because I despise stringly typed references in my code - fabb
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? - trojanfoe
Also I use objc-assetgen from objc-codegenutils which I redacted from the question to be simpler - fabb
Can we please stop to discuss xcassets, this is not the essence of this question. I will edit the question. - fabb

2 Answers

5
votes

There are two extremes: people who like using Xcode for everything and people who tend to avoid using Xcode except for work that can only be done via it. This is the answer from a developer who doesn't like all Xcode's non-IDE capabilities: I don't know the answer to your exact question and I don't even want to because I try avoid using Xcode for anything that goes beyond its default 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.

1
votes

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