Thursday, October 3, 2013

Making Visual Studio Solutions, CMake and Cygwin Play Nicely Together

I'm currently involved in development and maintenance of a set of command-line tools for analyzing Affymetrix micro-array data. The tools are developed in C++ and intended to be platform independent (compiled and tested on a couple of different Linux flavors, Windows using Visual C++, and OS X). We're working on version 2.0 which is the first major rewrite in several years.

Previous versions relied on separate Visual Studio solution files for Windows, and makefiles for Linux, which needed to be maintained in parallel. This was a pain, particularly in the Windows world: for Unix/Linux, Make hasn't changed much for decades, and if you learnt to use it in the 80's you'll probably still be able to get by. But, for Visual Studio things can change drastically between versions, so that the solution and project files in VS2008 and 2010 are completely different, and required a major transition. So, in version 2.0 we decided to switch to CMake to generate our build scripts across all platforms.

We've also been using Bamboo to run our continuous integration builds and nightly regression tests. For Linux this meant having Bamboo execute a bash script. So as to make things as uniform as possible between platforms, we opted to use a bash script under cygwin to run the builds and tests on Windows.

I found a number of issues and complications in getting cygwin to work with Visual Studio 2010/2012 solutions and CMake - some I found details of on the web, and others required experimentation to fix. Since there wasn't one place I could go to find out all these things, and much of the information I found was outdated, I'm going to gather them all together in one place as a public service. The intended audience is people who are familiar with CMake and bash, but struggling with getting things working in the Windows world.

Setting environment variables for MSBuild/VC++

There are a bunch of environment variables needed for running Visual Studio solutions in the command line, which can be set by one of the bat files vcvars*.bat (see http://generally.wordpress.com/2006/11/28/building-visual-studio-solutions-using-msbuild-in-cygwin/), Since were doing 64 bit only, we want the ones called vcvarsx86_amd64.bat. We need to call the right one for the version of Visual Studio we're using:

# Read vcvars.bat file to set Windows tools settings
CMD=/cygdrive/c/Windows/system32/cmd
# VS2012 version
#
${CMD} /c "C:\Program Files (x86)\Microsoft Visual Studio 11.0\VC\bin\x86_amd64\vcvarsx86_amd64.bat"



Or for Visual Studio 2010:

# VS2010 version

#
${CMD} /c "C:\Program Files (x86)\Microsoft Visual Studio 10.0\VC\bin\x86_amd64\vcvarsx86_amd64.bat"

Running CMake from cygwin

First we need to pick the appropriate executables for CMake and CPack:

CMAKE="/cygdrive/c/Program Files (x86)/CMake 2.8/bin/cmake.exe"
CTEST="/cygdrive/c/Program Files (x86)/CMake 2.8/bin/ctest.exe"

We're doing an out-of-source build, so we use two environment variables, SRCROOT for the source directory (in our case an svn checkout), and CMAKEROOT for the build directory. We may have to create the CMAKEROOT directory if it's not there already:

# Create cmake directory if not present
if [[ ! -d "${CMAKEROOT}" ]]
then
  mkdir "${CMAKEROOT}"
fi


In order to run CMake we'll need to specify the Generator, and to feed it the source directory in Windows path format. If you do "cmake --help" it will provide a list of available generators. In our case we want "Visual Studio 11 Win64". To get the directory name in the format CMake wants, rather than in cygwin format we use the "cygpath -w" utility.

This next bit is important:

We now come to a particularly annoying and hard to diagnose bug (see http://www.cmake.org/Bug/print_bug_page.php?bug_id=13131): cygwin by default declares certain environment variables like TEMP and TMP. CMake will use certain environment variables, including tmp and temp,  In cygwin this is fine and good, but Windows applications, and particularly MSBuild, do not distinguish between environment variables based on case. So when you run MSBuild after CMake in the cygwin environment, MSBuild will die because it sees that there are multiply defined environment variables.

The worst thing about this is that, once you've encountered the problem, simply undef-ing the relevant variables won't fix it. MSBuild starts some processes the first time it's run and those will continue to break. At this point you have to go into the Task Manager and kill any MSBuild processes before you can contiue. The work around is to undef all the variables before the first call of CMake.

This is the code from our scripts which runs CMake:

# Set the generator - VS2012 version
#
CMAKE_GENERATOR='Visual Studio 11 Win64'
 
# Have to unset some environment variables because of a weird bug with cmake under cygwin
# - see http://www.cmake.org/Bug/print_bug_page.php?bug_id=13131
unset tmp TMP temp TEMP
 
# Now call CMake
cd "${CMAKEROOT}"
"$CMAKE" -G "$CMAKE_GENERATOR" "$(cygpath -w ${SRCROOT})"
cmake_rv=$?
if [ "${cmake_rv}" != 0 ]
then
  echo "CMake encountered errors"
  exit 1
fi


Using MSBuild to build a Visual Studio solution file

CMake should create a .sln file and a bunch of vcxproj files (one for each target). First we need to pick the right MSBuild executable, and also decide on the configuration and platform for our build:

# This maybe system dependent
MSBUILD=/cygdrive/c/Windows/Microsoft.NET/Framework64/v4.0.30319/MSBuild.exe
 
# We're going to do a 64 bit release build
BUILD_CONFIG=Release
PLATFORM=x64

Checking for errors in MSBuild

With Make in the Linux/Unix world, we can check the output value ("$?") to see if the build was successful.  With MSBuild this doesn't necessarily work. Consequently, to check for a successful build we need to generate a log file, and scan the log file for an errors message - the log file should contain the line '0 Error(s)' if it was successful. (In the following the environment variable SLN_FILE is set to the name of the solution file).

MSBUILD_LOG=msbuild.log
"${MSBUILD}" ${SLN_FILE} /fl /property:Configuration=${BUILD_CONFIG};Platform=${PLATFORM}
msbuild_rv=$?
if [ ${msbuild_rv} != 0 ]
then
    echo "MSBuild ${SLN_FILE} exited with status '${msbuild_rv}'"
    exit 1
fi
grep -q '0 Error(s)' ${MSBUILD_LOG}
if [ $? != 0 ]
then
    echo "MSBuild ${SLN_FILE} detected errors"
    exit 1
fi


(The /fl option causes MSBuild to write a log file).

Using MSBuild to build a Project file

Our CMake scripts are configured to create a number of additional custom targets which are not part of the default build target (do not have ALL set), such as a target regression which will update the regression test data and run the regression tests. In Linux these translate to Makefile targets which are not called as part of the default target. In Visual Studio, these give rise to .vcxproj files which are not built by default when one builds the main project .sln file.

To build these we can call MSBuild directly on a project file:

REGRESSION_PROJ_FILE=regression.vcxproj
#
# Build regression project
"${MSBUILD}" ${REGRESSION_PROJ_FILE} /fl /property:Configuration=${BUILD_CONFIG};Platform=${PLATFORM}



And that's all. Hope this is useful to someone out there.

No comments:

Post a Comment