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/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/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/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/.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" < + 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 18c2c35b5e..d1e6ad9477 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -1195,7 +1195,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 @@ -1260,6 +1262,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 a33fceb3f1..07cb6da48f 100644 --- a/Tests/RunCMake/CMakeLists.txt +++ b/Tests/RunCMake/CMakeLists.txt @@ -1290,6 +1290,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 0000000000..c715e1bc73 Binary files /dev/null and b/Tests/RunCMake/RunCPack/AppImageTestApp/ApplicationIcon.png differ 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() +{ +}