]>
Commit | Line | Data |
---|---|---|
e5dd26cc ZJS |
1 | #!/usr/bin/env python3 |
2 | # SPDX-License-Identifier: LGPL-2.1+ | |
3 | ||
4 | import collections | |
5 | import sys | |
c351d568 | 6 | import os |
e5dd26cc ZJS |
7 | import shlex |
8 | import subprocess | |
9 | import io | |
e5dd26cc ZJS |
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 | ||
e5dd26cc ZJS |
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 | ||
08fe1b6c | 85 | def print_interface(iface, *, prefix, file, print_boring, only_interface, declarations): |
e5dd26cc ZJS |
86 | name = iface.get('name') |
87 | ||
08fe1b6c ZJS |
88 | is_boring = (name in BORING_INTERFACES or |
89 | only_interface is not None and name != only_interface) | |
90 | ||
e5dd26cc ZJS |
91 | if is_boring and print_boring: |
92 | print(f'''{prefix}interface {name} {{ ... }};''', file=file) | |
08fe1b6c | 93 | |
e5dd26cc ZJS |
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 | ||
08fe1b6c | 146 | def xml_to_text(destination, xml, *, only_interface=None): |
e5dd26cc ZJS |
147 | file = io.StringIO() |
148 | ||
149 | declarations = collections.defaultdict(list) | |
f92c8d1c | 150 | interfaces = [] |
e5dd26cc ZJS |
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, | |
08fe1b6c | 158 | only_interface=only_interface, |
e5dd26cc | 159 | declarations=declarations) |
f92c8d1c JR |
160 | name = iface.get('name') |
161 | if not name in BORING_INTERFACES: | |
162 | interfaces.append(name) | |
e5dd26cc ZJS |
163 | |
164 | print(f'''}};''', file=file) | |
165 | ||
f92c8d1c | 166 | return file.getvalue(), declarations, interfaces |
e5dd26cc ZJS |
167 | |
168 | def subst_output(document, programlisting): | |
c351d568 ZJS |
169 | executable = programlisting.get('executable', None) |
170 | if executable is None: | |
171 | # Not our thing | |
e5dd26cc | 172 | return |
c351d568 ZJS |
173 | executable = programlisting.get('executable') |
174 | node = programlisting.get('node') | |
175 | interface = programlisting.get('interface') | |
e5dd26cc | 176 | |
c351d568 | 177 | argv = [f'{build_dir}/{executable}', f'--bus-introspect={interface}'] |
e5dd26cc ZJS |
178 | print(f'COMMAND: {shlex.join(argv)}') |
179 | ||
e5dd26cc ZJS |
180 | try: |
181 | out = subprocess.check_output(argv, text=True) | |
c351d568 ZJS |
182 | except FileNotFoundError: |
183 | print(f'{executable} not found, ignoring', file=sys.stderr) | |
e5dd26cc ZJS |
184 | return |
185 | ||
186 | xml = etree.fromstring(out, parser=PARSER) | |
187 | ||
c351d568 ZJS |
188 | new_text, declarations, interfaces = xml_to_text(node, xml, only_interface=interface) |
189 | programlisting.text = '\n' + new_text + ' ' | |
e5dd26cc ZJS |
190 | |
191 | if declarations: | |
192 | missing = check_documented(document, declarations) | |
193 | parent = programlisting.getparent() | |
194 | ||
195 | # delete old comments | |
196 | for child in parent: | |
f92c8d1c JR |
197 | if (child.tag == etree.Comment |
198 | and 'Autogenerated' in child.text): | |
199 | parent.remove(child) | |
e5dd26cc ZJS |
200 | if (child.tag == etree.Comment |
201 | and 'not documented' in child.text): | |
202 | parent.remove(child) | |
f92c8d1c JR |
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 | |
e5dd26cc ZJS |
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') | |
86b52a39 | 261 | # massage format to avoid some lxml whitespace handling idiosyncrasies |
e5dd26cc ZJS |
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 | ||
c351d568 ZJS |
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 | ||
e5dd26cc ZJS |
282 | for page in pages: |
283 | process(page) |