benjamin.computer

Go-lang and CMake

22-06-2016

At QMUL, we are a cmake shop. I personally love cmake, and how easy it is to integrate into other things like test frameworks, jenkins and the like. Go makes a really good attempt to force you into proper management of your projects but it's a bit of a pain to force into your workflow sometimes. Fortunately, we've managed to get cmake to play nicely with golang's various commands.

I found this project: https://github.com/cpconduce/go_cmake. It consists of a couple of useful modules that basically setup a temporary GO_PATH directory, where all the compilation and install takes place. A CMakeLists.txt file would look something like this:

    cmake_minimum_required(VERSION 2.8)
    project(prm)

    include(cmake/GolangSimple.cmake)
    add_subdirectory(buildinfo)
    add_subdirectory(mainprogram)

    #Tests
    include(cmake/GoTests.cmake)
    set_target_for_tests("tests.xml" "coverage.xml")

The buildinfo bit is quite interesting. It contains another CMakeLists.txt file that looks like this:

    add_custom_command(OUTPUT buildinfo
               COMMAND ${CMAKE_COMMAND}
                       -DSOURCE_DIR=${CMAKE_SOURCE_DIR}
                       -DDEST_DIR=${GOPATH}/src/pass.hpc.qmul.ac.uk/prm
                       -P ${CMAKE_SOURCE_DIR}/cmake/PrmBuildInfo.cmake)

This creates a command called buildinfo. The actual command looks like this

    #based on http://stackoverflow.com/questions/3780667/use-cmake-to-get-build-time-svn-revision
    find_package(Git REQUIRED)
        exec_program(${GIT_EXECUTABLE} 
        ${SOURCE_DIR}
        ARGS "describe --dirty"
        OUTPUT_VARIABLE GIT_REVISION)

    set(PRM_REVISION "${GIT_REVISION}")

    #get the build host
    site_name(PRM_BUILD_HOST)

    #get the build timestamp in the default format
    string(TIMESTAMP BUILD_TIMESTAMP)

    # write relevant information into template header 
    file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/buildinfo.go.txt
        "package prm

        func GetVersionString() string {
        return \"${PRM_REVISION} ${PRM_BUILD_HOST} ${BUILD_TIMESTAMP}\"
        }
    ")

    # copy the file to the final header only if the data changes (currently it will be every time due to the times     24 tamp data) 
    execute_process(COMMAND ${CMAKE_COMMAND} -E copy_if_different ${CMAKE_CURRENT_BINARY_DIR}/buildinfo.go.txt ${D     25 EST_DIR}/version.go)

What we are doing here is automagically generating some go code that is compiled into our final executable, that displays the current version as generated by git. It's this sort of funky glue that really appeals to me with CMake.

One of the problems I had with our system is the executable also relies on another module that lives inside the same project directory as the main module. Basically, the top directory pass.qmul.ac.uk has two subdirs, one is the main executable module and the other is a set of functions that can be used independently but still need to be compiled. To get around this, I aktered the GolangSimple.cmake file I downloaded to have a module style command

    set(GOPATH "${CMAKE_CURRENT_BINARY_DIR}/go")
    file(MAKE_DIRECTORY ${GOPATH})

    function(GO_GET TARG)
    add_custom_target(${TARG} env GOPATH=${GOPATH} go get ${ARGN})
    endfunction(GO_GET)


    function(GO_COPY MODULE_NAME MODULE_SRC)
    get_filename_component(MODULE_SRC_ABS "../../${MODULE_SRC}" ABSOLUTE)

message(STATUS "Copying Local Module ${MODULE_SRC_ABS}")
    add_custom_target(${MODULE_NAME}
                COMMAND ${CMAKE_COMMAND} -E copy_directory
                ${MODULE_SRC_ABS} ${GOPATH}/src/${MODULE_SRC})

    endfunction(GO_COPY)


    function(ADD_GO_INSTALLABLE_PROGRAM NAME MAIN_SRC)
    get_filename_component(MAIN_SRC_ABS ${MAIN_SRC} ABSOLUTE)
    add_custom_target(${NAME} ALL DEPENDS ${NAME})
    add_custom_command(TARGET ${NAME}
                COMMAND env GOPATH=${GOPATH} go build 
                -o "${CMAKE_CURRENT_BINARY_DIR}/${NAME}"
                ${CMAKE_GO_FLAGS} ${MAIN_SRC}
                WORKING_DIRECTORY ${CMAKE_CURRENT_LIST_DIR}
                DEPENDS ${MAIN_SRC_ABS})
foreach(DEP ${ARGN})
    add_dependencies(${NAME} ${DEP})
endforeach()

    endfunction(ADD_GO_INSTALLABLE_PROGRAM)

Finally, automating our tests so that Jenkins can do it's thing is a big part of this. We have a cmake file that looks a little like this:

    GO_GET(go-junit-report github.com/jstemmer/go-junit-report)
    GO_GET(gocov github.com/axw/gocov/gocov)
    GO_GET(gocov-xml github.com/AlekSi/gocov-xml)

    FUNCTION(SET_TARGET_FOR_TESTS output_test_file output_coverage_file)

ADD_CUSTOM_TARGET(test

    DEPENDS go-junit-report gocov gocov-xml

    #Run the tests
    COMMAND env GOPATH=${GOPATH} go test -v pass.hpc.qmul.ac.uk/prm > tests.out

    #Run the conversion to xUnit format
    COMMAND cat ${CMAKE_BINARY_DIR}/tests.out | ${GOPATH}/bin/go-junit-report > ${CMAKE_BINARY_DIR}/${output_test_file}

    #Run the coverage tests
    COMMAND env GOPATH=${GOPATH} ${GOPATH}/bin/gocov test pass.hpc.qmul.ac.uk/prm > gocov.txt

    #Run the conversion to xml
    COMMAND cat ${CMAKE_BINARY_DIR}/gocov.txt | ${GOPATH}/bin/gocov-xml > ${CMAKE_BINARY_DIR}/${output_coverage_file}

    WORKING_DIRECTORY ${CMAKE_BINARY_DIR}
    COMMENT "Running tests."
)

    # Show info where to find the report
    ADD_CUSTOM_COMMAND(TARGET test POST_BUILD
    COMMAND ;
    COMMENT "Test report saved in ${output_test_file} and coverage report saved in ${output_coverage_file}"
    )
    ENDFUNCTION() # SET_TARGET_FOR_TESTS

We use the junit report style, with gocov and gocov-xml. Essentially, we can now type make test and have it all work seamlessly.

I really like Go's build, test and documentation support, but it does sort of stand on it's own and it takes a little work to shoe-horn it into shape, but fortunately, not too much. It's now quite trivial to test, develop, deploy and all the rest, thanks to the awesome cmake.


benjamin.computer