Skip to content

Find or install a package, aka lookup

Mayeul d'Avezac edited this page May 16, 2014 · 6 revisions

The objective is to define a way to either find a package with find_package and/or, depending on choices and circumstances, download and install that package.

The user should only have to include this file and add to their cmake files:

include(PackageLookup)
lookup_package(<name>    # Name for find_package and lookup recipe files
   [QUIET]               # Whether to avoid making noise about the whole process
   [REQUIRED]            # Fails if package can neither be found nor installed
   [DOWNLOAD_BY_DEFAULT] # Always dowload, build, and install package locally. Does not look for
                         # pre-installed packages. This ensures the external project is always
                         # compiled specifically for this project.
   [CHECK_EXTERNAL]      # Makes it possible to cache the external project builds.
                         # If this option and DOWNLOAD_BY_DEFAULT are both set, then it will look
                         # for the package in the directory defined by EXTERNAL_ROOT.
                         # If DOWNLOAD_BY_DEFAULT is *not* set, then this option does nothing.
                         # It is a convenience options for developers. 
   [KEEP]                # Keep the external project in the build tree after installation.
                         # By default, once it is detected in a subsequent CMake step, 
                         # it is forgotten, and hence not rebuilt.
   [...]                 # Arguments passed on to `find_package`.
   [ARGUMENTS <list>]    # Arguments specific to the look up recipe.
                         # They will be available inside the recipe under the variable
                         # ${name}_ARGUMENTS. Lookup recipes also have access to EXTERNAL_ROOT,
                         # a variable specifying a standard location for external projects in the
                         # build tree
)

This will first attempt to call find_package(name [...]) (with QUIET and without REQUIRED). If the package is not found, then it will attempt to find a lookup recipe for the package. This recipe should configure an external project that will install the missing package during the building process.

All external lookup targets are dependees of the custom cmake target lookup_dependencies. It is recommended that targets that depend on the external packages should be made to depend on lookup_dependencies. This is made a bit easier via the macro:

# Makes sure TARGET is built after the looked up projects.
depends_on_lookups(TARGET)

The name should match that of an existing find_package(<name>) file. The lookup_package function depends on files in directories in the cmake prefixes paths that have the name of the package:

  • ${CMAKE_MODULE_PATH}/${package}/${package}-lookup.cmake
  • ${CMAKE_MODULE_PATH}/${package}/LookUp${package}.cmake
  • ${CMAKE_MODULE_PATH}/${package}/lookup.cmake
  • ${CMAKE_MODULE_PATH}/${package}-lookup.cmake
  • ${CMAKE_MODULE_PATH}/LookUp${package}.cmake
  • ${CMAKE_MODULE_PATH}/${package}-lookup.cmake
  • ${CMAKE_LOOKUP_PATH}/${package}-lookup.cmake
  • ${CMAKE_LOOKUP_PATH}/LookUp${package}.cmake

These files are included when the function lookup_package is called. The files will generally have the structure:

# Parses arguments specific to the lookup recipe
# Optional step. Below, only a URL single-valued argument is specified.
if(package_ARGUMENTS)
    cmake_parse_arguments(package "" "URL" "" ${package_ARGUMENTS})
else()
    set(package_URL https://gaggledoo.doogaggle.com)
endif()
# The external project name `<package>` must match the package name exactly
ExternalProject_Add(package
  URL ${URL_
)
# Reincludes cmake so newly installed external can be found via find_package.
# Optional step.
add_recursive_cmake_step(...)

This pattern will first attempt to find the package on the system. If it is not found, an external project to create it is added, with an extra step to rerun cmake and find the newly installed package.

If a package is not found on the first call to configure, and then subsequently installed during the make process, it can be interesting to have the package found on a second automatic pass of configure. This is what the function add_recursive_cmake_step does. It adds a call to cmake as the last step of downloading, building, and installing an external project.

# Adds a custom step to the external project that calls cmake recusively
# This makes it possible for the newly built package to be installed.
add_recursive_cmake_step(<name> # Still the same package name
   <${name}_FOUND> # Variable set to true if package is found
   [...]           # Passed on to ExternalProject_Add_Step
                   # in general, it will be `DEPENDEES install`,
                   # making this step the last.
)
~~~


Finally, it is possible to add a post-lookup hook: a script that is executed if and only if the package is to be downloaded through look-up. It is executed on the second and subsequent call to cmake. In practice, this means it likely requires a recursive cmake step as given above. It can be used, for instance, to add install targets/scripts, or modify/add variables for whatever purpose. The hooks will be executed *prior* to trying to find the looked up packages. The process is to make a call to the following function *from within a recipe*.

~~~CMake
write_lookup_hook(<hook>   # hook=POST_LOOKUP for now, other hooks may follow
  <package>                # Same package name as the recipe from which this is called
  "line1\n"                # Will be written to a CMake script as is
  "line2\n"
  ...
)
~~~

In practice, the look-up process is as follows for any given package:

~~~
1. If lookup recipe has already been executed:
       - if a POST_LOOKUP hook exists, execute it
2. If DOWNLOAD_BY_DEFAULT is False:
       - execute find_package(...)
   Otherwise, if CHECK_EXTERNAL is True:
       - change the root to EXTERNAL_ROOT. This means packages/library/paths are found only within the directory pointed to EXTERNAL_ROOT.
       - do find_package(...)
       - undo root setup
3. If the package was not found:
       - look for a recipe. An error is thrown if no recipe is found.
       - execute the recipe (via include) 
~~~