How do I correctly check for crypt() with CMake?

Viewed 143

First of all, please don’t lecture me about using crypt() – I know what it does and what it does not. I have a little C program that is supposed to use the very crypt() that the OS is using to check user passwords (assuming it does, I know OpenBSD does not).

However, crypt() is a little different in every libc: some require unistd.h, some require crypt.h, some require no extra libraries, some require -lcrypt. And there’s libxcrypt as well, which is starting to replace glibc’s long deprecated crypt().

Long story short: I’m new to CMake, and while checking for something with FindSomething.cmake or pkg-config is easy, checking for which demons to invoke for libc crypt() is not, and I feel like I’m doing it wrong. It works, but looks way too complicated.

Also, I’d like to try using libxcrypt if crypt() wasn’t found with other methods. How can I do that without repeating half the checks?

cmake_minimum_required(VERSION 3.10)
project(pwhash)

set(USE_LIBXCRYPT OFF CACHE BOOL "Use libxcrypt for crypt()")

find_package(PkgConfig REQUIRED)

add_executable(pwhash pwhash.c)

if(USE_LIBXCRYPT)
  pkg_check_modules(LIBXCRYPT IMPORTED_TARGET REQUIRED libxcrypt)
  target_link_libraries(pwhash PUBLIC PkgConfig::LIBXCRYPT)
  target_compile_definitions(pwhash PUBLIC HAVE_CRYPT_H)
else()
  include(CheckIncludeFile)
  check_include_file(crypt.h HAVE_CRYPT_H)
  if(HAVE_CRYPT_H)
    list(APPEND CMAKE_REQUIRED_INCLUDES "crypt.h")
    target_compile_definitions(pwhash PUBLIC HAVE_CRYPT_H)
  endif()
  include(CheckSymbolExists)
  unset(HAVE_CRYPT CACHE)
  check_symbol_exists(crypt "unistd.h" HAVE_CRYPT)
  if(NOT HAVE_CRYPT)
    unset(HAVE_CRYPT CACHE)
    list(APPEND CMAKE_REQUIRED_LIBRARIES "crypt")
    check_symbol_exists(crypt "unistd.h" HAVE_CRYPT)
    if(HAVE_CRYPT)
      target_link_libraries(pwhash PUBLIC crypt)
    else()
      error("No crypt() found")
    endif()
  endif()
endif()
1 Answers

However, crypt() is a little different in every libc: some require unistd.h, some require crypt.h, some require no extra libraries, some require -lcrypt. And there’s libxcrypt as well, which is starting to replace glibc's long deprecated crypt().

As far as I know, the crypt function is declared in unistd.h on all major implementations. That includes macOS, FreeBSD, OpenBSD, and Linux. The extended set of crypt functions, like crypt_r, crypt_rn, and crypt_ra are in <crypt.h>. I'm curious to know which libcs do not have crypt in unistd.h.

Also, I’d like to try using libxcrypt if crypt() wasn’t found with other methods. How can I do that without repeating half the checks?

For this, we'll write a function to encapsulate each method. Each function will try to define an interface library pwhash_crypt that encapsulates the usage requirements for the crypt() function.

We'll start with the boilerplate:

cmake_minimum_required(VERSION 3.20)
project(pwhash LANGUAGES C)

First, we'll look at find_libxcrypt

function(find_libxcrypt)
  if (TARGET pwhash_crypt)
    return()
  endif ()

  find_package(PkgConfig REQUIRED)
  pkg_check_modules(LIBXCRYPT IMPORTED_TARGET libxcrypt)

  if (TARGET PkgConfig::LIBXCRYPT)
    add_library(pwhash_crypt INTERFACE)
    target_link_libraries(pwhash_crypt INTERFACE PkgConfig::LIBXCRYPT)
    target_compile_definitions(pwhash_crypt INTERFACE HAVE_CRYPT_H)
  endif ()
endfunction()

This should be fairly straightforward. If the pwhash_crypt target does not exist, then it will try to use pkg-config to find libxcrypt and create pwhash_crypt if so. Libraries propagate their interface properties to the targets linking to them, so later when we link pwhash to pwhash_crypt, it will have HAVE_CRYPT_H defined.

The function for the system crypt implementation find_sys_crypt is similarly straightforward:

function(find_sys_crypt)
  if (TARGET pwhash_crypt)
    return()
  endif ()

  include(CheckIncludeFile)
  include(CheckSymbolExists)

  set(defs "")
  set(CMAKE_REQUIRED_INCLUDES "")
  set(CMAKE_REQUIRED_LIBRARIES "")

  check_include_file("crypt.h" HAVE_CRYPT_H)
  if (HAVE_CRYPT_H)
    list(APPEND defs HAVE_CRYPT_H)
    list(APPEND CMAKE_REQUIRED_INCLUDES "crypt.h")
  endif ()

  check_symbol_exists(crypt "unistd.h" HAVE_CRYPT)
  if (NOT HAVE_CRYPT)
    list(APPEND CMAKE_REQUIRED_LIBRARIES crypt)
    check_symbol_exists(crypt "unistd.h" HAVE_CRYPT_WITH_LIB)
  endif ()

  if (HAVE_CRYPT OR HAVE_CRYPT_WITH_LIB)
    add_library(pwhash_crypt INTERFACE)
    target_link_libraries(pwhash_crypt INTERFACE ${CMAKE_REQUIRED_LIBRARIES})
    target_compile_definitions(pwhash_crypt INTERFACE ${defs})
  endif ()
endfunction()

This follows the same structure as the previous function and the checks in your question.

Then you can give users an option whether to look for libxcrypt first or second and then link the result to your pwhash executable:

option(PREFER_LIBXCRYPT "Prefer libxcrypt for crypt()" OFF)
if (PREFER_LIBXCRYPT)
  find_libxcrypt()
  find_sys_crypt()
else ()
  find_sys_crypt()
  find_libxcrypt()
endif ()

if (NOT TARGET pwhash_crypt)
  message(FATAL_ERROR "Could not find a suitable crypt() implementation")
endif ()

add_executable(pwhash pwhash.c)
target_link_libraries(pwhash PRIVATE pwhash_crypt)
Related