From e598536173302448ecfc76ccf5f52d8cea12df85 Mon Sep 17 00:00:00 2001 From: TheAssassin Date: Wed, 30 May 2018 19:21:08 +0200 Subject: [PATCH] Initial commit --- .gitignore | 3 + .gitmodules | 9 + CMakeLists.txt | 13 + LICENSE.txt | 19 + README.md | 5 + include/linuxdeploy/core/appdir.h | 71 ++++ include/linuxdeploy/core/desktopfile.h | 71 ++++ include/linuxdeploy/core/elf.h | 40 ++ include/linuxdeploy/core/log.h | 69 ++++ include/linuxdeploy/core/util.h | 57 +++ lib/CMakeLists.txt | 15 + lib/args | 1 + lib/cpp-feather-ini-parser | 1 + lib/cpp-subprocess | 1 + src/CMakeLists.txt | 4 + src/core/CMakeLists.txt | 27 ++ src/core/appdir.cpp | 538 +++++++++++++++++++++++++ src/core/desktopfile.cpp | 130 ++++++ src/core/elf.cpp | 124 ++++++ src/core/generate-excludelist.sh | 51 +++ src/core/log.cpp | 119 ++++++ src/core/main.cpp | 204 ++++++++++ 22 files changed, 1572 insertions(+) create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 CMakeLists.txt create mode 100644 LICENSE.txt create mode 100644 README.md create mode 100644 include/linuxdeploy/core/appdir.h create mode 100644 include/linuxdeploy/core/desktopfile.h create mode 100644 include/linuxdeploy/core/elf.h create mode 100644 include/linuxdeploy/core/log.h create mode 100644 include/linuxdeploy/core/util.h create mode 100644 lib/CMakeLists.txt create mode 160000 lib/args create mode 160000 lib/cpp-feather-ini-parser create mode 160000 lib/cpp-subprocess create mode 100644 src/CMakeLists.txt create mode 100644 src/core/CMakeLists.txt create mode 100644 src/core/appdir.cpp create mode 100644 src/core/desktopfile.cpp create mode 100644 src/core/elf.cpp create mode 100644 src/core/generate-excludelist.sh create mode 100644 src/core/log.cpp create mode 100644 src/core/main.cpp diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..fa52937 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +cmake-build-*/ +*build*/ +.idea/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..7b9f0db --- /dev/null +++ b/.gitmodules @@ -0,0 +1,9 @@ +[submodule "lib/cpp-subprocess"] + path = lib/cpp-subprocess + url = https://github.com/arun11299/cpp-subprocess.git +[submodule "lib/args"] + path = lib/args + url = https://github.com/Taywee/args.git +[submodule "lib/cpp-feather-ini-parser"] + path = lib/cpp-feather-ini-parser + url = https://github.com/Turbine1991/cpp-feather-ini-parser.git diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..7094a10 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,13 @@ +cmake_minimum_required(VERSION 3.2) + +project(linuxdeploy C CXX) + +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) + +set(LINUXDEPLOY_VERSION 0.1-alpha-1) +add_definitions(-DLINUXDEPLOY_VERSION="${LINUXDEPLOY_VERSION}") + +add_subdirectory(lib) + +add_subdirectory(src) diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..bf464e8 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,19 @@ +Copyright 2018 TheAssassin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5fe3703 --- /dev/null +++ b/README.md @@ -0,0 +1,5 @@ +# linuxdeploy + +AppDir creation and maintenance tool. + +**More info will follow soon!** diff --git a/include/linuxdeploy/core/appdir.h b/include/linuxdeploy/core/appdir.h new file mode 100644 index 0000000..c5e4646 --- /dev/null +++ b/include/linuxdeploy/core/appdir.h @@ -0,0 +1,71 @@ +// system includes +#include + +// library includes +#include + +// local includes +#include "linuxdeploy/core/desktopfile.h" + +#pragma once + +namespace linuxdeploy { + namespace core { + namespace appdir { + /* + * Base class for AppDirs. + */ + class AppDir { + private: + // private data class pattern + class PrivateData; + PrivateData* d; + + public: + // default constructor + // construct AppDir from given path + // the directory will be created if it doesn't exist + explicit AppDir(const boost::filesystem::path& path); + + ~AppDir(); + + // alternative constructor + // shortcut for using a normal string instead of a path + explicit AppDir(const std::string& path); + + // creates basic directory structure of an AppDir in "FHS" mode + bool createBasicStructure(); + + // deploy shared library + bool deployLibrary(const boost::filesystem::path& path); + + // deploy executable + bool deployExecutable(const boost::filesystem::path& path); + + // deploy desktop file + bool deployDesktopFile(const desktopfile::DesktopFile& desktopFile); + + // deploy icon + bool deployIcon(const boost::filesystem::path& path); + + // execute deferred copy operations + bool executeDeferredOperations(); + + // return path to AppDir + boost::filesystem::path path(); + + // create a list of all icon paths in the AppDir + std::vector deployedIconPaths(); + + // create a list of all executable paths in the AppDir + std::vector deployedExecutablePaths(); + + // create a list of all desktop file paths in the AppDir + std::vector deployedDesktopFiles(); + + // create symlinks for AppRun, desktop file and icon in the AppDir root directory + bool createLinksInAppDirRoot(const desktopfile::DesktopFile& desktopFile); + }; + } + } +} diff --git a/include/linuxdeploy/core/desktopfile.h b/include/linuxdeploy/core/desktopfile.h new file mode 100644 index 0000000..2e495cd --- /dev/null +++ b/include/linuxdeploy/core/desktopfile.h @@ -0,0 +1,71 @@ +// system includes + +// library includes +#include + +#pragma once + +namespace linuxdeploy { + namespace core { + namespace desktopfile { + /* + * Parse and read desktop files. + */ + class DesktopFile { + private: + // private data class pattern + class PrivateData; + PrivateData* d; + + public: + // default constructor + DesktopFile(); + + // construct from existing desktop file + // file must exist + explicit DesktopFile(const boost::filesystem::path& path); + + // read desktop file + // sets path associated with this file + bool read(const boost::filesystem::path& path); + + // get path associated with this file + boost::filesystem::path path() const; + + // sets the path associated with this desktop file + // used to e.g., save the desktop file + void setPath(const boost::filesystem::path& path); + + // clear contents of desktop file + void clear(); + + // save desktop file + bool save() const; + + // save desktop file to path + // does not change path associated with desktop file + bool save(const boost::filesystem::path& path) const; + + // check if entry exists in given section and key + bool entryExists(const std::string& section, const std::string& key) const; + + // get key from desktop file + // an std::string passed as value parameter will be populated with the contents + // returns true (and populates value) if the key exists, false otherwise + bool getEntry(const std::string& section, const std::string& key, std::string& value) const; + + // add key to section in desktop file + // the section will be created if it doesn't exist already + // returns true if an existing key was overwritten, false otherwise + bool setEntry(const std::string& section, const std::string& key, const std::string& value); + + // create common application entries in desktop file + // returns false if one of the keys exists and was left unmodified + bool addDefaultKeys(const std::string& executableFileName); + + // validate desktop file + bool validate() const; + }; + } + } +} diff --git a/include/linuxdeploy/core/elf.h b/include/linuxdeploy/core/elf.h new file mode 100644 index 0000000..d582ce6 --- /dev/null +++ b/include/linuxdeploy/core/elf.h @@ -0,0 +1,40 @@ +// system includes +#include +#include + +// library includes +#include + +#pragma once + +namespace linuxdeploy { + namespace core { + namespace elf { + class ElfFile { + private: + class PrivateData; + PrivateData* d; + + public: + explicit ElfFile(const boost::filesystem::path& path); + ~ElfFile(); + + public: + // recursively trace dynamic library dependencies of a given ELF file + // this works for both libraries and executables + // the resulting vector consists of absolute paths to the libraries determined by the same methods a system's + // linker would use + std::vector traceDynamicDependencies(); + + // fetch rpath stored in binary + // it appears that according to the ELF standard, the rpath is ignored in libraries, therefore if the path + // points to an executable, an empty string is returned + std::string getRPath(); + + // set rpath in ELF file + // returns true on success, false otherwise + bool setRPath(const std::string& value); + }; + } + } +} diff --git a/include/linuxdeploy/core/log.h b/include/linuxdeploy/core/log.h new file mode 100644 index 0000000..cd73c6e --- /dev/null +++ b/include/linuxdeploy/core/log.h @@ -0,0 +1,69 @@ +// system includes +#include + +// library includes +#include + +#pragma once + +namespace linuxdeploy { + namespace core { + namespace log { + enum LD_LOGLEVEL { + LD_DEBUG = 0, + LD_INFO, + LD_WARNING, + LD_ERROR + }; + + enum LD_STREAM_CONTROL { + LD_NOOP = 0, + LD_NO_SPACE, + }; + + class ldLog { + private: + // this is the type of std::cout + typedef std::basic_ostream > CoutType; + + // this is the function signature of std::endl + typedef CoutType& (* stdEndlType)(CoutType&); + + private: + static LD_LOGLEVEL verbosity; + + private: + bool prependSpace; + bool logLevelSet; + CoutType& stream = std::cout; + + LD_LOGLEVEL currentLogLevel; + + private: + // advanced behavior + ldLog(bool prependSpace, bool logLevelSet, LD_LOGLEVEL logLevel); + + void checkPrependSpace(); + + bool checkVerbosity(); + + public: + static void setVerbosity(LD_LOGLEVEL verbosity); + + public: + // public constructor + // does not implement the advanced behavior -- see private constructors for that + ldLog(); + + public: + ldLog operator<<(const std::string& message); + ldLog operator<<(const char* message); + ldLog operator<<(const boost::filesystem::path& path); + ldLog operator<<(const double val); + ldLog operator<<(stdEndlType strm); + ldLog operator<<(const LD_LOGLEVEL logLevel); + ldLog operator<<(const LD_STREAM_CONTROL streamControl); + }; + } + } +} diff --git a/include/linuxdeploy/core/util.h b/include/linuxdeploy/core/util.h new file mode 100644 index 0000000..5607b6b --- /dev/null +++ b/include/linuxdeploy/core/util.h @@ -0,0 +1,57 @@ +#pragma once + +#include +#include +#include + +namespace linuxdeploy { + namespace core { + namespace util { + static inline bool ltrim(std::string& s, char to_trim = ' ') { + // TODO: find more efficient way to check whether elements have been removed + size_t initialLength = s.length(); + s.erase(s.begin(), std::find_if(s.begin(), s.end(), [to_trim](int ch) { + return ch != to_trim; + })); + return s.length() < initialLength; + } + + static inline bool rtrim(std::string& s, char to_trim = ' ') { + // TODO: find more efficient way to check whether elements have been removed + auto initialLength = s.length(); + s.erase(std::find_if(s.rbegin(), s.rend(), [to_trim](int ch) { + return ch != to_trim; + }).base(), s.end()); + return s.length() < initialLength; + } + + static inline bool trim(std::string& s, char to_trim = ' ') { + // returns true if either modifies s + auto ltrim_result = ltrim(s, to_trim); + return rtrim(s, to_trim) && ltrim_result; + } + + static std::vector split(const std::string& s, char delim = ' ') { + std::vector result; + + std::stringstream ss(s); + std::string item; + + while (std::getline(ss, item, delim)) { + result.push_back(item); + } + + return result; + } + + static std::vector splitLines(const std::string& s) { + return split(s, '\n'); + } + + static inline std::string strLower(std::string s) { + std::transform(s.begin(), s.end(), s.begin(), [](unsigned char c) { return std::tolower(c); }); + return s; + } + } + } +} diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt new file mode 100644 index 0000000..202ebe3 --- /dev/null +++ b/lib/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(subprocess INTERFACE) +target_sources(subprocess INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/cpp-subprocess/subprocess.hpp) +target_include_directories(subprocess INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/cpp-subprocess) + +add_library(args INTERFACE) +target_sources(args INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/args/args.hxx) +target_include_directories(args INTERFACE args) + +add_library(cpp-feather-ini-parser INTERFACE) +target_sources(cpp-feather-ini-parser INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/cpp-feather-ini-parser/INI.h) +target_include_directories(cpp-feather-ini-parser INTERFACE cpp-feather-ini-parser) + +add_executable(test_cpp_feather_ini_parser EXCLUDE_FROM_ALL ${CMAKE_CURRENT_SOURCE_DIR}/cpp-feather-ini-parser/example/example.cpp) +target_link_libraries(test_cpp_feather_ini_parser PRIVATE cpp-feather-ini-parser) +add_test(test_cpp_feather_ini_parser test_cpp_feather_ini_parser) diff --git a/lib/args b/lib/args new file mode 160000 index 0000000..bd0429e --- /dev/null +++ b/lib/args @@ -0,0 +1 @@ +Subproject commit bd0429e91f5bb140271870d5421e412bf78b9f31 diff --git a/lib/cpp-feather-ini-parser b/lib/cpp-feather-ini-parser new file mode 160000 index 0000000..2dff628 --- /dev/null +++ b/lib/cpp-feather-ini-parser @@ -0,0 +1 @@ +Subproject commit 2dff628f921047dbee4b1795f40875b06ae27b50 diff --git a/lib/cpp-subprocess b/lib/cpp-subprocess new file mode 160000 index 0000000..05c76a5 --- /dev/null +++ b/lib/cpp-subprocess @@ -0,0 +1 @@ +Subproject commit 05c76a531180298a8404bdf5fccb71137c62cd76 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..fb58c3e --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,4 @@ +# globally include own includes +include_directories(${PROJECT_SOURCE_DIR}/include) + +add_subdirectory(core) diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt new file mode 100644 index 0000000..7d8731e --- /dev/null +++ b/src/core/CMakeLists.txt @@ -0,0 +1,27 @@ +# 3.5 is required for imported boost targets +# 3.6 is required for the PkgConfig module's IMPORTED_TARGET library feature +cmake_minimum_required(VERSION 3.6) + +# include headers to make CLion happy +file(GLOB HEADERS ${PROJECT_SOURCE_DIR}/include/linuxdeploy/core/*.h) + +find_package(Boost REQUIRED COMPONENTS filesystem regex) +find_package(Threads) + +find_package(PkgConfig) +pkg_check_modules(magick++ REQUIRED IMPORTED_TARGET Magick++) + +message(STATUS "Generating excludelist") +execute_process( + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/generate-excludelist.sh + WORKING_DIRECTORY ${CMAKE_CURRENT_BINARY_DIR} +) + +add_library(core elf.cpp log.cpp appdir.cpp desktopfile.cpp ${HEADERS}) +target_link_libraries(core Boost::filesystem Boost::regex subprocess cpp-feather-ini-parser PkgConfig::magick++ ${CMAKE_THREAD_LIBS_INIT}) +target_include_directories(core PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +add_executable(linuxdeploy main.cpp) +target_link_libraries(linuxdeploy core args) + +set_target_properties(linuxdeploy PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") diff --git a/src/core/appdir.cpp b/src/core/appdir.cpp new file mode 100644 index 0000000..b5eb4ea --- /dev/null +++ b/src/core/appdir.cpp @@ -0,0 +1,538 @@ +// library headers +#include +#include +#include +#include +#include +#include + +// local headers +#include "linuxdeploy/core/appdir.h" +#include "linuxdeploy/core/elf.h" +#include "linuxdeploy/core/log.h" +#include "linuxdeploy/core/util.h" +#include "excludelist.h" + +using namespace linuxdeploy::core; +using namespace linuxdeploy::core::log; + +namespace bf = boost::filesystem; + +namespace linuxdeploy { + namespace core { + namespace appdir { + class AppDir::PrivateData { + public: + bf::path appDirPath; + std::map copyOperations; + std::vector setElfRPathOperations; + + public: + PrivateData() { + this->copyOperations = {}; + }; + + public: + // actually copy file + // mimics cp command behavior + bool copyFile(const bf::path& from, bf::path to) { + ldLog() << "Copying file" << from << "to" << to << std::endl; + + try { + if (!to.parent_path().empty() && !bf::is_directory(to.parent_path()) && !bf::create_directories(to.parent_path())) { + ldLog() << LD_ERROR << "Failed to create parent directory" << to.parent_path() << "for path" << to << std::endl; + return false; + } + + if (*(to.string().end() - 1) == '/' || bf::is_directory(to)) + to /= from.filename(); + + bf::copy_file(from, to, bf::copy_option::overwrite_if_exists); + } catch (const bf::filesystem_error& e) { + return false; + } + + return true; + } + + // create symlink + bool symlinkFile(bf::path target, bf::path symlink, const bool useRelativePath = true) { + ldLog() << "Creating symlink for file" << target << "in/as" << symlink << std::endl; + + /*try { + if (!symlink.parent_path().empty() && !bf::is_directory(symlink.parent_path()) && !bf::create_directories(symlink.parent_path())) { + ldLog() << LD_ERROR << "Failed to create parent directory" << symlink.parent_path() << "for path" << symlink << std::endl; + return false; + } + + if (*(symlink.string().end() - 1) == '/' || bf::is_directory(symlink)) + symlink /= target.filename(); + + if (bf::exists(symlink) || bf::symbolic_link_exists(symlink)) + bf::remove(symlink); + + if (relativeDirectory != "") { + // TODO + } + + bf::create_symlink(target, symlink); + } catch (const bf::filesystem_error& e) { + return false; + }*/ + + if (!useRelativePath) { + ldLog() << LD_ERROR << "Not implemented" << std::endl; + return false; + } + + subprocess::Popen proc({"ln", "-f", "-s", "--relative", target.c_str(), symlink.c_str()}, + subprocess::output(subprocess::PIPE), + subprocess::error(subprocess::PIPE) + ); + + auto outputs = proc.communicate(); + + if (proc.retcode() != 0) { + ldLog() << LD_ERROR << "ln subprocess failed:" << std::endl + << outputs.first.buf << std::endl << outputs.second.buf << std::endl; + return false; + } + + return true; + } + + bool checkDuplicate(const bf::path& path) { + // FIXME: use more efficient search (e.g., binary search) + // a linear search is not _really_ efficient + //return std::binary_search(copyOperations.begin(), copyOperations.end(), path); + for (const auto& pair : copyOperations) { + if (pair.first == path) { + ldLog() << LD_DEBUG << "Duplicate:" << pair.first << std::endl; + return true; + } + } + + return false; + } + + // execute deferred copy operations registered with the deploy* functions + bool executeDeferredOperations() { + bool success = true; + + while (!copyOperations.empty()) { + const auto& pair = *(copyOperations.begin()); + const auto& from = pair.first; + const auto& to = pair.second; + + if (!copyFile(from, to)) { + ldLog() << LD_ERROR << "Failed to copy file" << from << "to" << to << std::endl; + success = false; + } + + copyOperations.erase(copyOperations.begin()); + } + + if (success) { + while (!setElfRPathOperations.empty()) { + static const auto rpath = "$ORIGIN/../lib"; + const auto& elfFilePath = *(setElfRPathOperations.begin()); + + ldLog() << "Setting rpath in ELF file" << elfFilePath << "to" << rpath << std::endl; + if (!elf::ElfFile(elfFilePath).setRPath(rpath)) { + ldLog() << LD_ERROR << "Failed to set rpath in ELF file:" << elfFilePath << std::endl; + success = false; + } + + setElfRPathOperations.erase(setElfRPathOperations.begin()); + } + } + + return success; + } + + // register copy operation that will be executed later + // by compiling a list of files to copy instead of just copying everything, one can ensure that + // the files are touched once only + void deployFile(const bf::path& from, bf::path to) { + ldLog() << LD_DEBUG << "Deploying file" << from << "to" << to << std::endl; + + // not sure whether this is 100% bullet proof, but it simulates the cp command behavior + if (to.string().back() == '/' || bf::is_directory(to)) { + to /= from.filename(); + } + + copyOperations[from] = to; + } + + bool deployElfDependencies(const bf::path& path) { + ldLog() << "Deploying dependencies for ELF file" << path << std::endl; + + for (const auto& dependencyPath : elf::ElfFile(path).traceDynamicDependencies()) { + if (!deployLibrary(dependencyPath)) + return false; + } + + return true; + } + + bool deployLibrary(const bf::path& path) { + if (checkDuplicate(path)) { + ldLog() << LD_DEBUG << "Skipping duplicate deployment of shared library" << path << std::endl; + return true; + } + + static auto isInExcludelist = [](const bf::path& fileName) { + for (const auto& excludePattern : generatedExcludelist) { + // simple string match is faster than using fnmatch + if (excludePattern == fileName) + return true; + + auto fnmatchResult = fnmatch(excludePattern.c_str(), fileName.string().c_str(), FNM_PATHNAME); + switch (fnmatchResult) { + case 0: + return true; + case FNM_NOMATCH: + break; + default: + ldLog() << LD_ERROR << "fnmatch() reported error:" << fnmatchResult << std::endl; + return false; + } + } + + return false; + }; + + if (isInExcludelist(path.filename())) { + ldLog() << "Skipping deployment of blacklisted library" << path << std::endl; + return true; + } else { + ldLog() << "Deploying shared library" << path << std::endl; + } + + deployFile(path, appDirPath / "usr/lib/"); + + setElfRPathOperations.push_back(appDirPath / "usr/lib" / path.filename()); + + if (!deployElfDependencies(path)) + return false; + + return true; + } + + bool deployExecutable(const bf::path& path) { + if (checkDuplicate(path)) { + ldLog() << LD_DEBUG << "Skipping duplicate deployment of executable" << path << std::endl; + return true; + } + + ldLog() << "Deploying executable" << path << std::endl; + + // FIXME: make executables executable + + deployFile(path, appDirPath / "usr/bin/"); + + setElfRPathOperations.push_back(appDirPath / "usr/bin" / path.filename()); + + if (!deployElfDependencies(path)) + return false; + + return true; + } + + bool deployDesktopFile(const desktopfile::DesktopFile& desktopFile) { + if (checkDuplicate(desktopFile.path())) { + ldLog() << LD_DEBUG << "Skipping duplicate deployment of desktop file" << desktopFile.path() << std::endl; + return true; + } + + if (desktopFile.validate()) { + ldLog() << LD_ERROR << "Failed to verify desktop file:" << desktopFile.path() << std::endl; + } + + ldLog() << "Deploying desktop file" << desktopFile.path() << std::endl; + + deployFile(desktopFile.path(), appDirPath / "usr/share/applications/"); + + return true; + } + + bool deployIcon(const bf::path& path) { + if (checkDuplicate(path)) { + ldLog() << LD_DEBUG << "Skipping duplicate deployment of icon" << path << std::endl; + return true; + } + + ldLog() << "Deploying icon" << path << std::endl; + + Magick::Image image; + + try { + image.read(path.string()); + } catch (const Magick::Exception& error) { + return false; + } + + auto xRes = image.columns(); + auto yRes = image.rows(); + + if (xRes != yRes) { + ldLog() << LD_WARNING << "x and y resolution of icon are not equal:" << path; + } + + auto resolution = std::to_string(xRes) + "x" + std::to_string(yRes); + + auto format = image.format(); + + // if file is a vector image, use "scalable" directory + if (util::strLower(bf::extension(path)) == "svg") { + resolution = "scalable"; + } else { + // otherwise, test resolution against "known good" values, and reject invalid ones + const auto knownResolutions = {8, 16, 20, 22, 24, 32, 48, 64, 72, 96, 128, 192, 256, 512}; + + // assume invalid + bool invalidXRes = true, invalidYRes = true; + + for (const auto res : knownResolutions) { + if (xRes == res) + invalidXRes = false; + if (yRes == res) + invalidYRes = false; + } + + if (invalidXRes) { + ldLog() << LD_ERROR << "Icon" << path << "has invalid x resolution:" << xRes; + return false; + } + + if (invalidYRes) { + ldLog() << LD_ERROR << "Icon" << path << "has invalid x resolution:" << xRes; + return false; + } + } + + deployFile(path, appDirPath / "usr/share/icons/hicolor" / resolution / "apps/"); + + return true; + } + }; + + AppDir::AppDir(const bf::path& path) { + d = new PrivateData(); + + d->appDirPath = path; + } + + AppDir::~AppDir() { + delete d; + } + + AppDir::AppDir(const std::string& path) : AppDir(bf::path(path)) {} + + bool AppDir::createBasicStructure() { + std::vector dirPaths = { + "usr/bin/", + "usr/lib/", + "usr/share/applications/", + "usr/share/icons/hicolor/", + }; + + for (const std::string& resolution : {"16x16", "32x32", "64x64", "128x128", "256x256", "scalable"}) { + auto iconPath = "usr/share/icons/hicolor/" + resolution + "/apps/"; + dirPaths.push_back(iconPath); + } + + for (const auto& dirPath : dirPaths) { + auto fullDirPath = d->appDirPath / dirPath; + + ldLog() << "Creating directory" << fullDirPath << std::endl; + + // skip directory if it exists + if (bf::is_directory(fullDirPath)) + continue; + + try { + bf::create_directories(fullDirPath); + } catch (const bf::filesystem_error&) { + ldLog() << LD_ERROR << "Failed to create directory" << fullDirPath; + return false; + } + } + + return true; + } + + bool AppDir::deployLibrary(const bf::path& path) { + return d->deployLibrary(path); + } + + bool AppDir::deployExecutable(const bf::path& path) { + return d->deployExecutable(path); + } + + bool AppDir::deployDesktopFile(const desktopfile::DesktopFile& desktopFile) { + return d->deployDesktopFile(desktopFile); + } + + bool AppDir::deployIcon(const bf::path& path) { + return d->deployIcon(path); + } + + bool AppDir::executeDeferredOperations() { + return d->executeDeferredOperations(); + } + + boost::filesystem::path AppDir::path() { + return d->appDirPath; + } + + static std::vector listFilesInDirectory(const bf::path& path, const bool recursive = true) { + std::vector foundPaths; + + std::vector pathBuf(path.string().size() + 1, '\0'); + strcpy(pathBuf.data(), path.string().c_str()); + + std::vector searchPaths = {pathBuf.data(), nullptr}; + + if (recursive) { + // reset errno + errno = 0; + + auto* fts = fts_open(searchPaths.data(), FTS_NOCHDIR | FTS_NOSTAT, nullptr); + + int error = errno; + if (fts == nullptr || error != 0) { + ldLog() << LD_ERROR << "fts() failed:" << strerror(error) << std::endl; + return {}; + } + + FTSENT* ent; + while ((ent = fts_read(fts)) != nullptr) { + // FIXME: use ent's fts_info member instead of boost::filesystem + if (bf::is_regular_file(ent->fts_path)) { + foundPaths.push_back(ent->fts_path); + } + }; + error = errno; + if (error != 0) { + ldLog() << LD_ERROR << "fts_read() failed:" << strerror(error) << std::endl; + return {}; + } + } else { + DIR* dir; + if ((dir = opendir(path.string().c_str())) == NULL) { + auto error = errno; + ldLog() << LD_ERROR << "opendir() failed:" << strerror(error); + } + + struct dirent* ent; + while ((ent = readdir(dir)) != NULL) { + auto fullPath = path / bf::path(ent->d_name); + if (bf::is_regular_file(fullPath)) { + foundPaths.push_back(fullPath); + } + } + } + + return foundPaths; + } + + std::vector AppDir::deployedIconPaths() { + return listFilesInDirectory(path() / "/usr/share/icons/"); + } + + std::vector AppDir::deployedExecutablePaths() { + auto paths = listFilesInDirectory(path() / "usr/bin/", false); + + paths.erase(std::remove_if(paths.begin(), paths.end(), [](const bf::path& path) { + return !bf::is_regular_file(path); + }), paths.end()); + + return paths; + } + + std::vector AppDir::deployedDesktopFiles() { + std::vector desktopFiles; + + auto paths = listFilesInDirectory(path() / "usr/share/applications/", false); + paths.erase(std::remove_if(paths.begin(), paths.end(), [](const bf::path& path) { + return path.extension() != ".desktop"; + }), paths.end()); + + for (const auto& path : paths) { + desktopFiles.push_back(desktopfile::DesktopFile(path)); + } + + return desktopFiles; + } + + bool AppDir::createLinksInAppDirRoot(const desktopfile::DesktopFile& desktopFile) { + ldLog() << "Deploying desktop file to AppDir root:" << desktopFile.path() << std::endl; + + // copy desktop file to root directory + if (!d->symlinkFile(desktopFile.path(), path())) { + ldLog() << LD_ERROR << "Failed to create link to desktop file in AppDir root:" << desktopFile.path() << std::endl; + return false; + } + + // look for suitable icon + std::string iconName; + + if (!desktopFile.getEntry("Desktop Entry", "Icon", iconName)) { + ldLog() << LD_ERROR << "Icon entry missing in desktop file:" << desktopFile.path() << std::endl; + return false; + } + + const auto foundIconPaths = deployedIconPaths(); + + if (foundIconPaths.empty()) { + ldLog() << LD_ERROR << "Could not find suitable executable for Exec entry:" << iconName << std::endl; + return false; + } + + for (const auto& iconPath : foundIconPaths) { + ldLog() << LD_DEBUG << "Icon found:" << iconPath << std::endl; + + if (iconPath.stem() == iconName) { + ldLog() << "Deploying icon to AppDir root:" << iconPath << std::endl; + + if (!d->symlinkFile(iconPath, path())) { + ldLog() << LD_ERROR << "Failed to create symlink for icon in AppDir root:" << iconPath << std::endl; + return false; + } + } + } + + // look for suitable binary to create AppRun symlink + std::string executableName; + + if (!desktopFile.getEntry("Desktop Entry", "Exec", executableName)) { + ldLog() << LD_ERROR << "Exec entry missing in desktop file:" << desktopFile.path() << std::endl; + return false; + } + + const auto foundExecutablePaths = deployedExecutablePaths(); + + if (foundExecutablePaths.empty()) { + ldLog() << LD_ERROR << "Could not find suitable executable for Exec entry:" << iconName << std::endl; + return false; + } + + for (const auto& executablePath : foundExecutablePaths) { + ldLog() << LD_DEBUG << "Executable found:" << executablePath << std::endl; + + if (executablePath.stem() == iconName) { + ldLog() << "Deploying AppRun symlink for executable in AppDir root:" << executablePath << std::endl; + + if (!d->symlinkFile(executablePath, path() / "AppRun")) { + ldLog() << LD_ERROR << "Failed to create AppRun symlink for executable in AppDir root:" << executablePath << std::endl; + return false; + } + } + } + + return true; + } + } + } +} diff --git a/src/core/desktopfile.cpp b/src/core/desktopfile.cpp new file mode 100644 index 0000000..d4ef4a9 --- /dev/null +++ b/src/core/desktopfile.cpp @@ -0,0 +1,130 @@ +// library headers +#include + +// local headers +#include "linuxdeploy/core/desktopfile.h" +#include "linuxdeploy/core/log.h" + +using namespace linuxdeploy::core; +using namespace linuxdeploy::core::log; + +namespace bf = boost::filesystem; + +namespace linuxdeploy { + namespace core { + namespace desktopfile { + class DesktopFile::PrivateData { + public: + bf::path path; + INI<> ini; + + public: + PrivateData() : path(), ini("", false) {}; + }; + + DesktopFile::DesktopFile() { + d = new PrivateData(); + } + + DesktopFile::DesktopFile(const bf::path& path) : DesktopFile() { + if (!read(path)) + throw std::runtime_error("Failed to read desktop file"); + }; + + bool DesktopFile::read(const boost::filesystem::path& path) { + setPath(path); + + clear(); + + // nothing to do + if (!bf::exists(path)) + return true; + + std::ifstream ifs(path.string()); + if (!ifs) + return false; + + d->ini.parse(ifs); + return true; + } + + boost::filesystem::path DesktopFile::path() const { + return d->path; + } + + void DesktopFile::setPath(const boost::filesystem::path& path) { + d->path = path; + } + + void DesktopFile::clear() { + d->ini.clear(); + } + + bool DesktopFile::save() const { + return save(d->path); + } + + bool DesktopFile::save(const boost::filesystem::path& path) const { + return d->ini.save(path.string()); + } + + bool DesktopFile::entryExists(const std::string& section, const std::string& key) const { + if (!d->ini.select(section)) + return false; + + std::string absolutelyUnlikeValue = "<>!§$%&/()=?+'#-.,_:;'*¹²³½¬{[]}^°|"; + + auto value = d->ini.get(section, key, absolutelyUnlikeValue); + + return value != absolutelyUnlikeValue; + } + + bool DesktopFile::setEntry(const std::string& section, const std::string& key, const std::string& value) { + // check if value exists -- used for return value + auto rv = entryExists(section, key); + + // set key + d->ini[section][key] = value; + + return rv; + } + + bool DesktopFile::getEntry(const std::string& section, const std::string& key, std::string& value) const { + if (!entryExists(section, key)) + return false; + + if (!d->ini.select(section)) + return false; + + value = d->ini.get(key); + return true; + } + + bool DesktopFile::addDefaultKeys(const std::string& executableFileName) { + auto rv = true; + + auto setDefault = [&rv, this](const std::string& section, const std::string& key, const std::string& value) { + if (setEntry(section, key, value)) { + rv = false; + } + }; + + setDefault("Desktop Entry", "Name", executableFileName); + setDefault("Desktop Entry", "Exec", executableFileName); + setDefault("Desktop Entry", "Icon", executableFileName); + setDefault("Desktop Entry", "Type", "Application"); + setDefault("Desktop Entry", "Categories", "Utility;"); + + return rv; + } + + bool DesktopFile::validate() const { + // FIXME: call desktop-file-validate + return true; + } + } + } +} + + + diff --git a/src/core/elf.cpp b/src/core/elf.cpp new file mode 100644 index 0000000..545096c --- /dev/null +++ b/src/core/elf.cpp @@ -0,0 +1,124 @@ +// library includes +#include +#include + +// local headers +#include "linuxdeploy/core/elf.h" +#include "linuxdeploy/core/log.h" +#include "linuxdeploy/core/util.h" + +using namespace linuxdeploy::core::log; + +namespace bf = boost::filesystem; + +namespace linuxdeploy { + namespace core { + namespace elf { + class ElfFile::PrivateData { + public: + const bf::path path; + + public: + explicit PrivateData(const bf::path& path) : path(path) {} + }; + + ElfFile::ElfFile(const boost::filesystem::path& path) { + d = new PrivateData(path); + } + + ElfFile::~ElfFile() { + delete d; + } + + std::vector ElfFile::traceDynamicDependencies() { + // this method's purpose is to abstract this process + // the caller doesn't care _how_ it's done, after all + + // for now, we use the same ldd based method linuxdeployqt uses + + std::vector paths; + + subprocess::Popen lddProc( + {"ldd", d->path.string().c_str()}, + subprocess::output{subprocess::PIPE}, + subprocess::error{subprocess::PIPE} + ); + + auto lddOutput = lddProc.communicate(); + auto& lddStdout = lddOutput.first; + auto& lddStderr = lddOutput.second; + + if (lddProc.retcode() != 0) { + ldLog() << LD_ERROR << "Call to ldd failed:" << std::endl << lddStderr.buf.data() << std::endl; + return {}; + } + + std::string lddStdoutContents(lddStdout.buf.data()); + + const boost::regex expr(R"(\s*(.+)\s+\=>\s+(.+)\s+\((.+)\)\s*)"); + boost::smatch what; + + for (const auto& line : util::splitLines(lddStdoutContents)) { + if (boost::regex_search(line, what, expr)) { + auto libraryPath = what[2].str(); + util::trim(libraryPath); + paths.push_back(bf::absolute(libraryPath)); + } else { + ldLog() << LD_DEBUG << "Invalid ldd output: " << line << std::endl; + } + } + + return paths; + } + + std::string ElfFile::getRPath() { + subprocess::Popen patchelfProc( + {"patchelf", "--print-rpath", d->path.c_str()}, + subprocess::output(subprocess::PIPE), + subprocess::error(subprocess::PIPE) + ); + + auto patchelfOutput = patchelfProc.communicate(); + auto& patchelfStdout = patchelfOutput.first; + auto& patchelfStderr = patchelfOutput.second; + + if (patchelfProc.retcode() != 0) { + std::string errStr(patchelfStderr.buf.data()); + + // if file is not an ELF executable, there is no need for a detailed error message + if (patchelfProc.retcode() == 1 && errStr.find("not an ELF executable")) { + return ""; + } else { + ldLog() << LD_ERROR << "Call to patchelf failed:" << std::endl << errStr; + return ""; + } + } + + std::string retval = patchelfStdout.buf.data(); + util::trim(retval, '\n'); + util::trim(retval); + + return retval; + } + + bool ElfFile::setRPath(const std::string& value) { + subprocess::Popen patchelfProc( + {"patchelf", "--set-rpath", value.c_str(), d->path.c_str()}, + subprocess::output(subprocess::PIPE), + subprocess::error(subprocess::PIPE) + ); + + auto patchelfOutput = patchelfProc.communicate(); + auto& patchelfStdout = patchelfOutput.first; + auto& patchelfStderr = patchelfOutput.second; + + if (patchelfProc.retcode() != 0) { + ldLog() << LD_ERROR << "Call to patchelf failed:" << std::endl << patchelfStderr.buf; + return false; + } + + return true; + } + } + } +} diff --git a/src/core/generate-excludelist.sh b/src/core/generate-excludelist.sh new file mode 100644 index 0000000..ccc8b39 --- /dev/null +++ b/src/core/generate-excludelist.sh @@ -0,0 +1,51 @@ +#!/bin/bash + +# Copyright 2018 Alexander Gottwald (https://github.com/ago1024) +# Copyright 2018 TheAssassin (https://github.com/TheAssassin) +# +# Dual-licensed under the terms of the GPLv3 and LGPL v3 licenses as part of +# linuxdeployqt (https://github.com/probonopd/linuxdeployqt). +# +# Changed to use C++ standard library containers instead of Qt ones. + +set -e + +# download excludelist +blacklisted=($(wget --quiet https://raw.githubusercontent.com/probonopd/AppImages/master/excludelist -O - | sort | uniq | grep -v "^#.*" | grep "[^-\s]")) + +# sanity check +if [ "$blacklisted" == "" ]; then + exit 1; +fi + +filename=excludelist.h + +# overwrite existing source file +cat > "$filename" < +#include + +static const std::vector generatedExcludelist = { +EOF + +# Create array +for item in ${blacklisted[@]:0:${#blacklisted[@]}-1}; do + echo -e ' "'"$item"'",' >> "$filename" +done +echo -e ' "'"${blacklisted[$((${#blacklisted[@]}-1))]}"'"' >> "$filename" + +echo "};" >> "$filename" diff --git a/src/core/log.cpp b/src/core/log.cpp new file mode 100644 index 0000000..59112cd --- /dev/null +++ b/src/core/log.cpp @@ -0,0 +1,119 @@ +// local includes +#include "linuxdeploy/core/log.h" + +namespace linuxdeploy { + namespace core { + namespace log { + LD_LOGLEVEL ldLog::verbosity = LD_INFO; + + void ldLog::setVerbosity(LD_LOGLEVEL verbosity) { + ldLog::verbosity = verbosity; + } + + ldLog::ldLog() { + prependSpace = false; + currentLogLevel = LD_INFO; + logLevelSet = false; + }; + + ldLog::ldLog(bool prependSpace, bool logLevelSet, LD_LOGLEVEL logLevel) { + this->prependSpace = prependSpace; + this->currentLogLevel = logLevel; + this->logLevelSet = logLevelSet; + } + + void ldLog::checkPrependSpace() { + if (prependSpace) { + stream << " "; + prependSpace = false; + } + } + + bool ldLog::checkVerbosity() { +// std::cerr << "current: " << currentLogLevel << " verbosity: " << verbosity << std::endl; + return (currentLogLevel >= verbosity); + } + + ldLog ldLog::operator<<(const std::string& message) { + if (checkVerbosity()) { + checkPrependSpace(); + stream << message; + } + + return ldLog(true, logLevelSet, currentLogLevel); + } + ldLog ldLog::operator<<(const char* message) { + if (checkVerbosity()) { + checkPrependSpace(); + stream << message; + } + + return ldLog(true, logLevelSet, currentLogLevel); + } + + ldLog ldLog::operator<<(const boost::filesystem::path& path) { + if (checkVerbosity()) { + checkPrependSpace(); + stream << path.string(); + } + + return ldLog(true, logLevelSet, currentLogLevel); + } + + ldLog ldLog::operator<<(const double val) { + return ldLog::operator<<(std::to_string(val)); + } + + ldLog ldLog::operator<<(stdEndlType strm) { + if (checkVerbosity()) { + checkPrependSpace(); + stream << strm; + } + + return ldLog(false, logLevelSet, currentLogLevel); + } + + ldLog ldLog::operator<<(const LD_LOGLEVEL logLevel) { + if (logLevelSet) { + throw std::runtime_error( + "log level must be first element passed via the stream insertion operator"); + } + + logLevelSet = true; + currentLogLevel = logLevel; + + if (checkVerbosity()) { + switch (logLevel) { + case LD_DEBUG: + stream << "DEBUG: "; + break; + case LD_WARNING: + stream << "WARNING: "; + break; + case LD_ERROR: + stream << "ERROR: "; + break; + default: + break; + } + } + + return ldLog(false, logLevelSet, currentLogLevel); + } + + ldLog ldLog::operator<<(const LD_STREAM_CONTROL streamControl) { + bool prependSpace = true; + + switch (streamControl) { + case LD_NO_SPACE: + prependSpace = false; + break; + default: + break; + } + + return ldLog(prependSpace, logLevelSet, currentLogLevel); + } + } + } +} diff --git a/src/core/main.cpp b/src/core/main.cpp new file mode 100644 index 0000000..d6fff91 --- /dev/null +++ b/src/core/main.cpp @@ -0,0 +1,204 @@ +// system headers +#include +#include + +// library headers +#include + +// local headers +#include "linuxdeploy/core/appdir.h" +#include "linuxdeploy/core/desktopfile.h" +#include "linuxdeploy/core/elf.h" +#include "linuxdeploy/core/log.h" + +using namespace linuxdeploy::core; +using namespace linuxdeploy::core::log; + +namespace bf = boost::filesystem; + +int main(int argc, char** argv) { + args::ArgumentParser parser( + "linuxdeploy -- create AppDir bundles with ease" + ); + + args::HelpFlag help(parser, "help", "Display this help text.", {'h', "help"}); + args::Flag showVersion(parser, "", "Print version and exit", {'V', "version"}); + args::ValueFlag verbosity(parser, "verbosity", "Verbosity of log output (0 = debug, 1 = info, 2 = warning, 3 = error)", {'v', "verbosity"}); + + args::Flag initAppDir(parser, "", "Create basic AppDir structure", {"init-appdir"}); + args::ValueFlag appDirPath(parser, "appdir", "Path to target AppDir", {"appdir"}); + args::ValueFlag appName(parser, "app-name", "Application name (used to initialize desktop file and name icons etc.)", {'n', "app-name"}); + + args::ValueFlagList sharedLibraryPaths(parser, "library", "Shared library to deploy", {'l', "lib", "library"}); + + args::ValueFlagList executablePaths(parser, "executable", "Executable to deploy", {'e', "executable"}); + + args::ValueFlagList desktopFilePaths(parser, "desktop file", "Desktop file to deploy", {'d', "desktop-file"}); + args::Flag createDesktopFile(parser, "", "Create basic desktop file that is good enough for some tests", {"create-desktop-file"}); + + args::ValueFlagList iconPaths(parser, "icon file", "Icon to deploy", {'i', "icon-file"}); + + try { + parser.ParseCLI(argc, argv); + } catch (args::Help&) { + std::cerr << parser; + return 0; + } catch (args::ParseError& e) { + std::cerr << e.what() << std::endl; + std::cerr << parser; + return 1; + } + + // always show version statement + std::cerr << "linuxdeploy version " << LINUXDEPLOY_VERSION << std::endl; + + // set verbosity + if (verbosity) { + ldLog::setVerbosity((LD_LOGLEVEL) verbosity.Get()); + } + + if (showVersion) + return 0; + + if (!appDirPath) { + std::cerr << "--appdir parameter required" << std::endl; + return 1; + } + + appdir::AppDir appDir(appDirPath.Get()); + + if (appName) { + ldLog() << std::endl << "-- Deploying application \"" << LD_NO_SPACE << appName.Get() << LD_NO_SPACE << "\" --" << std::endl; + } + + // initialize AppDir with common directories on request + if (initAppDir) { + ldLog() << std::endl << "-- Creating basic AppDir structure --" << std::endl; + + if (!appDir.createBasicStructure()) + return 1; + } + + // deploy shared libraries to usr/lib, and deploy their dependencies to usr/lib + if (sharedLibraryPaths) { + ldLog() << std::endl << "-- Deploying shared libraries --" << std::endl; + + for (const auto& libraryPath : sharedLibraryPaths.Get()) { + if (!bf::exists(libraryPath)) { + std::cerr << "No such file or directory: " << libraryPath << std::endl; + return 1; + } + + if (!appDir.deployLibrary(libraryPath)) { + std::cerr << "Failed to deploy library: " << libraryPath << std::endl; + return 1; + } + } + } + + // deploy executables to usr/bin, and deploy their dependencies to usr/lib + if (executablePaths) { + ldLog() << std::endl << "-- Deploying executables --" << std::endl; + + for (const auto& executablePath : executablePaths.Get()) { + if (!bf::exists(executablePath)) { + std::cerr << "No such file or directory: " << executablePath << std::endl; + return 1; + } + + if (!appDir.deployExecutable(executablePath)) { + std::cerr << "Failed to deploy executable: " << executablePath << std::endl; + return 1; + } + } + } + + if (iconPaths) { + ldLog() << std::endl << "-- Deploying icons --" << std::endl; + + for (const auto& iconPath : iconPaths.Get()) { + if (!bf::exists(iconPath)) { + std::cerr << "No such file or directory: " << iconPath << std::endl; + return 1; + } + + if (!appDir.deployIcon(iconPath)) { + std::cerr << "Failed to deploy desktop file: " << iconPath << std::endl; + return 1; + } + } + } + + if (desktopFilePaths) { + ldLog() << std::endl << "-- Deploying desktop files --" << std::endl; + + for (const auto& desktopFilePath : desktopFilePaths.Get()) { + if (!bf::exists(desktopFilePath)) { + std::cerr << "No such file or directory: " << desktopFilePath << std::endl; + return 1; + } + + desktopfile::DesktopFile desktopFile(desktopFilePath); + + if (!appDir.deployDesktopFile(desktopFile)) { + std::cerr << "Failed to deploy desktop file: " << desktopFilePath << std::endl; + return 1; + } + } + } + + // perform deferred copy operations before creating other files here or trying to copy the files to the AppDir root + ldLog() << std::endl << "-- Copying files into AppDir --" << std::endl; + if (!appDir.executeDeferredOperations()) { + return 1; + } + + if (createDesktopFile) { + if (!executablePaths) { + ldLog() << LD_ERROR << "--create-desktop-file requires at least one executable to be passed" << std::endl; + return 1; + } + + ldLog() << std::endl << "-- Creating desktop file --" << std::endl; + + auto executableName = bf::path(executablePaths.Get().front()).filename().string(); + + auto desktopFilePath = appDir.path() / "usr/share/applications" / (executableName + ".desktop"); + + if (bf::exists(desktopFilePath)) { + ldLog() << LD_WARNING << "Working on existing desktop file:" << desktopFilePath << std::endl; + } else { + ldLog() << "Creating new desktop file:" << desktopFilePath << std::endl; + } + + desktopfile::DesktopFile desktopFile(desktopFilePath); + if (!desktopFile.addDefaultKeys(executableName)) { + ldLog() << LD_WARNING << "Tried to overwrite existing entries in desktop file" << std::endl; + } + + if (!desktopFile.save()) { + ldLog() << LD_ERROR << "Failed to save desktop file:" << desktopFilePath << std::endl; + return 1; + } + } + + // search for desktop file and deploy it to AppDir root + { + ldLog() << std::endl << "-- Deploying files into AppDir root directory --" << std::endl; + + auto deployedDesktopFiles = appDir.deployedDesktopFiles(); + + if (deployedDesktopFiles.empty()) { + ldLog() << LD_WARNING << "Could not find desktop file in AppDir, cannot create links for AppRun, desktop file and icon in AppDir root" << std::endl; + } else { + auto& desktopFile = deployedDesktopFiles[0]; + + ldLog() << "Deploying desktop file:" << desktopFile.path(); + + if (!appDir.createLinksInAppDirRoot(desktopFile)) + return 1; + } + } + + return 0; +}