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.