1
0
Эх сурвалжийг харах

CPack: Add AppImage generator

This AppImage generator only relies on appimagetool and patchelf.

Closes: #27104
Co-authored-by: Brad King <[email protected]>
Daniel Nicoletti 2 сар өмнө
parent
commit
1a6dbcc9ea

+ 2 - 0
.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 "")

+ 1 - 0
.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

+ 130 - 0
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 $<TARGET_FILE:my_application_target>
+          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`

+ 1 - 0
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

+ 11 - 0
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

+ 457 - 0
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 <algorithm>
+#include <cctype>
+#include <cstddef>
+#include <utility>
+#include <vector>
+
+#include <fcntl.h>
+
+#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<std::string> 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<std::string> 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<std::string> 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<std::string, std::string>
+cmCPackAppImageGenerator::ParseDesktopFile(std::string const& filePath) const
+{
+  std::unordered_map<std::string, std::string> 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;
+}

+ 68 - 0
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 <string>
+#include <unordered_map>
+
+#include <cm/optional>
+
+#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<std::string> FindFile(std::string const& filename) const;
+
+  /**
+   * @brief AppImage format requires a desktop file
+   */
+  cm::optional<std::string> FindDesktopFile() const;
+
+  /**
+   * @brief Parses a desktop file [Desktop Entry]
+   */
+  std::unordered_map<std::string, std::string> 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;
+};

+ 10 - 0
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<cmCPackGenerator> cmCPackGeneratorFactory::NewGenerator(

+ 6 - 0
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}

+ 3 - 0
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()

+ 19 - 0
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

+ 43 - 0
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\.

+ 9 - 0
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)

BIN
Tests/RunCMake/RunCPack/AppImageTestApp/ApplicationIcon.png


+ 30 - 0
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)

+ 6 - 0
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

+ 3 - 0
Tests/RunCMake/RunCPack/AppImageTestApp/main.cpp

@@ -0,0 +1,3 @@
+int main()
+{
+}