Aido taught me about Travis. Now what does this mean? Travis is a “continuous integration system”. That is it can automatically build software and detect if the builds are fine or if they fail. While doing so it will create job logs, send emails for success and failure and it allows to render nice little status images like the following.
Click on the image to see the details for the current build. The nice thing about Travis is that nicely integrates with github and that it is completely free for open source projects. Thus I can now have some Travis configuration that will trigger whenever I commit my newest version of my DCF77 library to github. This will then try to build the newest version for all platforms I want and send me the results by mail. No more regressions for architecture / sketch combinations that I might forget to test. If a regression is introduced I will notice very early.
So how is the magic implemented. Lets first look at the original version of the configuration by Aido.
language: cpp env: global: - ARDUINO_PACKAGE_VERSION=1.8.2 - DISPLAY=:1.0 matrix: - BOARD=uno SKETCH=examples/DCF77_Scope/DCF77_Scope.ino - BOARD=uno SKETCH=examples/MB_Emulator/MB_Emulator.ino - BOARD=uno SKETCH=examples/Simple_Clock/Simple_Clock.ino - BOARD=uno SKETCH=examples/Simple_Clock_with_Timezone_Support/Simple_Clock_with_Timezone_Support.ino - BOARD=uno SKETCH=examples/Superfilter/Superfilter.ino - BOARD=uno SKETCH=examples/Swiss_Army_Debug_Helper/Swiss_Army_Debug_Helper.ino - BOARD=uno SKETCH=examples/The_Clock/The_Clock.ino - BOARD=uno SKETCH=examples/Time_Switch/Time_Switch.ino - BOARD=uno SKETCH=examples/Unit_Test/Unit_Test.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/DCF77_Scope/DCF77_Scope.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/MB_Emulator/MB_Emulator.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/Simple_Clock/Simple_Clock.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/Simple_Clock_with_Timezone_Support/Simple_Clock_with_Timezone_Support.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/Superfilter/Superfilter.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/Swiss_Army_Debug_Helper/Swiss_Army_Debug_Helper.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/The_Clock/The_Clock.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/Time_Switch/Time_Switch.ino - BOARD=diecimila:cpu=atmega168 SKETCH=examples/Unit_Test/Unit_Test.ino matrix: allow_failures: - env: BOARD=uno SKETCH=examples/Unit_Test/Unit_Test.ino - env: BOARD=diecimila:cpu=atmega168 SKETCH=examples/Unit_Test/Unit_Test.ino before_script: - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/xvfb_$TRAVIS_JOB_NUMBER.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16 - wget -q -O- http://downloads.arduino.cc/arduino-$ARDUINO_PACKAGE_VERSION-linux64.tar.xz | unxz -c | tar -xf - - mkdir -p ~/Arduino/libraries/ - ln -s $TRAVIS_BUILD_DIR ~/Arduino/libraries/ script: - $TRAVIS_BUILD_DIR/arduino-$ARDUINO_PACKAGE_VERSION/arduino --verbose --verify --board arduino:avr:$BOARD $TRAVIS_BUILD_DIR/$SKETCH
According to the documentation, Travis will create a virtual machine suitable to compile C++ code. Actually it will create such a machine for every BOARD behinde the matrix tag. Then it will run the “before script”.
The before script is part of bash shell script. If you look into it you notice it starts xvfb (a virtual X-Windows framebuffer). Then it downloads the desired Arduino IDE and unpacks it. It will also create a symbolic link to the Arduino libraries.
With other words it prepares everything such that the Arduino IDE can run “headless” in the new prepared virtual machine.
Then the “script” gets executed. It is of course also part of a bash script. This script just starts the Arduino IDE and tries to compile the desired sketch. For each of the virtual machines the success or failure is collected and then recorded (and also sent by mail). The output of such a run can be found here. The nice thing about this is that it indicates for each platform and sketch combination if it succeeded or failed. So far so good. But there is a small little things that kept nagging me. The aggregate runtime for travis was almost 15 minutes. You might ask “so what?” After all I get the runtime for free. Well, yes and no. I do not pay with money but I surely pay with waiting time and I am impatient. Although travis parralelizes the builds as an open source project I can not have an infinte amount of parallel builds. Also my builds will be queued and may be waiting for other (paid) builds to finish. Thus I wanted to cut down on the number of parallel builds while still getting a somewhat reasonable overview.
So what to do? The first thing that I noticed is that it is completely pointless to download the Arduino IDE for each and every build. Actually compiling one sketch does not pollute the build environment for all the others. Instead it would be perfectly feasible to build all sketches on after the other in one large bash script. I could just enumerate them. On the other side I wanted a quick feedback which platforms are good and which fail. Thus I pondered how to get a Travis configuration that will create a virtual machine per target platform and then tries to build all sketches for this platform. Of course it shall also log the results in a way that make analysis sufficiently easy in case something fails.
Below is my solution.
language: cpp env: global: - ARDUINO_PACKAGE_VERSION=1.8.2 - DISPLAY=:1.0 matrix: - TARGET_BOARD=diecimila - TARGET_BOARD=leonardo - TARGET_BOARD=due before_script: - /sbin/start-stop-daemon --start --quiet --pidfile /tmp/xvfb_$TRAVIS_JOB_NUMBER.pid --make-pidfile --background --exec /usr/bin/Xvfb -- :1 -ac -screen 0 1280x1024x16 - wget -q -O- http://downloads.arduino.cc/arduino-$ARDUINO_PACKAGE_VERSION-linux64.tar.xz | unxz -c | tar -xf - - mkdir -p ~/Arduino/libraries/ - ln -s $TRAVIS_BUILD_DIR ~/Arduino/libraries/ - $TRAVIS_BUILD_DIR/arduino-$ARDUINO_PACKAGE_VERSION/arduino --install-boards arduino:sam - function log() { RCODE="$1"; MSG="$2"; if [ "$RCODE" -ne 0 ]; then echo "failure for $MSG" >> build_log.txt; return 1; else echo "success for $MSG" >> build_log.txt; fi; } - function build_sketch() { BOARD="$1"; SPEC=$2 EXAMPLE="$3"; $TRAVIS_BUILD_DIR/arduino-$ARDUINO_PACKAGE_VERSION/arduino --verbose --verify --board "$SPEC" $TRAVIS_BUILD_DIR/examples/$EXAMPLE/$EXAMPLE.INO; log "$?" "$BOARD $EXAMPLE"; } - function build_on_match() { BOARD="$1"; SPEC=$2 PATTERN="$3"; EXAMPLE="$4"; if [[ "$BOARD" =~ $PATTERN ]]; then build_sketch "$BOARD" "$SPEC" "$EXAMPLE"; else echo "excluded $BOARD $EXAMPLE" >> build_log.txt; fi; } - function diecimila() { build_on_match "diecimila" arduino:avr:diecimila:cpu=atmega328 "$1" "$2"; } - function leonardo() { build_on_match "leonardo" arduino:avr:leonardo "$1" "$2"; } - function due() { build_on_match "due" arduino:sam:arduino_due_x "$1" "$2"; } script: - $TARGET_BOARD "diecimila|leonardo|due" "DCF77_Scope" - $TARGET_BOARD "diecimila|leonardo|due" "MB_Emulator" - $TARGET_BOARD "diecimila|leonardo|due" "Simple_Clock" - $TARGET_BOARD "diecimila|leonardo|due" "Simple_Clock_with_Timezone_Support" - $TARGET_BOARD "diecimila|leonardo|due" "Superfilter" - $TARGET_BOARD "diecimila|leonardo|due" "Swiss_Army_Debug_Helper" - $TARGET_BOARD "diecimila|leonardo|due" "The_Clock" - $TARGET_BOARD "diecimila|leonardo" "Time_Switch" - $TARGET_BOARD "due" "Unit_Test" - echo; echo "finished script for $TARGET_BOARD" - cat build_log.txt
So how is my new version different? First of all you notice that I have only three entries after the matrix tag. These are the target boards for which I want travis to run a build. Next you notice that my before script defines some functions “log”, “build_sketch”, “build_on_match” and “diecimila”, “leonardo”, “due”.
These functions are then leveraged by the script part to actually build the way I want it.
Lets start with the simplest of the functions. The log function. It will be called with 2 parameters. The first indicates success (=0) or failure (any value != 0). The second a textual description of what build target is concerned. With these parameters it will append a failure or succees message to the file build_log.txt.
The “build_sketch” function takes 3 parameters. The board (or better: the name of the board), the specification of the board and the name of the sketch to build. The last 2 parameters are used to build the sketch in the same fashion as Aido used to do it in his script. After the build the log function is called. It is passed the first parameter (=board name) and the third parameter (=sketch name) concatenated into one string. Thus the log will record the success or failure for this build.
The “build_on_match” function is a wrapper for the “build_sketch” function. It has one additional parameter (the third) which is a regular expression pattern. This function checks if the regular expression matches the board. If so it will call the build function otherwise it will record that it skipped building this combination.
Now to the “diecimila”, “leonardo” and “due” functions. They are structurally all three the same. They call the build_on_match function with the proper parameters for the board name (which coincidences with their own name) and the proper board specification for the Arduino IDE to build for the desired target board.
And finally we see my ultimate trick. The script will pick up the $TARGET_BOARD variable and substitute it as a shell command. Sine the $TARGET_BOARD definitions after matrix matches these function names the corresponding function will be called. This in turn passes control from e.g. function “diecimila” to “build_on_match” to “build_sketch” and finally to “log”.
The output of such a run can be found e.g. here. Of course now I can only see which target boards got a build success. But what in case of a build failure? Easy. Have a look at the last line of the script. It uses cat to show the build log. Thus in case of a failure I will click on the failed build and scroll right to the end of the log and there I can see something like the log tail below.
finished script for due The command "echo; echo "finished script for $TARGET_BOARD"" exited with 0. $ cat build_log.txt failure for due DCF77_Scope success for due MB_Emulator success for due Simple_Clock success for due Simple_Clock_with_Timezone_Support success for due Superfilter success for due Swiss_Army_Debug_Helper success for due The_Clock excluded due Time_Switch failure for due Unit_Test The command "cat build_log.txt" exited with 0. Done. Your build exited with 1.
And there of course I can immediately see which sketch is broken and which sketches are fine. Of course not as nicely formated as the more direct approach by Aido but I get something out of it. Now my Travis status usually arives much faster 🙂