Construction variables and environments in SCons

The users guide describes (e.g. here) how to build programs from source files. That's pretty fundamental, but what it never actually tells you how to do is pass flags to your C compiler. The closest it gets is to just start using the CCFLAGS variable at the start of section 5.1. But it never really introduces them, or tells you what some other very common construction variables are. So I wrote this.

I'll put comments in paragraphs like this one to relate SCons to Make, by way of analogy. I'm discussing the make situation as if you are using the built-in implicit rules that it comes with, instead of typing out the command line for these common tasks.

Starting point

Let's say we have a really simple program and a corresponding SConstruct that builds it:

hello.cpp
#include <iostream>
int main() {
  std::cout << "Hello world\n";
  return *(int*)0;
}

SConstruct
Program('hello', 'hello.cpp')

As you would expect, it builds correctly but has a little trouble during execution.

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o hello.o -c hello.cpp
g++ -o hello hello.o
scons: done building targets.
% ./hello
Hello world
zsh: segmentation fault ./hello

Man, what a nasty bug. If only we had some debugging info so we could use GDB to figure out what's going on. How can we do that?

In it's most basic form, this SConstruct file is like the following Makefile (which does work!):

Makefile
hello: hello.cpp

The main difference (besides differences in out-of-dateness checking that differ between the systems) is that Make goes directly to the executable while SCons uses (and keeps) an intermediary object file.

In both cases, the build system has built-in knowledge (to a first approximation) of how to use some tools, and it knows how to construct a command line given just the sources and targets.

The main difference in what's going on internally is that the SConstruct explicitly specifies what built-in knowledge to use (by calling the Program builder) while Make figures out that it knows how to build a file with no extension from one with a .cpp extension. (In fact, you don't even need the above Makefile. Delete it and just run make hello; it works!)

Passing CCFLAGS to a builder directly

Enter the CCFLAGS construction variable. This "variable" holds either a string or a list of strings which are passed to the compiler. (If it's a list, the elements are concatenated together with spaces, so ['--foo', '--bar'] behaves the same as '--foo --bar'.)

There are a couple different options for where you can set CCFLAGS and other construction variables. Now, construction variables aren't Python variables, so you can't just say CCFLAGS='-g' in your SConstruct. In our example, the easiest place to put it is right in the call to the Program builder, as follows:

SConstruct
Program('hello', 'hello.cpp', CCFLAGS='-g')

Now if we rebuild our project, we see that we do indeed have debugging information:

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o hello.o -c -g hello.cpp
g++ -o hello hello.o
scons: done building targets.
% gdb hello
GNU gdb (GDB) 7.0
....
Reading symbols from /afs/cs.wisc.edu/u/d/r/driscoll/delete/hello...done.
(gdb) run
Starting program: /afs/cs.wisc.edu/u/d/r/driscoll/delete/hello
Hello world

Program received signal SIGSEGV, Segmentation fault.
0x000000000040078c in main () at hello.cpp:5
5 return *(int*)0;

Mission accomplished!

Since we only passed CCFLAGS in that particular call, it won't apply to any other targets that you build using Program or any other builder. Of course, if you have a real program then you may have to specify those CCFLAGS multiple times. We wouln't want that.

Short of explicitly writing out the command line that you want, Make doesn't really have a way to do this. It may work to change CCFLAGS to what you want, give the rule for the target, then change it back; I'm not sure if that would do what you want.

Setting CCFLAGS in a construction environment

Simply stated, a construction environment is basically a dictionary that maps construction variables like CCFLAGS to their values. It also has some other information, and a set of available builders.

Creating an environment is very simple, as is setting construction variables. Then, when we want to register a target with the build engine, we simply call that builder as a method of the construction environment instead of just like a global function. This is easier illustrated than explained.

SConstruct
env = Environment()
env['CCFLAGS'] = '-g'
env.
Program('hello', 'hello.cpp')

The first line creates a new environment, the second sets the CCFLAGS value for that environment, and the third tells SCons to build hello using the environment env, instead of the global settings.

Another way to accomplish the same thing is to set the variable at the point the environment is created:

SConstruct
env = Environment(CCFLAGS='-g')
env.Program('hello', 'hello.cpp')

Both of these are functionally equivalent in this example to the preceeding examples.

Using multiple construction environments

One real power of the SCons environment becomes more apparent when you learn you can have multiple environments.

Suppose we want two different builds of our greeter program. For internal use, we want a version compiled without optimization and with debugging information. For release, we want a version with optimization and without debugging information.

One way to do this is the following: we'll create two different construction environments, one for each variant. One will create a program called hello-release and one will create a program called hello-debug. We'll also need separate object files, so we have to name them explicitly so it doesn't have two different ways to build hello.o. (More idiomatically these would be built to separate directories, but that's a little beyond the scope here.)

