diff --git a/src/Add-ons/Addon.cxx b/src/Add-ons/Addon.cxx new file mode 100644 index 000000000..be7e2d035 --- /dev/null +++ b/src/Add-ons/Addon.cxx @@ -0,0 +1,197 @@ +// -*- coding: utf-8 -*- +// +// Addon.cxx --- FlightGear class holding add-on metadata +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include +#include +#include + +#include +#include +#include +#include + +#include
+#include + +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonVersion.hxx" + +using std::string; + +namespace flightgear +{ + +Addon::Addon(std::string id, AddonVersion version, SGPath basePath, + std::string minFGVersionRequired, std::string maxFGVersionRequired, + SGPropertyNode* addonNode) + : _id(std::move(id)), + _version(new AddonVersion(std::move(version))), + _basePath(std::move(basePath)), + _minFGVersionRequired(std::move(minFGVersionRequired)), + _maxFGVersionRequired(std::move(maxFGVersionRequired)), + _addonNode(addonNode) +{ + if (_minFGVersionRequired.empty()) { + // This add-on metadata class appeared in FlightGear 2017.4.0 + _minFGVersionRequired = "2017.4.0"; + } + + if (_maxFGVersionRequired.empty()) { + _maxFGVersionRequired = "none"; // special value + } +} + +Addon::Addon() + : Addon(std::string(), AddonVersion(), SGPath(), std::string(), + std::string(), nullptr) +{ } + +std::string Addon::getId() const +{ return _id; } + +void Addon::setId(const std::string& addonId) +{ _id = addonId; } + +std::string Addon::getName() const +{ return _name; } + +void Addon::setName(const std::string& addonName) +{ _name = addonName; } + +AddonVersionRef Addon::getVersion() const +{ return _version; } + +void Addon::setVersion(const AddonVersion& addonVersion) +{ _version.reset(new AddonVersion(addonVersion)); } + +std::string Addon::getShortDescription() const +{ return _shortDescription; } + +void Addon::setShortDescription(const std::string& addonShortDescription) +{ _shortDescription = addonShortDescription; } + +std::string Addon::getLongDescription() const +{ return _longDescription; } + +void Addon::setLongDescription(const std::string& addonLongDescription) +{ _longDescription = addonLongDescription; } + +SGPath Addon::getBasePath() const +{ return _basePath; } + +void Addon::setBasePath(const SGPath& addonBasePath) +{ _basePath = addonBasePath; } + +std::string Addon::getMinFGVersionRequired() const +{ return _minFGVersionRequired; } + +void Addon::setMinFGVersionRequired(const string& minFGVersionRequired) +{ _minFGVersionRequired = minFGVersionRequired; } + +std::string Addon::getMaxFGVersionRequired() const +{ return _maxFGVersionRequired; } + +void Addon::setMaxFGVersionRequired(const string& maxFGVersionRequired) +{ + if (maxFGVersionRequired.empty()) { + _maxFGVersionRequired = "none"; // special value + } else { + _maxFGVersionRequired = maxFGVersionRequired; + } +} + +std::string Addon::getHomePage() const +{ return _homePage; } + +void Addon::setHomePage(const std::string& addonHomePage) +{ _homePage = addonHomePage; } + +std::string Addon::getDownloadUrl() const +{ return _downloadUrl; } + +void Addon::setDownloadUrl(const std::string& addonDownloadUrl) +{ _downloadUrl = addonDownloadUrl; } + +std::string Addon::getSupportUrl() const +{ return _supportUrl; } + +void Addon::setSupportUrl(const std::string& addonSupportUrl) +{ _supportUrl = addonSupportUrl; } + +SGPropertyNode_ptr Addon::getAddonNode() const +{ return _addonNode; } + +void Addon::setAddonNode(SGPropertyNode* addonNode) +{ _addonNode = SGPropertyNode_ptr(addonNode); } + +naRef Addon::getAddonPropsNode() const +{ + FGNasalSys* nas = globals->get_subsystem(); + return nas->wrappedPropsNode(_addonNode.get()); +} + +SGPropertyNode_ptr Addon::getLoadedFlagNode() const +{ + return { _addonNode->getChild("loaded", 0, 1) }; +} + +int Addon::getLoadSequenceNumber() const +{ return _loadSequenceNumber; } + +void Addon::setLoadSequenceNumber(int num) +{ _loadSequenceNumber = num; } + +std::string Addon::str() const +{ + std::ostringstream oss; + oss << "addon '" << _id << "' (version = " << *_version + << ", base path = '" << _basePath.utf8Str() + << "', minFGVersionRequired = '" << _minFGVersionRequired + << "', maxFGVersionRequired = '" << _maxFGVersionRequired << "')"; + + return oss.str(); +} + +// Static method +void Addon::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.Addon") + .member("id", &Addon::getId) + .member("name", &Addon::getName) + .member("version", &Addon::getVersion) + .member("shortDescription", &Addon::getShortDescription) + .member("longDescription", &Addon::getLongDescription) + .member("basePath", &Addon::getBasePath) + .member("minFGVersionRequired", &Addon::getMinFGVersionRequired) + .member("maxFGVersionRequired", &Addon::getMaxFGVersionRequired) + .member("homePage", &Addon::getHomePage) + .member("downloadUrl", &Addon::getDownloadUrl) + .member("supportUrl", &Addon::getSupportUrl) + .member("node", &Addon::getAddonPropsNode) + .member("loadSequenceNumber", &Addon::getLoadSequenceNumber); +} + +std::ostream& operator<<(std::ostream& os, const Addon& addon) +{ + return os << addon.str(); +} + +} // of namespace flightgear diff --git a/src/Add-ons/Addon.hxx b/src/Add-ons/Addon.hxx new file mode 100644 index 000000000..06679d49e --- /dev/null +++ b/src/Add-ons/Addon.hxx @@ -0,0 +1,135 @@ +// -*- coding: utf-8 -*- +// +// Addon.hxx --- FlightGear class holding add-on metadata +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_HXX +#define FG_ADDON_HXX + +#include +#include + +#include +#include +#include +#include +#include + +#include "addon_fwd.hxx" +#include "AddonVersion.hxx" + +namespace flightgear +{ + +class Addon : public SGReferenced { +public: + // Default constructor. 'minFGVersionRequired' is initialized to "2017.4.0" + // and 'maxFGVersionRequired' to "none". + Addon(); + // An empty value for 'minFGVersionRequired' is translated into "2017.4.0". + // An empty value for 'maxFGVersionRequired' is translated into "none". + Addon(std::string id, AddonVersion version, SGPath basePath, + std::string minFGVersionRequired = "", + std::string maxFGVersionRequired = "", + SGPropertyNode* addonNode = nullptr); + + std::string getId() const; + void setId(const std::string& addonId); + + std::string getName() const; + void setName(const std::string& addonName); + + AddonVersionRef getVersion() const; + void setVersion(const AddonVersion& addonVersion); + + std::string getShortDescription() const; + void setShortDescription(const std::string& addonShortDescription); + + std::string getLongDescription() const; + void setLongDescription(const std::string& addonLongDescription); + + SGPath getBasePath() const; + void setBasePath(const SGPath& addonBasePath); + + // Should be valid for use with simgear::strutils::compare_versions() + std::string getMinFGVersionRequired() const; + void setMinFGVersionRequired(const std::string& minFGVersionRequired); + + // Should be valid for use with simgear::strutils::compare_versions(), + // except for the special value "none". + std::string getMaxFGVersionRequired() const; + void setMaxFGVersionRequired(const std::string& maxFGVersionRequired); + + std::string getHomePage() const; + void setHomePage(const std::string& addonHomePage); + + std::string getDownloadUrl() const; + void setDownloadUrl(const std::string& addonDownloadUrl); + + std::string getSupportUrl() const; + void setSupportUrl(const std::string& addonSupportUrl); + + // Node pertaining to the add-on in the Global Property Tree + SGPropertyNode_ptr getAddonNode() const; + void setAddonNode(SGPropertyNode* addonNode); + // For Nasal: result as a props.Node object + naRef getAddonPropsNode() const; + + // Property node indicating whether the add-on is fully loaded + SGPropertyNode_ptr getLoadedFlagNode() const; + + // 0 for the first loaded add-on, 1 for the second, etc. + // -1 means “not set” (as done by the default constructor) + int getLoadSequenceNumber() const; + void setLoadSequenceNumber(int num); + + // Simple string representation + std::string str() const; + + static void setupGhost(nasal::Hash& addonsModule); + +private: + // The add-on identifier, in reverse DNS style. The AddonManager refuses to + // register two add-ons with the same id in a given FlightGear session. + std::string _id; + // Pretty name for the add-on (not constrained to reverse DNS style) + std::string _name; + // Use a smart pointer to expose the AddonVersion instance to Nasal without + // needing to copy the data every time. + AddonVersionRef _version; + // Strings describing what the add-on does + std::string _shortDescription; + std::string _longDescription; + SGPath _basePath; + // To be used with simgear::strutils::compare_versions() + std::string _minFGVersionRequired; + // Ditto, but there is a special value: "none" + std::string _maxFGVersionRequired; + std::string _homePage; + std::string _downloadUrl; + std::string _supportUrl; + // Main node for the add-on in the Property Tree + SGPropertyNode_ptr _addonNode; + // Semantics explained above + int _loadSequenceNumber = -1; +}; + +std::ostream& operator<<(std::ostream& os, const Addon& addonMetaData); + +} // of namespace flightgear + +#endif // of FG_ADDON_HXX diff --git a/src/Add-ons/AddonManager.cxx b/src/Add-ons/AddonManager.cxx new file mode 100644 index 000000000..8609e0748 --- /dev/null +++ b/src/Add-ons/AddonManager.cxx @@ -0,0 +1,404 @@ +// -*- coding: utf-8 -*- +// +// AddonManager.cxx --- Manager class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include +#include +#include + +#include +#include
+#include
+ +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonManager.hxx" +#include "AddonVersion.hxx" + +namespace strutils = simgear::strutils; + +using std::string; +using std::vector; +using std::shared_ptr; +using std::unique_ptr; + +namespace flightgear +{ + +static unique_ptr staticInstance; + +namespace addon_errors +{ +// *************************************************************************** +// * Base class for custom exceptions * +// *************************************************************************** + +// Prepending a prefix such as "Add-on error: " would be redundant given the +// messages used below. +error::error(const string& message, const string& origin) + : sg_exception(message, origin) +{ } + +error::error(const char* message, const char* origin) + : error(string(message), string(origin)) +{ } + +} // of namespace addon_errors + +// *************************************************************************** +// * AddonManager * +// *************************************************************************** + +// Static method +const unique_ptr& +AddonManager::createInstance() +{ + SG_LOG(SG_GENERAL, SG_DEBUG, "Initializing the AddonManager"); + + staticInstance.reset(new AddonManager()); + return staticInstance; +} + +// Static method +const unique_ptr& +AddonManager::instance() +{ + return staticInstance; +} + +// Static method +void +AddonManager::reset() +{ + SG_LOG(SG_GENERAL, SG_DEBUG, "Resetting the AddonManager"); + staticInstance.reset(); +} + +// Static method +void +AddonManager::loadConfigFileIfExists(const SGPath& configFile) +{ + if (!configFile.exists()) { + return; + } + + try { + readProperties(configFile, globals->get_props()); + } catch (const sg_exception &e) { + throw addon_errors::error_loading_config_file( + "unable to load add-on config file '" + configFile.utf8Str() + "': " + + e.getFormattedMessage()); + } + + SG_LOG(SG_GENERAL, SG_INFO, + "Loaded add-on config file: '" << configFile.utf8Str() + "'"); +} + +string +AddonManager::registerAddonMetadata(const SGPath& addonPath) +{ + SGPropertyNode addonRoot; + const SGPath metadataFile = addonPath / "addon-metadata.xml"; + + if (!metadataFile.exists()) { + throw addon_errors::no_metadata_file_found( + "unable to find add-on metadata file '" + metadataFile.utf8Str() + "'"); + } + + try { + readProperties(metadataFile, &addonRoot); + } catch (const sg_exception &e) { + throw addon_errors::error_loading_metadata_file( + "unable to load add-on metadata file '" + metadataFile.utf8Str() + "': " + + e.getFormattedMessage()); + } + + // Check the format version of the metadata file + SGPropertyNode *fmtVersionNode = addonRoot.getChild("format-version"); + if (fmtVersionNode == nullptr) { + throw addon_errors::error_loading_metadata_file( + "no 'format-version' node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + + int formatVersion = fmtVersionNode->getIntValue(); + if (formatVersion != 1) { + throw addon_errors::error_loading_metadata_file( + "unknown format version in add-on metadata file '" + + metadataFile.utf8Str() + "': " + std::to_string(formatVersion)); + } + + // Now the data we are really interested in + SGPropertyNode *addonNode = addonRoot.getChild("addon"); + if (addonNode == nullptr) { + throw addon_errors::error_loading_metadata_file( + "no 'addon' node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + + SGPropertyNode *idNode = addonNode->getChild("identifier"); + if (idNode == nullptr) { + throw addon_errors::error_loading_metadata_file( + "no 'identifier' node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + std::string addonId = strutils::strip(idNode->getStringValue()); + + // Require a non-empty identifier for the add-on + if (addonId.empty()) { + throw addon_errors::error_loading_metadata_file( + "empty or whitespace-only value for the 'identifier' node in add-on " + "metadata file '" + metadataFile.utf8Str() + "'"); + } else if (addonId.find('.') == string::npos) { + SG_LOG(SG_GENERAL, SG_WARN, + "Add-on identifier '" << addonId << "' does not use reverse DNS " + "style (e.g., org.flightgear.addons.MyAddon) in add-on metadata " + "file '" << metadataFile.utf8Str() + "'"); + } + + SGPropertyNode *nameNode = addonNode->getChild("name"); + if (nameNode == nullptr) { + throw addon_errors::error_loading_metadata_file( + "no 'name' node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + std::string addonName = strutils::strip(nameNode->getStringValue()); + + // Require a non-empty name for the add-on + if (addonName.empty()) { + throw addon_errors::error_loading_metadata_file( + "empty or whitespace-only value for the 'name' node in add-on " + "metadata file '" + metadataFile.utf8Str() + "'"); + } + + SGPropertyNode *versionNode = addonNode->getChild("version"); + if (versionNode == nullptr) { + throw addon_errors::error_loading_metadata_file( + "no 'version' node found in add-on metadata file '" + + metadataFile.utf8Str() + "'"); + } + AddonVersion addonVersion(versionNode->getStringValue()); + + std::string addonShortDescription; + SGPropertyNode *shortDescNode = addonNode->getChild("short-description"); + if (shortDescNode != nullptr) { + addonShortDescription = strutils::strip(shortDescNode->getStringValue()); + } + + std::string addonLongDescription; + SGPropertyNode *longDescNode = addonNode->getChild("long-description"); + if (longDescNode != nullptr) { + addonLongDescription = strutils::strip(longDescNode->getStringValue()); + } + + std::string addonMinFGVersionRequired; + SGPropertyNode *minNode = addonNode->getChild("min-FG-version"); + if (minNode != nullptr) { + addonMinFGVersionRequired = minNode->getStringValue(); + } + + std::string addonMaxFGVersionRequired; + SGPropertyNode *maxNode = addonNode->getChild("max-FG-version"); + if (maxNode != nullptr) { + addonMaxFGVersionRequired = maxNode->getStringValue(); + } + + std::string addonHomePage; + SGPropertyNode *homePageNode = addonNode->getChild("home-page"); + if (homePageNode != nullptr) { + addonHomePage = strutils::strip(homePageNode->getStringValue()); + } + + std::string addonDownloadUrl; + SGPropertyNode *downloadUrlNode = addonNode->getChild("download-url"); + if (downloadUrlNode != nullptr) { + addonDownloadUrl = strutils::strip(downloadUrlNode->getStringValue()); + } + + std::string addonSupportUrl; + SGPropertyNode *supportUrlNode = addonNode->getChild("support-url"); + if (supportUrlNode != nullptr) { + addonSupportUrl = strutils::strip(supportUrlNode->getStringValue()); + } + + SGPropertyNode* addonPropNode = fgGetNode("addons", true) + ->getChild("by-id", 0, 1) + ->getChild(addonId, 0, 1); + // Object holding all the add-on metadata + AddonRef addon(new Addon( + addonId, std::move(addonVersion), addonPath, + addonMinFGVersionRequired, addonMaxFGVersionRequired, + addonPropNode)); + addon->setName(addonName); + addon->setShortDescription(addonShortDescription); + addon->setLongDescription(addonLongDescription); + addon->setHomePage(addonHomePage); + addon->setDownloadUrl(addonDownloadUrl); + addon->setSupportUrl(addonSupportUrl); + addon->setLoadSequenceNumber(_loadSequenceNumber++); + + // Check that the FlightGear version satisfies the add-on requirements + std::string minFGversion = addon->getMinFGVersionRequired(); + if (strutils::compare_versions(FLIGHTGEAR_VERSION, minFGversion) < 0) { + throw addon_errors::fg_version_too_old( + "add-on '" + addonId + "' requires FlightGear " + minFGversion + + " or later, however this is FlightGear " + FLIGHTGEAR_VERSION); + } + + std::string maxFGversion = addon->getMaxFGVersionRequired(); + if (maxFGversion != "none" && + strutils::compare_versions(FLIGHTGEAR_VERSION, maxFGversion) > 0) { + throw addon_errors::fg_version_too_recent( + "add-on '" + addonId + "' requires FlightGear " + maxFGversion + + " or earlier, however this is FlightGear " + FLIGHTGEAR_VERSION); + } + + // Store the add-on metadata in _idToAddonMap + auto emplaceRetval = _idToAddonMap.emplace(addonId, std::move(addon)); + + // Prevent registration of two add-ons with the same id + if (!emplaceRetval.second) { + auto existingElt = _idToAddonMap.find(addonId); + assert(existingElt != _idToAddonMap.end()); + throw addon_errors::duplicate_registration_attempt( + "attempt to register add-on '" + addonId + "' with base path '" + + addonPath.utf8Str() + "', however it is already registered with base " + "path '" + existingElt->second->getBasePath().utf8Str() + "'"); + } + + SG_LOG(SG_GENERAL, SG_DEBUG, + "Loaded add-on metadata file: '" << metadataFile.utf8Str() + "'"); + + return addonId; +} + +string +AddonManager::registerAddon(const SGPath& addonPath) +{ + // Use realpath() as in FGGlobals::append_aircraft_path(), otherwise + // fgValidatePath() will deny access to resources under the add-on path if + // one of its components is a symlink. + const SGPath addonRealPath = addonPath.realpath(); + const string addonId = registerAddonMetadata(addonRealPath); + loadConfigFileIfExists(addonRealPath / "config.xml"); + globals->append_aircraft_path(addonRealPath); + + AddonRef addon{getAddon(addonId)}; + addon->getLoadedFlagNode()->setBoolValue(false); + SGPropertyNode_ptr addonNode = addon->getAddonNode(); + + // Set a few properties for the add-on under this node + addonNode->getNode("id", true)->setStringValue(addonId); + addonNode->getNode("name", true)->setStringValue(addon->getName()); + addonNode->getNode("version", true) + ->setStringValue(addonVersion(addonId)->str()); + addonNode->getNode("path", true)->setStringValue(addonRealPath.utf8Str()); + addonNode->getNode("load-seq-num", true) + ->setIntValue(addon->getLoadSequenceNumber()); + + // “Legacy node”. Should we remove these two lines? + SGPropertyNode* seqNumNode = fgGetNode("addons", true)->addChild("addon"); + seqNumNode->getNode("path", true)->setStringValue(addonRealPath.utf8Str()); + + string msg = "Registered add-on '" + addon->getName() + "' (" + addonId + + ") version " + addonVersion(addonId)->str() + "; " + "base path is '" + addonRealPath.utf8Str() + "'"; + // This preserves the registration order + _registeredAddons.push_back(std::move(addon)); + SG_LOG(SG_GENERAL, SG_INFO, msg); + + return addonId; +} + +bool +AddonManager::isAddonRegistered(const string& addonId) const +{ + return (_idToAddonMap.find(addonId) != _idToAddonMap.end()); +} + +bool +AddonManager::isAddonLoaded(const string& addonId) const +{ + return (isAddonRegistered(addonId) && + getAddon(addonId)->getLoadedFlagNode()->getBoolValue()); +} + +vector +AddonManager::registeredAddons() const +{ + return _registeredAddons; +} + +vector +AddonManager::loadedAddons() const +{ + vector v; + v.reserve(_idToAddonMap.size()); // will be the right size most of the times + + for (const auto& elem: _idToAddonMap) { + if (isAddonLoaded(elem.first)) { + v.push_back(elem.second); + } + } + + return v; +} + +AddonRef +AddonManager::getAddon(const string& addonId) const +{ + const auto it = _idToAddonMap.find(addonId); + + if (it == _idToAddonMap.end()) { + throw sg_exception("tried to get add-on '" + addonId + "', however no " + "such add-on has been registered."); + } + + return it->second; +} + +AddonVersionRef +AddonManager::addonVersion(const string& addonId) const +{ + return getAddon(addonId)->getVersion(); +} + +SGPath +AddonManager::addonBasePath(const string& addonId) const +{ + return getAddon(addonId)->getBasePath(); +} + +SGPropertyNode_ptr AddonManager::addonNode(const string& addonId) const +{ + return getAddon(addonId)->getAddonNode(); +} + +} // of namespace flightgear diff --git a/src/Add-ons/AddonManager.hxx b/src/Add-ons/AddonManager.hxx new file mode 100644 index 000000000..36fecaf68 --- /dev/null +++ b/src/Add-ons/AddonManager.hxx @@ -0,0 +1,136 @@ +// -*- coding: utf-8 -*- +// +// AddonManager.hxx --- Manager class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDONMANAGER_HXX +#define FG_ADDONMANAGER_HXX + +#include +#include +#include // std::unique_ptr, std::shared_ptr +#include + +#include +#include + +#include "addon_fwd.hxx" +#include "Addon.hxx" +#include "AddonVersion.hxx" + +namespace flightgear +{ + +namespace addon_errors +{ +// Custom exception classes +class error : public sg_exception +{ +public: + explicit error(const std::string& message, + const std::string& origin = std::string()); + explicit error(const char* message, const char* origin = nullptr); +}; + +class error_loading_config_file : public error +{ using error::error; /* inherit all constructors */ }; + +class no_metadata_file_found : public error +{ using error::error; }; + +class error_loading_metadata_file : public error +{ using error::error; }; + +class duplicate_registration_attempt : public error +{ using error::error; }; + +class fg_version_too_old : public error +{ using error::error; }; + +class fg_version_too_recent : public error +{ using error::error; }; + +} // of namespace addon_errors + +class AddonManager +{ +public: + AddonManager(const AddonManager&) = delete; + AddonManager& operator=(const AddonManager&) = delete; + AddonManager(AddonManager&&) = delete; + AddonManager& operator=(AddonManager&&) = delete; + // The instance is created by createInstance() -> private constructor + // but it should be deleted by its owning std::unique_ptr -> public destructor + ~AddonManager() = default; + + // Static creator + static const std::unique_ptr& createInstance(); + // Singleton accessor + static const std::unique_ptr& instance(); + // Reset the static smart pointer, i.e., shut down the AddonManager. + static void reset(); + + // Register an add-on and return its id. + // 'addonPath': directory containing the add-on to register + // + // This comprises the following steps, where $path = addonPath.realpath(): + // - load add-on metadata from $path/addon-metadata.xml and register it + // inside _idToAddonMap (this step is done via registerAddonMetadata()); + // - load $path/config.xml into the Property Tree; + // - append $path to the list of aircraft paths; + // - make part of the add-on metadata available in the Property Tree under + // the /addons node (/addons/by-id//{id,version,path,...}); + // - append a ref to the Addon instance to _registeredAddons. + std::string registerAddon(const SGPath& addonPath); + // Return the list of registered add-ons in registration order (which, BTW, + // is the same as load order). + std::vector registeredAddons() const; + bool isAddonRegistered(const std::string& addonId) const; + + // A loaded add-on is one whose main.nas file has been loaded. The returned + // vector is sorted by add-on id (cheap sorting based on UTF-8 code units, + // only guaranteed correct for ASCII chars). + std::vector loadedAddons() const; + bool isAddonLoaded(const std::string& addonId) const; + + AddonRef getAddon(const std::string& addonId) const; + AddonVersionRef addonVersion(const std::string& addonId) const; + SGPath addonBasePath(const std::string& addonId) const; + + // Base node pertaining to the add-on in the Global Property Tree + SGPropertyNode_ptr addonNode(const std::string& addonId) const; + +private: + // Constructor called from createInstance() only + explicit AddonManager() = default; + + static void loadConfigFileIfExists(const SGPath& configFile); + // Register add-on metadata inside _idToAddonMap and return the add-on id + std::string registerAddonMetadata(const SGPath& addonPath); + + // Map each add-on id to the corresponding Addon instance. + std::map _idToAddonMap; + // The order in _registeredAddons is the registration order. + std::vector _registeredAddons; + // 0 for the first loaded add-on, 1 for the second, etc. + // Also note that add-ons are loaded in their registration order. + int _loadSequenceNumber = 0; +}; + +} // of namespace flightgear + +#endif // of FG_ADDONMANAGER_HXX diff --git a/src/Add-ons/AddonVersion.cxx b/src/Add-ons/AddonVersion.cxx new file mode 100644 index 000000000..aa4c44d2e --- /dev/null +++ b/src/Add-ons/AddonVersion.cxx @@ -0,0 +1,396 @@ +// -*- coding: utf-8 -*- +// +// AddonVersion.cxx --- Version class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include // std::accumulate() +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include "addon_fwd.hxx" +#include "AddonVersion.hxx" + +using std::string; +using std::vector; +using simgear::enumValue; + +namespace strutils = simgear::strutils; + +namespace flightgear +{ + +// *************************************************************************** +// * AddonVersionSuffix * +// *************************************************************************** + +AddonVersionSuffix::AddonVersionSuffix( + AddonVersionSuffixPrereleaseType preReleaseType, int preReleaseNum, + bool developmental, int devNum) + : _preReleaseType(preReleaseType), + _preReleaseNum(preReleaseNum), + _developmental(developmental), + _devNum(devNum) +{ } + +// Construct an AddonVersionSuffix instance from a tuple (preReleaseType, +// preReleaseNum, developmental, devNum). This would be nicer with +// std::apply(), but it requires C++17. +AddonVersionSuffix::AddonVersionSuffix( + const std::tuple& t) + : AddonVersionSuffix(std::get<0>(t), std::get<1>(t), std::get<2>(t), + std::get<3>(t)) +{ } + +AddonVersionSuffix::AddonVersionSuffix(const std::string& suffix) + : AddonVersionSuffix(suffixStringToTuple(suffix)) +{ } + +AddonVersionSuffix::AddonVersionSuffix(const char* suffix) + : AddonVersionSuffix(string(suffix)) +{ } + +// Static method +string +AddonVersionSuffix::releaseTypeStr(AddonVersionSuffixPrereleaseType releaseType) +{ + switch (releaseType) { + case AddonVersionSuffixPrereleaseType::alpha: + return string("a"); + case AddonVersionSuffixPrereleaseType::beta: + return string("b"); + case AddonVersionSuffixPrereleaseType::candidate: + return string("rc"); + case AddonVersionSuffixPrereleaseType::none: + return string(); + default: + throw sg_error("unexpected value for member of " + "flightgear::AddonVersionSuffixPrereleaseType: " + + std::to_string(enumValue(releaseType))); + } +} + +string +AddonVersionSuffix::str() const +{ + string res = releaseTypeStr(_preReleaseType); + + if (!res.empty()) { + res += std::to_string(_preReleaseNum); + } + + if (_developmental) { + res += ".dev" + std::to_string(_devNum); + } + + return res; +} + +// Static method +std::tuple +AddonVersionSuffix::suffixStringToTuple(const std::string& suffix) +{ + // Use a simplified variant of the syntax described in PEP 440 + // : for the version suffix, only + // allow a pre-release segment and a development release segment, but no + // post-release segment. + std::regex versionSuffixRegexp(R"((?:(a|b|rc)(\d+))?(?:\.dev(\d+))?)"); + std::smatch results; + + if (std::regex_match(suffix, results, versionSuffixRegexp)) { + const string preReleaseType_s = results.str(1); + const string preReleaseNum_s = results.str(2); + const string devNum_s = results.str(3); + + AddonVersionSuffixPrereleaseType preReleaseType; + int preReleaseNum = 0; + int devNum = 0; + + if (preReleaseType_s.empty()) { + preReleaseType = AddonVersionSuffixPrereleaseType::none; + } else { + if (preReleaseType_s == "a") { + preReleaseType = AddonVersionSuffixPrereleaseType::alpha; + } else if (preReleaseType_s == "b") { + preReleaseType = AddonVersionSuffixPrereleaseType::beta; + } else if (preReleaseType_s == "rc") { + preReleaseType = AddonVersionSuffixPrereleaseType::candidate; + } else { + assert(false); // the regexp should prevent this + } + + assert(!preReleaseNum_s.empty()); + preReleaseNum = strutils::readNonNegativeInt(preReleaseNum_s); + + if (preReleaseNum < 1) { + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(prerelease number must be greater than or equal to 1, but got " + + preReleaseNum_s + ")"; + throw sg_format_exception(msg, suffix); + } + } + + if (!devNum_s.empty()) { + devNum = strutils::readNonNegativeInt(devNum_s); + + if (devNum < 1) { + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(development release number must be greater than or equal to 1, " + "but got " + devNum_s + ")"; + throw sg_format_exception(msg, suffix); + } + } + + return {preReleaseType, preReleaseNum, !devNum_s.empty(), devNum}; + } else { // the regexp didn't match + string msg = "invalid add-on version suffix: '" + suffix + "' " + "(expected form is [{a|b|rc}N1][.devN2] where N1 and N2 are positive " + "integers)"; + throw sg_format_exception(msg, suffix); + } +} + +// Beware, this is not suitable for sorting! cf. genSortKey() below. +std::tuple +AddonVersionSuffix::makeTuple() const +{ + return { _preReleaseType, _preReleaseNum, _developmental, _devNum }; +} + +std::tuple::type, + int, int, int> +AddonVersionSuffix::genSortKey() const +{ + using AddonRelType = AddonVersionSuffixPrereleaseType; + + // The first element means that a plain .devN is lower than everything else, + // except .devM with M <= N (namely: all dev and non-dev alpha, beta, + // candidates, as well as the empty suffix). + return { ((_developmental && _preReleaseType == AddonRelType::none) ? 0 : 1), + enumValue(_preReleaseType), + _preReleaseNum, + (_developmental ? 0 : 1), // e.g., 1.0.3a2.devN < 1.0.3a2 for all N + _devNum }; +} + +bool operator==(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return lhs.genSortKey() == rhs.genSortKey(); } + +bool operator!=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return !operator==(lhs, rhs); } + +bool operator< (const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return lhs.genSortKey() < rhs.genSortKey(); } + +bool operator> (const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return operator<(rhs, lhs); } + +bool operator<=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return !operator>(lhs, rhs); } + +bool operator>=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs) +{ return !operator<(lhs, rhs); } + +std::ostream& operator<<(std::ostream& os, + const AddonVersionSuffix& addonVersionSuffix) +{ return os << addonVersionSuffix.str(); } + +// *************************************************************************** +// * AddonVersion * +// *************************************************************************** + +AddonVersion::AddonVersion(int major, int minor, int patchLevel, + AddonVersionSuffix suffix) + : _major(major), + _minor(minor), + _patchLevel(patchLevel), + _suffix(std::move(suffix)) + { } + +// Construct an AddonVersion instance from a tuple (major, minor, patchLevel, +// suffix). This would be nicer with std::apply(), but it requires C++17. +AddonVersion::AddonVersion( + const std::tuple& t) + : AddonVersion(std::get<0>(t), std::get<1>(t), std::get<2>(t), std::get<3>(t)) +{ } + +AddonVersion::AddonVersion(const std::string& versionStr) + : AddonVersion(versionStringToTuple(versionStr)) +{ } + +AddonVersion::AddonVersion(const char* versionStr) + : AddonVersion(string(versionStr)) +{ } + +// Static method +std::tuple +AddonVersion::versionStringToTuple(const std::string& versionStr) +{ + // Use a simplified variant of the syntax described in PEP 440 + // (always 3 components in the + // release segment, pre-release segment + development release segment; no + // post-release segment allowed). + std::regex versionRegexp( + R"((\d+)\.(\d+).(\d+)(.*))"); + std::smatch results; + + if (std::regex_match(versionStr, results, versionRegexp)) { + const string majorNumber_s = results.str(1); + const string minorNumber_s = results.str(2); + const string patchLevel_s = results.str(3); + const string suffix_s = results.str(4); + + int major = strutils::readNonNegativeInt(majorNumber_s); + int minor = strutils::readNonNegativeInt(minorNumber_s); + int patchLevel = strutils::readNonNegativeInt(patchLevel_s); + + return {major, minor, patchLevel, AddonVersionSuffix(suffix_s)}; + } else { // the regexp didn't match + string msg = "invalid add-on version number: '" + versionStr + "' " + "(expected form is MAJOR.MINOR.PATCHLEVEL[{a|b|rc}N1][.devN2] where " + "N1 and N2 are positive integers)"; + throw sg_format_exception(msg, versionStr); + } +} + +int AddonVersion::majorNumber() const +{ return _major; } + +int AddonVersion::minorNumber() const +{ return _minor; } + +int AddonVersion::patchLevel() const +{ return _patchLevel; } + +AddonVersionSuffix AddonVersion::suffix() const +{ return _suffix; } + +std::string AddonVersion::suffixStr() const +{ return suffix().str(); } + +std::tuple AddonVersion::makeTuple() const +{ return {majorNumber(), minorNumber(), patchLevel(), suffix()}; } + +string AddonVersion::str() const +{ + // Assemble the major.minor.patchLevel string + vector v({majorNumber(), minorNumber(), patchLevel()}); + string relSeg = std::accumulate(std::next(v.begin()), v.end(), + std::to_string(v[0]), + [](string s, int num) { + return s + '.' + std::to_string(num); + }); + + // Concatenate with the suffix string + return relSeg + suffixStr(); +} + + +bool operator==(const AddonVersion& lhs, const AddonVersion& rhs) +{ return lhs.makeTuple() == rhs.makeTuple(); } + +bool operator!=(const AddonVersion& lhs, const AddonVersion& rhs) +{ return !operator==(lhs, rhs); } + +bool operator< (const AddonVersion& lhs, const AddonVersion& rhs) +{ return lhs.makeTuple() < rhs.makeTuple(); } + +bool operator> (const AddonVersion& lhs, const AddonVersion& rhs) +{ return operator<(rhs, lhs); } + +bool operator<=(const AddonVersion& lhs, const AddonVersion& rhs) +{ return !operator>(lhs, rhs); } + +bool operator>=(const AddonVersion& lhs, const AddonVersion& rhs) +{ return !operator<(lhs, rhs); } + +std::ostream& operator<<(std::ostream& os, const AddonVersion& addonVersion) +{ return os << addonVersion.str(); } + + +// *************************************************************************** +// * For the Nasal bindings * +// *************************************************************************** + +bool AddonVersion::equal(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this == *other; +} + +bool AddonVersion::nonEqual(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this != *other; +} + +bool AddonVersion::lowerThan(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this < *other; +} + +bool AddonVersion::lowerThanOrEqual(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this <= *other; +} + +bool AddonVersion::greaterThan(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this > *other; +} + +bool AddonVersion::greaterThanOrEqual(const nasal::CallContext& ctx) const +{ + auto other = ctx.requireArg(0); + return *this >= *other; +} + +// Static method +void AddonVersion::setupGhost(nasal::Hash& addonsModule) +{ + nasal::Ghost::init("addons.AddonVersion") + .member("majorNumber", &AddonVersion::majorNumber) + .member("minorNumber", &AddonVersion::minorNumber) + .member("patchLevel", &AddonVersion::patchLevel) + .member("suffix", &AddonVersion::suffixStr) + .method("str", &AddonVersion::str) + .method("equal", &AddonVersion::equal) + .method("nonEqual", &AddonVersion::nonEqual) + .method("lowerThan", &AddonVersion::lowerThan) + .method("lowerThanOrEqual", &AddonVersion::lowerThanOrEqual) + .method("greaterThan", &AddonVersion::greaterThan) + .method("greaterThanOrEqual", &AddonVersion::greaterThanOrEqual); +} + +} // of namespace flightgear diff --git a/src/Add-ons/AddonVersion.hxx b/src/Add-ons/AddonVersion.hxx new file mode 100644 index 000000000..a2f25be8e --- /dev/null +++ b/src/Add-ons/AddonVersion.hxx @@ -0,0 +1,175 @@ +// -*- coding: utf-8 -*- +// +// AddonVersion.hxx --- Version class for FlightGear add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDONVERSION_HXX +#define FG_ADDONVERSION_HXX + +#include +#include +#include +#include + +#include +#include +#include + +#include "addon_fwd.hxx" + +namespace flightgear +{ + +// Order matters for the sorting/comparison functions +enum class AddonVersionSuffixPrereleaseType { + alpha = 0, + beta, + candidate, + none +}; + +// *************************************************************************** +// * AddonVersionSuffix * +// *************************************************************************** + +class AddonVersionSuffix +{ +public: + AddonVersionSuffix(AddonVersionSuffixPrereleaseType _preReleaseType + = AddonVersionSuffixPrereleaseType::none, + int preReleaseNum = 0, bool developmental = false, + int devNum = 0); + // Construct from a string. The empty string is a valid input. + AddonVersionSuffix(const std::string& suffix); + AddonVersionSuffix(const char* suffix); + // Construct from a tuple + explicit AddonVersionSuffix( + const std::tuple& t); + + // Return all components of an AddonVersionSuffix instance as a tuple. + // Beware, this is not suitable for sorting! cf. genSortKey() below. + std::tuple makeTuple() const; + + // String representation of an AddonVersionSuffix + std::string str() const; + +private: + // Extract all components from a string representing a version suffix. + // The components of the return value are, in this order: + // + // preReleaseType, preReleaseNum, developmental, devNum + // + // Note: the empty string is a valid input. + static std::tuple + suffixStringToTuple(const std::string& suffix); + + // String representation of the release type component: "a", "b", "rc" or "". + static std::string releaseTypeStr(AddonVersionSuffixPrereleaseType); + // Useful for comparisons/sorting purposes + std::tuple::type, + int, int, int> genSortKey() const; + + friend bool operator==(const AddonVersionSuffix& lhs, + const AddonVersionSuffix& rhs); + friend bool operator<(const AddonVersionSuffix& lhs, + const AddonVersionSuffix& rhs); + + AddonVersionSuffixPrereleaseType _preReleaseType; + int _preReleaseNum; // integer >= 1 (0 when not applicable) + bool _developmental; // whether the suffix ends with '.devN' + int _devNum; // integer >= 1 (0 when not applicable) +}; + + +// operator==() and operator<() are declared above. +bool operator!=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); +bool operator> (const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); +bool operator<=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); +bool operator>=(const AddonVersionSuffix& lhs, const AddonVersionSuffix& rhs); + +std::ostream& operator<<(std::ostream&, const AddonVersionSuffix&); + +// *************************************************************************** +// * AddonVersion * +// *************************************************************************** + +// I suggest to use either the year-based FlightGear-type versioning, or +// semantic versioning (). For the suffix, we allow things +// like "a1" (alpha1), "b2" (beta2), "rc4" (release candidate 4), "a1.dev3" +// (development release for "a1", which sorts before "a1"), etc. It's a subset +// of the syntax allowed in . +class AddonVersion : public SGReferenced +{ +public: + AddonVersion(int major = 0, int minor = 0, int patchLevel = 0, + AddonVersionSuffix suffix = AddonVersionSuffix()); + AddonVersion(const std::string& version); + AddonVersion(const char* version); + explicit AddonVersion(const std::tuple& t); + + // Using the method names major() and minor() can lead to incomprehensible + // errors such as "major is not a member of flightgear::AddonVersion" + // because of a hideous glibc bug[1]: major() and minor() are defined by + // standard headers as *macros*! + // + // [1] https://bugzilla.redhat.com/show_bug.cgi?id=130601 + int majorNumber() const; + int minorNumber() const; + int patchLevel() const; + AddonVersionSuffix suffix() const; + std::string suffixStr() const; + + std::string str() const; + + // For the Nasal bindings (otherwise, we have operator==(), etc.) + bool equal(const nasal::CallContext& ctx) const; + bool nonEqual(const nasal::CallContext& ctx) const; + bool lowerThan(const nasal::CallContext& ctx) const; + bool lowerThanOrEqual(const nasal::CallContext& ctx) const; + bool greaterThan(const nasal::CallContext& ctx) const; + bool greaterThanOrEqual(const nasal::CallContext& ctx) const; + + static void setupGhost(nasal::Hash& addonsModule); + +private: + // Useful for comparisons/sorting purposes + std::tuple makeTuple() const; + + static std::tuple + versionStringToTuple(const std::string& versionStr); + + friend bool operator==(const AddonVersion& lhs, const AddonVersion& rhs); + friend bool operator<(const AddonVersion& lhs, const AddonVersion& rhs); + + int _major; + int _minor; + int _patchLevel; + AddonVersionSuffix _suffix; +}; + +// operator==() and operator<() are declared above. +bool operator!=(const AddonVersion& lhs, const AddonVersion& rhs); +bool operator> (const AddonVersion& lhs, const AddonVersion& rhs); +bool operator<=(const AddonVersion& lhs, const AddonVersion& rhs); +bool operator>=(const AddonVersion& lhs, const AddonVersion& rhs); + +std::ostream& operator<<(std::ostream&, const AddonVersion&); + +} // of namespace flightgear + +#endif // of FG_ADDONVERSION_HXX diff --git a/src/Add-ons/CMakeLists.txt b/src/Add-ons/CMakeLists.txt new file mode 100644 index 000000000..6c2ea9396 --- /dev/null +++ b/src/Add-ons/CMakeLists.txt @@ -0,0 +1,10 @@ +include(FlightGearComponent) + +set(SOURCES Addon.cxx AddonManager.cxx AddonVersion.cxx) +set(HEADERS addon_fwd.hxx Addon.hxx AddonManager.hxx AddonVersion.hxx) + +flightgear_component(AddonManagement "${SOURCES}" "${HEADERS}") + +if (COMMAND flightgear_test) + flightgear_test(test_AddonManagement test_AddonManagement.cxx) +endif() diff --git a/src/Add-ons/addon_fwd.hxx b/src/Add-ons/addon_fwd.hxx new file mode 100644 index 000000000..e4e62629a --- /dev/null +++ b/src/Add-ons/addon_fwd.hxx @@ -0,0 +1,52 @@ +// -*- coding: utf-8 -*- +// +// addon_fwd.hxx --- Forward declarations for the FlightGear add-on +// infrastructure +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_FWD_HXX +#define FG_ADDON_FWD_HXX + +#include + +namespace flightgear +{ + +class Addon; +class AddonManager; +class AddonVersion; +class AddonVersionSuffix; + +using AddonRef = SGSharedPtr; +using AddonVersionRef = SGSharedPtr; + +namespace addon_errors +{ + +class error; +class error_loading_config_file; +class no_metadata_file_found; +class error_loading_metadata_file; +class duplicate_registration_attempt; +class fg_version_too_old; +class fg_version_too_recent; + +} // of namespace addon_errors + +} // of namespace flightgear + +#endif // of FG_ADDON_FWD_HXX diff --git a/src/Add-ons/test_AddonManagement.cxx b/src/Add-ons/test_AddonManagement.cxx new file mode 100644 index 000000000..98597def5 --- /dev/null +++ b/src/Add-ons/test_AddonManagement.cxx @@ -0,0 +1,249 @@ +// -*- coding: utf-8 -*- +// +// test_AddonManagement.cxx --- Automated tests for FlightGear classes dealing +// with add-ons +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "config.h" +#include "unitTestHelpers.hxx" + +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "Add-ons/Addon.hxx" +#include "Add-ons/AddonVersion.hxx" + +using std::string; +using std::vector; + +using namespace flightgear; + + +void testAddonVersionSuffix() +{ + using AddonRelType = AddonVersionSuffixPrereleaseType; + + fgtest::initTestGlobals("AddonVersion"); + + AddonVersionSuffix v1(AddonRelType::beta, 2, true, 5); + AddonVersionSuffix v1Copy(v1); + AddonVersionSuffix v1NonDev(AddonRelType::beta, 2, false); + SG_CHECK_EQUAL(v1, v1Copy); + SG_CHECK_EQUAL(v1, AddonVersionSuffix("b2.dev5")); + SG_CHECK_EQUAL_NOSTREAM(v1.makeTuple(), + std::make_tuple(AddonRelType::beta, 2, true, 5)); + SG_CHECK_EQUAL(AddonVersionSuffix(), AddonVersionSuffix("")); + // A simple comparison + SG_CHECK_LT(v1, v1NonDev); // b2.dev5 < b2 + + // Check string representation with str() + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::none).str(), ""); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::none, 0, true, 12).str(), + ".dev12"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::alpha, 1).str(), "a1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::alpha, 1, false).str(), "a1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::alpha, 2, true, 4).str(), + "a2.dev4"); + + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::beta, 1).str(), "b1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::beta, 1, false).str(), "b1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::beta, 2, true, 4).str(), + "b2.dev4"); + + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::candidate, 1).str(), "rc1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::candidate, 1, false).str(), + "rc1"); + SG_CHECK_EQUAL(AddonVersionSuffix(AddonRelType::candidate, 2, true, 4).str(), + "rc2.dev4"); + + // Check stream output + std::ostringstream oss; + oss << AddonVersionSuffix(AddonRelType::candidate, 2, true, 4); + SG_CHECK_EQUAL(oss.str(), "rc2.dev4"); + + // Check ordering with all types of transitions, using operator<() + auto checkStrictOrdering = [](const vector& v) { + assert(v.size() > 1); + for (std::size_t i=0; i < v.size() - 1; i++) { + SG_CHECK_LT(v[i], v[i+1]); + } + }; + + checkStrictOrdering({ + {AddonRelType::none, 0, true, 1}, + {AddonRelType::none, 0, true, 2}, + {AddonRelType::alpha, 1, true, 1}, + {AddonRelType::alpha, 1, true, 2}, + {AddonRelType::alpha, 1, true, 3}, + {AddonRelType::alpha, 1, false}, + {AddonRelType::alpha, 2, true, 1}, + {AddonRelType::alpha, 2, true, 3}, + {AddonRelType::alpha, 2, false}, + {AddonRelType::beta, 1, true, 1}, + {AddonRelType::beta, 1, true, 25}, + {AddonRelType::beta, 1, false}, + {AddonRelType::beta, 2, true, 1}, + {AddonRelType::beta, 2, true, 2}, + {AddonRelType::beta, 2, false}, + {AddonRelType::candidate, 1, true, 1}, + {AddonRelType::candidate, 1, true, 2}, + {AddonRelType::candidate, 1, false}, + {AddonRelType::candidate, 2, true, 1}, + {AddonRelType::candidate, 2, true, 2}, + {AddonRelType::candidate, 2, false}, + {AddonRelType::candidate, 21, false}, + {AddonRelType::none} + }); + + // Check operator>() and operator!=() + SG_CHECK_GT(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::candidate, 21, false)); + + SG_CHECK_NE(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::candidate, 21, false)); + + // Check operator<=() and operator>=() + SG_CHECK_LE(AddonVersionSuffix(AddonRelType::candidate, 2, false), + AddonVersionSuffix(AddonRelType::candidate, 2, false)); + SG_CHECK_LE(AddonVersionSuffix(AddonRelType::candidate, 2, false), + AddonVersionSuffix(AddonRelType::none)); + + SG_CHECK_GE(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::none)); + SG_CHECK_GE(AddonVersionSuffix(AddonRelType::none), + AddonVersionSuffix(AddonRelType::candidate, 21, false)); + + fgtest::shutdownTestGlobals(); +} + +void testAddonVersion() +{ + using AddonRelType = AddonVersionSuffixPrereleaseType; + + fgtest::initTestGlobals("AddonVersion"); + + AddonVersionSuffix suffix(AddonRelType::beta, 2, true, 5); + AddonVersion v1(2017, 4, 7, suffix); + AddonVersion v1Copy(v1); + AddonVersion v2 = v1; + AddonVersion v3(std::move(v1Copy)); + AddonVersion v4 = std::move(v2); + + SG_CHECK_EQUAL(v1, AddonVersion("2017.4.7b2.dev5")); + SG_CHECK_EQUAL(v1, AddonVersion(std::make_tuple(2017, 4, 7, suffix))); + SG_CHECK_EQUAL(v1, v3); + SG_CHECK_EQUAL(v1, v4); + SG_CHECK_LT(v1, AddonVersion("2017.4.7b2")); + SG_CHECK_LE(v1, AddonVersion("2017.4.7b2")); + SG_CHECK_LE(v1, v1); + SG_CHECK_GT(AddonVersion("2017.4.7b2"), v1); + SG_CHECK_GE(AddonVersion("2017.4.7b2"), v1); + SG_CHECK_GE(v1, v1); + SG_CHECK_NE(v1, AddonVersion("2017.4.7b3")); + + SG_CHECK_EQUAL(v1.majorNumber(), 2017); + SG_CHECK_EQUAL(v1.minorNumber(), 4); + SG_CHECK_EQUAL(v1.patchLevel(), 7); + SG_CHECK_EQUAL(v1.suffix(), suffix); + + // Round-trips std::string <-> AddonVersion + SG_CHECK_EQUAL(AddonVersion("2017.4.7.dev13").str(), "2017.4.7.dev13"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7a2.dev8").str(), "2017.4.7a2.dev8"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7a2").str(), "2017.4.7a2"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7b2.dev5").str(), "2017.4.7b2.dev5"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7b2").str(), "2017.4.7b2"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7rc1.dev3").str(), "2017.4.7rc1.dev3"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7rc1").str(), "2017.4.7rc1"); + SG_CHECK_EQUAL(AddonVersion("2017.4.7").str(), "2017.4.7"); + + // Check stream output + std::ostringstream oss; + oss << AddonVersion("2017.4.7b2.dev5"); + SG_CHECK_EQUAL(oss.str(), "2017.4.7b2.dev5"); + + // Check ordering with all types of transitions, using operator<() + auto checkStrictOrdering = [](const vector& v) { + assert(v.size() > 1); + for (std::size_t i=0; i < v.size() - 1; i++) { + SG_CHECK_LT(v[i], v[i+1]); + } + }; + + checkStrictOrdering({ + "3.12.8.dev1", "3.12.8.dev2", "3.12.8.dev12", "3.12.8a1.dev1", + "3.12.8a1.dev2", "3.12.8a1", "3.12.8a2", "3.12.8b1.dev1", + "3.12.8b1.dev2", "3.12.8b1", "3.12.8b2", "3.12.8b10", + "3.12.8rc1.dev1", "3.12.8rc1.dev2", "3.12.8rc1.dev3", + "3.12.8rc1", "3.12.8rc2", "3.12.8rc3", "3.12.8", "3.12.9.dev1", + "3.12.9", "3.13.0", "4.0.0.dev1", "4.0.0.dev10", "4.0.0a1", "4.0.0", + "2017.4.0", "2017.4.1", "2017.4.10", "2017.5.0", "2018.0.0"}); + + fgtest::shutdownTestGlobals(); +} + +void testAddon() +{ + Addon m; + std::string addonId = "org.FlightGear.addons.MyGreatAddon"; + m.setId(addonId); + m.setVersion(AddonVersion("2017.2.5rc3")); + m.setBasePath(SGPath("/path/to/MyGreatAddon")); + m.setMinFGVersionRequired("2017.4.1"); + m.setMaxFGVersionRequired("none"); + + SG_CHECK_EQUAL(m.getId(), addonId); + SG_CHECK_EQUAL(*m.getVersion(), AddonVersion("2017.2.5rc3")); + SG_CHECK_EQUAL(m.getBasePath(), SGPath("/path/to/MyGreatAddon")); + SG_CHECK_EQUAL(m.getMinFGVersionRequired(), "2017.4.1"); + + const string refText = "addon '" + addonId + "' (version = 2017.2.5rc3, " + "base path = '/path/to/MyGreatAddon', " + "minFGVersionRequired = '2017.4.1', " + "maxFGVersionRequired = 'none')"; + SG_CHECK_EQUAL(m.str(), refText); + + // Check stream output + std::ostringstream oss; + oss << m; + SG_CHECK_EQUAL(oss.str(), refText); + + // Set a max FG version and recheck + m.setMaxFGVersionRequired("2018.2.5"); + const string refText2 = "addon '" + addonId + "' (version = 2017.2.5rc3, " + "base path = '/path/to/MyGreatAddon', " + "minFGVersionRequired = '2017.4.1', " + "maxFGVersionRequired = '2018.2.5')"; + SG_CHECK_EQUAL(m.getMaxFGVersionRequired(), "2018.2.5"); + SG_CHECK_EQUAL(m.str(), refText2); +} + +int main(int argc, const char* const* argv) +{ + testAddonVersionSuffix(); + testAddonVersion(); + testAddon(); + + return EXIT_SUCCESS; +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 0ed3618ba..206597440 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -3,7 +3,8 @@ include_directories(${PROJECT_SOURCE_DIR}) # note order here affects link order, and hence linking correctness # on systems with a traditional ld (eg, GNU ld on Linux) -foreach( mylibfolder +foreach( mylibfolder + Add-ons Airports Aircraft ATC diff --git a/src/Main/fg_init.cxx b/src/Main/fg_init.cxx index 54ac18f55..55d97c004 100644 --- a/src/Main/fg_init.cxx +++ b/src/Main/fg_init.cxx @@ -76,6 +76,8 @@ #include #include +#include + #include #include #include @@ -1135,7 +1137,10 @@ void fgStartNewReset() globals->resetPropertyRoot(); // otherwise channels are duplicated globals->get_channel_options_list()->clear(); - + + flightgear::AddonManager::reset(); + flightgear::AddonManager::createInstance(); + fgInitConfig(0, NULL, true); fgInitGeneral(); // all of this? diff --git a/src/Main/main.cxx b/src/Main/main.cxx index 3264d8d8f..44d8c291d 100644 --- a/src/Main/main.cxx +++ b/src/Main/main.cxx @@ -56,6 +56,7 @@ extern bool global_crashRptEnabled; #include #include +#include #include
#include #include @@ -534,6 +535,8 @@ int fgMainInit( int argc, char **argv ) return EXIT_SUCCESS; } + AddonManager::createInstance(); + configResult = flightgear::Options::sharedInstance()->processOptions(); if (configResult == flightgear::FG_OPTIONS_ERROR) { return EXIT_FAILURE; diff --git a/src/Main/options.cxx b/src/Main/options.cxx index b4ceffe6c..31323935a 100644 --- a/src/Main/options.cxx +++ b/src/Main/options.cxx @@ -65,6 +65,7 @@ #include #endif +#include #include
#include #include "globals.hxx" @@ -705,33 +706,16 @@ clearLocation () static int fgOptAddon(const char *arg) { - const SGPath path(SGPath::fromLocal8Bit(arg)); - const SGPath config_xml = path / "config.xml"; + const SGPath addonPath = SGPath::fromLocal8Bit(arg); + const auto& addonManager = AddonManager::instance(); - if (config_xml.exists()) { - try { - readProperties(config_xml, globals->get_props()); - } catch (const sg_exception &e) { - const string msg = "Unable to load '" + config_xml.utf8Str() + "'. " - "Please check that this file exists and is readable.\n\n" + - "Exception information: " + e.getFormattedMessage(); - SG_LOG(SG_GENERAL, SG_ALERT, msg); - flightgear::fatalMessageBoxThenExit( - "FlightGear", "Unable to load an addon's config.xml file.", msg); - } - - globals->append_aircraft_path(path); - fgGetNode("addons", true) - ->addChild("addon") - ->getNode("path", true) - ->setStringValue(path.utf8Str()); - } else { + try { + addonManager->registerAddon(addonPath); + } catch (const sg_exception &e) { + string msg = "Error registering an add-on: " + e.getFormattedMessage(); + SG_LOG(SG_GENERAL, SG_ALERT, msg); flightgear::fatalMessageBoxThenExit( - "FlightGear", - "Path specified with --addon does not exist or no config.xml found in " - "that path.", - "Unable to find the file '" + config_xml.utf8Str() + "'." - ); + "FlightGear", "Unable to register an add-on.", msg); } return FG_OPTIONS_OK; diff --git a/src/Scripting/CMakeLists.txt b/src/Scripting/CMakeLists.txt index 1a40daa70..4d4bf682d 100644 --- a/src/Scripting/CMakeLists.txt +++ b/src/Scripting/CMakeLists.txt @@ -3,6 +3,7 @@ include(FlightGearComponent) set(SOURCES NasalSys.cxx nasal-props.cxx + NasalAddons.cxx NasalAircraft.cxx NasalPositioned.cxx NasalPositioned_cppbind.cxx @@ -18,6 +19,7 @@ set(SOURCES set(HEADERS NasalSys.hxx NasalSys_private.hxx + NasalAddons.hxx NasalAircraft.hxx NasalPositioned.hxx NasalCanvas.hxx diff --git a/src/Scripting/NasalAddons.cxx b/src/Scripting/NasalAddons.cxx new file mode 100644 index 000000000..d6d657735 --- /dev/null +++ b/src/Scripting/NasalAddons.cxx @@ -0,0 +1,197 @@ +// -*- coding: utf-8 -*- +// +// NasalAddons.cxx --- Expose add-on classes to Nasal +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include +#include + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +namespace flightgear +{ + +// *************************************************************************** +// * AddonManager * +// *************************************************************************** + +static const std::unique_ptr& getAddonMgrWithCheck() +{ + const auto& addonMgr = AddonManager::instance(); + + if (!addonMgr) { + throw sg_exception("trying to access the AddonManager from Nasal, " + "however it is not initialized"); + } + + return addonMgr; +} + +static naRef f_registeredAddons(const nasal::CallContext& ctx) +{ + const auto& addonMgr = getAddonMgrWithCheck(); + return ctx.to_nasal(addonMgr->registeredAddons()); +} + +static naRef f_isAddonRegistered(const nasal::CallContext& ctx) +{ + const auto& addonMgr = getAddonMgrWithCheck(); + std::string addonId = ctx.requireArg(0); + return ctx.to_nasal(addonMgr->isAddonRegistered(addonId)); +} + +static naRef f_loadedAddons(const nasal::CallContext& ctx) +{ + const auto& addonMgr = getAddonMgrWithCheck(); + return ctx.to_nasal(addonMgr->loadedAddons()); +} + +static naRef f_isAddonLoaded(const nasal::CallContext& ctx) +{ + const auto& addonMgr = getAddonMgrWithCheck(); + std::string addonId = ctx.requireArg(0); + return ctx.to_nasal(addonMgr->isAddonLoaded(addonId)); +} + +static naRef f_getAddon(const nasal::CallContext& ctx) +{ + const auto& addonMgr = getAddonMgrWithCheck(); + return ctx.to_nasal(addonMgr->getAddon(ctx.requireArg(0))); +} + +static void wrapAddonManagerMethods(nasal::Hash& addonsModule) +{ + addonsModule.set("registeredAddons", &f_registeredAddons); + addonsModule.set("isAddonRegistered", &f_isAddonRegistered); + addonsModule.set("loadedAddons", &f_loadedAddons); + addonsModule.set("isAddonLoaded", &f_isAddonLoaded); + addonsModule.set("getAddon", &f_getAddon); +} + +// *************************************************************************** +// * AddonVersion * +// *************************************************************************** + +// Create a new AddonVersion instance. +// +// addons.AddonVersion.new(versionString) +// addons.AddonVersion.new(major[, minor[, patchLevel[, suffix]]]) +// +// where: +// - 'major', 'minor' and 'patchLevel' are integers; +// - 'suffix' is a string such as "", "a3", "b12" or "rc4" (resp. meaning: +// release, alpha 3, beta 12, release candidate 4) Suffixes for +// development releases are also allowed, such as ".dev4" (sorts before +// the release as well as any alphas, betas and rcs for the release) and +// "a3.dev10" (sorts before "a3.dev11", "a3.dev12", "a3", etc.). +// +// For details, see which is a +// proper superset of what is allowed here. +naRef f_createAddonVersion(const nasal::CallContext& ctx) +{ + int major = 0, minor = 0, patchLevel = 0; + std::string suffix; + + if (ctx.argc == 0 || ctx.argc > 4) { + naRuntimeError(ctx.c, + "AddonVersion.new(versionString) or " + "AddonVersion.new(major[, minor[, patchLevel[, suffix]]])"); + } + + if (ctx.argc == 1) { + naRef arg1 = ctx.args[0]; + + if (naIsString(arg1)) { + return ctx.to_nasal(AddonVersionRef(new AddonVersion(naStr_data(arg1)))); + } else if (naIsNum(arg1)) { + AddonVersionRef ref{new AddonVersion(arg1.num, minor, patchLevel, + AddonVersionSuffix(suffix))}; + return ctx.to_nasal(std::move(ref)); + } else { + naRuntimeError(ctx.c, + "AddonVersion.new(versionString) or " + "AddonVersion.new(major[, minor[, patchLevel[, suffix]]])"); + } + } + + assert(ctx.argc > 0); + if (!naIsNum(ctx.args[0])) { + naRuntimeError( + ctx.c, "addons.AddonVersion.new() requires major number as an integer"); + } + + major = ctx.args[0].num; + + if (ctx.argc > 1) { + if (!naIsNum(ctx.args[1])) { + naRuntimeError( + ctx.c, "addons.AddonVersion.new() requires minor number as an integer"); + } + + minor = ctx.args[1].num; + } + + if (ctx.argc > 2) { + if (!naIsNum(ctx.args[2])) { + naRuntimeError( + ctx.c, "addons.AddonVersion.new() requires patch level as an integer"); + } + + patchLevel = ctx.args[2].num; + } + + if (ctx.argc > 3) { + if (!naIsString(ctx.args[3])) { + naRuntimeError( + ctx.c, "addons.AddonVersion.new() requires suffix as a string"); + } + + suffix = naStr_data(ctx.args[3]); + } + + assert(ctx.argc <= 4); + + return ctx.to_nasal( + AddonVersionRef(new AddonVersion(major, minor, patchLevel, + AddonVersionSuffix(suffix)))); +} + +void initAddonClassesForNasal(naRef globals, naContext c) +{ + nasal::Hash globalsModule(globals, c); + nasal::Hash addonsModule = globalsModule.createHash("addons"); + + wrapAddonManagerMethods(addonsModule); + + Addon::setupGhost(addonsModule); + + AddonVersion::setupGhost(addonsModule); + addonsModule.createHash("AddonVersion").set("new", &f_createAddonVersion); +} + +} // of namespace flightgear diff --git a/src/Scripting/NasalAddons.hxx b/src/Scripting/NasalAddons.hxx new file mode 100644 index 000000000..a325da2db --- /dev/null +++ b/src/Scripting/NasalAddons.hxx @@ -0,0 +1,32 @@ +// -*- coding: utf-8 -*- +// +// NasalAddons.hxx --- Expose add-on classes to Nasal +// Copyright (C) 2017 Florent Rougon +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#ifndef FG_ADDON_NASAL_INTERFACE_HXX +#define FG_ADDON_NASAL_INTERFACE_HXX + +#include + +namespace flightgear +{ + +void initAddonClassesForNasal(naRef globals, naContext c); + +} // of namespace flightgear + +#endif // of FG_ADDON_NASAL_INTERFACE_HXX diff --git a/src/Scripting/NasalSys.cxx b/src/Scripting/NasalSys.cxx index 9e37a2e93..41f29f7bc 100644 --- a/src/Scripting/NasalSys.cxx +++ b/src/Scripting/NasalSys.cxx @@ -38,6 +38,7 @@ #include #include +#include "NasalAddons.hxx" #include "NasalSGPath.hxx" #include "NasalSys.hxx" #include "NasalSys_private.hxx" @@ -856,6 +857,8 @@ void FGNasalSys::setCmdArg(SGPropertyNode* aNode) void FGNasalSys::init() { + using namespace flightgear; + if (_inited) { SG_LOG(SG_GENERAL, SG_ALERT, "duplicate init of Nasal"); } @@ -881,7 +884,7 @@ void FGNasalSys::init() for(i=0; funcs[i].name; i++) hashset(_globals, funcs[i].name, naNewFunc(_context, naNewCCode(_context, funcs[i].func))); - nasal::Hash io_module = nasal::Hash(_globals, _context).get("io"); + nasal::Hash io_module = getGlobals().get("io"); io_module.set("open", f_open); // And our SGPropertyNode wrapper @@ -911,6 +914,8 @@ void FGNasalSys::init() .member("simulatedTime", &TimerObj::isSimTime, &f_timerObj_setSimTime) .member("isRunning", &TimerObj::isRunning); + initAddonClassesForNasal(_globals, _context); + // Now load the various source files in the Nasal directory simgear::Dir nasalDir(SGPath(globals->get_fg_root(), "Nasal")); loadScriptDirectory(nasalDir); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 07e964c1f..2a7c337c6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -7,6 +7,9 @@ set(sources Main/locale.cxx Main/util.cxx Main/positioninit.cxx + Add-ons/Addon.cxx + Add-ons/AddonManager.cxx + Add-ons/AddonVersion.cxx Aircraft/controls.cxx Aircraft/FlightHistory.cxx Aircraft/flightrecorder.cxx @@ -59,6 +62,7 @@ set(sources Time/TimeManager.cxx Time/bodysolver.cxx Scripting/NasalSys.cxx + Scripting/NasalAddons.cxx Scripting/NasalCondition.cxx Scripting/NasalAircraft.cxx Scripting/NasalString.cxx