Monday, January 18, 2016

iOS Builds

For library projects, it can be helpful to be able to cross-compile the code for iOS, to get reassurance that the code is usable on those platforms. My approach is to start by adding a distinct iOS build to the build matrix, and force objective-c as the language to ensure that the iOS SDKs are available:

    - os: osx
      compiler: clang
      language: objective-c
      env: BUILD_TYPE=ios
  

Then set a collection of config flags in the install step, before ./configure gets run:

    - |
         if [ "$BUILD_TYPE" = "ios" ]; then
             export CONFIG_OPTS=--host=arm-apple-darwin10
             export DEVPATH=`xcode-select -print-path`/Platforms/iPhoneOS.platform/Developer
             export IOSFLAGS="-isysroot $DEVPATH/SDKs/iPhoneOS.sdk -arch armv7 -miphoneos-version-min=8.0.0"
             export CFLAGS=$IOSFLAGS
             export CXXFLAGS=$IOSFLAGS
             export LDFLAGS=$IOSFLAGS
         fi
  

Finally, remember that if there's any attempt to execute the compiled code (such as running a test suite), that needs to be skipped for a cross-compile (e.g. with if [ "$BUILD_TYPE" != "ios" ]).

Thursday, November 5, 2015

Default Template

[Edited 2015-12-04 to add LSAN, MSAN]

For reference, here's the default Travis configuration that I use to get a selection of builds running against an Autotools-based projects that has a make check target. It includes the base {linux, osx} × {gcc, clang} matrix, together with sanitizer runs (ASAN, UBSAN) and run to generate code coverage information for Coveralls.

language: c
sudo: false
matrix:
    include:
        - os: linux
          compiler: gcc
          env: BUILD_TYPE=normal
        - os: linux
          compiler: clang
          env: BUILD_TYPE=normal
        - os: linux
          compiler: gcc
          env: BUILD_TYPE=coverage
        - os: linux
          compiler: clang
          env: BUILD_TYPE=ubsan
        - os: linux
          compiler: clang
          env: BUILD_TYPE=asan
        - os: linux
          compiler: clang
          env: BUILD_TYPE=lsan
        - os: linux
          compiler: clang
          env: BUILD_TYPE=msan
        - os: osx
          compiler: gcc
          env: BUILD_TYPE=normal
        - os: osx
          compiler: clang
          env: BUILD_TYPE=normal
install:
    - pip install --user 'requests[security]'
    - pip install --user cpp-coveralls
before_script:
    - |
        if [ "$BUILD_TYPE" = "coverage" ]; then
             export CFLAGS="-fprofile-arcs -ftest-coverage"
             export LDFLAGS="-fprofile-arcs -ftest-coverage"
        fi
    - |
         if [ "$BUILD_TYPE" = "asan" ]; then
             export CFLAGS=-fsanitize=address
             export LDFLAGS=-fsanitize=address
         fi
    - |
         if [ "$BUILD_TYPE" = "lsan" ]; then
             export CFLAGS=-fsanitize=leak
             export LDFLAGS=-fsanitize=leak
         fi
    - |
         if [ "$BUILD_TYPE" = "msan" ]; then
             export CFLAGS=-fsanitize=memory
             export LDFLAGS=-fsanitize=memory
         fi
    - |
         if [ "$BUILD_TYPE" = "ubsan" ]; then
             export UBSAN_FLAGS="-fsanitize=undefined"
             export CFLAGS=$UBSAN_FLAGS
             export LDFLAGS=$UBSAN_FLAGS
         fi
script:
    - ./configure && make && make check
    - |
          if [ "$BUILD_TYPE" = "coverage" ]; then
              cpp-coveralls --gcov-options '\-lp'
          fi

Taking Control of the Matrix

The normal way to set up your matrix of Travis build configurations is to specify each of the possible axes (os, compiler, env) as a list, so that Travis builds every possible combination of those options. However, once you start adding custom builds controlled by env values, such as the sanitizer builds described in the previous entry, not all of the possible combinations make sense – for example, you might choose to just run an ASAN build on Linux + Clang, or only generate code coverage output from a single build.

