2009-11-13

Zooming with SCons

I tried out Scons on a non-java project (i.e a project where it has a chance to succeed) to see if it really does zoom. And zooms it does! I am a bit picky with build systems, and I can't say SCons is without flaws, but just look at this (mildly anonymized) example:


I'm building a small middleware program. The environment I'm doing it in requires me to write my own stubs. I can't do much about that, but I want my build to ensure that each part of the client, server, middleware core, and stubs for the interfaces only include things from the directories they are allowed to depend on.


With GNU Make (my long-time favourite, since it in contrast to most other build tools actually performs its job), this is a mess. Since there can only be one pattern in a rule, you end up with something like


CLIENT_INCLUDES=-Iservice2_interface -Iservice2_stubs
SERVER_INCLUDES=-Iservice1_interface -Iservice2_interface
#...
$(BUILD)/client/%.o: client/%.c | $(BUILD)/client
$(CC) -c -o $ $^ $(CLIENT_INCLUDES) $(CFLAGS)
$(BUILD)/server/%.o: server/%.c | $(BUILD)/server
$(CC) -c -o $ $^ $(SERVER_INCLUDES) $(CFLAGS)
#...
$(BUILD)/client:
mkdir -p $@
$(BUILD)/server:
mkdir -p $@
#...


This is just for two sub-directories. And whenever something that is common to all changes, the rule for each sub-directory has to be updated. Not much fun. And no, a makefile for each directory is not a solution.


In SCons, it becomes (for all six sub-directories):


env=Environment()
VariantDir('build', '.', duplicate=0)·
objs=[]
for (dir, includes) in [ \
("server", ["service1_interface", "service2_interface"]), \
("client", ["service2_interface", "service2_stubs"]), \
("util", []), \
("middleware", ["service1_interface", "service1_stubs", "service2_interface", "service2_stubs"]), \
("service1_stubs", ["service1_interface"]), \
("service2_stubs", ["service2_interface"]) \
]:
local_env=env.Clone()
local_env["CPPPATH"].extend(includes)
sources=map(lambda s:"build/%s" % s, Glob("%s/*.c" % dir))
objs.extend(local_env.StaticObject(sources))
env.Program('my_test.exe', objs)


This code just loops over pairs of directory names and permitted includes, copies the global build environment (which is set up at the top, and can include useful things like env['CCFLAGS']="-Wall -Wextra -Werror"), appends the permitted includes to the list of include directories for this sub-directory. Next, it takes each C file in the sub-directory, and prefixes it with "build/" (this is one aspect I'm not that happy with), sets up rules for building object files for them, and then appends the names of those object files to the big list of all objects for the project. Finally, another rule is set up to build the binary from the objects.


A neat thing with SCons is that even though the rules are set up step by step in an imperative fashion (as can be expected from a system that uses Python as its configuration language), nothing is actually built directly by the StaticObject and Program calls above. Instead, what happens is that rules are appended to the environment. When the script terminates, SCons takes over and analyses the produced rule set. From that, it can build dependency chains, check what is already built, and re-build what is necessary. Just like it should.


That said, I haven't tried it on any more demanding jobs where intermediate files are built (which is what ant and other shell-script replacements that some mistake for build systems fail at), but the documentation for it looks promising, and the developers (and the Cons designers before them) seem to know what they are doing.


Veridct: Zooms!

No comments:

Post a Comment