[CMake] Please critique my "hello world" CMakeLists.txt and Config.cmake

Alexander Neundorf a.neundorf-work at gmx.net
Sun Feb 24 15:37:58 EST 2013


Hi Chris,

On Sunday 24 February 2013, Chris Stankevitz wrote:
> On Sun, Feb 24, 2013 at 2:20 AM, Alexander Neundorf <neundorf at kde.org> 
wrote:
> > * "export" the target (use the EXPORT option in install(TARGETS))
> > * the install this "export", using install(EXPORT ...), this will install
> > a cmake script file
> > * include() this export-file in the Config.cmake file
> > * use configure_package_config_file() instead of the plain
> > configure_file() to configure the file. This will help with absolute and
> > relative paths etc.
> 
> Alexander,
> 
> First, thank you for your reply.  This was exactly the kind of help I
> was looking for.
> 
> Second, I attempted to follow your instructions.  I am a big proponent
> of cmake at work.  I love how simple it is to create a "hello world"
> project (particularly when compared to autotools).  However, my head
> is spinning with the new jargon needed to properly install a simple
> "hello world" package.  Even after reading the docs and wiki I don't
> really understand what I am doing, I more or less just tried to copy
> it.  Most of the EXPORT and CONFIGURE_PACKAGE docs seem to be geared
> toward someone who already knows the "old" way of installing and just
> needs a refresher to the new style.  I hope my "hello" project, once
> it meets your approval, can be used to assist other new packagers in
> the future.
> 
> Third, would you please take a look at my updated "hello" project and
> the new "hello-client" project?  As you might imagine, hello-client is
> an executable that uses the hello library.  I have done something
> wrong in hello, hello-client, or both as hello-client fails to
> compile.  I suspect that among other mistakes, I should have some
> SET_AND_CHECKs in my Config.cmake.in file.  I also suspect that my
> hello-client is missing a FIND_PACKAGE reference.
> 
> github links:
> https://github.com/chrisstankevitz/hello
> https://github.com/chrisstankevitz/hello-client

I had a look, the find_package() is missing indeed.


Maybe we'll start with the old way and get to the new way.

Your client should look like this:

cmake_minimum_required(VERSION 2.8)
project(hello-client)

find_package(hello REQUIRED NO_MODULE)

include_directories( ${hello_INCLUDE_DIRS} )

add_executable(hello-client hello-client.cpp)

target_link_libraries(hello-client ${hello_LIBRARIES} )



I think this is what you probably also expected, but maybe got confused due to 
the talking about importing and exporting targets.

Since you want find_package() to search for a Config.cmake file, I recommend 
to make this explicit by adding the NO_MODULE keyword. This way people looking 
at the CMakeLists.txt can know that a Config.cmake file is searched, not a 
Findhello.cmake.
This Config.cmake file should, as has always been the case, set the 
<name>_INCLUDE_DIRS and <name>_LIBRARIES variables, which you should use as 
always.


Now to the Config.cmake file.
In the most basic case it would look like that:

set(hello_LIBRARIES /some/path/libhello.a)
set(hello_INCLUDE_DIRS /some/path/include/ )


This would already kind of work.
It has the problem that the directories are hardcoded, so e.g. under Windows a 
user could not install the package in a directory of his choice, i.e. it is 
not "relocatable".
Also, since the variables are simply set, there is not guarantee that the 
directories and files actually exist.

So if you use the configure_package_config_file() macro to configure that 
file, you put the @PACKAGE_INIT@ "macro" (this time really a macro, cmake will 
fill in code there) at the top, and this provides the set_and_check() 
function.
Then your file could look like this:


@PACKAGE_INIT@
set(hello_LIBRARIES /some/path/libhello.a)
set_and_check(hello_INCLUDE_DIRS /some/path/include/ )


This would still not be relocatable, but the set_and_check() would produce an 
error if the directory /some/path/include/ does not exist. This is good, since 
you would get an error at cmake time, and not at build time.
This error case should only happen if something went wrong, the install 
process was interrupted, files were moved or deleted manually, or something 
similar.

Now how do we get this file relocatable.
If you do this:


@PACKAGE_INIT@
set(hello_LIBRARIES /some/path/libhello.a)
set_and_check(hello_INCLUDE_DIRS @hello_DIRNAME_include@ )


and then call configure_package_config_file(), you'll get the full absolute 
hardcoded path again as above.
If you do instead


