blob: 8aa3742f60a5e2e00c3a8f3b10acc2dc3cc27673 [file] [log] [blame]
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +02001#!/usr/bin/env python3
2# SPDX-License-Identifier: LGPL-2.1+
3
4import collections
5import sys
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +02006import os
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +02007import shlex
8import subprocess
9import io
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020010from lxml import etree
11
12PARSER = etree.XMLParser(no_network=True,
13 remove_comments=False,
14 strip_cdata=False,
15 resolve_entities=False)
16
17PRINT_ERRORS = True
18
19class NoCommand(Exception):
20 pass
21
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020022BORING_INTERFACES = [
23 'org.freedesktop.DBus.Peer',
24 'org.freedesktop.DBus.Introspectable',
25 'org.freedesktop.DBus.Properties',
26]
27
28def print_method(declarations, elem, *, prefix, file, is_signal=False):
29 name = elem.get('name')
30 klass = 'signal' if is_signal else 'method'
31 declarations[klass].append(name)
32
33 print(f'''{prefix}{name}(''', file=file, end='')
34 lead = ',\n' + prefix + ' ' * len(name) + ' '
35
36 for num, arg in enumerate(elem.findall('./arg')):
37 argname = arg.get('name')
38
39 if argname is None:
40 if PRINT_ERRORS:
41 print(f'method {name}: argument {num+1} has no name', file=sys.stderr)
42 argname = 'UNNAMED'
43
44 type = arg.get('type')
45 if not is_signal:
46 direction = arg.get('direction')
47 print(f'''{lead if num > 0 else ''}{direction:3} {type} {argname}''', file=file, end='')
48 else:
49 print(f'''{lead if num > 0 else ''}{type} {argname}''', file=file, end='')
50
51 print(f');', file=file)
52
53ACCESS_MAP = {
54 'read' : 'readonly',
55 'write' : 'readwrite',
56}
57
58def value_ellipsis(type):
59 if type == 's':
60 return "'...'";
61 if type[0] == 'a':
62 inner = value_ellipsis(type[1:])
63 return f"[{inner}{', ...' if inner != '...' else ''}]";
64 return '...'
65
66def print_property(declarations, elem, *, prefix, file):
67 name = elem.get('name')
68 type = elem.get('type')
69 access = elem.get('access')
70
71 declarations['property'].append(name)
72
73 # @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
74 # @org.freedesktop.systemd1.Privileged("true")
75 # readwrite b EnableWallMessages = false;
76
77 for anno in elem.findall('./annotation'):
78 anno_name = anno.get('name')
79 anno_value = anno.get('value')
80 print(f'''{prefix}@{anno_name}("{anno_value}")''', file=file)
81
82 access = ACCESS_MAP.get(access, access)
83 print(f'''{prefix}{access} {type} {name} = {value_ellipsis(type)};''', file=file)
84
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020085def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020086 name = iface.get('name')
87
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020088 is_boring = (name in BORING_INTERFACES or
89 only_interface is not None and name != only_interface)
90
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020091 if is_boring and print_boring:
92 print(f'''{prefix}interface {name} {{ ... }};''', file=file)
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020093
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020094 elif not is_boring and not print_boring:
95 print(f'''{prefix}interface {name} {{''', file=file)
96 prefix2 = prefix + ' '
97
98 for num, elem in enumerate(iface.findall('./method')):
99 if num == 0:
100 print(f'''{prefix2}methods:''', file=file)
101 print_method(declarations, elem, prefix=prefix2 + ' ', file=file)
102
103 for num, elem in enumerate(iface.findall('./signal')):
104 if num == 0:
105 print(f'''{prefix2}signals:''', file=file)
106 print_method(declarations, elem, prefix=prefix2 + ' ', file=file, is_signal=True)
107
108 for num, elem in enumerate(iface.findall('./property')):
109 if num == 0:
110 print(f'''{prefix2}properties:''', file=file)
111 print_property(declarations, elem, prefix=prefix2 + ' ', file=file)
112
113 print(f'''{prefix}}};''', file=file)
114
115def document_has_elem_with_text(document, elem, item_repr):
116 predicate = f".//{elem}" # [text() = 'foo'] doesn't seem supported :(
117 for loc in document.findall(predicate):
118 if loc.text == item_repr:
119 return True
120 else:
121 return False
122
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200123def check_documented(document, declarations, stats):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200124 missing = []
125 for klass, items in declarations.items():
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200126 stats['total'] += len(items)
127
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200128 for item in items:
129 if klass == 'method':
130 elem = 'function'
131 item_repr = f'{item}()'
132 elif klass == 'signal':
133 elem = 'function'
134 item_repr = item
135 elif klass == 'property':
136 elem = 'varname'
137 item_repr = item
138 else:
139 assert False, (klass, item)
140
141 if not document_has_elem_with_text(document, elem, item_repr):
142 if PRINT_ERRORS:
143 print(f'{klass} {item} is not documented :(')
144 missing.append((klass, item))
145
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200146 stats['missing'] += len(missing)
147
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200148 return missing
149
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200150def xml_to_text(destination, xml, *, only_interface=None):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200151 file = io.StringIO()
152
153 declarations = collections.defaultdict(list)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200154 interfaces = []
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200155
156 print(f'''node {destination} {{''', file=file)
157
158 for print_boring in [False, True]:
159 for iface in xml.findall('./interface'):
160 print_interface(iface, prefix=' ', file=file,
161 print_boring=print_boring,
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200162 only_interface=only_interface,
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200163 declarations=declarations)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200164 name = iface.get('name')
165 if not name in BORING_INTERFACES:
166 interfaces.append(name)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200167
168 print(f'''}};''', file=file)
169
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200170 return file.getvalue(), declarations, interfaces
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200171
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200172def subst_output(document, programlisting, stats):
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200173 executable = programlisting.get('executable', None)
174 if executable is None:
175 # Not our thing
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200176 return
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200177 executable = programlisting.get('executable')
178 node = programlisting.get('node')
179 interface = programlisting.get('interface')
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200180
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200181 argv = [f'{build_dir}/{executable}', f'--bus-introspect={interface}']
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200182 print(f'COMMAND: {shlex.join(argv)}')
183
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200184 try:
185 out = subprocess.check_output(argv, text=True)
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200186 except FileNotFoundError:
187 print(f'{executable} not found, ignoring', file=sys.stderr)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200188 return
189
190 xml = etree.fromstring(out, parser=PARSER)
191
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200192 new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface)
193 programlisting.text = '\n' + new_text + ' '
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200194
195 if declarations:
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200196 missing = check_documented(document, declarations, stats)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200197 parent = programlisting.getparent()
198
199 # delete old comments
200 for child in parent:
201 if (child.tag == etree.Comment
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200202 and 'Autogenerated' in child.text):
203 parent.remove(child)
204 if (child.tag == etree.Comment
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200205 and 'not documented' in child.text):
206 parent.remove(child)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200207 if (child.tag == "variablelist"
208 and child.attrib.get("generated",False) == "True"):
209 parent.remove(child)
210
211 # insert pointer for systemd-directives generation
212 the_tail = programlisting.tail #tail is erased by addnext, so save it here.
213 prev_element = etree.Comment("Autogenerated cross-references for systemd.directives, do not edit")
214 programlisting.addnext(prev_element)
215 programlisting.tail = the_tail
216
217 for interface in interfaces:
218 variablelist = etree.Element("variablelist")
219 variablelist.attrib['class'] = 'dbus-interface'
220 variablelist.attrib['generated'] = 'True'
221 variablelist.attrib['extra-ref'] = interface
222
223 prev_element.addnext(variablelist)
224 prev_element.tail = the_tail
225 prev_element = variablelist
226
227 for decl_type,decl_list in declarations.items():
228 for declaration in decl_list:
229 variablelist = etree.Element("variablelist")
230 variablelist.attrib['class'] = 'dbus-'+decl_type
231 variablelist.attrib['generated'] = 'True'
232 if decl_type == 'method' :
233 variablelist.attrib['extra-ref'] = declaration + '()'
234 else:
235 variablelist.attrib['extra-ref'] = declaration
236
237 prev_element.addnext(variablelist)
238 prev_element.tail = the_tail
239 prev_element = variablelist
240
241 last_element = etree.Comment("End of Autogenerated section")
242 prev_element.addnext(last_element)
243 prev_element.tail = the_tail
244 last_element.tail = the_tail
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200245
246 # insert comments for undocumented items
247 for item in reversed(missing):
248 comment = etree.Comment(f'{item[0]} {item[1]} is not documented!')
249 comment.tail = programlisting.tail
250 parent.insert(parent.index(programlisting) + 1, comment)
251
252def process(page):
253 src = open(page).read()
254 xml = etree.fromstring(src, parser=PARSER)
255
256 # print('parsing {}'.format(name), file=sys.stderr)
257 if xml.tag != 'refentry':
258 return
259
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200260 stats = collections.Counter()
261
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200262 pls = xml.findall('.//programlisting')
263 for pl in pls:
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200264 subst_output(xml, pl, stats)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200265
266 out_text = etree.tostring(xml, encoding='unicode')
Frantisek Sumsal86b52a32020-04-21 20:46:53 +0200267 # massage format to avoid some lxml whitespace handling idiosyncrasies
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200268 # https://bugs.launchpad.net/lxml/+bug/526799
269 out_text = (src[:src.find('<refentryinfo')] +
270 out_text[out_text.find('<refentryinfo'):] +
271 '\n')
272
273 with open(page, 'w') as out:
274 out.write(out_text)
275
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200276 return stats
277
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200278if __name__ == '__main__':
279 pages = sys.argv[1:]
280
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200281 if pages[0].startswith('--build-dir='):
282 build_dir = pages[0].partition('=')[2]
283 pages = pages[1:]
284 else:
285 build_dir = 'build'
286
287 if not os.path.exists(f'{build_dir}/systemd'):
288 exit(f"{build_dir}/systemd doesn't exist. Use --build-dir=.")
289
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200290 stats = {page.split('/')[-1] : process(page) for page in pages}
291
292 # Let's print all statistics at the end
293 mlen = max(len(page) for page in stats)
294 total = 'total', sum(stats.values(), start=collections.Counter())
295 for page, counts in sorted(stats.items()) + [total]:
296 m = counts['missing']
297 t = counts['total']
298 p = page + ':'
299 print(f'{p:{mlen + 1}} {t - m}/{t}')