1/*
2 SPDX-FileCopyrightText: 2008 Aaron Seigo <aseigo@kde.org>
3 SPDX-FileCopyrightText: 2012-2017 Sebastian Kügler <sebas@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kpackagetool.h"
9
10#include <KAboutData>
11#include <KLocalizedString>
12#include <KShell>
13#include <QDebug>
14
15#include <KJob>
16#include <kpackage/package.h>
17#include <kpackage/packageloader.h>
18#include <kpackage/packagestructure.h>
19#include <kpackage/private/utils.h>
20
21#include <QCommandLineParser>
22#include <QDir>
23#include <QFileInfo>
24#include <QList>
25#include <QMap>
26#include <QRegularExpression>
27#include <QStandardPaths>
28#include <QStringList>
29#include <QTimer>
30#include <QUrl>
31#include <QXmlStreamWriter>
32
33#include <iomanip>
34#include <iostream>
35
36#include "options.h"
37
38#include "../kpackage/config-package.h"
39
40#include "kpackage_debug.h"
41
42Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cout, (stdout))
43Q_GLOBAL_STATIC_WITH_ARGS(QTextStream, cerr, (stderr))
44
45namespace KPackage
46{
47class PackageToolPrivate
48{
49public:
50 QString packageRoot;
51 QString packageFile;
52 QString package;
53 QString kpackageType = QStringLiteral("KPackage/Generic");
54 KPluginMetaData metadata;
55 QString installPath;
56 void output(const QString &msg);
57 QStringList packages(const QString &type, const QString &path = QString());
58 void renderTypeTable(const QMap<QString, QString> &plugins);
59 void listTypes();
60 void coutput(const QString &msg);
61 void cerror(const QString &msg);
62 QCommandLineParser *parser = nullptr;
63};
64
65PackageTool::PackageTool(int &argc, char **argv, QCommandLineParser *parser)
66 : QCoreApplication(argc, argv)
67{
68 d = new PackageToolPrivate;
69 d->parser = parser;
70 QTimer::singleShot(interval: 0, receiver: this, slot: &PackageTool::runMain);
71}
72
73PackageTool::~PackageTool()
74{
75 delete d;
76}
77
78void PackageTool::runMain()
79{
80 if (d->parser->isSet(option: Options::hash())) {
81 const QString path = d->parser->value(option: Options::hash());
82 KPackage::PackageStructure structure;
83 KPackage::Package package(&structure);
84 package.setPath(path);
85 const QString hash = QString::fromLocal8Bit(ba: package.cryptographicHash(algorithm: QCryptographicHash::Sha1));
86 if (hash.isEmpty()) {
87 d->coutput(i18n("Failed to generate a Package hash for %1", path));
88 exit(retcode: 9);
89 } else {
90 d->coutput(i18n("SHA1 hash for Package at %1: '%2'", package.path(), hash));
91 exit(retcode: 0);
92 }
93 return;
94 }
95
96 if (d->parser->isSet(option: Options::listTypes())) {
97 d->listTypes();
98 exit(retcode: 0);
99 return;
100 }
101
102 if (d->parser->isSet(option: Options::type())) {
103 d->kpackageType = d->parser->value(option: Options::type());
104 }
105 d->packageRoot = KPackage::PackageLoader::self()->loadPackage(packageFormat: d->kpackageType).defaultPackageRoot();
106
107 if (d->parser->isSet(option: Options::remove())) {
108 d->package = d->parser->value(option: Options::remove());
109 } else if (d->parser->isSet(option: Options::upgrade())) {
110 d->package = d->parser->value(option: Options::upgrade());
111 } else if (d->parser->isSet(option: Options::install())) {
112 d->package = d->parser->value(option: Options::install());
113 } else if (d->parser->isSet(option: Options::show())) {
114 d->package = d->parser->value(option: Options::show());
115 } else if (d->parser->isSet(option: Options::appstream())) {
116 d->package = d->parser->value(option: Options::appstream());
117 }
118
119 if (!QDir::isAbsolutePath(path: d->package)) {
120 d->packageFile = QDir(QDir::currentPath() + QLatin1Char('/') + d->package).absolutePath();
121 d->packageFile = QFileInfo(d->packageFile).canonicalFilePath();
122 if (d->parser->isSet(option: Options::upgrade())) {
123 d->package = d->packageFile;
124 }
125 } else {
126 d->packageFile = d->package;
127 }
128
129 if (!PackageLoader::self()->loadPackageStructure(packageFormat: d->kpackageType)) {
130 qWarning() << "Package type" << d->kpackageType << "not found";
131 }
132
133 if (d->parser->isSet(option: Options::show())) {
134 const QString pluginName = d->package;
135 showPackageInfo(pluginName);
136 return;
137 } else if (d->parser->isSet(option: Options::appstream())) {
138 const QString pluginName = d->package;
139 showAppstreamInfo(pluginName);
140 return;
141 }
142
143 if (d->parser->isSet(option: Options::list())) {
144 QString packageRoot = resolvePackageRootWithOptions();
145 d->coutput(i18n("Listing KPackageType: %1 in %2", d->kpackageType, packageRoot));
146 listPackages(kpackageType: d->kpackageType, path: packageRoot);
147 exit(retcode: 0);
148 } else {
149 // install, remove or upgrade
150 d->packageRoot = resolvePackageRootWithOptions();
151
152 if (d->parser->isSet(option: Options::remove()) || d->parser->isSet(option: Options::upgrade())) {
153 QString pkgPath;
154 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(packageFormat: d->kpackageType);
155 pkg.setPath(d->package);
156 if (pkg.isValid()) {
157 pkgPath = pkg.path();
158 if (pkgPath.isEmpty() && !d->packageFile.isEmpty()) {
159 pkgPath = d->packageFile;
160 }
161 }
162 if (pkgPath.isEmpty()) {
163 pkgPath = d->package;
164 }
165 QString _p = d->packageRoot;
166 if (!_p.endsWith(c: QLatin1Char('/'))) {
167 _p.append(c: QLatin1Char('/'));
168 }
169 _p.append(s: d->package);
170
171 if (!d->parser->isSet(option: Options::type())) {
172 d->kpackageType = readKPackageType(metaData: pkg.metadata());
173 }
174
175 QString pluginName;
176 if (pkg.metadata().isValid()) {
177 d->metadata = pkg.metadata();
178 if (!d->metadata.isValid()) {
179 pluginName = d->package;
180 } else if (!d->metadata.isValid() && d->metadata.pluginId().isEmpty()) {
181 // plugin id given in command line
182 pluginName = d->package;
183 } else {
184 // Parameter was a plasma package, get plugin id from the package
185 pluginName = d->metadata.pluginId();
186 }
187 }
188 QStringList installed = d->packages(type: d->kpackageType);
189
190 // Uninstalling ...
191 if (installed.contains(str: pluginName)) { // Assume it's a plugin id
192 KPackage::PackageJob *uninstallJob = KPackage::PackageJob::uninstall(packageFormat: d->kpackageType, pluginId: pluginName, packageRoot: d->packageRoot);
193 connect(sender: uninstallJob, signal: &KPackage::PackageJob::finished, context: this, slot: [uninstallJob, this]() {
194 packageUninstalled(job: uninstallJob);
195 });
196 return;
197 } else {
198 d->coutput(i18n("Error: Plugin %1 is not installed.", pluginName));
199 exit(retcode: 2);
200 }
201 }
202 if (d->parser->isSet(option: Options::install())) {
203 auto installJob = KPackage::PackageJob::install(packageFormat: d->kpackageType, sourcePackage: d->packageFile, packageRoot: d->packageRoot);
204 connect(sender: installJob, signal: &KPackage::PackageJob::finished, context: this, slot: [installJob, this]() {
205 packageInstalled(job: installJob);
206 });
207 return;
208 }
209 if (d->package.isEmpty()) {
210 qWarning() << i18nc(
211 "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",
212 "One of install, remove, upgrade or list is required.");
213 exit(retcode: 6);
214 }
215 }
216}
217
218void PackageToolPrivate::coutput(const QString &msg)
219{
220 *cout << msg << '\n';
221 (*cout).flush();
222}
223
224void PackageToolPrivate::cerror(const QString &msg)
225{
226 *cerr << msg << '\n';
227 (*cerr).flush();
228}
229
230QStringList PackageToolPrivate::packages(const QString &type, const QString &path)
231{
232 QStringList result;
233 const QList<KPluginMetaData> dataList = KPackage::PackageLoader::self()->listPackages(packageFormat: type, packageRoot: path);
234 for (const KPluginMetaData &data : dataList) {
235 if (!result.contains(str: data.pluginId())) {
236 result << data.pluginId();
237 }
238 }
239 return result;
240}
241
242void PackageTool::showPackageInfo(const QString &pluginName)
243{
244 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(packageFormat: d->kpackageType);
245 pkg.setDefaultPackageRoot(d->packageRoot);
246
247 if (QFile::exists(fileName: d->packageFile)) {
248 pkg.setPath(d->packageFile);
249 } else {
250 pkg.setPath(pluginName);
251 }
252
253 KPluginMetaData i = pkg.metadata();
254 if (!i.isValid()) {
255 *cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
256 exit(retcode: 3);
257 return;
258 }
259 d->coutput(i18n("Showing info for package: %1", pluginName));
260 d->coutput(i18n(" Name : %1", i.name()));
261 d->coutput(i18n(" Description: %1", i.description()));
262 d->coutput(i18n(" Plugin : %1", i.pluginId()));
263 auto const authors = i.authors();
264 QStringList authorNames;
265 for (const KAboutPerson &author : authors) {
266 authorNames << author.name();
267 }
268 d->coutput(i18n(" Author : %1", authorNames.join(QLatin1String(", "))));
269 d->coutput(i18n(" Path : %1", pkg.path()));
270
271 exit(retcode: 0);
272}
273
274bool translateKPluginToAppstream(const QString &tagName,
275 const QString &configField,
276 const QJsonObject &configObject,
277 QXmlStreamWriter &writer,
278 bool canEndWithDot)
279{
280 const QRegularExpression rx(QStringLiteral("%1\\[(.*)\\]").arg(a: configField));
281 const QJsonValue native = configObject.value(key: configField);
282 if (native.isUndefined()) {
283 return false;
284 }
285
286 QString content = native.toString();
287 if (!canEndWithDot && content.endsWith(c: QLatin1Char('.'))) {
288 content.chop(n: 1);
289 }
290 writer.writeTextElement(qualifiedName: tagName, text: content);
291 for (auto it = configObject.begin(), itEnd = configObject.end(); it != itEnd; ++it) {
292 const auto match = rx.match(subject: it.key());
293
294 if (match.hasMatch()) {
295 QString content = it->toString();
296 if (!canEndWithDot && content.endsWith(c: QLatin1Char('.'))) {
297 content.chop(n: 1);
298 }
299
300 writer.writeStartElement(qualifiedName: tagName);
301 writer.writeAttribute(QStringLiteral("xml:lang"), value: match.captured(nth: 1));
302 writer.writeCharacters(text: content);
303 writer.writeEndElement();
304 }
305 }
306 return true;
307}
308
309void PackageTool::showAppstreamInfo(const QString &pluginName)
310{
311 KPluginMetaData i;
312 // 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.
313 // This can happen in the case an application wanting to support kpackage based extensions includes in the same project both the packagestructure plugin and
314 // the packages themselves. In that case at build time the packagestructure plugin wouldn't be installed yet
315
316 if (QFile::exists(fileName: pluginName + QStringLiteral("/metadata.json"))) {
317 i = KPluginMetaData::fromJsonFile(jsonFile: pluginName + QStringLiteral("/metadata.json"));
318 } else {
319 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(packageFormat: d->kpackageType);
320
321 pkg.setDefaultPackageRoot(d->packageRoot);
322
323 if (QFile::exists(fileName: d->packageFile)) {
324 pkg.setPath(d->packageFile);
325 } else {
326 pkg.setPath(pluginName);
327 }
328
329 i = pkg.metadata();
330 }
331
332 if (!i.isValid()) {
333 *cerr << i18n("Error: Can't find plugin metadata: %1\n", pluginName);
334 std::exit(status: 3);
335 return;
336 }
337 QString parentApp = i.value(key: QLatin1String("X-KDE-ParentApp"));
338
339 if (i.value(QStringLiteral("NoDisplay"), defaultValue: false)) {
340 std::exit(status: 0);
341 }
342
343 QXmlStreamAttributes componentAttributes;
344 if (!parentApp.isEmpty()) {
345 componentAttributes << QXmlStreamAttribute(QLatin1String("type"), QLatin1String("addon"));
346 }
347
348 // Compatibility: without appstream-metainfo-output argument we print the XML output to STDOUT
349 // with the argument we'll print to the defined path.
350 // TODO: in KF6 we should switch to argument-only.
351 QIODevice *outputDevice = cout->device();
352 std::unique_ptr<QFile> outputFile;
353 const auto outputPath = d->parser->value(option: Options::appstreamOutput());
354 if (!outputPath.isEmpty()) {
355 auto outputUrl = QUrl::fromUserInput(userInput: outputPath);
356 outputFile.reset(p: new QFile(outputUrl.toLocalFile()));
357 if (!outputFile->open(flags: QFile::WriteOnly | QFile::Text)) {
358 *cerr << "Failed to open output file for writing.";
359 exit(retcode: 1);
360 }
361 outputDevice = outputFile.get();
362 }
363
364 if (i.description().isEmpty()) {
365 *cerr << "Error: description missing, will result in broken appdata field as <summary/> is mandatory at " << QFileInfo(i.fileName()).absoluteFilePath();
366 std::exit(status: 10);
367 }
368
369 QXmlStreamWriter writer(outputDevice);
370 writer.setAutoFormatting(true);
371 writer.writeStartDocument();
372 writer.writeStartElement(QStringLiteral("component"));
373 writer.writeAttributes(attributes: componentAttributes);
374
375 writer.writeTextElement(QStringLiteral("id"), text: i.pluginId());
376 if (!parentApp.isEmpty()) {
377 writer.writeTextElement(QStringLiteral("extends"), text: parentApp);
378 }
379
380 const QJsonObject rootObject = i.rawData()[QStringLiteral("KPlugin")].toObject();
381 translateKPluginToAppstream(QStringLiteral("name"), QStringLiteral("Name"), configObject: rootObject, writer, canEndWithDot: false);
382 translateKPluginToAppstream(QStringLiteral("summary"), QStringLiteral("Description"), configObject: rootObject, writer, canEndWithDot: false);
383 if (!i.website().isEmpty()) {
384 writer.writeStartElement(QStringLiteral("url"));
385 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("homepage"));
386 writer.writeCharacters(text: i.website());
387 writer.writeEndElement();
388 }
389
390 if (i.pluginId().startsWith(s: QLatin1String("org.kde."))) {
391 writer.writeStartElement(QStringLiteral("url"));
392 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("donation"));
393 writer.writeCharacters(QStringLiteral("https://www.kde.org/donate.php?app=%1").arg(a: i.pluginId()));
394 writer.writeEndElement();
395 }
396
397 const auto authors = i.authors();
398 if (!authors.isEmpty()) {
399 QStringList authorsText;
400 authorsText.reserve(asize: authors.size());
401 for (const auto &author : authors) {
402 authorsText += QStringLiteral("%1").arg(a: author.name());
403 }
404 writer.writeStartElement(QStringLiteral("developer"));
405 writer.writeAttribute(QStringLiteral("id"), QStringLiteral("kde.org"));
406 writer.writeTextElement(QStringLiteral("name"), text: authorsText.join(QStringLiteral(", ")));
407 writer.writeEndElement();
408 }
409
410 if (!i.iconName().isEmpty()) {
411 writer.writeStartElement(QStringLiteral("icon"));
412 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("stock"));
413 writer.writeCharacters(text: i.iconName());
414 writer.writeEndElement();
415 }
416 writer.writeTextElement(QStringLiteral("project_license"), text: KAboutLicense::byKeyword(keyword: i.license()).spdx());
417 writer.writeTextElement(QStringLiteral("metadata_license"), QStringLiteral("CC0-1.0"));
418 writer.writeEndElement();
419 writer.writeEndDocument();
420
421 exit(retcode: 0);
422}
423
424QString PackageTool::resolvePackageRootWithOptions()
425{
426 QString packageRoot;
427 if (d->parser->isSet(option: Options::packageRoot()) && d->parser->isSet(option: Options::global())) {
428 qWarning() << i18nc("The user entered conflicting options packageroot and global, this is the error message telling the user he can use only one",
429 "The packageroot and global options conflict with each other, please select only one.");
430 ::exit(status: 7);
431 } else if (d->parser->isSet(option: Options::packageRoot())) {
432 packageRoot = d->parser->value(option: Options::packageRoot());
433 // qDebug() << "(set via arg) d->packageRoot is: " << d->packageRoot;
434 } else if (d->parser->isSet(option: Options::global())) {
435 auto const paths = QStandardPaths::locateAll(type: QStandardPaths::GenericDataLocation, fileName: d->packageRoot, options: QStandardPaths::LocateDirectory);
436 if (!paths.isEmpty()) {
437 packageRoot = paths.last();
438 }
439 } else {
440 packageRoot = QStandardPaths::writableLocation(type: QStandardPaths::GenericDataLocation) + QLatin1Char('/') + d->packageRoot;
441 }
442 return packageRoot;
443}
444
445void PackageTool::listPackages(const QString &kpackageType, const QString &path)
446{
447 QStringList list = d->packages(type: kpackageType, path);
448 list.sort();
449 for (const QString &package : std::as_const(t&: list)) {
450 d->coutput(msg: package);
451 }
452 exit(retcode: 0);
453}
454
455void PackageToolPrivate::renderTypeTable(const QMap<QString, QString> &plugins)
456{
457 const QString nameHeader = i18n("KPackage Structure Name");
458 const QString pathHeader = i18n("Path");
459 int nameWidth = nameHeader.length();
460 int pathWidth = pathHeader.length();
461
462 QMapIterator<QString, QString> pluginIt(plugins);
463 while (pluginIt.hasNext()) {
464 pluginIt.next();
465 if (pluginIt.key().length() > nameWidth) {
466 nameWidth = pluginIt.key().length();
467 }
468
469 if (pluginIt.value().length() > pathWidth) {
470 pathWidth = pluginIt.value().length();
471 }
472 }
473
474 std::cout << nameHeader.toLocal8Bit().constData() << std::setw(nameWidth - nameHeader.length() + 2) << ' ' << pathHeader.toLocal8Bit().constData()
475 << std::setw(pathWidth - pathHeader.length() + 2) << ' ' << std::endl;
476 std::cout << std::setfill('-') << std::setw(nameWidth) << '-' << " " << std::setw(pathWidth) << '-' << " " << std::endl;
477 std::cout << std::setfill(' ');
478
479 pluginIt.toFront();
480 while (pluginIt.hasNext()) {
481 pluginIt.next();
482 std::cout << pluginIt.key().toLocal8Bit().constData() << std::setw(nameWidth - pluginIt.key().length() + 2) << ' '
483 << pluginIt.value().toLocal8Bit().constData() << std::setw(pathWidth - pluginIt.value().length() + 2) << std::endl;
484 }
485}
486
487void PackageToolPrivate::listTypes()
488{
489 coutput(i18n("Package types that are installable with this tool:"));
490 coutput(i18n("Built in:"));
491
492 QMap<QString, QString> builtIns;
493 builtIns.insert(i18n("KPackage/Generic"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/packages/"));
494 builtIns.insert(i18n("KPackage/GenericQML"), QStringLiteral(KPACKAGE_RELATIVE_DATA_INSTALL_DIR "/genericqml/"));
495
496 renderTypeTable(plugins: builtIns);
497
498 const QList<KPluginMetaData> offers = KPluginMetaData::findPlugins(QStringLiteral("kf6/packagestructure"));
499
500 if (!offers.isEmpty()) {
501 std::cout << std::endl;
502 coutput(i18n("Provided by plugins:"));
503
504 QMap<QString, QString> plugins;
505 for (const KPluginMetaData &info : offers) {
506 const QString type = readKPackageType(metaData: info);
507 if (type.isEmpty()) {
508 continue;
509 }
510 KPackage::Package pkg = KPackage::PackageLoader::self()->loadPackage(packageFormat: type);
511 plugins.insert(key: type, value: pkg.defaultPackageRoot());
512 }
513
514 renderTypeTable(plugins);
515 }
516}
517
518void PackageTool::packageInstalled(KPackage::PackageJob *job)
519{
520 bool success = (job->error() == KJob::NoError);
521 int exitcode = 0;
522 if (success) {
523 if (d->parser->isSet(option: Options::upgrade())) {
524 d->coutput(i18n("Successfully upgraded %1", job->package().path()));
525 } else {
526 d->coutput(i18n("Successfully installed %1", job->package().path()));
527 }
528 } else {
529 d->cerror(i18n("Error: Installation of %1 failed: %2", d->packageFile, job->errorText()));
530 exitcode = 4;
531 }
532 exit(retcode: exitcode);
533}
534
535void PackageTool::packageUninstalled(KPackage::PackageJob *job)
536{
537 bool success = (job->error() == KJob::NoError);
538 int exitcode = 0;
539 if (success) {
540 if (d->parser->isSet(option: Options::upgrade())) {
541 d->coutput(i18n("Upgrading package from file: %1", d->packageFile));
542 auto installJob = KPackage::PackageJob::install(packageFormat: d->kpackageType, sourcePackage: d->packageFile, packageRoot: d->packageRoot);
543 connect(sender: installJob, signal: &KPackage::PackageJob::finished, context: this, slot: [installJob, this]() {
544 packageInstalled(job: installJob);
545 });
546 return;
547 }
548 d->coutput(i18n("Successfully uninstalled %1", job->package().path()));
549 } else {
550 d->cerror(i18n("Error: Uninstallation of %1 failed: %2", d->packageFile, job->errorText()));
551 exitcode = 7;
552 }
553 exit(retcode: exitcode);
554}
555
556} // namespace KPackage
557
558#include "moc_kpackagetool.cpp"
559

source code of kpackage/src/kpackagetool/kpackagetool.cpp