Commit b5b6a46b authored by Eike Ziller's avatar Eike Ziller

ExtensionSystem: Use Qt 5 plugin metadata instead of .pluginspec files

Change-Id: I2b2c704260c613985a4bda179658ec1f8879e70f
Reviewed-by: default avatarChristian Kandeler <christian.kandeler@digia.com>
Reviewed-by: default avatarChristian Stenger <christian.stenger@digia.com>
Reviewed-by: default avatarDaniel Teske <daniel.teske@digia.com>
Reviewed-by: default avatarhjk <hjk121@nokiamail.com>
parent 8dc9d67e
......@@ -40,7 +40,6 @@ Thumbs.db
*.creator.user*
*.qbs.user*
*.qmlproject.user*
*.pluginspec
*.json
/src/app/Info.plist
app_version.h
......
......@@ -46,7 +46,7 @@
\list
\li \l{Getting and Building Qt Creator}
\li \l{Creating Your First Plugin}
\li \l{Plugin Specifications}
\li \l{Plugin Meta Data}
\li \l{Plugin Life Cycle}
\endlist
......
{
//! [1]
\"Name\" : \"Example\",
\"Version\" : \"0.0.1\",
\"CompatVersion\" : \"0.0.1\",
//! [1]
//! [2]
\"Vendor\" : \"My Company\",
\"Copyright\" : \"(C) My Company\",
\"License\" : \"BSD\",
\"Category\" : \"Examples\",
\"Description\" : \"Minimal plugin example.\",
\"Url\" : \"http://www.mycompany.com\",
//! [2]
//! [3]
$$dependencyList
//! [3]
}
//! [1]
<plugin name=\"Example\" version=\"0.0.1\" compatVersion=\"0.0.1\">
//! [1]
//! [2]
<vendor>MyCompany</vendor>
<copyright>(C) MyCompany</copyright>
<license>BSD</license>
<description>Minimal plugin example</description>
<url>http://www.mycompany.com</url>
//! [2]
//! [3]
$$dependencyList
//! [3]
</plugin>
......@@ -141,9 +141,10 @@
\li File
\li Role
\row
\li \c{Example.pluginspec.in}
\li Template plugin specification. QMake creates a \c{Example.pluginspec}
from this file, which is read by \QC to find out about the plugin.
\li \c{Example.json.in}
\li Plugin meta data template. QMake creates an \c{Example.json}
from this file, which is compiled into the plugin as meta data.
The meta data is read by \QC to find out about the plugin.
\row
\li \c{example.pro}
\li Project file, used by QMake to generate a Makefile that then is used to
......@@ -222,32 +223,32 @@
For more information about qmake, and writing .pro files in general,
see the \l{http://qt-project.org/doc/qt-4.8/qmake-manual.html}{qmake Manual}.
\section1 Plugin Specification
\section1 Plugin Meta Data Template
The .pluginspec file is an XML file that contains information that is needed by
The .json file is a JSON file that contains information that is needed by
the plugin manager to find your plugin and resolve its dependencies before actually
loading your plugin's library file. We will only have a short look at it here.
For more information, see \l{Plugin Specifications}.
For more information, see \l{Plugin Meta Data}.
The wizard doesn't actually create a .pluginspec file directly, but instead a
.pluginspec.in file. qmake uses this to generate the actual plugin specification
The wizard doesn't actually create a .json file directly, but instead a
.json.in file. qmake uses this to generate the actual plugin .json meta data
file, replacing variables like \c{QTCREATOR_VERSION} with their actual values.
Therefore you need to escape all backslashes and quotes in the .pluginspec.in file
Therefore you need to escape all backslashes and quotes in the .json.in file
(i.e. you need to write \c{\\} to get a backslash and \c{\"} to get a quote
in the generated plugin specification).
in the generated plugin JSON meta data).
\snippet exampleplugin/Example.pluginspec.in 1
\snippet exampleplugin/Example.json.in 1
The main tag of the plugin specification that is created by the wizard
defines the name of your plugin, its version, and with what version of this plugin
The first items in the meta data that is created by the wizard
define the name of your plugin, its version, and with what version of this plugin
the current version is binary compatible with.
\snippet exampleplugin/Example.pluginspec.in 2
\snippet exampleplugin/Example.json.in 2
After the main tag you'll find the information about the plugin
After that you'll find the information about the plugin
that you gave in the project wizard.
\snippet exampleplugin/Example.pluginspec.in 3
\snippet exampleplugin/Example.json.in 3
The \c{$$dependencyList} variable is automatically replaced by the dependency information
in \c{QTC_PLUGIN_DEPENDS} and \c{QTC_PLUGIN_RECOMMENDS} from your plugin's .pro file.
......@@ -272,6 +273,9 @@
All \QC plugins must be derived from \l{ExtensionSystem::IPlugin} and
are QObjects. The \c{Q_PLUGIN_METADATA} macro is necessary to create a valid Qt plugin.
The \c IID given in the macro must be \c{org.qt-project.Qt.QtCreatorPlugin}, to identify it
as a \QC plugin, and \c FILE must point to the plugin's meta data file as described
in \l{Plugin Meta Data}.
\snippet exampleplugin/exampleplugin.h plugin functions
......
......@@ -24,12 +24,18 @@
manager takes when you start or shut down \QC. This section describes
the process and the state that plugins go through in detail.
You can get more information about what happens when you start \QC by running it with the
environment variable \c QT_LOGGING_RULES set to \c {qtc.extensionsystem*=true} which enables
logging of plugin-related debug output.
When you start \QC, the plugin manager does the following:
\list 1
\li Looks in its search paths for
all .pluginspec files, and reads them. This is the first point where
loading a plugin can fail in the worst case of a malformed plugin spec.
all dynamic libraries, and reads their meta data. All libraries without meta data
and all libraries without the \c{org.qt-project.Qt.QtCreatorPlugin} IID are ignored.
This is the first point where loading a plugin can fail in the worst case of malformed
meta data.
\li Creates an instance of the \l{ExtensionSystem::PluginSpec} class for
each plugin. This class is a container for
......
......@@ -54,7 +54,7 @@ DEV_HELP_DEP_FILES = \
$$PWD/api/creating-plugins.qdoc \
$$PWD/api/getting-and-building.qdoc \
$$PWD/api/first-plugin.qdoc \
$$PWD/api/plugin-specifications.qdoc \
$$PWD/api/plugin-metadata.qdoc \
$$PWD/api/plugin-lifecycle.qdoc \
$$PWD/api/pluginmanager.qdoc \
$$PWD/api/qtcreator-dev$${COMPAT}.qdocconf
......
......@@ -7,7 +7,7 @@ QtcProduct {
type: ["dynamiclibrary", "pluginSpec"]
installDir: project.ide_plugin_path
property var pluginspecreplacements
property var pluginJsonReplacements
property var pluginRecommends: []
property string minimumQtVersion: "5.3.1"
......@@ -17,7 +17,7 @@ QtcProduct {
destinationDirectory: project.ide_plugin_path
Depends { name: "ExtensionSystem" }
Depends { name: "pluginspec" }
Depends { name: "pluginjson" }
Depends {
condition: project.testsEnabled
name: "Qt.test"
......@@ -39,9 +39,9 @@ QtcProduct {
cpp.includePaths: [pluginIncludeBase]
Group {
name: "PluginSpec"
files: [ product.name + ".pluginspec.in" ]
fileTags: ["pluginSpecIn"]
name: "PluginMetaData"
files: [ product.name + ".json.in" ]
fileTags: ["pluginJsonIn"]
}
Group {
......
......@@ -5,27 +5,31 @@ import qbs.FileInfo
Module {
Depends { id: qtcore; name: "Qt.core" }
additionalProductTypes: qtcore.versionMajor < 5 ? ["pluginSpec"] : ["qt_plugin_metadata"]
additionalProductTypes: ["qt_plugin_metadata"]
Rule {
inputs: ["pluginSpecIn"]
inputs: ["pluginJsonIn"]
Artifact {
fileTags: ["pluginSpec"]
filePath: input.fileName.replace(/\.[^\.]*$/,'')
fileTags: ["qt_plugin_metadata"]
filePath: {
var destdir = FileInfo.joinPaths(product.moduleProperty("Qt.core",
"generatedFilesDir"), input.fileName);
return destdir.replace(/\.[^\.]*$/,'')
}
}
prepare: {
var cmd = new JavaScriptCommand();
cmd.description = "prepare " + FileInfo.fileName(output.filePath);
cmd.highlight = "codegen";
cmd.pluginspecreplacements = product.pluginspecreplacements;
cmd.pluginJsonReplacements = product.pluginJsonReplacements;
cmd.plugin_depends = [];
var deps = product.dependencies;
for (var d in deps) {
var depdeps = deps[d].dependencies;
for (var dd in depdeps) {
if (depdeps[dd].name == 'pluginspec') {
if (depdeps[dd].name == 'pluginjson') {
cmd.plugin_depends.push(deps[d].name);
break;
}
......@@ -35,7 +39,7 @@ Module {
cmd.sourceCode = function() {
var i;
var vars = pluginspecreplacements || {};
var vars = pluginJsonReplacements || {};
var inf = new TextFile(input.filePath);
var all = inf.readAll();
// replace quoted quotes
......@@ -46,15 +50,15 @@ Module {
vars['IDE_VERSION_MAJOR'] = project.ide_version_major;
vars['IDE_VERSION_MINOR'] = project.ide_version_minor;
vars['IDE_VERSION_RELEASE'] = project.ide_version_release;
var deplist = ["<dependencyList>"];
var deplist = [];
for (i in plugin_depends) {
deplist.push(" <dependency name=\"" + plugin_depends[i] + "\" version=\"" + project.qtcreator_version + "\"/>");
deplist.push(" { \"Name\" : \"" + plugin_depends[i] + "\", \"Version\" : \"" + project.qtcreator_version + "\" }");
}
for (i in plugin_recommends) {
deplist.push(" <dependency name=\"" + plugin_recommends[i] + "\" version=\"" + project.qtcreator_version + "\" type=\"optional\"/>");
deplist.push(" { \"Name\" : \"" + plugin_recommends[i] + "\", \"Version\" : \"" + project.qtcreator_version + "\", \"Type\" : \"optional\" }");
}
deplist.push(" </dependencyList>");
vars['dependencyList'] = deplist.join("\n");
deplist = deplist.join(",\n")
vars['dependencyList'] = "\"Dependencies\" : [\n" + deplist + "\n ]";
for (i in vars) {
all = all.replace(new RegExp('\\\$\\\$' + i + '(?!\w)', 'g'), vars[i]);
}
......@@ -66,34 +70,5 @@ Module {
return cmd;
}
}
Rule {
inputs: ["pluginSpec"]
Artifact {
fileTags: ["qt_plugin_metadata"]
filePath: {
var destdir = FileInfo.joinPaths(product.moduleProperty("Qt.core", "generatedFilesDir"),
input.fileName);
return destdir.replace(/\.[^\.]*$/, '.json');
}
}
prepare: {
var xslFile = project.path + "/../qtcreatorplugin2json.xsl"; // project is "Plugins"
var xmlPatternsPath = product.moduleProperty("Qt.core", "binPath") + "/xmlpatterns";
var args = [
"-no-format",
"-output",
output.filePath,
xslFile,
input.filePath
];
var cmd = new Command(xmlPatternsPath, args);
cmd.description = "generating " + FileInfo.fileName(output.filePath);
cmd.highlight = "codegen";
return cmd;
}
}
}
......@@ -18,7 +18,7 @@ unix:!macx:!isEmpty(copydata):SUBDIRS += bin
OTHER_FILES += dist/copyright_template.txt \
$$files(dist/changes-*) \
qtcreator.qbs \
qbs/pluginspec/pluginspec.qbs \
qbs/pluginjson/pluginjson.qbs \
$$files(dist/installer/ifw/config/config-*) \
dist/installer/ifw/packages/org.qtproject.qtcreator/meta/package.xml.in \
dist/installer/ifw/packages/org.qtproject.qtcreator.application/meta/installscript.qs \
......
......@@ -95,7 +95,7 @@ while (1) {
next;
}
if (!$hasContact && $file !~ /\.pluginspec\.in$/) {
if (!$hasContact && $file !~ /\.json\.in$/) {
print "$file\tERROR\tWrong contact\n";
next;
}
......
{
\"Name\" : \"%PluginName%\",
\"Version\" : \"0.0.1\",
\"CompatVersion\" : \"0.0.1\",
\"Vendor\" : \"%VendorName%\",
\"Copyright\" : \"%Copyright%\",
\"License\" : \"%License%\",
\"Description\" : \"%Description%\",
\"Url\" : \"%URL%\",
$$dependencyList
}
<plugin name=\"%PluginName%\" version=\"0.0.1\" compatVersion=\"0.0.1\">
<vendor>%VendorName%</vendor>
<copyright>%Copyright%</copyright>
<license>%License%</license>
<description>%Description%</description>
<url>%URL%</url>
$$dependencyList
</plugin>
......@@ -44,7 +44,7 @@ leave room for the Qt 4 target page.
<displaycategory>Libraries</displaycategory>
<files>
<file source="myplugin.pro" target="%PluginName:l%.pro" openproject="true"/>
<file source="MyPlugin.pluginspec.in" target="%PluginName%.pluginspec.in" openeditor="true"/>
<file source="MyPlugin.json.in" target="%PluginName%.json.in" openeditor="true"/>
<file source="myplugin_global.h" target="%PluginName:l%_global.%CppHeaderSuffix%" openeditor="true"/>
<file source="mypluginconstants.h" target="%PluginName:l%constants.%CppHeaderSuffix%" openeditor="true"/>
<file source="myplugin.h" target="%PluginName:l%plugin.%CppHeaderSuffix%" openeditor="true"/>
......@@ -69,12 +69,12 @@ leave room for the Qt 4 target page.
<fielddescription>Copyright:</fielddescription>
</field>
<field name="License">
<fieldcontrol class="QTextEdit"
defaulttext="Put your license text here" />
<fieldcontrol class="QLineEdit"
defaulttext="Put your license information here" />
<fielddescription>License:</fielddescription>
</field>
<field name="Description">
<fieldcontrol class="QTextEdit"
<fieldcontrol class="QLineEdit"
defaulttext="Put a short description of your plugin here"/>
<fielddescription>Description:</fielddescription>
</field>
......
......@@ -314,7 +314,7 @@ int main(int argc, char **argv)
#endif
// Manually determine -settingspath command line option
// We can't use the regular way of the plugin manager, because that needs to parse pluginspecs
// We can't use the regular way of the plugin manager, because that needs to parse plugin meta data
// but the settings path can influence which plugins are enabled
QString settingsPath;
QStringList customPluginPaths;
......@@ -358,7 +358,7 @@ int main(int argc, char **argv)
QLatin1String(Core::Constants::IDE_SETTINGSVARIANT_STR),
QLatin1String("QtCreator"));
PluginManager pluginManager;
PluginManager::setFileExtension(QLatin1String("pluginspec"));
PluginManager::setPluginIID(QLatin1String("org.qt-project.Qt.QtCreatorPlugin"));
PluginManager::setGlobalSettings(globalSettings);
PluginManager::setSettings(settings);
......@@ -449,7 +449,7 @@ int main(int argc, char **argv)
}
if (!coreplugin) {
QString nativePaths = QDir::toNativeSeparators(pluginPaths.join(QLatin1Char(',')));
const QString reason = QCoreApplication::translate("Application", "Could not find 'Core.pluginspec' in %1").arg(nativePaths);
const QString reason = QCoreApplication::translate("Application", "Could not find Core plugin in %1").arg(nativePaths);
displayError(msgCoreLoadFailure(reason));
return 1;
}
......
......@@ -30,6 +30,7 @@
#ifndef EXTENSIONSYSTEM_GLOBAL_H
#define EXTENSIONSYSTEM_GLOBAL_H
#include <QLoggingCategory>
#include <qglobal.h>
#if defined(EXTENSIONSYSTEM_LIBRARY)
......@@ -38,4 +39,6 @@
# define EXTENSIONSYSTEM_EXPORT Q_DECL_IMPORT
#endif
Q_DECLARE_LOGGING_CATEGORY(pluginLog)
#endif // EXTENSIONSYSTEM_GLOBAL_H
......@@ -40,6 +40,7 @@
#include <QDateTime>
#include <QDir>
#include <QFile>
#include <QLibrary>
#include <QMetaProperty>
#include <QSettings>
#include <QTextStream>
......@@ -53,6 +54,8 @@
#include <QTest>
#endif
Q_LOGGING_CATEGORY(pluginLog, "qtc.extensionsystem")
const char C_IGNORED_PLUGINS[] = "Plugins/Ignored";
const char C_FORCEENABLED_PLUGINS[] = "Plugins/ForceEnabled";
const int DELAYED_INITIALIZE_INTERVAL = 20; // ms
......@@ -406,25 +409,25 @@ void PluginManager::setPluginPaths(const QStringList &paths)
}
/*!
The file extension of plugin description files.
The default is "xml".
The IID that valid plugins must have.
\sa setFileExtension()
\sa setPluginIID()
*/
QString PluginManager::fileExtension()
QString PluginManager::pluginIID()
{
return d->extension;
return d->pluginIID;
}
/*!
Sets the file extension of plugin description files.
The default is "xml".
Sets the IID that valid plugins must have. Only plugins with this IID are loaded, others are
silently ignored.
At the moment this must be called before setPluginPaths() is called.
// ### TODO let this + setPluginPaths read the plugin specs lazyly whenever loadPlugins() or plugins() is called.
// ### TODO let this + setPluginPaths read the plugin meta data lazyly whenever loadPlugins() or plugins() is called.
*/
void PluginManager::setFileExtension(const QString &extension)
void PluginManager::setPluginIID(const QString &iid)
{
d->extension = extension;
d->pluginIID = iid;
}
/*!
......@@ -892,7 +895,6 @@ void PluginManagerPrivate::nextDelayedInitialize()
\internal
*/
PluginManagerPrivate::PluginManagerPrivate(PluginManager *pluginManager) :
extension(QLatin1String("xml")),
delayedInitializeTimer(0),
shutdownEventLoop(0),
m_profileElapsedMS(0),
......@@ -1216,6 +1218,8 @@ void PluginManagerPrivate::loadPlugin(PluginSpec *spec, PluginSpec::State destSt
*/
void PluginManagerPrivate::setPluginPaths(const QStringList &paths)
{
qCDebug(pluginLog) << "Plugin search paths:" << paths;
qCDebug(pluginLog) << "Required IID:" << pluginIID;
pluginPaths = paths;
readSettings();
readPluginPaths();
......@@ -1231,14 +1235,16 @@ void PluginManagerPrivate::readPluginPaths()
pluginSpecs.clear();
pluginCategories.clear();
QStringList specFiles;
QStringList pluginFiles;
QStringList searchPaths = pluginPaths;
while (!searchPaths.isEmpty()) {
const QDir dir(searchPaths.takeFirst());
const QString pattern = QLatin1String("*.") + extension;
const QFileInfoList files = dir.entryInfoList(QStringList(pattern), QDir::Files);
foreach (const QFileInfo &file, files)
specFiles << file.absoluteFilePath();
const QFileInfoList files = dir.entryInfoList(QDir::Files | QDir::NoSymLinks);
foreach (const QFileInfo &file, files) {
const QString filePath = file.absoluteFilePath();
if (QLibrary::isLibrary(filePath))
pluginFiles.append(filePath);
}
const QFileInfoList dirs = dir.entryInfoList(QDir::Dirs|QDir::NoDotAndDotDot);
foreach (const QFileInfo &subdir, dirs)
searchPaths << subdir.absoluteFilePath();
......@@ -1246,9 +1252,10 @@ void PluginManagerPrivate::readPluginPaths()
defaultCollection = new PluginCollection(QString());
pluginCategories.insert(QString(), defaultCollection);
foreach (const QString &specFile, specFiles) {
foreach (const QString &pluginFile, pluginFiles) {
PluginSpec *spec = new PluginSpec;
spec->d->read(specFile);
if (!spec->d->read(pluginFile)) // not a Qt Creator plugin
continue;
PluginCollection *collection = 0;
// find correct plugin collection or create a new one
......
......@@ -118,10 +118,10 @@ public:
static void loadPlugins();
static QStringList pluginPaths();
static void setPluginPaths(const QStringList &paths);
static QString pluginIID();
static void setPluginIID(const QString &iid);
static QList<PluginSpec *> plugins();
static QHash<QString, PluginCollection *> pluginCollections();
static void setFileExtension(const QString &extension);
static QString fileExtension();
static bool hasError();
// Settings
......
......@@ -102,7 +102,7 @@ public:
QList<PluginSpec *> pluginSpecs;
QList<TestSpec> testSpecs;
QStringList pluginPaths;
QString extension;
QString pluginIID;
QList<QObject *> allObjects; // ### make this a QList<QPointer<QObject> > > ?
QStringList defaultDisabledPlugins; // Plugins/Ignored from install settings
QStringList defaultEnabledPlugins; // Plugins/ForceEnabled from install settings
......
This diff is collapsed.
......@@ -33,10 +33,12 @@
#include "pluginspec.h"
#include "iplugin.h"
#include <QJsonObject>
#include <QObject>
#include <QPluginLoader>
#include <QRegExp>
#include <QStringList>
#include <QXmlStreamReader>
#include <QRegExp>
namespace ExtensionSystem {
......@@ -62,6 +64,8 @@ public:
IPlugin::ShutdownFlag stop();
void kill();
QPluginLoader loader;
QString name;
QString version;
QString compatVersion;
......@@ -98,18 +102,12 @@ public:
void disableIndirectlyIfDependencyDisabled();
bool readMetaData(const QJsonObject &metaData);
private:
PluginSpec *q;
bool reportError(const QString &err);
void readPluginSpec(QXmlStreamReader &reader);
void readDependencies(QXmlStreamReader &reader);
void readDependencyEntry(QXmlStreamReader &reader);
void readArgumentDescriptions(QXmlStreamReader &reader);
void readArgumentDescription(QXmlStreamReader &reader);
bool readBooleanValue(QXmlStreamReader &reader, const char *key);
static QRegExp &versionRegExp();
};
......
{
\"Name\" : \"AnalyzerBase\",
\"Version\" : \"$$QTCREATOR_VERSION\",
\"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\",
\"Vendor\" : \"Digia Plc\",
\"Copyright\" : \"(C) 2014 Digia Plc\",
\"License\" : [ \"Commercial Usage\",
\"\",
\"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Digia.\",
\"\",
\"GNU Lesser General Public License Usage\",
\"\",
\"Alternatively, this plugin may be used under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. Please review the following information to ensure the GNU Lesser General Public License version 2.1 requirements will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.\"
],
\"Category\" : \"Code Analyzer\",
\"Description\" : \"Code Analyzer Base Plugin.\",
\"Url\" : \"http://www.qt-project.org\",
$$dependencyList
}
<plugin name=\"AnalyzerBase\" version=\"$$QTCREATOR_VERSION\" compatVersion=\"$$QTCREATOR_COMPAT_VERSION\">
<vendor>Digia Plc</vendor>
<copyright>(C) 2014 Digia Plc</copyright>
<license>
Commercial Usage
Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Digia.
GNU Lesser General Public License Usage
Alternatively, this plugin may be used under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. Please review the following information to ensure the GNU Lesser General Public License version 2.1 requirements will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.
</license>
<category>Code Analyzer</category>
<description>Code Analyzer Base Plugin</description>
<url>http://www.qt-project.org</url>
$$dependencyList
</plugin>
{
\"Name\" : \"Android\",
\"Version\" : \"$$QTCREATOR_VERSION\",
\"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\",
\"Vendor\" : \"KDE Necessitas\",
\"Copyright\" : \"(C) 2014 BogDan Vatra\",
\"License\" : [ \"Commercial Usage\",
\"\",
\"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and Digia.\",
\"\",
\"GNU Lesser General Public License Usage\",
\"\",
\"Alternatively, this plugin may be used under the terms of the GNU Lesser General Public License version 2.1 as published by the Free Software Foundation. Please review the following information to ensure the GNU Lesser General Public License version 2.1 requirements will be met: http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html.\"
],
\"Category\" : \"Device Support\",
\"Description\" : \"Support for deployment to and execution on Android Devices.\",