Looking for a Makefile alternative

For starters, the examples I tried left a terrible impression, as there is no such thing as a “simple automake example”.

The smallest hello world requires multiple configuration files and command sequences, greatly hindering the learning process.

I reached the end without understanding any step.

Second, after fully completing the task I completely failed to see any improvement over Make.

The language I was using was just as complicated, the procedure just as cumbersome, the documentation just as obscure.

Using CMake or Automake yielded absolutely no benefit for me, so I simply dropped them.

A Ray of HopeSo there I was, back to using Make.

I was not happy about it however, so I kept looking for another way.

At some point I even considered simply using a custom bash script and just be done with it.

Finally I stumbled into SCons, and it was love at first sight.

Contrary to other options, SCons starts with the right mindset: do not reinvent the wheel.

There are already a ton of different programming languages, no need to create another one; and what better choice than Python, one of the most beloved scripting language of the last two decades.

In fact, SCons is more of a Python library for build dependency than an actual build tool.

SCons scripts are just Python scripts that already include a certain environment; I would honestly like it more if an explicit import statement was needed.

SCons is invoked similarly to Make, by running the scons command when a file named SConstruct is present in the current folder.

BrevityUsing special functions the developer defines the dependencies and command to compile the project.

The most beautiful thing about it is that it is as simple as it needs to be.

Take a single-file C project; using a makefile, you would need to writehello: main.

c gcc main.

c -o helloNote that is a very poor makefile.

I cannot avoid specifying the entire command needed to compile the single source, I had to specifically use a tab character for the second line, and a good developer would use wildcard and variables instead.

The fact alone that a raw command would achieve the same result with less lines of code is a good indicator of how cumbersome this is.

