Intro to using GNU/Linux for C++ Programming - Part 4: SCons



SCons

SCons Documentation

SCons is not included with the CD install of Linux Mint 12. It can be installed using aptitude:
$ sudo aptitude update
$ sudo aptitude install scons
While GNU Make uses its own special syntax, SCons uses Python which is fully featured and general purpose.

SConstruct files

The Scons equivalent of a GNU 'makefile' is a file named SConstruct, Sconstruct, or sconstruct. They are written like a Python script (though lines aren't necessarily executed in order like a normal script; they are 'declarative' rather than 'imperative'):
$ vim Sconstruct
$ cat Sconstruct
# simple myproject Sconstruct file
Program(target = 'runme', source = Split('mymain.cc mycode.cc') )
$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o mycode.o -c mycode.cc
g++ -o mymain.o -c mymain.cc
g++ -o runme mymain.o mycode.o
scons: done building targets.
You can see that g++ is invoked automatically when we provide two C++ source files to the builder method Program(...) and creates a target executable of the name provided. If you were to run SCons on a Windows system, the proper compiler will be used instead and output a Windows .exe file.

Split(...) is an SCons function that makes a list from the string argument (a cleaner alternative to the Python list syntax). Note that the hash-bang (#!/bin/bash) at the top of the file will result in text editors like vim or gedit to apply a more complete color scheme.

To clean up, provide the command line option '-c':
$ scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed mycode.o
Removed mymain.o
Removed runme
scons: done cleaning targets.
SCons will know what files need to be removed based on the structure of the Sconstruct file.

Environments

Like the variables used in makefiles, we can define mutable Python objects to pass as arguments within the SConstruct script:
$ cat Sconstruct
#!/bin/bash
# myproject Sconstruct file

# set up environment
env = Environment(
    CXXFLAGS = '-Wall'
)

# define the source files
srcfiles = Glob('*.cc')

# define build method
env.Program(target = 'runme', source = srcfiles)
The function Glob( ... ) takes care of creating a list similar to the wildcard function of GNU Make.

Be aware there are three kinds of environments in SCons. In the SConstruct file above, env is a 'construction environment'. It stores a number of 'construction variables' that may be used in building targets from sources (eg CXXFLAGS above).

A list of construction variables is available in appendix A of the user guide. The construction variable 'ENV' is another type of 'environment' in SCons: the 'execution environment'. This holds PATH which is a list of paths where SCons will look for executables (eg /usr/local/bin:/opt/bin:/bin:/usr/bin).

The last type of environment not yet mentioned is the 'external environment' which is the user's own environment at the time SCons runs. This is the os.environ dictionary so you will need to import os in any SConstruct file in order to to access it.

Seperate directories with a single SConstruct file

Below is an example of an Sconstruct file that will create object files and an executable in a build subdirectory (build/) from source files in the src/ subdirectory:
$ cat Sconstruct
#!/bin/bash
# myproject Sconstruct file

# set up environment
env = Environment( CXXFLAGS = '-Wall' )
output = 'runme'
builddir = 'build/'
srcdir = 'src/'

# define the source files
srcfiles = Glob( srcdir + '*.cc' )

# create list of object files
objfiles = []
for item in srcfiles:
    objfiles.append( builddir + item.name.rsplit( ".", 1 )[ 0 ] + '.o' )

# define object build methods
for src, obj in zip( srcfiles, objfiles ):
    env.StaticObject( target = obj, source = src )

# define program build method
env.Program( target = builddir + output, source = objfiles )
$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o build/mycode.o -c -Wall src/mycode.cc
g++ -o build/mymain.o -c -Wall src/mymain.cc
g++ -o build/runme build/mycode.o build/mymain.o
scons: done building targets.
Glob() works just fine when we provide the src directory as part of the wildcard.

The list of object files to be created in the build/ directory is constructed manually by using the Python function rsplit to replace the file extensions with '.o'. Glob() creates a list of SCons 'File' objects which store the filename string (no directory) in the .name attribute. Accordingly, we specify the StaticObject() builder method for each object/source file pair in the next loop. Finally, we specify the Program() builder method.

This is essentially the same as the Makefile we ended up with in the previous post. But having to specify build objects manually by extension makes this script non-portable to Windows based systems. SCons provides a portable way to handle this situation but it will require additional scripts.

Hierarchical & Variant Builds

To separate the source code from the build directory requires the creation of subsidiary Scons scripts called 'SConscript' files (or Sconscript or sconscript; though you can actually specify another name when calling the SConscript( ... ) function used below).

In the main project directory will be your master or 'root' SConstruct file. In this file we specify what other subsidiary SConscript files exist, thereby creating a 'hierarchy' of scripts. These SConscript files could also be intermediate to other SConscript files.

Before looking at the content of the scripts, here is an example usage of SCons from the command line:
$ ls
build  NOTES  README  Sconstruct  src
$ ls src
mycode.cc  mycode.hh  mymain.cc  mysup.hh  Sconscript
$ ls build
debug  release
$ ls build/debug/ build/release/
build/debug/:

build/release/:
$ scons
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o build/debug/mycode.o -c -Wall -DDEBUG src/mycode.cc
g++ -o build/debug/mymain.o -c -Wall -DDEBUG src/mymain.cc
g++ -o build/debug/runme_d build/debug/mycode.o build/debug/mymain.o
g++ -o build/release/mycode.o -c -Wall -DNDEBUG src/mycode.cc
g++ -o build/release/mymain.o -c -Wall -DNDEBUG src/mymain.cc
g++ -o build/release/runme build/release/mycode.o build/release/mymain.o
scons: done building targets.
$ ls build/debug/ build/release/
build/debug/:
mycode.o  mymain.o  runme_d

build/release/:
mycode.o  mymain.o  runme
$ ./build/debug/runme_d 
runme_d: src/mymain.cc:8: int main(): Assertion `0' failed.
Aborted
$ ./build/release/runme
Hello World
mycode foo(): -11
$ scons -c
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Cleaning targets ...
Removed build/debug/mycode.o
Removed build/debug/mymain.o
Removed build/debug/runme_d
Removed build/release/mycode.o
Removed build/release/mymain.o
Removed build/release/runme
scons: done cleaning targets.
$ scons build/debug
scons: Reading SConscript files ...
scons: done reading SConscript files.
scons: Building targets ...
g++ -o build/debug/mycode.o -c -Wall -DDEBUG src/mycode.cc
g++ -o build/debug/mymain.o -c -Wall -DDEBUG src/mymain.cc
g++ -o build/debug/runme_d build/debug/mycode.o build/debug/mymain.o
scons: done building targets.
$ ls build/debug/ build/release/
build/debug/:
mycode.o  mymain.o  runme_d

build/release/:

You can see that the directory structure is the same as in the last example. There is a new file called 'Sconscript' now in the source directory.

Upon executing the 'scons' command, both variants are built (build/debug and build/release) with appropriate command line options and you can see that the object files and build targets are in the proper directories.

Cleaning with the '-c' option removes files from both variant builds. You can also build or clean just one of the variants by providing that variant directory as at the command line as shown in the example.

Now the scripts:

$ cat Sconstruct 
#!/usr/bin/python
# myproject Sconstruct file

# debug setup
cxxflags = '-Wall -DDEBUG'
output = 'runme_d'
Export( 'cxxflags', 'output' )
VariantDir( 'build/debug', 'src', duplicate=0 )
SConscript( 'build/debug/Sconscript' )

# release setup
cxxflags = '-Wall -DNDEBUG'
output = 'runme'
Export( 'cxxflags', 'output' )
VariantDir( 'build/release', 'src', duplicate=0 )
SConscript( 'build/release/Sconscript' )
$ cat src/Sconscript 
#!/usr/bin/python
# debug Sconscript file

Import( 'cxxflags', 'output' )
env = Environment( CXXFLAGS = cxxflags )
env.Program( target = output, source = Glob('*.cc') )

The Sconstruct file specifies how to build two variant builds: 'build/debug' and 'build/release'. For both, we use the SCons function 'Export( ... )' to export variables to the subsidiary Sconstruct files for each build. You can see in the Sconscript file the corresponding call to 'Import( ... )'.

Next, the VariantDir( ... ) function is used to specify the details of each build variant. The first argument is the output directory, the second argument is the source directory, and the third argument is required to prevent SCons from copying the source files (and SConscript file) to the output directory. (Having duplicated source code in the same directory in the output may be required for some compilers or other situations but it is not necessary in this case).

Finally the SConscript( ... ) function finalizes the execution of this particular build. Both Export() and VariantDir() are equivalent ways of passing various variables to the SConscript function. (See the SCons manual entry for the SConscript() function).

SCons notes

The above example is fairly simple and does not include any libraries. The following notes may aid in learning how to use SCons.

-'Execute("command")' in an SCons script will execute the provided 'command' in the local shell.

Part 5: GDB & Valgrind

No comments:

Post a Comment