blob: 448206cf957005902597cc06b895a95db9e5c82e [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
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +02004import argparse
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +02005import collections
6import sys
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +02007import os
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +02008import shlex
9import subprocess
10import io
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020011from lxml import etree
12
13PARSER = etree.XMLParser(no_network=True,
14 remove_comments=False,
15 strip_cdata=False,
16 resolve_entities=False)
17
18PRINT_ERRORS = True
19
20class NoCommand(Exception):
21 pass
22
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020023BORING_INTERFACES = [
24 'org.freedesktop.DBus.Peer',
25 'org.freedesktop.DBus.Introspectable',
26 'org.freedesktop.DBus.Properties',
27]
28
29def print_method(declarations, elem, *, prefix, file, is_signal=False):
30 name = elem.get('name')
31 klass = 'signal' if is_signal else 'method'
32 declarations[klass].append(name)
33
34 print(f'''{prefix}{name}(''', file=file, end='')
35 lead = ',\n' + prefix + ' ' * len(name) + ' '
36
37 for num, arg in enumerate(elem.findall('./arg')):
38 argname = arg.get('name')
39
40 if argname is None:
41 if PRINT_ERRORS:
42 print(f'method {name}: argument {num+1} has no name', file=sys.stderr)
43 argname = 'UNNAMED'
44
45 type = arg.get('type')
46 if not is_signal:
47 direction = arg.get('direction')
48 print(f'''{lead if num > 0 else ''}{direction:3} {type} {argname}''', file=file, end='')
49 else:
50 print(f'''{lead if num > 0 else ''}{type} {argname}''', file=file, end='')
51
52 print(f');', file=file)
53
54ACCESS_MAP = {
55 'read' : 'readonly',
56 'write' : 'readwrite',
57}
58
59def value_ellipsis(type):
60 if type == 's':
61 return "'...'";
62 if type[0] == 'a':
63 inner = value_ellipsis(type[1:])
64 return f"[{inner}{', ...' if inner != '...' else ''}]";
65 return '...'
66
67def print_property(declarations, elem, *, prefix, file):
68 name = elem.get('name')
69 type = elem.get('type')
70 access = elem.get('access')
71
72 declarations['property'].append(name)
73
74 # @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
75 # @org.freedesktop.systemd1.Privileged("true")
76 # readwrite b EnableWallMessages = false;
77
78 for anno in elem.findall('./annotation'):
79 anno_name = anno.get('name')
80 anno_value = anno.get('value')
81 print(f'''{prefix}@{anno_name}("{anno_value}")''', file=file)
82
83 access = ACCESS_MAP.get(access, access)
84 print(f'''{prefix}{access} {type} {name} = {value_ellipsis(type)};''', file=file)
85
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020086def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020087 name = iface.get('name')
88
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020089 is_boring = (name in BORING_INTERFACES or
90 only_interface is not None and name != only_interface)
91
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020092 if is_boring and print_boring:
93 print(f'''{prefix}interface {name} {{ ... }};''', file=file)
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020094
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020095 elif not is_boring and not print_boring:
96 print(f'''{prefix}interface {name} {{''', file=file)
97 prefix2 = prefix + ' '
98
99 for num, elem in enumerate(iface.findall('./method')):
100 if num == 0:
101 print(f'''{prefix2}methods:''', file=file)
102 print_method(declarations, elem, prefix=prefix2 + ' ', file=file)
103
104 for num, elem in enumerate(iface.findall('./signal')):
105 if num == 0:
106 print(f'''{prefix2}signals:''', file=file)
107 print_method(declarations, elem, prefix=prefix2 + ' ', file=file, is_signal=True)
108
109 for num, elem in enumerate(iface.findall('./property')):
110 if num == 0:
111 print(f'''{prefix2}properties:''', file=file)
112 print_property(declarations, elem, prefix=prefix2 + ' ', file=file)
113
114 print(f'''{prefix}}};''', file=file)
115
116def document_has_elem_with_text(document, elem, item_repr):
117 predicate = f".//{elem}" # [text() = 'foo'] doesn't seem supported :(
118 for loc in document.findall(predicate):
119 if loc.text == item_repr:
120 return True
121 else:
122 return False
123
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200124def check_documented(document, declarations, stats):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200125 missing = []
126 for klass, items in declarations.items():
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200127 stats['total'] += len(items)
128
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200129 for item in items:
130 if klass == 'method':
131 elem = 'function'
132 item_repr = f'{item}()'
133 elif klass == 'signal':
134 elem = 'function'
135 item_repr = item
136 elif klass == 'property':
137 elem = 'varname'
138 item_repr = item
139 else:
140 assert False, (klass, item)
141
142 if not document_has_elem_with_text(document, elem, item_repr):
143 if PRINT_ERRORS:
144 print(f'{klass} {item} is not documented :(')
145 missing.append((klass, item))
146
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200147 stats['missing'] += len(missing)
148
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200149 return missing
150
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200151def xml_to_text(destination, xml, *, only_interface=None):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200152 file = io.StringIO()
153
154 declarations = collections.defaultdict(list)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200155 interfaces = []
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200156
157 print(f'''node {destination} {{''', file=file)
158
159 for print_boring in [False, True]:
160 for iface in xml.findall('./interface'):
161 print_interface(iface, prefix=' ', file=file,
162 print_boring=print_boring,
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200163 only_interface=only_interface,
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200164 declarations=declarations)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200165 name = iface.get('name')
166 if not name in BORING_INTERFACES:
167 interfaces.append(name)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200168
169 print(f'''}};''', file=file)
170
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200171 return file.getvalue(), declarations, interfaces
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200172
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200173def subst_output(document, programlisting, stats):
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200174 executable = programlisting.get('executable', None)
175 if executable is None:
176 # Not our thing
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200177 return
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200178 executable = programlisting.get('executable')
179 node = programlisting.get('node')
180 interface = programlisting.get('interface')
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200181
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200182 argv = [f'{opts.build_dir}/{executable}', f'--bus-introspect={interface}']
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200183 print(f'COMMAND: {shlex.join(argv)}')
184
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200185 try:
186 out = subprocess.check_output(argv, text=True)
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200187 except FileNotFoundError:
188 print(f'{executable} not found, ignoring', file=sys.stderr)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200189 return
190
191 xml = etree.fromstring(out, parser=PARSER)
192
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200193 new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface)
194 programlisting.text = '\n' + new_text + ' '
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200195
196 if declarations:
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200197 missing = check_documented(document, declarations, stats)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200198 parent = programlisting.getparent()
199
200 # delete old comments
201 for child in parent:
202 if (child.tag == etree.Comment
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200203 and 'Autogenerated' in child.text):
204 parent.remove(child)
205 if (child.tag == etree.Comment
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200206 and 'not documented' in child.text):
207 parent.remove(child)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200208 if (child.tag == "variablelist"
209 and child.attrib.get("generated",False) == "True"):
210 parent.remove(child)
211
212 # insert pointer for systemd-directives generation
213 the_tail = programlisting.tail #tail is erased by addnext, so save it here.
214 prev_element = etree.Comment("Autogenerated cross-references for systemd.directives, do not edit")
215 programlisting.addnext(prev_element)
216 programlisting.tail = the_tail
217
218 for interface in interfaces:
219 variablelist = etree.Element("variablelist")
220 variablelist.attrib['class'] = 'dbus-interface'
221 variablelist.attrib['generated'] = 'True'
222 variablelist.attrib['extra-ref'] = interface
223
224 prev_element.addnext(variablelist)
225 prev_element.tail = the_tail
226 prev_element = variablelist
227
228 for decl_type,decl_list in declarations.items():
229 for declaration in decl_list:
230 variablelist = etree.Element("variablelist")
231 variablelist.attrib['class'] = 'dbus-'+decl_type
232 variablelist.attrib['generated'] = 'True'
233 if decl_type == 'method' :
234 variablelist.attrib['extra-ref'] = declaration + '()'
235 else:
236 variablelist.attrib['extra-ref'] = declaration
237
238 prev_element.addnext(variablelist)
239 prev_element.tail = the_tail
240 prev_element = variablelist
241
242 last_element = etree.Comment("End of Autogenerated section")
243 prev_element.addnext(last_element)
244 prev_element.tail = the_tail
245 last_element.tail = the_tail
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200246
247 # insert comments for undocumented items
248 for item in reversed(missing):
249 comment = etree.Comment(f'{item[0]} {item[1]} is not documented!')
250 comment.tail = programlisting.tail
251 parent.insert(parent.index(programlisting) + 1, comment)
252
253def process(page):
254 src = open(page).read()
255 xml = etree.fromstring(src, parser=PARSER)
256
257 # print('parsing {}'.format(name), file=sys.stderr)
258 if xml.tag != 'refentry':
259 return
260
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200261 stats = collections.Counter()
262
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200263 pls = xml.findall('.//programlisting')
264 for pl in pls:
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200265 subst_output(xml, pl, stats)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200266
267 out_text = etree.tostring(xml, encoding='unicode')
Frantisek Sumsal86b52a32020-04-21 20:46:53 +0200268 # massage format to avoid some lxml whitespace handling idiosyncrasies
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200269 # https://bugs.launchpad.net/lxml/+bug/526799
270 out_text = (src[:src.find('<refentryinfo')] +
271 out_text[out_text.find('<refentryinfo'):] +
272 '\n')
273
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200274 if not opts.test:
275 with open(page, 'w') as out:
276 out.write(out_text)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200277
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200278 return dict(stats=stats, outdated=(out_text != src))
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200279
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200280def parse_args():
281 p = argparse.ArgumentParser()
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200282 p.add_argument('--test', action='store_true',
283 help='only verify that everything is up2date')
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200284 p.add_argument('--build-dir', default='build')
285 p.add_argument('pages', nargs='+')
286 return p.parse_args()
287
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200288if __name__ == '__main__':
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200289 opts = parse_args()
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200290
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200291 if not os.path.exists(f'{opts.build_dir}/systemd'):
292 exit(f"{opts.build_dir}/systemd doesn't exist. Use --build-dir=.")
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200293
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200294 stats = {page.split('/')[-1] : process(page) for page in opts.pages}
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200295
296 # Let's print all statistics at the end
297 mlen = max(len(page) for page in stats)
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200298 total = sum((item['stats'] for item in stats.values()), start=collections.Counter())
299 total = 'total', dict(stats=total, outdated=False)
300 outdated = []
301 for page, info in sorted(stats.items()) + [total]:
302 m = info['stats']['missing']
303 t = info['stats']['total']
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200304 p = page + ':'
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200305 c = 'OUTDATED' if info['outdated'] else ''
306 if c:
307 outdated.append(page)
308 print(f'{p:{mlen + 1}} {t - m}/{t} {c}')
309
310 if opts.test and outdated:
311 exit(f'Outdated pages: {", ".join(outdated)}')