So when your valid build combinations are sparse in the matrix, it can be easier to explicitly include the combinations that are valid, like so:

    matrix:
        include:
            - os: linux
              compiler: gcc
              env: BUILD_TYPE=normal
            - os: linux
              compiler: clang
              env: BUILD_TYPE=normal
            - os: linux
              compiler: gcc
              env: BUILD_TYPE=coverage
            - os: linux
              compiler: clang
              env: BUILD_TYPE=ubsan
            - os: linux
              compiler: clang
              env: BUILD_TYPE=asan
            - os: osx
              compiler: gcc
              env: BUILD_TYPE=normal
            - os: osx
              compiler: clang
              env: BUILD_TYPE=normal
  

Note that it's still possible to use the env.global key to specify an environment variable that should apply across all of the builds (rather than adding a new combination option).

    env:
      global:
        - CPPFLAGS = -Wall
  

One more trick to help with complex build matrices – YAML anchors and aliases can help to cut down repetition in your Travis config. Roughly speaking, adding &name after a key allows later config lines to use *name to repeat the value of the earlier key, either as-is or as the base for other additions:

    matrix:
      include:
        - os: linux
          compiler: clang
          addons:
            apt:
              sources: &common_sources
                - ubuntu-toolchain-r-test
                - llvm-toolchain-precise-3.6
              packages: &common_packages
                - autoconf
                - automake
        - os: linux
          compiler: clang
          env: REAL_CC=clang-3.6 REAL_CXX=clang++-3.6
          addons:
            apt:
              sources: *common_sources
              packages:
              - *common_packages
              - clang-3.6
    …
  

(Thanks to Al for suggesting this tip, and check out the Certificate Transparency Travis config for example of this in action.)

Sanitize All The Things!

[Edited 2015-12-04 to add LSAN]

