Published on

Packager une application Linux avec des librairies dynamiques

Authors
  • Name
    Anthony Rabine

Packager une application Linux en vue de la redistribuer n'est pas forcément une mince affaire. Là où les systèmes MacOS et Windows sont assez figés, les multiples distribtions Linux n'aident pas à la diffusion de logiciel. Dans cet articles, nous allons voir une solution faite maison pour générer un paquet de type archive universelle qui contiendra toutes les librairies nécessaires.

But du projet et problématiques

Ici nous n'allons pas produire d'archive DEB ou RPM pour votre distribution. C'est un moyen intéressant mais lourd car il faut suivre les évolutions et nécessite des tests plus larges. Le seul véritable moyen universel pour Linux est encore de distribuer votre application sous la forme d'une archive complète. D'autres solutions existent comme Flatpack, snap ou autre mais les contraintes sont encore là.

Le problème, c'est la gestion des dépendances. Si votre exécutable nécessite d'être lié à plusieurs librairies dynaiques externe, il suffit de l'indiquer dans le cas d'un paquet DEB ou RPM. Dans notre cas, nous ne nous reposerons pas sur les librairies dynamiques livrées par le système mais sur les notres qui seront dans l'archive.

Sauf que là où sur Windows, il suffit de placer les DLL dans le même répertoire que l'exécutable, il en est tout autre pour Linux.

Pour illuster notre exemple, nous allons utiliser le projet réalisé dans un article précédent, le projet qui affiche une image animée utilisant la librairie SDL2. On est alors dans un cas typique de distribution d'un jeu vidéo ; en effet, si vous utilisez une plateforme fommr GOGS ou Steam, on vous demandera une archive à déployer.

Préparation

Nous travaillons (pour compiler) sur un ordinateur équipé d'un OS Ubuntu. Un système de développement est un très mauvais système pour tester le déploiement d'application car il contient déjà toutes les librairies nécessaires (cela pour Windows ou Linux).

Donc, il faut tester dans un environnement "neuf", fraichement instllé. Vous pouvez utiliser une Sandbox sous Windows ou une machine virtuelle. Pour notre application, nous allons utiliser un OS viruel Arch Linux, bien différent de notre Ubuntu.

image

Sous VirtualBox, configurer un partage de répertoire et tenter de lancer l'application SDL2 :

bash
[osboxes@osboxes build-sdl2-tiles-scrolling-Desktop_Qt_5_15_2_GCC_64bit-Debug]$ ./sdl2-tiles-scrolling 
./sdl2-tiles-scrolling: error while loading shared libraries: libSDL2_image-2.0.so.0: cannot open shared object file: No such file or directory

Parfait ! Nous avons l'erreur que nous voulons. Cherchons à le corriger maintenant.

La magie est dans rpath

Toute la stratégie que nous allons adoper passe dans une petite option passé au linker : le rpath. Cette option permet de spécifier un chemin relatif codé en dur dans l'exécutable indiquant l'endroit où se trouvent les librairies dynamiques.

L'option est la suivante, si vous linkez avec GCC (c'est une option à passer lors de l'invocation du linker) :

makefile
-Wl,-rpath-link=./lib

Dans ce cas, le chemin sera ./lib par rapport à l'exécutable.

et avec CMake :

cmake
set(CMAKE_INSTALL_RPATH "\$ORIGIN/lib")
set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)

Copie des librairies

Ok, maintenant, comment générer un package avec toutes les librairies ? Pour cela, je me suis inspiré du CMakeFile du jeu OpenTTD et de quelques réponses de StackOverflow. Voici le CMakeFile terminé :

cmake
# ==================================================================================================
# GLOBAL VARIABLES AND PACKAGES DISCOVERY
# ==================================================================================================
cmake_minimum_required(VERSION 3.5)

set(PROJ_NAME sdl2-tiles-scrolling )

project(${PROJ_NAME} LANGUAGES CXX C)

set(CMAKE_CXX_STANDARD 11)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(SDL2)

# ==================================================================================================
# TARGET AND LINK STUFF
# ==================================================================================================

