Initial commit

This commit is contained in:
TheAssassin 2018-05-30 19:21:08 +02:00
commit e598536173
22 changed files with 1572 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
cmake-build-*/
*build*/
.idea/

9
.gitmodules vendored Normal file
View File

@ -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

13
CMakeLists.txt Normal file
View File

@ -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)

19
LICENSE.txt Normal file
View File

@ -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.

5
README.md Normal file
View File

@ -0,0 +1,5 @@
# linuxdeploy
AppDir creation and maintenance tool.
**More info will follow soon!**

View File

@ -0,0 +1,71 @@
// system includes
#include <string>
// library includes
#include <boost/filesystem.hpp>
// 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<boost::filesystem::path> deployedIconPaths();
// create a list of all executable paths in the AppDir
std::vector<boost::filesystem::path> deployedExecutablePaths();
// create a list of all desktop file paths in the AppDir
std::vector<desktopfile::DesktopFile> deployedDesktopFiles();
// create symlinks for AppRun, desktop file and icon in the AppDir root directory
bool createLinksInAppDirRoot(const desktopfile::DesktopFile& desktopFile);
};
}
}
}

View File

@ -0,0 +1,71 @@
// system includes
// library includes
#include <boost/filesystem.hpp>
#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;
};
}
}
}

View File

@ -0,0 +1,40 @@
// system includes
#include <vector>
#include <string>
// library includes
#include <boost/filesystem.hpp>
#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<boost::filesystem::path> 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);
};
}
}
}

View File

@ -0,0 +1,69 @@
// system includes
#include <iostream>
// library includes
#include <boost/filesystem.hpp>
#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<char, std::char_traits<char> > 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);
};
}
}
}

View File

@ -0,0 +1,57 @@
#pragma once
#include <sstream>
#include <string>
#include <vector>
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<std::string> split(const std::string& s, char delim = ' ') {
std::vector<std::string> result;
std::stringstream ss(s);
std::string item;
while (std::getline(ss, item, delim)) {
result.push_back(item);
}
return result;
}
static std::vector<std::string> 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;
}
}
}
}

15
lib/CMakeLists.txt Normal file
View File

@ -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)

1
lib/args Submodule

@ -0,0 +1 @@
Subproject commit bd0429e91f5bb140271870d5421e412bf78b9f31

@ -0,0 +1 @@
Subproject commit 2dff628f921047dbee4b1795f40875b06ae27b50

1
lib/cpp-subprocess Submodule

@ -0,0 +1 @@
Subproject commit 05c76a531180298a8404bdf5fccb71137c62cd76

4
src/CMakeLists.txt Normal file
View File

@ -0,0 +1,4 @@
# globally include own includes
include_directories(${PROJECT_SOURCE_DIR}/include)
add_subdirectory(core)

27
src/core/CMakeLists.txt Normal file
View File

@ -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")

538
src/core/appdir.cpp Normal file
View File

@ -0,0 +1,538 @@
// library headers
#include <boost/filesystem.hpp>
#include <Magick++.h>
#include <fnmatch.h>
#include <dirent.h>
#include <fts.h>
#include <subprocess.hpp>
// 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<bf::path, bf::path> copyOperations;
std::vector<bf::path> 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<std::string> 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<bf::path> listFilesInDirectory(const bf::path& path, const bool recursive = true) {
std::vector<bf::path> foundPaths;
std::vector<char> pathBuf(path.string().size() + 1, '\0');
strcpy(pathBuf.data(), path.string().c_str());
std::vector<char*> 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<bf::path> AppDir::deployedIconPaths() {
return listFilesInDirectory(path() / "/usr/share/icons/");
}
std::vector<bf::path> 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<desktopfile::DesktopFile> AppDir::deployedDesktopFiles() {
std::vector<desktopfile::DesktopFile> 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;
}
}
}
}

130
src/core/desktopfile.cpp Normal file
View File

@ -0,0 +1,130 @@
// library headers
#include <INI.h>
// 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<std::string, std::string, std::string>(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;
}
}
}
}

124
src/core/elf.cpp Normal file
View File

@ -0,0 +1,124 @@
// library includes
#include <boost/regex.hpp>
#include <subprocess.hpp>
// 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<bf::path> 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<bf::path> 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;
}
}
}
}

View File

@ -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" <<EOF
/*
* List of libraries to exclude for different reasons.
*
* Automatically generated from
* https://raw.githubusercontent.com/probonopd/AppImages/master/excludelist
*
* This file shall be committed by the developers occassionally,
* otherwise systems without access to the internet won't be able to build
* fully working versions of linuxdeployqt.
*
* See https://github.com/probonopd/linuxdeployqt/issues/274 for more
* information.
*/
#include <string>
#include <vector>
static const std::vector<std::string> 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"

119
src/core/log.cpp Normal file
View File

@ -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);
}
}
}
}

204
src/core/main.cpp Normal file
View File

@ -0,0 +1,204 @@
// system headers
#include <glob.h>
#include <iostream>
// library headers
#include <args.hxx>
// 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<int> 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<std::string> appDirPath(parser, "appdir", "Path to target AppDir", {"appdir"});
args::ValueFlag<std::string> appName(parser, "app-name", "Application name (used to initialize desktop file and name icons etc.)", {'n', "app-name"});
args::ValueFlagList<std::string> sharedLibraryPaths(parser, "library", "Shared library to deploy", {'l', "lib", "library"});
args::ValueFlagList<std::string> executablePaths(parser, "executable", "Executable to deploy", {'e', "executable"});
args::ValueFlagList<std::string> 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<std::string> 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;
}