On the SCons side, there is not a single unneeded character:Program("hello", ["main.

c"])“Create the program hello from the following sources: main.

c”.

Nothing more, nothing less.

Beautiful.

Although this is the case for smaller projects, brevity is not the most prominent advantage over Make.

For some makefiles SCons might end up being more verbose, since it tends to have a less declarative approach (Python is an imperative language).

Where SCons really shines is simplicity and flexibility.

SimplicityAs stated, SCons is more of a library than a dedicated build tool.

In a SConstruct script I can take advantage of the Python language in all of its glory and power.

SCons scripts are easy to read and create because you do not need to know every nook and cranny of an obscure ad-hoc language.

Besides being more simple, they have immediate access to the Python runtime.

If I want to check whether a folder exists in a Makefile I’ll need to remember how to do it in bash, ask myself if Make syntax is equivalent and finally find out that I cannot write the command in multiple lines because reasons.

Total processing time: 20 minutes and no less than 3 Google searches.

Much more cleanly, in SCons one can import the os python module and use os.

path.

exists to achieve the same result.

Moreover, most software projects already include Python scripts for some reasons or others: code generation, configuration, directory setup, etc… .

With SCons those can be quickly integrated into the build process and called directly as functions instead of command lines.

Finally, being a Python library allows SCons to be installed through pip .

Normally there is no reason to do that, as most Operating Systems have pre-built binary versions of every Python package.

If you lack the luxury of being a sudoer however you can still take advantage of scons by installing it as a user with pip –user or a virtualenv.

ScalableDespite its simpler nature, SCons adapts neatly to bigger builds.

For one, it is way easier to add new source files and branches, thanks to the immediate Glob function for pattern matching.

Instead of creating hierarchical scripts or painstaikingly matching every source file, every new folder can be summarized in a single function call:sources = Glob("{}/*.

S".

format(BUILD))sources += Glob("{}/*.

c".

format(BUILD))sources += Glob("{}/emulated/*.

c".

format(BUILD))sources += Glob("{}/hal/*.

c".

format(BUILD))sources += ["res/font.

bin"]env = Environment(**env_options) # My environment optionsenv.

Program(ALL, sources)Here I include assembler sources, C files from three different directories and a binary font.

Then, the Program directive just commands to use all of them to create the final product, and every file will be handled accordingly: assembler sources will be assembled, C files will be compiled and the binary font font.

bin, being an already processed result, will be used only in the final linking phase.

Notice also how simple it is to include new files using regular expressions throught the Glob() function, opposed to the weird substitution macros and wildcards of a Makefile.

While I generally advise against them, hierarchical builds are still possible.

By using the SConscript function one can invoke separate scripts for structured projects; unlike make, the processo to pass environment and parameters is much cleaner.

With makefiles, every entity of the shell environment becomes a variable accessible from the script; when invoking a recursive make, variables from the environment and the command line are passed and user defined ones are ignored (unless specified through the export directive).

This in practice translates into a very messy and polluted global environment.

In SCons, environments are dictionaries that are passed around as needed by the different scripts.

Moving from a global variable to an always present parameter might not seem a big deal at first, but it ends up being a real quality of life change.

A Few ExamplesTo rest my case I’ll provide a couple of practical scons examples that I am using these days.

The first one is used to compile firmware for an stm32 ARM microcontroller.

Since the architecture is different (unless you’re working on a Raspberry Pi) the scripts needs to specify a cross compiler and a linker script; plus, I’m working with libopencm3, so there is also a static archive to be linked with my source code.

This started as a Makefile, but the result was quickly becoming messy.

import osTOOLCHAIN = "arm-none-eabi-"ELF = "experiment.

elf"BIN = "experiment.

bin"LIBOPENCM3 = "/home/maldus/Source/Github/libopencm3/"CPUFLAGS = ["-mcpu=cortex-m3", "-mthumb"]CFLAGS = ["-Wall", "-Wextra", "-g3", "-O0", "-MD", "-DSTM32F1", "-I{}include".

format(LIBOPENCM3)] + CPUFLAGSLDFLAGS = ["-nostartfiles", "-L{}lib".

format( LIBOPENCM3), "-Wl,-T,{}lib/stm32/f1/stm32f103x8.

ld".

format(LIBOPENCM3)] + CPUFLAGSLDLIBS = ["-lopencm3_stm32f1"]# Creates a Phony targetdef PhonyTargets( target, action, depends, env=None,): if not env: env = DefaultEnvironment() t = env.

Alias(target, depends, action) env.

AlwaysBuild(t)externalEnvironment = {}if 'PATH' in os.

environ.

keys(): externalEnvironment['PATH'] = os.

environ['PATH']env_options = { "ENV": externalEnvironment, "CC": "{}gcc".

format(TOOLCHAIN), "CPPPATH": ["{}/include".

format(LIBOPENCM3)], "CCFLAGS": CFLAGS, "LINKFLAGS": LDFLAGS, "LIBS" : LDLIBS}sources = Glob("*.

c")env = Environment(**env_options)env.

Program(ELF, sources)env.

Command(BIN, ELF, "{}objcopy -O binary {} {}".

format(TOOLCHAIN, ELF, BIN))env.

Default(BIN)PhonyTargets("flash", "st-flash write {} 0x8000000".

format(BIN), BIN, env)PhonyTargets("openocd", "openocd -f interface/stlink-v2.

cfg -f target/stm32f1x.

cfg", BIN, env)At the beginning of the script I define libraries, utilities and toolchain path to be used.

Those are then formatted into the env_options dictionary, effectively declaring the desiderd behaviour for the build process.

Once everything is set up, the compilation is requested in just two lines of code:env.

Program(ELF, sources)env.

Command(BIN, ELF, "{}objcopy -O binary {}Create the ELF from the sources, then format it into a binary file.

The rest of the script defines a couple of phony targets that don’t actually produce outputs (similary to the make .

phony directive), like flash to program the device and openocd to debug it.

To execute binaries such as st-flash and openocd, the external environment should extracted and added into env_options (same goes for any GUI, as the DISPLAY variable might not be present into the scons environment).

The second example is a polar opposite: a website built with Elm.

Elm is a fully functional programming language compiled into Javascript to build web apps; it is shipped through npm with is own build tool (the elm CLI), so what is the use of SCons?Not much really, but the devil is in the details.

The website I’m building uses Bootstrap, which in turn requires a sass based configuration.

Sass is an evolution of style languages I didn’t know about, a more cleaner and higher level language that is still compiled into css.

So my project now has a two step compilation process, three if you count the elm reactor call to start the local web server and see the changes.

Let’s see how SCons manages it.

import osELM = "elm.

js"CSS = "compiled.

css"# Creates a Phony targetdef PhonyTargets( target, action, depends, env=None,): if not env: env = DefaultEnvironment() t = env.

Alias(target, depends, action) env.

AlwaysBuild(t)externalEnvironment = {}if 'PATH' in os.

environ.

keys(): externalEnvironment['PATH'] = os.

environ['PATH']env_options = { "ENV": externalEnvironment }env = Environment(**env_options)final = env.

Alias("all", [ELM, CSS])env.

Default(final)env.

Command(ELM, "src/main.

elm", "elm make src/main.

elm –output={}".

format(ELM))env.

Command(CSS, "custom.

scss", "sass custom.

scss > {}".

format(CSS))PhonyTargets("reactor", "elm reactor", final, env)Considering the final objective this might seem a bit overkill, but Python syntax is so simple and mnemonic that it took almost no effort on my side.

Unsurprisingly, neither elm nor sass natively supported by SCons, but that’s not an issue: the Command function serves exactly the purpose of specifying any custom command to generate the output.

Now by running scons reactor the script updates both the css and js files before starting the development web server, shaving precious seconds when rebuilding the sass configuration is not required.

Bottom line is: Make was great, now let’s move on.

Makefiles generator are a way to put an ugly patch on the problem, but today we have the means for a proper solution.

SCons is simple, powerful and effective, and I should have heard about it much sooner; if you need it give it a try!.

. More details

Leave a Reply