KPackage

kpackagetool.cpp
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(0, this, &PackageTool::runMain);
71}
72
73PackageTool::~PackageTool()
74{
75 delete d;
76}
77
78void PackageTool::runMain()
79{
80 if (d->parser->isSet(Options::hash())) {
81 const QString path = d->parser->value(Options::hash());
83 KPackage::Package package(&structure);
84 package.setPath(path);
85 const QString hash = QString::fromLocal8Bit(package.cryptographicHash(QCryptographicHash::Sha1));
86 if (hash.isEmpty()) {
87 d->coutput(i18n("Failed to generate a Package hash for %1", path));
88 exit(9);
89 } else {
90 d->coutput(i18n("SHA1 hash for Package at %1: '%2'", package.path(), hash));
91 exit(0);
92 }
93 return;
94 }
95
96 if (d->parser->isSet(Options::listTypes())) {
97 d->listTypes();
98 exit(0);
99 return;
100 }
101
102 if (d->parser->isSet(Options::type())) {
103 d->kpackageType = d->parser->value(Options::type());
104 }
105 d->packageRoot = KPackage::PackageLoader::self()->loadPackage(d->kpackageType).defaultPackageRoot();
106
107 if (d->parser->isSet(Options::remove())) {
108 d->package = d->parser->value(Options::remove());
109 } else if (d->parser->isSet(Options::upgrade())) {
110 d->package = d->parser->value(Options::upgrade());
111 } else if (d->parser->isSet(Options::install())) {
112 d->package = d->parser->value(Options::install());
113 } else if (d->parser->isSet(Options::show())) {
114 d->package = d->parser->value(Options::show());
115 } else if (d->parser->isSet(Options::appstream())) {
116 d->package = d->parser->value(Options::appstream());
117 }
118
119 if (!QDir::isAbsolutePath(d->package)) {
120 d->packageFile = QDir(QDir::currentPath() + QLatin1Char('/') + d->package).absolutePath();
121 d->packageFile = QFileInfo(d->packageFile).canonicalFilePath();
122 if (d->parser->isSet(Options::upgrade())) {
123 d->package = d->packageFile;
124 }
125 } else {
126 d->packageFile = d->package;
127 }
128
129 if (!PackageLoader::self()->loadPackageStructure(d->kpackageType)) {
130 qWarning() << "Package type" << d->kpackageType << "not found";
131 }
132
133 if (d->parser->isSet(Options::show())) {
134 const QString pluginName = d->package;
135 showPackageInfo(pluginName);
136 return;
137 } else if (d->parser->isSet(Options::appstream())) {
138 const QString pluginName = d->package;
139 showAppstreamInfo(pluginName);
140 return;
141 }
142
143 if (d->parser->isSet(Options::list())) {
144 QString packageRoot = resolvePackageRootWithOptions();
145 d->coutput(i18n("Listing KPackageType: %1 in %2", d->kpackageType, packageRoot));
146 listPackages(d->kpackageType, packageRoot);
147 exit(0);
148 } else {
149 // install, remove or upgrade
150 d->packageRoot = resolvePackageRootWithOptions();
151
152 if (d->parser->isSet(Options::remove()) || d->parser->isSet(Options::upgrade())) {
153 QString pkgPath;
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(QLatin1Char('/'))) {
167 _p.append(QLatin1Char('/'));
168 }
169 _p.append(d->package);
170
171 if (!d->parser->isSet(Options::type())) {
172 d->kpackageType = readKPackageType(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(d->kpackageType);
189
190 // Uninstalling ...
191 if (installed.contains(pluginName)) { // Assume it's a plugin id
192 KPackage::PackageJob *uninstallJob = KPackage::PackageJob::uninstall(d->kpackageType, pluginName, d->packageRoot);
193 connect(uninstallJob, &KPackage::PackageJob::finished, this, [uninstallJob, this]() {
194 packageUninstalled(uninstallJob);
195 });
196 return;
197 } else {
198 d->coutput(i18n("Error: Plugin %1 is not installed.", pluginName));
199 exit(2);
200 }
201 }
202 if (d->parser->isSet(Options::install())) {
203 auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
204 connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
205 packageInstalled(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(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;
234 for (const KPluginMetaData &data : dataList) {
235 if (!result.contains(data.pluginId())) {
236 result << data.pluginId();
237 }
238 }
239 return result;
240}
241
242void PackageTool::showPackageInfo(const QString &pluginName)
243{
245 pkg.setDefaultPackageRoot(d->packageRoot);
246
247 if (QFile::exists(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(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(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(configField));
281 const QJsonValue native = configObject.value(configField);
282 if (native.isUndefined()) {
283 return false;
284 }
285
286 QString content = native.toString();
287 if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
288 content.chop(1);
289 }
290 writer.writeTextElement(tagName, content);
291 for (auto it = configObject.begin(), itEnd = configObject.end(); it != itEnd; ++it) {
292 const auto match = rx.match(it.key());
293
294 if (match.hasMatch()) {
295 QString content = it->toString();
296 if (!canEndWithDot && content.endsWith(QLatin1Char('.'))) {
297 content.chop(1);
298 }
299
300 writer.writeStartElement(tagName);
301 writer.writeAttribute(QStringLiteral("xml:lang"), match.captured(1));
302 writer.writeCharacters(content);
303 writer.writeEndElement();
304 }
305 }
306 return true;
307}
308
309void PackageTool::showAppstreamInfo(const QString &pluginName)
310{
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(pluginName + QStringLiteral("/metadata.json"))) {
317 i = KPluginMetaData::fromJsonFile(pluginName + QStringLiteral("/metadata.json"));
318 } else {
320
321 pkg.setDefaultPackageRoot(d->packageRoot);
322
323 if (QFile::exists(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(3);
335 return;
336 }
337 QString parentApp = i.value(QLatin1String("X-KDE-ParentApp"));
338
339 if (i.value(QStringLiteral("NoDisplay"), false)) {
340 std::exit(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(Options::appstreamOutput());
354 if (!outputPath.isEmpty()) {
355 auto outputUrl = QUrl::fromUserInput(outputPath);
356 outputFile.reset(new QFile(outputUrl.toLocalFile()));
357 if (!outputFile->open(QFile::WriteOnly | QFile::Text)) {
358 *cerr << "Failed to open output file for writing.";
359 exit(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(10);
367 }
368
369 QXmlStreamWriter writer(outputDevice);
370 writer.setAutoFormatting(true);
371 writer.writeStartDocument();
372 writer.writeStartElement(QStringLiteral("component"));
373 writer.writeAttributes(componentAttributes);
374
375 writer.writeTextElement(QStringLiteral("id"), i.pluginId());
376 if (!parentApp.isEmpty()) {
377 writer.writeTextElement(QStringLiteral("extends"), parentApp);
378 }
379
380 const QJsonObject rootObject = i.rawData()[QStringLiteral("KPlugin")].toObject();
381 translateKPluginToAppstream(QStringLiteral("name"), QStringLiteral("Name"), rootObject, writer, false);
382 translateKPluginToAppstream(QStringLiteral("summary"), QStringLiteral("Description"), rootObject, writer, false);
383 if (!i.website().isEmpty()) {
384 writer.writeStartElement(QStringLiteral("url"));
385 writer.writeAttribute(QStringLiteral("type"), QStringLiteral("homepage"));
386 writer.writeCharacters(i.website());
387 writer.writeEndElement();
388 }
389
390 if (i.pluginId().startsWith(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(i.pluginId()));
394 writer.writeEndElement();
395 }
396
397 const auto authors = i.authors();
398 if (!authors.isEmpty()) {
399 QStringList authorsText;
400 authorsText.reserve(authors.size());
401 for (const auto &author : authors) {
402 authorsText += QStringLiteral("%1").arg(author.name());
403 }
404 writer.writeStartElement(QStringLiteral("developer"));
405 writer.writeAttribute(QStringLiteral("id"), QStringLiteral("kde.org"));
406 writer.writeTextElement(QStringLiteral("name"), 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(i.iconName());
414 writer.writeEndElement();
415 }
416 writer.writeTextElement(QStringLiteral("project_license"), KAboutLicense::byKeyword(i.license()).spdx());
417 writer.writeTextElement(QStringLiteral("metadata_license"), QStringLiteral("CC0-1.0"));
418 writer.writeEndElement();
419 writer.writeEndDocument();
420
421 exit(0);
422}
423
424QString PackageTool::resolvePackageRootWithOptions()
425{
426 QString packageRoot;
427 if (d->parser->isSet(Options::packageRoot()) && d->parser->isSet(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(7);
431 } else if (d->parser->isSet(Options::packageRoot())) {
432 packageRoot = d->parser->value(Options::packageRoot());
433 // qDebug() << "(set via arg) d->packageRoot is: " << d->packageRoot;
434 } else if (d->parser->isSet(Options::global())) {
436 if (!paths.isEmpty()) {
437 packageRoot = paths.last();
438 }
439 } else {
441 }
442 return packageRoot;
443}
444
445void PackageTool::listPackages(const QString &kpackageType, const QString &path)
446{
447 QStringList list = d->packages(kpackageType, path);
448 list.sort();
449 for (const QString &package : std::as_const(list)) {
450 d->coutput(package);
451 }
452 exit(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(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
505 for (const KPluginMetaData &info : offers) {
506 const QString type = readKPackageType(info);
507 if (type.isEmpty()) {
508 continue;
509 }
511 plugins.insert(type, 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(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(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(Options::upgrade())) {
541 d->coutput(i18n("Upgrading package from file: %1", d->packageFile));
542 auto installJob = KPackage::PackageJob::install(d->kpackageType, d->packageFile, d->packageRoot);
543 connect(installJob, &KPackage::PackageJob::finished, this, [installJob, this]() {
544 packageInstalled(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(exitcode);
554}
555
556} // namespace KPackage
557
558#include "moc_kpackagetool.cpp"
static KAboutLicense byKeyword(const QString &keyword)
QString spdx() const
int error() const
void finished(KJob *job)
QString errorText() const
KJob subclass that allows async install/update/uninstall operations for packages.
Definition packagejob.h:27
static PackageJob * uninstall(const QString &packageFormat, const QString &pluginId, const QString &packageRoot=QString())
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.
Package loadPackage(const QString &packageFormat, const QString &packagePath=QString())
Load a Package plugin.
static PackageLoader * self()
Return the active plugin loader.
QList< KPluginMetaData > listPackages(const QString &packageFormat, const QString &packageRoot=QString())
List all available packages of a certain type.
This class is used to define the filesystem structure of a package type.
object representing an installed package
Definition package.h:63
void setPath(const QString &path)
Sets the path to the root of this package.
Definition package.cpp:439
QString defaultPackageRoot() const
Definition package.cpp:128
bool isValid() const
Definition package.cpp:66
const QString path() const
Definition package.cpp:558
void setDefaultPackageRoot(const QString &packageRoot)
Sets preferred package root.
Definition package.cpp:133
KPluginMetaData metadata() const
Definition package.cpp:179
QString pluginId() const
QString website() const
QString license() const
QList< KAboutPerson > authors() const
QJsonObject rawData() const
bool value(const QString &key, bool defaultValue) const
QString fileName() const
static QList< KPluginMetaData > findPlugins(const QString &directory, std::function< bool(const KPluginMetaData &)> filter={}, KPluginMetaDataOptions options={})
QString iconName() const
QString name() const
bool isValid() const
static KPluginMetaData fromJsonFile(const QString &jsonFile)
QString description() const
QString i18nc(const char *context, const char *text, const TYPE &arg...)
QString i18n(const char *text, const TYPE &arg...)
Type type(const QSqlDatabase &db)
KCOREADDONS_EXPORT Result match(QStringView pattern, QStringView str)
QString path(const QString &relativePath)
KIOCORE_EXPORT QStringList list(const QString &fileClass)
const char * constData() const const
QString absolutePath() const const
QString currentPath()
bool isAbsolutePath(const QString &path)
bool exists() const const
QString absoluteFilePath() const const
QString canonicalFilePath() const const
iterator begin()
iterator end()
QJsonValue value(QLatin1StringView key) const const
bool isUndefined() const const
QString toString() const const
bool isEmpty() const const
void reserve(qsizetype size)
iterator insert(const Key &key, const T &value)
QStringList locateAll(StandardLocation type, const QString &fileName, LocateOptions options)
QString writableLocation(StandardLocation type)
QString & append(QChar ch)
void chop(qsizetype n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLocal8Bit(QByteArrayView str)
bool isEmpty() const const
QString last(qsizetype n) const const
qsizetype length() const const
bool startsWith(QChar c, Qt::CaseSensitivity cs) const const
QByteArray toLocal8Bit() const const
bool contains(QLatin1StringView str, Qt::CaseSensitivity cs) const const
QString join(QChar separator) const const
void sort(Qt::CaseSensitivity cs)
QFuture< ArgsType< Signal > > connect(Sender *sender, Signal signal)
QUrl fromUserInput(const QString &userInput, const QString &workingDirectory, UserInputResolutionOptions options)
void setAutoFormatting(bool enable)
void writeAttribute(QAnyStringView namespaceUri, QAnyStringView name, QAnyStringView value)
void writeAttributes(const QXmlStreamAttributes &attributes)
void writeCharacters(QAnyStringView text)
void writeEndDocument()
void writeEndElement()
void writeStartDocument()
void writeStartElement(QAnyStringView namespaceUri, QAnyStringView name)
void writeTextElement(QAnyStringView namespaceUri, QAnyStringView name, QAnyStringView text)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Tue Mar 26 2024 11:16:48 by doxygen 1.10.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.