Advance Wayland and KDE package bring-up
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
add_subdirectory(kpackage)
|
||||
add_subdirectory(kpackagetool)
|
||||
|
||||
ecm_qt_install_logging_categories(
|
||||
EXPORT KPACKAGE
|
||||
FILES kpackage.categories
|
||||
DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR}
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Invoke the extractrc script on all .ui, .rc, and .kcfg files in the sources.
|
||||
# The results are stored in a pseudo .cpp file to be picked up by xgettext.
|
||||
lst=`find . -name \*.rc -o -name \*.ui -o -name \*.kcfg`
|
||||
if [ -n "$lst" ] ; then
|
||||
$EXTRACTRC $lst >> rc.cpp
|
||||
fi
|
||||
|
||||
# Run xgettext to extract strings from all source files.
|
||||
$XGETTEXT `find . -name \*.cpp -o -name \*.h -o -name \*.qml` -o $podir/libkpackage6.pot
|
||||
@@ -0,0 +1,111 @@
|
||||
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/config-package.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-package.h)
|
||||
|
||||
add_library(KF6Package)
|
||||
add_library(KF6::Package ALIAS KF6Package)
|
||||
|
||||
set_target_properties(KF6Package PROPERTIES
|
||||
VERSION ${PACKAGE_VERSION}
|
||||
SOVERSION ${PACKAGE_SOVERSION}
|
||||
EXPORT_NAME Package
|
||||
)
|
||||
|
||||
target_sources(KF6Package PRIVATE
|
||||
package.cpp
|
||||
packagestructure.cpp
|
||||
packageloader.cpp
|
||||
packagejob.cpp
|
||||
private/packages.cpp
|
||||
private/packagejobthread.cpp
|
||||
)
|
||||
|
||||
ecm_qt_declare_logging_category(KF6Package
|
||||
HEADER kpackage_debug.h
|
||||
IDENTIFIER KPACKAGE_LOG
|
||||
CATEGORY_NAME kf.package
|
||||
OLD_CATEGORY_NAMES kf5.kpackage
|
||||
DESCRIPTION "kpackage (lib)"
|
||||
EXPORT KPACKAGE
|
||||
)
|
||||
|
||||
ecm_generate_export_header(KF6Package
|
||||
EXPORT_FILE_NAME kpackage/package_export.h
|
||||
BASE_NAME KPackage
|
||||
GROUP_BASE_NAME KF
|
||||
VERSION ${KF_VERSION}
|
||||
USE_VERSION_HEADER
|
||||
DEPRECATED_BASE_VERSION 0
|
||||
DEPRECATION_VERSIONS
|
||||
EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT}
|
||||
)
|
||||
|
||||
target_link_libraries(KF6Package
|
||||
PUBLIC
|
||||
KF6::CoreAddons
|
||||
PRIVATE
|
||||
KF6::Archive
|
||||
KF6::I18n
|
||||
)
|
||||
|
||||
if (HAVE_DBUS)
|
||||
target_link_libraries(KF6Package
|
||||
PRIVATE
|
||||
Qt::DBus # notification
|
||||
)
|
||||
target_compile_definitions(KF6Package PRIVATE -DHAVE_QTDBUS=1)
|
||||
else()
|
||||
target_compile_definitions(KF6Package PRIVATE -DHAVE_QTDBUS=0)
|
||||
endif()
|
||||
|
||||
target_include_directories(KF6Package PUBLIC
|
||||
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/..;${CMAKE_CURRENT_BINARY_DIR};${CMAKE_CURRENT_BINARY_DIR}/KPackage>"
|
||||
)
|
||||
target_include_directories(KF6Package INTERFACE
|
||||
"$<INSTALL_INTERFACE:${KDE_INSTALL_INCLUDEDIR_KF}/KPackage>"
|
||||
)
|
||||
|
||||
########### install files ###############
|
||||
ecm_generate_headers(Package_CamelCase_HEADERS
|
||||
HEADER_NAMES
|
||||
Package
|
||||
PackageStructure
|
||||
PackageLoader
|
||||
PackageJob
|
||||
packagestructure_compat_p
|
||||
REQUIRED_HEADERS Package_HEADERS
|
||||
PREFIX KPackage
|
||||
)
|
||||
|
||||
install(FILES
|
||||
${Package_HEADERS}
|
||||
${CMAKE_CURRENT_BINARY_DIR}/kpackage/package_export.h
|
||||
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KPackage/kpackage COMPONENT Devel)
|
||||
|
||||
install(FILES
|
||||
${Package_CamelCase_HEADERS}
|
||||
DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KPackage/KPackage COMPONENT Devel)
|
||||
|
||||
install(TARGETS KF6Package EXPORT KF6PackageTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
if (NOT BUILD_SHARED_LIBS)
|
||||
install(TARGETS kpackage_common_STATIC EXPORT KF6PackageTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
endif()
|
||||
|
||||
if(BUILD_QCH)
|
||||
ecm_add_qch(
|
||||
KF6Package_QCH
|
||||
NAME KPackage
|
||||
BASE_NAME KF6Package
|
||||
VERSION ${KF_VERSION}
|
||||
ORG_DOMAIN org.kde
|
||||
SOURCES # using only public headers, to cover only public API
|
||||
${Package_HEADERS}
|
||||
MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md"
|
||||
LINK_QCHS
|
||||
KF6CoreAddons_QCH
|
||||
BLANK_MACROS
|
||||
KPACKAGE_EXPORT
|
||||
"KPACKAGE_DEPRECATED_VERSION(x, y, t)"
|
||||
TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
|
||||
QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR}
|
||||
COMPONENT Devel
|
||||
)
|
||||
endif()
|
||||
@@ -0,0 +1,4 @@
|
||||
|
||||
#define KPACKAGE_RELATIVE_DATA_INSTALL_DIR "@KPACKAGE_RELATIVE_DATA_INSTALL_DIR@"
|
||||
|
||||
#define KDE_INSTALL_FULL_LIBEXECDIR_KF "@KDE_INSTALL_FULL_LIBEXECDIR_KF@"
|
||||
@@ -0,0 +1,903 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2010 Marco Martin <notmart@gmail.com>
|
||||
SPDX-FileCopyrightText: 2010 Kevin Ottens <ervin@kde.org>
|
||||
SPDX-FileCopyrightText: 2009 Rob Scheepmaker
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "package.h"
|
||||
|
||||
#include <QResource>
|
||||
#include <qtemporarydir.h>
|
||||
|
||||
#include "kpackage_debug.h"
|
||||
#include <KArchive>
|
||||
#include <KLocalizedString>
|
||||
#include <KTar>
|
||||
#include <kzip.h>
|
||||
|
||||
#include "config-package.h"
|
||||
|
||||
#include <QMimeDatabase>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include "packageloader.h"
|
||||
#include "packagestructure.h"
|
||||
#include "private/package_p.h"
|
||||
#include "private/packageloader_p.h"
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
Package::Package(PackageStructure *structure)
|
||||
: d(new PackagePrivate())
|
||||
{
|
||||
d->structure = structure;
|
||||
|
||||
if (d->structure) {
|
||||
addFileDefinition("metadata", QStringLiteral("metadata.json"));
|
||||
d->structure.data()->initPackage(this);
|
||||
}
|
||||
}
|
||||
|
||||
Package::Package(const Package &other)
|
||||
: d(other.d)
|
||||
{
|
||||
}
|
||||
|
||||
Package::~Package() = default;
|
||||
|
||||
Package &Package::operator=(const Package &rhs)
|
||||
{
|
||||
if (&rhs != this) {
|
||||
d = rhs.d;
|
||||
}
|
||||
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool Package::hasValidStructure() const
|
||||
{
|
||||
qWarning() << d->structure << requiredFiles();
|
||||
return d->structure;
|
||||
}
|
||||
|
||||
bool Package::isValid() const
|
||||
{
|
||||
if (!d->structure) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Minimal packages with no metadata *are* supposed to be possible
|
||||
// so if !metadata().isValid() go ahead
|
||||
if (metadata().isValid() && metadata().value(QStringLiteral("isHidden"), QStringLiteral("false")) == QLatin1String("true")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (d->checkedValid) {
|
||||
return d->valid;
|
||||
}
|
||||
|
||||
const QString rootPath = d->tempRoot.isEmpty() ? d->path : d->tempRoot;
|
||||
if (rootPath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
d->valid = true;
|
||||
|
||||
// search for the file in all prefixes and in all possible paths for each prefix
|
||||
// even if it's a big nested loop, usually there is one prefix and one location
|
||||
// so shouldn't cause too much disk access
|
||||
for (auto it = d->contents.cbegin(), end = d->contents.cend(); it != end; ++it) {
|
||||
if (it.value().required && filePath(it.key()).isEmpty()) {
|
||||
qCWarning(KPACKAGE_LOG) << "Could not find required" << (it.value().directory ? "directory" : "file") << it.key() << "for package" << path()
|
||||
<< "should be" << it.value().paths;
|
||||
d->valid = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return d->valid;
|
||||
}
|
||||
|
||||
bool Package::isRequired(const QByteArray &key) const
|
||||
{
|
||||
auto it = d->contents.constFind(key);
|
||||
if (it == d->contents.constEnd()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return it.value().required;
|
||||
}
|
||||
|
||||
QStringList Package::mimeTypes(const QByteArray &key) const
|
||||
{
|
||||
auto it = d->contents.constFind(key);
|
||||
if (it == d->contents.constEnd()) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
if (it.value().mimeTypes.isEmpty()) {
|
||||
return d->mimeTypes;
|
||||
}
|
||||
|
||||
return it.value().mimeTypes;
|
||||
}
|
||||
|
||||
QString Package::defaultPackageRoot() const
|
||||
{
|
||||
return d->defaultPackageRoot;
|
||||
}
|
||||
|
||||
void Package::setDefaultPackageRoot(const QString &packageRoot)
|
||||
{
|
||||
d.detach();
|
||||
d->defaultPackageRoot = packageRoot;
|
||||
if (!d->defaultPackageRoot.isEmpty() && !d->defaultPackageRoot.endsWith(QLatin1Char('/'))) {
|
||||
d->defaultPackageRoot.append(QLatin1Char('/'));
|
||||
}
|
||||
}
|
||||
|
||||
void Package::setFallbackPackage(const KPackage::Package &package)
|
||||
{
|
||||
if ((d->fallbackPackage && d->fallbackPackage->path() == package.path() && d->fallbackPackage->metadata() == package.metadata()) ||
|
||||
// can't be fallback of itself
|
||||
(package.path() == path() && package.metadata() == metadata()) || d->hasCycle(package)) {
|
||||
return;
|
||||
}
|
||||
|
||||
d->fallbackPackage = std::make_unique<Package>(package);
|
||||
}
|
||||
|
||||
KPackage::Package Package::fallbackPackage() const
|
||||
{
|
||||
if (d->fallbackPackage) {
|
||||
return (*d->fallbackPackage);
|
||||
} else {
|
||||
return Package();
|
||||
}
|
||||
}
|
||||
|
||||
bool Package::allowExternalPaths() const
|
||||
{
|
||||
return d->externalPaths;
|
||||
}
|
||||
|
||||
void Package::setMetadata(const KPluginMetaData &data)
|
||||
{
|
||||
Q_ASSERT(data.isValid());
|
||||
d->metadata = data;
|
||||
}
|
||||
|
||||
void Package::setAllowExternalPaths(bool allow)
|
||||
{
|
||||
d.detach();
|
||||
d->externalPaths = allow;
|
||||
}
|
||||
|
||||
KPluginMetaData Package::metadata() const
|
||||
{
|
||||
// qCDebug(KPACKAGE_LOG) << "metadata: " << d->path << filePath("metadata");
|
||||
if (!d->metadata && !d->path.isEmpty()) {
|
||||
const QString metadataPath = filePath("metadata", QStringLiteral("metadata.json"));
|
||||
|
||||
if (!metadataPath.isEmpty()) {
|
||||
d->createPackageMetadata(metadataPath);
|
||||
} else {
|
||||
// d->path might still be a file, if its path has a trailing /,
|
||||
// the fileInfo lookup will fail, so remove it.
|
||||
QString p = d->path;
|
||||
if (p.endsWith(QLatin1Char('/'))) {
|
||||
p.chop(1);
|
||||
}
|
||||
QFileInfo fileInfo(p);
|
||||
|
||||
if (fileInfo.isDir()) {
|
||||
d->createPackageMetadata(d->path);
|
||||
} else if (fileInfo.exists()) {
|
||||
d->path = fileInfo.canonicalFilePath();
|
||||
d->tempRoot = d->unpack(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Set a dummy KPluginMetaData object, this way we don't try to do the expensive
|
||||
// search for the metadata again if none of the paths have changed
|
||||
if (!d->metadata) {
|
||||
d->metadata = KPluginMetaData();
|
||||
}
|
||||
|
||||
return d->metadata.value();
|
||||
}
|
||||
|
||||
QString PackagePrivate::unpack(const QString &filePath)
|
||||
{
|
||||
KArchive *archive = nullptr;
|
||||
QMimeDatabase db;
|
||||
QMimeType mimeType = db.mimeTypeForFile(filePath);
|
||||
|
||||
if (mimeType.inherits(QStringLiteral("application/zip"))) {
|
||||
archive = new KZip(filePath);
|
||||
} else if (mimeType.inherits(QStringLiteral("application/x-compressed-tar")) || //
|
||||
mimeType.inherits(QStringLiteral("application/x-gzip")) || //
|
||||
mimeType.inherits(QStringLiteral("application/x-tar")) || //
|
||||
mimeType.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
|
||||
mimeType.inherits(QStringLiteral("application/x-xz")) || //
|
||||
mimeType.inherits(QStringLiteral("application/x-lzma"))) {
|
||||
archive = new KTar(filePath);
|
||||
} else {
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << filePath << mimeType.name();
|
||||
}
|
||||
QString tempRoot;
|
||||
if (archive && archive->open(QIODevice::ReadOnly)) {
|
||||
const KArchiveDirectory *source = archive->directory();
|
||||
QTemporaryDir tempdir;
|
||||
tempdir.setAutoRemove(false);
|
||||
tempRoot = tempdir.path() + QLatin1Char('/');
|
||||
source->copyTo(tempRoot);
|
||||
|
||||
if (!QFile::exists(tempdir.path() + QLatin1String("/metadata.json"))) {
|
||||
// search metadata.json, the zip file might have the package contents in a subdirectory
|
||||
QDir unpackedPath(tempdir.path());
|
||||
const auto entries = unpackedPath.entryInfoList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
for (const auto &pack : entries) {
|
||||
if (QFile::exists(pack.filePath() + QLatin1String("/metadata.json"))) {
|
||||
tempRoot = pack.filePath() + QLatin1Char('/');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
createPackageMetadata(tempRoot);
|
||||
} else {
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not open package file:" << path;
|
||||
}
|
||||
|
||||
delete archive;
|
||||
return tempRoot;
|
||||
}
|
||||
|
||||
bool PackagePrivate::isInsidePackageDir(const QString &canonicalPath) const
|
||||
{
|
||||
// make sure that the target file is actually inside the package dir to prevent
|
||||
// path traversal using symlinks or "../" path segments
|
||||
|
||||
// make sure we got passed a valid path
|
||||
Q_ASSERT(QFileInfo::exists(canonicalPath));
|
||||
Q_ASSERT(QFileInfo(canonicalPath).canonicalFilePath() == canonicalPath);
|
||||
// make sure that the base path is also canonical
|
||||
// this was not the case until 5.8, making this check fail e.g. if /home is a symlink
|
||||
// which in turn would make plasmashell not find the .qml files
|
||||
// installed package
|
||||
if (tempRoot.isEmpty()) {
|
||||
Q_ASSERT(QDir(path).exists());
|
||||
Q_ASSERT(path == QStringLiteral("/") || QDir(path).canonicalPath() + QLatin1Char('/') == path);
|
||||
|
||||
if (canonicalPath.startsWith(path)) {
|
||||
return true;
|
||||
}
|
||||
// temporary compressed package
|
||||
} else {
|
||||
Q_ASSERT(QDir(tempRoot).exists());
|
||||
Q_ASSERT(tempRoot == QStringLiteral("/") || QDir(tempRoot).canonicalPath() + QLatin1Char('/') == tempRoot);
|
||||
|
||||
if (canonicalPath.startsWith(tempRoot)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
qCWarning(KPACKAGE_LOG) << "Path traversal attempt detected:" << canonicalPath << "is not inside" << path;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString Package::filePath(const QByteArray &fileType, const QString &filename) const
|
||||
{
|
||||
if (!d->valid && d->checkedValid) { // Don't check the validity here, because we'd have infinite recursion
|
||||
QString result = d->fallbackFilePath(fileType, filename);
|
||||
if (result.isEmpty()) {
|
||||
// qCDebug(KPACKAGE_LOG) << fileType << "file with name" << filename
|
||||
// << "was not found in package with path" << d->path;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
const QString discoveryKey(QString::fromUtf8(fileType) + filename);
|
||||
const auto path = d->discoveries.value(discoveryKey);
|
||||
if (!path.isEmpty()) {
|
||||
return path;
|
||||
}
|
||||
|
||||
QStringList paths;
|
||||
|
||||
if (!fileType.isEmpty()) {
|
||||
const auto contents = d->contents.constFind(fileType);
|
||||
if (contents == d->contents.constEnd()) {
|
||||
// qCDebug(KPACKAGE_LOG) << "package does not contain" << fileType << filename;
|
||||
return d->fallbackFilePath(fileType, filename);
|
||||
}
|
||||
|
||||
paths = contents->paths;
|
||||
|
||||
if (paths.isEmpty()) {
|
||||
// qCDebug(KPACKAGE_LOG) << "no matching path came of it, while looking for" << fileType << filename;
|
||||
d->discoveries.insert(discoveryKey, QString());
|
||||
return d->fallbackFilePath(fileType, filename);
|
||||
}
|
||||
} else {
|
||||
// when filetype is empty paths is always empty, so try with an empty string
|
||||
paths << QString();
|
||||
}
|
||||
|
||||
// Nested loop, but in the medium case resolves to just one iteration
|
||||
// qCDebug(KPACKAGE_LOG) << "prefixes:" << d->contentsPrefixPaths.count() << d->contentsPrefixPaths;
|
||||
for (const QString &contentsPrefix : std::as_const(d->contentsPrefixPaths)) {
|
||||
QString prefix;
|
||||
// We are an installed package
|
||||
if (d->tempRoot.isEmpty()) {
|
||||
prefix = fileType == "metadata" ? d->path : (d->path + contentsPrefix);
|
||||
// We are a compressed package temporarily uncompressed in /tmp
|
||||
} else {
|
||||
prefix = fileType == "metadata" ? d->tempRoot : (d->tempRoot + contentsPrefix);
|
||||
}
|
||||
|
||||
for (const QString &path : std::as_const(paths)) {
|
||||
QString file = prefix + path;
|
||||
|
||||
if (!filename.isEmpty()) {
|
||||
file.append(QLatin1Char('/') + filename);
|
||||
}
|
||||
|
||||
QFileInfo fi(file);
|
||||
if (fi.exists()) {
|
||||
if (d->externalPaths) {
|
||||
// qCDebug(KPACKAGE_LOG) << "found" << file;
|
||||
d->discoveries.insert(discoveryKey, file);
|
||||
return file;
|
||||
}
|
||||
|
||||
// ensure that we don't return files outside of our base path
|
||||
// due to symlink or ../ games
|
||||
if (d->isInsidePackageDir(fi.canonicalFilePath())) {
|
||||
// qCDebug(KPACKAGE_LOG) << "found" << file;
|
||||
d->discoveries.insert(discoveryKey, file);
|
||||
return file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// qCDebug(KPACKAGE_LOG) << fileType << filename << "does not exist in" << prefixes << "at root" << d->path;
|
||||
return d->fallbackFilePath(fileType, filename);
|
||||
}
|
||||
|
||||
QUrl Package::fileUrl(const QByteArray &fileType, const QString &filename) const
|
||||
{
|
||||
QString path = filePath(fileType, filename);
|
||||
// construct a qrc:/ url or a file:/ url, the only two protocols supported
|
||||
if (path.startsWith(QStringLiteral(":"))) {
|
||||
return QUrl(QStringLiteral("qrc") + path);
|
||||
} else {
|
||||
return QUrl::fromLocalFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
QStringList Package::entryList(const QByteArray &key) const
|
||||
{
|
||||
if (!d->valid) {
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
const auto it = d->contents.constFind(key);
|
||||
if (it == d->contents.constEnd()) {
|
||||
qCWarning(KPACKAGE_LOG) << "couldn't find" << key << "when trying to list entries";
|
||||
return QStringList();
|
||||
}
|
||||
|
||||
QStringList list;
|
||||
for (const QString &prefix : std::as_const(d->contentsPrefixPaths)) {
|
||||
// qCDebug(KPACKAGE_LOG) << " looking in" << prefix;
|
||||
const QStringList paths = it.value().paths;
|
||||
for (const QString &path : paths) {
|
||||
// qCDebug(KPACKAGE_LOG) << " looking in" << path;
|
||||
if (it.value().directory) {
|
||||
// qCDebug(KPACKAGE_LOG) << "it's a directory, so trying out" << d->path + prefix + path;
|
||||
QDir dir(d->path + prefix + path);
|
||||
if (d->externalPaths) {
|
||||
list += dir.entryList(QDir::Files | QDir::Readable);
|
||||
} else {
|
||||
// ensure that we don't return files outside of our base path
|
||||
// due to symlink or ../ games
|
||||
QString canonicalized = dir.canonicalPath();
|
||||
if (canonicalized.startsWith(d->path)) {
|
||||
list += dir.entryList(QDir::Files | QDir::Readable);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const QString fullPath = d->path + prefix + path;
|
||||
// qCDebug(KPACKAGE_LOG) << "it's a file at" << fullPath << QFile::exists(fullPath);
|
||||
if (!QFile::exists(fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (d->externalPaths) {
|
||||
list += fullPath;
|
||||
} else {
|
||||
QDir dir(fullPath);
|
||||
QString canonicalized = dir.canonicalPath() + QDir::separator();
|
||||
|
||||
// qCDebug(KPACKAGE_LOG) << "testing that" << canonicalized << "is in" << d->path;
|
||||
if (canonicalized.startsWith(d->path)) {
|
||||
list += fullPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
void Package::setPath(const QString &path)
|
||||
{
|
||||
// if the path is already what we have, don't bother
|
||||
if (path == d->path) {
|
||||
return;
|
||||
}
|
||||
|
||||
// our dptr is shared, and it is almost certainly going to change.
|
||||
// hold onto the old pointer just in case it does not, however!
|
||||
QExplicitlySharedDataPointer<PackagePrivate> oldD(d);
|
||||
d.detach();
|
||||
d->metadata = std::nullopt;
|
||||
|
||||
// without structure we're doomed
|
||||
if (!d->structure) {
|
||||
d->path.clear();
|
||||
d->discoveries.clear();
|
||||
d->valid = false;
|
||||
d->checkedValid = true;
|
||||
qCWarning(KPACKAGE_LOG) << "Cannot set a path in a package without structure" << path;
|
||||
return;
|
||||
}
|
||||
|
||||
// empty path => nothing to do
|
||||
if (path.isEmpty()) {
|
||||
d->path.clear();
|
||||
d->discoveries.clear();
|
||||
d->valid = false;
|
||||
d->structure.data()->pathChanged(this);
|
||||
return;
|
||||
}
|
||||
|
||||
// now we look for all possible paths, including resolving
|
||||
// relative paths against the system search paths
|
||||
QStringList paths;
|
||||
if (QDir::isRelativePath(path)) {
|
||||
QString p;
|
||||
|
||||
if (d->defaultPackageRoot.isEmpty()) {
|
||||
p = path % QLatin1Char('/');
|
||||
} else {
|
||||
p = d->defaultPackageRoot % path % QLatin1Char('/');
|
||||
}
|
||||
|
||||
if (QDir::isRelativePath(p)) {
|
||||
// FIXME: can searching of the qrc be better?
|
||||
paths << QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, p, QStandardPaths::LocateDirectory);
|
||||
} else {
|
||||
const QDir dir(p);
|
||||
if (QFile::exists(dir.canonicalPath())) {
|
||||
paths << p;
|
||||
}
|
||||
}
|
||||
|
||||
// qCDebug(KPACKAGE_LOG) << "paths:" << p << paths << d->defaultPackageRoot;
|
||||
} else {
|
||||
const QDir dir(path);
|
||||
if (QFile::exists(dir.canonicalPath())) {
|
||||
paths << path;
|
||||
}
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(path);
|
||||
if (fileInfo.isFile() && d->tempRoot.isEmpty()) {
|
||||
d->path = fileInfo.canonicalFilePath();
|
||||
d->tempRoot = d->unpack(path);
|
||||
}
|
||||
|
||||
// now we search each path found, caching our previous path to know if
|
||||
// anything actually really changed
|
||||
const QString previousPath = d->path;
|
||||
for (const QString &p : std::as_const(paths)) {
|
||||
d->checkedValid = false;
|
||||
QDir dir(p);
|
||||
|
||||
Q_ASSERT(QFile::exists(dir.canonicalPath()));
|
||||
|
||||
d->path = dir.canonicalPath();
|
||||
// canonicalPath() does not include a trailing slash (unless it is the root dir)
|
||||
if (!d->path.endsWith(QLatin1Char('/'))) {
|
||||
d->path.append(QLatin1Char('/'));
|
||||
}
|
||||
|
||||
const QString fallbackPath = metadata().value(QStringLiteral("X-Plasma-RootPath"));
|
||||
if (!fallbackPath.isEmpty()) {
|
||||
const KPackage::Package fp = KPackage::PackageLoader::self()->loadPackage(QStringLiteral("Plasma/Applet"), fallbackPath);
|
||||
setFallbackPackage(fp);
|
||||
}
|
||||
|
||||
// we need to tell the structure we're changing paths ...
|
||||
d->structure.data()->pathChanged(this);
|
||||
// ... and then testing the results for validity
|
||||
if (isValid()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// if nothing did change, then we go back to the old dptr
|
||||
if (d->path == previousPath) {
|
||||
d = oldD;
|
||||
return;
|
||||
}
|
||||
|
||||
// .. but something did change, so we get rid of our discovery cache
|
||||
d->discoveries.clear();
|
||||
|
||||
// Do NOT override the metadata when the PackageStructure has set it
|
||||
if (!previousPath.isEmpty()) {
|
||||
d->metadata = std::nullopt;
|
||||
}
|
||||
|
||||
// uh-oh, but we didn't end up with anything valid, so we sadly reset ourselves
|
||||
// to futility.
|
||||
if (!d->valid) {
|
||||
d->path.clear();
|
||||
d->structure.data()->pathChanged(this);
|
||||
}
|
||||
}
|
||||
|
||||
const QString Package::path() const
|
||||
{
|
||||
return d->path;
|
||||
}
|
||||
|
||||
QStringList Package::contentsPrefixPaths() const
|
||||
{
|
||||
return d->contentsPrefixPaths;
|
||||
}
|
||||
|
||||
void Package::setContentsPrefixPaths(const QStringList &prefixPaths)
|
||||
{
|
||||
d.detach();
|
||||
d->contentsPrefixPaths = prefixPaths;
|
||||
if (d->contentsPrefixPaths.isEmpty()) {
|
||||
d->contentsPrefixPaths << QString();
|
||||
} else {
|
||||
// the code assumes that the prefixes have a trailing slash
|
||||
// so let's make that true here
|
||||
QMutableStringListIterator it(d->contentsPrefixPaths);
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
|
||||
if (!it.value().endsWith(QLatin1Char('/'))) {
|
||||
it.setValue(it.value() % QLatin1Char('/'));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
QByteArray Package::cryptographicHash(QCryptographicHash::Algorithm algorithm) const
|
||||
{
|
||||
if (!d->valid) {
|
||||
qCWarning(KPACKAGE_LOG) << "can not create hash due to Package being invalid";
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
QCryptographicHash hash(algorithm);
|
||||
const QString guessedMetaDataJson = d->path + QLatin1String("metadata.json");
|
||||
const QString metadataPath = QFile::exists(guessedMetaDataJson) ? guessedMetaDataJson : QString();
|
||||
if (!metadataPath.isEmpty()) {
|
||||
QFile f(metadataPath);
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
while (!f.atEnd()) {
|
||||
hash.addData(f.read(1024));
|
||||
}
|
||||
} else {
|
||||
qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading.";
|
||||
}
|
||||
} else {
|
||||
qCWarning(KPACKAGE_LOG) << "no metadata at" << metadataPath;
|
||||
}
|
||||
|
||||
for (const QString &prefix : std::as_const(d->contentsPrefixPaths)) {
|
||||
const QString basePath = d->path + prefix;
|
||||
QDir dir(basePath);
|
||||
|
||||
if (!dir.exists()) {
|
||||
return QByteArray();
|
||||
}
|
||||
|
||||
d->updateHash(basePath, QString(), dir, hash);
|
||||
}
|
||||
|
||||
return hash.result().toHex();
|
||||
}
|
||||
|
||||
void Package::addDirectoryDefinition(const QByteArray &key, const QString &path)
|
||||
{
|
||||
const auto contentsIt = d->contents.constFind(key);
|
||||
ContentStructure s;
|
||||
|
||||
if (contentsIt != d->contents.constEnd()) {
|
||||
if (contentsIt->paths.contains(path) && contentsIt->directory == true) {
|
||||
return;
|
||||
}
|
||||
s = *contentsIt;
|
||||
}
|
||||
|
||||
d.detach();
|
||||
|
||||
s.paths.append(path);
|
||||
s.directory = true;
|
||||
|
||||
d->contents[key] = s;
|
||||
}
|
||||
|
||||
void Package::addFileDefinition(const QByteArray &key, const QString &path)
|
||||
{
|
||||
const auto contentsIt = d->contents.constFind(key);
|
||||
ContentStructure s;
|
||||
|
||||
if (contentsIt != d->contents.constEnd()) {
|
||||
if (contentsIt->paths.contains(path) && contentsIt->directory == true) {
|
||||
return;
|
||||
}
|
||||
s = *contentsIt;
|
||||
}
|
||||
|
||||
d.detach();
|
||||
|
||||
s.paths.append(path);
|
||||
s.directory = false;
|
||||
|
||||
d->contents[key] = s;
|
||||
}
|
||||
|
||||
void Package::removeDefinition(const QByteArray &key)
|
||||
{
|
||||
if (d->contents.contains(key)) {
|
||||
d.detach();
|
||||
d->contents.remove(key);
|
||||
}
|
||||
|
||||
if (d->discoveries.contains(QString::fromLatin1(key))) {
|
||||
d.detach();
|
||||
d->discoveries.remove(QString::fromLatin1(key));
|
||||
}
|
||||
}
|
||||
|
||||
void Package::setRequired(const QByteArray &key, bool required)
|
||||
{
|
||||
QHash<QByteArray, ContentStructure>::iterator it = d->contents.find(key);
|
||||
if (it == d->contents.end()) {
|
||||
qCWarning(KPACKAGE_LOG) << key << "is now a known key for the package. File is thus not set to being required";
|
||||
return;
|
||||
}
|
||||
|
||||
d.detach();
|
||||
// have to find the item again after detaching: d->contents is a different object now
|
||||
it = d->contents.find(key);
|
||||
it.value().required = required;
|
||||
}
|
||||
|
||||
void Package::setDefaultMimeTypes(const QStringList &mimeTypes)
|
||||
{
|
||||
d.detach();
|
||||
d->mimeTypes = mimeTypes;
|
||||
}
|
||||
|
||||
void Package::setMimeTypes(const QByteArray &key, const QStringList &mimeTypes)
|
||||
{
|
||||
if (!d->contents.contains(key)) {
|
||||
return;
|
||||
}
|
||||
|
||||
d.detach();
|
||||
d->contents[key].mimeTypes = mimeTypes;
|
||||
}
|
||||
|
||||
QList<QByteArray> Package::directories() const
|
||||
{
|
||||
QList<QByteArray> dirs;
|
||||
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
|
||||
if (it.value().directory) {
|
||||
dirs << it.key();
|
||||
}
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
QList<QByteArray> Package::requiredDirectories() const
|
||||
{
|
||||
QList<QByteArray> dirs;
|
||||
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
|
||||
if (it.value().directory && it.value().required) {
|
||||
dirs << it.key();
|
||||
}
|
||||
}
|
||||
return dirs;
|
||||
}
|
||||
|
||||
QList<QByteArray> Package::files() const
|
||||
{
|
||||
QList<QByteArray> files;
|
||||
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
|
||||
if (!it.value().directory) {
|
||||
files << it.key();
|
||||
}
|
||||
}
|
||||
return files;
|
||||
}
|
||||
|
||||
QList<QByteArray> Package::requiredFiles() const
|
||||
{
|
||||
QList<QByteArray> files;
|
||||
for (auto it = d->contents.cbegin(); it != d->contents.cend(); ++it) {
|
||||
if (!it.value().directory && it.value().required) {
|
||||
files << it.key();
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
|
||||
PackagePrivate::PackagePrivate()
|
||||
: QSharedData()
|
||||
{
|
||||
contentsPrefixPaths << QStringLiteral("contents/");
|
||||
}
|
||||
|
||||
PackagePrivate::PackagePrivate(const PackagePrivate &other)
|
||||
: QSharedData()
|
||||
{
|
||||
*this = other;
|
||||
if (other.metadata && other.metadata.value().isValid()) {
|
||||
metadata = other.metadata;
|
||||
}
|
||||
}
|
||||
|
||||
PackagePrivate::~PackagePrivate()
|
||||
{
|
||||
if (!tempRoot.isEmpty()) {
|
||||
QDir dir(tempRoot);
|
||||
dir.removeRecursively();
|
||||
}
|
||||
}
|
||||
|
||||
PackagePrivate &PackagePrivate::operator=(const PackagePrivate &rhs)
|
||||
{
|
||||
if (&rhs == this) {
|
||||
return *this;
|
||||
}
|
||||
|
||||
structure = rhs.structure;
|
||||
if (rhs.fallbackPackage) {
|
||||
fallbackPackage = std::make_unique<Package>(*rhs.fallbackPackage);
|
||||
} else {
|
||||
fallbackPackage = nullptr;
|
||||
}
|
||||
if (rhs.metadata && rhs.metadata.value().isValid()) {
|
||||
metadata = rhs.metadata;
|
||||
}
|
||||
path = rhs.path;
|
||||
contentsPrefixPaths = rhs.contentsPrefixPaths;
|
||||
contents = rhs.contents;
|
||||
mimeTypes = rhs.mimeTypes;
|
||||
defaultPackageRoot = rhs.defaultPackageRoot;
|
||||
externalPaths = rhs.externalPaths;
|
||||
valid = rhs.valid;
|
||||
return *this;
|
||||
}
|
||||
|
||||
void PackagePrivate::updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash)
|
||||
{
|
||||
// hash is calculated as a function of:
|
||||
// * files ordered alphabetically by name, with each file's:
|
||||
// * path relative to the content root
|
||||
// * file data
|
||||
// * directories ordered alphabetically by name, with each dir's:
|
||||
// * path relative to the content root
|
||||
// * file listing (recursing)
|
||||
// symlinks (in both the file and dir case) are handled by adding
|
||||
// the name of the symlink itself and the abs path of what it points to
|
||||
|
||||
const QDir::SortFlags sorting = QDir::Name | QDir::IgnoreCase;
|
||||
const QDir::Filters filters = QDir::Hidden | QDir::System | QDir::NoDotAndDotDot;
|
||||
const auto lstEntries = dir.entryList(QDir::Files | filters, sorting);
|
||||
for (const QString &file : lstEntries) {
|
||||
if (!subPath.isEmpty()) {
|
||||
hash.addData(subPath.toUtf8());
|
||||
}
|
||||
|
||||
hash.addData(file.toUtf8());
|
||||
|
||||
QFileInfo info(dir.path() + QLatin1Char('/') + file);
|
||||
if (info.isSymLink()) {
|
||||
hash.addData(info.symLinkTarget().toUtf8());
|
||||
} else {
|
||||
QFile f(info.filePath());
|
||||
if (f.open(QIODevice::ReadOnly)) {
|
||||
while (!f.atEnd()) {
|
||||
hash.addData(f.read(1024));
|
||||
}
|
||||
} else {
|
||||
qCWarning(KPACKAGE_LOG) << "could not add" << f.fileName() << "to the hash; file could not be opened for reading. "
|
||||
<< "permissions fail?" << info.permissions() << info.isFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const auto lstEntries2 = dir.entryList(QDir::Dirs | filters, sorting);
|
||||
for (const QString &subDirPath : lstEntries2) {
|
||||
const QString relativePath = subPath + subDirPath + QLatin1Char('/');
|
||||
hash.addData(relativePath.toUtf8());
|
||||
|
||||
QDir subDir(dir.path());
|
||||
subDir.cd(subDirPath);
|
||||
|
||||
if (subDir.path() != subDir.canonicalPath()) {
|
||||
hash.addData(subDir.canonicalPath().toUtf8());
|
||||
} else {
|
||||
updateHash(basePath, relativePath, subDir, hash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PackagePrivate::createPackageMetadata(const QString &path)
|
||||
{
|
||||
if (QFileInfo(path).isDir()) {
|
||||
if (const QString jsonPath = path + QLatin1String("/metadata.json"); QFileInfo::exists(jsonPath)) {
|
||||
metadata = KPluginMetaData::fromJsonFile(jsonPath);
|
||||
} else {
|
||||
qCDebug(KPACKAGE_LOG) << "No metadata file in the package, expected it at:" << jsonPath;
|
||||
}
|
||||
} else {
|
||||
metadata = KPluginMetaData::fromJsonFile(path);
|
||||
}
|
||||
}
|
||||
|
||||
QString PackagePrivate::fallbackFilePath(const QByteArray &key, const QString &filename) const
|
||||
{
|
||||
// don't fallback if the package isn't valid and never fallback the metadata file
|
||||
if (key != "metadata" && fallbackPackage && fallbackPackage->isValid()) {
|
||||
return fallbackPackage->filePath(key, filename);
|
||||
} else {
|
||||
return QString();
|
||||
}
|
||||
}
|
||||
|
||||
bool PackagePrivate::hasCycle(const KPackage::Package &package)
|
||||
{
|
||||
if (!package.d->fallbackPackage) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// This is the Floyd cycle detection algorithm
|
||||
// http://en.wikipedia.org/wiki/Cycle_detection#Tortoise_and_hare
|
||||
const KPackage::Package *slowPackage = &package;
|
||||
const KPackage::Package *fastPackage = &package;
|
||||
|
||||
while (fastPackage && fastPackage->d->fallbackPackage) {
|
||||
// consider two packages the same if they have the same metadata
|
||||
if ((fastPackage->d->fallbackPackage->metadata().isValid() && fastPackage->d->fallbackPackage->metadata() == slowPackage->metadata())
|
||||
|| (fastPackage->d->fallbackPackage->d->fallbackPackage && fastPackage->d->fallbackPackage->d->fallbackPackage->metadata().isValid()
|
||||
&& fastPackage->d->fallbackPackage->d->fallbackPackage->metadata() == slowPackage->metadata())) {
|
||||
qCWarning(KPACKAGE_LOG) << "Warning: the fallback chain of " << package.metadata().pluginId() << "contains a cyclical dependency.";
|
||||
return true;
|
||||
}
|
||||
fastPackage = fastPackage->d->fallbackPackage->d->fallbackPackage.get();
|
||||
slowPackage = slowPackage->d->fallbackPackage.get();
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // Namespace
|
||||
@@ -0,0 +1,317 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007-2011 Aaron Seigo <aseigo@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_PACKAGE_H
|
||||
#define KPACKAGE_PACKAGE_H
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QMetaType>
|
||||
#include <QStringList>
|
||||
#include <QUrl>
|
||||
|
||||
#include <KPluginMetaData>
|
||||
|
||||
#include <kpackage/package_export.h>
|
||||
|
||||
#include <KJob>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
/**
|
||||
* @class Package kpackage/package.h <KPackage/Package>
|
||||
*
|
||||
* @short object representing an installed package
|
||||
*
|
||||
* Package defines what is in a package and provides easy access to the contents.
|
||||
*
|
||||
* To define a package, one might write the following code:
|
||||
*
|
||||
@code
|
||||
Package package;
|
||||
|
||||
package.addDirectoryDefinition("images", "pics/");
|
||||
package.setMimeTypes("images", QStringList{"image/svg", "image/png", "image/jpeg"});
|
||||
|
||||
package.addDirectoryDefinition("scripts", "code/");
|
||||
package.setMimeTypes("scripts", QStringList{"text/\*"});
|
||||
|
||||
package.addFileDefinition("mainscript", "code/main.js");
|
||||
package.setRequired("mainscript", true);
|
||||
@endcode
|
||||
* One may also choose to create a subclass of PackageStructure and include the setup
|
||||
* in the constructor.
|
||||
*
|
||||
* Either way, Package creates a self-documenting contract between the packager and
|
||||
* the application without exposing package internals such as actual on-disk structure
|
||||
* of the package or requiring that all contents be explicitly known ahead of time.
|
||||
*
|
||||
* Subclassing PackageStructure does have provide a number of potential const benefits:
|
||||
* * the package can be notified of path changes via the virtual pathChanged() method
|
||||
* * the subclass may implement mechanisms to install and remove packages using the
|
||||
* virtual install and uninstall methods
|
||||
* * subclasses can be compiled as plugins for easy re-use
|
||||
**/
|
||||
// TODO: write documentation on USING a package
|
||||
|
||||
class PackagePrivate;
|
||||
class PackageStructure;
|
||||
|
||||
class KPACKAGE_EXPORT Package
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Default constructor
|
||||
*
|
||||
* @param structure if a null pointer is passed in, this will creates an empty (invalid) Package;
|
||||
* otherwise the structure is allowed to set up the Package's initial layout
|
||||
*/
|
||||
explicit Package(PackageStructure *structure = nullptr);
|
||||
|
||||
/**
|
||||
* Copy constructor
|
||||
*/
|
||||
Package(const Package &other);
|
||||
|
||||
virtual ~Package();
|
||||
|
||||
/**
|
||||
* Assignment operator
|
||||
*/
|
||||
Package &operator=(const Package &rhs);
|
||||
|
||||
/**
|
||||
* @return true if this package has a valid PackageStructure associatedw it with it.
|
||||
* A package may not be valid, but have a valid structure. Useful when dealing with
|
||||
* Package objects in a semi-initialized state (e.g. before calling setPath())
|
||||
* @since 5.1
|
||||
*/
|
||||
bool hasValidStructure() const;
|
||||
|
||||
/**
|
||||
* @return true if all the required components exist
|
||||
**/
|
||||
bool isValid() const;
|
||||
|
||||
/**
|
||||
* Sets the path to the root of this package
|
||||
* @param path an absolute path, or a relative path to the default package root
|
||||
*/
|
||||
void setPath(const QString &path);
|
||||
|
||||
/**
|
||||
* @return the path to the root of this particular package
|
||||
*/
|
||||
const QString path() const;
|
||||
|
||||
/**
|
||||
* Get the path to a given file based on the key and an optional filename.
|
||||
* Example: finding the main script in a scripting package:
|
||||
* filePath("mainscript")
|
||||
*
|
||||
* Example: finding a specific image in the images directory:
|
||||
* filePath("images", "myimage.png")
|
||||
*
|
||||
* @param key the key of the file type to look for,
|
||||
* @param filename optional name of the file to locate within the package
|
||||
* @return path to the file on disk. QString() if not found.
|
||||
**/
|
||||
QString filePath(const QByteArray &key, const QString &filename = QString()) const;
|
||||
|
||||
/**
|
||||
* Get the url to a given file based on the key and an optional filename, is the file:// or qrc:// format
|
||||
* Example: finding the main script in a scripting package:
|
||||
* filePath("mainscript")
|
||||
*
|
||||
* Example: finding a specific image in the images directory:
|
||||
* filePath("images", "myimage.png")
|
||||
*
|
||||
* @param key the key of the file type to look for,
|
||||
* @param filename optional name of the file to locate within the package
|
||||
* @return path to the file on disk. QUrl() if not found.
|
||||
* @since 5.41
|
||||
**/
|
||||
QUrl fileUrl(const QByteArray &key, const QString &filename = QString()) const;
|
||||
|
||||
/**
|
||||
* Get the list of files of a given type.
|
||||
*
|
||||
* @param fileType the type of file to look for, as defined in the
|
||||
* package structure.
|
||||
* @return list of files by name, suitable for passing to filePath
|
||||
**/
|
||||
QStringList entryList(const QByteArray &key) const;
|
||||
|
||||
/**
|
||||
* @return true if the item at path exists and is required
|
||||
**/
|
||||
bool isRequired(const QByteArray &key) const;
|
||||
|
||||
/**
|
||||
* @return the mimeTypes associated with the path, if any
|
||||
**/
|
||||
QStringList mimeTypes(const QByteArray &key) const;
|
||||
|
||||
/**
|
||||
* @return the prefix paths inserted between the base path and content entries, in order of priority.
|
||||
* When searching for a file, all paths will be tried in order.
|
||||
*/
|
||||
QStringList contentsPrefixPaths() const;
|
||||
|
||||
/**
|
||||
* @return preferred package root. This defaults to kpackage/generic/
|
||||
*/
|
||||
QString defaultPackageRoot() const;
|
||||
|
||||
/**
|
||||
* @return true if paths/symlinks outside the package itself should be followed.
|
||||
* By default this is set to false for security reasons.
|
||||
*/
|
||||
bool allowExternalPaths() const;
|
||||
|
||||
/**
|
||||
* Sets the metadata for the KPackage. This overwrites the current metadata.
|
||||
* This should be used in case a kpackage gets loaded by name, based
|
||||
* on the path a C++ plugin which has embedded metadata.
|
||||
* @since 5.88
|
||||
*/
|
||||
void setMetadata(const KPluginMetaData &data);
|
||||
|
||||
/**
|
||||
* @return the package metadata object.
|
||||
*/
|
||||
KPluginMetaData metadata() const;
|
||||
|
||||
/**
|
||||
* @return a hash digest of the contents of the package in hexadecimal form
|
||||
* @since 5.21
|
||||
*/
|
||||
QByteArray cryptographicHash(QCryptographicHash::Algorithm algorithm) const;
|
||||
|
||||
/**
|
||||
* Adds a directory to the structure of the package. It is added as
|
||||
* a not-required element with no associated mimeTypes.
|
||||
* If an entry with the given key already exists, the path
|
||||
* is added to it as a search alternative.
|
||||
*
|
||||
* @param key used as an internal label for this directory
|
||||
* @param path the path within the package for this directory
|
||||
*/
|
||||
void addDirectoryDefinition(const QByteArray &key, const QString &path);
|
||||
|
||||
/**
|
||||
* Adds a file to the structure of the package. It is added as
|
||||
* a not-required element with no associated mimeTypes.
|
||||
* If an entry with the given key already exists, the path
|
||||
* is added to it as a search alternative.
|
||||
*
|
||||
* @param key used as an internal label for this file
|
||||
* @param path the path within the package for this file
|
||||
*/
|
||||
void addFileDefinition(const QByteArray &key, const QString &path);
|
||||
|
||||
/**
|
||||
* Removes a definition from the structure of the package.
|
||||
* @param key the internal label of the file or directory to remove
|
||||
*/
|
||||
void removeDefinition(const QByteArray &key);
|
||||
|
||||
/**
|
||||
* Sets whether or not a given part of the structure is required or not.
|
||||
* The path must already have been added using addDirectoryDefinition
|
||||
* or addFileDefinition.
|
||||
*
|
||||
* @param key the entry within the package
|
||||
* @param required true if this entry is required, false if not
|
||||
*/
|
||||
void setRequired(const QByteArray &key, bool required);
|
||||
|
||||
/**
|
||||
* Defines the default mimeTypes for any definitions that do not have
|
||||
* associated mimeTypes. Handy for packages with only one or predominantly
|
||||
* one file type.
|
||||
*
|
||||
* @param mimeTypes a list of mimeTypes
|
||||
**/
|
||||
void setDefaultMimeTypes(const QStringList &mimeTypes);
|
||||
|
||||
/**
|
||||
* Define mimeTypes for a given part of the structure
|
||||
* The path must already have been added using addDirectoryDefinition
|
||||
* or addFileDefinition.
|
||||
*
|
||||
* @param key the entry within the package
|
||||
* @param mimeTypes a list of mimeTypes
|
||||
**/
|
||||
void setMimeTypes(const QByteArray &key, const QStringList &mimeTypes);
|
||||
|
||||
/**
|
||||
* Sets the prefixes that all the contents in this package should
|
||||
* appear under. This defaults to "contents/" and is added automatically
|
||||
* between the base path and the entries as defined by the package
|
||||
* structure. Multiple entries can be added.
|
||||
* In this case each file request will be searched in all prefixes in order,
|
||||
* and the first found will be returned.
|
||||
*
|
||||
* @param prefix paths the directory prefix to use
|
||||
*/
|
||||
void setContentsPrefixPaths(const QStringList &prefixPaths);
|
||||
|
||||
/**
|
||||
* Sets whether or not external paths/symlinks can be followed by a package
|
||||
* @param allow true if paths/symlinks outside of the package should be followed,
|
||||
* false if they should be rejected.
|
||||
*/
|
||||
void setAllowExternalPaths(bool allow);
|
||||
|
||||
/**
|
||||
* Sets preferred package root.
|
||||
*/
|
||||
void setDefaultPackageRoot(const QString &packageRoot);
|
||||
|
||||
/**
|
||||
* Sets the fallback package root path
|
||||
* If a file won't be found in this package, it will search it in the package
|
||||
* with the same structure identified by path
|
||||
* It is intended to be used by the packageStructure
|
||||
* @param path package root path @see setPath
|
||||
*/
|
||||
void setFallbackPackage(const KPackage::Package &package);
|
||||
|
||||
/**
|
||||
* @return The fallback package root path
|
||||
*/
|
||||
KPackage::Package fallbackPackage() const;
|
||||
|
||||
// Content structure description methods
|
||||
/**
|
||||
* @return all directories registered as part of this Package's structure
|
||||
*/
|
||||
QList<QByteArray> directories() const;
|
||||
|
||||
/**
|
||||
* @return all directories registered as part of this Package's required structure
|
||||
*/
|
||||
QList<QByteArray> requiredDirectories() const;
|
||||
|
||||
/**
|
||||
* @return all files registered as part of this Package's structure
|
||||
*/
|
||||
QList<QByteArray> files() const;
|
||||
|
||||
/**
|
||||
* @return all files registered as part of this Package's required structure
|
||||
*/
|
||||
QList<QByteArray> requiredFiles() const;
|
||||
|
||||
private:
|
||||
QExplicitlySharedDataPointer<PackagePrivate> d;
|
||||
friend class PackagePrivate;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(KPackage::Package)
|
||||
#endif
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
|
||||
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "packagejob.h"
|
||||
|
||||
#include "config-package.h"
|
||||
#include "packageloader.h"
|
||||
#include "packagestructure.h"
|
||||
#include "private/package_p.h"
|
||||
#include "private/packagejobthread_p.h"
|
||||
#include "private/utils.h"
|
||||
|
||||
#include "kpackage_debug.h"
|
||||
|
||||
#if HAVE_QTDBUS
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#endif
|
||||
|
||||
#include <QDebug>
|
||||
#include <QStandardPaths>
|
||||
#include <QThreadPool>
|
||||
#include <QTimer>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
struct StructureOrErrorJob {
|
||||
PackageStructure *structure = nullptr;
|
||||
PackageJob *errorJob = nullptr;
|
||||
};
|
||||
class PackageJobPrivate
|
||||
{
|
||||
public:
|
||||
static StructureOrErrorJob loadStructure(const QString &packageFormat)
|
||||
{
|
||||
if (auto structure = PackageLoader::self()->loadPackageStructure(packageFormat)) {
|
||||
return StructureOrErrorJob{structure, nullptr};
|
||||
} else {
|
||||
auto job = new PackageJob(PackageJob::Install, Package(), QString(), QString());
|
||||
job->setErrorText(QStringLiteral("Could not load package structure ") + packageFormat);
|
||||
job->setError(PackageJob::JobError::InvalidPackageStructure);
|
||||
QTimer::singleShot(0, job, [job]() {
|
||||
job->emitResult();
|
||||
});
|
||||
return StructureOrErrorJob{nullptr, job};
|
||||
}
|
||||
}
|
||||
PackageJobThread *thread = nullptr;
|
||||
Package package;
|
||||
QString installPath;
|
||||
};
|
||||
|
||||
PackageJob::PackageJob(OperationType type, const Package &package, const QString &src, const QString &dest)
|
||||
: KJob()
|
||||
, d(new PackageJobPrivate)
|
||||
{
|
||||
d->thread = new PackageJobThread(type, src, dest, package);
|
||||
d->package = package;
|
||||
|
||||
connect(d->thread, &PackageJobThread::installPathChanged, this, [this](const QString &installPath) {
|
||||
d->package.setPath(installPath);
|
||||
});
|
||||
|
||||
// setupNotificationsOnJobFinished connects to jobThreadFinished,
|
||||
// don't connect to it again
|
||||
if (type == Install) {
|
||||
setupNotificationsOnJobFinished(QStringLiteral("packageInstalled"));
|
||||
} else if (type == Update) {
|
||||
setupNotificationsOnJobFinished(QStringLiteral("packageUpdated"));
|
||||
d->thread->update(src, dest, package);
|
||||
} else if (type == Uninstall) {
|
||||
setupNotificationsOnJobFinished(QStringLiteral("packageUninstalled"));
|
||||
} else {
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
PackageJob::~PackageJob() = default;
|
||||
|
||||
void PackageJob::start()
|
||||
{
|
||||
if (d->thread) {
|
||||
QThreadPool::globalInstance()->start(d->thread);
|
||||
d->thread = nullptr;
|
||||
} else {
|
||||
qCWarning(KPACKAGE_LOG) << "The KPackage::PackageJob was already started";
|
||||
}
|
||||
}
|
||||
|
||||
PackageJob *PackageJob::install(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot)
|
||||
{
|
||||
auto structOrErr = PackageJobPrivate::loadStructure(packageFormat);
|
||||
if (auto structure = structOrErr.structure) {
|
||||
Package package(structure);
|
||||
package.setPath(sourcePackage);
|
||||
QString dest = packageRoot.isEmpty() ? package.defaultPackageRoot() : packageRoot;
|
||||
PackageLoader::invalidateCache();
|
||||
|
||||
// use absolute paths if passed, otherwise go under share
|
||||
if (!QDir::isAbsolutePath(dest)) {
|
||||
dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest;
|
||||
}
|
||||
auto job = new PackageJob(Install, package, sourcePackage, dest);
|
||||
job->start();
|
||||
return job;
|
||||
} else {
|
||||
return structOrErr.errorJob;
|
||||
}
|
||||
}
|
||||
|
||||
PackageJob *PackageJob::update(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot)
|
||||
{
|
||||
auto structOrErr = PackageJobPrivate::loadStructure(packageFormat);
|
||||
if (auto structure = structOrErr.structure) {
|
||||
Package package(structure);
|
||||
package.setPath(sourcePackage);
|
||||
QString dest = packageRoot.isEmpty() ? package.defaultPackageRoot() : packageRoot;
|
||||
PackageLoader::invalidateCache();
|
||||
|
||||
// use absolute paths if passed, otherwise go under share
|
||||
if (!QDir::isAbsolutePath(dest)) {
|
||||
dest = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + dest;
|
||||
}
|
||||
auto job = new PackageJob(Update, package, sourcePackage, dest);
|
||||
job->start();
|
||||
return job;
|
||||
} else {
|
||||
return structOrErr.errorJob;
|
||||
}
|
||||
}
|
||||
|
||||
PackageJob *PackageJob::uninstall(const QString &packageFormat, const QString &pluginId, const QString &packageRoot)
|
||||
{
|
||||
auto structOrErr = PackageJobPrivate::loadStructure(packageFormat);
|
||||
if (auto structure = structOrErr.structure) {
|
||||
Package package(structure);
|
||||
QString uninstallPath;
|
||||
// We handle the empty path when uninstalling the package
|
||||
// If the dir already got deleted the pluginId is an empty string, without this
|
||||
// check we would delete the package root, BUG: 410682
|
||||
if (!pluginId.isEmpty()) {
|
||||
uninstallPath = packageRoot + QLatin1Char('/') + pluginId;
|
||||
}
|
||||
package.setPath(uninstallPath);
|
||||
|
||||
PackageLoader::invalidateCache();
|
||||
auto job = new PackageJob(Uninstall, package, QString(), QString());
|
||||
job->start();
|
||||
return job;
|
||||
} else {
|
||||
return structOrErr.errorJob;
|
||||
}
|
||||
}
|
||||
|
||||
KPackage::Package PackageJob::package() const
|
||||
{
|
||||
return d->package;
|
||||
}
|
||||
void PackageJob::setupNotificationsOnJobFinished(const QString &messageName)
|
||||
{
|
||||
// capture first as uninstalling wipes d->package
|
||||
// or d-package can become dangling during the job if deleted externally
|
||||
const QString pluginId = d->package.metadata().pluginId();
|
||||
const QString kpackageType = readKPackageType(d->package.metadata());
|
||||
|
||||
auto onJobFinished = [=, this](bool ok, JobError errorCode, const QString &error) {
|
||||
#if HAVE_QTDBUS
|
||||
if (ok) {
|
||||
auto msg = QDBusMessage::createSignal(QStringLiteral("/KPackage/") + kpackageType, QStringLiteral("org.kde.plasma.kpackage"), messageName);
|
||||
msg.setArguments({pluginId});
|
||||
QDBusConnection::sessionBus().send(msg);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (ok) {
|
||||
setError(NoError);
|
||||
} else {
|
||||
setError(errorCode);
|
||||
setErrorText(error);
|
||||
}
|
||||
emitResult();
|
||||
};
|
||||
connect(d->thread, &PackageJobThread::jobThreadFinished, this, onJobFinished, Qt::QueuedConnection);
|
||||
}
|
||||
|
||||
} // namespace KPackage
|
||||
|
||||
#include "moc_packagejob.cpp"
|
||||
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
|
||||
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_PACKAGEJOB_H
|
||||
#define KPACKAGE_PACKAGEJOB_H
|
||||
|
||||
#include <kpackage/package_export.h>
|
||||
|
||||
#include <KJob>
|
||||
#include <memory>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
class PackageJobPrivate;
|
||||
class Package;
|
||||
class PackageStructure;
|
||||
|
||||
/**
|
||||
* @class PackageJob kpackage/packagejob.h <KPackage/PackageJob>
|
||||
* @short KJob subclass that allows async install/update/uninstall operations for packages
|
||||
*/
|
||||
class KPACKAGE_EXPORT PackageJob : public KJob
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/**
|
||||
* Error codes for the install/update/remove jobs
|
||||
*/
|
||||
enum JobError {
|
||||
InvalidPackageStructure = KJob::UserDefinedError + 1, /**< Could not find/load the given package structure */
|
||||
RootCreationError, /**< Cannot create package root directory */
|
||||
PackageFileNotFoundError, /**< The package file does not exist */
|
||||
UnsupportedArchiveFormatError, /**< The archive format of the package is not supported */
|
||||
PackageOpenError, /**< Can't open the package file for reading */
|
||||
PluginIdInvalidError, /**< The plugin id is not specified in the metadata.json file or contains
|
||||
characters different from letters, digits, dots and underscores */
|
||||
UpdatePackageTypeMismatchError, /**< A package with this plugin id was already installed, but has a different type in the metadata.json file */
|
||||
OldVersionRemovalError, /**< Failed to remove the old version of the package during an upgrade */
|
||||
NewerVersionAlreadyInstalledError, /**< We tried to update, but the same version or a newer one is already installed */
|
||||
PackageAlreadyInstalledError, /**< The package is already installed and a normal install (not update) was performed */
|
||||
PackageMoveError, /**< Failure to move a package from the system temporary folder to its final destination */
|
||||
PackageCopyError, /**< Failure to copy a package folder from somewhere in the filesystem to its final destination */
|
||||
PackageUninstallError, /**< Failure to uninstall a package */
|
||||
};
|
||||
|
||||
~PackageJob() override;
|
||||
/// Installs the given package. The returned job is already started
|
||||
static PackageJob *install(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot = QString());
|
||||
/// Installs the given package. The returned job is already started
|
||||
static PackageJob *update(const QString &packageFormat, const QString &sourcePackage, const QString &packageRoot = QString());
|
||||
/// Installs the given package. The returned job is already started
|
||||
static PackageJob *uninstall(const QString &packageFormat, const QString &pluginId, const QString &packageRoot = QString());
|
||||
|
||||
KPackage::Package package() const;
|
||||
|
||||
private:
|
||||
friend class PackageJobThread;
|
||||
enum OperationType {
|
||||
Install,
|
||||
Update,
|
||||
Uninstall,
|
||||
};
|
||||
void start() override;
|
||||
|
||||
KPACKAGE_NO_EXPORT explicit PackageJob(OperationType type, const Package &package, const QString &src, const QString &dest);
|
||||
KPACKAGE_NO_EXPORT void setupNotificationsOnJobFinished(const QString &messageName);
|
||||
|
||||
const std::unique_ptr<PackageJobPrivate> d;
|
||||
friend PackageJobPrivate;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,291 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "packageloader.h"
|
||||
#include "private/packageloader_p.h"
|
||||
#include "private/utils.h"
|
||||
|
||||
#include "kpackage_debug.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QDateTime>
|
||||
#include <QDirIterator>
|
||||
#include <QList>
|
||||
#include <QStandardPaths>
|
||||
|
||||
#include <KLazyLocalizedString>
|
||||
#include <KPluginFactory>
|
||||
#include <KPluginMetaData>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "config-package.h"
|
||||
|
||||
#include "package.h"
|
||||
#include "packagestructure.h"
|
||||
#include "private/packagejobthread_p.h"
|
||||
#include "private/packages_p.h"
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
PackageLoader::PackageLoader()
|
||||
: d(new PackageLoaderPrivate)
|
||||
{
|
||||
}
|
||||
|
||||
PackageLoader::~PackageLoader()
|
||||
{
|
||||
for (auto wp : std::as_const(d->structures)) {
|
||||
delete wp.data();
|
||||
}
|
||||
delete d;
|
||||
}
|
||||
|
||||
PackageLoader *PackageLoader::self()
|
||||
{
|
||||
static PackageLoader *s_packageTrader = new PackageLoader;
|
||||
return s_packageTrader;
|
||||
}
|
||||
|
||||
Package PackageLoader::loadPackage(const QString &packageFormat, const QString &packagePath)
|
||||
{
|
||||
if (packageFormat.isEmpty()) {
|
||||
return Package();
|
||||
}
|
||||
|
||||
if (PackageStructure *structure = loadPackageStructure(packageFormat)) {
|
||||
Package p(structure);
|
||||
if (!packagePath.isEmpty()) {
|
||||
p.setPath(packagePath);
|
||||
}
|
||||
return p;
|
||||
}
|
||||
|
||||
return Package();
|
||||
}
|
||||
|
||||
QList<Package> PackageLoader::listKPackages(const QString &packageFormat, const QString &packageRoot)
|
||||
{
|
||||
QList<Package> lst;
|
||||
|
||||
// has been a root specified?
|
||||
QString actualRoot = packageRoot;
|
||||
|
||||
PackageStructure *structure = d->structures.value(packageFormat).data();
|
||||
// try to take it from the package structure
|
||||
if (actualRoot.isEmpty()) {
|
||||
if (!structure) {
|
||||
if (packageFormat == QLatin1String("KPackage/Generic")) {
|
||||
structure = new GenericPackage();
|
||||
} else if (packageFormat == QLatin1String("KPackage/GenericQML")) {
|
||||
structure = new GenericQMLPackage();
|
||||
} else {
|
||||
structure = loadPackageStructure(packageFormat);
|
||||
}
|
||||
}
|
||||
|
||||
if (structure) {
|
||||
d->structures.insert(packageFormat, structure);
|
||||
actualRoot = Package(structure).defaultPackageRoot();
|
||||
}
|
||||
}
|
||||
|
||||
if (actualRoot.isEmpty()) {
|
||||
actualRoot = packageFormat;
|
||||
}
|
||||
|
||||
QStringList paths;
|
||||
if (QDir::isAbsolutePath(actualRoot)) {
|
||||
paths = QStringList(actualRoot);
|
||||
} else {
|
||||
const auto listPath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
|
||||
for (const QString &path : listPath) {
|
||||
paths += path + QLatin1Char('/') + actualRoot;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const &plugindir : std::as_const(paths)) {
|
||||
QDirIterator it(plugindir, QDir::Dirs | QDir::NoDotAndDotDot);
|
||||
std::unordered_set<QString> dirs;
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
|
||||
const QString dir = it.filePath();
|
||||
if (!dirs.insert(it.fileInfo().fileName()).second) {
|
||||
continue;
|
||||
}
|
||||
Package package(structure);
|
||||
package.setPath(dir);
|
||||
if (package.isValid()) {
|
||||
// Ignore packages with empty metadata here
|
||||
if (packageFormat.isEmpty() || !package.metadata().isValid() || readKPackageType(package.metadata()) == packageFormat) {
|
||||
lst << package;
|
||||
} else {
|
||||
qInfo() << "KPackage in" << package.path() << readKPackageType(package.metadata()) << "does not match requested format" << packageFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return lst;
|
||||
}
|
||||
QList<KPluginMetaData> PackageLoader::listPackages(const QString &packageFormat, const QString &packageRoot)
|
||||
{
|
||||
// Note: Use QDateTime::currentSecsSinceEpoch() once we can depend on Qt 5.8
|
||||
const qint64 now = qRound64(QDateTime::currentMSecsSinceEpoch() / 1000.0);
|
||||
bool useRuntimeCache = true;
|
||||
if (now - d->pluginCacheAge > d->maxCacheAge && d->pluginCacheAge != 0) {
|
||||
// cache is old and we're not within a few seconds of startup anymore
|
||||
useRuntimeCache = false;
|
||||
d->pluginCache.clear();
|
||||
}
|
||||
|
||||
const QString cacheKey = packageFormat + QLatin1Char('.') + packageRoot;
|
||||
if (useRuntimeCache) {
|
||||
auto it = d->pluginCache.constFind(cacheKey);
|
||||
if (it != d->pluginCache.constEnd()) {
|
||||
return *it;
|
||||
}
|
||||
}
|
||||
if (d->pluginCacheAge == 0) {
|
||||
d->pluginCacheAge = now;
|
||||
}
|
||||
|
||||
QList<KPluginMetaData> lst;
|
||||
|
||||
// has been a root specified?
|
||||
QString actualRoot = packageRoot;
|
||||
|
||||
// try to take it from the package structure
|
||||
if (actualRoot.isEmpty()) {
|
||||
PackageStructure *structure = d->structures.value(packageFormat).data();
|
||||
if (!structure) {
|
||||
if (packageFormat == QLatin1String("KPackage/Generic")) {
|
||||
structure = new GenericPackage();
|
||||
} else if (packageFormat == QLatin1String("KPackage/GenericQML")) {
|
||||
structure = new GenericQMLPackage();
|
||||
} else {
|
||||
structure = loadPackageStructure(packageFormat);
|
||||
}
|
||||
}
|
||||
|
||||
if (structure) {
|
||||
d->structures.insert(packageFormat, structure);
|
||||
Package p(structure);
|
||||
actualRoot = p.defaultPackageRoot();
|
||||
}
|
||||
}
|
||||
|
||||
if (actualRoot.isEmpty()) {
|
||||
actualRoot = packageFormat;
|
||||
}
|
||||
|
||||
QSet<QString> uniqueIds;
|
||||
QStringList paths;
|
||||
if (QDir::isAbsolutePath(actualRoot)) {
|
||||
paths = QStringList(actualRoot);
|
||||
} else {
|
||||
const auto listPath = QStandardPaths::standardLocations(QStandardPaths::GenericDataLocation);
|
||||
for (const QString &path : listPath) {
|
||||
paths += path + QLatin1Char('/') + actualRoot;
|
||||
}
|
||||
}
|
||||
|
||||
for (auto const &plugindir : std::as_const(paths)) {
|
||||
QDirIterator it(plugindir, QStringList{QStringLiteral("metadata.json")}, QDir::Files, QDirIterator::Subdirectories);
|
||||
std::unordered_set<QString> dirs;
|
||||
while (it.hasNext()) {
|
||||
it.next();
|
||||
|
||||
const QString dir = it.fileInfo().absoluteDir().path();
|
||||
if (!dirs.insert(dir).second) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const QString metadataPath = it.fileInfo().absoluteFilePath();
|
||||
KPluginMetaData info = KPluginMetaData::fromJsonFile(metadataPath);
|
||||
|
||||
if (!info.isValid() || uniqueIds.contains(info.pluginId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (packageFormat.isEmpty() || readKPackageType(info) == packageFormat) {
|
||||
uniqueIds << info.pluginId();
|
||||
lst << info;
|
||||
} else {
|
||||
qInfo() << "KPackageStructure of" << info << "does not match requested format" << packageFormat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (useRuntimeCache) {
|
||||
d->pluginCache.insert(cacheKey, lst);
|
||||
}
|
||||
return lst;
|
||||
}
|
||||
|
||||
QList<KPluginMetaData> PackageLoader::listPackagesMetadata(const QString &packageFormat, const QString &packageRoot)
|
||||
{
|
||||
return listPackages(packageFormat, packageRoot);
|
||||
}
|
||||
|
||||
QList<KPluginMetaData>
|
||||
PackageLoader::findPackages(const QString &packageFormat, const QString &packageRoot, std::function<bool(const KPluginMetaData &)> filter)
|
||||
{
|
||||
QList<KPluginMetaData> lst;
|
||||
const auto lstPlugins = listPackages(packageFormat, packageRoot);
|
||||
for (auto const &plugin : lstPlugins) {
|
||||
if (!filter || filter(plugin)) {
|
||||
lst << plugin;
|
||||
}
|
||||
}
|
||||
return lst;
|
||||
}
|
||||
|
||||
KPackage::PackageStructure *PackageLoader::loadPackageStructure(const QString &packageFormat)
|
||||
{
|
||||
PackageStructure *structure = d->structures.value(packageFormat).data();
|
||||
if (!structure) {
|
||||
if (packageFormat == QLatin1String("KPackage/Generic")) {
|
||||
structure = new GenericPackage();
|
||||
d->structures.insert(packageFormat, structure);
|
||||
} else if (packageFormat == QLatin1String("KPackage/GenericQML")) {
|
||||
structure = new GenericQMLPackage();
|
||||
d->structures.insert(packageFormat, structure);
|
||||
}
|
||||
}
|
||||
|
||||
if (structure) {
|
||||
return structure;
|
||||
}
|
||||
|
||||
const KPluginMetaData metaData = structureForKPackageType(packageFormat);
|
||||
if (!metaData.isValid()) {
|
||||
qCWarning(KPACKAGE_LOG) << "Invalid metadata for package structure" << packageFormat;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto result = KPluginFactory::instantiatePlugin<PackageStructure>(metaData);
|
||||
if (!result) {
|
||||
qCWarning(KPACKAGE_LOG).noquote() << "Could not load installer for package of type" << packageFormat << "Error reported was: " << result.errorString;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
structure = result.plugin;
|
||||
|
||||
d->structures.insert(packageFormat, structure);
|
||||
|
||||
return structure;
|
||||
}
|
||||
|
||||
void PackageLoader::addKnownPackageStructure(const QString &packageFormat, KPackage::PackageStructure *structure)
|
||||
{
|
||||
d->structures.insert(packageFormat, structure);
|
||||
}
|
||||
|
||||
void PackageLoader::invalidateCache()
|
||||
{
|
||||
self()->d->maxCacheAge = -1;
|
||||
}
|
||||
|
||||
} // KPackage Namespace
|
||||
@@ -0,0 +1,132 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_LOADER_H
|
||||
#define KPACKAGE_LOADER_H
|
||||
|
||||
#include <kpackage/package.h>
|
||||
|
||||
#include <kpackage/package_export.h>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
class PackageLoaderPrivate;
|
||||
|
||||
/**
|
||||
* @class PackageLoader kpackage/packageloader.h <KPackage/PackageLoader>
|
||||
*
|
||||
* This is an abstract base class which defines an interface to which the package
|
||||
* loading logic can communicate with a parent application. The plugin loader
|
||||
* must be set before any plugins are loaded, otherwise (for safety reasons), the
|
||||
* default PackageLoader implementation will be used. The reimplemented version should
|
||||
* not do more than simply returning a loaded plugin. It should not init() it, and it should not
|
||||
* hang on to it.
|
||||
*
|
||||
* @author Ryan Rix <ry@n.rix.si>
|
||||
**/
|
||||
class KPACKAGE_EXPORT PackageLoader
|
||||
{
|
||||
public:
|
||||
/**
|
||||
* Load a Package plugin.
|
||||
*
|
||||
* @param packageFormat the format of the package to load
|
||||
* @param packagePath the package name: the path of the package relative to the
|
||||
* packageFormat root path. If not specified it will have to be set manually
|
||||
* with Package::setPath() by the caller.
|
||||
*
|
||||
* @return a Package object matching name, or an invalid package on failure
|
||||
**/
|
||||
Package loadPackage(const QString &packageFormat, const QString &packagePath = QString());
|
||||
|
||||
/**
|
||||
* List all available packages of a certain type
|
||||
*
|
||||
* @param packageFormat the format of the packages to list
|
||||
* @param packageRoot the root folder where the packages are installed.
|
||||
* If not specified the default from the packageformat will be taken.
|
||||
*
|
||||
* @return metadata for all the matching packages
|
||||
*/
|
||||
QList<KPluginMetaData> listPackages(const QString &packageFormat, const QString &packageRoot = QString());
|
||||
|
||||
/**
|
||||
* @overload
|
||||
* @since 6.0
|
||||
*/
|
||||
QList<KPluginMetaData> listPackagesMetadata(const QString &packageFormat, const QString &packageRoot = QString());
|
||||
|
||||
/**
|
||||
* List all available packages of a certain type. This should be used in case the package structure modifies the metadata or you need to access the
|
||||
* contained files of the package.
|
||||
*
|
||||
* @param packageFormat the format of the packages to list
|
||||
* @param packageRoot the root folder where the packages are installed.
|
||||
* If not specified the default from the packageformat will be taken.
|
||||
*
|
||||
* @since 6.0
|
||||
*/
|
||||
QList<Package> listKPackages(const QString &packageFormat, const QString &packageRoot = QString());
|
||||
|
||||
/**
|
||||
* List package of a certain type that match a certain filter function
|
||||
*
|
||||
* @param packageFormat the format of the packages to list
|
||||
* @param packageRoot the root folder where the packages are installed.
|
||||
* If not specified the default from the packageformat will be taken.
|
||||
* @param filter a filter function that will be called on each package:
|
||||
* will return true for the matching ones
|
||||
*
|
||||
* @return metadata for all the matching packages
|
||||
* @since 5.10
|
||||
*/
|
||||
QList<KPluginMetaData> findPackages(const QString &packageFormat,
|
||||
const QString &packageRoot = QString(),
|
||||
std::function<bool(const KPluginMetaData &)> filter = std::function<bool(const KPluginMetaData &)>());
|
||||
|
||||
/**
|
||||
* Loads a PackageStructure for a given format. The structure can then be used as
|
||||
* paramenter for a Package instance constructor
|
||||
*
|
||||
* @note The returned pointer is managed by KPackage, and should never be deleted
|
||||
*
|
||||
* @param packageFormat the package format, such as "KPackage/GenericQML"
|
||||
* @return the structure instance (ownership retained by KPackage)
|
||||
*/
|
||||
KPackage::PackageStructure *loadPackageStructure(const QString &packageFormat);
|
||||
|
||||
/**
|
||||
* Adds a new known package structure that can be used by the functions to load packages such
|
||||
* as loadPackage, findPackages etc
|
||||
* @param packageFormat the package format, such as "KPackage/GenericQML"
|
||||
* @param structure the package structure we want to be able to load packages from
|
||||
* @since 5.10
|
||||
*/
|
||||
void addKnownPackageStructure(const QString &packageFormat, KPackage::PackageStructure *structure);
|
||||
|
||||
/**
|
||||
* Return the active plugin loader
|
||||
**/
|
||||
static PackageLoader *self();
|
||||
|
||||
protected:
|
||||
PackageLoader();
|
||||
virtual ~PackageLoader();
|
||||
|
||||
private:
|
||||
friend class Package;
|
||||
friend class PackageJob;
|
||||
KPACKAGE_NO_EXPORT static void invalidateCache();
|
||||
|
||||
PackageLoaderPrivate *const d;
|
||||
Q_DISABLE_COPY(PackageLoader)
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
Q_DECLARE_METATYPE(KPackage::PackageLoader *)
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2011 Aaron Seigo <aseigo@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "packagestructure.h"
|
||||
#include "kpackage_debug.h"
|
||||
#include "packagejob.h"
|
||||
#include "private/package_p.h"
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
PackageStructure::PackageStructure(QObject *parent, const QVariantList & /*args*/)
|
||||
: QObject(parent)
|
||||
{
|
||||
Q_UNUSED(d)
|
||||
}
|
||||
|
||||
PackageStructure::~PackageStructure()
|
||||
{
|
||||
}
|
||||
|
||||
void PackageStructure::initPackage(Package * /*package*/)
|
||||
{
|
||||
}
|
||||
|
||||
void PackageStructure::pathChanged(Package * /*package*/)
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#include "moc_packagestructure.cpp"
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2011 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_PACKAGESTRUCTURE_H
|
||||
#define KPACKAGE_PACKAGESTRUCTURE_H
|
||||
|
||||
#include <QStringList>
|
||||
|
||||
#include <KPluginFactory>
|
||||
|
||||
#include <kpackage/package.h>
|
||||
#include <kpackage/package_export.h>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
/**
|
||||
* @class PackageStructure kpackage/packagestructure.h <KPackage/PackageStructure>
|
||||
*
|
||||
* This class is used to define the filesystem structure of a package type.
|
||||
* A PackageStructure is implemented as a dynamically loaded plugin, in the reimplementation
|
||||
* of initPackage the allowed fines and directories in the package are set into the package,
|
||||
* for instance:
|
||||
*
|
||||
* @code
|
||||
* package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml"));
|
||||
* package->setDefaultPackageRoot(QStringLiteral("plasma/wallpapers/"));
|
||||
* package->addDirectoryDefinition("images", QStringLiteral("images"));
|
||||
* package->addDirectoryDefinition("theme", QStringLiteral("theme"));
|
||||
* QStringList mimetypes{QStringLiteral("image/svg+xml"), QStringLiteral("image/png"), QStringLiteral("image/jpeg")};
|
||||
* package->setMimeTypes("images", mimetypes);
|
||||
* @endcode
|
||||
*/
|
||||
class KPACKAGE_EXPORT PackageStructure : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit PackageStructure(QObject *parent = nullptr, const QVariantList &args = QVariantList());
|
||||
|
||||
~PackageStructure() override;
|
||||
|
||||
/**
|
||||
* Called when a the PackageStructure should initialize a Package with the initial
|
||||
* structure. This allows setting paths before setPath is called.
|
||||
*
|
||||
* Note: one special value is "metadata" which can be set to the location of KPluginMetaData
|
||||
* compatible .json file within the package. If not defined, it is assumed that this file
|
||||
* exists under the top level directory of the package.
|
||||
*
|
||||
* @param package the Package to set up. The object is empty of all definition when
|
||||
* first passed in.
|
||||
*/
|
||||
virtual void initPackage(Package *package);
|
||||
|
||||
/**
|
||||
* Called whenever the path changes so that subclasses may take
|
||||
* package specific actions.
|
||||
*/
|
||||
virtual void pathChanged(Package *package);
|
||||
|
||||
private:
|
||||
void *d;
|
||||
};
|
||||
|
||||
} // KPackage namespace
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,46 @@
|
||||
// SPDX-FileCopyrightText: 2023 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#ifndef KPACKAGE_PACKAGESTRUCTURE_COMPAT_P_H
|
||||
#define KPACKAGE_PACKAGESTRUCTURE_COMPAT_P_H
|
||||
|
||||
#include <KPackage/Package>
|
||||
#include <QMap>
|
||||
|
||||
class KConfigGroup;
|
||||
class KDesktopFile;
|
||||
namespace KPackagePrivate
|
||||
{
|
||||
template<typename DesktopFile = KDesktopFile, typename ConfigGroup = KConfigGroup>
|
||||
/**
|
||||
* @param package KPackage which will have the desktop file metadata set to (if present)
|
||||
* @param customValueTypes Additional keys and their types that should be read from the desktop file
|
||||
*/
|
||||
void convertCompatMetaDataDesktopFile(KPackage::Package *package, const QMap<QString, QMetaType::Type> &customValueTypes = {})
|
||||
{
|
||||
if (const QString legacyPath = package->filePath("metadata"); !legacyPath.isEmpty() && legacyPath.endsWith(QLatin1String(".desktop"))) {
|
||||
DesktopFile file(legacyPath);
|
||||
const ConfigGroup grp = file.desktopGroup();
|
||||
QJsonObject kplugin{
|
||||
{QLatin1String("Name"), grp.readEntry("Name")},
|
||||
{QLatin1String("Description"), grp.readEntry("Comment")},
|
||||
{QLatin1String("Version"), grp.readEntry("X-KDE-PluginInfo-Version")},
|
||||
{QLatin1String("Id"), grp.readEntry("X-KDE-PluginInfo-Name")},
|
||||
};
|
||||
QJsonObject obj{
|
||||
{QLatin1String("KPlugin"), kplugin},
|
||||
{QLatin1String("KPackageStructure"), grp.readEntry("X-KDE-ServiceTypes")},
|
||||
};
|
||||
for (auto it = customValueTypes.begin(), end = customValueTypes.end(); it != end; ++it) {
|
||||
if (const QString value = grp.readEntry(it.key()); !value.isEmpty()) {
|
||||
if (QVariant variant(value); variant.convert(QMetaType(it.value()))) { // Make sure the type in resulting json is what the API caller needs
|
||||
obj.insert(it.key(), QJsonValue::fromVariant(variant));
|
||||
}
|
||||
}
|
||||
}
|
||||
package->setMetadata(KPluginMetaData(obj, legacyPath));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2009 Rob Scheepmaker <r.scheepmaker@student.utwente.nl>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_PACKAGE_P_H
|
||||
#define KPACKAGE_PACKAGE_P_H
|
||||
|
||||
#include "../package.h"
|
||||
|
||||
#include <QCryptographicHash>
|
||||
#include <QDir>
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
#include <QSharedData>
|
||||
#include <QString>
|
||||
#include <optional>
|
||||
namespace KPackage
|
||||
{
|
||||
class ContentStructure
|
||||
{
|
||||
public:
|
||||
ContentStructure()
|
||||
{
|
||||
}
|
||||
|
||||
ContentStructure(const ContentStructure &other)
|
||||
{
|
||||
paths = other.paths;
|
||||
mimeTypes = other.mimeTypes;
|
||||
directory = other.directory;
|
||||
required = other.required;
|
||||
}
|
||||
|
||||
ContentStructure &operator=(const ContentStructure &) = default;
|
||||
|
||||
QStringList paths;
|
||||
QStringList mimeTypes;
|
||||
bool directory = false;
|
||||
bool required = false;
|
||||
};
|
||||
|
||||
class PackagePrivate : public QSharedData
|
||||
{
|
||||
public:
|
||||
PackagePrivate();
|
||||
PackagePrivate(const PackagePrivate &other);
|
||||
~PackagePrivate();
|
||||
|
||||
PackagePrivate &operator=(const PackagePrivate &rhs);
|
||||
|
||||
void createPackageMetadata(const QString &path);
|
||||
QString unpack(const QString &filePath);
|
||||
void updateHash(const QString &basePath, const QString &subPath, const QDir &dir, QCryptographicHash &hash);
|
||||
QString fallbackFilePath(const QByteArray &key, const QString &filename = QString()) const;
|
||||
bool hasCycle(const KPackage::Package &package);
|
||||
bool isInsidePackageDir(const QString &canonicalPath) const;
|
||||
|
||||
QPointer<PackageStructure> structure;
|
||||
QString path;
|
||||
QString tempRoot;
|
||||
QStringList contentsPrefixPaths;
|
||||
QString defaultPackageRoot;
|
||||
QHash<QString, QString> discoveries;
|
||||
QHash<QByteArray, ContentStructure> contents;
|
||||
std::unique_ptr<Package> fallbackPackage;
|
||||
QStringList mimeTypes;
|
||||
std::optional<KPluginMetaData> metadata;
|
||||
bool externalPaths = false;
|
||||
bool valid = false;
|
||||
bool checkedValid = false;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,403 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "private/packagejobthread_p.h"
|
||||
#include "private/utils.h"
|
||||
|
||||
#include "config-package.h"
|
||||
#include "package.h"
|
||||
|
||||
#include <KArchive>
|
||||
#include <KLocalizedString>
|
||||
#include <KTar>
|
||||
#include <kzip.h>
|
||||
|
||||
#include "kpackage_debug.h"
|
||||
#include <QDir>
|
||||
#include <QFile>
|
||||
#include <QIODevice>
|
||||
#include <QJsonDocument>
|
||||
#include <QMimeDatabase>
|
||||
#include <QMimeType>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QUrl>
|
||||
#include <qtemporarydir.h>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
bool copyFolder(QString sourcePath, QString targetPath)
|
||||
{
|
||||
QDir source(sourcePath);
|
||||
if (!source.exists()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QDir target(targetPath);
|
||||
if (!target.exists()) {
|
||||
QString targetName = target.dirName();
|
||||
target.cdUp();
|
||||
target.mkdir(targetName);
|
||||
target = QDir(targetPath);
|
||||
}
|
||||
|
||||
const auto lstEntries = source.entryList(QDir::Files);
|
||||
for (const QString &fileName : lstEntries) {
|
||||
QString sourceFilePath = sourcePath + QDir::separator() + fileName;
|
||||
QString targetFilePath = targetPath + QDir::separator() + fileName;
|
||||
|
||||
if (!QFile::copy(sourceFilePath, targetFilePath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
const auto lstEntries2 = source.entryList(QDir::AllDirs | QDir::NoDotAndDotDot);
|
||||
for (const QString &subFolderName : lstEntries2) {
|
||||
QString sourceSubFolderPath = sourcePath + QDir::separator() + subFolderName;
|
||||
QString targetSubFolderPath = targetPath + QDir::separator() + subFolderName;
|
||||
|
||||
if (!copyFolder(sourceSubFolderPath, targetSubFolderPath)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool removeFolder(QString folderPath)
|
||||
{
|
||||
QDir folder(folderPath);
|
||||
return folder.removeRecursively();
|
||||
}
|
||||
|
||||
class PackageJobThreadPrivate
|
||||
{
|
||||
public:
|
||||
QString installPath;
|
||||
QString errorMessage;
|
||||
std::function<void()> run;
|
||||
int errorCode;
|
||||
};
|
||||
|
||||
PackageJobThread::PackageJobThread(PackageJob::OperationType type, const QString &src, const QString &dest, const KPackage::Package &package)
|
||||
: QObject()
|
||||
, QRunnable()
|
||||
{
|
||||
d = new PackageJobThreadPrivate;
|
||||
d->errorCode = KJob::NoError;
|
||||
if (type == PackageJob::Install) {
|
||||
d->run = [this, src, dest, package]() {
|
||||
install(src, dest, package);
|
||||
};
|
||||
} else if (type == PackageJob::Update) {
|
||||
d->run = [this, src, dest, package]() {
|
||||
update(src, dest, package);
|
||||
};
|
||||
} else if (type == PackageJob::Uninstall) {
|
||||
const QString packagePath = package.path();
|
||||
d->run = [this, packagePath]() {
|
||||
uninstall(packagePath);
|
||||
};
|
||||
|
||||
} else {
|
||||
Q_UNREACHABLE();
|
||||
}
|
||||
}
|
||||
|
||||
PackageJobThread::~PackageJobThread()
|
||||
{
|
||||
delete d;
|
||||
}
|
||||
|
||||
void PackageJobThread::run()
|
||||
{
|
||||
Q_ASSERT(d->run);
|
||||
d->run();
|
||||
}
|
||||
bool PackageJobThread::install(const QString &src, const QString &dest, const Package &package)
|
||||
{
|
||||
bool ok = installPackage(src, dest, package, PackageJob::Install);
|
||||
Q_EMIT installPathChanged(d->installPath);
|
||||
Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
|
||||
return ok;
|
||||
}
|
||||
|
||||
static QString resolveHandler(const QString &scheme)
|
||||
{
|
||||
QString envOverride = qEnvironmentVariable("KPACKAGE_DEP_RESOLVERS_PATH");
|
||||
QStringList searchDirs;
|
||||
if (!envOverride.isEmpty()) {
|
||||
searchDirs.push_back(envOverride);
|
||||
}
|
||||
searchDirs.append(QStringLiteral(KDE_INSTALL_FULL_LIBEXECDIR_KF "/kpackagehandlers"));
|
||||
// We have to use QStandardPaths::findExecutable here to handle the .exe suffix on Windows.
|
||||
return QStandardPaths::findExecutable(scheme + QLatin1String("handler"), searchDirs);
|
||||
}
|
||||
|
||||
bool PackageJobThread::installDependency(const QUrl &destUrl)
|
||||
{
|
||||
auto handler = resolveHandler(destUrl.scheme());
|
||||
if (handler.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QProcess process;
|
||||
process.setProgram(handler);
|
||||
process.setArguments({destUrl.toString()});
|
||||
process.setProcessChannelMode(QProcess::ForwardedChannels);
|
||||
process.start();
|
||||
process.waitForFinished();
|
||||
|
||||
return process.exitCode() == 0;
|
||||
}
|
||||
|
||||
bool PackageJobThread::installPackage(const QString &src, const QString &dest, const Package &package, PackageJob::OperationType operation)
|
||||
{
|
||||
QDir root(dest);
|
||||
if (!root.exists()) {
|
||||
QDir().mkpath(dest);
|
||||
if (!root.exists()) {
|
||||
d->errorMessage = i18n("Could not create package root directory: %1", dest);
|
||||
d->errorCode = PackageJob::JobError::RootCreationError;
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not create package root directory: " << dest;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
QFileInfo fileInfo(src);
|
||||
if (!fileInfo.exists()) {
|
||||
d->errorMessage = i18n("No such file: %1", src);
|
||||
d->errorCode = PackageJob::JobError::PackageFileNotFoundError;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString path;
|
||||
QTemporaryDir tempdir;
|
||||
bool archivedPackage = false;
|
||||
|
||||
if (fileInfo.isDir()) {
|
||||
// we have a directory, so let's just install what is in there
|
||||
path = src;
|
||||
// make sure we end in a slash!
|
||||
if (!path.endsWith(QLatin1Char('/'))) {
|
||||
path.append(QLatin1Char('/'));
|
||||
}
|
||||
} else {
|
||||
KArchive *archive = nullptr;
|
||||
QMimeDatabase db;
|
||||
QMimeType mimetype = db.mimeTypeForFile(src);
|
||||
if (mimetype.inherits(QStringLiteral("application/zip"))) {
|
||||
archive = new KZip(src);
|
||||
} else if (mimetype.inherits(QStringLiteral("application/x-compressed-tar")) || //
|
||||
mimetype.inherits(QStringLiteral("application/x-tar")) || //
|
||||
mimetype.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || //
|
||||
mimetype.inherits(QStringLiteral("application/x-xz")) || //
|
||||
mimetype.inherits(QStringLiteral("application/x-lzma"))) {
|
||||
archive = new KTar(src);
|
||||
} else {
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not open package file, unsupported archive format:" << src << mimetype.name();
|
||||
d->errorMessage = i18n("Could not open package file, unsupported archive format: %1 %2", src, mimetype.name());
|
||||
d->errorCode = PackageJob::JobError::UnsupportedArchiveFormatError;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!archive->open(QIODevice::ReadOnly)) {
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not open package file:" << src;
|
||||
delete archive;
|
||||
d->errorMessage = i18n("Could not open package file: %1", src);
|
||||
d->errorCode = PackageJob::JobError::PackageOpenError;
|
||||
return false;
|
||||
}
|
||||
|
||||
archivedPackage = true;
|
||||
path = tempdir.path() + QLatin1Char('/');
|
||||
|
||||
d->installPath = path;
|
||||
|
||||
const KArchiveDirectory *source = archive->directory();
|
||||
source->copyTo(path);
|
||||
|
||||
QStringList entries = source->entries();
|
||||
if (entries.count() == 1) {
|
||||
const KArchiveEntry *entry = source->entry(entries[0]);
|
||||
if (entry->isDirectory()) {
|
||||
path = path + entry->name() + QLatin1Char('/');
|
||||
}
|
||||
}
|
||||
|
||||
delete archive;
|
||||
}
|
||||
|
||||
Package copyPackage = package;
|
||||
copyPackage.setPath(path);
|
||||
if (!copyPackage.isValid()) {
|
||||
d->errorMessage = i18n("Package is not considered valid");
|
||||
d->errorCode = PackageJob::JobError::InvalidPackageStructure;
|
||||
return false;
|
||||
}
|
||||
|
||||
KPluginMetaData meta = copyPackage.metadata(); // The packagestructure might have set the metadata, so use that
|
||||
QString pluginName = meta.pluginId().isEmpty() ? QFileInfo(src).baseName() : meta.pluginId();
|
||||
qCDebug(KPACKAGE_LOG) << "pluginname: " << meta.pluginId();
|
||||
if (pluginName == QLatin1String("metadata")) {
|
||||
// qCWarning(KPACKAGE_LOG) << "Package plugin id not specified";
|
||||
d->errorMessage = i18n("Package plugin id not specified: %1", src);
|
||||
d->errorCode = PackageJob::JobError::PluginIdInvalidError;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Ensure that package names are safe so package uninstall can't inject
|
||||
// bad characters into the paths used for removal.
|
||||
const QRegularExpression validatePluginName(QStringLiteral("^[\\w\\-\\.]+$")); // Only allow letters, numbers, underscore and period.
|
||||
if (!validatePluginName.match(pluginName).hasMatch()) {
|
||||
// qCDebug(KPACKAGE_LOG) << "Package plugin id " << pluginName << "contains invalid characters";
|
||||
d->errorMessage = i18n("Package plugin id %1 contains invalid characters", pluginName);
|
||||
d->errorCode = PackageJob::JobError::PluginIdInvalidError;
|
||||
return false;
|
||||
}
|
||||
|
||||
QString targetName = dest;
|
||||
if (targetName[targetName.size() - 1] != QLatin1Char('/')) {
|
||||
targetName.append(QLatin1Char('/'));
|
||||
}
|
||||
targetName.append(pluginName);
|
||||
|
||||
if (QFile::exists(targetName)) {
|
||||
if (operation == PackageJob::Update) {
|
||||
KPluginMetaData oldMeta;
|
||||
if (QString jsonPath = targetName + QLatin1String("/metadata.json"); QFileInfo::exists(jsonPath)) {
|
||||
oldMeta = KPluginMetaData::fromJsonFile(jsonPath);
|
||||
}
|
||||
|
||||
if (readKPackageType(oldMeta) != readKPackageType(meta)) {
|
||||
d->errorMessage = i18n("The new package has a different type from the old version already installed.");
|
||||
d->errorCode = PackageJob::JobError::UpdatePackageTypeMismatchError;
|
||||
} else if (isVersionNewer(oldMeta.version(), meta.version())) {
|
||||
const bool ok = uninstallPackage(targetName);
|
||||
if (!ok) {
|
||||
d->errorMessage = i18n("Impossible to remove the old installation of %1 located at %2. error: %3", pluginName, targetName, d->errorMessage);
|
||||
d->errorCode = PackageJob::JobError::OldVersionRemovalError;
|
||||
}
|
||||
} else {
|
||||
d->errorMessage = i18n("Not installing version %1 of %2. Version %3 already installed.", meta.version(), meta.pluginId(), oldMeta.version());
|
||||
d->errorCode = PackageJob::JobError::NewerVersionAlreadyInstalledError;
|
||||
}
|
||||
} else {
|
||||
d->errorMessage = i18n("%1 already exists", targetName);
|
||||
d->errorCode = PackageJob::JobError::PackageAlreadyInstalledError;
|
||||
}
|
||||
|
||||
if (d->errorCode != KJob::NoError) {
|
||||
d->installPath = targetName;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// install dependencies
|
||||
const QStringList optionalDependencies{QStringLiteral("sddmtheme.knsrc")};
|
||||
const QStringList dependencies = meta.value(QStringLiteral("X-KPackage-Dependencies"), QStringList());
|
||||
for (const QString &dep : dependencies) {
|
||||
QUrl depUrl(dep);
|
||||
const QString knsrcFilePath = QStandardPaths::locate(QStandardPaths::GenericDataLocation, QLatin1String("knsrcfiles/") + depUrl.host());
|
||||
if (knsrcFilePath.isEmpty() && optionalDependencies.contains(depUrl.host())) {
|
||||
qWarning() << "Skipping depdendency due to knsrc files being missing" << depUrl;
|
||||
continue;
|
||||
}
|
||||
if (!installDependency(depUrl)) {
|
||||
d->errorMessage = i18n("Could not install dependency: '%1'", dep);
|
||||
d->errorCode = PackageJob::JobError::PackageCopyError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (archivedPackage) {
|
||||
// it's in a temp dir, so just move it over.
|
||||
const bool ok = copyFolder(path, targetName);
|
||||
removeFolder(path);
|
||||
if (!ok) {
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not move package to destination:" << targetName;
|
||||
d->errorMessage = i18n("Could not move package to destination: %1", targetName);
|
||||
d->errorCode = PackageJob::JobError::PackageMoveError;
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// it's a directory containing the stuff, so copy the contents rather
|
||||
// than move them
|
||||
const bool ok = copyFolder(path, targetName);
|
||||
if (!ok) {
|
||||
// qCWarning(KPACKAGE_LOG) << "Could not copy package to destination:" << targetName;
|
||||
d->errorMessage = i18n("Could not copy package to destination: %1", targetName);
|
||||
d->errorCode = PackageJob::JobError::PackageCopyError;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (archivedPackage) {
|
||||
// no need to remove the temp dir (which has been successfully moved if it's an archive)
|
||||
tempdir.setAutoRemove(false);
|
||||
}
|
||||
|
||||
d->installPath = targetName;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool PackageJobThread::update(const QString &src, const QString &dest, const Package &package)
|
||||
{
|
||||
bool ok = installPackage(src, dest, package, PackageJob::Update);
|
||||
Q_EMIT installPathChanged(d->installPath);
|
||||
Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool PackageJobThread::uninstall(const QString &packagePath)
|
||||
{
|
||||
bool ok = uninstallPackage(packagePath);
|
||||
// Do not emit the install path changed, information about the removed package might be useful for consumers
|
||||
// qCDebug(KPACKAGE_LOG) << "Thread: installFinished" << ok;
|
||||
Q_EMIT jobThreadFinished(ok, errorCode(), d->errorMessage);
|
||||
return ok;
|
||||
}
|
||||
|
||||
bool PackageJobThread::uninstallPackage(const QString &packagePath)
|
||||
{
|
||||
if (!QFile::exists(packagePath)) {
|
||||
d->errorMessage = packagePath.isEmpty() ? i18n("package path was deleted manually") : i18n("%1 does not exist", packagePath);
|
||||
d->errorCode = PackageJob::JobError::PackageFileNotFoundError;
|
||||
return false;
|
||||
}
|
||||
QString pkg;
|
||||
QString root;
|
||||
{
|
||||
// TODO KF6 remove, pass in packageroot, type and pluginName separately?
|
||||
QStringList ps = packagePath.split(QLatin1Char('/'));
|
||||
int ix = ps.count() - 1;
|
||||
if (packagePath.endsWith(QLatin1Char('/'))) {
|
||||
ix = ps.count() - 2;
|
||||
}
|
||||
pkg = ps[ix];
|
||||
ps.pop_back();
|
||||
root = ps.join(QLatin1Char('/'));
|
||||
}
|
||||
|
||||
bool ok = removeFolder(packagePath);
|
||||
if (!ok) {
|
||||
d->errorMessage = i18n("Could not delete package from: %1", packagePath);
|
||||
d->errorCode = PackageJob::JobError::PackageUninstallError;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
PackageJob::JobError PackageJobThread::errorCode() const
|
||||
{
|
||||
return static_cast<PackageJob::JobError>(d->errorCode);
|
||||
}
|
||||
|
||||
} // namespace KPackage
|
||||
|
||||
#include "moc_packagejobthread_p.cpp"
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_PACKAGEJOBTHREAD_P_H
|
||||
#define KPACKAGE_PACKAGEJOBTHREAD_P_H
|
||||
|
||||
#include "package.h"
|
||||
#include "packagejob.h"
|
||||
#include <QRunnable>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
class PackageJobThreadPrivate;
|
||||
|
||||
bool indexDirectory(const QString &dir, const QString &dest);
|
||||
|
||||
class PackageJobThread : public QObject, public QRunnable
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit PackageJobThread(PackageJob::OperationType type, const QString &src, const QString &dest, const KPackage::Package &package);
|
||||
~PackageJobThread() override;
|
||||
|
||||
void run() override;
|
||||
|
||||
bool install(const QString &src, const QString &dest, const Package &package);
|
||||
bool update(const QString &src, const QString &dest, const Package &package);
|
||||
bool uninstall(const QString &packagePath);
|
||||
|
||||
PackageJob::JobError errorCode() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
void jobThreadFinished(bool success, PackageJob::JobError errorCode, const QString &errorMessage = QString());
|
||||
void percentChanged(int percent);
|
||||
void error(const QString &errorMessage);
|
||||
void installPathChanged(const QString &installPath);
|
||||
|
||||
private:
|
||||
// OperationType says whether we want to install, update or any
|
||||
// new similar operation it will be expanded
|
||||
bool installDependency(const QUrl &src);
|
||||
bool installPackage(const QString &src, const QString &dest, const Package &package, PackageJob::OperationType operation);
|
||||
bool uninstallPackage(const QString &packagePath);
|
||||
PackageJobThreadPrivate *d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2010 Ryan Rix <ry@n.rix.si>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef KPACKAGE_PACKAGELOADER_P_H
|
||||
#define KPACKAGE_PACKAGELOADER_P_H
|
||||
|
||||
#include "packagestructure.h"
|
||||
#include <KPluginMetaData>
|
||||
#include <QHash>
|
||||
#include <QPointer>
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
class PackageLoaderPrivate
|
||||
{
|
||||
public:
|
||||
QHash<QString, QPointer<PackageStructure>> structures;
|
||||
// We only use this cache during start of the process to speed up many consecutive calls
|
||||
// After that, we're too afraid to produce race conditions and it's not that time-critical anyway
|
||||
// the 20 seconds here means that the cache is only used within 20sec during startup, after that,
|
||||
// complexity goes up and we'd have to update the cache in order to avoid subtle bugs
|
||||
// just not using the cache is way easier then, since it doesn't make *that* much of a difference,
|
||||
// anyway
|
||||
int maxCacheAge = 20;
|
||||
qint64 pluginCacheAge = 0;
|
||||
QHash<QString, QList<KPluginMetaData>> pluginCache;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007-2009 Aaron Seigo <aseigo@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "packages_p.h"
|
||||
|
||||
#include <math.h>
|
||||
|
||||
#include <KLocalizedString>
|
||||
|
||||
#include "kpackage/package.h"
|
||||
|
||||
void GenericPackage::initPackage(KPackage::Package *package)
|
||||
{
|
||||
KPackage::PackageStructure::initPackage(package);
|
||||
|
||||
package->setDefaultPackageRoot(QStringLiteral("kpackage/generic/"));
|
||||
|
||||
package->addDirectoryDefinition("images", QStringLiteral("images"));
|
||||
package->addDirectoryDefinition("theme", QStringLiteral("theme"));
|
||||
const QStringList mimetypes{QStringLiteral("image/svg+xml"), QStringLiteral("image/png"), QStringLiteral("image/jpeg")};
|
||||
package->setMimeTypes("images", mimetypes);
|
||||
package->setMimeTypes("theme", mimetypes);
|
||||
|
||||
package->addDirectoryDefinition("config", QStringLiteral("config"));
|
||||
package->setMimeTypes("config", QStringList{QStringLiteral("text/xml")});
|
||||
|
||||
package->addDirectoryDefinition("ui", QStringLiteral("ui"));
|
||||
|
||||
package->addDirectoryDefinition("data", QStringLiteral("data"));
|
||||
|
||||
package->addDirectoryDefinition("scripts", QStringLiteral("code"));
|
||||
package->setMimeTypes("scripts", QStringList{QStringLiteral("text/plain")});
|
||||
|
||||
package->addDirectoryDefinition("translations", QStringLiteral("locale"));
|
||||
}
|
||||
|
||||
void GenericQMLPackage::initPackage(KPackage::Package *package)
|
||||
{
|
||||
GenericPackage::initPackage(package);
|
||||
|
||||
package->addFileDefinition("mainscript", QStringLiteral("ui/main.qml"));
|
||||
package->setRequired("mainscript", true);
|
||||
package->setDefaultPackageRoot(QStringLiteral("kpackage/genericqml/"));
|
||||
}
|
||||
|
||||
#include "moc_packages_p.cpp"
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2007 Aaron Seigo <aseigo@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kpackage/packagestructure.h"
|
||||
|
||||
class GenericPackage : public KPackage::PackageStructure
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
void initPackage(KPackage::Package *package) override;
|
||||
};
|
||||
|
||||
class GenericQMLPackage : public GenericPackage
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
void initPackage(KPackage::Package *package) override;
|
||||
};
|
||||
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2021 Alexander Lohnau <alexander.lohnau@gmx.de>
|
||||
// SPDX-FileCopyrightText: 2021 Nicolas Fella <nicolas.fella@gmx.de>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "kpackage_debug.h"
|
||||
#include <KPluginMetaData>
|
||||
#include <QString>
|
||||
#include <QVersionNumber>
|
||||
|
||||
inline QString readKPackageType(const KPluginMetaData &metaData)
|
||||
{
|
||||
return metaData.value(QStringLiteral("KPackageStructure"));
|
||||
}
|
||||
|
||||
inline KPluginMetaData structureForKPackageType(const QString &packageFormat)
|
||||
{
|
||||
const QString guessedPath = QStringLiteral("kf6/packagestructure/") + QString(packageFormat).toLower().replace(QLatin1Char('/'), QLatin1Char('_'));
|
||||
KPluginMetaData guessedData(guessedPath);
|
||||
if (guessedData.isValid() && readKPackageType(guessedData) == packageFormat) {
|
||||
return guessedData;
|
||||
}
|
||||
qCDebug(KPACKAGE_LOG) << "Could not find package structure for" << packageFormat << "by plugin path. The guessed path was" << guessedPath;
|
||||
|
||||
const QList<KPluginMetaData> plugins =
|
||||
KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure"), [packageFormat](const KPluginMetaData &metaData) {
|
||||
return readKPackageType(metaData) == packageFormat;
|
||||
});
|
||||
return plugins.isEmpty() ? KPluginMetaData() : plugins.first();
|
||||
}
|
||||
|
||||
inline bool isVersionNewer(const QString &version1, const QString &version2)
|
||||
{
|
||||
const auto v1 = QVersionNumber::fromString(version1);
|
||||
const auto v2 = QVersionNumber::fromString(version2);
|
||||
return v2 > v1;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
add_executable(kpackagetool6)
|
||||
|
||||
ecm_mark_nongui_executable(kpackagetool6)
|
||||
|
||||
target_sources(kpackagetool6 PRIVATE
|
||||
main.cpp
|
||||
kpackagetool.cpp
|
||||
)
|
||||
ecm_qt_declare_logging_category(kpackagetool6
|
||||
HEADER kpackage_debug.h
|
||||
IDENTIFIER KPACKAGE_LOG
|
||||
CATEGORY_NAME kf.package
|
||||
)
|
||||
target_link_libraries(kpackagetool6 KF6::Archive KF6::Package KF6::I18n KF6::CoreAddons)
|
||||
|
||||
install(TARGETS kpackagetool6 EXPORT KF6PackageToolsTargets ${KF_INSTALL_TARGETS_DEFAULT_ARGS})
|
||||
@@ -0,0 +1,558 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2012-2017 Sebastian Kügler <sebas@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#include "kpackagetool.h"
|
||||
|
||||
#include <KAboutData>
|
||||
#include <KLocalizedString>
|
||||
#include <KShell>
|
||||
#include <QDebug>
|
||||
|
||||
#include <KJob>
|
||||
#include <kpackage/package.h>
|
||||
#include <kpackage/packageloader.h>
|
||||
#include <kpackage/packagestructure.h>
|
||||
#include <kpackage/private/utils.h>
|
||||
|
||||
#include <QCommandLineParser>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QList>
|
||||
#include <QMap>
|
||||
#include <QRegularExpression>
|
||||
#include <QStandardPaths>
|
||||
#include <QStringList>
|
||||
#include <QTimer>
|
||||
#include <QUrl>
|
||||
#include <QXmlStreamWriter>
|
||||
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
|
||||
#include "options.h"
|
||||
|
||||
#include "../kpackage/config-package.h"
|
||||
|
||||
#include "kpackage_debug.h"
|
||||
|
||||
Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cout, (stdout))
|
||||
Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cerr, (stderr))
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
class PackageToolPrivate
|
||||
{
|
||||
public:
|
||||
QString packageRoot;
|
||||
QString packageFile;
|
||||
QString package;
|
||||
QString kpackageType = QStringLiteral("KPackage/Generic");
|
||||
KPluginMetaData metadata;
|
||||
QString installPath;
|
||||
void output(const QString &msg);
|
||||
QStringList packages(const QString &type, const QString &path = QString());
|
||||
void renderTypeTable(const QMap<QString, QString> &plugins);
|
||||
void listTypes();
|
||||
void coutput(const QString &msg);
|
||||
void cerror(const QString &msg);
|
||||
QCommandLineParser *parser = nullptr;
|
||||
};
|
||||
|
||||
PackageTool::PackageTool(int &argc, char **argv, QCommandLineParser *parser)
|
||||
: QCoreApplication(argc, argv)
|
||||
{
|
||||
d = new PackageToolPrivate;
|
||||
d->parser = parser;
|
||||
QTimer::singleShot(0, this, &PackageTool::runMain);
|
||||
}
|
||||
|
||||
PackageTool::~PackageTool()
|
||||
{
|
||||
delete d;
|
||||
}
|
||||
|
||||
void PackageTool::runMain()
|
||||
{
|
||||
if (d->parser->isSet(Options::hash())) {
|
||||
const QString path = d->parser->value(Options::hash());
|
||||
KPackage::PackageStructure structure;
|
||||
KPackage::Package package(&structure);
|
||||
package.setPath(path);
|
||||
const QString hash = QString::fromLocal8Bit(package.cryptographicHash(QCryptographicHash::Sha1));
|
||||
if (hash.isEmpty()) {
|
||||
d->coutput(i18n("Failed to generate a Package hash for %1", path));
|
||||
exit(9);
|
||||
} else {
|
||||
d->coutput(i18n("SHA1 hash for Package at %1: '%2'", package.path(), hash));
|
||||
exit(0);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (d->parser->isSet(Options::listTypes())) {
|
||||
d->listTypes();
|
||||
exit(0);
|
||||
return;
|
||||
}
|
||||
|
||||
if (d->parser->isSet(Options::type())) {
|
||||
d->kpackageType = d->parser->value(Options::type());
|
||||
}
|
||||
d->packageRoot = KPackage::PackageLoader::self()->loadPackage(d->kpackageType).defaultPackageRoot();
|
||||
|
||||
if (d->parser->isSet(Options::remove())) {
|
||||
d->package = d->parser->value(Options::remove());
|
||||
} else if (d->parser->isSet(Options::upgrade())) {
|
||||
d->package = d->parser->value(Options::upgrade());
|
||||
} else if (d->parser->isSet(Options::install())) {
|
||||
d->package = d->parser->value(Options::install());
|
||||
} else if (d->parser->isSet(Options::show())) {
|
||||
d->package = d->parser->value(Options::show());
|
||||
} else if (d->parser->isSet(Options::appstream())) {
|
||||
d->package = d->parser->value(Options::appstream());
|
||||
}
|
||||
|
||||
if (!QDir::isAbsolutePath(d->package)) {
|
||||
d->packageFile = QDir(QDir::currentPath() + QLatin1Char('/') + d->package).absolutePath();
|
||||
d->packageFile = QFileInfo(d->packageFile).canonicalFilePath();
|
||||
if (d->parser->isSet(Options::upgrade())) {
|
||||
d->package = d->packageFile;
|
||||
}
|
||||
} else {
|
||||
d->packageFile = d->package;
|
||||
}
|
||||
|
||||
if (!PackageLoader::self()->loadPackageStructure(d->kpackageType)) {
|
||||
qWarning() << "Package type" << d->kpackageType << "not found";
|
||||
}
|
||||
|
||||
if (d->parser->isSet(Options::show())) {
|
||||
const QString pluginName = d->package;
|
||||
showPackageInfo(pluginName);
|
||||
return;
|
||||
} else if (d->parser->isSet(Options::appstream())) {
|
||||
const QString pluginName = d->package;
|
||||
showAppstreamInfo(pluginName);
|
||||
return;
|
||||
}
|
||||
|
||||
if (d->parser->isSet(Options::list())) {
|
||||
QString packageRoot = resolvePackageRootWithOptions();
|
||||
d->coutput(i18n("Listing KPackageType: %1 in %2", d->kpackageType, packageRoot));
|
||||
listPackages(d->kpackageType, packageRoot);
|
||||
exit(0);
|
||||
} else {
|
||||
// install, remove or upgrade
|
||||
d->packageRoot = resolvePackageRootWithOptions();
|
||||
|
||||
if (d->parser->isSet(Options::remove()) || d->parser->isSet(Options::upgrade())) {
|
||||
QString pkgPath;
|
||||
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
|
||||
pkg.setPath(d->package);
|
||||
if (pkg.isValid()) {
|
||||
pkgPath = pkg.path();
|
||||
if (pkgPath.isEmpty() && !d->packageFile.isEmpty()) {
|
||||
pkgPath = d->packageFile;
|
||||
}
|
||||
}
|
||||
if (pkgPath.isEmpty()) {
|
||||
pkgPath = d->package;
|
||||
}
|
||||
QString _p = d->packageRoot;
|
||||
if (!_p.endsWith(QLatin1Char('/'))) {
|
||||
_p.append(QLatin1Char('/'));
|
||||
}
|
||||
_p.append(d->package);
|
||||
|
||||
if (!d->parser->isSet(Options::type())) {
|
||||
d->kpackageType = readKPackageType(pkg.metadata());
|
||||
}
|
||||
|
||||
QString pluginName;
|
||||
if (pkg.metadata().isValid()) {
|
||||
d->metadata = pkg.metadata();
|
||||
if (!d->metadata.isValid()) {
|
||||
pluginName = d->package;
|
||||
} else if (!d->metadata.isValid() && d->metadata.pluginId().isEmpty()) {
|
||||
// plugin id given in command line
|
||||
pluginName = d->package;
|
||||
} else {
|
||||
// Parameter was a plasma package, get plugin id from the package
|
||||
pluginName = d->metadata.pluginId();
|
||||
}
|
||||
}
|
||||
QStringList installed = d->packages(d->kpackageType);
|
||||
|
||||
// Uninstalling ...
|
||||
if (installed.contains(pluginName)) { // Assume it's a plugin id
|
||||
KPackage::PackageJob *uninstallJob = KPackage::PackageJob::uninstall(d->kpackageType, pluginName, d->packageRoot);
|
||||
connect(uninstallJob, &KPackage::PackageJob::finished, this, [uninstallJob, this]() {
|
||||
packageUninstalled(uninstallJob);
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
d->coutput(i18n("Error: Plugin %1 is not installed.", pluginName));
|
||||
exit(2);
|
||||
}
|
||||
}
|
||||
if (d->parser->isSet(Options::install())) {
|
||||
auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
|
||||
connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
|
||||
packageInstalled(installJob);
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (d->package.isEmpty()) {
|
||||
qWarning() << i18nc(
|
||||
"No option was given, this is the error message telling the user he needs at least one, do not translate install, remove, upgrade nor list",
|
||||
"One of install, remove, upgrade or list is required.");
|
||||
exit(6);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void PackageToolPrivate::coutput(const QString &msg)
|
||||
{
|
||||
*cout << msg << '\n';
|
||||
(*cout).flush();
|
||||
}
|
||||
|
||||
void PackageToolPrivate::cerror(const QString &msg)
|
||||
{
|
||||
*cerr << msg << '\n';
|
||||
(*cerr).flush();
|
||||
}
|
||||
|
||||
QStringList PackageToolPrivate::packages(const QString &type, const QString &path)
|
||||
{
|
||||
QStringList result;
|
||||
const QList<KPluginMetaData> dataList = KPackage::PackageLoader::self()->listPackages(type, path);
|
||||
for (const KPluginMetaData &data : dataList) {
|
||||
if (!result.contains(data.pluginId())) {
|
||||
result << data.pluginId();
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
void PackageTool::showPackageInfo(const QString &pluginName)
|
||||
{
|
||||
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
|
||||
pkg.setDefaultPackageRoot(d->packageRoot);
|
||||
|
||||
if (QFile::exists(d->packageFile)) {
|
||||
pkg.setPath(d->packageFile);
|
||||
} else {
|
||||
pkg.setPath(pluginName);
|
||||
}
|
||||
|
||||
KPluginMetaData i = pkg.metadata();
|
||||
if (!i.isValid()) {
|
||||
*cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
|
||||
exit(3);
|
||||
return;
|
||||
}
|
||||
d->coutput(i18n("Showing info for package: %1", pluginName));
|
||||
d->coutput(i18n(" Name : %1", i.name()));
|
||||
d->coutput(i18n(" Description: %1", i.description()));
|
||||
d->coutput(i18n(" Plugin : %1", i.pluginId()));
|
||||
auto const authors = i.authors();
|
||||
QStringList authorNames;
|
||||
for (const KAboutPerson &author : authors) {
|
||||
authorNames << author.name();
|
||||
}
|
||||
d->coutput(i18n(" Author : %1", authorNames.join(QLatin1String(", "))));
|
||||
d->coutput(i18n(" Path : %1", pkg.path()));
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
bool translateKPluginToAppstream(const QString &tagName,
|
||||
const QString &configField,
|
||||
const QJsonObject &configObject,
|
||||
QXmlStreamWriter &writer,
|
||||
bool canEndWithDot)
|
||||
{
|
||||
const QRegularExpression rx(QStringLiteral("%1\\[(.*)\\]").arg(configField));
|
||||
const QJsonValue native = configObject.value(configField);
|
||||
if (native.isUndefined()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
QString content = native.toString();
|
||||
if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
|
||||
content.chop(1);
|
||||
}
|
||||
writer.writeTextElement(tagName, content);
|
||||
for (auto it = configObject.begin(), itEnd = configObject.end(); it != itEnd; ++it) {
|
||||
const auto match = rx.match(it.key());
|
||||
|
||||
if (match.hasMatch()) {
|
||||
QString content = it->toString();
|
||||
if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
|
||||
content.chop(1);
|
||||
}
|
||||
|
||||
writer.writeStartElement(tagName);
|
||||
writer.writeAttribute(QStringLiteral("xml:lang"), match.captured(1));
|
||||
writer.writeCharacters(content);
|
||||
writer.writeEndElement();
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void PackageTool::showAppstreamInfo(const QString &pluginName)
|
||||
{
|
||||
KPluginMetaData i;
|
||||
// if the path passed is an absolute path, and a metadata file is found under it, use that metadata file to generate the appstream info.
|
||||
// This can happen in the case an application wanting to support kpackage based extensions includes in the same project both the packagestructure plugin and
|
||||
// the packages themselves. In that case at build time the packagestructure plugin wouldn't be installed yet
|
||||
|
||||
if (QFile::exists(pluginName + QStringLiteral("/metadata.json"))) {
|
||||
i = KPluginMetaData::fromJsonFile(pluginName + QStringLiteral("/metadata.json"));
|
||||
} else {
|
||||
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType);
|
||||
|
||||
pkg.setDefaultPackageRoot(d->packageRoot);
|
||||
|
||||
if (QFile::exists(d->packageFile)) {
|
||||
pkg.setPath(d->packageFile);
|
||||
} else {
|
||||
pkg.setPath(pluginName);
|
||||
}
|
||||
|
||||
i = pkg.metadata();
|
||||
}
|
||||
|
||||
if (!i.isValid()) {
|
||||
*cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
|
||||
std::exit(3);
|
||||
return;
|
||||
}
|
||||
QString parentApp = i.value(QLatin1String("X-KDE-ParentApp"));
|
||||
|
||||
if (i.value(QStringLiteral("NoDisplay"), false)) {
|
||||
std::exit(0);
|
||||
}
|
||||
|
||||
QXmlStreamAttributes componentAttributes;
|
||||
if (!parentApp.isEmpty()) {
|
||||
componentAttributes << QXmlStreamAttribute(QLatin1String("type"), QLatin1String("addon"));
|
||||
}
|
||||
|
||||
// Compatibility: without appstream-metainfo-output argument we print the XML output to STDOUT
|
||||
// with the argument we'll print to the defined path.
|
||||
// TODO: in KF6 we should switch to argument-only.
|
||||
QIODevice *outputDevice = cout->device();
|
||||
std::unique_ptr<QFile> outputFile;
|
||||
const auto outputPath = d->parser->value(Options::appstreamOutput());
|
||||
if (!outputPath.isEmpty()) {
|
||||
auto outputUrl = QUrl::fromUserInput(outputPath);
|
||||
outputFile.reset(new QFile(outputUrl.toLocalFile()));
|
||||
if (!outputFile->open(QFile::WriteOnly | QFile::Text)) {
|
||||
*cerr << "Failed to open output file for writing.";
|
||||
exit(1);
|
||||
}
|
||||
outputDevice = outputFile.get();
|
||||
}
|
||||
|
||||
if (i.description().isEmpty()) {
|
||||
*cerr << "Error: description missing, will result in broken appdata field as <summary/> is mandatory at " << QFileInfo(i.fileName()).absoluteFilePath();
|
||||
std::exit(10);
|
||||
}
|
||||
|
||||
QXmlStreamWriter writer(outputDevice);
|
||||
writer.setAutoFormatting(true);
|
||||
writer.writeStartDocument();
|
||||
writer.writeStartElement(QStringLiteral("component"));
|
||||
writer.writeAttributes(componentAttributes);
|
||||
|
||||
writer.writeTextElement(QStringLiteral("id"), i.pluginId());
|
||||
if (!parentApp.isEmpty()) {
|
||||
writer.writeTextElement(QStringLiteral("extends"), parentApp);
|
||||
}
|
||||
|
||||
const QJsonObject rootObject = i.rawData()[QStringLiteral("KPlugin")].toObject();
|
||||
translateKPluginToAppstream(QStringLiteral("name"), QStringLiteral("Name"), rootObject, writer, false);
|
||||
translateKPluginToAppstream(QStringLiteral("summary"), QStringLiteral("Description"), rootObject, writer, false);
|
||||
if (!i.website().isEmpty()) {
|
||||
writer.writeStartElement(QStringLiteral("url"));
|
||||
writer.writeAttribute(QStringLiteral("type"), QStringLiteral("homepage"));
|
||||
writer.writeCharacters(i.website());
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
if (i.pluginId().startsWith(QLatin1String("org.kde."))) {
|
||||
writer.writeStartElement(QStringLiteral("url"));
|
||||
writer.writeAttribute(QStringLiteral("type"), QStringLiteral("donation"));
|
||||
writer.writeCharacters(QStringLiteral("https://www.kde.org/donate.php?app=%1").arg(i.pluginId()));
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
const auto authors = i.authors();
|
||||
if (!authors.isEmpty()) {
|
||||
QStringList authorsText;
|
||||
authorsText.reserve(authors.size());
|
||||
for (const auto &author : authors) {
|
||||
authorsText += QStringLiteral("%1").arg(author.name());
|
||||
}
|
||||
writer.writeStartElement(QStringLiteral("developer"));
|
||||
writer.writeAttribute(QStringLiteral("id"), QStringLiteral("kde.org"));
|
||||
writer.writeTextElement(QStringLiteral("name"), authorsText.join(QStringLiteral(", ")));
|
||||
writer.writeEndElement();
|
||||
}
|
||||
|
||||
if (!i.iconName().isEmpty()) {
|
||||
writer.writeStartElement(QStringLiteral("icon"));
|
||||
writer.writeAttribute(QStringLiteral("type"), QStringLiteral("stock"));
|
||||
writer.writeCharacters(i.iconName());
|
||||
writer.writeEndElement();
|
||||
}
|
||||
writer.writeTextElement(QStringLiteral("project_license"), KAboutLicense::byKeyword(i.license()).spdx());
|
||||
writer.writeTextElement(QStringLiteral("metadata_license"), QStringLiteral("CC0-1.0"));
|
||||
writer.writeEndElement();
|
||||
writer.writeEndDocument();
|
||||
|
||||
exit(0);
|
||||
}
|
||||
|
||||
QString PackageTool::resolvePackageRootWithOptions()
|
||||
{
|
||||
QString packageRoot;
|
||||
if (d->parser->isSet(Options::packageRoot()) && d->parser->isSet(Options::global())) {
|
||||
qWarning() << i18nc("The user entered conflicting options packageroot and global, this is the error message telling the user he can use only one",
|
||||
"The packageroot and global options conflict with each other, please select only one.");
|
||||
::exit(7);
|
||||
} else if (d->parser->isSet(Options::packageRoot())) {
|
||||
packageRoot = d->parser->value(Options::packageRoot());
|
||||
// qDebug() << "(set via arg) d->packageRoot is: " << d->packageRoot;
|
||||
} else if (d->parser->isSet(Options::global())) {
|
||||
auto const paths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, d->packageRoot, QStandardPaths::LocateDirectory);
|
||||
if (!paths.isEmpty()) {
|
||||
packageRoot = paths.last();
|
||||
}
|
||||
} else {
|
||||
packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->packageRoot;
|
||||
}
|
||||
return packageRoot;
|
||||
}
|
||||
|
||||
void PackageTool::listPackages(const QString &kpackageType, const QString &path)
|
||||
{
|
||||
QStringList list = d->packages(kpackageType, path);
|
||||
list.sort();
|
||||
for (const QString &package : std::as_const(list)) {
|
||||
d->coutput(package);
|
||||
}
|
||||
exit(0);
|
||||
}
|
||||
|
||||
void PackageToolPrivate::renderTypeTable(const QMap<QString, QString> &plugins)
|
||||
{
|
||||
const QString nameHeader = i18n("KPackage Structure Name");
|
||||
const QString pathHeader = i18n("Path");
|
||||
int nameWidth = nameHeader.length();
|
||||
int pathWidth = pathHeader.length();
|
||||
|
||||
QMapIterator<QString, QString> pluginIt(plugins);
|
||||
while (pluginIt.hasNext()) {
|
||||
pluginIt.next();
|
||||
if (pluginIt.key().length() > nameWidth) {
|
||||
nameWidth = pluginIt.key().length();
|
||||
}
|
||||
|
||||
if (pluginIt.value().length() > pathWidth) {
|
||||
pathWidth = pluginIt.value().length();
|
||||
}
|
||||
}
|
||||
|
||||
std::cout << nameHeader.toLocal8Bit().constData() << std::setw(nameWidth - nameHeader.length() + 2) << ' ' << pathHeader.toLocal8Bit().constData()
|
||||
<< std::setw(pathWidth - pathHeader.length() + 2) << ' ' << std::endl;
|
||||
std::cout << std::setfill('-') << std::setw(nameWidth) << '-' << " " << std::setw(pathWidth) << '-' << " " << std::endl;
|
||||
std::cout << std::setfill(' ');
|
||||
|
||||
pluginIt.toFront();
|
||||
while (pluginIt.hasNext()) {
|
||||
pluginIt.next();
|
||||
std::cout << pluginIt.key().toLocal8Bit().constData() << std::setw(nameWidth - pluginIt.key().length() + 2) << ' '
|
||||
<< pluginIt.value().toLocal8Bit().constData() << std::setw(pathWidth - pluginIt.value().length() + 2) << std::endl;
|
||||
}
|
||||
}
|
||||
|
||||
void PackageToolPrivate::listTypes()
|
||||
{
|
||||
coutput(i18n("Package types that are installable with this tool:"));
|
||||
coutput(i18n("Built in:"));
|
||||
|
||||
QMap<QString, QString> builtIns;
|
||||
builtIns.insert(i18n("KPackage/Generic"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/packages/"));
|
||||
builtIns.insert(i18n("KPackage/GenericQML"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/genericqml/"));
|
||||
|
||||
renderTypeTable(builtIns);
|
||||
|
||||
const QList<KPluginMetaData> offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure"));
|
||||
|
||||
if (!offers.isEmpty()) {
|
||||
std::cout << std::endl;
|
||||
coutput(i18n("Provided by plugins:"));
|
||||
|
||||
QMap<QString, QString> plugins;
|
||||
for (const KPluginMetaData &info : offers) {
|
||||
const QString type = readKPackageType(info);
|
||||
if (type.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(type);
|
||||
plugins.insert(type, pkg.defaultPackageRoot());
|
||||
}
|
||||
|
||||
renderTypeTable(plugins);
|
||||
}
|
||||
}
|
||||
|
||||
void PackageTool::packageInstalled(KPackage::PackageJob *job)
|
||||
{
|
||||
bool success = (job->error() == KJob::NoError);
|
||||
int exitcode = 0;
|
||||
if (success) {
|
||||
if (d->parser->isSet(Options::upgrade())) {
|
||||
d->coutput(i18n("Successfully upgraded %1", job->package().path()));
|
||||
} else {
|
||||
d->coutput(i18n("Successfully installed %1", job->package().path()));
|
||||
}
|
||||
} else {
|
||||
d->cerror(i18n("Error: Installation of %1 failed: %2", d->packageFile, job->errorText()));
|
||||
exitcode = 4;
|
||||
}
|
||||
exit(exitcode);
|
||||
}
|
||||
|
||||
void PackageTool::packageUninstalled(KPackage::PackageJob *job)
|
||||
{
|
||||
bool success = (job->error() == KJob::NoError);
|
||||
int exitcode = 0;
|
||||
if (success) {
|
||||
if (d->parser->isSet(Options::upgrade())) {
|
||||
d->coutput(i18n("Upgrading package from file: %1", d->packageFile));
|
||||
auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
|
||||
connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
|
||||
packageInstalled(installJob);
|
||||
});
|
||||
return;
|
||||
}
|
||||
d->coutput(i18n("Successfully uninstalled %1", job->package().path()));
|
||||
} else {
|
||||
d->cerror(i18n("Error: Uninstallation of %1 failed: %2", d->packageFile, job->errorText()));
|
||||
exitcode = 7;
|
||||
}
|
||||
exit(exitcode);
|
||||
}
|
||||
|
||||
} // namespace KPackage
|
||||
|
||||
#include "moc_kpackagetool.cpp"
|
||||
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2012 Sebastian Kügler <sebas@kde.org>
|
||||
|
||||
SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
*/
|
||||
|
||||
#ifndef PACKAGETOOL_H
|
||||
#define PACKAGETOOL_H
|
||||
|
||||
#include "package.h"
|
||||
#include "packagejob.h"
|
||||
#include <QCoreApplication>
|
||||
|
||||
class QCommandLineParser;
|
||||
class KJob;
|
||||
|
||||
namespace KPackage
|
||||
{
|
||||
class PackageToolPrivate;
|
||||
|
||||
class PackageTool : public QCoreApplication
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
PackageTool(int &argc, char **argv, QCommandLineParser *parser);
|
||||
~PackageTool() override;
|
||||
|
||||
void listPackages(const QString &kpackageType, const QString &path = QString());
|
||||
void showPackageInfo(const QString &pluginName);
|
||||
void showAppstreamInfo(const QString &pluginName);
|
||||
QString resolvePackageRootWithOptions();
|
||||
|
||||
private Q_SLOTS:
|
||||
void runMain();
|
||||
void packageInstalled(KPackage::PackageJob *job);
|
||||
void packageUninstalled(KPackage::PackageJob *job);
|
||||
|
||||
private:
|
||||
PackageToolPrivate *d;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
|
||||
SPDX-FileCopyrightText: 2013 Sebastian Kügler <sebas@kde.org>
|
||||
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
|
||||
/**
|
||||
* kpackagetool6 exit codes used in this program
|
||||
|
||||
0 No error
|
||||
|
||||
1 Unspecified error
|
||||
2 Plugin is not installed
|
||||
3 Plugin or package invalid
|
||||
4 Installation failed, see stderr for reason
|
||||
5 Could not find a suitable installer for package type
|
||||
6 No install option given
|
||||
7 Conflicting arguments supplied
|
||||
8 Uninstallation failed, see stderr for reason
|
||||
9 Failed to generate package hash
|
||||
|
||||
*/
|
||||
|
||||
#include <KLocalizedString>
|
||||
#include <QCommandLineParser>
|
||||
|
||||
#include "kpackagetool.h"
|
||||
#include "options.h"
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
QCommandLineParser parser;
|
||||
KPackage::PackageTool app(argc, argv, &parser);
|
||||
|
||||
const QString description = i18n("KPackage Manager");
|
||||
const auto version = QStringLiteral("2.0");
|
||||
|
||||
app.setApplicationVersion(version);
|
||||
parser.addVersionOption();
|
||||
parser.addHelpOption();
|
||||
parser.setApplicationDescription(description);
|
||||
parser.addOptions({Options::hash(),
|
||||
Options::global(),
|
||||
Options::type(),
|
||||
Options::install(),
|
||||
Options::show(),
|
||||
Options::upgrade(),
|
||||
Options::list(),
|
||||
Options::listTypes(),
|
||||
Options::remove(),
|
||||
Options::packageRoot(),
|
||||
Options::appstream(),
|
||||
Options::appstreamOutput()});
|
||||
parser.process(app);
|
||||
|
||||
// at least one operation should be specified
|
||||
if (!parser.isSet(QStringLiteral("hash")) && !parser.isSet(QStringLiteral("g")) && !parser.isSet(QStringLiteral("i")) && !parser.isSet(QStringLiteral("s"))
|
||||
&& !parser.isSet(QStringLiteral("appstream-metainfo")) && !parser.isSet(QStringLiteral("u")) && !parser.isSet(QStringLiteral("l"))
|
||||
&& !parser.isSet(QStringLiteral("list-types")) && !parser.isSet(QStringLiteral("r")) && !parser.isSet(QStringLiteral("generate-index"))
|
||||
&& !parser.isSet(QStringLiteral("remove-index"))) {
|
||||
parser.showHelp(0);
|
||||
}
|
||||
return app.exec();
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
#ifndef OPTIONS_H
|
||||
#define OPTIONS_H
|
||||
|
||||
#include <QCommandLineOption>
|
||||
|
||||
namespace Options
|
||||
{
|
||||
static QCommandLineOption hash()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("hash"),
|
||||
i18nc("Do not translate <path>", "Generate a SHA1 hash for the package at <path>"),
|
||||
QStringLiteral("path")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption global()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("g"), QStringLiteral("global")},
|
||||
i18n("For install or remove, operates on packages installed for all users.")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption type()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("t"), QStringLiteral("type")},
|
||||
i18nc("theme, wallpaper, etc. are keywords, but they may be translated, as both versions "
|
||||
"are recognized by the application "
|
||||
"(if translated, should be same as messages with 'package type' context below)",
|
||||
"The type of package, corresponding to the service type of the package plugin, e.g. KPackage/Generic, Plasma/Theme, "
|
||||
"Plasma/Wallpaper, Plasma/Applet, etc."),
|
||||
QStringLiteral("type"),
|
||||
QStringLiteral("KPackage/Generic")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption install()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("i"), QStringLiteral("install")},
|
||||
i18nc("Do not translate <path>", "Install the package at <path>"),
|
||||
QStringLiteral("path")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption show()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("s"), QStringLiteral("show")},
|
||||
i18nc("Do not translate <name>", "Show information of package <name>"),
|
||||
QStringLiteral("name")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption upgrade()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("u"), QStringLiteral("upgrade")},
|
||||
i18nc("Do not translate <path>", "Upgrade the package at <path>"),
|
||||
QStringLiteral("path")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption list()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("l"), QStringLiteral("list")}, i18n("List installed packages")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption listTypes()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("list-types")}, i18n("List all known package types that can be installed")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption remove()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("r"), QStringLiteral("remove")},
|
||||
i18nc("Do not translate <name>", "Remove the package named <name>"),
|
||||
QStringLiteral("name")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption packageRoot()
|
||||
{
|
||||
static QCommandLineOption o{QStringList{QStringLiteral("p"), QStringLiteral("packageroot")},
|
||||
i18n("Absolute path to the package root. If not supplied, then the standard data"
|
||||
" directories for this KDE session will be searched instead."),
|
||||
QStringLiteral("path")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption appstream()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("appstream-metainfo"),
|
||||
i18nc("Do not translate <path>", "Outputs the metadata for the package <path>"),
|
||||
QStringLiteral("path")};
|
||||
return o;
|
||||
}
|
||||
static QCommandLineOption appstreamOutput()
|
||||
{
|
||||
static QCommandLineOption o{QStringLiteral("appstream-metainfo-output"),
|
||||
i18nc("Do not translate <path>", "Outputs the metadata for the package into <path>"),
|
||||
QStringLiteral("path")};
|
||||
return o;
|
||||
}
|
||||
}
|
||||
|
||||
#endif // OPTIONS_H
|
||||
Reference in New Issue
Block a user