Summer Adventure 2018: Learning CMake #2
It has been two days since my last wrestle with CMake. In this time I continued to play around with the build system and test my various hypotheses about the way the system works. To provide at least the minimal structure necessary to articulate the experience, from henceforth I will try to recount and explain what I did in the order I did it in.
Built-In Variables
I wanted to test if my theory on how the setting of various global, provided variables affected the build chain (variables such as PROJECT_SOURCE_DIR, CMAKE_BINARY_DIR, etc). To that end I had a generated header file define the various variables as macros and my C++ printed these out. My theory was correct in that setting these variables changed as I predicted them to. However, I came to realise these variables, with the exception of output directories like CMAKE_RUNTIME_OUTPUT_DIR and CMAKE_ARCHIVE_OUTPUT_DIR, are intended to be used read-only and changing them once CMake has begun has no effect. It made sense to me people were using 'binary' interchangeably with 'build' which in retrospect makes sense since all compiled code goes into the build/binary directory, but before I had realised this I was accustomed to using 'binary' solely for executables and not for midway compiled and object code.
(Static) Libraries
The next thing on my list of to explore was the creation of libraries. the add_library() command is used for naming a target (which will become the library) and using a library type like STATIC, and the source files which comprise that library. To test this out I made a static library out of a single 'greeting' function and successfully linked it in with my program. This further reinforced my intuition of what add_subdirectory() did. This command is used as sort of and in-build subdirectory additive instead of adding external projects with ExternalProject_Add. By this I mean when you need to build subdirectories which are logically part of the main project instead of being 3rd-party add_subdirectory() is used.
ExternalProject_Add
The way I had hoped to have my build system work was that all external libraries were built once only and installed the first time, instead of having them be rescanned and possibly rebuilt as would have happened if add_subdirectory() was used. This served as the motivation into using ExternalProject_Add(). Once the module was include(ExternalProject)'d into my top-level CMakeLists.txt file, I got to explore its customisation options which are incredibly flexible and offer far more customisation options than the average user needs. You can tailor everything from downloading from VCS like Git or using local copies to configuring the project, to patching, building, installing, testing...and more! You can actually add more steps which is incredible. For my purposes however I only wanted to change the configure stage, so I defined the SOURCE_DIR and BINARY_DIR to my directory's extern and extern/build folders. I also passed in a custom CMAKE_INSTALL_PREFIX so that when CMake installed the external project (which I set up as a library) it put in my project/lib directory - very nice.
install
While I was messing around with ExternalProject_Add()'s install options I also learnt about the install target that CMake let's you use. I learnt CMake, again, has got you covered as you can install individual targets into distinct locations, install all the different types of libraries into their own directories too using commands like install(TARGETS <target> ARCHIVE DESTINATION <dest>), or instead of archive, library, module, etc. You can also install whole directories! That doesn't seem to grand but the realisation of its convenience of moving headers from the source tree into easier-to-access locations it makes organising the project for an orderly person like myself very simple. Directories, similar to the rest of the install() family, are installed like install(DIRECTORY <dir> DESTINATION <dest>).
I should point out that install is very, very flexible with external projects and you can do things like import various exported targets from external projects (the import/export is actually more elaborate, but I didn't go that far) and install them to desired places. I only lightly dabbled in this to begin with.
Variables
It was only a small discovery and since CMake's syntax is your run-of-the-mill scripting language, I figured that variables get interpolated as strings and so you can say something like "USE_${VAR}" and the result will be "USE_FOO", assuming of course VAR held the value of "FOO", but you get the idea. Similar to recursive structures in functional languages you can also build up a list of strings and store it into a variable (even into itself!) with something like set(VAR ${VAR} arg1 arg2...). Even when you use VAR for the first time it defaults to the empty string so this always works.
Control Structures
The last tool I needed to start my rudimentary build system was basic conditional and looping structures. Unsurprisingly CMake has if() where a boolean variable is tested to see if it's true (or non-empty I think?), foreach() for iterating over a list with a loop variable, and functions too. These were very easy to pick up as it was near identical to all other imperative languages which have these constructs, so not much to say here.
With the bare minimum knowledge of all of these topics under my belt I was actually able to write a CMakeLists.txt file which fit my needs! In a future post I'll detail what my plan is in relation to learning OpenGL and what I will put this build-system to use for.
Built-In Variables
I wanted to test if my theory on how the setting of various global, provided variables affected the build chain (variables such as PROJECT_SOURCE_DIR, CMAKE_BINARY_DIR, etc). To that end I had a generated header file define the various variables as macros and my C++ printed these out. My theory was correct in that setting these variables changed as I predicted them to. However, I came to realise these variables, with the exception of output directories like CMAKE_RUNTIME_OUTPUT_DIR and CMAKE_ARCHIVE_OUTPUT_DIR, are intended to be used read-only and changing them once CMake has begun has no effect. It made sense to me people were using 'binary' interchangeably with 'build' which in retrospect makes sense since all compiled code goes into the build/binary directory, but before I had realised this I was accustomed to using 'binary' solely for executables and not for midway compiled and object code.
(Static) Libraries
The next thing on my list of to explore was the creation of libraries. the add_library() command is used for naming a target (which will become the library) and using a library type like STATIC, and the source files which comprise that library. To test this out I made a static library out of a single 'greeting' function and successfully linked it in with my program. This further reinforced my intuition of what add_subdirectory() did. This command is used as sort of and in-build subdirectory additive instead of adding external projects with ExternalProject_Add. By this I mean when you need to build subdirectories which are logically part of the main project instead of being 3rd-party add_subdirectory() is used.
ExternalProject_Add
The way I had hoped to have my build system work was that all external libraries were built once only and installed the first time, instead of having them be rescanned and possibly rebuilt as would have happened if add_subdirectory() was used. This served as the motivation into using ExternalProject_Add(). Once the module was include(ExternalProject)'d into my top-level CMakeLists.txt file, I got to explore its customisation options which are incredibly flexible and offer far more customisation options than the average user needs. You can tailor everything from downloading from VCS like Git or using local copies to configuring the project, to patching, building, installing, testing...and more! You can actually add more steps which is incredible. For my purposes however I only wanted to change the configure stage, so I defined the SOURCE_DIR and BINARY_DIR to my directory's extern and extern/build folders. I also passed in a custom CMAKE_INSTALL_PREFIX so that when CMake installed the external project (which I set up as a library) it put in my project/lib directory - very nice.
install
While I was messing around with ExternalProject_Add()'s install options I also learnt about the install target that CMake let's you use. I learnt CMake, again, has got you covered as you can install individual targets into distinct locations, install all the different types of libraries into their own directories too using commands like install(TARGETS <target> ARCHIVE DESTINATION <dest>), or instead of archive, library, module, etc. You can also install whole directories! That doesn't seem to grand but the realisation of its convenience of moving headers from the source tree into easier-to-access locations it makes organising the project for an orderly person like myself very simple. Directories, similar to the rest of the install() family, are installed like install(DIRECTORY <dir> DESTINATION <dest>).
I should point out that install is very, very flexible with external projects and you can do things like import various exported targets from external projects (the import/export is actually more elaborate, but I didn't go that far) and install them to desired places. I only lightly dabbled in this to begin with.
Variables
It was only a small discovery and since CMake's syntax is your run-of-the-mill scripting language, I figured that variables get interpolated as strings and so you can say something like "USE_${VAR}" and the result will be "USE_FOO", assuming of course VAR held the value of "FOO", but you get the idea. Similar to recursive structures in functional languages you can also build up a list of strings and store it into a variable (even into itself!) with something like set(VAR ${VAR} arg1 arg2...). Even when you use VAR for the first time it defaults to the empty string so this always works.
Control Structures
The last tool I needed to start my rudimentary build system was basic conditional and looping structures. Unsurprisingly CMake has if() where a boolean variable is tested to see if it's true (or non-empty I think?), foreach() for iterating over a list with a loop variable, and functions too. These were very easy to pick up as it was near identical to all other imperative languages which have these constructs, so not much to say here.
With the bare minimum knowledge of all of these topics under my belt I was actually able to write a CMakeLists.txt file which fit my needs! In a future post I'll detail what my plan is in relation to learning OpenGL and what I will put this build-system to use for.
Comments
Post a Comment