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