skip to main content

Bye autotools hello Scons

Building C++ projects with Scons

published icon  |  category icon programming

tags icon C++ python build ecosystem

Remember this?

  • ./configure
  • make
  • make install

That’s not so bad, as long as you have the right compiler and linker flags configured, depending on the target OS. The real problem, however, is trying to figure out how to alter something if you didn’t write the Makefile yourself. Or if you in fact did write it, but it was some time ago. Two days. No, four hours.

The problem

Try to study the autoconf and automake flow diagram, explained on Wikipedia: the GNU build system. Headache coming up? Suppose we would like to use these … uhm, “thingies”, for a simple C++ project.

First, let me define simple:

  • It has some (shared) library dependencies
  • The source lives in src
  • Since it’s obviously written the TDD way, the tests live in test

Onward, to the Makefile creation station! This is a sample file, from the Google Test Makefile:


USER_DIR = ../samples

CPPFLAGS += -isystem $(GTEST_DIR)/include
CXXFLAGS += -g -Wall -Wextra -pthread

TESTS = sample1_unittest
GTEST_HEADERS = $(GTEST_DIR)/include/gtest/*.h \

all : $(TESTS)

clean :
	rm -f $(TESTS) gtest.a gtest_main.a *.o

gtest-all.o : $(GTEST_SRCS_)

gtest_main.o : $(GTEST_SRCS_)

gtest.a : gtest-all.o
	$(AR) $(ARFLAGS) $@ $^

gtest_main.a : gtest-all.o gtest_main.o
	$(AR) $(ARFLAGS) $@ $^

sample1.o : $(USER_DIR)/ $(USER_DIR)/sample1.h $(GTEST_HEADERS)

sample1_unittest.o : $(USER_DIR)/ \
                     $(USER_DIR)/sample1.h $(GTEST_HEADERS)

sample1_unittest : sample1.o sample1_unittest.o gtest_main.a
	$(CXX) $(CPPFLAGS) $(CXXFLAGS) -lpthread $^ -o $@

This first builds the gtest_main.a binary, to be able to link that with our test after the source (sample1.o) has been is built. The syntax is clumsy, simple files require me to have a deep knowledge how flags and linking work, and I don’t want to specify everything in one block.

As esr in his blog post Scons is full of win today said, it’s a maintenance nightmare. What to do?

There are a few alternatives which aim to cover everything autotools does, such as QMake from Trolltech or CMake (that actually generates Makefiles. You’re not helping, CMake!). Or, one could go for Scons.

build your software, better.

Scons starts with a single SConstruct file, which acts as the makefile. You can bootstrap the default build target using the scons command. (cleaning with scons --clean). The big deal here is that the contents of that file is simply python (2.7, I know)!

Want to write a utility function to gather all your cpp files? Fine, go ahead, def mystuff(): (you do know this already exists, right? Use Glob()) Want to unit test these, and include them? Done. Want to split up everything per source directory? Use SConscript files and include these from within your root SConstruct using SConscript('file', 'envVarToExport').

This is my blueprint construct file:

env = Environment(CXX = 'g++')

gtest = env.SConscript('lib/gtest/SConscript', 'env')
src = env.SConscript('src/SConscript', 'env')
out = env.SConscript('test/SConscript', 'env gtest src')

# output is an array with path to built binaries. We only built one file - run it (includes gtest_main).
test = Command( target = "testoutput",
                source = str(out[0]),
                action = str(out[0]) )

Things to note:

  • Scons works with Environments which can be shared and cloned (see below)
  • You can share variables with the second parameter
  • Executing after a build also works, passing in the result of conscripts.
  • Ensure to always build your test with AlwaysBuild()

This is the conscript which builds google test:

env = env.Clone(CPPPATH = './:./include')

env.Append(CXXFLAGS = ['-g', '-Wall', '-Wextra', '-pthread'])
gtest = env.Library(target = 'gtest', source = ['src/', 'src/'])


Things to note:

  • Fetch the shared variables with Import() and return stuff with Return() (it’s a function)
  • specify flags all you want.
  • Building something? Program(), Library() or SharedLibrary().


env = env.Clone(CPPPATH = './')
src = env.Library(target = 'wizards', source = Glob('*.cc'))


Things to note:

  • Glob() auto-reads all files in the current dir.

And finally, test, linking both source and google test:

Import('env', 'gtest', 'src')
env = env.Clone()

env.Append(LIBPATH = ['#lib/gtest', '#src'])
env.Append(LIBS = [gtest, src])
out = env.Program(target = 'wizards_unittests', source = Glob('*.cc'))


Things to note:

  • Use the hashtag # to point to the root dir where the SConstruct file resides.
  • Linking is as simple as providing LIBS and the right path.

So where does that leave us? Yes there’s still “syntax” to be learned, even if you’re a seasoned python developer; you need to know which function to use for what, that’s what the excellent scons doc is for. I know it made my life a lot easier while trying to do something simple and this is only the tip of the iceberg. Scons is relatively popular according to Stack Overflow, the documentation is excellent and if all else fails you can write your own garbage in a full-fledged dynamic language.

The only really irritating bit is the python 2.7 dependency, so don’t forget to use virtualenv.

In practice

The section below contains practical snippets used by myself in some point in the past, commented in Dutch. Feel free to grab a piece.

  1. SCons Wiki Frontpage
  2. Single HTML Manpage
  3. SCons Construction Variables om bvb de compiler te specifiëren.

Separating SConstruct and SConscript


Defining a build output, duplicating source files, etc. For example, in a SConstruct file:

SConscript('SConscript', variant_dir######'build', duplicate0)

This is an example file to build and run some GTest in SConscript:

def Glob( pattern ###### '*.*', dir  '.' ):
    import os, fnmatch
    files = []
    for file in os.listdir( Dir(dir).srcnode().abspath ):
        if fnmatch.fnmatch(file, pattern) :
            files.append( os.path.join( dir, file ) )
    return files

# construction variables:
env ###### Environment(CXX  'g++',
                                    CPPPATH = '../:./include')

# add to library search path env.Append(LIBPATH = ['/usr/local/lib/'])
# add to libraries link path env.Append(LIBS = ['SDL_image','GL'])

env.Append(CPPFLAGS = ['-isystem ./include'])
env.Append(CXXFLAGS = ['-g', '-Wall', '-Wextra', '-pthread'])

env.SharedLibrary(target ###### 'gtest_main.dll', source  ['../src/'])

# after that, we should link with gtest_main
A photo of Me!

I'm Wouter Groeneveld, a level 35 Brain Baker, and I love the smell of freshly baked thoughts (and bread) in the morning. I sometimes convince others to bake their brain (and bread) too.

If you found this article amusing and/or helpful, you can buy me a coffee - although I'm more of a tea fan myself. I also like to hear your feedback via Mastodon or e-mail. Thanks!