From b862cd7419df663b23cec056f5fdc09e919a8682 Mon Sep 17 00:00:00 2001 From: Rene Meusel Date: Fri, 27 Oct 2023 15:01:53 +0200 Subject: [PATCH 1/2] Handle the .lib/.dll pair explicitly on windows --- configure.py | 3 +++ src/scripts/install.py | 5 ++++- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/configure.py b/configure.py index 3af0bd1e6e2..6912dafc229 100755 --- a/configure.py +++ b/configure.py @@ -2315,6 +2315,9 @@ def test_exe_extra_ldflags(): if osinfo.soname_pattern_patch is not None: variables['soname_patch'] = osinfo.soname_pattern_patch.format(**variables) + if options.os == 'windows': + variables['implib_name'] = variables['static_lib_name'] + variables['lib_link_cmd'] = variables['lib_link_cmd'].format(**variables) for var in ['exe_link_cmd']: diff --git a/src/scripts/install.py b/src/scripts/install.py index f4aee90b8c2..b09f64f9a54 100755 --- a/src/scripts/install.py +++ b/src/scripts/install.py @@ -162,7 +162,7 @@ def copy_executable(src, dst): copy_file(full_header_path, prepend_destdir(os.path.join(target_include_dir, header))) - if build_static_lib or target_os == 'windows': + if build_static_lib: static_lib = cfg['static_lib_name'] copy_file(os.path.join(out_dir, static_lib), prepend_destdir(os.path.join(lib_dir, os.path.basename(static_lib)))) @@ -171,8 +171,11 @@ def copy_executable(src, dst): if target_os == "windows": libname = cfg['libname'] soname_base = libname + '.dll' + implib = cfg['implib_name'] copy_executable(os.path.join(out_dir, soname_base), prepend_destdir(os.path.join(bin_dir, soname_base))) + copy_file(os.path.join(out_dir, implib), + prepend_destdir(os.path.join(lib_dir, os.path.basename(implib)))) elif target_os == "mingw": shared_lib_name = cfg['shared_lib_name'] copy_executable(os.path.join(out_dir, shared_lib_name), From 3a052c423800761a13b7578cc47b5a4a7e1fbf2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ren=C3=A9=20Fischer?= Date: Fri, 27 Oct 2023 09:09:33 +0200 Subject: [PATCH 2/2] Ship CMake configurations with the library MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This generates botan-config-version.cmake and botan-config.cmake files and ships them with the library during the 'install' build step. These files facilitate the consumption of the library using CMake's find_package() in downstream applications. Co-Authored-By: René Meusel --- .gitignore | 1 - configure.py | 4 + src/build-data/botan-config-version.cmake.in | 16 +++ src/build-data/botan-config.cmake.in | 116 +++++++++++++++++ src/scripts/ci/cmake_tests/CMakeLists.txt | 128 +++++++++++++++++++ src/scripts/ci/cmake_tests/CMakePresets.json | 21 +++ src/scripts/ci/cmake_tests/main.cpp | 7 + src/scripts/ci_check_install.py | 33 +++++ src/scripts/install.py | 8 ++ 9 files changed, 333 insertions(+), 1 deletion(-) create mode 100644 src/build-data/botan-config-version.cmake.in create mode 100644 src/build-data/botan-config.cmake.in create mode 100644 src/scripts/ci/cmake_tests/CMakeLists.txt create mode 100644 src/scripts/ci/cmake_tests/CMakePresets.json create mode 100644 src/scripts/ci/cmake_tests/main.cpp diff --git a/.gitignore b/.gitignore index 169aa6ae565..f0b64ff482f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,4 @@ /Makefile -CMakeLists.txt* build.ninja .ninja_log libbotan*.so.* diff --git a/configure.py b/configure.py index 6912dafc229..f8c5ba08b49 100755 --- a/configure.py +++ b/configure.py @@ -3345,6 +3345,10 @@ def in_build_module_info(p): write_template(in_build_dir('build.h'), in_build_data('buildh.in')) write_template(in_build_dir('botan.doxy'), in_build_data('botan.doxy.in')) + robust_makedirs(in_build_dir("cmake")) + write_template(in_build_dir('cmake/botan-config.cmake'), in_build_data('botan-config.cmake.in')) + write_template(in_build_dir('cmake/botan-config-version.cmake'), in_build_data('botan-config-version.cmake.in')) + if 'botan_pkgconfig' in template_vars: write_template(template_vars['botan_pkgconfig'], in_build_data('botan.pc.in')) diff --git a/src/build-data/botan-config-version.cmake.in b/src/build-data/botan-config-version.cmake.in new file mode 100644 index 00000000000..890468d3ab5 --- /dev/null +++ b/src/build-data/botan-config-version.cmake.in @@ -0,0 +1,16 @@ +set(PACKAGE_VERSION %{version}) + +# Botan follows semver: +# * the requested version should be less or equal to the installed version, however +# * the requested major version must fit exactly. + +if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION) + set(PACKAGE_VERSION_COMPATIBLE FALSE) +elseif(PACKAGE_FIND_VERSION_MAJOR VERSION_LESS %{version_major}) + set(PACKAGE_VERSION_COMPATIBLE FALSE) +else() + set(PACKAGE_VERSION_COMPATIBLE TRUE) + if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION) + set(PACKAGE_VERSION_EXACT TRUE) + endif() +endif() diff --git a/src/build-data/botan-config.cmake.in b/src/build-data/botan-config.cmake.in new file mode 100644 index 00000000000..8d14c4e775b --- /dev/null +++ b/src/build-data/botan-config.cmake.in @@ -0,0 +1,116 @@ +#.rst: +# botan-config.cmake +# ----------- +# +# Find the botan library. +# +# This CMake configuration file, installed as part of the Botan build, +# provides support for find_package(Botan). +# +# Required version(s) can be passed as usual: +# find_package(Botan 3.3.0 REQUIRED) +# +# COMPONENTS and OPTIONAL_COMPONENTS can be used to specify Botan +# modules that must or should be enabled in the Botan build: +# find_package(Botan 3.3.0 COMPONENTS rsa ecdsa) +# +# IMPORTED Targets +# ^^^^^^^^^^^^^^^^ +# +# This module defines :prop_tgt:`IMPORTED` targets: +# +# ``Botan::Botan`` +# The botan shared library, if found. +# ``Botan::Botan-static`` +# The botan static library, if found. +# +# Result variables +# ^^^^^^^^^^^^^^^^ +# +# This module defines the following variables: +# +# :: +# +# Botan_FOUND - true if the headers and library were found +# Botan_VERSION - library version that was found, if any +# + +set(_Botan_supported_components +%{for mod_list} +%{i} +%{endfor} +) + +unset(${CMAKE_FIND_PACKAGE_NAME}_FOUND) +unset(_Botan_missing_required_modules) + +foreach(_comp IN LISTS ${CMAKE_FIND_PACKAGE_NAME}_FIND_COMPONENTS) + if (NOT _comp IN_LIST _Botan_supported_components) + set(${CMAKE_FIND_PACKAGE_NAME}_${_comp}_FOUND False) + if(${CMAKE_FIND_PACKAGE_NAME}_FIND_REQUIRED_${_comp}) + list(APPEND _Botan_missing_required_modules ${_comp}) + endif() + else() + set(${CMAKE_FIND_PACKAGE_NAME}_${_comp}_FOUND True) + endif() +endforeach() + +if(_Botan_missing_required_modules) + set(${CMAKE_FIND_PACKAGE_NAME}_FOUND False) + list(JOIN _Botan_missing_required_modules ", " _missing_modules) + set(${CMAKE_FIND_PACKAGE_NAME}_NOT_FOUND_MESSAGE "Unsupported module(s): ${_missing_modules}") +endif() + +if(DEFINED ${CMAKE_FIND_PACKAGE_NAME}_FOUND AND NOT ${${CMAKE_FIND_PACKAGE_NAME}_FOUND}) + return() +endif() + +# botan-config.cmake lives in "${_Botan_PREFIX}/lib/cmake/Botan-X": traverse up to $_Botan_PREFIX +set(_Botan_PREFIX "${CMAKE_CURRENT_LIST_DIR}") +get_filename_component(_Botan_PREFIX "${_Botan_PREFIX}" DIRECTORY) +get_filename_component(_Botan_PREFIX "${_Botan_PREFIX}" DIRECTORY) +get_filename_component(_Botan_PREFIX "${_Botan_PREFIX}" DIRECTORY) + +%{if build_static_lib} +if(NOT TARGET Botan::Botan-static) + add_library(Botan::Botan-static STATIC IMPORTED) + set_target_properties(Botan::Botan-static + PROPERTIES + IMPORTED_LOCATION "${_Botan_PREFIX}/lib/%{static_lib_name}" + INTERFACE_INCLUDE_DIRECTORIES "${_Botan_PREFIX}/include/botan-%{version_major}" + IMPORTED_LINK_INTERFACE_LANGUAGES "CXX" + INTERFACE_LINK_OPTIONS "SHELL:%{cxx_abi_flags}") +endif() +%{endif} + +%{if implib_name} +set(_Botan_implib "${_Botan_PREFIX}/lib/%{implib_name}") +set(_Botan_shared_lib "${_Botan_PREFIX}/bin/%{shared_lib_name}") +%{endif} +%{unless implib_name} +set(_Botan_implib "") +%{endif} + +%{if build_shared_lib} +if(NOT TARGET Botan::Botan) + if(NOT DEFINED _Botan_shared_lib) + set(_Botan_shared_lib "${_Botan_PREFIX}/lib/%{shared_lib_name}") + endif() + + add_library(Botan::Botan SHARED IMPORTED) + set_target_properties(Botan::Botan + PROPERTIES + IMPORTED_LOCATION "${_Botan_shared_lib}" + IMPORTED_IMPLIB "${_Botan_implib}" + INTERFACE_INCLUDE_DIRECTORIES "${_Botan_PREFIX}/include/botan-%{version_major}" + INTERFACE_LINK_OPTIONS "SHELL:%{cxx_abi_flags}") + set_property(TARGET Botan::Botan APPEND PROPERTY IMPORTED_CONFIGURATIONS NOCONFIG) + set_target_properties(Botan::Botan + PROPERTIES + IMPORTED_LOCATION_NOCONFIG "${_Botan_PREFIX}/lib/%{shared_lib_name}" + IMPORTED_SONAME_NOCONFIG "%{shared_lib_name}" + IMPORTED_IMPLIB_NOCONFIG "${_Botan_implib}") +endif() +%{endif} + +set(${CMAKE_FIND_PACKAGE_NAME}_FOUND True) diff --git a/src/scripts/ci/cmake_tests/CMakeLists.txt b/src/scripts/ci/cmake_tests/CMakeLists.txt new file mode 100644 index 00000000000..e1a546a26e1 --- /dev/null +++ b/src/scripts/ci/cmake_tests/CMakeLists.txt @@ -0,0 +1,128 @@ +cmake_minimum_required(VERSION 3.24) + +project(Botan_CMake_Tests CXX) + +set(CMAKE_CXX_STANDARD 20) +set(CMAKE_CXX_STANDARD_REQUIRED On) +set(CMAKE_CXX_EXTENSIONS Off) + +# test #1: find any Botan version and link dynamically +find_package(Botan REQUIRED) +if(NOT Botan_FOUND) + message(FATAL_ERROR "Test #1 failed: Failed to find Botan with any version") +endif() + +if(TARGET Botan::Botan) + add_executable(botan_version_test main.cpp) + target_link_libraries(botan_version_test Botan::Botan) + + if(WIN32) + add_custom_command(TARGET botan_version_test POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy $ $ + COMMAND_EXPAND_LISTS) + endif() + + add_test(NAME "Using shared library" + COMMAND botan_version_test + PASS_REGULAR_EXPRESSION "${Botan_VERSION}") +endif() + +unset(Botan_FOUND) + +# test #2: link statically +find_package(Botan REQUIRED) +if(TARGET Botan::Botan-static) + add_executable(botan_version_test_static main.cpp) + target_link_libraries(botan_version_test_static Botan::Botan-static) + + add_test(NAME "Using static library" + COMMAND botan_version_test_static + PASS_REGULAR_EXPRESSION "${Botan_VERSION}") +endif() + +unset(Botan_FOUND) + +# test #3: find Botan version installed by version number +find_package(Botan ${Botan_VERSION} REQUIRED) +if(NOT Botan_FOUND) + message(FATAL_ERROR "Failed to find Botan with version ${Botan_VERSION}") +endif() + +unset(Botan_FOUND) + +# test #4: find Botan with specific modules +find_package(Botan ${Botan_VERSION} COMPONENTS ecdsa rsa REQUIRED) +if(NOT Botan_ecdsa_FOUND OR NOT Botan_rsa_FOUND) + message(FATAL_ERROR "Package did not specify that the (optional) components were found: ecdsa, rsa") +endif() + +if(NOT Botan_FOUND) + message(FATAL_ERROR "Failed to find Botan with version ${Botan_VERSION} containing modules: ecdsa, rsa") +endif() + +unset(Botan_FOUND) + +# test #5: fail if required module is not included +find_package(Botan ${Botan_VERSION} COMPONENTS tpm QUIET) +if(Botan_FOUND) + message(FATAL_ERROR "Found Botan with version ${Botan_VERSION}: Found module tpm but expected it to be absent") +endif() + +unset(Botan_FOUND) + +# test #6: do not fail if optional module is not included +find_package(Botan ${Botan_VERSION} OPTIONAL_COMPONENTS tpm REQUIRED) +if(NOT Botan_FOUND) + message(FATAL_ERROR "Failed to find Botan with version ${Botan_VERSION}, despite missing optional modules: tpm") +endif() +if(NOT DEFINED Botan_tpm_FOUND OR ${Botan_tpm_FOUND}) + message(FATAL_ERROR "Failed to notify that the optional component was not found") +endif() + +unset(Botan_FOUND) + +# test #7: fail with some specific (but unavailable) component and fall-back to generic +find_package(Botan ${Botan_VERSION} COMPONENTS nonexistent unobtainium QUIET) +if(NOT Botan_FOUND) + # try again without requiring specific components + find_package(Botan ${Botan_VERSION}) +else() + message(FATAL_ERROR "Found Botan regardless of the non-existent component requirement") +endif() +if(NOT Botan_FOUND) + message(FATAL_ERROR "Failed to find generic Botan as a second attempt") +endif() + +unset(Botan_FOUND) + +# test #8: try to find a future version +math(EXPR Botan_FUTURE_MAJOR_VERSION "${Botan_VERSION_MAJOR} + 1") +find_package(Botan ${Botan_FUTURE_MAJOR_VERSION} QUIET) +if(Botan_FOUND) + message(FATAL_ERROR "Found a future major version that doesn't exist yet: ${Botan_FUTURE_MAJOR_VERSION}") +endif() + +unset(Botan_FOUND) + +# test #9: try to find an older version (that is not compatible with the installed one) +math(EXPR Botan_PAST_MAJOR_VERSION "${Botan_VERSION_MAJOR} - 1") +find_package(Botan ${Botan_PAST_MAJOR_VERSION} QUIET) +if(Botan_FOUND) + message(FATAL_ERROR "Found a legacy major version that doesn't exist yet: ${Botan_PAST_MAJOR_VERSION}") +endif() + +unset(Botan_FOUND) + +# test #10: try to find Botan with a different case-sensitive spelling +find_package(botan COMPONENTS rsa REQUIRED) +if(NOT botan_FOUND) + message(FATAL_ERROR "Failed to find Botan with a different case-spelling") +endif() +if(NOT botan_rsa_FOUND) + message(FATAL_ERROR "Failed to find Botan component with a different case-spelling") +endif() + +unset(Botan_FOUND) +unset(botan_FOUND) + +enable_testing() diff --git a/src/scripts/ci/cmake_tests/CMakePresets.json b/src/scripts/ci/cmake_tests/CMakePresets.json new file mode 100644 index 00000000000..68b0ef539fc --- /dev/null +++ b/src/scripts/ci/cmake_tests/CMakePresets.json @@ -0,0 +1,21 @@ +{ + "version": 5, + "cmakeMinimumRequired": { + "major": 3, + "minor": 24, + "patch": 0 + }, + "configurePresets": [ + { + "name": "windows_x86_64", + "architecture": "x64" + }, + { + "name": "windows_x86", + "architecture": "Win32" + }, + { + "name": "unix" + } + ] +} diff --git a/src/scripts/ci/cmake_tests/main.cpp b/src/scripts/ci/cmake_tests/main.cpp new file mode 100644 index 00000000000..632dc92d11d --- /dev/null +++ b/src/scripts/ci/cmake_tests/main.cpp @@ -0,0 +1,7 @@ +#include +#include + +int main() { + std::cout << Botan::short_version_string() << std::endl; + return 0; +} diff --git a/src/scripts/ci_check_install.py b/src/scripts/ci_check_install.py index e691bd05a5d..dd4d88e0120 100755 --- a/src/scripts/ci_check_install.py +++ b/src/scripts/ci_check_install.py @@ -6,6 +6,7 @@ This script is used to validate the results of `make install` (C) 2020 Jack Lloyd, René Meusel, Hannes Rantzsch +(C) 2023 René Meusel Botan is released under the Simplified BSD License (see license.txt) """ @@ -14,6 +15,7 @@ import sys import json import re +import subprocess def verify_library(build_config): lib_dir = build_config['libdir'] @@ -75,6 +77,34 @@ def verify_includes(build_config): return True +def verify_cmake_package(build_config): + if build_config['os'] not in ['windows', 'linux', 'macos']: + return True # skip (e.g. for mingw) + + cmake_build_dir = os.path.join(build_config['abs_root_dir'], build_config['build_dir'], 'cmake_test') + cmake_test_dir = os.path.join(build_config['abs_root_dir'], "src", "scripts", "ci", "cmake_tests") + + def cmake_preset(): + if build_config['os'] == 'windows': + return 'windows_x86_64' if build_config['arch'] == 'x86_64' else 'windows_x86' + return 'unix' + + def test_target(): + return 'test' if build_config['os'] != 'windows' else 'RUN_TESTS' + + try: + subprocess.run(["cmake", "--preset", cmake_preset(), + "-B", cmake_build_dir, + "-S", cmake_test_dir, + "-DCMAKE_PREFIX_PATH=%s" % build_config['prefix']], check=True) + subprocess.run(["cmake", "--build", cmake_build_dir, "--config", "Release"], check=True) + subprocess.run(["cmake", "--build", cmake_build_dir, "--config", "Release", "--target", test_target()], check=True) + except RuntimeError as e: + print("Using the CMake package failed: %s" % str(e)) + return False + + return True + def main(args=None): if args is None: args = sys.argv @@ -98,6 +128,9 @@ def main(args=None): if not verify_library(build_config): return 1 + if not verify_cmake_package(build_config): + return 1 + return 0 if __name__ == '__main__': diff --git a/src/scripts/install.py b/src/scripts/install.py index b09f64f9a54..81ba766f951 100755 --- a/src/scripts/install.py +++ b/src/scripts/install.py @@ -146,6 +146,7 @@ def copy_executable(src, dst): lib_dir = cfg['libdir'] target_include_dir = cfg['installed_include_dir'] pkgconfig_dir = 'pkgconfig' + cmake_dir = 'cmake' prefix = cfg['prefix'] @@ -209,6 +210,13 @@ def copy_executable(src, dst): copy_file(cfg['botan_pkgconfig'], prepend_destdir(os.path.join(pkgconfig_dir, os.path.basename(cfg['botan_pkgconfig'])))) + cmake_dir = os.path.join(prefix, lib_dir, cmake_dir, 'Botan-%s' % cfg["version"]) + makedirs(prepend_destdir(cmake_dir)) + copy_file('build/cmake/botan-config.cmake', + prepend_destdir(os.path.join(cmake_dir, 'botan-config.cmake'))) + copy_file('build/cmake/botan-config-version.cmake', + prepend_destdir(os.path.join(cmake_dir, 'botan-config-version.cmake'))) + if 'ffi' in cfg['mod_list'] and cfg['build_shared_lib'] is True and cfg['install_python_module'] is True: for ver in cfg['python_version'].split(','): py_lib_path = os.path.join(lib_dir, 'python%s' % (ver), 'site-packages')