| /* 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; |
| } |