3
\$\begingroup\$

The following module AssertLibraryFunction improves upon CheckLibrariesExists in two respects:

  • It honors the optional argument REQUIRED of find_package: if the check fails, it raises a FATAL_ERROR. It also honors QUIET.
  • Its result is cached in a variable named ${LIBNAME}_${FUNCTION}. Therefore, argument 4 of CheckLibrariesExists, VARIABLE, is no longer needed.

Of course this comes at a slight expense in flexibility. Nevertheless, I believe this module could help substantially in writing compact Find<Package>.cmake modules.

Questions:

  • Is this novel functionality, or could I achieve about the same, with comparable brevity, with an existing module?
  • Would it make sense to propose AssertLibraryFunction for inclusion in future cmake versions?
  • Regardless of the above, are there ways to improve my cmake coding style?

Here the module AssertLibraryFunction:

#.rst:
# AssertLibraryFunction
# ---------------------
#
# ASSERT_LIBRARY_FUNCTION checks whether given libraries contain
# a given function. If this is not the case, a fatal error is raised.
#
# CHECK_LIBRARY_EXISTS (LIBNAME FUNCTION LOCATION)
#
# ::
#
#   LIBNAME  - library name (case sensitive)
#   FUNCTION - name of the function to be searched in ${LIBNAME}_LIBRARIES
#   LOCATION - where the library should be found (if unsure, use "")
#
#
# The following variables may be set before calling this macro to modify
# the way the check is run:
#
# ::
#
#   CMAKE_REQUIRED_FLAGS = string of compile command line flags
#   CMAKE_REQUIRED_DEFINITIONS = list of macros to define (-DFOO=bar)
#   CMAKE_REQUIRED_LIBRARIES = list of libraries to link
#   CMAKE_REQUIRED_QUIET = execute quietly without messages
#
# This function is meant to be used in Find<Package>.cmake modules,
# which in turn should be called through
#
# ::
#
#   find_package(<Package> [QUIET] [REQUIRED])
#
# Within a Find<Package>.cmake module, find_package_handle_standard_args
# must be called before any call of ASSERT_LIBRARY_FUNCTION.
# Typically, Find<Package>.cmake looks like the following:
#
# ::
# 
#   find_path(<Package>_INCLUDE_DIR <include_file>)
#   find_library(<Package>_LIBRARIES NAMES <library_name> <Package>)
#   
#   include(FindPackageHandleStandardArgs)
#   find_package_handle_standard_args(<Package> DEFAULT_MSG <Package>_LIBRARIES <Package>_INCLUDE_DIR)
#   
#   include(AssertLibraryFunction)
#   assert_library_function(<Package> <function_name> "")
#   
#   mark_as_advanced(<Package>_INCLUDE_DIR <Package>_LIBRARIES)
#   
# The result of ASSERT_LIBRARY_FUNCTION is cached in a variable named
# ${LIBNAME}_${FUNCTION}.

#=============================================================================
# Based on CheckLibrariesExists (Copyright 2002-2009 Kitware, Inc.)
# Author: Joachim Wuttke (Copyright 2015 Forschungszentrum Jülich)
# License: BSD (see cmake License for details)
#=============================================================================


macro(alf_status_message _msg)
    if( ${LIBNAME}_FIND_QUIETLY )
    else()
        message(STATUS ${_msg})
    endif()
endmacro()

function(ASSERT_LIBRARY_FUNCTION LIBNAME FUNCTION LOCATION)
    set(LIBRARY ${${LIBNAME}_LIBRARIES})
    set(VARIABLE ${LIBNAME}_${FUNCTION})
    set(_MSG "Search ${FUNCTION} in ${LIBRARY}")
    if(DEFINED "${VARIABLE}")
        if(${${VARIABLE}})
            alf_status_message("${_MSG} -- cached")
            return()
        endif()
    endif()
    alf_status_message("Search ${FUNCTION} in ${LIBRARY}")
    set(MACRO_CHECK_LIBRARY_EXISTS_DEFINITION
        "-DCHECK_FUNCTION_EXISTS=${FUNCTION} ${CMAKE_REQUIRED_FLAGS}")
    set(CHECK_LIBRARY_EXISTS_LIBRARIES ${LIBRARY})
    if(CMAKE_REQUIRED_LIBRARIES)
        set(CHECK_LIBRARY_EXISTS_LIBRARIES
            ${CHECK_LIBRARY_EXISTS_LIBRARIES} ${CMAKE_REQUIRED_LIBRARIES})
    endif()
    try_compile(COMPILE_OK
        ${CMAKE_BINARY_DIR}
        ${CMAKE_ROOT}/Modules/CheckFunctionExists.c
        COMPILE_DEFINITIONS ${CMAKE_REQUIRED_DEFINITIONS}
        LINK_LIBRARIES ${CHECK_LIBRARY_EXISTS_LIBRARIES}
        CMAKE_FLAGS
        -DCOMPILE_DEFINITIONS:STRING=${MACRO_CHECK_LIBRARY_EXISTS_DEFINITION}
        -DLINK_DIRECTORIES:STRING=${LOCATION}
        OUTPUT_VARIABLE OUTPUT)

    if(${COMPILE_OK})
        if(NOT CMAKE_REQUIRED_QUIET)
            alf_status_message("${_MSG} -- found")
        endif()
        set(${VARIABLE} 1 CACHE INTERNAL "Library ${LIBRARY} has ${function}")
        file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeOutput.log
            "Determining if the function ${FUNCTION} exists in the ${LIBRARY} "
            "passed with the following output:\n"
            "${OUTPUT}\n\n")
    else()
        set(${VARIABLE} "" CACHE INTERNAL "Library ${LIBRARY} has no ${function}")
        file(APPEND ${CMAKE_BINARY_DIR}${CMAKE_FILES_DIRECTORY}/CMakeError.log
            "Determining if the function ${FUNCTION} exists in the ${LIBRARY} "
            "failed with the following output:\n"
            "${OUTPUT}\n\n")
        if( ${LIBNAME}_FIND_REQUIRED )
            message(FATAL_ERROR "${_MSG} -- not found")
        else()
            alf_status_message("${_MSG} -- not found")
        endif()
    endif()