SConstruct
# Build the debug version
debug_env = Environment(CCFLAGS='-g')
obj = debug_env.Object('hello-debug.o', 'hello.cpp')
debug_env.Program('hello-debug', obj)

# Build the release version
release_env = Environment(CCFLAGS='-O2')
obj = release_env.Object('hello-release.o', 'hello.cpp')
release_env.Program('hello-release', obj)

And if we try it out, we see it's building both, passing the appropriate compiler flags.

% scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o hello-debug.o -c -g hello.cpp
g++ -o hello-debug hello-debug.o
g++ -o hello-release.o -c -O2 hello.cpp
g++ -o hello-release hello-release.o
scons: done building targets.

Of course, the power of this approach becomes more apparent when you don't have to essentially repeat the lines that build each of the targets. Again, the idiomatic way of doing this is to build the targets to a different directory, but you need to learn about SConscripts and stuff to do that. But once you have that set up, what you can do is set up what are called variant builds. You give the instructions of how to build a program once, and then "call" those instructions multiple times with different construction environments.

The default construction environment

Now, I've written a couple of times something like "if you use a construction environment", but that's actually a misnomer. Even in the very first example SConstruct, the Program call takes place in the context of a construction environment called the default environment. The default environment is used whenever the environment isn't explicitly stated.

It's actually possible to modify the default environment, and this gives the final variant on my running example. We'll set up the default environment to use the debugging flag and then tell it about the hello target.

SConstruct
DefaultEnvironment(CCFLAGS = '-g')
Program('hello', 'hello.cpp')

And this works as we want: hello builds with debugging information.

So we finally have something that has an equivalent in make. It looks like this:

SConstruct
CXXFLAGS=-g
hello: hello.cpp

Aside from the variable name, it looks basically the same, and it does behave the same, aside from the extra object file with SCons.

(Make uses CFLAGS for C code only and CXXFLAGS for C++ code only. SCons uses CFLAGS for C code only, CXXFLAGS for C++ only, but also has CCFLAGS which is passed to both.)

However, make doesn't really have any equivalent of multiple construction environments, though there are ways to do similar things as the variant build example.

Some common construction variables

This section will give some commonly useful construction variables for a couple tasks. See here for more information, though that's not exactly a pocket-sized reference.

C and C++ compiling

Linking

Conceptually this provides similar stuff to make, but there are some differences. Make tends to use slightly different names, particularly for the variables that drive the linker command line. (LD instead of LINK.) But it also doesn't offer the richer variables like CPPDEFINES in nearly as nice a form.

Other uses for construction variables

You are free to set whatever variables you want in your environments; it doesn't have to be the pre-defined ones. One reason to do this is if you define your own builder, or are using the Command builder. For instance, you could rewrite the running example as:

SConstruct
DefaultEnvironment(CCFLAGS='-g')
Command('hello', 'hello.cpp', 'g++ -o $TARGET $CCFLAGS $SOURCES')

This is like the Makefile where you write out the command line:

Makefile
CCFLAGS=-g
hello: hello.cpp
   g++ -o $@ $(CCFLAGS) $^

It is also possible to just store information in an environment and retrieve it later. This is particularly useful when calling a subsidiary SConscript as you only have to pass one variable (the environment) down the chain.