When I moved the default CSS rules into a separate file I was facing the old dilemma: how can I embed this in the static library but still be able to easily add contents to it via Xcode. I previously explained how you can turn any file into a c-Byte-array. But this still required manual work.
I did a bit of researching and found that on regular Linux systems people seem to have a tool named objcopy which can copy files as their are into an object file (.o) which can be linked together to the final binary by the linker. But unfortunately this tool does not come with Xcode. So it is out of the question because I want everybody to be able to build DTCoreText.
Xcode Build Rules come to the rescue. They can automate any kind of preprocessing you like and it turns out they are an easy solution for this very problem.
There are essentially three kinds of source file that Xcode knows how to deal with (greatly simplified):
- c, c++ or Objective-C source code, or any code that can be somehow compiled
- header files
- other files
“Other files”, if you add them to a target, are treated as resources that simply get copied into your app bundle.
And now I’ll teach you how you can make any kind of file compilable. Be it some textures that you are compressing for building your game, or be it some resource that you want to embed into your binary.
Make a Build Rule
When Xcode is asked to compile a file it has a set of rules how it does that. So what we are going to do is to add our own rule how a css file is turned into a .c file. And for .c files are something that Xcode has a built-in rule for. So it can happen that you have two or more daisy chained rules that hand off their results to each other.
It is somewhat inconvenient that you cannot define rules globally. Instead you have to repeat your custom rules for each target.
To add your own rule, go into the projects info where you see your targets and on the target you want the rule for add it like shown.
This rule matches all files that end with .css (asterisk is a wildcard) and executes a custom script:
cd "$INPUT_FILE_DIR" # move into file dir, otherwise xxd takes the full path for the symbol /usr/bin/xxd -i "$INPUT_FILE_NAME" "$DERIVED_SOURCES_DIR/$INPUT_FILE_BASE.css.c" # builds a c file with a hex array |
I’m moving into the folder of the input file first because xxd will always take the full passed file path and turn it into the name of the c-array. This way it will just be named default_c and the length will be in default_c_len.
The above rule is all it takes to teach Xcode how to turn a .css file into a .c file. Now the next step is to make sure that we are actually compiling the default.css file as opposed to copying it together with the other resources.
Compile not Copy
You can see that default.css is compiled by having it in the Compile Sources section of the Build Phases.
If you build the project you see two hints about what happens now. Towards the top of the build log you see the preprocessing of the file. A little further down you see how the intermediate c file is being compiled.
Further down the library tool puts all object files into the library archive. If you don’t believe me, you can employ the nm tool to inspect the symbols in the final product.
nm DemoApp | grep default_c 00045474 D _default_css 00045fe0 D _default_css_len |
These are the two symbols from the default.css.c file that made it into the final product.
This guide wouldn’t be complete if I didn’t show you how to get to these symbols.
Accessing the Objectified File
I told you above that xxd uses the passed file name to make two variables, one for the c-style array, one for the length. Only change is that it has to turn the file name characters into a legal symbol, so dots are turned into underscores.
In DTCSSStylesheet.m I am accessing this array like so. I define these two variables as external which prompts the linker to insert their correct address.
+ (DTCSSStylesheet *)defaultStyleSheet // external symbols generated via custom build rule and xxd extern unsigned char default_css[]; extern unsigned int default_css_len; } |
And to get the string out of them is just as easy:
+ (DTCSSStylesheet *)defaultStyleSheet { // get the data from the external symbol NSData *data = [NSData dataWithBytes:default_css length:default_css_len]; NSString *cssString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding]; return [[DTCSSStylesheet alloc] initWithStyleBlock:cssString]; } |
This works because I know that all my source files are UTF8 encoded, i.e. one byte per (normal) character. It is quite handy to have the default_css_len integer telling us the length of the array, because in c an array is just a pointer to the first byte.
Conclusion
If you know how to employ custom build rules to your advantage many tasks that you would have previously run external scripts for now become part of the build process. What you can do with this knowledge is only limited by your imagination … and your scripting skills.
But the general advantage of build rules versus external programs is that they are part of the project. So if they use standard commands they are portable to other developer’s machines. Perfect for Open Source projects.
Categories: Recipes
I have a similar task to perform and wonder if you can help. I have a runtime stylesheet that is monitored so I can update my app at runtime in the simulator. If I choose to build to the device I want to copy the contents of the runtime stylesheet into a “default.css” file. I am trying to do this in a build rule by just copying the contents of one file into another like so :
cd “$INPUT_FILE_DIR”
“$INPUT_FILE_NAME” > “$INPUT_FILE_DIR”/default.css
doesn’t seem to work.
🙁
In case anyone else runs into the same problem: if you do this and suddenly start getting errors in your prefix header about foundation symbols being undefined, check that you aren’t referencing Objective-C symbols outside of the `#if __OBJC__ \ #endif` block. I had a typedef for an error block with NSError argument and that was causing everything to not compile.
Wow, thanks! That post is really great and helpful. I can now do something I’ve been trying to achieve via script build-steps, and never really worked right.
Still I’d like to ask a few questions, because my situation is just a little different.
1. What is it in your rule definition that causes the daisy-chain (the fact that resulting .c file get compiled) ?
2. What if my “compiler” (google protobuf tool) produces both .cp and .h files, and I want to export those generated headers? as the resulting header files are not referenced in the project, I can’t tag them “public” directly in the UI. How can I do it in the rule?
3. Where do I learn more about the semantics of the rules?
Examples: I see in your example 4 related environment variables: $INPUT_FILE_DIR, $INPUT_FILE_NAME, $DERIVED_SOURCES_DIR, $INPUT_FILE_BASE
3.1 What other environment variables are available to the “Rule” ?
3.2 What are their exact meaning?
3.3 What kind of results do the “rule” expect, so it knows when there were errors, and can link between the source and the errors?