diff --git a/include/linuxdeploy/core/appdir.h b/include/linuxdeploy/core/appdir.h index 290d6ccd..804e5286 100644 --- a/include/linuxdeploy/core/appdir.h +++ b/include/linuxdeploy/core/appdir.h @@ -39,7 +39,7 @@ namespace linuxdeploy { explicit AppDir(const std::string& path); // creates basic directory structure of an AppDir in "FHS" mode - bool createBasicStructure(); + bool createBasicStructure() const; // deploy shared library // @@ -66,39 +66,42 @@ namespace linuxdeploy { // deploy arbitrary file boost::filesystem::path deployFile(const boost::filesystem::path& from, const boost::filesystem::path& to); + // copy arbitrary file (immediately) + bool copyFile(const boost::filesystem::path& from, const boost::filesystem::path& to, bool overwrite = false) const; + // create an relative symlink to at . - bool createRelativeSymlink(const boost::filesystem::path& target, const boost::filesystem::path& symlink); + bool createRelativeSymlink(const boost::filesystem::path& target, const boost::filesystem::path& symlink) const; // execute deferred copy operations bool executeDeferredOperations(); // return path to AppDir - boost::filesystem::path path(); + boost::filesystem::path path() const; // create a list of all icon paths in the AppDir - std::vector deployedIconPaths(); + std::vector deployedIconPaths() const; // create a list of all executable paths in the AppDir - std::vector deployedExecutablePaths(); + std::vector deployedExecutablePaths() const; // create a list of all desktop file paths in the AppDir - std::vector deployedDesktopFiles(); + std::vector deployedDesktopFiles() const; // create symlinks for AppRun, desktop file and icon in the AppDir root directory - bool createLinksInAppDirRoot(const desktopfile::DesktopFile& desktopFile, boost::filesystem::path customAppRunPath = ""); + bool setUpAppDirRoot(const desktopfile::DesktopFile& desktopFile, boost::filesystem::path customAppRunPath = ""); // list all executables in /usr/bin // this function does not perform a recursive search, but only searches the bin directory - std::vector listExecutables(); + std::vector listExecutables() const; // list all shared libraries in /usr/lib // this function recursively searches the entire lib directory for shared libraries - std::vector listSharedLibraries(); + std::vector listSharedLibraries() const; // search for executables and libraries and deploy their dependencies // calling this function can turn sure file trees created by make install commands into working // AppDirs - bool deployDependenciesForExistingFiles(); + bool deployDependenciesForExistingFiles() const; // disable deployment of copyright files for this instance void setDisableCopyrightFilesDeployment(bool disable); diff --git a/src/core.cpp b/src/core.cpp index 920f3f59..406b3964 100644 --- a/src/core.cpp +++ b/src/core.cpp @@ -79,8 +79,8 @@ namespace linuxdeploy { try { desktopfile::DesktopFile desktopFile = getMainDesktopFile(desktopFilePaths, deployedDesktopFiles); - ldLog() << "Deploying desktop file:" << desktopFile.path() << std::endl; - return appDir.createLinksInAppDirRoot(desktopFile, customAppRunPath); + ldLog() << "Deploying files to AppDir root using desktop file:" << desktopFile.path() << std::endl; + return appDir.setUpAppDirRoot(desktopFile, customAppRunPath); } catch (const DeployError& er) { return false; } diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 137443b4..5a291a9d 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -19,7 +19,7 @@ target_link_libraries(linuxdeploy_core_log PUBLIC ${BOOST_LIBS}) add_subdirectory(copyright) -add_library(linuxdeploy_core STATIC elf.cpp appdir.cpp ${HEADERS}) +add_library(linuxdeploy_core STATIC elf.cpp appdir.cpp ${HEADERS} appdir_root_setup.cpp) target_link_libraries(linuxdeploy_core PUBLIC linuxdeploy_plugin linuxdeploy_core_log linuxdeploy_util linuxdeploy_desktopfile_static ${BOOST_LIBS} CImg ${CMAKE_THREAD_LIBS_INIT} diff --git a/src/core/appdir.cpp b/src/core/appdir.cpp index 73f17cbc..6d6fdc18 100644 --- a/src/core/appdir.cpp +++ b/src/core/appdir.cpp @@ -20,6 +20,7 @@ // auto-generated headers #include "excludelist.h" +#include "appdir_root_setup.h" using namespace linuxdeploy::core; using namespace linuxdeploy::desktopfile; @@ -81,7 +82,7 @@ namespace linuxdeploy { // actually copy file // mimics cp command behavior - bool copyFile(const bf::path& from, bf::path to, bool overwrite = false) { + static bool copyFile(const bf::path& from, bf::path to, bool overwrite = false) { ldLog() << "Copying file" << from << "to" << to << std::endl; try { @@ -559,7 +560,7 @@ namespace linuxdeploy { return true; } - bool isInDebugSymbolsLocation(const bf::path& path) { + static bool isInDebugSymbolsLocation(const bf::path& path) { // TODO: check if there's more potential locations for debug symbol files for (const std::string& dbgSymbolsPrefix : {".debug/"}) { if (path.string().substr(0, dbgSymbolsPrefix.size()) == dbgSymbolsPrefix) @@ -578,7 +579,7 @@ namespace linuxdeploy { AppDir::AppDir(const std::string& path) : AppDir(bf::path(path)) {} - bool AppDir::createBasicStructure() { + bool AppDir::createBasicStructure() const { std::vector dirPaths = { "usr/bin/", "usr/lib/", @@ -635,7 +636,7 @@ namespace linuxdeploy { return d->executeDeferredOperations(); } - boost::filesystem::path AppDir::path() { + boost::filesystem::path AppDir::path() const { return d->appDirPath; } @@ -665,7 +666,7 @@ namespace linuxdeploy { return foundPaths; } - std::vector AppDir::deployedIconPaths() { + std::vector AppDir::deployedIconPaths() const { auto icons = listFilesInDirectory(path() / "usr/share/icons/"); auto pixmaps = listFilesInDirectory(path() / "usr/share/pixmaps/", false); icons.reserve(pixmaps.size()); @@ -673,11 +674,11 @@ namespace linuxdeploy { return icons; } - std::vector AppDir::deployedExecutablePaths() { + std::vector AppDir::deployedExecutablePaths() const { return listFilesInDirectory(path() / "usr/bin/", false); } - std::vector AppDir::deployedDesktopFiles() { + std::vector AppDir::deployedDesktopFiles() const { std::vector desktopFiles; auto paths = listFilesInDirectory(path() / "usr/share/applications/", false); @@ -692,131 +693,24 @@ namespace linuxdeploy { return desktopFiles; } - bool AppDir::createLinksInAppDirRoot(const DesktopFile& desktopFile, boost::filesystem::path customAppRunPath) { - 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 - DesktopFileEntry iconEntry; - - if (!desktopFile.getEntry("Desktop Entry", "Icon", iconEntry)) { - ldLog() << LD_ERROR << "Icon entry missing in desktop file:" << desktopFile.path() << std::endl; - return false; - } - - bool iconDeployed = false; - - const auto foundIconPaths = deployedIconPaths(); - - if (foundIconPaths.empty()) { - ldLog() << LD_ERROR << "Could not find icon executable for Icon entry:" << iconEntry.value() << std::endl; - return false; - } - - for (const auto& iconPath : foundIconPaths) { - ldLog() << LD_DEBUG << "Icon found:" << iconPath << std::endl; - - const bool matchesFilenameWithExtension = iconPath.filename() == iconEntry.value(); - - if (iconPath.stem() == iconEntry.value() || matchesFilenameWithExtension) { - if (matchesFilenameWithExtension) { - ldLog() << LD_WARNING << "Icon= entry filename contains extension" << std::endl; - } - - 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; - } - - iconDeployed = true; - break; - } - } - - if (!iconDeployed) { - ldLog() << LD_ERROR << "Could not find suitable icon for Icon entry:" << iconEntry.value() << std::endl; - return false; - } - - if (!customAppRunPath.empty()) { - // copy custom AppRun executable - // FIXME: make sure this file is executable - ldLog() << "Deploying custom AppRun:" << customAppRunPath; - - if (!d->copyFile(customAppRunPath, path() / "AppRun")) - return false; - } else { - // check if there is a custom AppRun already - // in that case, skip deployment of symlink - if (bf::exists(path() / "AppRun")) { - ldLog() << LD_WARNING << "Custom AppRun detected, skipping deployment of symlink" << std::endl; - } else { - // look for suitable binary to create AppRun symlink - DesktopFileEntry executableEntry; - - if (!desktopFile.getEntry("Desktop Entry", "Exec", executableEntry)) { - ldLog() << LD_ERROR << "Exec entry missing in desktop file:" << desktopFile.path() - << std::endl; - return false; - } - - auto executableName = util::split(executableEntry.value())[0]; - - const auto foundExecutablePaths = deployedExecutablePaths(); - - if (foundExecutablePaths.empty()) { - ldLog() << LD_ERROR << "Could not find suitable executable for Exec entry:" << executableName - << std::endl; - return false; - } - - bool deployedExecutable = false; - - for (const auto& executablePath : foundExecutablePaths) { - ldLog() << LD_DEBUG << "Executable found:" << executablePath << std::endl; - - if (executablePath.filename() == executableName) { - 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; - } - - deployedExecutable = true; - break; - } - } - - if (!deployedExecutable) { - ldLog() << LD_ERROR << "Could not deploy symlink for executable: could not find suitable executable for Exec entry:" << executableName << std::endl; - return false; - } - } - } - - return true; + bool AppDir::setUpAppDirRoot(const DesktopFile& desktopFile, boost::filesystem::path customAppRunPath) { + AppDirRootSetup setup(*this); + setup.run(desktopFile, customAppRunPath); } bf::path AppDir::deployFile(const boost::filesystem::path& from, const boost::filesystem::path& to) { return d->deployFile(from, to, true); } - bool AppDir::createRelativeSymlink(const bf::path& target, const bf::path& symlink) { + bool AppDir::copyFile(const bf::path& from, const bf::path& to, bool overwrite) const { + return d->copyFile(from, to, overwrite); + } + + bool AppDir::createRelativeSymlink(const bf::path& target, const bf::path& symlink) const { return d->symlinkFile(target, symlink, true); } - std::vector AppDir::listExecutables() { + std::vector AppDir::listExecutables() const { std::vector executables; for (const auto& file : listFilesInDirectory(path() / "usr" / "bin", false)) { @@ -834,7 +728,7 @@ namespace linuxdeploy { return executables; } - std::vector AppDir::listSharedLibraries() { + std::vector AppDir::listSharedLibraries() const { std::vector sharedLibraries; for (const auto& file : listFilesInDirectory(path() / "usr" / "lib", true)) { @@ -856,7 +750,7 @@ namespace linuxdeploy { return sharedLibraries; } - bool AppDir::deployDependenciesForExistingFiles() { + bool AppDir::deployDependenciesForExistingFiles() const { for (const auto& executable : listExecutables()) { if (bf::is_symlink(executable)) continue; diff --git a/src/core/appdir_root_setup.cpp b/src/core/appdir_root_setup.cpp new file mode 100644 index 00000000..39b2e46d --- /dev/null +++ b/src/core/appdir_root_setup.cpp @@ -0,0 +1,272 @@ +// local headers +#include +#include +#include "appdir_root_setup.h" + + +namespace bf = boost::filesystem; + +namespace linuxdeploy { + using namespace desktopfile; + + namespace core { + using namespace appdir; + using namespace log; + + + class AppDirRootSetup::Private { + public: + static constexpr auto APPRUN_HOOKS_DIRNAME = "apprun-hooks"; + + public: + const AppDir& appDir; + + public: + explicit Private(const AppDir& appDir) : appDir(appDir) {} + + public: + bool deployDesktopFileAndIcon(const DesktopFile& desktopFile) const { + ldLog() << "Deploying desktop file to AppDir root:" << desktopFile.path() << std::endl; + + // copy desktop file to root directory + if (!appDir.createRelativeSymlink(desktopFile.path(), appDir.path())) { + ldLog() << LD_ERROR << "Failed to create link to desktop file in AppDir root:" << desktopFile.path() << std::endl; + return false; + } + + // look for suitable icon + DesktopFileEntry iconEntry; + + if (!desktopFile.getEntry("Desktop Entry", "Icon", iconEntry)) { + ldLog() << LD_ERROR << "Icon entry missing in desktop file:" << desktopFile.path() << std::endl; + return false; + } + + bool iconDeployed = false; + + const auto foundIconPaths = appDir.deployedIconPaths(); + + if (foundIconPaths.empty()) { + ldLog() << LD_ERROR << "Could not find icon executable for Icon entry:" << iconEntry.value() << std::endl; + return false; + } + + for (const auto& iconPath : foundIconPaths) { + ldLog() << LD_DEBUG << "Icon found:" << iconPath << std::endl; + + const bool matchesFilenameWithExtension = iconPath.filename() == iconEntry.value(); + + if (iconPath.stem() == iconEntry.value() || matchesFilenameWithExtension) { + if (matchesFilenameWithExtension) { + ldLog() << LD_WARNING << "Icon= entry filename contains extension" << std::endl; + } + + ldLog() << "Deploying icon to AppDir root:" << iconPath << std::endl; + + if (!appDir.createRelativeSymlink(iconPath, appDir.path())) { + ldLog() << LD_ERROR << "Failed to create symlink for icon in AppDir root:" << iconPath << std::endl; + return false; + } + + iconDeployed = true; + break; + } + } + + if (!iconDeployed) { + ldLog() << LD_ERROR << "Could not find suitable icon for Icon entry:" << iconEntry.value() << std::endl; + return false; + } + } + + bool deployCustomAppRunFile(const bf::path& customAppRunPath) const { + // copy custom AppRun executable + // FIXME: make sure this file is executable + ldLog() << "Deploying custom AppRun:" << customAppRunPath; + + if (!appDir.copyFile(customAppRunPath, appDir.path() / "AppRun")) + return false; + } + + bool deployStandardAppRunFromDesktopFile(const DesktopFile& desktopFile, const bf::path& customAppRunPath) const { + // check if there is a custom AppRun already + // in that case, skip deployment of symlink + if (bf::exists(appDir.path() / "AppRun")) { + ldLog() << LD_WARNING << "Existing AppRun detected, skipping deployment of symlink" << std::endl; + } else { + // look for suitable binary to create AppRun symlink + DesktopFileEntry executableEntry; + + if (!desktopFile.getEntry("Desktop Entry", "Exec", executableEntry)) { + ldLog() << LD_ERROR << "Exec entry missing in desktop file:" << desktopFile.path() + << std::endl; + return false; + } + + auto executableName = util::split(executableEntry.value())[0]; + + const auto foundExecutablePaths = appDir.deployedExecutablePaths(); + + if (foundExecutablePaths.empty()) { + ldLog() << LD_ERROR << "Could not find suitable executable for Exec entry:" << executableName + << std::endl; + return false; + } + + bool deployedExecutable = false; + + for (const auto& executablePath : foundExecutablePaths) { + ldLog() << LD_DEBUG << "Executable found:" << executablePath << std::endl; + + if (executablePath.filename() == executableName) { + ldLog() << "Deploying AppRun symlink for executable in AppDir root:" << executablePath + << std::endl; + + if (!appDir.createRelativeSymlink(executablePath, appDir.path() / "AppRun")) { + ldLog() << LD_ERROR + << "Failed to create AppRun symlink for executable in AppDir root:" + << executablePath << std::endl; + return false; + } + + deployedExecutable = true; + break; + } + } + + if (!deployedExecutable) { + ldLog() << LD_ERROR << "Could not deploy symlink for executable: could not find suitable executable for Exec entry:" << executableName << std::endl; + return false; + } + } + } + + bool deployAppRunWrapperIfNecessary() const { + const bf::path appRunPath(appDir.path() / "AppRun"); + const bf::path wrappedAppRunPath(appRunPath.string() + ".wrapped"); + + const bf::path appRunHooksPath(appDir.path() / APPRUN_HOOKS_DIRNAME); + + // first, we check whether there's that special directory containing hooks + if (!bf::is_directory(appRunHooksPath)) { + ldLog() << LD_DEBUG << "Could not find apprun-hooks dir, no need to deploy the AppRun wrapper" << std::endl; + return true; + } + + // if there's no files in there we don't have to do anything + bf::directory_iterator firstRegularFile = std::find_if( + bf::directory_iterator(appRunHooksPath), + bf::directory_iterator{}, + [](const bf::path& p) { + return bf::is_regular_file(p); + } + ); + if (firstRegularFile == bf::directory_iterator{}) { + ldLog() << LD_WARNING << "Found an empty apprun-hooks directory, assuming there is no need to deploy the AppRun wrapper" << std::endl; + return true; + } + + // any file within that directory is considered to be a script + // we can't perform any validity checks, that would be way too much complexity and even tools which + // claim they can, like e.g., shellcheck, aren't perfect, they only aid in avoiding bugs but cannot + // prevent them completely + + // let's put together the wrapper script's contents + std::ostringstream oss; + + oss << "#! /usr/bin/env bash" << std::endl + << std::endl + << "# autogenerated by linuxdeploy" << std::endl + << std::endl + << "# make sure errors in sourced scripts will cause this script to stop" << std::endl + << "set -e" << std::endl + << std::endl + << "this_dir=$(readlink -f $(dirname \"$0\"))" << std::endl + << std::endl; + + std::for_each(bf::directory_iterator(appRunHooksPath), bf::directory_iterator{}, [&oss](const bf::path& p) { + if (!bf::is_regular_file(p)) + return; + + oss << "source \"$this_dir\"/" << APPRUN_HOOKS_DIRNAME << "/" << p.filename(); + }); + + oss << std::endl + << "exec \"$this_dir\"/AppRun.wrapped" << std::endl; + + // first we need to make sure we're not running this more than once + // this might cause more harm than good + // we require the user to clean up the mess at first + // FIXME: try to find a way how to rewrap AppRun on subsequent runs or, even better, become idempotent + if (bf::exists(wrappedAppRunPath)) { + ldLog() << LD_WARNING << "Already found wrapped AppRun, using existing file/symlink" << std::endl; + } else { + // backup original AppRun + bf::rename(appRunPath, wrappedAppRunPath); + } + + // in case the above check triggered a warning, it's possible that there is another AppRun in the AppDir + // this one has to be cleaned up in that case + if (bf::exists(appRunPath)) { + ldLog() << LD_WARNING << "Found an AppRun file/symlink, possibly due to re-run of linuxdeploy, " + "overwriting" << std::endl; + bf::remove(appRunPath); + } + + + // install new script + std::ofstream ofs(appRunPath.string()); + ofs << oss.str(); + + // make sure data is written to disk + ofs.flush(); + ofs.close(); + + // make new file executable + bf::permissions(appRunPath, + bf::perms::owner_all | bf::perms::group_read | bf::perms::others_read | + bf::perms::group_exe | bf::perms::others_exe + ); + + // we're done! + return true; + } + }; + + AppDirRootSetup::AppDirRootSetup(const AppDir& appDir) : d(new Private(appDir)) {} + + bool AppDirRootSetup::run(const DesktopFile& desktopFile, const bf::path& customAppRunPath) const { + // first step that is always required is to deploy the desktop file and the corresponding icon + if (!d->deployDesktopFileAndIcon(desktopFile)) + return false; + + // the algorithm depends on whether the user wishes to deploy their own AppRun file + // in case they do, the algorithm shall deploy that file + // otherwise, the standard algorithm shall be run which takes information from the desktop file to + // deploy a symlink pointing to the AppImage's main binary + // this allows power users to define their own AppImage initialization steps or run different binaries + // based on parameters etc. + if (!customAppRunPath.empty()) { + if (!d->deployCustomAppRunFile(customAppRunPath)) + return false; + } else { + if (!d->deployStandardAppRunFromDesktopFile(desktopFile, customAppRunPath)) + return false; + } + + // plugins might need to run some initializing code to make certain features work + // these involve setting environment variables because libraries or frameworks don't support any other + // way of pointing them to resources inside the AppDir instead of looking into config files in locations + // inside the AppImage, etc. + // the linuxdeploy plugin specification states that if plugins put files into a specified directory in + // the AppImage, linuxdeploy will make sure they're run before running the regular AppRun + if (!d->deployAppRunWrapperIfNecessary()) + return false; + + return true; + } + } +} + + + diff --git a/src/core/appdir_root_setup.h b/src/core/appdir_root_setup.h new file mode 100644 index 00000000..b2964b7b --- /dev/null +++ b/src/core/appdir_root_setup.h @@ -0,0 +1,36 @@ +#pragma once + +// system headers +#include + +// local headers +#include "linuxdeploy/core/appdir.h" + +namespace linuxdeploy { + namespace core { + /** + * Wrapper for an AppDir that encapsulates all functionality to set up the AppDir root directory. + */ + class AppDirRootSetup { + private: + // PImpl + class Private; + std::shared_ptr d; + + public: + explicit AppDirRootSetup(const appdir::AppDir& appdir); + + /** + * Deploy files to the AppDir root directory using the provided desktop file and the information within it. + * Optionally, a custom AppRun path can be provided which is deployed instead of following the internal + * default mechanism, which usually just places a symlink to the main binary as AppRun. + * + * @param desktopFile + * @param customAppRunPath + * @return + */ + bool run(const desktopfile::DesktopFile& desktopFile, const boost::filesystem::path& customAppRunPath = "") const; + }; + + } +}