From 8f89f080314d7c6fd6e4fe3634f6f2c6d9d4cd0e Mon Sep 17 00:00:00 2001 From: Silvio Traversaro Date: Tue, 22 Nov 2022 00:05:55 +0100 Subject: [PATCH] Add functions to enable binary relocatability in downstream libraries Signed-off-by: Silvio Traversaro --- cmake/GzUtils.cmake | 178 +++++++++++++++++- test/CMakeLists.txt | 6 + test/get_install_prefix/CMakeLists.txt | 57 ++++++ .../get_install_prefix_test.cc | 82 ++++++++ .../get_install_prefix_test_shared.h | 13 ++ .../get_install_prefix_test_static.h | 13 ++ 6 files changed, 347 insertions(+), 2 deletions(-) create mode 100644 test/get_install_prefix/CMakeLists.txt create mode 100644 test/get_install_prefix/get_install_prefix_test.cc create mode 100644 test/get_install_prefix/get_install_prefix_test_shared.h create mode 100644 test/get_install_prefix/get_install_prefix_test_static.h diff --git a/cmake/GzUtils.cmake b/cmake/GzUtils.cmake index 8ed3a2a3..3fdda222 100644 --- a/cmake/GzUtils.cmake +++ b/cmake/GzUtils.cmake @@ -1801,7 +1801,8 @@ endmacro() # SOURCES # [LIB_DEPS ] # [INCLUDE_DIRS ] -# [TEST_LIST ]) +# [TEST_LIST ] +# [ENVIRONMENT ]) # # Build tests for a Gazebo project. Arguments are as follows: # @@ -1826,6 +1827,8 @@ endmacro() # will also skip the step of copying the runtime library # into your executable's directory. # +# ENVIRONMENT: Optional. Used to set the ENVIRONMENT property of the tests. +# macro(ign_build_tests) # TODO(chapulina) Enable warnings after all libraries have migrated. # message(WARNING "ign_build_tests is deprecated, use gz_build_tests instead.") @@ -1846,7 +1849,7 @@ macro(gz_build_tests) # Define the expected arguments set(options SOURCE EXCLUDE_PROJECT_LIB) # NOTE: DO NOT USE "SOURCE", we're adding it here to catch typos set(oneValueArgs TYPE TEST_LIST) - set(multiValueArgs SOURCES LIB_DEPS INCLUDE_DIRS) + set(multiValueArgs SOURCES LIB_DEPS INCLUDE_DIRS ENVIRONMENT) #------------------------------------ # Parse the arguments @@ -1918,6 +1921,10 @@ macro(gz_build_tests) add_test(NAME ${target_name} COMMAND ${target_name} --gtest_output=xml:${CMAKE_BINARY_DIR}/test_results/${target_name}.xml) + if(gz_build_tests_ENVIRONMENT) + set_property(TEST ${target_name} PROPERTY ENVIRONMENT ${gz_build_tests_ENVIRONMENT}) + endif() + if(UNIX) # gtest requies pthread when compiled on a Unix machine target_link_libraries(${target_name} pthread) @@ -1971,3 +1978,170 @@ macro(_gz_cmake_parse_arguments prefix options oneValueArgs multiValueArgs) endif() endmacro() + +################################################# +# gz_add_get_install_prefix_impl(GET_INSTALL_PREFIX_FUNCTION +# GET_INSTALL_PREFIX_HEADER +# OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE ) +# +# This macro adds to ${PROJECT_LIBRARY_TARGET_NAME} the implementation of +# of the function passed by GET_INSTALL_PREFIX_FUNCTION and declared in +# GET_INSTALL_PREFIX_HEADER . +# +# The defined functions implements a GET_INSTALL_PREFIX_FUNCTION that returns +# the installation directory of the package (CMAKE_INSTALL_PREFIX at build time) +# as following: +# * if the library is shared and GZ_ENABLE_RELOCATABLE_INSTALL is ON, by extracting the +# location of the shared library via dladdr, and computing the corresponding +# install prefix from it +# * if the library is static or GZ_ENABLE_RELOCATABLE_INSTALL is OFF, by using the exact +# value of CMAKE_INSTALL_PREFIX that was hardcoded in the library at compilation time +# +# As in some cases it is important to have the ability to control and change the value returned by +# the GET_INSTALL_PREFIX_FUNCTION at runtime, in both cases the library returns the value of +# the OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE if the environment variable is defined +# +# To use this macro, please add gz_find_package(DL) +# in the dependencies of your project +macro(gz_add_get_install_prefix_impl) + set(_options) + set(_oneValueArgs + GET_INSTALL_PREFIX_FUNCTION + GET_INSTALL_PREFIX_HEADER + OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE + ) + set(_multiValueArgs ) + cmake_parse_arguments(gz_add_get_install_prefix_impl "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN}) + + if(NOT DEFINED gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_FUNCTION) + message(FATAL_ERROR + "gz_add_get_install_prefix_impl: missing parameter GET_INSTALL_PREFIX_FUNCTION") + endif() + + if(NOT DEFINED gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_HEADER) + message(FATAL_ERROR + "gz_add_get_install_prefix_impl: missing parameter GET_INSTALL_PREFIX_HEADER") + endif() + + if(NOT DEFINED gz_add_get_install_prefix_impl_OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE) + message(FATAL_ERROR + "gz_add_get_install_prefix_impl: missing parameter OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE") + endif() + + + if(NOT TARGET ${PROJECT_LIBRARY_TARGET_NAME}) + message(FATAL_ERROR + "Target ${PROJECT_LIBRARY_TARGET_NAME} required by gz_add_get_install_prefix_impl\n" + "does not exist.") + endif() + + if(NOT TARGET ${DL_TARGET}) + message(FATAL_ERROR + "gz_add_get_install_prefix_impl called without DL_TARGET defined,\n" + "please add gz_find_package(DL) if you want to use gz_add_get_install_prefix_impl.") + endif() + + get_target_property(target_type ${PROJECT_LIBRARY_TARGET_NAME} TYPE) + if(NOT (target_type STREQUAL "STATIC_LIBRARY" OR target_type STREQUAL "MODULE_LIBRARY" OR target_type STREQUAL "SHARED_LIBRARY")) + message(FATAL_ERROR "gz_add_get_install_prefix_impl: library ${_library} is of unsupported type ${target_type}") + endif() + + + set(gz_add_get_install_prefix_impl_GENERATED_CPP + ${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_LIBRARY_TARGET_NAME}_get_install_prefix_impl.cc) + + # Write cpp for shared or module library type + option(GZ_ENABLE_RELOCATABLE_INSTALL "If ON, enable the feature of providing a relocatable install prefix in shared library." OFF) + if ((target_type STREQUAL "MODULE_LIBRARY" OR target_type STREQUAL "SHARED_LIBRARY") AND GZ_ENABLE_RELOCATABLE_INSTALL) + # We can't query the LOCATION property of the target due to https://cmake.org/cmake/help/v3.25/policy/CMP0026.html + # We can only access the directory of the library at generation time via $ + file(GENERATE OUTPUT "${gz_add_get_install_prefix_impl_GENERATED_CPP}" + CONTENT +"// This file is automatically generated by the gz_add_get_install_prefix_impl CMake macro. + +#include +#include +#include +#include + +#include + +#include <${gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_HEADER}> + +std::string ${gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_FUNCTION}() +{ + if(const char* override_env_var = std::getenv(\"${gz_add_get_install_prefix_impl_OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE}\")) + { + return std::string(override_env_var); + } + + std::error_code fs_error; + std::filesystem::path library_location; + + // Get location of the library + Dl_info address_info; + int res_val = dladdr(reinterpret_cast(&${gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_FUNCTION}), &address_info); + if (address_info.dli_fname && res_val > 0) + { + library_location = address_info.dli_fname; + } + else + { + return \"${CMAKE_INSTALL_PREFIX}\"; + } + + const std::filesystem::path library_directory = library_location.parent_path(); + // Given the library_directory, return the install prefix via the relative path +#ifndef _WIN32 + const std::filesystem::path rel_path_from_install_prefix_to_lib = std::string(\"${CMAKE_INSTALL_LIBDIR}\"); +#else + const std::filesystem::path rel_path_from_install_prefix_to_lib = std::string(\"${CMAKE_INSTALL_BINDIR}\"); +#endif + const std::filesystem::path rel_path_from_lib_to_install_prefix = + std::filesystem::relative(std::filesystem::current_path(), std::filesystem::current_path() / rel_path_from_install_prefix_to_lib, fs_error); + + if (fs_error) + { + return \"${CMAKE_INSTALL_PREFIX}\"; + } + + const std::filesystem::path install_prefix = library_directory / rel_path_from_lib_to_install_prefix; + const std::filesystem::path install_prefix_canonical = std::filesystem::canonical(install_prefix, fs_error); + + if (fs_error) + { + return \"${CMAKE_INSTALL_PREFIX}\"; + } + + // Return install prefix + return install_prefix_canonical.string(); +} +") + else() + # For static library, fallback to just provide return CMAKE_INSTALL_PREFIX + file(GENERATE OUTPUT "${gz_add_get_install_prefix_impl_GENERATED_CPP}" + CONTENT +"// This file is automatically generated by the gz_add_get_install_prefix_impl CMake macro. +#include + +#include <${gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_HEADER}> + +std::string ${gz_add_get_install_prefix_impl_GET_INSTALL_PREFIX_FUNCTION}() +{ + if(const char* override_env_var = std::getenv(\"${gz_add_get_install_prefix_impl_OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE}\")) + { + return std::string(override_env_var); + } + + return \"${CMAKE_INSTALL_PREFIX}\"; +} +") +endif() + + # Add cpp to library + target_sources(${PROJECT_LIBRARY_TARGET_NAME} PRIVATE ${gz_add_get_install_prefix_impl_GENERATED_CPP}) + + # Link DL_TARGET that provides dladdr + target_link_libraries(${PROJECT_LIBRARY_TARGET_NAME} PRIVATE ${DL_TARGET}) + +endmacro() diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9303a087..db3ee2d3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -7,3 +7,9 @@ if (UNIX) ${CMAKE_BINARY_DIR}/test_results ) endif() + +# This test requires the use of std::filesystem, so it is only compiled on non-GCC +# compilers of with GCC >= 11 +if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_VERSION VERSION_GREATER_EQUAL 11.0) + add_subdirectory(get_install_prefix) +endif() diff --git a/test/get_install_prefix/CMakeLists.txt b/test/get_install_prefix/CMakeLists.txt new file mode 100644 index 00000000..4ae0cff1 --- /dev/null +++ b/test/get_install_prefix/CMakeLists.txt @@ -0,0 +1,57 @@ +# For the tests, we make sure that the relative path in the build location is the same +# of the install, and we make sure that the value returned by the shared library is ${CMAKE_BINARY_DIR} +# while for the static library we make sure that the value returned is ${CMAKE_INSTALL_PREFIX} +set(CMAKE_RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_BINDIR}") +set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") +set(CMAKE_ARCHIVE_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/${CMAKE_INSTALL_LIBDIR}") + +include(GzUtils) + +# dladdr from dl is a compulsory requirement for +# gz_add_get_install_prefix_impl +gz_find_package(DL REQUIRED) + +# shared test +add_library(get-install-prefix-test-shared SHARED) +set(PROJECT_LIBRARY_TARGET_NAME get-install-prefix-test-shared) + +gz_add_get_install_prefix_impl(GET_INSTALL_PREFIX_HEADER get_install_prefix_test_shared.h + GET_INSTALL_PREFIX_FUNCTION gz::cmake::test::sharedlib::getInstallPrefix + OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE GET_INSTALL_PREFIX_TEST_INSTALL_PREFIX) +set_target_properties(get-install-prefix-test-shared PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) +target_include_directories(get-install-prefix-test-shared PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + + +# static test +add_library(get-install-prefix-test-static STATIC) +set(PROJECT_LIBRARY_TARGET_NAME get-install-prefix-test-static) + +gz_add_get_install_prefix_impl(GET_INSTALL_PREFIX_HEADER get_install_prefix_test_static.h + GET_INSTALL_PREFIX_FUNCTION gz::cmake::test::staticlib::getInstallPrefix + OVERRIDE_INSTALL_PREFIX_ENV_VARIABLE GET_INSTALL_PREFIX_TEST_INSTALL_PREFIX) +target_include_directories(get-install-prefix-test-static PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) + + +# Write header with CMake variables to check +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/get_install_prefix_test_cmake_variables.h +"#pragma once + +#define CMAKE_INSTALL_PREFIX \"${CMAKE_INSTALL_PREFIX}\" +#define CMAKE_BINARY_DIR \"${CMAKE_BINARY_DIR}\" + +") + + + +# Add test executable +add_executable(get_install_prefix_test get_install_prefix_test.cc) +target_include_directories(get_install_prefix_test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(get_install_prefix_test PRIVATE get-install-prefix-test-shared + get-install-prefix-test-static) + +# Add the test only if GZ_ENABLE_RELOCATABLE_INSTALL is enabled, +# as the test on gz_add_get_install_prefix_impl rely on GZ_ENABLE_RELOCATABLE_INSTALL +# being enabled +if(GZ_ENABLE_RELOCATABLE_INSTALL) + add_test(NAME get_install_prefix_test COMMAND get_install_prefix_test) +endif() diff --git a/test/get_install_prefix/get_install_prefix_test.cc b/test/get_install_prefix/get_install_prefix_test.cc new file mode 100644 index 00000000..a3803993 --- /dev/null +++ b/test/get_install_prefix/get_install_prefix_test.cc @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +#include +#include +#include + +#include +#include +#include + +std::string toCanonical(const std::string input_path) +{ + return std::filesystem::weakly_canonical(std::filesystem::path(input_path)).string(); +} + +int main() +{ + // Test nominal behaviour + + std::string sharedInstallPrefix = gz::cmake::test::sharedlib::getInstallPrefix(); + std::string staticInstallPrefix = gz::cmake::test::staticlib::getInstallPrefix(); + + std::cerr << "get-install-prefix test:" << std::endl; + std::cerr << "sharedInstallPrefix: " << sharedInstallPrefix << std::endl; + std::cerr << "CMAKE_BINARY_DIR: " << CMAKE_BINARY_DIR << std::endl; + std::cerr << "staticInstallPrefix: " << staticInstallPrefix << std::endl; + std::cerr << "CMAKE_INSTALL_PREFIX: " << CMAKE_INSTALL_PREFIX << std::endl; + + if (toCanonical(sharedInstallPrefix) != toCanonical(CMAKE_BINARY_DIR)) + { + std::cerr << "getInstallPrefixShared returned unexpected value, test is failing." << std::endl; + return EXIT_FAILURE; + } + + if (toCanonical(staticInstallPrefix) != toCanonical(CMAKE_INSTALL_PREFIX)) + { + std::cerr << "getInstallPrefixStatic returned unexpected value, test is failing." << std::endl; + return EXIT_FAILURE; + } + + // Test behaviour after setting the environment variable to modify the return values (only on Unix so we can use setenv) +#ifndef _WIN32 + std::string overrideValue = "test_override_value"; + int overwrite = 1; + setenv("GET_INSTALL_PREFIX_TEST_INSTALL_PREFIX" , overrideValue.c_str(), overwrite); + std::string sharedInstallPrefixWithOverride = gz::cmake::test::sharedlib::getInstallPrefix(); + std::string staticInstallPrefixWithOverride = gz::cmake::test::staticlib::getInstallPrefix(); + + std::cerr << "overrideValue: " << overrideValue << std::endl; + std::cerr << "sharedInstallPrefixWithOverride: " << sharedInstallPrefixWithOverride << std::endl; + std::cerr << "staticInstallPrefixWithOverride: " << staticInstallPrefixWithOverride << std::endl; + + if (overrideValue != sharedInstallPrefixWithOverride) + { + std::cerr << "getInstallPrefixShared with env variable override returned unexpected value, test is failing." << std::endl; + return EXIT_FAILURE; + } + + if (overrideValue != sharedInstallPrefixWithOverride) + { + std::cerr << "getInstallPrefixShared with env variable override returned unexpected value, test is failing." << std::endl; + return EXIT_FAILURE; + } +#endif + + return EXIT_SUCCESS; +} diff --git a/test/get_install_prefix/get_install_prefix_test_shared.h b/test/get_install_prefix/get_install_prefix_test_shared.h new file mode 100644 index 00000000..c022e711 --- /dev/null +++ b/test/get_install_prefix/get_install_prefix_test_shared.h @@ -0,0 +1,13 @@ +namespace gz +{ + namespace cmake + { + namespace test + { + namespace sharedlib + { + std::string getInstallPrefix(); + } + } + } +} diff --git a/test/get_install_prefix/get_install_prefix_test_static.h b/test/get_install_prefix/get_install_prefix_test_static.h new file mode 100644 index 00000000..e0b0687d --- /dev/null +++ b/test/get_install_prefix/get_install_prefix_test_static.h @@ -0,0 +1,13 @@ +namespace gz +{ + namespace cmake + { + namespace test + { + namespace staticlib + { + std::string getInstallPrefix(); + } + } + } +}