diff --git a/.github/workflows/conda-forge-ci.yml b/.github/workflows/conda-forge-ci.yml new file mode 100644 index 0000000..a52d9dc --- /dev/null +++ b/.github/workflows/conda-forge-ci.yml @@ -0,0 +1,63 @@ +name: C++ CI Workflow with conda-forge dependencies + +on: + push: + pull_request: + schedule: + # * is a special character in YAML so you have to quote this string + # Execute a "weekly" build at 2 AM UTC on Sunday + - cron: '0 2 * * 0' + +jobs: + build: + name: '[${{ matrix.os }}@${{ matrix.build_type }}@conda]' + runs-on: ${{ matrix.os }} + strategy: + matrix: + build_type: [Release] + os: [ubuntu-latest, windows-2019, macOS-latest] + fail-fast: false + + steps: + - uses: actions/checkout@v2 + + - uses: conda-incubator/setup-miniconda@v2 + with: + mamba-version: "*" + channels: conda-forge,defaults + miniforge-variant: Mambaforge + channel-priority: true + + - name: Dependencies + shell: bash -l {0} + run: | + mamba install cmake compilers make ninja pkg-config ycm-cmake-modules + + - name: Additional Dependencies [Windows] + if: contains(matrix.os, 'windows') + shell: bash -l {0} + run: | + mamba install vs2019_win-64 + + + - name: Configure&Build&Test&Install [Linux&macOS] + if: contains(matrix.os, 'macos') || contains(matrix.os, 'ubuntu') + shell: bash -l {0} + run: | + mkdir -p build + cd build + cmake -GNinja -DBUILD_TESTING:BOOL=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_INSTALL_PREFIX=./install .. + cmake --build . --config ${{ matrix.build_type }} + ctest --output-on-failure -C ${{ matrix.build_type }} + cmake --install . --config ${{ matrix.build_type }} + + - name: Configure&Build&Test&Install [Windows] + if: contains(matrix.os, 'windows') + shell: cmd /C CALL {0} + run: | + mkdir build + cd build + cmake -GNinja -DBUILD_TESTING:BOOL=ON -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} -DCMAKE_INSTALL_PREFIX=./install .. + cmake --build . --config ${{ matrix.build_type }} + ctest --output-on-failure -C ${{ matrix.build_type }} + cmake --install . --config ${{ matrix.build_type }} diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..574662f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + +## [Unreleased] + +## [0.1.0] - 2022-11-14 + +### Added + +- First version of the library (https://github.com/ami-iit/reloc-cpp/pull/1). diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..6cdee7d --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Fondazione Istituto Italiano di Tecnologia (IIT) +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +project(reloc-cpp + LANGUAGES CXX C + VERSION 0.1.0) + +include(GNUInstallDirs) + +# Make reloc_cpp_generate available +include(${CMAKE_CURRENT_SOURCE_DIR}/RelocCPPGenerate.cmake) + +if(CMAKE_SOURCE_DIR STREQUAL CMAKE_CURRENT_SOURCE_DIR) + set(RELOC_CPP_STANDALONE ON) +endif() + +option(RELOC_CPP_INSTALL "Enable installation of reloc-cpp" ${RELOC_CPP_STANDALONE}) + +# Build test related commands? +option(BUILD_TESTING "Create tests using CMake" ${RELOC_CPP_STANDALONE}) +if(BUILD_TESTING) + enable_testing() +endif() + +if(RELOC_CPP_INSTALL) + set(RELOC_CPP_INSTALL_MODULE_DIR "${CMAKE_INSTALL_DATADIR}/reloc-cpp") + set(RELOC_CPP_INSTALL_CMAKE_DIR "${CMAKE_INSTALL_DATADIR}/reloc-cpp/cmake") + + install(FILES ${CMAKE_CURRENT_SOURCE_DIR}/RelocCPPGenerate.cmake + DESTINATION "${CMAKE_INSTALL_DATADIR}/reloc-cpp") + + find_package(YCM REQUIRED) + include(InstallBasicPackageFiles) + install_basic_package_files(${PROJECT_NAME} + VERSION ${${PROJECT_NAME}_VERSION} + COMPATIBILITY AnyNewerVersion + VARS_PREFIX ${PROJECT_NAME} + NO_CHECK_REQUIRED_COMPONENTS_MACRO + ARCH_INDEPENDENT + NO_EXPORT) + include(AddUninstallTarget) +endif() + +# Add integration tests (unit tests for each library should be in each sublibrary directory). +if(BUILD_TESTING) + add_subdirectory(tests) +endif() diff --git a/README.md b/README.md index 4a86675..0af1185 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,66 @@ # reloc-cpp -CMake/C++ library to get the installation prefix of shared library. + +CMake/C++ library to get the installation prefix of a shared library in a relocatable way. + +In a nutshell, it permits to avoid the need to hardcode the location of `CMAKE_INSTALL_PREFIX` in a shared library if you need it to localize other resources installed with the package. This permits to easily move the installation prefix in a location different from `CMAKE_INSTALL_PREFIX` after the installation (i.e. making it a *relocatable* installation), as long as the library is compiled as shared. + +In the case that the library is compiled as static, `reloc-cpp` will fall back to hardcode `CMAKE_INSTALL_PREFIX` in the library. + +## Installation + +### FetchContent + +~~~cmake +include(FetchContent) +FetchContent_Declare( + reloc-cpp + GIT_REPOSITORY https://github.com/ami-iit/reloc-cpp.git + GIT_TAG v0.1.0 +) + +FetchContent_MakeAvailable(reloc-cpp) +~~~ + +## Usage + +In your CMake build system you can use `reloc-cpp` as: + +```cmake +add_library(yourLibrary) + +# ... + +reloc_cpp_generate(yourLibrary + GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/yourLibrary_getInstallPrefix.h + GENERATED_FUNCTION yourLibrary::getInstallPrefix) +``` + +then, you can use it in C++ as: + +~~~cpp +#include + +// This return the value corresponding to CMAKE_INSTALL_PREFIX +std::string installPrefix = yourLibrary::getInstallPrefix().value(); +~~~ + + +## Contributing + +Pull requests are welcome. For major changes, please open an issue first +to discuss what you would like to change. + +## References + +References that were useful as inspiration when developing reloc-cpp: +* ["Helping C/C++ Packages be Relocatable" presentation](https://indico.cern.ch/event/848215/contributions/3591953/attachments/1923018/3181752/HSFPackagingRelocation.pdf) +* [Resourceful: Techniques for installing and accessing resource files using C++ and Python.](https://github.com/drbenmorgan/Resourceful) +* ["Qt is relocatable" blog post](https://www.qt.io/blog/qt-is-relocatable) +* [binreloc: Library for creating relocatable software](https://github.com/limbahq/binreloc) + +Resources that could be useful as an alternative to reloc-cpp: +* [cmrc: A Resource Compiler in a Single CMake Script ](https://github.com/vector-of-bool/cmrc) + +## License + +[BSD-3-Clause](https://choosealicense.com/licenses/bsd-3-clause/) diff --git a/RelocCPPGenerate.cmake b/RelocCPPGenerate.cmake new file mode 100644 index 0000000..75bc6f5 --- /dev/null +++ b/RelocCPPGenerate.cmake @@ -0,0 +1,204 @@ +# SPDX-FileCopyrightText: Fondazione Istituto Italiano di Tecnologia (IIT) +# SPDX-License-Identifier: BSD-3-Clause + +# reloc_cpp_generate( +# GENERATED_HEADER +# GENERATED_FUNCTION +# DISABLE_RELOCATABLE +# EXPORT_MACRO_INCLUDE +# EXPORT_MACRO_NAME +# VERBOSE ) +# +function(RELOC_CPP_GENERATE _library) + set(_options) + set(_oneValueArgs + GENERATED_HEADER + GENERATED_FUNCTION + DISABLE_RELOCATABLE + EXPORT_MACRO_INCLUDE + EXPORT_MACRO_NAME + VERBOSE + ) + set(_multiValueArgs ) + cmake_parse_arguments(RCG "${_options}" "${_oneValueArgs}" "${_multiValueArgs}" ${ARGN} ) + + if(NOT DEFINED RCG_GENERATED_HEADER) + message(FATAL_ERROR "reloc_cpp_generate: missing parameter GENERATED_HEADER") + endif() + + if(NOT DEFINED RCG_GENERATED_FUNCTION) + message(FATAL_ERROR "reloc_cpp_generate: missing parameter GENERATED_FUNCTION") + endif() + + if(DEFINED RCG_EXPORT_MACRO_INCLUDE) + set(RCG_EXPORT_MACRO_INCLUDE_LINE "#include <${RCG_EXPORT_MACRO_INCLUDE}>") + else() + set(RCG_EXPORT_MACRO_INCLUDE_LINE "") + endif() + + if(DEFINED RCG_EXPORT_MACRO_NAME) + set(RCG_EXPORT_MACRO_NAME "") + endif() + + if(DEFINED RCG_EXPORT_MACRO_NAME) + set(RCG_EXPORT_MACRO_NAME_WITH_SPACE "${RCG_EXPORT_MACRO_NAME} ") + else() + set(RCG_EXPORT_MACRO_NAME_WITH_SPACE "${RCG_EXPORT_MACRO_NAME}") + endif() + + if(NOT TARGET ${_library}) + message(FATAL_ERROR "reloc_cpp_generate: library ${_library} does not exist") + endif() + + if(NOT TARGET ${_library}) + message(FATAL_ERROR "reloc_cpp_generate: library ${_library} does not exist") + endif() + + get_target_property(target_type ${_library} TYPE) + if(NOT (target_type STREQUAL "STATIC_LIBRARY" OR target_type STREQUAL "MODULE_LIBRARY" OR target_type STREQUAL "SHARED_LIBRARY")) + message(FATAL_ERROR "reloc_cpp_generate: library ${_library} is of unsupported type ${target_type}") + endif() + + # Get generated .cpp name + get_filename_component(RCG_GENERATED_HEADER_DIRECTORY ${RCG_GENERATED_HEADER} DIRECTORY) + get_filename_component(RCG_GENERATED_HEADER_NAME_WE ${RCG_GENERATED_HEADER} NAME_WE) + get_filename_component(RCG_GENERATED_HEADER_NAME ${RCG_GENERATED_HEADER} NAME) + set(RCG_GENERATED_CPP ${RCG_GENERATED_HEADER_DIRECTORY}/${RCG_GENERATED_HEADER_NAME_WE}.cpp) + + if (DEFINED RCG_VERBOSE AND RCG_VERBOSE) + message(STATUS "reloc_cpp_generate: RCG_GENERATED_CPP=${RCG_GENERATED_CPP}") + endif() + + # Decompose scoped function name in unscoped name and namespaces + string(REPLACE "::" ";" RCG_GENERATED_FUNCTION_NAMESPACES ${RCG_GENERATED_FUNCTION}) + list(POP_BACK RCG_GENERATED_FUNCTION_NAMESPACES RCG_GENERATED_FUNCTION_UNSCOPED) + + # Generate namespace-related lines + set(RCG_HEADER_OPEN_NAMESPACES "") + set(RCG_HEADER_CLOSE_NAMESPACES "") + + foreach(RCG_NAMESPACE IN ITEMS ${RCG_GENERATED_FUNCTION_NAMESPACES}) + string(APPEND RCG_HEADER_OPEN_NAMESPACES "namespace ${RCG_NAMESPACE} {\n") + string(APPEND RCG_HEADER_CLOSE_NAMESPACES "}\n") + endforeach() + + # Write header + file(GENERATE OUTPUT "${RCG_GENERATED_HEADER}" + CONTENT +"// This file is automatically generated reloc_cpp_generate CMake function. +#pragma once + +#include +#include + +${RCG_EXPORT_MACRO_INCLUDE_LINE} + +${RCG_HEADER_OPEN_NAMESPACES} + +${RCG_EXPORT_MACRO_NAME_WITH_SPACE}std::optional ${RCG_GENERATED_FUNCTION_UNSCOPED}(); + +${RCG_HEADER_CLOSE_NAMESPACES} +") + + # Write cpp for shared or module library type + if (target_type STREQUAL "MODULE_LIBRARY" OR target_type STREQUAL "SHARED_LIBRARY" AND NOT RCG_DISABLE_RELOCATABLE) + # 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 "${RCG_GENERATED_CPP}" + CONTENT +"// This file is automatically generated reloc_cpp_generate CMake function. +#include \"${RCG_GENERATED_HEADER_NAME}\" + +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include + +std::optional ${RCG_GENERATED_FUNCTION}() +{ + std::error_code fs_error; + + // Get location of the library + std::filesystem::path library_location; +#ifndef _WIN32 + Dl_info address_info; + int res_val = dladdr(reinterpret_cast(&${RCG_GENERATED_FUNCTION}), &address_info); + if (address_info.dli_fname && res_val > 0) + { + library_location = address_info.dli_fname; + } + else + { + return {}; + } +#else + // See + char module_path[MAX_PATH]; + HMODULE hm = NULL; + + if (GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | + GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, + (LPCSTR) &${RCG_GENERATED_FUNCTION}, &hm) == 0) + { + return {}; + } + if (GetModuleFileNameA(hm, module_path, sizeof(module_path)) == 0) + { + return {}; + } + + library_location = std::string(module_path); +#endif + + 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); + // TODO(traversaro): handle fs_error errors + 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); + // TODO(traversaro): handle fs_error errors + + // Return install prefix + return install_prefix_canonical.string(); +} +") + else() + # For static library, fallback to just provide return CMAKE_INSTALL_PREFIX + file(GENERATE OUTPUT "${RCG_GENERATED_CPP}" + CONTENT +"// This file is automatically generated reloc_cpp_generate CMake function. + +#include \"${RCG_GENERATED_HEADER_NAME}\" + +std::optional ${RCG_GENERATED_FUNCTION}() +{ + return \"${CMAKE_INSTALL_PREFIX}\"; +} +") +endif() + + # Add cpp to library + target_sources(${_library} PRIVATE ${RCG_GENERATED_CPP}) + + # Specify that we need C++17 features + target_compile_features(${_library} PUBLIC cxx_std_17) + + # Link dl due to the use of dladdr + if(NOT WIN32) + target_link_libraries(${_library} PRIVATE ${CMAKE_DL_LIBS}) + endif() + +endfunction() diff --git a/reloc-cpp-config.cmake.in b/reloc-cpp-config.cmake.in new file mode 100644 index 0000000..610ec82 --- /dev/null +++ b/reloc-cpp-config.cmake.in @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: Istituto Italiano di Tecnologia (IIT) +# SPDX-License-Identifier: BSD-3-Clause + +@PACKAGE_INIT@ + +set_and_check(RELOC_CPP_MODULE_DIR "@PACKAGE_RELOC_CPP_INSTALL_MODULE_DIR@") +include(${RELOC_CPP_MODULE_DIR}/RelocCPPGenerate.cmake) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..bf30774 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,51 @@ +# SPDX-FileCopyrightText: Fondazione Istituto Italiano di Tecnologia (IIT) +# SPDX-License-Identifier: BSD-3-Clause + +# 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}") + +# shared test +add_library(reloc-cpp-test-shared SHARED) + +reloc_cpp_generate(reloc-cpp-test-shared + GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/reloc_cpp_test_shared.h + GENERATED_FUNCTION RelocCPP::test::sharedlib::getInstallPrefix) +set_target_properties(reloc-cpp-test-shared PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) + +# static test +add_library(reloc-cpp-test-static STATIC) + +reloc_cpp_generate(reloc-cpp-test-static + GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/reloc_cpp_test_static.h + GENERATED_FUNCTION RelocCPP::test::staticlib::getInstallPrefix) + +# shared-relocatable-disabled +# In this case, we test the use of several options: +# * DISABLE_RELOCATABLE +# Use of function without namespace +add_library(reloc-cpp-test-shared-relocatable-disabled SHARED) + +reloc_cpp_generate(reloc-cpp-test-shared-relocatable-disabled + GENERATED_HEADER ${CMAKE_CURRENT_BINARY_DIR}/reloc_cpp_test_shared_relocatable_disabled.h + GENERATED_FUNCTION getInstallPrefixSharedRelocatableDisabled + DISABLE_RELOCATABLE ON) +set_target_properties(reloc-cpp-test-shared-relocatable-disabled PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS ON) + +# Write header with CMake variables to check +file(WRITE ${CMAKE_CURRENT_BINARY_DIR}/reloc_cpp_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(reloc-cpp-test reloc_cpp_test.cpp) +target_include_directories(reloc-cpp-test PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) +target_link_libraries(reloc-cpp-test PRIVATE reloc-cpp-test-shared reloc-cpp-test-static reloc-cpp-test-shared-relocatable-disabled) +add_test(NAME reloc-cpp-test COMMAND reloc-cpp-test) diff --git a/tests/reloc_cpp_test.cpp b/tests/reloc_cpp_test.cpp new file mode 100644 index 0000000..9789249 --- /dev/null +++ b/tests/reloc_cpp_test.cpp @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: Fondazione Istituto Italiano di Tecnologia (IIT) +// SPDX-License-Identifier: BSD-3-Clause + +#include +#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() +{ + std::string sharedInstallPrefix = RelocCPP::test::sharedlib::getInstallPrefix().value(); + std::string staticInstallPrefix = RelocCPP::test::staticlib::getInstallPrefix().value();; + std::string sharedRelocatableDisabledInstallPrefix = getInstallPrefixSharedRelocatableDisabled().value(); + + std::cerr << "reloc-cpp 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; + std::cerr << "sharedRelocatableDisabledInstallPrefix: " << sharedRelocatableDisabledInstallPrefix << 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; + } + + if (toCanonical(sharedRelocatableDisabledInstallPrefix) != toCanonical(CMAKE_INSTALL_PREFIX)) + { + std::cerr << "getInstallPrefixSharedRelocatableDisabled returned unexpected value, test is failing." << std::endl; + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +}