{"id":5779,"date":"2020-08-07T07:00:51","date_gmt":"2020-08-07T14:00:51","guid":{"rendered":"http:\/\/writeasync.net\/?p=5779"},"modified":"2020-08-06T11:07:11","modified_gmt":"2020-08-06T18:07:11","slug":"cross-platform-without-complexity-command-line","status":"publish","type":"post","link":"http:\/\/writeasync.net\/?p=5779","title":{"rendered":"Cross-platform without complexity: command line"},"content":{"rendered":"<p>At this point we have a <a href=\"http:\/\/writeasync.net\/?p=5765\">Windows build<\/a> and <a href=\"http:\/\/writeasync.net\/?p=5773\">remote Linux build<\/a> working from Windows in the Visual Studio IDE. This is all good and it&#8217;s hard to beat VS in terms of productivity while actively developing the code. However, there are still plenty of scenarios where command line is superior (e.g. when setting up <a href=\"https:\/\/en.wikipedia.org\/wiki\/Build_automation\">build automation<\/a>).<\/p>\n<p>Unfortunately, it is not quite as easy as running <code>cmake.exe<\/code> on the command line to get the same experience that VS has provided so far. There are actually three things that are happening when building and testing the project in VS:<\/p>\n<ol>\n<li>Producing native build system files via a <a href=\"https:\/\/cmake.org\/cmake\/help\/latest\/manual\/cmake-generators.7.html\">CMake generator<\/a><\/li>\n<li>Executing the native build step<\/li>\n<li>Discovering and executing the tests<\/li>\n<\/ol>\n<p>The first step is where CMake comes in but it must be properly initialized. This requires understanding which generator and make program to use as well as some of the required paths. By observing how VS launches CMake (<a href=\"https:\/\/docs.microsoft.com\/en-us\/sysinternals\/downloads\/procmon\">Sysinternals Process Monitor<\/a> FTW!), I was able to construct a command line that has identical behavior. It looks something like this:<\/p>\n<blockquote><p>\ncmake.exe -G Ninja -DCMAKE_BUILD_TYPE=Debug -DCMAKE_CXX_COMPILER:FILEPATH=cl.exe -DCMAKE_INSTALL_PREFIX:PATH=&#8221;PROJ\\PATH\\out\\install\\x64-Debug&#8221; -DCMAKE_MAKE_PROGRAM=ninja.exe -DCMAKE_TOOLCHAIN_FILE=&#8221;VCPKG\/PATH\/scripts\/buildsystems\/vcpkg.cmake&#8221;   &#8220;PROJ\\PATH&#8221;\n<\/p><\/blockquote>\n<p>There are some paths to fill in based on the particular system and project, but this gives you a general idea. Also note that this command must be executed in the build output path, e.g. <code>out\\build\\x64-Debug<\/code>.<\/p>\n<p>Given that <a href=\"https:\/\/devblogs.microsoft.com\/cppblog\/linux-development-with-visual-studio-first-class-support-for-gdbserver-improved-build-times-with-ninja-and-updates-to-the-connection-manager\/\">VS uses the Ninja generator<\/a> to produce build system files, we would need to invoke <code>ninja.exe<\/code>. In this case, it is just a simple matter of launching the EXE &#8212; any other arguments are optional and would only be needed if you wanted to change output logging behavior, for example.<\/p>\n<p>Testing is also just as easy. Since CMake and Ninja have already done all the hard work, we would launch <code>ctest.exe<\/code> to execute tests.<\/p>\n<p>Given the tedium of trying to stitch together all the required paths and setting up the right folders, these steps are just begging to be automated. So here is a batch file <code>build.cmd<\/code> which does just that:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n@echo off\r\nsetlocal\r\n\r\npushd %~dp0\r\nset ROOT_PATH=%CD%\r\n\r\nset EXIT_CODE=1\r\nif NOT DEFINED VSCMD_ARG_HOST_ARCH set ERR_MSG=%0 must be run from a VS command prompt.&amp; goto :Quit\r\n\r\nset VCPKG_FILE=%LOCALAPPDATA%\\vcpkg\\vcpkg.path.txt\r\nif NOT EXIST &quot;%VCPKG_FILE%&quot; set ERR_MSG=Could not find vcpkg.&amp; goto :Quit\r\nset \/p VCPKG_PATH=&lt;%VCPKG_FILE%\r\n\r\nset CLEAN=0\r\nset NO_TEST=0\r\nset VERBOSE=0\r\nset BUILD_TYPE=Debug\r\nset ERR_MSG=Usage: %0 &#x5B;--clean] &#x5B;--no-test] &#x5B;--verbose] &#x5B;build_type]\r\n\r\n:NextArg\r\nset NEXT_ARG=%~1\r\nshift\r\nif NOT DEFINED NEXT_ARG goto :EndArg\r\nif \/i &quot;%NEXT_ARG%&quot; == &quot;--clean&quot; set CLEAN=1&amp; goto :NextArg\r\nif \/i &quot;%NEXT_ARG%&quot; == &quot;--no-test&quot; set NO_TEST=1&amp; goto :NextArg\r\nif \/i &quot;%NEXT_ARG%&quot; == &quot;--verbose&quot; set VERBOSE=1&amp; goto :NextArg\r\nif &quot;%NEXT_ARG:~0,1%&quot; == &quot;-&quot; goto :Quit\r\nset BUILD_TYPE=%NEXT_ARG%\r\nif \/i &quot;%BUILD_TYPE%&quot; == &quot;Debug&quot; goto :NextArg\r\nif \/i &quot;%BUILD_TYPE%&quot; == &quot;Release&quot; goto :NextArg\r\ngoto :Quit\r\n:EndArg\r\n\r\nset BUILD_CONFIG=%VSCMD_ARG_HOST_ARCH%-%BUILD_TYPE%\r\necho == Build config '%BUILD_CONFIG%'\r\n\r\nif \/i &quot;%BUILD_TYPE%&quot; == &quot;Release&quot; set BUILD_TYPE=RelWithDebInfo\r\n\r\nset OUTPUT_PATH=%ROOT_PATH%\\out\r\nset BUILD_PATH=%OUTPUT_PATH%\\build\\%BUILD_CONFIG%\r\n\r\nif &quot;%CLEAN%&quot; == &quot;0&quot; goto :Make\r\necho == Remove %BUILD_PATH%\r\nif EXIST &quot;%BUILD_PATH%&quot; rd \/s \/q &quot;%BUILD_PATH%&quot;\r\n\r\n:Make\r\nif NOT EXIST &quot;%BUILD_PATH%&quot; md &quot;%BUILD_PATH%&quot;\r\ncd &quot;%BUILD_PATH%&quot;\r\n\r\nset CMAKE_ARGS=^\r\n  -G Ninja ^\r\n  -DCMAKE_BUILD_TYPE=%BUILD_TYPE% ^\r\n  -DCMAKE_CXX_COMPILER:FILEPATH=cl.exe ^\r\n  -DCMAKE_INSTALL_PREFIX:PATH=&quot;%OUTPUT_PATH%\\install\\%BUILD_CONFIG%&quot; ^\r\n  -DCMAKE_MAKE_PROGRAM=ninja.exe ^\r\n  -DCMAKE_TOOLCHAIN_FILE=&quot;%VCPKG_PATH%\/scripts\/buildsystems\/vcpkg.cmake&quot; ^\r\n  &quot;%ROOT_PATH%&quot;\r\n\r\nif &quot;%VERBOSE%&quot; == &quot;1&quot; set CMAKE_ARGS=%CMAKE_ARGS% --log-level=VERBOSE\r\n\r\necho == cmake.exe %CMAKE_ARGS%\r\ncmake.exe %CMAKE_ARGS%\r\nset EXIT_CODE=%ERRORLEVEL%\r\nset ERR_MSG=cmake.exe failed.\r\nif NOT &quot;%EXIT_CODE%&quot; == &quot;0&quot; goto :Quit\r\n\r\nset NINJA_ARGS=\r\n\r\nif &quot;%VERBOSE%&quot; == &quot;1&quot; set NINJA_ARGS=%NINJA_ARGS% -v\r\n\r\necho == ninja.exe %NINJA_ARGS%\r\nninja.exe %NINJA_ARGS%\r\nset EXIT_CODE=%ERRORLEVEL%\r\nset ERR_MSG=ninja.exe failed.\r\nif NOT &quot;%EXIT_CODE%&quot; == &quot;0&quot; goto :Quit\r\n\r\nif &quot;%NO_TEST%&quot; == &quot;1&quot; goto :Quit\r\n\r\nset CTEST_ARGS=\r\n\r\nif &quot;%VERBOSE%&quot; == &quot;1&quot; set CTEST_ARGS=%CTEST_ARGS% -V\r\n\r\necho == ctest.exe %CTEST_ARGS%\r\nctest.exe %CTEST_ARGS%\r\nset ERR_MSG=ctest.exe failed.\r\nset EXIT_CODE=%ERRORLEVEL%\r\n\r\n:Quit\r\npopd\r\nif NOT &quot;%EXIT_CODE%&quot; == &quot;0&quot; echo %ERR_MSG%\r\nexit \/b %EXIT_CODE%\r\n<\/pre>\n<p>Note the additional functionality which can be enabled via command line options: <code>--clean<\/code> to delete the previous output folder before starting, <code>--no-test<\/code> to skip the testing step, and <code>--verbose<\/code> to pass verbose flags to each of the tools. The default build target is <code>Debug<\/code>, or you can say <code>build Release<\/code> for, you guessed it, the release build. Since this assumes the same development environment as Visual Studio, it should be run from inside a VS <a href=\"https:\/\/docs.microsoft.com\/en-us\/dotnet\/framework\/tools\/developer-command-prompt-for-vs\">developer command prompt<\/a>.<\/p>\n<p>Running it for the first time or on any changes will generate all the binaries and run tests. Running it without any changes should quickly blaze past the generation\/compilation steps and just run tests:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n== Build config 'x64-release'\r\n== cmake.exe   -G Ninja   -DCMAKE_BUILD_TYPE=RelWithDebInfo   -DCMAKE_CXX_COMPILER:FILEPATH=cl.exe   -DCMAKE_INSTALL_PREFIX:PATH=&quot;X:\\root\\CMakeSampleVS\\out\\install\\x64-release&quot;   -DCMAKE_MAKE_PROGRAM=ninja.exe   -DCMAKE_TOOLCHAIN_FILE=&quot;X:\/root\/vcpkg\/scripts\/buildsystems\/vcpkg.cmake&quot;   &quot;X:\\root\\CMakeSampleVS&quot;\r\nRe-run cmake no build system arguments\r\n-- Configuring done\r\n-- Generating done\r\n-- Build files have been written to: X:\/root\/CMakeSampleVS\/out\/build\/x64-release\r\n== ninja.exe\r\nninja: no work to do.\r\n== ctest.exe\r\nTest project X:\/root\/CMakeSampleVS\/out\/build\/x64-release\r\n    Start 1: SampleTest.GetName\r\n1\/3 Test #1: SampleTest.GetName ...............   Passed    0.11 sec\r\n    Start 2: sample-app-UsageError\r\n2\/3 Test #2: sample-app-UsageError ............   Passed    0.01 sec\r\n    Start 3: sample-app-HelloWorld\r\n3\/3 Test #3: sample-app-HelloWorld ............   Passed    0.01 sec\r\n\r\n100% tests passed, 0 tests failed out of 3\r\n\r\nTotal Test time (real) =   0.14 sec\r\n<\/pre>\n<p>If Windows were our only concern, we would be done here. But I promised cross-platform, so we need to figure out the equivalent steps in a Linux environment. Luckily for us, in this case, the steps are very similar. We just need a Linux-native way to perform them, which means it&#8217;s time for <a href=\"https:\/\/bash.cyberciti.biz\/guide\/Main_Page\">Bash<\/a>! Here is <code>build.sh<\/code>, a relatively faithful port of the above script using the all-around superior Bash constructs (conditionals, looping, functions, etc.):<\/p>\n<pre class=\"brush: bash; title: ; notranslate\" title=\"\">\r\n#!\/bin\/bash\r\n\r\nquit() {\r\n    echo $1\r\n    popd &gt; \/dev\/null \r\n    exit $2;\r\n}\r\n\r\npushd `dirname $0` &gt; \/dev\/null\r\nROOT_PATH=&quot;${PWD}&quot;\r\n\r\nCLEAN=0\r\nNO_TEST=0\r\nVERBOSE=0\r\nBUILD_TYPE=&quot;Debug&quot;\r\n\r\nwhile &#x5B;&#x5B; &quot;$1&quot; != &quot;&quot; ]] ; do\r\n  case $1 in\r\n    --clean)   CLEAN=1 ;;\r\n    --no-test) NO_TEST=1 ;;\r\n    --verbose) VERBOSE=1 ;;\r\n    debug)     BUILD_TYPE=&quot;Debug&quot; ;;\r\n    release)   BUILD_TYPE=&quot;Release&quot; ;;\r\n    *) quit &quot;Usage: $0 &#x5B;--clean] &#x5B;--no-test] &#x5B;--verbose] &#x5B;build_type]&quot; 1 ;;\r\n  esac\r\n\r\n  shift\r\ndone\r\n\r\nBUILD_CONFIG=&quot;Linux-Clang-${BUILD_TYPE}&quot;\r\necho == Build config $BUILD_CONFIG\r\n\r\n&#x5B;&#x5B; &quot;${BUILD_TYPE}&quot; = &quot;Release&quot; ]] &amp;&amp; BUILD_TYPE=&quot;RelWithDebInfo&quot;\r\n\r\nOUTPUT_PATH=&quot;${ROOT_PATH}\/out&quot;\r\nBUILD_PATH=&quot;${OUTPUT_PATH}\/build\/${BUILD_CONFIG}&quot;\r\n\r\nif &#x5B;&#x5B; $CLEAN -eq 1 ]]; then\r\n  echo &quot;== Remove ${BUILD_PATH}&quot;\r\n  &#x5B;&#x5B; -d &quot;${BUILD_PATH}&quot; ]] &amp;&amp; rm -rf &quot;${BUILD_PATH}&quot;\r\nfi\r\n\r\n&#x5B;&#x5B; ! -d &quot;${BUILD_PATH}&quot; ]] &amp;&amp; mkdir &quot;${BUILD_PATH}&quot;\r\ncd &quot;${BUILD_PATH}&quot;\r\n\r\nCMAKE_ARGS=&quot;\r\n  -G Ninja\r\n  -DCMAKE_BUILD_TYPE=${BUILD_TYPE}\r\n  -DCMAKE_CXX_COMPILER:FILEPATH=clang++\r\n  -DCMAKE_INSTALL_PREFIX:PATH=${OUTPUT_PATH}\/install\/${BUILD_CONFIG}\r\n  -S ${ROOT_PATH}\r\n&quot;\r\n\r\n&#x5B;&#x5B; $VERBOSE -eq 1 ]] &amp;&amp; CMAKE_ARGS=&quot;${CMAKE_ARGS} --log-level=VERBOSE&quot;\r\n\r\necho == cmake $CMAKE_ARGS\r\ncmake $CMAKE_ARGS\r\nEXIT_CODE=$?\r\n&#x5B;&#x5B; $EXIT_CODE -ne 0 ]] &amp;&amp; quit 'cmake failed' $EXIT_CODE\r\n\r\nNINJA_ARGS=&quot;&quot;\r\n\r\n&#x5B;&#x5B; $VERBOSE -eq 1 ]] &amp;&amp; NINJA_ARGS=&quot;${NINJA_ARGS} -v&quot;\r\n\r\necho == ninja $NINJA_ARGS\r\nninja $NINJA_ARGS\r\nEXIT_CODE=$?\r\n&#x5B;&#x5B; $EXIT_CODE -ne 0 ]] &amp;&amp; quit 'ninja failed' $EXIT_CODE\r\n\r\n&#x5B;&#x5B; $NO_TEST -eq 1 ]] &amp;&amp; quit 'Done' 0\r\n\r\nCTEST_ARGS=&quot;&quot;\r\n\r\n&#x5B;&#x5B; $VERBOSE -eq 1 ]] &amp;&amp; CTEST_ARGS=&quot;${CTEST_ARGS} -V&quot;\r\n\r\necho == ctest $CTEST_ARGS\r\nctest $CTEST_ARGS\r\nEXIT_CODE=$?\r\n&#x5B;&#x5B; $EXIT_CODE -ne 0 ]] &amp;&amp; quit 'ctest failed' $EXIT_CODE\r\n\r\nquit 'Done' 0\r\n<\/pre>\n<p>You might notice that the cmake arguments are slightly different here. For Linux, we don&#8217;t really need vcpkg and we choose the CLang compiler since MSVC (<code>cl.exe<\/code>) wouldn&#8217;t make sense. I also found that it has trouble with quoted paths so I omitted them. Anyway, just as we had hoped, the Linux behavior is equivalent to Windows when launching the script from an already produced Release build:<\/p>\n<pre class=\"brush: plain; title: ; notranslate\" title=\"\">\r\n== Build config Linux-Clang-Release\r\n== cmake -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER:FILEPATH=clang++ -DCMAKE_INSTALL_PREFIX:PATH=\/mnt\/x\/root\/CMakeSampleVS\/out\/install\/Linux-Clang-Release -S \/mnt\/x\/root\/CMakeSampleVS\r\n-- Configuring done\r\n-- Generating done\r\n-- Build files have been written to: \/mnt\/x\/root\/CMakeSampleVS\/out\/build\/Linux-Clang-Release\r\n== ninja\r\nninja: no work to do.\r\n== ctest\r\nTest project \/mnt\/x\/root\/CMakeSampleVS\/out\/build\/Linux-Clang-Release\r\n    Start 1: SampleTest.GetName\r\n1\/3 Test #1: SampleTest.GetName ...............   Passed    0.01 sec\r\n    Start 2: sample-app-UsageError\r\n2\/3 Test #2: sample-app-UsageError ............   Passed    0.01 sec\r\n    Start 3: sample-app-HelloWorld\r\n3\/3 Test #3: sample-app-HelloWorld ............   Passed    0.01 sec\r\n\r\n100% tests passed, 0 tests failed out of 3\r\n\r\nTotal Test time (real) =   0.04 sec\r\nDone\r\n<\/pre>\n<p>Here is the GitHub repo with the changes: <a href=\"https:\/\/github.com\/bobbymcr\/CMakeSampleVS\/tree\/292093d29abbd832dea4b5d7380e8f0d79262d5b\">CMakeSampleVS<\/a><\/p>\n<p>Amazingly, we have achieved our original goals. We have a project structure which works with the VS IDE on Windows, produces platform-specific binaries for Windows and Linux, supports a command line build for both Windows and Linux, and has full unit test support on both platforms. What kinds of modern C++ software could we create with these newfound capabilities?<\/p>\n","protected":false},"excerpt":{"rendered":"<p>At this point we have a Windows build and remote Linux build working from Windows in the Visual Studio IDE. This is all good and it&#8217;s hard to beat VS in terms of productivity while actively developing the code. However,&hellip; <\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[107,101],"tags":[],"class_list":["post-5779","post","type-post","status-publish","format-standard","hentry","category-cross-platform","category-native"],"_links":{"self":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5779","targetHints":{"allow":["GET"]}}],"collection":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=5779"}],"version-history":[{"count":5,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5779\/revisions"}],"predecessor-version":[{"id":5786,"href":"http:\/\/writeasync.net\/index.php?rest_route=\/wp\/v2\/posts\/5779\/revisions\/5786"}],"wp:attachment":[{"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=5779"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=5779"},{"taxonomy":"post_tag","embeddable":true,"href":"http:\/\/writeasync.net\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=5779"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}