blob: 8fea0b8a0071113090dad09cfd362125c3d6d987 [file] [log] [blame]
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +02001#!/usr/bin/env python3
Yu Watanabedb9ecf02020-11-09 13:23:58 +09002# SPDX-License-Identifier: LGPL-2.1-or-later
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +02003
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 subprocess
9import io
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020010
Zbigniew Jędrzejewski-Szmek8aaf6112020-09-18 18:51:42 +020011try:
12 from lxml import etree
13except ModuleNotFoundError as e:
14 etree = e
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020015
Zbigniew Jędrzejewski-Szmek198fda42020-09-20 13:15:44 +020016try:
17 from shlex import join as shlex_join
18except ImportError as e:
19 shlex_join = e
20
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020021class NoCommand(Exception):
22 pass
23
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020024BORING_INTERFACES = [
25 'org.freedesktop.DBus.Peer',
26 'org.freedesktop.DBus.Introspectable',
27 'org.freedesktop.DBus.Properties',
28]
29
Zbigniew Jędrzejewski-Szmek8aaf6112020-09-18 18:51:42 +020030def xml_parser():
31 return etree.XMLParser(no_network=True,
32 remove_comments=False,
33 strip_cdata=False,
34 resolve_entities=False)
35
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020036def print_method(declarations, elem, *, prefix, file, is_signal=False):
37 name = elem.get('name')
38 klass = 'signal' if is_signal else 'method'
39 declarations[klass].append(name)
40
41 print(f'''{prefix}{name}(''', file=file, end='')
42 lead = ',\n' + prefix + ' ' * len(name) + ' '
43
44 for num, arg in enumerate(elem.findall('./arg')):
45 argname = arg.get('name')
46
47 if argname is None:
Zbigniew Jędrzejewski-Szmek04aa6fa2020-08-27 20:15:30 +020048 if opts.print_errors:
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020049 print(f'method {name}: argument {num+1} has no name', file=sys.stderr)
50 argname = 'UNNAMED'
51
52 type = arg.get('type')
53 if not is_signal:
54 direction = arg.get('direction')
55 print(f'''{lead if num > 0 else ''}{direction:3} {type} {argname}''', file=file, end='')
56 else:
57 print(f'''{lead if num > 0 else ''}{type} {argname}''', file=file, end='')
58
59 print(f');', file=file)
60
61ACCESS_MAP = {
62 'read' : 'readonly',
63 'write' : 'readwrite',
64}
65
66def value_ellipsis(type):
67 if type == 's':
68 return "'...'";
69 if type[0] == 'a':
70 inner = value_ellipsis(type[1:])
71 return f"[{inner}{', ...' if inner != '...' else ''}]";
72 return '...'
73
74def print_property(declarations, elem, *, prefix, file):
75 name = elem.get('name')
76 type = elem.get('type')
77 access = elem.get('access')
78
79 declarations['property'].append(name)
80
81 # @org.freedesktop.DBus.Property.EmitsChangedSignal("false")
82 # @org.freedesktop.systemd1.Privileged("true")
83 # readwrite b EnableWallMessages = false;
84
85 for anno in elem.findall('./annotation'):
86 anno_name = anno.get('name')
87 anno_value = anno.get('value')
88 print(f'''{prefix}@{anno_name}("{anno_value}")''', file=file)
89
90 access = ACCESS_MAP.get(access, access)
91 print(f'''{prefix}{access} {type} {name} = {value_ellipsis(type)};''', file=file)
92
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020093def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020094 name = iface.get('name')
95
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +020096 is_boring = (name in BORING_INTERFACES or
97 only_interface is not None and name != only_interface)
98
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +020099 if is_boring and print_boring:
100 print(f'''{prefix}interface {name} {{ ... }};''', file=file)
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200101
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200102 elif not is_boring and not print_boring:
103 print(f'''{prefix}interface {name} {{''', file=file)
104 prefix2 = prefix + ' '
105
106 for num, elem in enumerate(iface.findall('./method')):
107 if num == 0:
108 print(f'''{prefix2}methods:''', file=file)
109 print_method(declarations, elem, prefix=prefix2 + ' ', file=file)
110
111 for num, elem in enumerate(iface.findall('./signal')):
112 if num == 0:
113 print(f'''{prefix2}signals:''', file=file)
114 print_method(declarations, elem, prefix=prefix2 + ' ', file=file, is_signal=True)
115
116 for num, elem in enumerate(iface.findall('./property')):
117 if num == 0:
118 print(f'''{prefix2}properties:''', file=file)
119 print_property(declarations, elem, prefix=prefix2 + ' ', file=file)
120
121 print(f'''{prefix}}};''', file=file)
122
123def document_has_elem_with_text(document, elem, item_repr):
124 predicate = f".//{elem}" # [text() = 'foo'] doesn't seem supported :(
125 for loc in document.findall(predicate):
126 if loc.text == item_repr:
127 return True
Yu Watanabe45752a22020-11-20 19:47:11 +0900128 return False
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200129
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200130def check_documented(document, declarations, stats):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200131 missing = []
132 for klass, items in declarations.items():
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200133 stats['total'] += len(items)
134
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200135 for item in items:
136 if klass == 'method':
137 elem = 'function'
138 item_repr = f'{item}()'
139 elif klass == 'signal':
140 elem = 'function'
141 item_repr = item
142 elif klass == 'property':
143 elem = 'varname'
144 item_repr = item
145 else:
146 assert False, (klass, item)
147
148 if not document_has_elem_with_text(document, elem, item_repr):
Zbigniew Jędrzejewski-Szmek04aa6fa2020-08-27 20:15:30 +0200149 if opts.print_errors:
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200150 print(f'{klass} {item} is not documented :(')
151 missing.append((klass, item))
152
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200153 stats['missing'] += len(missing)
154
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200155 return missing
156
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200157def xml_to_text(destination, xml, *, only_interface=None):
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200158 file = io.StringIO()
159
160 declarations = collections.defaultdict(list)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200161 interfaces = []
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200162
163 print(f'''node {destination} {{''', file=file)
164
165 for print_boring in [False, True]:
166 for iface in xml.findall('./interface'):
167 print_interface(iface, prefix=' ', file=file,
168 print_boring=print_boring,
Zbigniew Jędrzejewski-Szmek08fe1b62020-04-10 14:46:44 +0200169 only_interface=only_interface,
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200170 declarations=declarations)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200171 name = iface.get('name')
172 if not name in BORING_INTERFACES:
173 interfaces.append(name)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200174
175 print(f'''}};''', file=file)
176
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200177 return file.getvalue(), declarations, interfaces
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200178
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200179def subst_output(document, programlisting, stats):
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200180 executable = programlisting.get('executable', None)
181 if executable is None:
182 # Not our thing
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200183 return
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200184 executable = programlisting.get('executable')
185 node = programlisting.get('node')
186 interface = programlisting.get('interface')
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200187
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200188 argv = [f'{opts.build_dir}/{executable}', f'--bus-introspect={interface}']
Zbigniew Jędrzejewski-Szmek198fda42020-09-20 13:15:44 +0200189 print(f'COMMAND: {shlex_join(argv)}')
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200190
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200191 try:
192 out = subprocess.check_output(argv, text=True)
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200193 except FileNotFoundError:
194 print(f'{executable} not found, ignoring', file=sys.stderr)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200195 return
196
Zbigniew Jędrzejewski-Szmek8aaf6112020-09-18 18:51:42 +0200197 xml = etree.fromstring(out, parser=xml_parser())
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200198
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200199 new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface)
200 programlisting.text = '\n' + new_text + ' '
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200201
202 if declarations:
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200203 missing = check_documented(document, declarations, stats)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200204 parent = programlisting.getparent()
205
206 # delete old comments
207 for child in parent:
208 if (child.tag == etree.Comment
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200209 and 'Autogenerated' in child.text):
210 parent.remove(child)
211 if (child.tag == etree.Comment
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200212 and 'not documented' in child.text):
213 parent.remove(child)
Jérémy Rosenf92c8d12020-04-18 20:19:50 +0200214 if (child.tag == "variablelist"
215 and child.attrib.get("generated",False) == "True"):
216 parent.remove(child)
217
218 # insert pointer for systemd-directives generation
219 the_tail = programlisting.tail #tail is erased by addnext, so save it here.
220 prev_element = etree.Comment("Autogenerated cross-references for systemd.directives, do not edit")
221 programlisting.addnext(prev_element)
222 programlisting.tail = the_tail
223
224 for interface in interfaces:
225 variablelist = etree.Element("variablelist")
226 variablelist.attrib['class'] = 'dbus-interface'
227 variablelist.attrib['generated'] = 'True'
228 variablelist.attrib['extra-ref'] = interface
229
230 prev_element.addnext(variablelist)
231 prev_element.tail = the_tail
232 prev_element = variablelist
233
234 for decl_type,decl_list in declarations.items():
235 for declaration in decl_list:
236 variablelist = etree.Element("variablelist")
237 variablelist.attrib['class'] = 'dbus-'+decl_type
238 variablelist.attrib['generated'] = 'True'
239 if decl_type == 'method' :
240 variablelist.attrib['extra-ref'] = declaration + '()'
241 else:
242 variablelist.attrib['extra-ref'] = declaration
243
244 prev_element.addnext(variablelist)
245 prev_element.tail = the_tail
246 prev_element = variablelist
247
248 last_element = etree.Comment("End of Autogenerated section")
249 prev_element.addnext(last_element)
250 prev_element.tail = the_tail
251 last_element.tail = the_tail
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200252
253 # insert comments for undocumented items
254 for item in reversed(missing):
255 comment = etree.Comment(f'{item[0]} {item[1]} is not documented!')
256 comment.tail = programlisting.tail
257 parent.insert(parent.index(programlisting) + 1, comment)
258
259def process(page):
260 src = open(page).read()
Zbigniew Jędrzejewski-Szmek8aaf6112020-09-18 18:51:42 +0200261 xml = etree.fromstring(src, parser=xml_parser())
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200262
263 # print('parsing {}'.format(name), file=sys.stderr)
264 if xml.tag != 'refentry':
265 return
266
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200267 stats = collections.Counter()
268
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200269 pls = xml.findall('.//programlisting')
270 for pl in pls:
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200271 subst_output(xml, pl, stats)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200272
273 out_text = etree.tostring(xml, encoding='unicode')
Frantisek Sumsal86b52a32020-04-21 20:46:53 +0200274 # massage format to avoid some lxml whitespace handling idiosyncrasies
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200275 # https://bugs.launchpad.net/lxml/+bug/526799
276 out_text = (src[:src.find('<refentryinfo')] +
277 out_text[out_text.find('<refentryinfo'):] +
278 '\n')
279
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200280 if not opts.test:
281 with open(page, 'w') as out:
282 out.write(out_text)
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200283
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200284 return dict(stats=stats, outdated=(out_text != src))
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200285
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200286def parse_args():
287 p = argparse.ArgumentParser()
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200288 p.add_argument('--test', action='store_true',
289 help='only verify that everything is up2date')
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200290 p.add_argument('--build-dir', default='build')
291 p.add_argument('pages', nargs='+')
Zbigniew Jędrzejewski-Szmek04aa6fa2020-08-27 20:15:30 +0200292 opts = p.parse_args()
293 opts.print_errors = not opts.test
294 return opts
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200295
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200296if __name__ == '__main__':
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200297 opts = parse_args()
Zbigniew Jędrzejewski-Szmeke5dd26c2020-04-07 16:58:58 +0200298
Zbigniew Jędrzejewski-Szmek198fda42020-09-20 13:15:44 +0200299 for item in (etree, shlex_join):
300 if isinstance(item, Exception):
301 print(item, file=sys.stderr)
302 exit(77 if opts.test else 1)
Zbigniew Jędrzejewski-Szmek8aaf6112020-09-18 18:51:42 +0200303
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200304 if not os.path.exists(f'{opts.build_dir}/systemd'):
305 exit(f"{opts.build_dir}/systemd doesn't exist. Use --build-dir=.")
Zbigniew Jędrzejewski-Szmekc351d562020-04-24 12:09:07 +0200306
Zbigniew Jędrzejewski-Szmek0f5cea02020-08-27 19:27:18 +0200307 stats = {page.split('/')[-1] : process(page) for page in opts.pages}
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200308
309 # Let's print all statistics at the end
310 mlen = max(len(page) for page in stats)
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200311 total = sum((item['stats'] for item in stats.values()), start=collections.Counter())
312 total = 'total', dict(stats=total, outdated=False)
313 outdated = []
314 for page, info in sorted(stats.items()) + [total]:
315 m = info['stats']['missing']
316 t = info['stats']['total']
Zbigniew Jędrzejewski-Szmekaf4c7dc2020-08-27 19:21:21 +0200317 p = page + ':'
Zbigniew Jędrzejewski-Szmek1b584f32020-08-27 19:55:55 +0200318 c = 'OUTDATED' if info['outdated'] else ''
319 if c:
320 outdated.append(page)
321 print(f'{p:{mlen + 1}} {t - m}/{t} {c}')
322
323 if opts.test and outdated:
Zbigniew Jędrzejewski-Szmekc91e3112020-08-27 20:18:05 +0200324 exit(f'Outdated pages: {", ".join(outdated)}\n'
325 f'Hint: ninja -C {opts.build_dir} man/update-dbus-docs')