SCons is touted as an easier, more reliable and faster way to build software with in-built support for multiple languages and an easily extensible user-defined builder system.
Program('app.c')
Let’s talk briefly about build tools so as to understand the role SCons plays in the build process.
Build tools are described as programs that automate the creation of executable applications from source files.
We should keep in mind that the build process incorporates the processes of compiling,linking, and packaging source code into an executable.
Build tools help ease this process by tracking the following processes and items: build requirements, build sequence, and project depencencies hence making the build process consistent.
Currently build tools are split into:
The above can be further subdivided into other broader sub-categories.
Next, let’s take a brief look at a typical build process. In this case we’ll look at a simple C program build process.
The application source code is as below:
In this initial stage the pre-processor joins continued lines and removes comments. Preprocessor commands such as
#include <stdio.h>
are interpreted to form a macro language. Symbolic constant are then evaluated to their defined values example
#define UNIVERSAL_CONSTANT 42
Let’s inspect the result of this process by executing the following command
gcc -E <filename.c> -o <filename.i>
For such a simple program, you’ll notice a very massive expanded file.
However, of interest, you should notice the following section in the intermediate .i
expanded source code file.
int main(int argc, char const *argv[])
{
printf("Well the answer is %i", 42);
return 0;
}
You’ll notice that our macro UNIVERSAL_CONSTANT
has been substituted with 42
and all comments in the code have been stripped off.
In this this stage, the expanded source code file is taken as input and translated into assembly instructions for the target processor architecture by the compiler .
Let’s see this in action by running the following command
gcc -S <filename.c> -o <filename.s>
In our example application the output will be as shown :
In this stage the assembler takes the intermediate assembly code as input and translates it into an object file that can be run by the target processor in a format known as relocatable object program .
Note that most of the functions will not be resolved and will only be resolved at a later stage.
Let’s have a look at this stage by running the following commands:
gcc <filename.c> -c <filename.o>
The output will be as shown (using hedxump
utility in GNU/Linux Ubuntu)
When we try to read the file contents via cat
we get the following output
From the above output, not much can be derived other than our expected output Well the answer is %i
.
In this stage, the object code will be re-arranged by the linker to facilitate function calls. The linker will also resolve various function calls from libraries and plug(merge) them into the program.
The linker will handle instructions for setting up the running environment like parsing command line arguments, environment variables and return values. The object code is finally converted into an executable by the linker.
Let’s inpect this process by running the following command
gcc <filename.c>
You should now get an executable a.out
file, which you can run by executing the following command
./a.out
Understanding of machine-level code and compiler translation processes enables one to make good decisions on how to write efficient code.
In building large applications, understanding the linker process can be of great use in solving some of the most complex link-time errors.
Understanding how data is read and in which format helps in avoiding gaping security holes commonly present in most applications.
You could have generated all intermediate files by running
gcc -save-temps <filename.c>
You can also name the final artifact by passing the -o
flag to gcc
gcc -o <executablename> <filename.c>
The above processes can be visualized as below
Now that we have an idea of the build process, lets dive into SCons
As of the time of writing this article the latest version is 3.0.1
The procedure is pretty simple : Create a directory to install SCons and download the latest version and install
sudo mkdir -p /opt/src &&\
cd /opt/src/ &&\
sudo wget http://prdownloads.sourceforge.net/scons/scons-3.0.1.tar.gz &&\
sudo tar -xvzf scons-3.0.1.tar.gz &&\
cd scons-3.0.1/ &&\
sudo python setup.py install
sudo apt-get install scons -y
sudo pacman -S scons
SCons will search for a SConstruct or Sconstruct or sconstruct file to fetch build configurations.
Because SConstruct files are treated as normal Python files, one can leverage Python’s scripting capabilities to handle complex build processes.
Let’s start by developing a simple application that generates fractal images in PPM format.
The application structure is a shown as below:
.
|-- SConstruct
|-- lib
| |-- mandelbrot.cc
| `-- mandelbrot.hpp
`-- src
`-- app.cc
We’ll begin with a barebones SConstruct configuration.
Library('mandelbrot',['./lib/mandelbrot.cc'])
Program('./src/app.cc', LIBS=['mandelbrot'], LIBPATH='.')
In this instance we’re instructing SCons to build a mandelbrot
library and also passing the library source files lib/mandelbrot.cc
. Then we build our application whose source files are located inside src/app.c
by linking it to our library.
Then inside the project directory we run SCons
scons
Inside your project directory you should now have a tree similar to this
|-- SConstruct
|-- lib
| |-- mandelbrot.cc
| |-- mandelbrot.hpp
| `-- mandelbrot.o
|-- libmandelbrot.a
`-- src
|-- app
|-- app.cc
`-- app.o
You’ll notice that we now have an executable app
inside our src/
directory which can be launched from the commandline by executing ./src/app
.
Now let’s clean up the the output and try something different. Run the following command:
scons -c
In this configuration we’ll define the output directory for our executable: in this case the output will be inside the dist
directory.
Library('mandelbrot',['./lib/mandelbrot.cc'])
Program('dist/mandelbrot','./src/app.cc', LIBS=['mandelbrot'], LIBPATH='.')
Running the scons
command results in to the creation of an executable inside the dist
folder.
.
|-- SConstruct
|-- dist
| `-- mandelbrot
|-- lib
| |-- mandelbrot.cc
| |-- mandelbrot.hpp
| `-- mandelbrot.o
|-- libmandelbrot.a
`-- src
|-- app.cc
`-- app.o
Now we can clean the result by running the scons -c
command.
But you’ll notice that the dist
folder will remain after the clean command is run. We can add a custom Clean()
method that will remove specified folders and directories. The Clean()
expects :
Now, the SConstruct will look like this
Library('mandelbrot',['./lib/mandelbrot.cc'])
Program('dist/mandelbrot','./src/app.cc', LIBS=['mandelbrot'], LIBPATH='.')
target = Program('dist/mandelbrot','./src/app.cc', LIBS=['mandelbrot'], LIBPATH='.')
Clean(target,'./dist')
This can be useful if the build command generates log files or certain artifacts that you might later want to discard.
Suppose now you’d like to allow your users to supply custom build options.
Library('mandelbrot',['./lib/mandelbrot.cc'])
Help("""
Usage: 'scons .' to build the mandelbrot application
'scons -Q IMAGETYPE=PNG' to generate PNG output only, default is PPM
'scons -Q IMAGETYPE=ALL' to generate both PPM and PNG images
""")
import platform
platform_ = platform.system()
if platform_ == 'Linux':
Help("\nType: 'scons linux' Build with native Linux support.\n")
if platform_ == 'Windows':
Help("\nType: 'scons win32' Build with native Windows support.\n")
vars = Variables(None)
vars.Add(EnumVariable('IMAGETYPE', 'Set to PNG to build for PNG image output support or ALL to generate PNG and PPM', 'PPM' ,
allowed_values=('PNG','PPM','ALL')))
env = Environment(variables = vars,
CPPDEFINES={'IMAGETYPE' : '"${IMAGETYPE}"'})
unknown = vars.UnknownVariables()
if unknown:
print("Unknown variables: %s"%unknown.keys())
Exit(1)
env.Program('dist/mandelbrot','./src/app.cc', LIBS=['mandelbrot'], LIBPATH='.')
target = env.Program('dist/mandelbrot','./src/app.cc', LIBS=['mandelbrot'], LIBPATH='.')
Clean(target,'./dist')
In the above instance you can see we practically invoked Python to read OS Information. This can be useful if you want to call native platform build or disable builds for certain platforms.
We’re also letting the user know that they can pass scons -Q IMAGETYPE=PNG
to build the app with PNG support. Also note that IMAGETYPE
is set to PPM
by default. See the EnumVariable()
function.
Now when a user types scons -h
from the project directory they should see the following (GNU/Linux example):
Now, we have a
MACRO
in our sample application that will generate a PNG image whenIMAGETYPE=PNG
is passed in the compilation flags.
SCons offers a provision to check for the existence specific header files in the the system, as well as programs, functions and datatypes.
This is facilitated by the Configure(arg)
function :
conf = Configure(env)
if not conf.CheckCHeader('stdlib.h'):
print 'C standard library not found!'
Exit(1)
For C++ the above check can be modified as follows conf.CheckCXXHeader('header.h')
if not conf.CheckCXXHeader('math.h'):
print 'Math.h must be installed!'
Exit(1)
{% codeblock SContruct lang:python %} if not conf.CheckLib(‘png’): print ‘PNG Library not found’ Exit(1)
### Check for an application
```py
if not conf.CheckProg('xterm'):
print 'XTerm must be installed'
Exit(1)
One can also write custom checks. Suppose we’d like to check for assert.h
without relying on SCons' inbuilt checks:
test_assert = """
#include <assert.h>
int test_assert(int x)
{
assert(x <= 4);
return x;
}
int main()
{
test_assert(2);
return 0;
}
"""
def CheckForAssert(context):
context.Message('Checking for Assert.h ... ')
result = context.TryLink(test_assert, '.c')
context.Result(result)
return result
conf = Configure(env, custom_tests = {'CheckForAssert' : CheckForAssert})
if not conf.CheckForAssert():
print 'Could not find assert.h !'
Exit(1)
Note that the TryLink(arg1, arg2)
generates an arg2
filetype - eg. .c
- to run the check.
You’ll notice that a.sconf_temp
directory is created during these tests and in one of the files the following lines can be found
#include <assert.h>
int test_assert(int x)
{
assert(x <= 4);
return x;
}
int main()
{
test_assert(2);
return 0;
}
When run, the resulting output is like shown below
The complete SConstruct file for this blog is as given below; change to suite your application build requirements
Get up and runing with SCons without installing it in your PC. Follow through with this Katacoda scenario to get started.
Happy Building! :)