@PACKAGE_INIT@
set(hello_LIBRARIES /some/path/libhello.a)
set_and_check(hello_INCLUDE_DIR @PACKAGE_hello_DIRNAME_include@ )
set(hello_INCLUDE_DIRS ${hello_INCLUDE_DIR} )


and call
configure_package_config_file(... PATH_VARS hello_DIRNAME_include ...)
then cmake will put in some code so that the Config.cmake file will not have 
the absolute path to the include dir, but the relative path to the include, 
starting from the installed Config.cmake file itself.
(This is a bit tricky, you can have a look at the generated code in the 
Config.cmake file).

In the example above there are the variables hello_INCLUDE_DIR and 
hello_INCLUDE_DIRS. The second, plural one is the "public" variable, which is 
potentially a list of directories, this one should be used in your client 
programs.
But set_and_check() can only set a single variable, because it can only check 
a single directory, so the helper variable hello_INCLUDE_DIR is needed.

So, with this you get a relocatable ${hello_INCLUDE_DIRS}.


Now to the importing and exporting of targets (and let's ignore whether it is 
relocatable or not for now).

When doing find_library() for some installed project, the author of the Find-
module has to know the details about the library to be found, and he gas to 
keep the Find-module up-to-date when new versions of the library are released. 
If the library is a static library, he has to search for potentially needed 
dependent libraries too.
Especially under Windows the author of the Find-module must take care of 
debug- and release-versions of the library, getting the wrong one will make 
the executable crash.

The idea is that Config.cmake files are provided by the project itself, so it 
can contain "first hand" information.

In a simple case the Config.cmake file you could write a Config.cmake file 
look like this (ignoring the include dirs, since we had that already above):


add_library(hello IMPORTED)
set_target_properties(hello PROPERTIES
             IMPORTED_LOCATION /some/path/libhello.a)
set(hello_LIBRARIES hello)

set(hello_INCLUDE_DIRS ... )


When this file is loaded by the client project, it will create a library 
target, which you can use like a normal target and link against it.
But the "IMPORTED" keyword says that it is not built in the project, but 
exists already, and just a virtual reference to that existing library is 
created. By setting target properties on this "imported" library the actual 
path to the file on disk is attached to this target.

So you could then do

add_executable(whatever main.cpp)
target_link_libraries(whatever hello)

and cmake would see that hello is an imported target, and via the target 
property it would see which file this actually is on disk.

By setting the ${hello_LIBRARIES} variable in the Config.cmake file the client 
project does not have to care whether this is an imported target or a path to 
a file on disk, but can simply use that variable and it will work.

Now writing those

add_library(hello IMPORTED)
set_target_properties(...)

lines manually is not easy and error-prone, so cmake can do this better for 
you.

If you want to create that information for a library you want to install, you 
put that library into an "export" (a set of targets which will be installed 
and exported, so they can be imported again later by clients):
install(TARGETS hello DESTINATION ... EXPORT MyExports)

This just puts the target into a set of targets, which can be referenced by 
the given name "MyExports". You can choose any name you want.

This export then also has to be installed:

install(EXPORT MyExport FILE HelloTargets.cmake DESTINATION ... )

This call will actually create a file called HelloTargets.cmake, which 
contains these add_library(... IMPORTED ...) calls for all targets which have 
been put into this export set.
Actually it does more than that, but this is the basic idea.
You can have a look at the installed files, it additionally adds checks that 
the files actually exist on disk, it sets a whole bunch more target 
properties, like which dependent libraries have to be linked too, whether it 
is a debug or release or some other configurarion build, etc.

But the basic idea is that this creates a file which contains the code which 
creates the add_library(... IMPORTED ...) calls.

Now to get these imported targets into your Config.cmake file, you have to 
include() the file:

include(${CMAKE_CURRENT_LIST_DIR}/HelloTargets.cmake)

but you may do this only once in a project, otherwise a target with the same 
name would be created twice, so a check for that has to be added.

So the full helloConfig.cmake.on file should look like this:

@PACKAGE_INIT@

set_and_check(hello_INCLUDE_DIR @PACKAGE_hello_DIRNAME_include@ )

if(NOT TARGET hello)
  include(${CMAKE_CURRENT_LIST_DIR}/HelloTargets.cmake)
endif()

set(hello_INCLUDE_DIRS ${hello_INCLUDE_DIR} )
set(hello_LIBRARIES hello)


I hope this helps.

Alex

P.S. if this is helpful, feel free to put it in the cmake wiki :-)


More information about the CMake mailing list