endfunction()

And here is a sample module FindReadline.cmake that would use the above:

# Find libreadline
#
# Usage:
#   find_package(Readline [REQUIRED] [QUIET])
#
# Sets the following variables:
#   - Readline_FOUND        .. true if library is found
#   - Readline_LIBRARIES    .. full path to library
#   - Readline_INCLUDE_DIR  .. full path to include directory
#
# Copyright 2015 Joachim Wuttke, Forschungszentrum Jülich.
# Redistribution permitted.

find_path(Readline_INCLUDE_DIR readline/readline.h)
find_library(Readline_LIBRARIES NAMES readline Readline)

include(FindPackageHandleStandardArgs) # handles QUIET, REQUIRED; sets Readline_FOUND
find_package_handle_standard_args(Readline DEFAULT_MSG Readline_LIBRARIES Readline_INCLUDE_DIR)

include(AssertLibraryFunction)
assert_library_function(Readline rl_initialize "")
assert_library_function(Readline add_history "")

mark_as_advanced(Readline_INCLUDE_DIR Readline_LIBRARIES)
\$\endgroup\$
1
\$\begingroup\$

I think your changes would definetly improve CHECK_LIBRARY_EXISTS().

If I understand correctly you want to improve upon something like

cmake_push_check_state()
set(CMAKE_REQUIRED_QUIET ${Readline_FIND_QUIETLY})
check_library_exists("${Readline_LIBRARIES}" rl_initialize "" Readline_rl_initialize)
cmake_pop_check_state()

with

assert_library_function(Readline rl_initialize "")

assert_library_function() Review

After giving your code some test runs, here are my thoughts:

  1. If I think about the LIBNAME parameter more in terms of a PACKAGENAME parameter, then you can make use of CMAKE_FIND_PACKAGE_NAME:

    macro(alf_status_message _msg)
        if(NOT DEFINED CMAKE_FIND_PACKAGE_NAME OR 
           NOT "${${CMAKE_FIND_PACKAGE_NAME}_FIND_QUIETLY}")
            message(STATUS ${_msg})
        endif()
    endmacro()
    
    • The fact that CMAKE_FIND_PACKAGE_NAME is defined inside a find_package() call gives two new possibilities
      • omitting the LIBNAME parameter of ASSERT_LIBRARY_FUNCTION() completely
      • or extending CHECK_LIBRARY_EXISTS() itself with this kind of checks
  2. With one of my first tries the find module was delivering ${${LIBNAME}_LIBRARIES} as Readline_LIBRARIES-NOTFOUND. So your function should probably check and exit if the library was not found with e.g.

    if (NOT LIBRARY)
        return()
    endif()
    
  3. With your approach to take the content of Xxx_LIBRARIES I think you can remove the LOCATION parameter from ASSERT_LIBRARY_FUNCTION. The libraries will come as full paths. See

  4. The last one is a very small one: to keep in mind the I possibly handle a list of libraries, I would rename local variable LIBRARY to LIBRARIES

Side Note

Just as a side note FindBZip2.cmake has just recently switched to the use of CHECK_SYMBOL_EXISTS():

cmake_push_check_state()
set(CMAKE_REQUIRED_QUIET ${BZip2_FIND_QUIETLY})
CHECK_LIBRARY_EXISTS("${BZIP2_LIBRARIES}" BZ2_bzCompressInit "" BZIP2_NEED_PREFIX)
cmake_pop_check_state()

With the following comment for the commit:

Run our check for the '_' prefix using a bzip2 API function as declared with a prototype in the real header file. This is needed in case the function is provided in a DLL import library where the symbol name may not match without proper markup from the header.

Footnote

Ever since I have read the following comment in the CMP0060 documentation

In fact, many Find Modules are learning to provide Imported Targets instead of just the traditional Foo_LIBRARIES variable listing library files.

the thought of "this being something all find modules should have" never left my mind. So here are my five cents for FindReadline.cmake:

find_path(Readline_INCLUDE_DIR readline/readline.h)
find_library(Readline_LIBRARIES NAMES readline Readline)

include(FindPackageHandleStandardArgs) # handles QUIET, REQUIRED; sets Readline_FOUND
find_package_handle_standard_args(Readline DEFAULT_MSG Readline_LIBRARIES Readline_INCLUDE_DIR)

if(Readline_FOUND AND NOT TARGET Readline::Readline)
    add_library(Readline::Readline UNKNOWN IMPORTED)
    set_target_properties(
        Readline::Readline 
        PROPERTIES
            IMPORTED_LOCATION "${Readline_LIBRARY}"
            INTERFACE_INCLUDE_DIRECTORIES "${Readline_INCLUDE_DIR}"
    )
endif()

include(AssertLibraryFunction)
assert_library_function(Readline rl_initialize "")
assert_library_function(Readline add_history "")

mark_as_advanced(Readline_INCLUDE_DIR Readline_LIBRARIES)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.