mirror of
https://gitlab.freedesktop.org/NetworkManager/NetworkManager
synced 2024-07-21 10:14:41 +00:00
![Thomas Haller](/assets/img/avatar_default.png)
When we generate the manual page for nm-settings-nmcli, we run: "/usr/bin/python" \ ./tools/generate-docs-nm-settings-docs-merge.py \ --only-from-first \ man/nm-settings-docs-nmcli.xml \ src/nmcli/gen-metadata-nm-settings-nmcli.xml \ src/libnm-client-impl/nm-property-infos-nmcli.xml \ src/libnm-client-impl/nm-settings-docs-gir.xml If "gen-metadata-nm-settings-nmcli.xml" contains either a <description> or a <description-docbook>, then we must not continue searching the other XML documents. The user provided an explicit override, and fallback (search further) is wrong. Previously, we might take <description> from the first file, and <description-docbook> from the second file. As "man/nm-settings-nmcli.xsl" prefers <description-docbook>, it takes the wrong text. Instead, as we search the files during merge, we must prefer the first one. Note that the change doesn't really matter anymore, because each XML now must also contain both <description> and <description-docbook>. There is an assertion for that. Also, stop generating <deprecated-docbook>. First, it lacked the important "since=" attribute and was necessary. Also, it's redundant and does not contain anything interesting. So far, we don't need special formatting for the deprecated message, and we likely never will. Also, stop accepting or generating the "description=" attribute. This should always be an XML element now.
373 lines
11 KiB
Python
Executable file
373 lines
11 KiB
Python
Executable file
#!/usr/bin/env python
|
|
# SPDX-License-Identifier: LGPL-2.1-or-later
|
|
#
|
|
# Copyright (C) 2009 - 2017 Red Hat, Inc.
|
|
#
|
|
from __future__ import print_function, unicode_literals
|
|
import xml.etree.ElementTree as ET
|
|
import argparse
|
|
import os
|
|
import gi
|
|
import re
|
|
|
|
gi.require_version("GIRepository", "2.0")
|
|
from gi.repository import GIRepository
|
|
|
|
try:
|
|
libs = os.environ["LD_LIBRARY_PATH"].split(":")
|
|
libs.reverse()
|
|
for lib in libs:
|
|
GIRepository.Repository.prepend_library_path(lib)
|
|
except AttributeError:
|
|
# An old GI version, that has no prepend_library_path
|
|
# It's alright, it probably interprets LD_LIBRARY_PATH
|
|
# correctly.
|
|
pass
|
|
except KeyError:
|
|
pass
|
|
|
|
gi.require_version("NM", "1.0")
|
|
from gi.repository import NM, GObject
|
|
|
|
dbus_type_name_map = {
|
|
"b": "boolean",
|
|
"s": "string",
|
|
"i": "int32",
|
|
"u": "uint32",
|
|
"t": "uint64",
|
|
"x": "int64",
|
|
"y": "byte",
|
|
"as": "array of string",
|
|
"au": "array of uint32",
|
|
"ay": "byte array",
|
|
"a{ss}": "dict of string to string",
|
|
"a{sv}": "vardict",
|
|
"aa{sv}": "array of vardict",
|
|
"aau": "array of array of uint32",
|
|
"aay": "array of byte array",
|
|
"a(ayuay)": "array of legacy IPv6 address struct",
|
|
"a(ayuayu)": "array of legacy IPv6 route struct",
|
|
}
|
|
|
|
ns_map = {
|
|
"c": "http://www.gtk.org/introspection/c/1.0",
|
|
"gi": "http://www.gtk.org/introspection/core/1.0",
|
|
"glib": "http://www.gtk.org/introspection/glib/1.0",
|
|
}
|
|
identifier_key = "{%s}identifier" % ns_map["c"]
|
|
nick_key = "{%s}nick" % ns_map["glib"]
|
|
symbol_prefix_key = "{%s}symbol-prefix" % ns_map["c"]
|
|
|
|
constants = {
|
|
"TRUE": "TRUE",
|
|
"FALSE": "FALSE",
|
|
"G_MAXUINT32": "G_MAXUINT32",
|
|
"NULL": "NULL",
|
|
}
|
|
setting_names = {}
|
|
|
|
|
|
def get_setting_name_define(setting):
|
|
n = setting.attrib[symbol_prefix_key]
|
|
if n and n.startswith("setting_"):
|
|
return n[8:].upper()
|
|
raise Exception('Unexpected symbol_prefix_key "%s"' % (n))
|
|
|
|
|
|
def init_constants(girxml, settings):
|
|
for const in girxml.findall("./gi:namespace/gi:constant", ns_map):
|
|
cname = const.attrib["{%s}type" % ns_map["c"]]
|
|
cvalue = const.attrib["value"]
|
|
if const.find('./gi:type[@name="utf8"]', ns_map) is not None:
|
|
cvalue = '"%s"' % cvalue
|
|
constants[cname] = cvalue
|
|
|
|
for enum in girxml.findall("./gi:namespace/gi:enumeration", ns_map):
|
|
for enumval in enum.findall("./gi:member", ns_map):
|
|
cname = enumval.attrib[identifier_key]
|
|
cvalue = "%s (%s)" % (cname, enumval.attrib["value"])
|
|
constants[cname] = cvalue
|
|
|
|
for enum in girxml.findall("./gi:namespace/gi:bitfield", ns_map):
|
|
for enumval in enum.findall("./gi:member", ns_map):
|
|
cname = enumval.attrib[identifier_key]
|
|
cvalue = "%s (0x%x)" % (cname, int(enumval.attrib["value"]))
|
|
constants[cname] = cvalue
|
|
|
|
for setting in settings:
|
|
setting_type_name = "NM" + setting.attrib["name"]
|
|
setting_name_symbol = (
|
|
"NM_SETTING_" + get_setting_name_define(setting) + "_SETTING_NAME"
|
|
)
|
|
if setting_name_symbol in constants:
|
|
setting_name = constants[setting_name_symbol]
|
|
setting_names[setting_type_name] = setting_name
|
|
|
|
|
|
def get_prop_type(setting, pspec):
|
|
dbus_type = setting.get_dbus_property_type(pspec.name).dup_string()
|
|
prop_type = dbus_type_name_map[dbus_type]
|
|
|
|
if GObject.type_is_a(pspec.value_type, GObject.TYPE_ENUM) or GObject.type_is_a(
|
|
pspec.value_type, GObject.TYPE_FLAGS
|
|
):
|
|
prop_type = "%s (%s)" % (pspec.value_type.name, prop_type)
|
|
|
|
return prop_type
|
|
|
|
|
|
def remove_prefix(line, prefix):
|
|
return line[len(prefix) :] if line.startswith(prefix) else line
|
|
|
|
|
|
def format_docs(doc_xml):
|
|
doc = doc_xml.text
|
|
|
|
# split docs into lines
|
|
lines = re.split("\n", doc)
|
|
# strip leading *char and strip white spaces
|
|
lines = [remove_prefix(l, "*").strip() for l in lines]
|
|
doc = ""
|
|
for l in lines:
|
|
if l:
|
|
doc += l + " "
|
|
else:
|
|
doc = doc.strip(" ") + "\n\n"
|
|
|
|
doc = doc.strip("\n ")
|
|
|
|
# Expand constants
|
|
doc = re.sub(r"%([^%]\w*)", lambda match: constants[match.group(1)], doc)
|
|
|
|
# #NMSettingWired:mac-address -> "mac-address"
|
|
doc = re.sub(r"#[A-Za-z0-9_]*:([A-Za-z0-9_-]*)", r'"\1"', doc)
|
|
|
|
# #NMSettingWired setting -> "802-3-ethernet" setting
|
|
doc = re.sub(
|
|
r"#([A-Z]\w*) setting",
|
|
lambda match: setting_names[match.group(1)] + " setting",
|
|
doc,
|
|
)
|
|
|
|
# remaining gtk-doc cleanup
|
|
doc = doc.replace("%%", "%")
|
|
doc = doc.replace("<!-- -->", "")
|
|
doc = re.sub(r" Element-.ype:.*", "", doc)
|
|
doc = re.sub(r"#([A-Z]\w*)", r"\1", doc)
|
|
|
|
# Remove sentences that refer to functions
|
|
if "FindProxyForURL()" in doc:
|
|
# FIXME: this would break the description for "proxy.pac-script"
|
|
# Work around. But really the entire approach here is flawed
|
|
# and needs improvement.
|
|
pass
|
|
else:
|
|
doc = re.sub(r"\.\s+[^.]*\w\(\)[^.]*\.", r".", doc)
|
|
|
|
return doc
|
|
|
|
|
|
def get_docs(propxml):
|
|
doc_xml = propxml.find("gi:doc", ns_map)
|
|
if doc_xml is None:
|
|
return None
|
|
else:
|
|
return format_docs(doc_xml)
|
|
|
|
|
|
def get_default_value(setting, pspec, propxml):
|
|
default_value = setting.get_property(pspec.name.replace("-", "_"))
|
|
if default_value is None:
|
|
return default_value
|
|
|
|
value_type = get_prop_type(setting, pspec)
|
|
if value_type == "string" and default_value != "" and pspec.name != "name":
|
|
default_value = '"%s"' % default_value
|
|
elif value_type == "boolean":
|
|
default_value = str(default_value).upper()
|
|
elif value_type == "byte array":
|
|
default_value = "[]"
|
|
elif str(default_value).startswith("<"):
|
|
default_value = None
|
|
elif str(default_value).startswith("["):
|
|
default_value = None
|
|
|
|
return default_value
|
|
|
|
|
|
def settings_sort_key(x):
|
|
x_prefix = x.attrib["{%s}symbol-prefix" % ns_map["c"]]
|
|
# always sort NMSettingConnection first
|
|
return (x_prefix != "setting_connection", x_prefix)
|
|
|
|
|
|
def create_desc_docbook(desc_docbook, description):
|
|
lines = re.split("\n", description)
|
|
|
|
paragraph = ET.SubElement(
|
|
desc_docbook,
|
|
"para",
|
|
)
|
|
|
|
for l in lines:
|
|
if not l:
|
|
# A blank line. This starts a new paragraph
|
|
paragraph = ET.SubElement(desc_docbook, "para")
|
|
continue
|
|
paragraph.text = l
|
|
|
|
|
|
def main(gir_path_str, output_path_str):
|
|
girxml = ET.parse(gir_path_str).getroot()
|
|
|
|
basexml = girxml.find('./gi:namespace/gi:class[@name="Setting"]', ns_map)
|
|
settings = girxml.findall('./gi:namespace/gi:class[@parent="Setting"]', ns_map)
|
|
# Hack. Need a better way to do this
|
|
ipxml = girxml.find('./gi:namespace/gi:class[@name="SettingIPConfig"]', ns_map)
|
|
settings.extend(
|
|
girxml.findall('./gi:namespace/gi:class[@parent="SettingIPConfig"]', ns_map)
|
|
)
|
|
settings = sorted(settings, key=settings_sort_key)
|
|
|
|
init_constants(girxml, settings)
|
|
|
|
nm_settings_docs_element = ET.Element("nm-setting-docs")
|
|
docs_gir = ET.ElementTree(nm_settings_docs_element)
|
|
|
|
for settingxml in settings:
|
|
if "abstract" in settingxml.attrib:
|
|
continue
|
|
|
|
new_func = NM.__getattr__(settingxml.attrib["name"])
|
|
setting = new_func()
|
|
|
|
class_desc = get_docs(settingxml)
|
|
if class_desc is None:
|
|
raise Exception(
|
|
"%s needs a gtk-doc block with one-line description"
|
|
% setting.props.name
|
|
)
|
|
setting_element = ET.SubElement(
|
|
nm_settings_docs_element,
|
|
"setting",
|
|
attrib={
|
|
"name": setting.props.name,
|
|
"description": class_desc,
|
|
"name_upper": get_setting_name_define(settingxml),
|
|
},
|
|
)
|
|
|
|
setting_properties = {
|
|
prop.name: prop
|
|
for prop in GObject.list_properties(setting)
|
|
if prop.name != "name"
|
|
}
|
|
|
|
for prop in sorted(setting_properties):
|
|
pspec = setting_properties[prop]
|
|
|
|
propxml = settingxml.find('./gi:property[@name="%s"]' % pspec.name, ns_map)
|
|
if propxml is None:
|
|
propxml = basexml.find('./gi:property[@name="%s"]' % pspec.name, ns_map)
|
|
if propxml is None:
|
|
propxml = ipxml.find('./gi:property[@name="%s"]' % pspec.name, ns_map)
|
|
|
|
value_type = get_prop_type(setting, pspec)
|
|
value_desc = get_docs(propxml)
|
|
default_value = get_default_value(setting, pspec, propxml)
|
|
|
|
if "deprecated" in propxml.attrib:
|
|
deprecated = True
|
|
deprecated_since = propxml.attrib["deprecated-version"]
|
|
deprecated_desc = format_docs(propxml.find("gi:doc-deprecated", ns_map))
|
|
else:
|
|
deprecated = False
|
|
|
|
prop_upper = prop.upper().replace("-", "_")
|
|
|
|
if value_desc is None:
|
|
raise Exception(
|
|
"%s.%s needs a documentation description"
|
|
% (setting.props.name, prop)
|
|
)
|
|
|
|
property_attributes = {
|
|
"name": prop,
|
|
"name_upper": prop_upper,
|
|
"type": value_type,
|
|
}
|
|
|
|
if default_value is not None:
|
|
property_attributes["default"] = str(default_value)
|
|
|
|
property_element = ET.SubElement(
|
|
setting_element,
|
|
"property",
|
|
attrib=property_attributes,
|
|
)
|
|
|
|
ET.SubElement(
|
|
property_element,
|
|
"description",
|
|
).text = value_desc
|
|
|
|
if value_desc:
|
|
description_docbook = ET.SubElement(
|
|
property_element,
|
|
"description-docbook",
|
|
)
|
|
|
|
create_desc_docbook(description_docbook, value_desc)
|
|
|
|
if deprecated:
|
|
ET.SubElement(
|
|
property_element,
|
|
"deprecated",
|
|
attrib={
|
|
"since": deprecated_since,
|
|
},
|
|
).text = deprecated_desc
|
|
|
|
# The text should only be one line. Otherwise, our simple "<deprecated>" element
|
|
# cannot be rendered nicely.
|
|
assert re.split("\n", deprecated_desc) == [deprecated_desc]
|
|
|
|
docs_gir.write(
|
|
output_path_str,
|
|
xml_declaration=True,
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument(
|
|
"-l",
|
|
"--lib-path",
|
|
metavar="PATH",
|
|
action="append",
|
|
help="path to scan for shared libraries",
|
|
)
|
|
parser.add_argument(
|
|
"-g",
|
|
"--gir",
|
|
metavar="FILE",
|
|
help="NM-1.0.gir file",
|
|
required=True,
|
|
)
|
|
parser.add_argument(
|
|
"-o",
|
|
"--output",
|
|
metavar="FILE",
|
|
help="output file",
|
|
required=True,
|
|
)
|
|
|
|
args = parser.parse_args()
|
|
|
|
if args.lib_path:
|
|
for lib in args.lib_path:
|
|
GIRepository.Repository.prepend_library_path(lib)
|
|
|
|
main(args.gir, args.output)
|