# Activate the rpath in the executable: dynamic librairies will be searched in the relative ./lib
# directory from the executable path 
set(CMAKE_INSTALL_RPATH "\$ORIGIN/lib")
set(CMAKE_BUILD_WITH_INSTALL_RPATH ON)

add_executable(${PROJ_NAME} main.cpp glad.c)
target_include_directories(${PROJ_NAME} PUBLIC glad)
target_link_libraries(${PROJ_NAME} SDL2 dl)


# ==================================================================================================
# CPACK CONFIGURATION (INSTALLER)
# ==================================================================================================
# CPack doit être situé à la fin du CMake pour bénéficier de toutes les variables

set(BINARY_DESTINATION_DIR ".")
set(DATA_DESTINATION_DIR ".")
set(DOCS_DESTINATION_DIR ".")
set(MAN_DESTINATION_DIR ".")

set(CPACK_SYSTEM_NAME "x86_64")

set(CPACK_PACKAGE_NAME ${PROJ_NAME})
set(CPACK_PACKAGE_VENDOR "Anthony Rabine")
set(CPACK_PACKAGE_DESCRIPTION ${PROJ_NAME})
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY ${PROJ_NAME})
set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.d8s.eu")
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${PROJ_NAME})
set(CPACK_PACKAGE_CHECKSUM "SHA256")

set(CPACK_RESOURCE_FILE_README "${CMAKE_SOURCE_DIR}/README.md")
set(CPACK_MONOLITHIC_INSTALL YES)
set(CPACK_PACKAGE_EXECUTABLES ${PROJ_NAME})
set(CPACK_STRIP_FILES YES)
set(CPACK_SET_DESTDIR true)
set(CPACK_INSTALL_PREFIX /) # suppress /usr/lib, default install prefix on Unix

# On dit ici à CMake que le type de package est une simple archive compressée
set(CPACK_GENERATOR "TXZ")

include(CPack)


# Transfer the executable name to "install" world
install(CODE "set(PROJ_NAME \"${PROJ_NAME}\")")

# Thank-you OpenTTD:
# Install all dependencies we can resolve, with the exception of ones that
# every Linux machine should have. This should make this build as generic
# as possible, where it runs on any machine with the same or newer libc
# than the one this is compiled with.
# We copy these libraries into lib/ folder, so they can be found on game
# startup. See comment in root CMakeLists.txt for how this works exactly.
install(CODE [[
    file(GET_RUNTIME_DEPENDENCIES
            RESOLVED_DEPENDENCIES_VAR DEPENDENCIES
            UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPENDENCIES
            EXECUTABLES ${PROJ_NAME}
            POST_EXCLUDE_REGEXES "ld-linux|libc.so|libdl.so|libm.so|libgcc_s.so|libpthread.so|librt.so|libstdc...so")
    file(INSTALL
            DESTINATION "${CMAKE_INSTALL_PREFIX}/lib"
            FILES ${DEPENDENCIES}
            FOLLOW_SYMLINK_CHAIN)

    # This should not be possible, but error out when a dependency cannot
    # be resolved.
    list(LENGTH UNRESOLVED_DEPENDENCIES UNRESOLVED_LENGTH)
    if(${UNRESOLVED_LENGTH} GREATER 0)
        message(FATAL_ERROR "Unresolved dependencies: ${UNRESOLVED_DEPENDENCIES}")
    endif()
]])

# Copie de l'exécutable
install(TARGETS ${PROJ_NAME}
        RUNTIME
            DESTINATION ${BINARY_DESTINATION_DIR}
            COMPONENT Runtime
        )

# Copie des ressources (images, sons, fichiers divers ...)
install(FILES
            ${CMAKE_SOURCE_DIR}/background.png
            ${CMAKE_SOURCE_DIR}/tile.png
        DESTINATION ${DOCS_DESTINATION_DIR}
        COMPONENT assets)

Et voilà, notre logiciel fonctionne dans la VM : les librairies sont bient trouvées. Pour tester cela, renommez le répertoire "lib" et vous verrez que le logiciel ne se lance plus (en ligne de commande vous verrez l'erreur).

image

Retrouvez le code complet de l'article ici : https://github.com/arabine/sdl2-tiles-scrolling