If your C/C++ project includes run-time tests (you do have tests, don't you?), then it's a really good idea to run the tests under all of the sanitizers that are available from the compiler:

  • Address Sanitizer (ASAN, -fsanitize=address): detect out-of-bounds memory accesses
  • Memory Sanitizer (MSAN, -fsanitize=memory): detect reads of uninitialized memory.
  • Undefined Behaviour Sanitizer (UBSAN, -fsanitize=undefined): detect reads of uninitialized memory.
  • Thread Sanitizer (TSAN, -fsanitize=thread): detect data races in multi-threaded programs.
  • Leak Sanitizer (LSAN, -fsanitize=leak): detect memory leaks (recent Clang versions enable this by default when ASAN is used)

These tools have been available in Clang for a while, but more recent versions of GCC also include some of them:

Compiler VersionASANMSANUBSANTSANPPA / package
gcc-4.4 N N N N
gcc-4.5 N N N N
gcc-4.6 N N N N
gcc-4.7 N N N N ubuntu-toolchain-r / gcc-4.7
gcc-4.8 Y N N Y ubuntu-toolchain-r / gcc-4.8
gcc-5 Y N Y Y ubuntu-toolchain-r / gcc-5
clang-3.4 Y Y Y Y
clang-3.5 Y Y Y Y ubuntu-toolchain-r-test, llvm-toolchain-precise-3.5 /clang-3.5*
clang-3.6 Y Y Y Y ubuntu-toolchain-r-test, llvm-toolchain-precise-3.6 / clang-3.6*
clang-3.7 Y Y Y Y ubuntu-toolchain-r-test, llvm-toolchain-precise-3.7 / clang-3.7*
clang-3.8 Y Y Y Y ubuntu-toolchain-r-test, llvm-toolchain-precise / clang-3.8*

*: Only one of the different clang versions can be installed at a time, so you need to pick a single version for your build. (The gcc packages allow multiple gcc-ver binaries to be installed in parallel.)

There are a couple of things to be aware of when pointing the sanitizers at your code:

  • MSAN expects that all of the code that makes up your test executable has been compiled with -fsanitize=memory. If it hasn't – for example, if you link in some system-provided library from /usr/lib – then there will be lots of false positive uninitialized-memory-read errors (because memory initialization done by the uninstrumented library is missed). If your project has a lot of external dependencies, this may make it much harder to use MSAN.
  • UBSAN can be a little overwhelming when applied to older C codebases, so you might need to replace the overall -fsanitize=undefined with a subset of the individual checks.

Finally, to help confirm that your sanitizer builds are correctly catching errors, here's a test program that has every error under the sun:

#include <stdlib.h>
#include <stdio.h>
#include <pthread.h>

void *inc_x(void *p) {
  int *px = (int*)p;
  (*px)++;
  return NULL;
}

int main() {
  char *p = malloc(10);
  p[5] = '\0';
  if (p[2] == '\0')  /* MSAN: uninitialized memory read */
    printf("found null\n");

  p[11] = '\0'; /* ASAN: heap-buffer-overflow */

  int i = 23;
  i <<= 32;  /* UBSAN: shift overflow */
  char array[3] = "ab";
  printf("one step beyond: %c\n", array[4]); /* UBSAN: index out of bounds*/
  char data[5] = {0x00, 0x01, 0x02, 0x03, 0x04};
  int *pi = (int*)&(data[1]);
  printf("int %08x\n", *pi); /* UBSAN: misaligned address */

  pthread_t t;
  int x = 0;
  printf("x=%d\n", x);
  pthread_create(&t, NULL, inc_x, &x);
  x = 3; /* TSAN: data race */
  pthread_join(t, NULL);
  printf("x=%d\n", x);

  p = NULL; /* LSAN: leak p */
  return 0;
}

When is gcc not gcc?

If you're trying to use Travis to get as diverse a build matrix as possible, here's one thing to watch out for. A Travis config like:

    language: c
    compiler: gcc
    os: osx
  
has an interesting detail in the output:

    $ gcc --version
    Configured with: --prefix=/Applications/Xcode.app/Contents/Developer/usr --with-gxx-include-dir=/usr/include/c++/4.2.1
    Apple LLVM version 6.0 (clang-600.0.54) (based on LLVM 3.5svn)
    Target: x86_64-apple-darwin13.4.0
    Thread model: posix

Huh? Although we asked for gcc, we're getting something that looks like LLVM! Issue 2423 has the down-low, which is basically that by default OS X has a gcc binary that's a front-end for LLVM. That issue also has the work-around, which is to export CC=gcc-4.8.


Which libtool?

Projects that are based around the GNU Autotools will normally use a tool called libtool to abstract away the different tool options that are needed to build software libraries on different platforms.

However, if you're using Travis's support for the OS X platform to build your project, you may encounter a problem: OS X has its own libtool that does something slightly different. As a result, there's an alternate name for the GNU libtool on OS X, namely glibtool; similarly, the companion libtoolize tool is called glibtoolize.

This problem doesn't usually show up when you're building the distributed version of an project, because that will normally ship with a pre-built configure script, and that configure script can generate and use its own ./libtool (normally from an included ltmain.sh file).

However, Travis typically builds things straight from a Git repo, which normally doesn't include the outputs of the autotools process (i.e. the Travis build normally includes a step like ./autogen.sh or autoreconf to generate configure, ltmain.sh and friends). This also means that a missing libtoolize may well show up as the first sign of this problem, since libtoolize is used in this generation process.

If you hit this problem, you may need to fiddle with your Autotools wrapper script so that it copes with either glibtool or libtool. Here's a couple of examples:

See also: Stack Overflow question.

Beware of Absolute Notification Channels

Travis includes lots of different notification mechanisms. However, for popular projects there's a small danger that's not immediately obvious until it's too late.

If you add a notification channel that's directed at an explicit address, then that explicit address will also be present in the .travis.yml file for any forks of your project. That means that any Travis build failures for those forks will also notify the explicit address – so think carefully about whether you want (say) your IRC channel to receive build notifications for everyone's fork of the project.