blob: 3020f2f8a2f64e07c09c1c79d053c31e64afbaf8 [file] [log] [blame] [edit]
/* 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;
}