TSANCHEZ'S BLOG

Project Setup

Simple Demos

Setting up an actual code project is a lot of work. For the simplest of demos, just building an running code is easy. Just type everything out into a file, run the compiler and have a nice day.

g++ test.cpp
./a.out

However, once you get to the point of wanting to use an IDE or a more complicated build process, there's a lot of setup that you need to do. Thankfully, there's plenty of tools to help out.

CMake

Basics

Cmake to the rescue!

CMake is a fairly amazing build tool helper. It allows you to form a single project definition which works cross platform and cross IDE. There's some minimal setup you may have to do specific to each platform if your linking in any platform-specific libraries, otherwise it's a one-size-fits-all solution.

CMake basic setup, again can start out really simple. You create a CMakeLists.txt file which contains information about which files to compile, and what type of output you expect to be building (a library? a binary? etc.). This could be as simple as:

add_binary(test test.cpp)

However, there's lots of cool things you can do with CMake.

Version Stamping

Version stamping your builds is a really nice way to confirm at a later point in time, exactly which build an error is being reported from. I personally use git as my version control system, and the following serves me well for adding the git version information to my binaries.

First, you can tell CMake to run arbitrary commands via the execute_process function, and thus collect the output for later use. We can run two git commands to gather information about the current branch and commit hash.

# Get the current working branch
execute_process(
  COMMAND git rev-parse --abbrev-ref HEAD
  WORKING_DIRECTORY $CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_BRANCH
  OUTPUT_STRIP_TRAILING_WHITESPACE)

# Get the latest abbreviated commit hash of the working branch
execute_process(
  COMMAND git log -1 --format=%h
  WORKING_DIRECTORY $CMAKE_SOURCE_DIR}
  OUTPUT_VARIABLE GIT_COMMIT_HASH
  OUTPUT_STRIP_TRAILING_WHITESPACE)

Once we have that information, we can have CMake generate a special type of file for us. The configure_file command will instruct CMake to "pre-process" a input template, substituting in strings from variables defined within your CMakeFiles. I personally output anything "generated" by the build process to a new ".generated" folder to help prevent checking in the generated outputs, and make for easier cleanup.

# Configure a version.h containing the above collected version
# information.
configure_file (
  "$PROJECT_SOURCE_DIR}/version.h.in"
	"$PROJECT_SOURCE_DIR}/.generated/version.h"
)
set(PROJECT_GENERATED_ROOT "$PROJECT_SOURCE_DIR}/.generated")
include_directories($PROJECT_GENERATED_ROOT}

This works alongside a file version.h.in which has substitution tags matching the variables we generated above.

#define BUILD_BRANCH_ID "@GIT_BRANCH@"
#define BUILD_VERSION_HASH "@GIT_COMMIT_HASH@"
#define BUILD_TIMESTAMP __DATE__

At this point you can write your test.cpp "hello world" binary:

#include <iostream>
#include "version.h"

int main(int argc, char **argv) {
  std::cout << "Test run for branch " << BUILD_BRANCH_ID << "@"
            << BUILD_VERSION_HASH << " on " << BUILD_TIMESTAMP << std::endl;
  return 0;
}

Generators

Besides generating a version file, I often find myself building a tool which is used to generate other files needed in the build process. This is a fairly complex process in CMake to setup.
  1. You must come up with a way to define the output files that will be generated. Every build rule in CMake must have the list of input dependencies pre-defined.
  2. You must define a rule to generate every relevant file.
  3. You must specially mark every relevant file as both dependent on the generator and as "generated" so that CMake knows not to look for it until after the generation step is run.
My basic setup for this is the following build function:
function(proto_generate SOURCES)
  if(NOT ARGN)
    message(SEND_ERROR "proto_generate() called without schema files")
  endif()

  set(${SOURCES})
  foreach(schema_file ${ARGN})
    # Convert the source filename.
    # Get the path relative to the project root, and make a new
    # path which is rooted under the .generated/ folder.
    get_filename_component(file_path "${schema_file}" ABSOLUTE)
    file(RELATIVE_PATH genfile_rel_dir ${PROJECT_SOURCE_DIR} ${file_path})
    set(output_base "${PROJECT_GENERATED_ROOT}/${genfile_rel_dir}")
    string(REPLACE ".proto" ".pb" output_base ${output_base})
    get_filename_component(file_dir "${output_base}" PATH)

    # Create the output folder
    file(MAKE_DIRECTORY ${file_dir})

    # Run the build command
    set(protoc_location $<TARGET_FILE:protoc>)
    add_custom_command(
      OUTPUT  "${output_base}.cpp" "${output_base}.h"
      COMMAND "${protoc_location}"
      ARGS --infile "${file_path}"
           --outfileprefix "${output_base}"
      DEPENDS protoc
      WORKING_DIRECTORY ${PROJECT_SOURCE_DIR}
      COMMENT "Generating proto for: ${schema_file}"
      USES_TERMINAL
    )

    # Add the generated output filenames to the sources list
    list(APPEND ${SOURCES} "${output_base}.cpp")
    list(APPEND ${SOURCES} "${output_base}.h")
  endforeach()

  # Mark every file as "generated"
  set_source_files_properties(${${SOURCES}} PROPERTIES GENERATED TRUE)
  # Push the list of sources out to the caller
  set(${SOURCES} ${${SOURCES}} PARENT_SCOPE)
endfunction()

GIT

With all of the above settings in place, there's a lot of files that appear in the source folder that you don't want to be checking in. In order to prevent that, the .gitignore file in the root of your repository should have the following items in it:
# CMake / Gentools
.build/
.generated/
.data/
*.swp

# CPPCoverage (windows)
LastCoverageResults.log
CoverageReport*/

Travis CI

Continuous integration is important for any project. It goes hand in hand with automated test suites as a way to:
  • Provide early feedback that your code doesn't function as expected.
  • Serves as proof that a pull request is merged correctly.
  • Generate releases for others working on the project to use.
I currently have a TravisCI setup for my projects, which works nicely for the above needs. However, there is one major downside at present, which is that it has no native support for understaning the phases of a release. The whole run only has a success or failure state, and you have to dig into the logs to figure out if it died at the build, test, or deploy stages.

Copyright © 2002-2019 Travis Sanchez. All rights reserved.