72414d039a
With FlightGear commit 4d36082, the contents of /addon/authors (resp. /addon/maintainers) in addon-metadata.xml files is now structured. It may contain an arbitrary number of 'author' (resp. 'maintainer') child nodes, each of which contains subnodes 'name', 'email' and 'url' ('name' being mandatory, 'email' and 'url' being optional). This commit updates Docs/README.add-ons regarding this change in the syntax for addon-metadata.xml files, as well as the related modifications to the add-on Nasal interface.
595 lines
22 KiB
Text
595 lines
22 KiB
Text
-*- coding: utf-8; fill-column: 72; -*-
|
|
|
|
Add-ons in FlightGear
|
|
=====================
|
|
|
|
This document explains how add-ons work in FlightGear. The add-on
|
|
feature was first added in FlightGear 2017.3. This document describes an
|
|
evolution of the framework that appeared in FlightGear 2017.4.
|
|
|
|
|
|
Contents
|
|
--------
|
|
|
|
1. Terminology
|
|
2. The addon-metadata.xml file
|
|
3. Add-on metadata in the Property Tree
|
|
4. How to run code after an add-on is loaded
|
|
5. Overview of the C++ API
|
|
6. Nasal API
|
|
|
|
|
|
Introduction
|
|
------------
|
|
|
|
fgfs can be passed the --addon=<path> option, where <path> indicates an
|
|
add-on directory. Such a directory, when used as the argument of
|
|
--addon, receives special treatment :
|
|
|
|
1) The add-on directory is added to the list of aircraft paths.
|
|
|
|
2) The add-on directory must contain a PropertyList file called
|
|
addon-metadata.xml that gives the name of the add-on, its
|
|
identifier (id), its version and possibly a few other things (see
|
|
details below).
|
|
|
|
3) The add-on directory may contain a PropertyList file called
|
|
config.xml, in which case it will be loaded into the Property Tree
|
|
at FlightGear startup, as if it were passed to the --config fgfs
|
|
option.
|
|
|
|
4) The add-on directory must contain a Nasal file called main.nas.
|
|
This file will be loaded at startup too, and its main() function
|
|
run in the namespace __addon[ADDON_ID]__, where ADDON_ID is the
|
|
add-on identifier specified in the addon-metadata.xml file. This
|
|
operation is done by $FG_ROOT/Nasal/addons.nas at the time of this
|
|
writing.
|
|
|
|
Also, the Property Tree is populated (under /addons) with information
|
|
about registered add-ons. More details will be given below.
|
|
|
|
The --addon option can be specified zero or more times; each of the
|
|
operations indicated above is carried out for every specified add-on in
|
|
the order given by the --addon options used: that's what we call add-on
|
|
registration order, or add-on load order. In other words, add-ons are
|
|
registered and loaded in the order specified by the --addon options
|
|
used.
|
|
|
|
|
|
1. Terminology
|
|
~~~~~~~~~~~
|
|
|
|
add-on base path
|
|
|
|
Path to a directory containing all of the add-on files. This is the
|
|
path passed to the --addon fgfs option, when one wants to load the
|
|
add-on in question.
|
|
|
|
add-on identifier (id)
|
|
|
|
A string such as org.flightgear.addons.ATCChatter or
|
|
user.joe.MyGreatAddon, used to uniquely identify an add-on. The add-on
|
|
identifier is declared in <path>/addon-metadata.xml, where <path> is
|
|
the add-on base path.
|
|
|
|
add-on registration
|
|
|
|
When a --addon option is processed, FlightGear ensures that the add-on
|
|
identifier found in the corresponding addon-metadata.xml file isn't
|
|
already used by an add-on from a previous --addon option on the same
|
|
command line, and stores the add-on metadata inside dedicated C++
|
|
objects. This process is called add-on registration.
|
|
|
|
add-on loading
|
|
|
|
The following sequence of actions:
|
|
|
|
a) loading an add-on's main.nas file in the namespace
|
|
__addon[ADDON_ID]__
|
|
b) calling its main() function
|
|
|
|
is performed later (see $FG_ROOT/Nasal/addons.nas) and called add-on
|
|
loading.
|
|
|
|
|
|
2. The addon-metadata.xml file
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
Every add-on must have in its base directory a file called
|
|
'addon-metadata.xml'. This section explains how to write this file.
|
|
|
|
Sample addon-metadata.xml file
|
|
==============================
|
|
|
|
Here is an example of an addon-metadata.xml file, for a hypothetical
|
|
add-on called “Flying Turtle” distributed by Joe User:
|
|
|
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
<PropertyList>
|
|
<meta>
|
|
<file-type type="string">FlightGear add-on metadata</file-type>
|
|
<format-version type="int">1</format-version>
|
|
</meta>
|
|
|
|
<addon>
|
|
<identifier type="string">user.joe.FlyingTurtle</identifier>
|
|
<name type="string">Flying Turtle</name>
|
|
<version type="string">1.0.0rc2</version>
|
|
|
|
<authors>
|
|
<author>
|
|
<name type="string">Joe User</name>
|
|
<email type="string">optional_address@example.com</email>
|
|
<url type="string">http://joe.example.com/foobar/</url>
|
|
</author>
|
|
|
|
<author>
|
|
<name type="string">Jane Maintainer</name>
|
|
<email type="string">jane@example.com</email>
|
|
<url type="string">https://jane.example.com/</url>
|
|
</author>
|
|
</authors>
|
|
|
|
<maintainers>
|
|
<maintainer>
|
|
<name type="string">Jane Maintainer</name>
|
|
<email type="string">jane@example.com</email>
|
|
<url type="string">https://jane.example.com/</url>
|
|
</maintainer>
|
|
</maintainers>
|
|
|
|
<short-description type="string">
|
|
Allow flying with new foobar powers.
|
|
</short-description>
|
|
|
|
<long-description type="string">
|
|
This add-on enables something really great involving turtles...
|
|
</long-description>
|
|
|
|
<license>
|
|
<designation type="string">
|
|
GNU GPL version 2 or later
|
|
</designation>
|
|
|
|
<file type="string">
|
|
COPYING
|
|
</file>
|
|
|
|
<url type="string">
|
|
https://www.gnu.org/licenses/old-licenses/gpl-2.0.en.html
|
|
</url>
|
|
</license>
|
|
|
|
<min-FG-version type="string">2017.4.0</min-FG-version>
|
|
<max-FG-version type="string">none</max-FG-version>
|
|
|
|
<urls>
|
|
<home-page type="string">
|
|
https://example.com/quux
|
|
</home-page>
|
|
|
|
<download type="string">
|
|
https://example.com/quux/download
|
|
</download>
|
|
|
|
<support type="string">
|
|
https://example.com/quux/support
|
|
</support>
|
|
|
|
<code-repository type="string">
|
|
https://example.com/quux/code-repository
|
|
</code-repository>
|
|
</urls>
|
|
|
|
<tags>
|
|
<tag type="string">first tag</tag>
|
|
<tag type="string">second tag</tag>
|
|
<tag type="string">etc.</tag>
|
|
</tags>
|
|
</addon>
|
|
</PropertyList>
|
|
|
|
General rules
|
|
=============
|
|
|
|
We use the terms “field” or “node” interchangeably here to refer to
|
|
nodes of the addon-metadata.xml PropertyList file (technically, a field
|
|
always has a value, possibly empty, therefore fields are all leaf
|
|
nodes).
|
|
|
|
Leading and trailing whitespace in each field of addon-metadata.xml is
|
|
removed. All other whitespace is a priori preserved (this could depend
|
|
on the particular field, though).
|
|
|
|
Most fields are optional. In most cases, omitting a field is the same as
|
|
leaving it empty. But don't write empty tag fields, it is really too
|
|
ugly. ;-)
|
|
|
|
Name and id
|
|
===========
|
|
|
|
Nodes: /addon/name and /addon/identifier
|
|
|
|
The add-on name is the pretty form. It should not be overly long, but
|
|
otherwise isn't constrained. On the other hand, the add-on identifier
|
|
(id), which serves to uniquely identify an add-on:
|
|
- must contain only ASCII letters (A-Z, a-z) and dots ('.');
|
|
- must be in reverse DNS style (even if the domain doesn't exist),
|
|
e.g., org.flightgear.addons.ATCChatter for an add-on distributed in
|
|
FGAddon, or user.joe.FlyingTurtle for Joe User's “Flying Turtle”
|
|
add-on. Of course, if Joe User owns a domain name and uses it to
|
|
distribute his add-on, he should put it here.
|
|
|
|
Authors and maintainers
|
|
=======================
|
|
|
|
Nodes: /addon/authors and /addon/maintainers
|
|
|
|
Authors are people who contributed significantly to the add-on.
|
|
Maintainers are people currently in charge of maintaining it.
|
|
|
|
It is possible to declare any number of authors and any number of
|
|
maintainers---the example above shows only one maintainer for shortness,
|
|
but this is not a restriction.
|
|
|
|
For each author and maintainer, you can give a name, an email address
|
|
and a URL. The name must be non-empty, but the email address and URL
|
|
need not be specified or may be left empty, which is equivalent.
|
|
Obviously, if no email address nor URL is given for any maintainer, it
|
|
is highly desirable that /addon/urls/support contains a usable URL for
|
|
contacting the add-on maintainers.
|
|
|
|
The data in children nodes of /addon/maintainers may refer either to
|
|
real persons or to more abstract entities such as mailing-lists. In case
|
|
of a real person, the corresponding URL, if specified, is expected to be
|
|
the person's home page. On the other hand, if a declared “maintainer” is
|
|
a mailing-list, a good use for the 'url' field is to indicate the
|
|
address of a web page from which people can subscribe to the
|
|
mailing-list.
|
|
|
|
Short and long descriptions
|
|
===========================
|
|
|
|
Nodes: /addon/short-description and /addon/long-description
|
|
|
|
The short description should fit on one line (try not to exceed, say, 78
|
|
characters), and in general consist of only one sentence.
|
|
|
|
The long description is essentially free-form, but only break lines when
|
|
you do want a line break at this point. In other words, don't wrap lines
|
|
manually in the XML file: this will be automatically done by the
|
|
software displaying the add-on description, according to the particular
|
|
line width it uses (which can depend on the user's screen or
|
|
configuration, etc.). A single \n inside a paragraph (see footnote [1])
|
|
means a hard line break. Two \n in a row (i.e., a blank line) should be
|
|
used to separate paragraphs. Example:
|
|
|
|
This is a paragraph.
|
|
This is the second line of the same paragraph. It can be very, very, very long and contain several sentences.
|
|
|
|
This is a different paragraph. Again, don't break lines (i.e., don't press Enter) unless a particular formatting reason makes it necessary. For instance, it is okay to break lines in order to present a list of items, but not for line wrapping.
|
|
|
|
Licensing terms
|
|
===============
|
|
|
|
Nodes: /addon/license/designation
|
|
/addon/license/file
|
|
/addon/license/url
|
|
|
|
The /add-on/license/designation node should describe the add-on
|
|
licensing terms in a short but accurate way, if possible. If this is not
|
|
practically doable, use the value “Custom”. If the add-on is distributed
|
|
under several licenses, use the value “Multiple”. In all cases, make
|
|
sure the licensing terms are clearly specified in other files of the
|
|
add-on (typically, at least README.txt or COPYING). Values for
|
|
/add-on/license/designation could be “GNU GPL version 2 or later”, “CC0
|
|
1.0 Universal”, “3-clause BSD”, etc.
|
|
|
|
In most cases, the add-on should contain a file containing the full
|
|
license text. Use the /add-on/license/file node to point to this file:
|
|
it should contain a file path that is relative to the add-on base
|
|
directory. This path must use slash separators ('/'), even if you use
|
|
Windows.
|
|
|
|
The /add-on/license/url node should contain a single URL if there is an
|
|
official, stable URL for the license under which the add-on is
|
|
distributed. The term “official” here is to be interpreted in the
|
|
context of the particular license. For instance, for a GNU license
|
|
(GPL2, LGPL2.1, etc.), the URL domain must be gnu.org; for a CC license
|
|
(CC0 1.0 Universal, CC-BY-SA 4.0...), it must be creativecommons.org,
|
|
etc.
|
|
|
|
Minimum and maximum FlightGear versions
|
|
=======================================
|
|
|
|
Nodes: /addon/min-FG-version and /addon/max-FG-version
|
|
|
|
These two nodes are optional and may be omitted unless the add-on is
|
|
known not to work with particular FlightGear versions.
|
|
/addon/min-FG-version defaults to 2017.4.0 and /addon/max-FG-version to
|
|
the special value 'none' (only allowed for /addon/max-FG-version). Apart
|
|
from this special case, every non-empty value present in one of these
|
|
two fields must be a proper FlightGear version number usable with
|
|
simgear::strutils::compare_versions(), for instance '2017.4.1'.
|
|
|
|
Add-on version
|
|
==============
|
|
|
|
Node: /addon/version
|
|
|
|
The /addon/version node gives the version of the add-on and must obey a
|
|
strict syntax[2], which is a subset of what is described in PEP 440:
|
|
|
|
https://www.python.org/dev/peps/pep-0440/
|
|
|
|
Valid examples are, in increasing sort order:
|
|
|
|
1.2.5.dev1 # first development release of 1.2.5
|
|
1.2.5.dev4 # fourth development release of 1.2.5
|
|
1.2.5
|
|
1.2.9
|
|
1.2.10a1.dev2 # second dev release of the first alpha release of 1.2.10
|
|
1.2.10a1 # first alpha release of 1.2.10
|
|
1.2.10b5 # fifth beta release of 1.2.10
|
|
1.2.10rc12 # twelfth release candidate for 1.2.10
|
|
1.2.10
|
|
1.3.0
|
|
2017.4.12a2
|
|
2017.4.12b1
|
|
2017.4.12rc1
|
|
2017.4.12
|
|
|
|
.devN suffixes can of course be used on beta and release candidates too,
|
|
just as with the 1.2.10a1.dev2 example given above for an alpha release.
|
|
Note that a development release always sorts before the corresponding
|
|
non-development release (e.g., 2017.2.1b5.dev4 comes before 2017.2.1b5).
|
|
|
|
Other fields
|
|
============
|
|
|
|
The other nodes of 'addon-metadata.xml' should be self-explanatory. :-)
|
|
|
|
|
|
3. Add-on metadata in the Property Tree
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The most important metadata for each registered add-on is made
|
|
accessible in the Property Tree under /addons/by-id/ADDON_ID and the
|
|
property /addons/by-id/ADDON_ID/loaded can be checked or listened to, in
|
|
order to determine when a particular add-on is loaded. There is also a
|
|
Nasal interface to access add-on metadata in a convenient way (see
|
|
below).
|
|
|
|
More precisely, when an add-on is registered, its name, id, base path,
|
|
version (converted to a string), loaded status (boolean) and load
|
|
sequence number (int) become available in the Property Tree as
|
|
/addons/by-id/ADDON_ID/{name,id,path,version,loaded,load-seq-num}. The
|
|
loaded status is initially false, and set to true when the add-on
|
|
loading phase is complete.
|
|
|
|
There are also /addons/addon[i]/path nodes where i is 0 for the first
|
|
registered add-on, 1 for the second one, etc.
|
|
|
|
|
|
4. How to run code after an add-on is loaded
|
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
You may want to set up Nasal code to be run after an add-on is loaded;
|
|
here is how to do that:
|
|
|
|
var addonId = "user.joe.FlyingTurtle";
|
|
var loadedFlagNode = props.globals.getNode("/addons")
|
|
.getChild("by-id", 0, 1)
|
|
.getChild(addonId, 0, 1)
|
|
.getChild("loaded", 0, 1);
|
|
|
|
if (loadedFlagNode.getBoolValue()) {
|
|
logprint(5, addonId ~ " is already loaded");
|
|
} else {
|
|
# Define a function to be called after the add-on is loaded
|
|
var id = setlistener(
|
|
loadedFlagNode,
|
|
func(changedNode, listenedNode) {
|
|
if (listenedNode.getBoolValue()) {
|
|
removelistener(id);
|
|
logprint(5, addonId ~ " is loaded");
|
|
};
|
|
},
|
|
0, 0);
|
|
}
|
|
|
|
|
|
5. Overview of the C++ API
|
|
~~~~~~~~~~~~~~~~~~~~~~~
|
|
|
|
The add-on C++ infrastructure mainly relies on the following classes:
|
|
AddonManager, Addon and AddonVersion. AddonManager is used to register
|
|
add-ons, which later leads to their loading. AddonManager relies on an
|
|
std::map<std::string, AddonRef>, where keys are add-on identifiers and
|
|
AddonRef is SGSharedPtr<Addon> at the time of this writing (changing it
|
|
to another kind of smart pointer should be a mere one-line change). This
|
|
map holds the metadata of each registered add-on. Accessor methods are
|
|
available for:
|
|
|
|
- retrieving the lists of registered and loaded add-ons;
|
|
|
|
- checking if a particular add-on has already been registered or
|
|
loaded;
|
|
|
|
- for each add-on, obtaining an Addon instance which can be queried
|
|
for its identifier, its name, identifier, version, base path, the
|
|
minimum and maximum FlightGear versions it requires, its base node
|
|
in the Property Tree, its order in the load sequence...
|
|
|
|
The AddonVersion class handles everything about add-on version numbers:
|
|
- initialization from the individual components or from a string;
|
|
- conversion to a string and output to an std::ostream;
|
|
- access to every component;
|
|
- comparisons using the standard operators: ==, !=, <, <=, >, >=.
|
|
|
|
Registering an add-on using AddonManager::registerAddon() ensures
|
|
uniqueness of the add-on identifier and makes its name, identifier, base
|
|
path, version (converted to a string), loaded status (boolean) and load
|
|
sequence number (int) available in the Property Tree as
|
|
/addons/by-id/ADDON_ID/{name,id,path,version,loaded,load-seq-num}.
|
|
|
|
Note: if C++ code needs to use the add-on base path, better use
|
|
AddonManager::addonBasePath() or Addon::getBasePath(), whose
|
|
return values can't be tampered with by Nasal code.
|
|
|
|
AddonManager::registerAddon() fails with a specific exception if the
|
|
running FlightGear instance doesn't match the min-FG-version and
|
|
max-FG-version requirements declared in the addon-metadata.xml file, as
|
|
well as in the obvious other cases (config.xml or addon-metadata.xml not
|
|
found, invalid syntax in any of these files, etc.). The code in
|
|
options.cxx (fgOptAddon()) catches such exceptions and displays the
|
|
appropriate error message with SG_LOG() and fatalMessageBoxThenExit().
|
|
|
|
|
|
6. Nasal API
|
|
~~~~~~~~~
|
|
|
|
The Nasal add-on API all lives in the 'addons' namespace. It gives Nasal
|
|
code easy access to add-on metadata, for instance like this:
|
|
|
|
var myAddon = addons.getAddon("user.joe.FlyingTurtle");
|
|
print(myAddon.id);
|
|
print(myAddon.name);
|
|
print(myAddon.version.str());
|
|
|
|
foreach (var author; myAddon.authors) {
|
|
print(author.name, author.email, author.url);
|
|
}
|
|
|
|
foreach (var maintainer; myAddon.maintainers) {
|
|
print(maintainer.name, maintainer.email, maintainer.url);
|
|
}
|
|
|
|
print(myAddon.shortDescription);
|
|
print(myAddon.longDescription);
|
|
print(myAddon.licenseDesignation);
|
|
print(myAddon.licenseFile);
|
|
print(myAddon.licenseUrl);
|
|
print(myAddon.basePath);
|
|
print(myAddon.minFGVersionRequired);
|
|
print(myAddon.maxFGVersionRequired);
|
|
print(myAddon.homePage);
|
|
print(myAddon.downloadUrl);
|
|
print(myAddon.supportUrl);
|
|
print(myAddon.codeRepositoryUrl);
|
|
|
|
foreach (var tag; myAddon.tags) {
|
|
print(tag);
|
|
}
|
|
|
|
print(myAddon.loadSequenceNumber);
|
|
# myAddon.node is a props.Node object for /addons/by-id/ADDON_ID
|
|
print(myAddon.node.getPath());
|
|
|
|
Among other things, the Nasal add-on API allows one to get the version
|
|
of any registered add-on as a ghost and reliably compare it to another
|
|
instance of addons.AddonVersion:
|
|
|
|
var myAddon = addons.getAddon("user.joe.FlyingTurtle");
|
|
var firstVersionOK = addons.AddonVersion.new("2.12.5rc1");
|
|
# Or alternatively:
|
|
# var firstVersionOK = addons.AddonVersion.new(2, 12, 5, "rc1");
|
|
|
|
if (myAddon.version.lowerThan(firstVersionOK)) {
|
|
...
|
|
|
|
Here follows the complete Nasal add-on API, at the time of this writing.
|
|
All strings are encoded in UTF-8.
|
|
|
|
Queries to the AddonManager:
|
|
|
|
addons.isAddonRegistered(string addonId) -> bool (1 or 0)
|
|
addons.registeredAddons() -> vector<addons.Addon>
|
|
(in registration/load order)
|
|
addons.isAddonLoaded(string addonId) -> bool (1 or 0)
|
|
addons.loadedAddons() -> vector<addons.Addon>
|
|
(in lexicographic order)
|
|
addons.getAddon(string addonId) -> addons.Addon instance (ghost)
|
|
|
|
Read-only data members (attributes) of addons.Addon objects:
|
|
|
|
id the add-on identifier, in reverse DNS style (string)
|
|
name the add-on “pretty name” (string)
|
|
version the add-on version (instance of addons.AddonVersion,
|
|
ghost)
|
|
authors the add-on authors (vector of addons.Author ghosts)
|
|
maintainers the add-on maintainers (vector of addons.Maintainer
|
|
ghosts)
|
|
shortDescription the add-on short description (string)
|
|
longDescription the add-on long description (string)
|
|
licenseDesignation licensing terms: "GNU GPL version 2 or later",
|
|
"CC0 1.0 Universal", etc. (string)
|
|
licenseFile relative, slash-separated path to a file under
|
|
the add-on base directory containing the license
|
|
text (string)
|
|
licenseUrl stable, official URL for the add-on license text
|
|
(string)
|
|
basePath path to the add-on base directory (string)
|
|
minFGVersionRequired minimum required FG version for the add-on (string)
|
|
maxFGVersionRequired max. required FG version... or "none" (string)
|
|
homePage add-on home page (string)
|
|
downloadUrl add-on download URL (string)
|
|
supportUrl add-on support URL (string)
|
|
codeRepositoryUrl URL pointing to the development repository of
|
|
the add-on (Git, Subversion, etc.; string)
|
|
tags vector containing the add-on tags used to help
|
|
users find add-ons (vector of strings)
|
|
node base node for the add-on in the Property Tree:
|
|
/addons/by-id/ADDON_ID (props.Node object)
|
|
loadSequenceNumber 0 for the first registered add-on, 1 for the
|
|
second one, etc. (integer)
|
|
|
|
Read-only data members (attributes) of addons.AddonVersion objects:
|
|
|
|
majorNumber non-negative integer
|
|
minorNumber non-negative integer
|
|
patchLevel non-negative integer
|
|
suffix string such as "", "a1", "b2.dev45", "rc12"...
|
|
|
|
Member functions (methods) of addons.AddonVersion objects:
|
|
|
|
new(string version) | construct from string
|
|
|
|
new(int major, int minor=0, int patchLevel=0, | construct
|
|
string suffix="") | from components
|
|
|
|
str() | string representation
|
|
|
|
equal(addons.AddonVersion other) |
|
|
nonEqual(addons.AddonVersion other) | compare to another
|
|
lowerThan(addons.AddonVersion other) | addons.AddonVersion
|
|
lowerThanOrEqual(addons.AddonVersion other) | instance
|
|
greaterThan(addons.AddonVersion other) |
|
|
greaterThanOrEqual(addons.AddonVersion other) |
|
|
|
|
Read-only data members (attributes) of addons.Author objects:
|
|
|
|
name author name (non-empty string)
|
|
email email address of the author (string)
|
|
url home page of the author (string)
|
|
|
|
Read-only data members (attributes) of addons.Maintainer objects:
|
|
|
|
name maintainer name (non-empty string)
|
|
email email address of the maintainer (string)
|
|
url home page of the maintainer, if a person; if the
|
|
maintainer is a mailing-list, the URL can point
|
|
to a web page from which people can subscribe to
|
|
that mailing-list (string)
|
|
|
|
Footnotes
|
|
---------
|
|
|
|
[1] \n represents end-of-line in string literals of languages such as C,
|
|
C++, Python and many others. We use this convention here to
|
|
represent the end-of-line character sequence in the XML data.
|
|
|
|
[2] MAJOR.MINOR.PATCHLEVEL[{a|b|rc}N1][.devN2] where MAJOR, MINOR and
|
|
PATCHLEVEL are non-negative integers, and N1 and N2 are positive
|
|
integers.
|