File indexing completed on 2025-02-09 04:25:44
0001 /* 0002 SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org> 0003 SPDX-FileCopyrightText: 2012-2017 Sebastian Kügler <sebas@kde.org> 0004 0005 SPDX-License-Identifier: LGPL-2.0-or-later 0006 */ 0007 0008 #include "kpackagetool.h" 0009 0010 #include <KAboutData> 0011 #include <KLocalizedString> 0012 #include <KShell> 0013 #include <QDebug> 0014 0015 #include <KJob> 0016 #include <kpackage/package.h> 0017 #include <kpackage/packageloader.h> 0018 #include <kpackage/packagestructure.h> 0019 #include <kpackage/private/utils.h> 0020 0021 #include <QCommandLineParser> 0022 #include <QDir> 0023 #include <QFileInfo> 0024 #include <QList> 0025 #include <QMap> 0026 #include <QRegularExpression> 0027 #include <QStandardPaths> 0028 #include <QStringList> 0029 #include <QTimer> 0030 #include <QUrl> 0031 #include <QXmlStreamWriter> 0032 0033 #include <iomanip> 0034 #include <iostream> 0035 0036 #include "options.h" 0037 0038 #include "../kpackage/config-package.h" 0039 0040 #include "kpackage_debug.h" 0041 0042 Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cout, (stdout)) 0043 Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cerr, (stderr)) 0044 0045 namespace KPackage 0046 { 0047 class PackageToolPrivate 0048 { 0049 public: 0050 QString packageRoot; 0051 QString packageFile; 0052 QString package; 0053 QString kpackageType = QStringLiteral("KPackage/Generic"); 0054 KPluginMetaData metadata; 0055 QString installPath; 0056 void output(const QString &msg); 0057 QStringList packages(const QString &type, const QString &path = QString()); 0058 void renderTypeTable(const QMap<QString, QString> &plugins); 0059 void listTypes(); 0060 void coutput(const QString &msg); 0061 void cerror(const QString &msg); 0062 QCommandLineParser *parser = nullptr; 0063 }; 0064 0065 PackageTool::PackageTool(int &argc, char **argv, QCommandLineParser *parser) 0066 : QCoreApplication(argc, argv) 0067 { 0068 d = new PackageToolPrivate; 0069 d->parser = parser; 0070 QTimer::singleShot(0, this, &PackageTool::runMain); 0071 } 0072 0073 PackageTool::~PackageTool() 0074 { 0075 delete d; 0076 } 0077 0078 void PackageTool::runMain() 0079 { 0080 if (d->parser->isSet(Options::hash())) { 0081 const QString path = d->parser->value(Options::hash()); 0082 KPackage::PackageStructure structure; 0083 KPackage::Package package(&structure); 0084 package.setPath(path); 0085 const QString hash = QString::fromLocal8Bit(package.cryptographicHash(QCryptographicHash::Sha1)); 0086 if (hash.isEmpty()) { 0087 d->coutput(i18n("Failed to generate a Package hash for %1", path)); 0088 exit(9); 0089 } else { 0090 d->coutput(i18n("SHA1 hash for Package at %1: '%2'", package.path(), hash)); 0091 exit(0); 0092 } 0093 return; 0094 } 0095 0096 if (d->parser->isSet(Options::listTypes())) { 0097 d->listTypes(); 0098 exit(0); 0099 return; 0100 } 0101 0102 if (d->parser->isSet(Options::type())) { 0103 d->kpackageType = d->parser->value(Options::type()); 0104 } 0105 d->packageRoot = KPackage::PackageLoader::self()->loadPackage(d->kpackageType).defaultPackageRoot(); 0106 0107 if (d->parser->isSet(Options::remove())) { 0108 d->package = d->parser->value(Options::remove()); 0109 } else if (d->parser->isSet(Options::upgrade())) { 0110 d->package = d->parser->value(Options::upgrade()); 0111 } else if (d->parser->isSet(Options::install())) { 0112 d->package = d->parser->value(Options::install()); 0113 } else if (d->parser->isSet(Options::show())) { 0114 d->package = d->parser->value(Options::show()); 0115 } else if (d->parser->isSet(Options::appstream())) { 0116 d->package = d->parser->value(Options::appstream()); 0117 } 0118 0119 if (!QDir::isAbsolutePath(d->package)) { 0120 d->packageFile = QDir(QDir::currentPath() + QLatin1Char('/') + d->package).absolutePath(); 0121 d->packageFile = QFileInfo(d->packageFile).canonicalFilePath(); 0122 if (d->parser->isSet(Options::upgrade())) { 0123 d->package = d->packageFile; 0124 } 0125 } else { 0126 d->packageFile = d->package; 0127 } 0128 0129 if (!PackageLoader::self()->loadPackageStructure(d->kpackageType)) { 0130 qWarning() << "Package type" << d->kpackageType << "not found"; 0131 } 0132 0133 if (d->parser->isSet(Options::show())) { 0134 const QString pluginName = d->package; 0135 showPackageInfo(pluginName); 0136 return; 0137 } else if (d->parser->isSet(Options::appstream())) { 0138 const QString pluginName = d->package; 0139 showAppstreamInfo(pluginName); 0140 return; 0141 } 0142 0143 if (d->parser->isSet(Options::list())) { 0144 QString packageRoot = resolvePackageRootWithOptions(); 0145 d->coutput(i18n("Listing KPackageType: %1 in %2", d->kpackageType, packageRoot)); 0146 listPackages(d->kpackageType, packageRoot); 0147 exit(0); 0148 } else { 0149 // install, remove or upgrade 0150 d->packageRoot = resolvePackageRootWithOptions(); 0151 0152 if (d->parser->isSet(Options::remove()) || d->parser->isSet(Options::upgrade())) { 0153 QString pkgPath; 0154 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType); 0155 pkg.setPath(d->package); 0156 if (pkg.isValid()) { 0157 pkgPath = pkg.path(); 0158 if (pkgPath.isEmpty() && !d->packageFile.isEmpty()) { 0159 pkgPath = d->packageFile; 0160 } 0161 } 0162 if (pkgPath.isEmpty()) { 0163 pkgPath = d->package; 0164 } 0165 QString _p = d->packageRoot; 0166 if (!_p.endsWith(QLatin1Char('/'))) { 0167 _p.append(QLatin1Char('/')); 0168 } 0169 _p.append(d->package); 0170 0171 if (!d->parser->isSet(Options::type())) { 0172 d->kpackageType = readKPackageType(pkg.metadata()); 0173 } 0174 0175 QString pluginName; 0176 if (pkg.metadata().isValid()) { 0177 d->metadata = pkg.metadata(); 0178 if (!d->metadata.isValid()) { 0179 pluginName = d->package; 0180 } else if (!d->metadata.isValid() && d->metadata.pluginId().isEmpty()) { 0181 // plugin id given in command line 0182 pluginName = d->package; 0183 } else { 0184 // Parameter was a plasma package, get plugin id from the package 0185 pluginName = d->metadata.pluginId(); 0186 } 0187 } 0188 QStringList installed = d->packages(d->kpackageType); 0189 0190 // Uninstalling ... 0191 if (installed.contains(pluginName)) { // Assume it's a plugin id 0192 KPackage::PackageJob *uninstallJob = KPackage::PackageJob::uninstall(d->kpackageType, pluginName, d->packageRoot); 0193 connect(uninstallJob, &KPackage::PackageJob::finished, this, [uninstallJob, this]() { 0194 packageUninstalled(uninstallJob); 0195 }); 0196 return; 0197 } else { 0198 d->coutput(i18n("Error: Plugin %1 is not installed.", pluginName)); 0199 exit(2); 0200 } 0201 } 0202 if (d->parser->isSet(Options::install())) { 0203 auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot); 0204 connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() { 0205 packageInstalled(installJob); 0206 }); 0207 return; 0208 } 0209 if (d->package.isEmpty()) { 0210 qWarning() << i18nc( 0211 "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", 0212 "One of install, remove, upgrade or list is required."); 0213 exit(6); 0214 } 0215 } 0216 } 0217 0218 void PackageToolPrivate::coutput(const QString &msg) 0219 { 0220 *cout << msg << '\n'; 0221 (*cout).flush(); 0222 } 0223 0224 void PackageToolPrivate::cerror(const QString &msg) 0225 { 0226 *cerr << msg << '\n'; 0227 (*cerr).flush(); 0228 } 0229 0230 QStringList PackageToolPrivate::packages(const QString &type, const QString &path) 0231 { 0232 QStringList result; 0233 const QList<KPluginMetaData> dataList = KPackage::PackageLoader::self()->listPackages(type, path); 0234 for (const KPluginMetaData &data : dataList) { 0235 if (!result.contains(data.pluginId())) { 0236 result << data.pluginId(); 0237 } 0238 } 0239 return result; 0240 } 0241 0242 void PackageTool::showPackageInfo(const QString &pluginName) 0243 { 0244 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType); 0245 pkg.setDefaultPackageRoot(d->packageRoot); 0246 0247 if (QFile::exists(d->packageFile)) { 0248 pkg.setPath(d->packageFile); 0249 } else { 0250 pkg.setPath(pluginName); 0251 } 0252 0253 KPluginMetaData i = pkg.metadata(); 0254 if (!i.isValid()) { 0255 *cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName); 0256 exit(3); 0257 return; 0258 } 0259 d->coutput(i18n("Showing info for package: %1", pluginName)); 0260 d->coutput(i18n(" Name : %1", i.name())); 0261 d->coutput(i18n(" Description: %1", i.description())); 0262 d->coutput(i18n(" Plugin : %1", i.pluginId())); 0263 auto const authors = i.authors(); 0264 QStringList authorNames; 0265 for (const KAboutPerson &author : authors) { 0266 authorNames << author.name(); 0267 } 0268 d->coutput(i18n(" Author : %1", authorNames.join(QLatin1String(", ")))); 0269 d->coutput(i18n(" Path : %1", pkg.path())); 0270 0271 exit(0); 0272 } 0273 0274 bool translateKPluginToAppstream(const QString &tagName, 0275 const QString &configField, 0276 const QJsonObject &configObject, 0277 QXmlStreamWriter &writer, 0278 bool canEndWithDot) 0279 { 0280 const QRegularExpression rx(QStringLiteral("%1\\[(.*)\\]").arg(configField)); 0281 const QJsonValue native = configObject.value(configField); 0282 if (native.isUndefined()) { 0283 return false; 0284 } 0285 0286 QString content = native.toString(); 0287 if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) { 0288 content.chop(1); 0289 } 0290 writer.writeTextElement(tagName, content); 0291 for (auto it = configObject.begin(), itEnd = configObject.end(); it != itEnd; ++it) { 0292 const auto match = rx.match(it.key()); 0293 0294 if (match.hasMatch()) { 0295 QString content = it->toString(); 0296 if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) { 0297 content.chop(1); 0298 } 0299 0300 writer.writeStartElement(tagName); 0301 writer.writeAttribute(QStringLiteral("xml:lang"), match.captured(1)); 0302 writer.writeCharacters(content); 0303 writer.writeEndElement(); 0304 } 0305 } 0306 return true; 0307 } 0308 0309 void PackageTool::showAppstreamInfo(const QString &pluginName) 0310 { 0311 KPluginMetaData i; 0312 // 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. 0313 // This can happen in the case an application wanting to support kpackage based extensions includes in the same project both the packagestructure plugin and 0314 // the packages themselves. In that case at build time the packagestructure plugin wouldn't be installed yet 0315 0316 if (QFile::exists(pluginName + QStringLiteral("/metadata.json"))) { 0317 i = KPluginMetaData::fromJsonFile(pluginName + QStringLiteral("/metadata.json")); 0318 } else { 0319 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(d->kpackageType); 0320 0321 pkg.setDefaultPackageRoot(d->packageRoot); 0322 0323 if (QFile::exists(d->packageFile)) { 0324 pkg.setPath(d->packageFile); 0325 } else { 0326 pkg.setPath(pluginName); 0327 } 0328 0329 i = pkg.metadata(); 0330 } 0331 0332 if (!i.isValid()) { 0333 *cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName); 0334 std::exit(3); 0335 return; 0336 } 0337 QString parentApp = i.value(QLatin1String("X-KDE-ParentApp")); 0338 0339 if (i.value(QStringLiteral("NoDisplay"), false)) { 0340 std::exit(0); 0341 } 0342 0343 QXmlStreamAttributes componentAttributes; 0344 if (!parentApp.isEmpty()) { 0345 componentAttributes << QXmlStreamAttribute(QLatin1String("type"), QLatin1String("addon")); 0346 } 0347 0348 // Compatibility: without appstream-metainfo-output argument we print the XML output to STDOUT 0349 // with the argument we'll print to the defined path. 0350 // TODO: in KF6 we should switch to argument-only. 0351 QIODevice *outputDevice = cout->device(); 0352 std::unique_ptr<QFile> outputFile; 0353 const auto outputPath = d->parser->value(Options::appstreamOutput()); 0354 if (!outputPath.isEmpty()) { 0355 auto outputUrl = QUrl::fromUserInput(outputPath); 0356 outputFile.reset(new QFile(outputUrl.toLocalFile())); 0357 if (!outputFile->open(QFile::WriteOnly | QFile::Text)) { 0358 *cerr << "Failed to open output file for writing."; 0359 exit(1); 0360 } 0361 outputDevice = outputFile.get(); 0362 } 0363 0364 if (i.description().isEmpty()) { 0365 *cerr << "Error: description missing, will result in broken appdata field as <summary/> is mandatory at " << QFileInfo(i.fileName()).absoluteFilePath(); 0366 std::exit(10); 0367 } 0368 0369 QXmlStreamWriter writer(outputDevice); 0370 writer.setAutoFormatting(true); 0371 writer.writeStartDocument(); 0372 writer.writeStartElement(QStringLiteral("component")); 0373 writer.writeAttributes(componentAttributes); 0374 0375 writer.writeTextElement(QStringLiteral("id"), i.pluginId()); 0376 if (!parentApp.isEmpty()) { 0377 writer.writeTextElement(QStringLiteral("extends"), parentApp); 0378 } 0379 0380 const QJsonObject rootObject = i.rawData()[QStringLiteral("KPlugin")].toObject(); 0381 translateKPluginToAppstream(QStringLiteral("name"), QStringLiteral("Name"), rootObject, writer, false); 0382 translateKPluginToAppstream(QStringLiteral("summary"), QStringLiteral("Description"), rootObject, writer, false); 0383 if (!i.website().isEmpty()) { 0384 writer.writeStartElement(QStringLiteral("url")); 0385 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("homepage")); 0386 writer.writeCharacters(i.website()); 0387 writer.writeEndElement(); 0388 } 0389 0390 if (i.pluginId().startsWith(QLatin1String("org.kde."))) { 0391 writer.writeStartElement(QStringLiteral("url")); 0392 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("donation")); 0393 writer.writeCharacters(QStringLiteral("https://www.kde.org/donate.php?app=%1").arg(i.pluginId())); 0394 writer.writeEndElement(); 0395 } 0396 0397 const auto authors = i.authors(); 0398 if (!authors.isEmpty()) { 0399 QStringList authorsText; 0400 authorsText.reserve(authors.size()); 0401 for (const auto &author : authors) { 0402 authorsText += QStringLiteral("%1").arg(author.name()); 0403 } 0404 writer.writeStartElement(QStringLiteral("developer")); 0405 writer.writeAttribute(QStringLiteral("id"), QStringLiteral("kde.org")); 0406 writer.writeTextElement(QStringLiteral("name"), authorsText.join(QStringLiteral(", "))); 0407 writer.writeEndElement(); 0408 } 0409 0410 if (!i.iconName().isEmpty()) { 0411 writer.writeStartElement(QStringLiteral("icon")); 0412 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("stock")); 0413 writer.writeCharacters(i.iconName()); 0414 writer.writeEndElement(); 0415 } 0416 writer.writeTextElement(QStringLiteral("project_license"), KAboutLicense::byKeyword(i.license()).spdx()); 0417 writer.writeTextElement(QStringLiteral("metadata_license"), QStringLiteral("CC0-1.0")); 0418 writer.writeEndElement(); 0419 writer.writeEndDocument(); 0420 0421 exit(0); 0422 } 0423 0424 QString PackageTool::resolvePackageRootWithOptions() 0425 { 0426 QString packageRoot; 0427 if (d->parser->isSet(Options::packageRoot()) && d->parser->isSet(Options::global())) { 0428 qWarning() << i18nc("The user entered conflicting options packageroot and global, this is the error message telling the user he can use only one", 0429 "The packageroot and global options conflict with each other, please select only one."); 0430 ::exit(7); 0431 } else if (d->parser->isSet(Options::packageRoot())) { 0432 packageRoot = d->parser->value(Options::packageRoot()); 0433 // qDebug() << "(set via arg) d->packageRoot is: " << d->packageRoot; 0434 } else if (d->parser->isSet(Options::global())) { 0435 auto const paths = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, d->packageRoot, QStandardPaths::LocateDirectory); 0436 if (!paths.isEmpty()) { 0437 packageRoot = paths.last(); 0438 } 0439 } else { 0440 packageRoot = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->packageRoot; 0441 } 0442 return packageRoot; 0443 } 0444 0445 void PackageTool::listPackages(const QString &kpackageType, const QString &path) 0446 { 0447 QStringList list = d->packages(kpackageType, path); 0448 list.sort(); 0449 for (const QString &package : std::as_const(list)) { 0450 d->coutput(package); 0451 } 0452 exit(0); 0453 } 0454 0455 void PackageToolPrivate::renderTypeTable(const QMap<QString, QString> &plugins) 0456 { 0457 const QString nameHeader = i18n("KPackage Structure Name"); 0458 const QString pathHeader = i18n("Path"); 0459 int nameWidth = nameHeader.length(); 0460 int pathWidth = pathHeader.length(); 0461 0462 QMapIterator<QString, QString> pluginIt(plugins); 0463 while (pluginIt.hasNext()) { 0464 pluginIt.next(); 0465 if (pluginIt.key().length() > nameWidth) { 0466 nameWidth = pluginIt.key().length(); 0467 } 0468 0469 if (pluginIt.value().length() > pathWidth) { 0470 pathWidth = pluginIt.value().length(); 0471 } 0472 } 0473 0474 std::cout << nameHeader.toLocal8Bit().constData() << std::setw(nameWidth - nameHeader.length() + 2) << ' ' << pathHeader.toLocal8Bit().constData() 0475 << std::setw(pathWidth - pathHeader.length() + 2) << ' ' << std::endl; 0476 std::cout << std::setfill('-') << std::setw(nameWidth) << '-' << " " << std::setw(pathWidth) << '-' << " " << std::endl; 0477 std::cout << std::setfill(' '); 0478 0479 pluginIt.toFront(); 0480 while (pluginIt.hasNext()) { 0481 pluginIt.next(); 0482 std::cout << pluginIt.key().toLocal8Bit().constData() << std::setw(nameWidth - pluginIt.key().length() + 2) << ' ' 0483 << pluginIt.value().toLocal8Bit().constData() << std::setw(pathWidth - pluginIt.value().length() + 2) << std::endl; 0484 } 0485 } 0486 0487 void PackageToolPrivate::listTypes() 0488 { 0489 coutput(i18n("Package types that are installable with this tool:")); 0490 coutput(i18n("Built in:")); 0491 0492 QMap<QString, QString> builtIns; 0493 builtIns.insert(i18n("KPackage/Generic"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/packages/")); 0494 builtIns.insert(i18n("KPackage/GenericQML"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/genericqml/")); 0495 0496 renderTypeTable(builtIns); 0497 0498 const QList<KPluginMetaData> offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure")); 0499 0500 if (!offers.isEmpty()) { 0501 std::cout << std::endl; 0502 coutput(i18n("Provided by plugins:")); 0503 0504 QMap<QString, QString> plugins; 0505 for (const KPluginMetaData &info : offers) { 0506 const QString type = readKPackageType(info); 0507 if (type.isEmpty()) { 0508 continue; 0509 } 0510 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(type); 0511 plugins.insert(type, pkg.defaultPackageRoot()); 0512 } 0513 0514 renderTypeTable(plugins); 0515 } 0516 } 0517 0518 void PackageTool::packageInstalled(KPackage::PackageJob *job) 0519 { 0520 bool success = (job->error() == KJob::NoError); 0521 int exitcode = 0; 0522 if (success) { 0523 if (d->parser->isSet(Options::upgrade())) { 0524 d->coutput(i18n("Successfully upgraded %1", job->package().path())); 0525 } else { 0526 d->coutput(i18n("Successfully installed %1", job->package().path())); 0527 } 0528 } else { 0529 d->cerror(i18n("Error: Installation of %1 failed: %2", d->packageFile, job->errorText())); 0530 exitcode = 4; 0531 } 0532 exit(exitcode); 0533 } 0534 0535 void PackageTool::packageUninstalled(KPackage::PackageJob *job) 0536 { 0537 bool success = (job->error() == KJob::NoError); 0538 int exitcode = 0; 0539 if (success) { 0540 if (d->parser->isSet(Options::upgrade())) { 0541 d->coutput(i18n("Upgrading package from file: %1", d->packageFile)); 0542 auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot); 0543 connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() { 0544 packageInstalled(installJob); 0545 }); 0546 return; 0547 } 0548 d->coutput(i18n("Successfully uninstalled %1", job->package().path())); 0549 } else { 0550 d->cerror(i18n("Error: Uninstallation of %1 failed: %2", d->packageFile, job->errorText())); 0551 exitcode = 7; 0552 } 0553 exit(exitcode); 0554 } 0555 0556 } // namespace KPackage 0557 0558 #include "moc_kpackagetool.cpp"