From 069f858f42411d8d24b8dc0ce32ab09aa8c1c01a Mon Sep 17 00:00:00 2001 From: Brad King Date: Mon, 1 Sep 2025 16:39:53 -0400 Subject: [PATCH 1/3] ci: Add patchelf and appstream to Fedora base image These are needed to test the CPack AppImage generator. --- .gitlab/ci/docker/fedora42/deps_packages.lst | 2 ++ .gitlab/os-linux.yml | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitlab/ci/docker/fedora42/deps_packages.lst b/.gitlab/ci/docker/fedora42/deps_packages.lst index c3f876855b..4aa4e8136e 100644 --- a/.gitlab/ci/docker/fedora42/deps_packages.lst +++ b/.gitlab/ci/docker/fedora42/deps_packages.lst @@ -56,6 +56,8 @@ mercurial subversion # Packages needed to test CPack. +appstream +patchelf rpm-build # Packages needed to test find modules. diff --git a/.gitlab/os-linux.yml b/.gitlab/os-linux.yml index 47403df393..bd789c4413 100644 --- a/.gitlab/os-linux.yml +++ b/.gitlab/os-linux.yml @@ -83,7 +83,7 @@ ### Fedora .fedora42: - image: "kitware/cmake:ci-fedora42-x86_64-2025-07-22" + image: "kitware/cmake:ci-fedora42-x86_64-2025-09-01" variables: GIT_CLONE_PATH: "$CI_BUILDS_DIR/cmake ci/long file name for testing purposes" From 9f2949bc68e5957c1d37f012ef014793cbdea600 Mon Sep 17 00:00:00 2001 From: Brad King Date: Mon, 1 Sep 2025 16:41:16 -0400 Subject: [PATCH 2/3] ci: Add script to install appimagetool in Linux jobs --- .gitlab/.gitignore | 1 + .gitlab/ci/appimagetool-env.sh | 3 +++ .gitlab/ci/appimagetool.sh | 32 ++++++++++++++++++++++++++++ .gitlab/ci/repackage/appimagetool.sh | 28 ++++++++++++++++++++++++ 4 files changed, 64 insertions(+) create mode 100644 .gitlab/ci/appimagetool-env.sh create mode 100755 .gitlab/ci/appimagetool.sh create mode 100755 .gitlab/ci/repackage/appimagetool.sh diff --git a/.gitlab/.gitignore b/.gitlab/.gitignore index 1afc482f6d..21d2f94b66 100644 --- a/.gitlab/.gitignore +++ b/.gitlab/.gitignore @@ -1,5 +1,6 @@ # Ignore files known to be downloaded by CI jobs. /5.15.1-0-202009071110* +/appimagetool /bcc* /cmake* /emsdk diff --git a/.gitlab/ci/appimagetool-env.sh b/.gitlab/ci/appimagetool-env.sh new file mode 100644 index 0000000000..1dd4674880 --- /dev/null +++ b/.gitlab/ci/appimagetool-env.sh @@ -0,0 +1,3 @@ +.gitlab/ci/appimagetool.sh +export PATH=$PWD/.gitlab/appimagetool/bin:$PATH +appimagetool --version diff --git a/.gitlab/ci/appimagetool.sh b/.gitlab/ci/appimagetool.sh new file mode 100755 index 0000000000..6ab430f0d3 --- /dev/null +++ b/.gitlab/ci/appimagetool.sh @@ -0,0 +1,32 @@ +#!/bin/sh + +set -e + +readonly version="1.9.0.20250814" + +case "$(uname -s)-$(uname -m)" in + Linux-x86_64) + shatool="sha256sum" + sha256sum="6414d395eafee09453d2e203d9cc65f867e6ff7e1a8a6c08e444d86cb1d106ad" + filename="appimagetool-$version-x86_64" + ;; + *) + echo "Unrecognized platform $(uname -s)-$(uname -m)" + exit 1 + ;; +esac +readonly shatool +readonly sha256sum + +cd .gitlab + +# This URL is only visible inside of Kitware's network. See above filename table. +baseurl="https://cmake.org/files/dependencies/internal" + +tarball="$filename.tar.gz" +echo "$sha256sum $tarball" > appimagetool.sha256sum +curl -OL "$baseurl/$tarball" +$shatool --check appimagetool.sha256sum +tar xzf "$tarball" +rm "$tarball" appimagetool.sha256sum +mv "$filename" "appimagetool" diff --git a/.gitlab/ci/repackage/appimagetool.sh b/.gitlab/ci/repackage/appimagetool.sh new file mode 100755 index 0000000000..03ce12d24e --- /dev/null +++ b/.gitlab/ci/repackage/appimagetool.sh @@ -0,0 +1,28 @@ +#!/usr/bin/env bash + +set -e + +arch="$1" +version="${2-1.9.0.20250814}" + +dir="appimagetool-$version-$arch" +mkdir "$dir" +mkdir -p "$dir/lib/appimagetool" + +filename="appimagetool-$arch.AppImage" +curl -OL "https://github.com/AppImage/appimagetool/releases/download/continuous/$filename" +chmod +x "$filename" +"./$filename" --appimage-extract +mv "squashfs-root/usr/bin" "$dir/bin" +rm -rf "$filename" "squashfs-root" + +filename="runtime-$arch" +curl -OL "https://github.com/AppImage/type2-runtime/releases/download/continuous/$filename" +mv "$filename" "$dir/lib/appimagetool/runtime" + +cat >"$dir/README.txt" < Date: Wed, 30 Jul 2025 12:39:43 -0300 Subject: [PATCH 3/3] CPack: Add AppImage generator This AppImage generator only relies on appimagetool and patchelf. Closes: #27104 Co-authored-by: Brad King --- .gitlab/ci/configure_fedora42_ninja.cmake | 2 + .gitlab/ci/env_fedora42_ninja.sh | 1 + Help/cpack_gen/appimage.rst | 130 +++++ Help/manual/cpack-generators.7.rst | 1 + Source/CMakeLists.txt | 11 + Source/CPack/cmCPackAppImageGenerator.cxx | 457 ++++++++++++++++++ Source/CPack/cmCPackAppImageGenerator.h | 68 +++ Source/CPack/cmCPackGeneratorFactory.cxx | 10 + Tests/RunCMake/CMakeLists.txt | 6 + ...AppImageTestApp-cpack-AppImage-check.cmake | 3 + .../AppImageTestApp-cpack-AppImage-stderr.txt | 19 + .../AppImageTestApp-cpack-AppImage-stdout.txt | 43 ++ .../CPack_AppImage/RunCMakeTest.cmake | 9 + .../AppImageTestApp/ApplicationIcon.png | Bin 0 -> 2335 bytes .../RunCPack/AppImageTestApp/CMakeLists.txt | 30 ++ .../AppImageTestApp/com.example.app.desktop | 6 + .../RunCPack/AppImageTestApp/main.cpp | 3 + 17 files changed, 799 insertions(+) create mode 100644 Help/cpack_gen/appimage.rst create mode 100644 Source/CPack/cmCPackAppImageGenerator.cxx create mode 100644 Source/CPack/cmCPackAppImageGenerator.h create mode 100644 Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-check.cmake create mode 100644 Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stderr.txt create mode 100644 Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stdout.txt create mode 100644 Tests/RunCMake/CPack_AppImage/RunCMakeTest.cmake create mode 100644 Tests/RunCMake/RunCPack/AppImageTestApp/ApplicationIcon.png create mode 100644 Tests/RunCMake/RunCPack/AppImageTestApp/CMakeLists.txt create mode 100644 Tests/RunCMake/RunCPack/AppImageTestApp/com.example.app.desktop create mode 100644 Tests/RunCMake/RunCPack/AppImageTestApp/main.cpp diff --git a/.gitlab/ci/configure_fedora42_ninja.cmake b/.gitlab/ci/configure_fedora42_ninja.cmake index 07ce93b339..3d9e405fc4 100644 --- a/.gitlab/ci/configure_fedora42_ninja.cmake +++ b/.gitlab/ci/configure_fedora42_ninja.cmake @@ -1,5 +1,7 @@ set(CMake_TEST_GUI "ON" CACHE BOOL "") if (NOT "$ENV{CMAKE_CI_NIGHTLY}" STREQUAL "") + set(CMake_TEST_CPACK_APPIMAGE "ON" CACHE STRING "") + set(CMake_TEST_CPACK_APPIMAGE_RUNTIME_FILE "$ENV{CI_PROJECT_DIR}/.gitlab/appimagetool/lib/appimagetool/runtime" CACHE FILEPATH "") set(CMake_TEST_ISPC "ON" CACHE STRING "") endif() set(CMake_TEST_MODULE_COMPILATION "named,compile_commands,collation,partitions,internal_partitions,export_bmi,install_bmi,shared,bmionly,build_database" CACHE STRING "") diff --git a/.gitlab/ci/env_fedora42_ninja.sh b/.gitlab/ci/env_fedora42_ninja.sh index 217ff305df..a2b941b61c 100644 --- a/.gitlab/ci/env_fedora42_ninja.sh +++ b/.gitlab/ci/env_fedora42_ninja.sh @@ -1,3 +1,4 @@ if test "$CMAKE_CI_NIGHTLY" = "true"; then + source .gitlab/ci/appimagetool-env.sh source .gitlab/ci/ispc-env.sh fi diff --git a/Help/cpack_gen/appimage.rst b/Help/cpack_gen/appimage.rst new file mode 100644 index 0000000000..13b0f4629d --- /dev/null +++ b/Help/cpack_gen/appimage.rst @@ -0,0 +1,130 @@ +CPack AppImage generator +------------------------ + +.. versionadded:: 4.2 + +CPack `AppImage`_ generator allows to bundle an application into +AppImage format. It uses ``appimagetool`` to pack the application, +and ``patchelf`` to set the application ``RPATH`` to a relative path +based on where the AppImage will be mounted. + +.. _`AppImage`: https://appimage.org + +The ``appimagetool`` does not scan for libraries dependencies it only +packs the installed content and check if the provided ``.desktop`` file +was properly created. For best compatibility it's recommended to choose +some old LTS distro and built it there, as well as including most +dependencies on the generated file. + +The snipped below can be added to your ``CMakeLists.txt`` file +replacing ``my_application_target`` with your application target, +it will do a best effort to scan and copy the libraries your +application links to and copy to install location. + +.. code-block:: cmake + + install(CODE [[ + file(GET_RUNTIME_DEPENDENCIES + EXECUTABLES $ + RESOLVED_DEPENDENCIES_VAR resolved_deps + ) + + foreach(dep ${resolved_deps}) + # copy the symlink + file(COPY ${dep} DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) + + # Resolve the real path of the dependency (follows symlinks) + file(REAL_PATH ${dep} resolved_dep_path) + + # Copy the resolved file to the destination + file(COPY ${resolved_dep_path} DESTINATION ${CMAKE_INSTALL_PREFIX}/lib) + endforeach() + ]]) + +For Qt based projects it's recommended to call +``qt_generate_deploy_app_script()`` or ``qt_generate_deploy_qml_app_script()`` +and install the files generated by the script, this will install +Qt module's plugins. + +You must also set :variable:`CPACK_PACKAGE_ICON` with the same value +listed in the Desktop file. + +Variables specific to CPack AppImage generator +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. variable:: CPACK_APPIMAGE_TOOL_EXECUTABLE + + Name of the ``appimagetool`` executable, might be located in the build dir, + full path or reachable in ``PATH``. + + :Default: ``appimagetool`` :variable:`CPACK_PACKAGE_FILE_NAME` + +.. variable:: CPACK_APPIMAGE_PATCHELF_EXECUTABLE + + Name of the ``patchelf`` executable, might be located in the build dir, + full path or reachable in ``PATH``. + + :Default: ``patchelf`` :variable:`CPACK_APPIMAGE_PATCHELF_EXECUTABLE` + +.. variable:: CPACK_APPIMAGE_DESKTOP_FILE + + Name of freedesktop.org desktop file installed. + + :Mandatory: Yes + :Default: :variable:`CPACK_APPIMAGE_DESKTOP_FILE` + +.. variable:: CPACK_APPIMAGE_UPDATE_INFORMATION + + Embed update information STRING; if zsyncmake is installed, + generate zsync file. + + :Default: :variable:`CPACK_APPIMAGE_UPDATE_INFORMATION` + +.. variable:: CPACK_APPIMAGE_GUESS_UPDATE_INFORMATION + + Guess update information based on GitHub or GitLab environment variables. + + :Default: :variable:`CPACK_APPIMAGE_GUESS_UPDATE_INFORMATION` + +.. variable:: CPACK_APPIMAGE_COMPRESSOR + + Squashfs compression. + + :Default: :variable:`CPACK_APPIMAGE_COMPRESSOR` + +.. variable:: CPACK_APPIMAGE_MKSQUASHFS_OPTIONS + + Arguments to pass through to mksquashfs. + + :Default: :variable:`CPACK_APPIMAGE_MKSQUASHFS_OPTIONS` + +.. variable:: CPACK_APPIMAGE_NO_APPSTREAM + + Do not check AppStream metadata. + + :Default: :variable:`CPACK_APPIMAGE_NO_APPSTREAM` + +.. variable:: CPACK_APPIMAGE_EXCLUDE_FILE + + Uses given file as exclude file for mksquashfs, + in addition to .appimageignore. + + :Default: :variable:`CPACK_APPIMAGE_EXCLUDE_FILE` + +.. variable:: CPACK_APPIMAGE_RUNTIME_FILE + + Runtime file to use, if not set a bash script will be generated. + + :Default: :variable:`CPACK_APPIMAGE_RUNTIME_FILE` + +.. variable:: CPACK_APPIMAGE_SIGN + + Sign with gpg[2]. + + :Default: :variable:`CPACK_APPIMAGE_SIGN` + +.. variable:: CPACK_APPIMAGE_SIGN_KEY + + Key ID to use for gpg[2] signatures. + + :Default: :variable:`CPACK_APPIMAGE_SIGN_KEY` diff --git a/Help/manual/cpack-generators.7.rst b/Help/manual/cpack-generators.7.rst index abb291b46e..5c8c075aa4 100644 --- a/Help/manual/cpack-generators.7.rst +++ b/Help/manual/cpack-generators.7.rst @@ -13,6 +13,7 @@ Generators .. toctree:: :maxdepth: 1 + /cpack_gen/appimage /cpack_gen/archive /cpack_gen/bundle /cpack_gen/cygwin diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 22e215db9b..ae60d4db7d 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -1191,7 +1191,9 @@ add_library( CPack/cmCPackArchiveGenerator.cxx CPack/cmCPackComponentGroup.cxx CPack/cmCPackDebGenerator.cxx + CPack/cmCPackDebGenerator.h CPack/cmCPackExternalGenerator.cxx + CPack/cmCPackExternalGenerator.h CPack/cmCPackGeneratorFactory.cxx CPack/cmCPackGenerator.cxx CPack/cmCPackLog.cxx @@ -1256,6 +1258,15 @@ if(UNIX) endif() endif() +if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + target_sources( + CPackLib + PRIVATE + CPack/cmCPackAppImageGenerator.cxx + CPack/cmCPackAppImageGenerator.h + ) +endif() + if(CYGWIN) target_sources( CPackLib diff --git a/Source/CPack/cmCPackAppImageGenerator.cxx b/Source/CPack/cmCPackAppImageGenerator.cxx new file mode 100644 index 0000000000..3020f2f8a2 --- /dev/null +++ b/Source/CPack/cmCPackAppImageGenerator.cxx @@ -0,0 +1,457 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ + +#include "cmCPackAppImageGenerator.h" + +#include +#include +#include +#include +#include + +#include + +#include "cmsys/FStream.hxx" + +#include "cmCPackLog.h" +#include "cmELF.h" +#include "cmGeneratedFileStream.h" +#include "cmSystemTools.h" +#include "cmValue.h" + +cmCPackAppImageGenerator::cmCPackAppImageGenerator() = default; + +cmCPackAppImageGenerator::~cmCPackAppImageGenerator() = default; + +int cmCPackAppImageGenerator::InitializeInternal() +{ + this->SetOptionIfNotSet("CPACK_APPIMAGE_TOOL_EXECUTABLE", "appimagetool"); + this->AppimagetoolPath = cmSystemTools::FindProgram( + *this->GetOption("CPACK_APPIMAGE_TOOL_EXECUTABLE")); + + if (this->AppimagetoolPath.empty()) { + cmCPackLogger( + cmCPackLog::LOG_ERROR, + "Cannot find AppImageTool: '" + << *this->GetOption("CPACK_APPIMAGE_TOOL_EXECUTABLE") + << "' check if it's installed, is executable, or is in your PATH" + << std::endl); + + return 0; + } + + this->SetOptionIfNotSet("CPACK_APPIMAGE_PATCHELF_EXECUTABLE", "patchelf"); + this->PatchElfPath = cmSystemTools::FindProgram( + *this->GetOption("CPACK_APPIMAGE_PATCHELF_EXECUTABLE")); + + if (this->PatchElfPath.empty()) { + cmCPackLogger( + cmCPackLog::LOG_ERROR, + "Cannot find patchelf: '" + << *this->GetOption("CPACK_APPIMAGE_PATCHELF_EXECUTABLE") + << "' check if it's installed, is executable, or is in your PATH" + << std::endl); + + return 0; + } + + return Superclass::InitializeInternal(); +} + +int cmCPackAppImageGenerator::PackageFiles() +{ + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "AppDir: \"" << this->toplevel << "\"" << std::endl); + + // Desktop file must be in the toplevel dir + auto const desktopFile = FindDesktopFile(); + if (!desktopFile) { + cmCPackLogger(cmCPackLog::LOG_WARNING, + "A desktop file is required to build an AppImage, make sure " + "it's listed for install()." + << std::endl); + return 0; + } + + { + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "Found Desktop file: \"" << desktopFile.value() << "\"" + << std::endl); + std::string desktopSymLink = this->toplevel + "/" + + cmSystemTools::GetFilenameName(desktopFile.value()); + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "Desktop file destination: \"" << desktopSymLink << "\"" + << std::endl); + auto status = cmSystemTools::CreateSymlink( + cmSystemTools::RelativePath(toplevel, *desktopFile), desktopSymLink); + if (status.IsSuccess()) { + cmCPackLogger(cmCPackLog::LOG_DEBUG, + "Desktop symbolic link created successfully." + << std::endl); + } else { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Error creating symbolic link." << status.GetString() + << std::endl); + return 0; + } + } + + auto const desktopEntry = ParseDesktopFile(*desktopFile); + + { + // Prepare Icon file + auto const iconValue = desktopEntry.find("Icon"); + if (iconValue == desktopEntry.end()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "An Icon key is required to build an AppImage, make sure " + "the desktop file has a reference to one." + << std::endl); + return 0; + } + + auto icon = this->GetOption("CPACK_PACKAGE_ICON"); + if (!icon) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_PACKAGE_ICON is required to build an AppImage." + << std::endl); + return 0; + } + + if (!cmSystemTools::StringStartsWith(*icon, iconValue->second.c_str())) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "CPACK_PACKAGE_ICON must match the file name referenced " + "in the desktop file." + << std::endl); + return 0; + } + + auto const iconFile = FindFile(icon); + if (!iconFile) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Could not find the Icon referenced in the desktop file: " + << *icon << std::endl); + return 0; + } + + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "Icon file: \"" << *iconFile << "\"" << std::endl); + std::string iconSymLink = + this->toplevel + "/" + cmSystemTools::GetFilenameName(*iconFile); + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "Icon link destination: \"" << iconSymLink << "\"" + << std::endl); + auto status = cmSystemTools::CreateSymlink( + cmSystemTools::RelativePath(toplevel, *iconFile), iconSymLink); + if (status.IsSuccess()) { + cmCPackLogger(cmCPackLog::LOG_DEBUG, + "Icon symbolic link created successfully." << std::endl); + } else { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Error creating symbolic link." << status.GetString() + << std::endl); + return 0; + } + } + + std::string application; + { + // Prepare executable file + auto const execValue = desktopEntry.find("Exec"); + if (execValue == desktopEntry.end() || execValue->second.empty()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "An Exec key is required to build an AppImage, make sure " + "the desktop file has a reference to one." + << std::endl); + return 0; + } + + auto const execName = + cmSystemTools::SplitString(execValue->second, ' ').front(); + auto const mainExecutable = FindFile(execName); + + if (!mainExecutable) { + cmCPackLogger( + cmCPackLog::LOG_ERROR, + "Could not find the Executable referenced in the desktop file: " + << execName << std::endl); + return 0; + } + application = cmSystemTools::RelativePath(toplevel, *mainExecutable); + } + + std::string const appRunFile = this->toplevel + "/AppRun"; + { + // AppRun script will run our application + cmGeneratedFileStream appRun(appRunFile); + appRun << R"sh(#! /usr/bin/env bash + + # autogenerated by CPack + + # make sure errors in sourced scripts will cause this script to stop + set -e + + this_dir="$(readlink -f "$(dirname "$0")")" + )sh" << std::endl; + appRun << R"sh(exec "$this_dir"/)sh" << application << R"sh( "$@")sh" + << std::endl; + } + + mode_t permissions; + { + auto status = cmSystemTools::GetPermissions(appRunFile, permissions); + if (!status.IsSuccess()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Error getting AppRun permission: " << status.GetString() + << std::endl); + return 0; + } + } + + auto status = + cmSystemTools::SetPermissions(appRunFile, permissions | S_IXUSR); + if (!status.IsSuccess()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Error changing AppRun permission: " << status.GetString() + << std::endl); + return 0; + } + + // Set RPATH to "$ORIGIN/../lib" + if (!ChangeRPath()) { + return 0; + } + + // Run appimagetool + std::vector command{ + this->AppimagetoolPath, + this->toplevel, + }; + command.emplace_back("../" + *this->GetOption("CPACK_PACKAGE_FILE_NAME") + + this->GetOutputExtension()); + + auto addOptionFlag = [&command, this](std::string const& op, + std::string commandFlag) { + auto opt = this->GetOption(op); + if (opt) { + command.emplace_back(commandFlag); + } + }; + + auto addOption = [&command, this](std::string const& op, + std::string commandFlag) { + auto opt = this->GetOption(op); + if (opt) { + command.emplace_back(commandFlag); + command.emplace_back(*opt); + } + }; + + auto addOptions = [&command, this](std::string const& op, + std::string commandFlag) { + auto opt = this->GetOption(op); + if (opt) { + auto const options = cmSystemTools::SplitString(*opt, ';'); + for (auto const& mkOpt : options) { + command.emplace_back(commandFlag); + command.emplace_back(mkOpt); + } + } + }; + + addOption("CPACK_APPIMAGE_UPDATE_INFORMATION", "--updateinformation"); + + addOptionFlag("CPACK_APPIMAGE_GUESS_UPDATE_INFORMATION", "--guess"); + + addOption("CPACK_APPIMAGE_COMPRESSOR", "--comp"); + + addOptions("CPACK_APPIMAGE_MKSQUASHFS_OPTIONS", "--mksquashfs-opt"); + + addOptionFlag("CPACK_APPIMAGE_NO_APPSTREAM", "--no-appstream"); + + addOption("CPACK_APPIMAGE_EXCLUDE_FILE", "--exclude-file"); + + addOption("CPACK_APPIMAGE_RUNTIME_FILE", "--runtime-file"); + + addOptionFlag("CPACK_APPIMAGE_SIGN", "--sign"); + + addOption("CPACK_APPIMAGE_SIGN_KEY", "--sign-key"); + + cmCPackLogger(cmCPackLog::LOG_OUTPUT, + "Running AppImageTool: " + << cmSystemTools::PrintSingleCommand(command) << std::endl); + int retVal = 1; + bool resS = cmSystemTools::RunSingleCommand( + command, nullptr, nullptr, &retVal, this->toplevel.c_str(), + cmSystemTools::OutputOption::OUTPUT_PASSTHROUGH); + if (!resS || retVal) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Problem running appimagetool: " << this->AppimagetoolPath + << std::endl); + return 0; + } + + return 1; +} + +cm::optional cmCPackAppImageGenerator::FindFile( + std::string const& filename) const +{ + for (std::string const& file : this->files) { + if (cmSystemTools::GetFilenameName(file) == filename) { + cmCPackLogger(cmCPackLog::LOG_DEBUG, "Found file:" << file << std::endl); + return file; + } + } + return cm::nullopt; +} + +cm::optional cmCPackAppImageGenerator::FindDesktopFile() const +{ + cmValue desktopFileOpt = GetOption("CPACK_APPIMAGE_DESKTOP_FILE"); + if (desktopFileOpt) { + return FindFile(*desktopFileOpt); + } + + for (std::string const& file : this->files) { + if (cmSystemTools::StringEndsWith(file, ".desktop")) { + cmCPackLogger(cmCPackLog::LOG_DEBUG, + "Found desktop file:" << file << std::endl); + return file; + } + } + + return cm::nullopt; +} + +namespace { +// Trim leading and trailing whitespace from a string +std::string trim(std::string const& str) +{ + auto start = std::find_if_not( + str.begin(), str.end(), [](unsigned char c) { return std::isspace(c); }); + auto end = std::find_if_not(str.rbegin(), str.rend(), [](unsigned char c) { + return std::isspace(c); + }).base(); + return (start < end) ? std::string(start, end) : std::string(); +} +} // namespace + +std::unordered_map +cmCPackAppImageGenerator::ParseDesktopFile(std::string const& filePath) const +{ + std::unordered_map ret; + + cmsys::ifstream file(filePath); + if (!file.is_open()) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Failed to open desktop file:" << filePath << std::endl); + return ret; + } + + bool inDesktopEntry = false; + std::string line; + while (std::getline(file, line)) { + line = trim(line); + + if (line.empty() || line[0] == '#') { + // Skip empty lines or comments + continue; + } + + if (line.front() == '[' && line.back() == ']') { + // We only care for [Desktop Entry] section + inDesktopEntry = (line == "[Desktop Entry]"); + continue; + } + + if (inDesktopEntry) { + size_t delimiter_pos = line.find('='); + if (delimiter_pos == std::string::npos) { + cmCPackLogger(cmCPackLog::LOG_WARNING, + "Invalid desktop file line format: " << line + << std::endl); + continue; + } + + std::string key = trim(line.substr(0, delimiter_pos)); + std::string value = trim(line.substr(delimiter_pos + 1)); + if (!key.empty()) { + ret.emplace(key, value); + } + } + } + + return ret; +} + +bool cmCPackAppImageGenerator::ChangeRPath() +{ + // AppImages are mounted in random locations so we need RPATH to resolve to + // that location + std::string const newRPath = "$ORIGIN/../lib"; + + for (std::string const& file : this->files) { + cmELF elf(file.c_str()); + + auto const type = elf.GetFileType(); + switch (type) { + case cmELF::FileType::FileTypeExecutable: + case cmELF::FileType::FileTypeSharedLibrary: { + std::string oldRPath; + auto const* rpath = elf.GetRPath(); + if (rpath) { + oldRPath = rpath->Value; + } else { + auto const* runpath = elf.GetRunPath(); + if (runpath) { + oldRPath = runpath->Value; + } else { + oldRPath = ""; + } + } + + if (cmSystemTools::StringStartsWith(oldRPath, "$ORIGIN")) { + // Skip libraries with ORIGIN RPATH set + continue; + } + + if (!PatchElfSetRPath(file, newRPath)) { + return false; + } + + break; + } + default: + cmCPackLogger(cmCPackLog::LOG_DEBUG, + "ELF <" << file << "> type: " << type << std::endl); + break; + } + } + + return true; +} + +bool cmCPackAppImageGenerator::PatchElfSetRPath(std::string const& file, + std::string const& rpath) const +{ + cmCPackLogger(cmCPackLog::LOG_DEBUG, + "Changing RPATH: " << file << " to: " << rpath << std::endl); + int retVal = 1; + bool resS = cmSystemTools::RunSingleCommand( + { + this->PatchElfPath, + "--set-rpath", + rpath, + file, + }, + nullptr, nullptr, &retVal, nullptr, + cmSystemTools::OutputOption::OUTPUT_NONE); + if (!resS || retVal) { + cmCPackLogger(cmCPackLog::LOG_ERROR, + "Problem running patchelf to change RPATH: " << file + << std::endl); + return false; + } + + return true; +} diff --git a/Source/CPack/cmCPackAppImageGenerator.h b/Source/CPack/cmCPackAppImageGenerator.h new file mode 100644 index 0000000000..2e1a84be12 --- /dev/null +++ b/Source/CPack/cmCPackAppImageGenerator.h @@ -0,0 +1,68 @@ +/* Distributed under the OSI-approved BSD 3-Clause License. See accompanying + file LICENSE.rst or https://cmake.org/licensing for details. */ +#pragma once + +#include +#include + +#include + +#include "cmCPackGenerator.h" + +/** \class cmCPackAppImageGenerator + * \brief A generator for creating AppImages with CPack + */ +class cmCPackAppImageGenerator : public cmCPackGenerator +{ +public: + cmCPackTypeMacro(cmCPackAppImageGenerator, cmCPackGenerator); + + char const* GetOutputExtension() override { return ".AppImage"; } + + cmCPackAppImageGenerator(); + ~cmCPackAppImageGenerator() override; + +protected: + /** + * @brief Initializes the CPack engine with our defaults + */ + int InitializeInternal() override; + + /** + * @brief AppImages are for single applications + */ + bool SupportsComponentInstallation() const override { return false; } + + /** + * Main Packaging step + */ + int PackageFiles() override; + +private: + /** + * @brief Finds the first installed file by it's name + */ + cm::optional FindFile(std::string const& filename) const; + + /** + * @brief AppImage format requires a desktop file + */ + cm::optional FindDesktopFile() const; + + /** + * @brief Parses a desktop file [Desktop Entry] + */ + std::unordered_map ParseDesktopFile( + std::string const& filePath) const; + + /** + * @brief changes the RPATH so that AppImage can find it's libraries + */ + bool ChangeRPath(); + + bool PatchElfSetRPath(std::string const& file, + std::string const& rpath) const; + + std::string AppimagetoolPath; + std::string PatchElfPath; +}; diff --git a/Source/CPack/cmCPackGeneratorFactory.cxx b/Source/CPack/cmCPackGeneratorFactory.cxx index daf9fa62e5..c46710ceb1 100644 --- a/Source/CPack/cmCPackGeneratorFactory.cxx +++ b/Source/CPack/cmCPackGeneratorFactory.cxx @@ -39,6 +39,10 @@ # include "WiX/cmCPackWIXGenerator.h" #endif +#ifdef __linux__ +# include "cmCPackAppImageGenerator.h" +#endif + cmCPackGeneratorFactory::cmCPackGeneratorFactory() { if (cmCPackArchiveGenerator::CanGenerate()) { @@ -132,6 +136,12 @@ cmCPackGeneratorFactory::cmCPackGeneratorFactory() cmCPackFreeBSDGenerator::CreateGenerator); } #endif +#ifdef __linux__ + if (cmCPackAppImageGenerator::CanGenerate()) { + this->RegisterGenerator("AppImage", "AppImage packages", + cmCPackAppImageGenerator::CreateGenerator); + } +#endif } std::unique_ptr cmCPackGeneratorFactory::NewGenerator( diff --git a/Tests/RunCMake/CMakeLists.txt b/Tests/RunCMake/CMakeLists.txt index 740e7753f4..56947fc98d 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -1287,6 +1287,12 @@ endif() add_RunCMake_test_group(CPack "${cpack_tests}") +if(CMake_TEST_CPACK_APPIMAGE) + add_RunCMake_test(CPack_AppImage + -DCMake_TEST_CPACK_APPIMAGE_RUNTIME_FILE=${CMake_TEST_CPACK_APPIMAGE_RUNTIME_FILE} + ) +endif() + if(CMake_TEST_CPACK_WIX3 OR CMake_TEST_CPACK_WIX4) add_RunCMake_test(CPack_WIX -DCMake_TEST_CPACK_WIX3=${CMake_TEST_CPACK_WIX3} diff --git a/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-check.cmake b/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-check.cmake new file mode 100644 index 0000000000..4836eb3128 --- /dev/null +++ b/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-check.cmake @@ -0,0 +1,3 @@ +if(NOT EXISTS "${RunCMake_TEST_BINARY_DIR}/GeneratorTest-1.2.3-Linux.AppImage") + set(RunCMake_TEST_FAILED "AppImage package not generated") +endif() diff --git a/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stderr.txt b/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stderr.txt new file mode 100644 index 0000000000..68694f06a4 --- /dev/null +++ b/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stderr.txt @@ -0,0 +1,19 @@ +appimagetool[^ +]* +Using architecture x86_64 +Deleting pre-existing \.DirIcon +Creating \.DirIcon symlink based on information from desktop file +WARNING: AppStream upstream metadata is missing, please consider creating it + in usr/share/metainfo/com\.example\.app\.appdata\.xml + Please see https://www\.freedesktop\.org/software/appstream/docs/chap-Quickstart\.html#sect-Quickstart-DesktopApps + for more information or use the generator at + https://docs\.appimage\.org/packaging-guide/optional/appstream\.html#using-the-appstream-generator +Generating squashfs\.\.\. +Embedding ELF\.\.\. +Marking the AppImage as executable\.\.\. +Embedding MD5 digest +Success + +Please consider submitting your AppImage to AppImageHub, the crowd-sourced +central directory of available AppImages, by opening a pull request +at https://github\.com/AppImage/appimage\.github\.io diff --git a/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stdout.txt b/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stdout.txt new file mode 100644 index 0000000000..09ffa269f5 --- /dev/null +++ b/Tests/RunCMake/CPack_AppImage/AppImageTestApp-cpack-AppImage-stdout.txt @@ -0,0 +1,43 @@ +CPack: Create package using AppImage +CPack: Install projects +CPack: - Install project: CPackAppImageGenerator \[Release\] +CPack: Create package +CPack: AppDir: "[^"]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux" +CPack: Found Desktop file: "[^"]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux/share/applications/com\.example\.app\.desktop" +CPack: Desktop file destination: "[^"]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux/com\.example\.app\.desktop" +CPack: Icon file: "[^"]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux/share/icons/hicolor/64x64/apps/ApplicationIcon\.png" +CPack: Icon link destination: "[^"]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux/ApplicationIcon\.png" +CPack: Running AppImageTool: "[^"]*" "[^"]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux" "\.\./GeneratorTest-1\.2\.3-Linux\.AppImage" "--runtime-file" "[^"]*" +[^ +]*/_CPack_Packages/Linux/AppImage/GeneratorTest-1\.2\.3-Linux should be packaged as \.\./GeneratorTest-1\.2\.3-Linux\.AppImage +Parallel mksquashfs: Using [0-9]+ processors +Creating 4\.0 filesystem on [^ +]*/GeneratorTest-1\.2\.3-Linux\.AppImage, block size [0-9]+\. +.* +Exportable Squashfs 4\.0 filesystem, zstd compressed, data block size [0-9]+ +[ ]compressed data, compressed metadata, compressed fragments, +[ ]compressed xattrs, compressed ids +[ ]duplicates are removed +Filesystem size [0-9.]+ Kbytes \([0-9.]+ Mbytes\) +[ ][0-9.]+% of uncompressed filesystem size \([0-9.]+ Kbytes\) +Inode table size [0-9]+ bytes \([0-9.]+ Kbytes\) +[ ][0-9.]+% of uncompressed inode table size \([0-9]+ bytes\) +Directory table size [0-9]+ bytes \([0-9.]+ Kbytes\) +[ ][0-9.]+% of uncompressed directory table size \([0-9]+ bytes\) +Number of duplicate files found [0-9]+ +Number of inodes [0-9]+ +Number of files [0-9]+ +Number of fragments [0-9]+ +Number of symbolic links [0-9]+ +Number of device nodes [0-9]+ +Number of fifo nodes [0-9]+ +Number of socket nodes [0-9]+ +Number of directories [0-9]+ +Number of hard-links [0-9]+ +Number of ids \(unique uids \+ gids\) [0-9]+ +Number of uids [0-9]+ +[ ]root \([0-9]+\) +Number of gids [0-9]+ +[ ]root \([0-9]+\) +CPack: - package: [^ +]*/Tests/RunCMake/CPack_AppImage/AppImageTestApp-build/GeneratorTest-1\.2\.3-Linux\.AppImage generated\. diff --git a/Tests/RunCMake/CPack_AppImage/RunCMakeTest.cmake b/Tests/RunCMake/CPack_AppImage/RunCMakeTest.cmake new file mode 100644 index 0000000000..2273e9c1d3 --- /dev/null +++ b/Tests/RunCMake/CPack_AppImage/RunCMakeTest.cmake @@ -0,0 +1,9 @@ +include(RunCPack) + +set(RunCPack_GENERATORS AppImage) + +if(CMake_TEST_CPACK_APPIMAGE_RUNTIME_FILE) + list(APPEND RunCMake_TEST_OPTIONS "-DCPACK_APPIMAGE_RUNTIME_FILE=${CMake_TEST_CPACK_APPIMAGE_RUNTIME_FILE}") +endif() + +run_cpack(AppImageTestApp BUILD) diff --git a/Tests/RunCMake/RunCPack/AppImageTestApp/ApplicationIcon.png b/Tests/RunCMake/RunCPack/AppImageTestApp/ApplicationIcon.png new file mode 100644 index 0000000000000000000000000000000000000000..c715e1bc7336344cad881cf44e80beb61c6eddf6 GIT binary patch literal 2335 zcmV+)3E=jLP)k(&zS4oUq8ekI=8tJNiaj zfL&(Z7~2Wk0z;@z;ktzwRy4a4hR_Dk1rmaUAdnCw1c8JgAqXS{2|*wsKMqI;0trDv z5J<@3Pfl#|O=Ax+&8&}|*|9DX@*^_rrk-rIWv=}uvEv@iiLPD0fZDNx&C4fMRvzCnEC$HFAz2n#}!-zD3R~k&rC^uJG)`a1v0EQ(H`hi!pWttb$y^ z6Ww##)h(EKfFtPFU;rOprVcz9+Euq-geIZbpV48ESmDNL8M?K=$PWM9K7qNwyc~^`bwRp&f-tCQ?h8^?zddi40 zOYAD3GO>)1z%FuZfGN4If1|fMqi2nJsmDBVcEaK&-!?>(HRYpURWu#2G{$Wq`m^R4 z{La71Bdk^{eg{yQ1R)E#u4B`i-*WEJX2tJ=IZvG6>Ci$V=^^6JUp~NBl|7B~SgYW30sR81s6eG#>{#WHvy*r@O@P z=kI%W`DeN3&<7fV$GzMUIrZJ%_<7}H$t&t6(l>S3@q6Lq;9{Je1Ukg*CO+=6XMWXY zCGM=6OkMZcQ1sHbx?|@&+ZsN}R1-M*Po;qauX}xA3rz{BnaEtr@)D3hej!OPspRAT ztP1a-wI-{;7Nm+no&o#C7Hj%uP4*$bkr$Ctlo$bdggwMmz{056PQ7s|em$HD&j(VJ zplGVJJIX3>gs4hjaA2Y9oWt+Z#jy9G8t*P?N{9jjGaq5=VBynE9@1({-)hvR3oidY z-Q?d*H;Ez4YM+x=0pX@LU_fj^y7j51z=4_8iWMbil<8@= zW{Un$JhQcCgzQ{o-ne=^b^Tn3sijzv26;$=6ewGeDJ~w4UkF%i~Pq!2iv=udLp`M5x34btzy^DLSXQZtb{Cq)T9kjKb{arB*@@ZKs5 zF%=Fh5PyWHm;eH#NFmm0B`;NN>SWYIzbyeo&JjDQWj6IMRcgrHZ+mv`pNF%%+!$<2 z^BIbRv%l<6hc)vv`8>?lst8f8f4tloJ8sfTy_8Rrjj#{~lAPP(As>FKDR!V1^N{Rx zd>l^4*D;*P8g#myw1wyoKexm$fQ3;POSq5M`*ww$oH5tanR*NZ1v=W>3kn=zrqb9u zPKdj7Bc#!3F{;E}miZc@j5@^SJ!^L!LO}U5v4Ft|&X=KMhmwE}>2(nQFx(l#GoP|=tJfML*MwHX-3HV+o4yU`6y+@rh7mFwq~8ZRrqq(S z%rHV=`x{8IE}&y(g7n*l5%O4F=rB%@G=7>^t;+*c5F#)0{%wWZcYr6sI;R5ks~vNh zS5@yPXfgBNP~xU(Klw&5#kq15Tv(uB!7_vpN0@GeB``+t3>>laIfy?#)fzl_=}NuY ziS&&?W#WX8fESiwkfPVu`&*^=upt-qS7(&zAQ+KUTtK65A#m-z-G)C=Hk!Qr^2cqP ztrYUvNctk6GNCjVfu9vyfs;%#TbGX~ul{s6eqUZ(g861w>}(D65$&*=MhC| z^>v3~H}Mhpp5T?16q6}7Cr%BfIUC3U&QD=kkdWHNy(w1wO0DRa!|zmzr9DJ{`f!4U^<6#DW8}VL48|-(nHD5OaX+pr#wu#X%Gtg@$vSiTh2*<~ zk^NSBkc$#I*Lh|29sBhgjJQbCxR0tvLKHtXe1^x@g_&LC{U8g+i&C2%ziOqpNiX%{ zQk)zHUYD;I#J+~}pPb(w3Hg@5M^3;;PXL)Q5Bcy4A5MT=UzamHQ5V?}cJe=Rc8!FTjwCLAq&~RaH;Jtri=X_jy1*Vt2m%Q~LJ&xZ;Xy)> z5Cjr}gdmU*Bm{wkAR!1O1PNIelSBzCG9xKkU>$z*{{y_Vblu<5zDEE6002ovPDHLk FV1ju)WxfCa literal 0 HcmV?d00001 diff --git a/Tests/RunCMake/RunCPack/AppImageTestApp/CMakeLists.txt b/Tests/RunCMake/RunCPack/AppImageTestApp/CMakeLists.txt new file mode 100644 index 0000000000..c792a5caee --- /dev/null +++ b/Tests/RunCMake/RunCPack/AppImageTestApp/CMakeLists.txt @@ -0,0 +1,30 @@ +cmake_minimum_required(VERSION 4.0) + +project(CPackAppImageGenerator + LANGUAGES CXX + VERSION "1.2.3" +) + +add_executable(app main.cpp) + +install(TARGETS app + RUNTIME + DESTINATION bin + COMPONENT applications) + +install(FILES com.example.app.desktop DESTINATION share/applications) +install(FILES ApplicationIcon.png DESTINATION share/icons/hicolor/64x64/apps) + +# Create AppImage package +set(CPACK_GENERATOR AppImage) +set(CPACK_PACKAGE_NAME GeneratorTest) +set(CPACK_PACKAGE_VERSION ${CMAKE_PROJECT_VERSION}) +set(CPACK_PACKAGE_VENDOR "ACME Inc") +set(CPACK_PACKAGE_DESCRIPTION "An AppImage package for testing CMake's CPack AppImage generator") +set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A test AppImage package") +set(CPACK_PACKAGE_HOMEPAGE_URL "https://www.example.com") + +# AppImage generator variables +set(CPACK_PACKAGE_ICON ApplicationIcon.png) + +include(CPack) diff --git a/Tests/RunCMake/RunCPack/AppImageTestApp/com.example.app.desktop b/Tests/RunCMake/RunCPack/AppImageTestApp/com.example.app.desktop new file mode 100644 index 0000000000..0ab1085f86 --- /dev/null +++ b/Tests/RunCMake/RunCPack/AppImageTestApp/com.example.app.desktop @@ -0,0 +1,6 @@ +[Desktop Entry] +Name=App +Exec=app %u +Icon=ApplicationIcon +Type=Application +Categories=System diff --git a/Tests/RunCMake/RunCPack/AppImageTestApp/main.cpp b/Tests/RunCMake/RunCPack/AppImageTestApp/main.cpp new file mode 100644 index 0000000000..5047a34e39 --- /dev/null +++ b/Tests/RunCMake/RunCPack/AppImageTestApp/main.cpp @@ -0,0 +1,3 @@ +int main() +{ +}