]> git.ipfire.org Git - thirdparty/systemd.git/blob - tools/update-dbus-docs.py
Merge pull request #15167 from ssahani/address-gen-mode
[thirdparty/systemd.git] / tools / update-dbus-docs.py
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: LGPL-2.1+
3
4 import collections
5 import sys
6 import os
7 import shlex
8 import subprocess
9 import io
10 from lxml import etree
11
12 PARSER = etree.XMLParser(no_network=True,
13 remove_comments=False,
14 strip_cdata=False,
15 resolve_entities=False)
16
17 PRINT_ERRORS = True
18
19 class NoCommand(Exception):
20 pass
21
22 BORING_INTERFACES = [
23 'org.freedesktop.DBus.Peer',
24 'org.freedesktop.DBus.Introspectable',
25 'org.freedesktop.DBus.Properties',
26 ]
27
28 def 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
53 ACCESS_MAP = {
54 'read' : 'readonly',
55 'write' : 'readwrite',
56 }
57
58 def 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
66 def 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
85 def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations):
86 name = iface.get('name')
87
88 is_boring = (name in BORING_INTERFACES or
89 only_interface is not None and name != only_interface)
90
91 if is_boring and print_boring:
92 print(f'''{prefix}interface {name} {{ ... }};''', file=file)
93
94 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
115 def 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
123 def check_documented(document, declarations):
124 missing = []
125 for klass, items in declarations.items():
126 for item in items:
127 if klass == 'method':
128 elem = 'function'
129 item_repr = f'{item}()'
130 elif klass == 'signal':
131 elem = 'function'
132 item_repr = item
133 elif klass == 'property':
134 elem = 'varname'
135 item_repr = item
136 else:
137 assert False, (klass, item)
138
139 if not document_has_elem_with_text(document, elem, item_repr):
140 if PRINT_ERRORS:
141 print(f'{klass} {item} is not documented :(')
142 missing.append((klass, item))
143
144 return missing
145
146 def xml_to_text(destination, xml, *, only_interface=None):
147 file = io.StringIO()
148
149 declarations = collections.defaultdict(list)
150 interfaces = []
151
152 print(f'''node {destination} {{''', file=file)
153
154 for print_boring in [False, True]:
155 for iface in xml.findall('./interface'):
156 print_interface(iface, prefix=' ', file=file,
157 print_boring=print_boring,
158 only_interface=only_interface,
159 declarations=declarations)
160 name = iface.get('name')
161 if not name in BORING_INTERFACES:
162 interfaces.append(name)
163
164 print(f'''}};''', file=file)
165
166 return file.getvalue(), declarations, interfaces
167
168 def subst_output(document, programlisting):
169 executable = programlisting.get('executable', None)
170 if executable is None:
171 # Not our thing
172 return
173 executable = programlisting.get('executable')
174 node = programlisting.get('node')
175 interface = programlisting.get('interface')
176
177 argv = [f'{build_dir}/{executable}', f'--bus-introspect={interface}']
178 print(f'COMMAND: {shlex.join(argv)}')
179
180 try:
181 out = subprocess.check_output(argv, text=True)
182 except FileNotFoundError:
183 print(f'{executable} not found, ignoring', file=sys.stderr)
184 return
185
186 xml = etree.fromstring(out, parser=PARSER)
187
188 new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface)
189 programlisting.text = '\n' + new_text + ' '
190
191 if declarations:
192 missing = check_documented(document, declarations)
193 parent = programlisting.getparent()
194
195 # delete old comments
196 for child in parent:
197 if (child.tag == etree.Comment
198 and 'Autogenerated' in child.text):
199 parent.remove(child)
200 if (child.tag == etree.Comment
201 and 'not documented' in child.text):
202 parent.remove(child)
203 if (child.tag == "variablelist"
204 and child.attrib.get("generated",False) == "True"):
205 parent.remove(child)
206
207 # insert pointer for systemd-directives generation
208 the_tail = programlisting.tail #tail is erased by addnext, so save it here.
209 prev_element = etree.Comment("Autogenerated cross-references for systemd.directives, do not edit")
210 programlisting.addnext(prev_element)
211 programlisting.tail = the_tail
212
213 for interface in interfaces:
214 variablelist = etree.Element("variablelist")
215 variablelist.attrib['class'] = 'dbus-interface'
216 variablelist.attrib['generated'] = 'True'
217 variablelist.attrib['extra-ref'] = interface
218
219 prev_element.addnext(variablelist)
220 prev_element.tail = the_tail
221 prev_element = variablelist
222
223 for decl_type,decl_list in declarations.items():
224 for declaration in decl_list:
225 variablelist = etree.Element("variablelist")
226 variablelist.attrib['class'] = 'dbus-'+decl_type
227 variablelist.attrib['generated'] = 'True'
228 if decl_type == 'method' :
229 variablelist.attrib['extra-ref'] = declaration + '()'
230 else:
231 variablelist.attrib['extra-ref'] = declaration
232
233 prev_element.addnext(variablelist)
234 prev_element.tail = the_tail
235 prev_element = variablelist
236
237 last_element = etree.Comment("End of Autogenerated section")
238 prev_element.addnext(last_element)
239 prev_element.tail = the_tail
240 last_element.tail = the_tail
241
242 # insert comments for undocumented items
243 for item in reversed(missing):
244 comment = etree.Comment(f'{item[0]} {item[1]} is not documented!')
245 comment.tail = programlisting.tail
246 parent.insert(parent.index(programlisting) + 1, comment)
247
248 def process(page):
249 src = open(page).read()
250 xml = etree.fromstring(src, parser=PARSER)
251
252 # print('parsing {}'.format(name), file=sys.stderr)
253 if xml.tag != 'refentry':
254 return
255
256 pls = xml.findall('.//programlisting')
257 for pl in pls:
258 subst_output(xml, pl)
259
260 out_text = etree.tostring(xml, encoding='unicode')
261 # massage format to avoid some lxml whitespace handling idiosyncrasies
262 # https://bugs.launchpad.net/lxml/+bug/526799
263 out_text = (src[:src.find('<refentryinfo')] +
264 out_text[out_text.find('<refentryinfo'):] +
265 '\n')
266
267 with open(page, 'w') as out:
268 out.write(out_text)
269
270 if __name__ == '__main__':
271 pages = sys.argv[1:]
272
273 if pages[0].startswith('--build-dir='):
274 build_dir = pages[0].partition('=')[2]
275 pages = pages[1:]
276 else:
277 build_dir = 'build'
278
279 if not os.path.exists(f'{build_dir}/systemd'):
280 exit(f"{build_dir}/systemd doesn't exist. Use --build-dir=.")
281
282 for page in pages:
283 process(page)