History

Merge tag 'docs-6.18' of git://git.lwn.net/linux

Pull documentation updates from Jonathan Corbet: "It has been a relatively busy cycle in docsland, with changes all over: - Bring the kernel memory-model docs into the Sphinx build in the "literal include" mode. - Lots of build-infrastructure work, further cleaning up long-term kernel-doc technical debt. The sphinx-pre-install tool has been converted to Python and updated for current systems. - A new tool to detect when documents have been moved and generate HTML redirects; this can be used on kernel.org (or any other site hosting the rendered docs) to avoid breaking links. - Automated processing of the YAML files describing the netlink protocol. - A significant update of the maintainer's PGP guide. ... and a seemingly endless series of typo fixes, build-problem fixes, etc" * tag 'docs-6.18' of git://git.lwn.net/linux: (193 commits) Documentation/features: Update feature lists for 6.17-rc7 docs: remove cdomain.py Documentation/process: submitting-patches: fix typo in "were do" docs: dev-tools/lkmm: Fix typo of missing file extension Documentation: trace: histogram: Convert ftrace docs cross-reference Documentation: trace: histogram-design: Wrap introductory note in note:: directive Documentation: trace: historgram-design: Separate sched_waking histogram section heading and the following diagram Documentation: trace: histogram-design: Trim trailing vertices in diagram explanation text Documentation: trace: histogram: Fix histogram trigger subsection number order docs: driver-api: fix spelling of "buses". Documentation: fbcon: Use admonition directives Documentation: fbcon: Reindent 8th step of attach/detach/unload Documentation: fbcon: Add boot options and attach/detach/unload section headings docs: filesystems: sysfs: add remaining top level sysfs directory descriptions docs: filesystems: sysfs: clarify symlink destinations in dev and bus/devices descriptions docs: filesystems: sysfs: remove top level sysfs net directory docs: maintainer: Fix ambiguous subheading formatting docs: kdoc: a few more dump_typedef() tweaks docs: kdoc: remove redundant comment stripping in dump_typedef() docs: kdoc: remove some dead code in dump_typedef() ...

Linus Torvalds committed 1 year ago

Commit ee2fe812 parents 50647a1 99510c3173 files · +7050 −3564Browse files

Showing the changes to scripts (5 of 173 files). Show all

Unified · Split

5 files
  1. scripts/kernel-doc.py +24 −10
  2. scripts/lib/kdoc/kdoc_parser.py +414 −434
  3. scripts/selinux/install_policy.sh +1 −1
  4. scripts/sphinx-build-wrapper +719 −0
  5. scripts/sphinx-pre-install +1617 −1052
scripts/kernel-doc.py+24−10
@@ -2,8 +2,17 @@ · 1 unmodified line
2# SPDX-License-Identifier: GPL-2.02# SPDX-License-Identifier: GPL-2.0
3# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.3# Copyright(c) 2025: Mauro Carvalho Chehab <mchehab@kernel.org>.
4#4#
5# pylint: disable=C0103,R09155# pylint: disable=C0103,R0912,R0914,R0915
6#6
7# NOTE: While kernel-doc requires at least version 3.6 to run, the
8# command line should work with Python 3.2+ (tested with 3.4).
9# The rationale is that it shall fail gracefully during Kernel
10# compilation with older Kernel versions. Due to that:
11# - encoding line is needed here;
12# - no f-strings can be used on this file.
13# - the libraries that require newer versions can only be included
14# after Python version is checked.
15
7# Converted from the kernel-doc script originally written in Perl16# Converted from the kernel-doc script originally written in Perl
8# under GPLv2, copyrighted since 1998 by the following authors:17# under GPLv2, copyrighted since 1998 by the following authors:
9#18#
@@ -107,9 +116,6 @@ SRC_DIR = os.path.dirname(os.path.realpath(__file__)) · 97 unmodified lines
107 116
108sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR))117sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR))
109 118
110from kdoc_files import KernelFiles # pylint: disable=C0413
111from kdoc_output import RestFormat, ManFormat # pylint: disable=C0413
112
113DESC = """119DESC = """
114Read C language source or header FILEs, extract embedded documentation comments,120Read C language source or header FILEs, extract embedded documentation comments,
115and print formatted documentation to standard output.121and print formatted documentation to standard output.
@@ -273,14 +279,22 @@ def main(): · 157 unmodified lines
273 279
274 python_ver = sys.version_info[:2]280 python_ver = sys.version_info[:2]
275 if python_ver < (3,6):281 if python_ver < (3,6):
276 logger.warning("Python 3.6 or later is required by kernel-doc")282 # Depending on Kernel configuration, kernel-doc --none is called at
283 # build time. As we don't want to break compilation due to the
284 # usage of an old Python version, return 0 here.
285 if args.none:
286 logger.error("Python 3.6 or later is required by kernel-doc. skipping checks")
287 sys.exit(0)
277 288
278 # Return 0 here to avoid breaking compilation289 sys.exit("Python 3.6 or later is required by kernel-doc. Aborting.")
279 sys.exit(0)
280 290
281 if python_ver < (3,7):291 if python_ver < (3,7):
282 logger.warning("Python 3.7 or later is required for correct results")292 logger.warning("Python 3.7 or later is required for correct results")
283 293
294 # Import kernel-doc libraries only after checking Python version
295 from kdoc_files import KernelFiles # pylint: disable=C0415
296 from kdoc_output import RestFormat, ManFormat # pylint: disable=C0415
297
284 if args.man:298 if args.man:
285 out_style = ManFormat(modulename=args.modulename)299 out_style = ManFormat(modulename=args.modulename)
286 elif args.none:300 elif args.none:
@@ -308,11 +322,11 @@ def main(): · 21 unmodified lines
308 sys.exit(0)322 sys.exit(0)
309 323
310 if args.werror:324 if args.werror:
311 print(f"{error_count} warnings as errors")325 print("%s warnings as errors" % error_count) # pylint: disable=C0209
312 sys.exit(error_count)326 sys.exit(error_count)
313 327
314 if args.verbose:328 if args.verbose:
315 print(f"{error_count} errors")329 print("%s errors" % error_count) # pylint: disable=C0209
316 330
317 if args.none:331 if args.none:
318 sys.exit(0)332 sys.exit(0)
scripts/lib/kdoc/kdoc_parser.py+414−434
@@ -46,7 +46,7 @@ doc_decl = doc_com + KernRe(r'(\w+)', cache=False) · 45 unmodified lines
46known_section_names = 'description|context|returns?|notes?|examples?'46known_section_names = 'description|context|returns?|notes?|examples?'
47known_sections = KernRe(known_section_names, flags = re.I)47known_sections = KernRe(known_section_names, flags = re.I)
48doc_sect = doc_com + \48doc_sect = doc_com + \
49 KernRe(r'\s*(\@[.\w]+|\@\.\.\.|' + known_section_names + r')\s*:([^:].*)?$',49 KernRe(r'\s*(@[.\w]+|@\.\.\.|' + known_section_names + r')\s*:([^:].*)?$',
50 flags=re.I, cache=False)50 flags=re.I, cache=False)
51 51
52doc_content = doc_com_body + KernRe(r'(.*)', cache=False)52doc_content = doc_com_body + KernRe(r'(.*)', cache=False)
@@ -54,13 +54,11 @@ doc_inline_start = KernRe(r'^\s*/\*\*\s*$', cache=False) · 1 unmodified line
54doc_inline_sect = KernRe(r'\s*\*\s*(@\s*[\w][\w\.]*\s*):(.*)', cache=False)54doc_inline_sect = KernRe(r'\s*\*\s*(@\s*[\w][\w\.]*\s*):(.*)', cache=False)
55doc_inline_end = KernRe(r'^\s*\*/\s*$', cache=False)55doc_inline_end = KernRe(r'^\s*\*/\s*$', cache=False)
56doc_inline_oneline = KernRe(r'^\s*/\*\*\s*(@[\w\s]+):\s*(.*)\s*\*/\s*$', cache=False)56doc_inline_oneline = KernRe(r'^\s*/\*\*\s*(@[\w\s]+):\s*(.*)\s*\*/\s*$', cache=False)
57attribute = KernRe(r"__attribute__\s*\(\([a-z0-9,_\*\s\(\)]*\)\)",
58 flags=re.I | re.S, cache=False)
59 57
60export_symbol = KernRe(r'^\s*EXPORT_SYMBOL(_GPL)?\s*\(\s*(\w+)\s*\)\s*', cache=False)58export_symbol = KernRe(r'^\s*EXPORT_SYMBOL(_GPL)?\s*\(\s*(\w+)\s*\)\s*', cache=False)
61export_symbol_ns = KernRe(r'^\s*EXPORT_SYMBOL_NS(_GPL)?\s*\(\s*(\w+)\s*,\s*"\S+"\)\s*', cache=False)59export_symbol_ns = KernRe(r'^\s*EXPORT_SYMBOL_NS(_GPL)?\s*\(\s*(\w+)\s*,\s*"\S+"\)\s*', cache=False)
62 60
63type_param = KernRe(r"\@(\w*((\.\w+)|(->\w+))*(\.\.\.)?)", cache=False)61type_param = KernRe(r"@(\w*((\.\w+)|(->\w+))*(\.\.\.)?)", cache=False)
64 62
65#63#
66# Tests for the beginning of a kerneldoc block in its various forms.64# Tests for the beginning of a kerneldoc block in its various forms.
@@ -74,6 +72,133 @@ doc_begin_func = KernRe(str(doc_com) + # initial " * ' · 7 unmodified lines
74 r'(?:[-:].*)?$', # description (not captured)72 r'(?:[-:].*)?$', # description (not captured)
75 cache = False)73 cache = False)
76 74
75#
76# Here begins a long set of transformations to turn structure member prefixes
77# and macro invocations into something we can parse and generate kdoc for.
78#
79struct_args_pattern = r'([^,)]+)'
80
81struct_xforms = [
82 # Strip attributes
83 (KernRe(r"__attribute__\s*\(\([a-z0-9,_\*\s\(\)]*\)\)", flags=re.I | re.S, cache=False), ' '),
84 (KernRe(r'\s*__aligned\s*\([^;]*\)', re.S), ' '),
85 (KernRe(r'\s*__counted_by\s*\([^;]*\)', re.S), ' '),
86 (KernRe(r'\s*__counted_by_(le|be)\s*\([^;]*\)', re.S), ' '),
87 (KernRe(r'\s*__packed\s*', re.S), ' '),
88 (KernRe(r'\s*CRYPTO_MINALIGN_ATTR', re.S), ' '),
89 (KernRe(r'\s*____cacheline_aligned_in_smp', re.S), ' '),
90 (KernRe(r'\s*____cacheline_aligned', re.S), ' '),
91 (KernRe(r'\s*__cacheline_group_(begin|end)\([^\)]+\);'), ''),
92 #
93 # Unwrap struct_group macros based on this definition:
94 # __struct_group(TAG, NAME, ATTRS, MEMBERS...)
95 # which has variants like: struct_group(NAME, MEMBERS...)
96 # Only MEMBERS arguments require documentation.
97 #
98 # Parsing them happens on two steps:
99 #
100 # 1. drop struct group arguments that aren't at MEMBERS,
101 # storing them as STRUCT_GROUP(MEMBERS)
102 #
103 # 2. remove STRUCT_GROUP() ancillary macro.
104 #
105 # The original logic used to remove STRUCT_GROUP() using an
106 # advanced regex:
107 #
108 # \bSTRUCT_GROUP(\(((?:(?>[^)(]+)|(?1))*)\))[^;]*;
109 #
110 # with two patterns that are incompatible with
111 # Python re module, as it has:
112 #
113 # - a recursive pattern: (?1)
114 # - an atomic grouping: (?>...)
115 #
116 # I tried a simpler version: but it didn't work either:
117 # \bSTRUCT_GROUP\(([^\)]+)\)[^;]*;
118 #
119 # As it doesn't properly match the end parenthesis on some cases.
120 #
121 # So, a better solution was crafted: there's now a NestedMatch
122 # class that ensures that delimiters after a search are properly
123 # matched. So, the implementation to drop STRUCT_GROUP() will be
124 # handled in separate.
125 #
126 (KernRe(r'\bstruct_group\s*\(([^,]*,)', re.S), r'STRUCT_GROUP('),
127 (KernRe(r'\bstruct_group_attr\s*\(([^,]*,){2}', re.S), r'STRUCT_GROUP('),
128 (KernRe(r'\bstruct_group_tagged\s*\(([^,]*),([^,]*),', re.S), r'struct \1 \2; STRUCT_GROUP('),
129 (KernRe(r'\b__struct_group\s*\(([^,]*,){3}', re.S), r'STRUCT_GROUP('),
130 #
131 # Replace macros
132 #
133 # TODO: use NestedMatch for FOO($1, $2, ...) matches
134 #
135 # it is better to also move those to the NestedMatch logic,
136 # to ensure that parenthesis will be properly matched.
137 #
138 (KernRe(r'__ETHTOOL_DECLARE_LINK_MODE_MASK\s*\(([^\)]+)\)', re.S),
139 r'DECLARE_BITMAP(\1, __ETHTOOL_LINK_MODE_MASK_NBITS)'),
140 (KernRe(r'DECLARE_PHY_INTERFACE_MASK\s*\(([^\)]+)\)', re.S),
141 r'DECLARE_BITMAP(\1, PHY_INTERFACE_MODE_MAX)'),
142 (KernRe(r'DECLARE_BITMAP\s*\(' + struct_args_pattern + r',\s*' + struct_args_pattern + r'\)',
143 re.S), r'unsigned long \1[BITS_TO_LONGS(\2)]'),
144 (KernRe(r'DECLARE_HASHTABLE\s*\(' + struct_args_pattern + r',\s*' + struct_args_pattern + r'\)',
145 re.S), r'unsigned long \1[1 << ((\2) - 1)]'),
146 (KernRe(r'DECLARE_KFIFO\s*\(' + struct_args_pattern + r',\s*' + struct_args_pattern +
147 r',\s*' + struct_args_pattern + r'\)', re.S), r'\2 *\1'),
148 (KernRe(r'DECLARE_KFIFO_PTR\s*\(' + struct_args_pattern + r',\s*' +
149 struct_args_pattern + r'\)', re.S), r'\2 *\1'),
150 (KernRe(r'(?:__)?DECLARE_FLEX_ARRAY\s*\(' + struct_args_pattern + r',\s*' +
151 struct_args_pattern + r'\)', re.S), r'\1 \2[]'),
152 (KernRe(r'DEFINE_DMA_UNMAP_ADDR\s*\(' + struct_args_pattern + r'\)', re.S), r'dma_addr_t \1'),
153 (KernRe(r'DEFINE_DMA_UNMAP_LEN\s*\(' + struct_args_pattern + r'\)', re.S), r'__u32 \1'),
154]
155#
156# Regexes here are guaranteed to have the end limiter matching
157# the start delimiter. Yet, right now, only one replace group
158# is allowed.
159#
160struct_nested_prefixes = [
161 (re.compile(r'\bSTRUCT_GROUP\('), r'\1'),
162]
163
164#
165# Transforms for function prototypes
166#
167function_xforms = [
168 (KernRe(r"^static +"), ""),
169 (KernRe(r"^extern +"), ""),
170 (KernRe(r"^asmlinkage +"), ""),
171 (KernRe(r"^inline +"), ""),
172 (KernRe(r"^__inline__ +"), ""),
173 (KernRe(r"^__inline +"), ""),
174 (KernRe(r"^__always_inline +"), ""),
175 (KernRe(r"^noinline +"), ""),
176 (KernRe(r"^__FORTIFY_INLINE +"), ""),
177 (KernRe(r"__init +"), ""),
178 (KernRe(r"__init_or_module +"), ""),
179 (KernRe(r"__deprecated +"), ""),
180 (KernRe(r"__flatten +"), ""),
181 (KernRe(r"__meminit +"), ""),
182 (KernRe(r"__must_check +"), ""),
183 (KernRe(r"__weak +"), ""),
184 (KernRe(r"__sched +"), ""),
185 (KernRe(r"_noprof"), ""),
186 (KernRe(r"__printf\s*\(\s*\d*\s*,\s*\d*\s*\) +"), ""),
187 (KernRe(r"__(?:re)?alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\) +"), ""),
188 (KernRe(r"__diagnose_as\s*\(\s*\S+\s*(?:,\s*\d+\s*)*\) +"), ""),
189 (KernRe(r"DECL_BUCKET_PARAMS\s*\(\s*(\S+)\s*,\s*(\S+)\s*\)"), r"\1, \2"),
190 (KernRe(r"__attribute_const__ +"), ""),
191 (KernRe(r"__attribute__\s*\(\((?:[\w\s]+(?:\([^)]*\))?\s*,?)+\)\)\s+"), ""),
192]
193
194#
195# Apply a set of transforms to a block of text.
196#
197def apply_transforms(xforms, text):
198 for search, subst in xforms:
199 text = search.sub(subst, text)
200 return text
201
77#202#
78# A little helper to get rid of excess white space203# A little helper to get rid of excess white space
79#204#
@@ -81,6 +206,21 @@ multi_space = KernRe(r'\s\s+') · 1 unmodified line
81def trim_whitespace(s):206def trim_whitespace(s):
82 return multi_space.sub(' ', s.strip())207 return multi_space.sub(' ', s.strip())
83 208
209#
210# Remove struct/enum members that have been marked "private".
211#
212def trim_private_members(text):
213 #
214 # First look for a "public:" block that ends a private region, then
215 # handle the "private until the end" case.
216 #
217 text = KernRe(r'/\*\s*private:.*?/\*\s*public:.*?\*/', flags=re.S).sub('', text)
218 text = KernRe(r'/\*\s*private:.*', flags=re.S).sub('', text)
219 #
220 # We needed the comments to do the above, but now we can take them out.
221 #
222 return KernRe(r'\s*/\*.*?\*/\s*', flags=re.S).sub('', text).strip()
223
84class state:224class state:
85 """225 """
86 State machine enums226 State machine enums
@@ -318,36 +458,26 @@ class KernelDoc: · 231 unmodified lines
318 458
319 param = KernRe(r'[\[\)].*').sub('', param, count=1)459 param = KernRe(r'[\[\)].*').sub('', param, count=1)
320 460
321 if dtype == "" and param.endswith("..."):461 #
322 if KernRe(r'\w\.\.\.$').search(param):462 # Look at various "anonymous type" cases.
323 # For named variable parameters of the form `x...`,463 #
324 # remove the dots464 if dtype == '':
325 param = param[:-3]465 if param.endswith("..."):
326 else:466 if len(param) > 3: # there is a name provided, use that
327 # Handles unnamed variable parameters467 param = param[:-3]
328 param = "..."468 if not self.entry.parameterdescs.get(param):
329 469 self.entry.parameterdescs[param] = "variable arguments"
330 if param not in self.entry.parameterdescs or \470
331 not self.entry.parameterdescs[param]:471 elif (not param) or param == "void":
332 472 param = "void"
333 self.entry.parameterdescs[param] = "variable arguments"473 self.entry.parameterdescs[param] = "no arguments"
334 474
335 elif dtype == "" and (not param or param == "void"):475 elif param in ["struct", "union"]:
336 param = "void"476 # Handle unnamed (anonymous) union or struct
337 self.entry.parameterdescs[param] = "no arguments"477 dtype = param
338 478 param = "{unnamed_" + param + "}"
339 elif dtype == "" and param in ["struct", "union"]:479 self.entry.parameterdescs[param] = "anonymous\n"
340 # Handle unnamed (anonymous) union or struct480 self.entry.anon_struct_union = True
341 dtype = param
342 param = "{unnamed_" + param + "}"
343 self.entry.parameterdescs[param] = "anonymous\n"
344 self.entry.anon_struct_union = True
345
346 # Handle cache group enforcing variables: they do not need
347 # to be described in header files
348 elif "__cacheline_group" in param:
349 # Ignore __cacheline_group_begin and __cacheline_group_end
350 return
351 481
352 # Warn if parameter has no description482 # Warn if parameter has no description
353 # (but ignore ones starting with # as these are not parameters483 # (but ignore ones starting with # as these are not parameters
@@ -389,9 +519,6 @@ class KernelDoc: · 35 unmodified lines
389 args = arg_expr.sub(r"\1#", args)519 args = arg_expr.sub(r"\1#", args)
390 520
391 for arg in args.split(splitter):521 for arg in args.split(splitter):
392 # Strip comments
393 arg = KernRe(r'\/\*.*\*\/').sub('', arg)
394
395 # Ignore argument attributes522 # Ignore argument attributes
396 arg = KernRe(r'\sPOS0?\s').sub(' ', arg)523 arg = KernRe(r'\sPOS0?\s').sub(' ', arg)
397 524
@@ -407,81 +534,76 @@ class KernelDoc: · 9 unmodified lines
407 # Treat preprocessor directive as a typeless variable534 # Treat preprocessor directive as a typeless variable
408 self.push_parameter(ln, decl_type, arg, "",535 self.push_parameter(ln, decl_type, arg, "",
409 "", declaration_name)536 "", declaration_name)
410 537 #
538 # The pointer-to-function case.
539 #
411 elif KernRe(r'\(.+\)\s*\(').search(arg):540 elif KernRe(r'\(.+\)\s*\(').search(arg):
412 # Pointer-to-function
413
414 arg = arg.replace('#', ',')541 arg = arg.replace('#', ',')
415 542 r = KernRe(r'[^\(]+\(\*?\s*' # Everything up to "(*"
416 r = KernRe(r'[^\(]+\(\*?\s*([\w\[\]\.]*)\s*\)')543 r'([\w\[\].]*)' # Capture the name and possible [array]
544 r'\s*\)') # Make sure the trailing ")" is there
417 if r.match(arg):545 if r.match(arg):
418 param = r.group(1)546 param = r.group(1)
419 else:547 else:
420 self.emit_msg(ln, f"Invalid param: {arg}")548 self.emit_msg(ln, f"Invalid param: {arg}")
421 param = arg549 param = arg
422 550 dtype = arg.replace(param, '')
423 dtype = KernRe(r'([^\(]+\(\*?)\s*' + re.escape(param)).sub(r'\1', arg)551 self.push_parameter(ln, decl_type, param, dtype, arg, declaration_name)
424 self.push_parameter(ln, decl_type, param, dtype,552 #
425 arg, declaration_name)553 # The array-of-pointers case. Dig the parameter name out from the middle
426 554 # of the declaration.
555 #
427 elif KernRe(r'\(.+\)\s*\[').search(arg):556 elif KernRe(r'\(.+\)\s*\[').search(arg):
428 # Array-of-pointers557 r = KernRe(r'[^\(]+\(\s*\*\s*' # Up to "(" and maybe "*"
429 558 r'([\w.]*?)' # The actual pointer name
430 arg = arg.replace('#', ',')559 r'\s*(\[\s*\w+\s*\]\s*)*\)') # The [array portion]
431 r = KernRe(r'[^\(]+\(\s*\*\s*([\w\[\]\.]*?)\s*(\s*\[\s*[\w]+\s*\]\s*)*\)')
432 if r.match(arg):560 if r.match(arg):
433 param = r.group(1)561 param = r.group(1)
434 else:562 else:
435 self.emit_msg(ln, f"Invalid param: {arg}")563 self.emit_msg(ln, f"Invalid param: {arg}")
436 param = arg564 param = arg
437 565 dtype = arg.replace(param, '')
438 dtype = KernRe(r'([^\(]+\(\*?)\s*' + re.escape(param)).sub(r'\1', arg)566 self.push_parameter(ln, decl_type, param, dtype, arg, declaration_name)
439
440 self.push_parameter(ln, decl_type, param, dtype,
441 arg, declaration_name)
442
443 elif arg:567 elif arg:
568 #
569 # Clean up extraneous spaces and split the string at commas; the first
570 # element of the resulting list will also include the type information.
571 #
444 arg = KernRe(r'\s*:\s*').sub(":", arg)572 arg = KernRe(r'\s*:\s*').sub(":", arg)
445 arg = KernRe(r'\s*\[').sub('[', arg)573 arg = KernRe(r'\s*\[').sub('[', arg)
446
447 args = KernRe(r'\s*,\s*').split(arg)574 args = KernRe(r'\s*,\s*').split(arg)
448 if args[0] and '*' in args[0]:575 args[0] = re.sub(r'(\*+)\s*', r' \1', args[0])
449 args[0] = re.sub(r'(\*+)\s*', r' \1', args[0])576 #
450 577 # args[0] has a string of "type a". If "a" includes an [array]
451 first_arg = []578 # declaration, we want to not be fooled by any white space inside
452 r = KernRe(r'^(.*\s+)(.*?\[.*\].*)$')579 # the brackets, so detect and handle that case specially.
453 if args[0] and r.match(args[0]):580 #
454 args.pop(0)581 r = KernRe(r'^([^[\]]*\s+)(.*)$')
455 first_arg.extend(r.group(1))582 if r.match(args[0]):
456 first_arg.append(r.group(2))583 args[0] = r.group(2)
584 dtype = r.group(1)
457 else:585 else:
458 first_arg = KernRe(r'\s+').split(args.pop(0))586 # No space in args[0]; this seems wrong but preserves previous behavior
459 587 dtype = ''
460 args.insert(0, first_arg.pop())
461 dtype = ' '.join(first_arg)
462 588
589 bitfield_re = KernRe(r'(.*?):(\w+)')
463 for param in args:590 for param in args:
464 if KernRe(r'^(\*+)\s*(.*)').match(param):591 #
465 r = KernRe(r'^(\*+)\s*(.*)')592 # For pointers, shift the star(s) from the variable name to the
466 if not r.match(param):593 # type declaration.
467 self.emit_msg(ln, f"Invalid param: {param}")594 #
468 continue595 r = KernRe(r'^(\*+)\s*(.*)')
469 596 if r.match(param):
470 param = r.group(1)
471
472 self.push_parameter(ln, decl_type, r.group(2),597 self.push_parameter(ln, decl_type, r.group(2),
473 f"{dtype} {r.group(1)}",598 f"{dtype} {r.group(1)}",
474 arg, declaration_name)599 arg, declaration_name)
475 600 #
476 elif KernRe(r'(.*?):(\w+)').search(param):601 # Perform a similar shift for bitfields.
477 r = KernRe(r'(.*?):(\w+)')602 #
478 if not r.match(param):603 elif bitfield_re.search(param):
479 self.emit_msg(ln, f"Invalid param: {param}")
480 continue
481
482 if dtype != "": # Skip unnamed bit-fields604 if dtype != "": # Skip unnamed bit-fields
483 self.push_parameter(ln, decl_type, r.group(1),605 self.push_parameter(ln, decl_type, bitfield_re.group(1),
484 f"{dtype}:{r.group(2)}",606 f"{dtype}:{bitfield_re.group(2)}",
485 arg, declaration_name)607 arg, declaration_name)
486 else:608 else:
487 self.push_parameter(ln, decl_type, param, dtype,609 self.push_parameter(ln, decl_type, param, dtype,
@@ -520,13 +642,11 @@ class KernelDoc: · 32 unmodified lines
520 self.emit_msg(ln,642 self.emit_msg(ln,
521 f"No description found for return value of '{declaration_name}'")643 f"No description found for return value of '{declaration_name}'")
522 644
523 def dump_struct(self, ln, proto):645 #
524 """646 # Split apart a structure prototype; returns (struct|union, name, members) or None
525 Store an entry for an struct or union647 #
526 """648 def split_struct_proto(self, proto):
527
528 type_pattern = r'(struct|union)'649 type_pattern = r'(struct|union)'
529
530 qualifiers = [650 qualifiers = [
531 "__attribute__",651 "__attribute__",
532 "__packed",652 "__packed",
@@ -534,288 +654,202 @@ class KernelDoc: · 1 unmodified line
534 "____cacheline_aligned_in_smp",654 "____cacheline_aligned_in_smp",
535 "____cacheline_aligned",655 "____cacheline_aligned",
536 ]656 ]
537
538 definition_body = r'\{(.*)\}\s*' + "(?:" + '|'.join(qualifiers) + ")?"657 definition_body = r'\{(.*)\}\s*' + "(?:" + '|'.join(qualifiers) + ")?"
539 struct_members = KernRe(type_pattern + r'([^\{\};]+)(\{)([^\{\}]*)(\})([^\{\}\;]*)(\;)')
540
541 # Extract struct/union definition
542 members = None
543 declaration_name = None
544 decl_type = None
545 658
546 r = KernRe(type_pattern + r'\s+(\w+)\s*' + definition_body)659 r = KernRe(type_pattern + r'\s+(\w+)\s*' + definition_body)
547 if r.search(proto):660 if r.search(proto):
548 decl_type = r.group(1)661 return (r.group(1), r.group(2), r.group(3))
549 declaration_name = r.group(2)
550 members = r.group(3)
551 else:662 else:
552 r = KernRe(r'typedef\s+' + type_pattern + r'\s*' + definition_body + r'\s*(\w+)\s*;')663 r = KernRe(r'typedef\s+' + type_pattern + r'\s*' + definition_body + r'\s*(\w+)\s*;')
553
554 if r.search(proto):664 if r.search(proto):
555 decl_type = r.group(1)665 return (r.group(1), r.group(3), r.group(2))
556 declaration_name = r.group(3)666 return None
557 members = r.group(2)667 #
558 668 # Rewrite the members of a structure or union for easier formatting later on.
559 if not members:669 # Among other things, this function will turn a member like:
560 self.emit_msg(ln, f"{proto} error: Cannot parse struct or union!")670 #
561 return671 # struct { inner_members; } foo;
562 672 #
563 if self.entry.identifier != declaration_name:673 # into:
564 self.emit_msg(ln,674 #
565 f"expecting prototype for {decl_type} {self.entry.identifier}. Prototype was for {decl_type} {declaration_name} instead\n")675 # struct foo; inner_members;
566 return676 #
567 677 def rewrite_struct_members(self, members):
568 args_pattern = r'([^,)]+)'
569
570 sub_prefixes = [
571 (KernRe(r'\/\*\s*private:.*?\/\*\s*public:.*?\*\/', re.S | re.I), ''),
572 (KernRe(r'\/\*\s*private:.*', re.S | re.I), ''),
573
574 # Strip comments
575 (KernRe(r'\/\*.*?\*\/', re.S), ''),
576
577 # Strip attributes
578 (attribute, ' '),
579 (KernRe(r'\s*__aligned\s*\([^;]*\)', re.S), ' '),
580 (KernRe(r'\s*__counted_by\s*\([^;]*\)', re.S), ' '),
581 (KernRe(r'\s*__counted_by_(le|be)\s*\([^;]*\)', re.S), ' '),
582 (KernRe(r'\s*__packed\s*', re.S), ' '),
583 (KernRe(r'\s*CRYPTO_MINALIGN_ATTR', re.S), ' '),
584 (KernRe(r'\s*____cacheline_aligned_in_smp', re.S), ' '),
585 (KernRe(r'\s*____cacheline_aligned', re.S), ' '),
586
587 # Unwrap struct_group macros based on this definition:
588 # __struct_group(TAG, NAME, ATTRS, MEMBERS...)
589 # which has variants like: struct_group(NAME, MEMBERS...)
590 # Only MEMBERS arguments require documentation.
591 #
592 # Parsing them happens on two steps:
593 #
594 # 1. drop struct group arguments that aren't at MEMBERS,
595 # storing them as STRUCT_GROUP(MEMBERS)
596 #
597 # 2. remove STRUCT_GROUP() ancillary macro.
598 #
599 # The original logic used to remove STRUCT_GROUP() using an
600 # advanced regex:
601 #
602 # \bSTRUCT_GROUP(\(((?:(?>[^)(]+)|(?1))*)\))[^;]*;
603 #
604 # with two patterns that are incompatible with
605 # Python re module, as it has:
606 #
607 # - a recursive pattern: (?1)
608 # - an atomic grouping: (?>...)
609 #
610 # I tried a simpler version: but it didn't work either:
611 # \bSTRUCT_GROUP\(([^\)]+)\)[^;]*;
612 #
613 # As it doesn't properly match the end parenthesis on some cases.
614 #
615 # So, a better solution was crafted: there's now a NestedMatch
616 # class that ensures that delimiters after a search are properly
617 # matched. So, the implementation to drop STRUCT_GROUP() will be
618 # handled in separate.
619
620 (KernRe(r'\bstruct_group\s*\(([^,]*,)', re.S), r'STRUCT_GROUP('),
621 (KernRe(r'\bstruct_group_attr\s*\(([^,]*,){2}', re.S), r'STRUCT_GROUP('),
622 (KernRe(r'\bstruct_group_tagged\s*\(([^,]*),([^,]*),', re.S), r'struct \1 \2; STRUCT_GROUP('),
623 (KernRe(r'\b__struct_group\s*\(([^,]*,){3}', re.S), r'STRUCT_GROUP('),
624
625 # Replace macros
626 #
627 # TODO: use NestedMatch for FOO($1, $2, ...) matches
628 #
629 # it is better to also move those to the NestedMatch logic,
630 # to ensure that parenthesis will be properly matched.
631
632 (KernRe(r'__ETHTOOL_DECLARE_LINK_MODE_MASK\s*\(([^\)]+)\)', re.S), r'DECLARE_BITMAP(\1, __ETHTOOL_LINK_MODE_MASK_NBITS)'),
633 (KernRe(r'DECLARE_PHY_INTERFACE_MASK\s*\(([^\)]+)\)', re.S), r'DECLARE_BITMAP(\1, PHY_INTERFACE_MODE_MAX)'),
634 (KernRe(r'DECLARE_BITMAP\s*\(' + args_pattern + r',\s*' + args_pattern + r'\)', re.S), r'unsigned long \1[BITS_TO_LONGS(\2)]'),
635 (KernRe(r'DECLARE_HASHTABLE\s*\(' + args_pattern + r',\s*' + args_pattern + r'\)', re.S), r'unsigned long \1[1 << ((\2) - 1)]'),
636 (KernRe(r'DECLARE_KFIFO\s*\(' + args_pattern + r',\s*' + args_pattern + r',\s*' + args_pattern + r'\)', re.S), r'\2 *\1'),
637 (KernRe(r'DECLARE_KFIFO_PTR\s*\(' + args_pattern + r',\s*' + args_pattern + r'\)', re.S), r'\2 *\1'),
638 (KernRe(r'(?:__)?DECLARE_FLEX_ARRAY\s*\(' + args_pattern + r',\s*' + args_pattern + r'\)', re.S), r'\1 \2[]'),
639 (KernRe(r'DEFINE_DMA_UNMAP_ADDR\s*\(' + args_pattern + r'\)', re.S), r'dma_addr_t \1'),
640 (KernRe(r'DEFINE_DMA_UNMAP_LEN\s*\(' + args_pattern + r'\)', re.S), r'__u32 \1'),
641 (KernRe(r'VIRTIO_DECLARE_FEATURES\s*\(' + args_pattern + r'\)', re.S), r'u64 \1; u64 \1_array[VIRTIO_FEATURES_DWORDS]'),
642 ]
643
644 # Regexes here are guaranteed to have the end limiter matching
645 # the start delimiter. Yet, right now, only one replace group
646 # is allowed.
647
648 sub_nested_prefixes = [
649 (re.compile(r'\bSTRUCT_GROUP\('), r'\1'),
650 ]
651
652 for search, sub in sub_prefixes:
653 members = search.sub(sub, members)
654
655 nested = NestedMatch()
656
657 for search, sub in sub_nested_prefixes:
658 members = nested.sub(search, sub, members)
659
660 # Keeps the original declaration as-is
661 declaration = members
662
663 # Split nested struct/union elements
664 #
665 # This loop was simpler at the original kernel-doc perl version, as
666 # while ($members =~ m/$struct_members/) { ... }
667 # reads 'members' string on each interaction.
668 #
669 # Python behavior is different: it parses 'members' only once,
670 # creating a list of tuples from the first interaction.
671 #678 #
672 # On other words, this won't get nested structs.679 # Process struct/union members from the most deeply nested outward. The
680 # trick is in the ^{ below - it prevents a match of an outer struct/union
681 # until the inner one has been munged (removing the "{" in the process).
673 #682 #
674 # So, we need to have an extra loop on Python to override such683 struct_members = KernRe(r'(struct|union)' # 0: declaration type
675 # re limitation.684 r'([^\{\};]+)' # 1: possible name
676 685 r'(\{)'
677 while True:686 r'([^\{\}]*)' # 3: Contents of declaration
678 tuples = struct_members.findall(members)687 r'(\})'
679 if not tuples:688 r'([^\{\};]*)(;)') # 5: Remaining stuff after declaration
680 break689 tuples = struct_members.findall(members)
681 690 while tuples:
682 for t in tuples:691 for t in tuples:
683 newmember = ""692 newmember = ""
684 maintype = t[0]693 oldmember = "".join(t) # Reconstruct the original formatting
685 s_ids = t[5]694 dtype, name, lbr, content, rbr, rest, semi = t
686 content = t[3]695 #
687 696 # Pass through each field name, normalizing the form and formatting.
688 oldmember = "".join(t)697 #
689 698 for s_id in rest.split(','):
690 for s_id in s_ids.split(','):
691 s_id = s_id.strip()699 s_id = s_id.strip()
692 700 newmember += f"{dtype} {s_id}; "
693 newmember += f"{maintype} {s_id}; "701 #
702 # Remove bitfield/array/pointer info, getting the bare name.
703 #
694 s_id = KernRe(r'[:\[].*').sub('', s_id)704 s_id = KernRe(r'[:\[].*').sub('', s_id)
695 s_id = KernRe(r'^\s*\**(\S+)\s*').sub(r'\1', s_id)705 s_id = KernRe(r'^\s*\**(\S+)\s*').sub(r'\1', s_id)
696 706 #
707 # Pass through the members of this inner structure/union.
708 #
697 for arg in content.split(';'):709 for arg in content.split(';'):
698 arg = arg.strip()710 arg = arg.strip()
699 711 #
700 if not arg:712 # Look for (type)(*name)(args) - pointer to function
701 continue713 #
702 714 r = KernRe(r'^([^\(]+\(\*?\s*)([\w.]*)(\s*\).*)')
703 r = KernRe(r'^([^\(]+\(\*?\s*)([\w\.]*)(\s*\).*)')
704 if r.match(arg):715 if r.match(arg):
716 dtype, name, extra = r.group(1), r.group(2), r.group(3)
705 # Pointer-to-function717 # Pointer-to-function
706 dtype = r.group(1)
707 name = r.group(2)
708 extra = r.group(3)
709
710 if not name:
711 continue
712
713 if not s_id:718 if not s_id:
714 # Anonymous struct/union719 # Anonymous struct/union
715 newmember += f"{dtype}{name}{extra}; "720 newmember += f"{dtype}{name}{extra}; "
716 else:721 else:
717 newmember += f"{dtype}{s_id}.{name}{extra}; "722 newmember += f"{dtype}{s_id}.{name}{extra}; "
718 723 #
724 # Otherwise a non-function member.
725 #
719 else:726 else:
720 arg = arg.strip()727 #
721 # Handle bitmaps728 # Remove bitmap and array portions and spaces around commas
729 #
722 arg = KernRe(r':\s*\d+\s*').sub('', arg)730 arg = KernRe(r':\s*\d+\s*').sub('', arg)
723
724 # Handle arrays
725 arg = KernRe(r'\[.*\]').sub('', arg)731 arg = KernRe(r'\[.*\]').sub('', arg)
726
727 # Handle multiple IDs
728 arg = KernRe(r'\s*,\s*').sub(',', arg)732 arg = KernRe(r'\s*,\s*').sub(',', arg)
729 733 #
734 # Look for a normal decl - "type name[,name...]"
735 #
730 r = KernRe(r'(.*)\s+([\S+,]+)')736 r = KernRe(r'(.*)\s+([\S+,]+)')
731
732 if r.search(arg):737 if r.search(arg):
733 dtype = r.group(1)738 for name in r.group(2).split(','):
734 names = r.group(2)739 name = KernRe(r'^\s*\**(\S+)\s*').sub(r'\1', name)
740 if not s_id:
741 # Anonymous struct/union
742 newmember += f"{r.group(1)} {name}; "
743 else:
744 newmember += f"{r.group(1)} {s_id}.{name}; "
735 else:745 else:
736 newmember += f"{arg}; "746 newmember += f"{arg}; "
737 continue747 #
738 748 # At the end of the s_id loop, replace the original declaration with
739 for name in names.split(','):749 # the munged version.
740 name = KernRe(r'^\s*\**(\S+)\s*').sub(r'\1', name).strip()750 #
741
742 if not name:
743 continue
744
745 if not s_id:
746 # Anonymous struct/union
747 newmember += f"{dtype} {name}; "
748 else:
749 newmember += f"{dtype} {s_id}.{name}; "
750
751 members = members.replace(oldmember, newmember)751 members = members.replace(oldmember, newmember)
752 #
753 # End of the tuple loop - search again and see if there are outer members
754 # that now turn up.
755 #
756 tuples = struct_members.findall(members)
757 return members
752 758
753 # Ignore other nested elements, like enums759 #
754 members = re.sub(r'(\{[^\{\}]*\})', '', members)760 # Format the struct declaration into a standard form for inclusion in the
755 761 # resulting docs.
756 self.create_parameter_list(ln, decl_type, members, ';',762 #
757 declaration_name)763 def format_struct_decl(self, declaration):
758 self.check_sections(ln, declaration_name, decl_type)764 #
759 765 # Insert newlines, get rid of extra spaces.
760 # Adjust declaration for better display766 #
761 declaration = KernRe(r'([\{;])').sub(r'\1\n', declaration)767 declaration = KernRe(r'([\{;])').sub(r'\1\n', declaration)
762 declaration = KernRe(r'\}\s+;').sub('};', declaration)768 declaration = KernRe(r'\}\s+;').sub('};', declaration)
763 769 #
764 # Better handle inlined enums770 # Format inline enums with each member on its own line.
765 while True:771 #
766 r = KernRe(r'(enum\s+\{[^\}]+),([^\n])')772 r = KernRe(r'(enum\s+\{[^\}]+),([^\n])')
767 if not r.search(declaration):773 while r.search(declaration):
768 break
769
770 declaration = r.sub(r'\1,\n\2', declaration)774 declaration = r.sub(r'\1,\n\2', declaration)
771 775 #
776 # Now go through and supply the right number of tabs
777 # for each line.
778 #
772 def_args = declaration.split('\n')779 def_args = declaration.split('\n')
773 level = 1780 level = 1
774 declaration = ""781 declaration = ""
775 for clause in def_args:782 for clause in def_args:
783 clause = KernRe(r'\s+').sub(' ', clause.strip(), count=1)
784 if clause:
785 if '}' in clause and level > 1:
786 level -= 1
787 if not clause.startswith('#'):
788 declaration += "\t" * level
789 declaration += "\t" + clause + "\n"
790 if "{" in clause and "}" not in clause:
791 level += 1
792 return declaration
776 793
777 clause = clause.strip()
778 clause = KernRe(r'\s+').sub(' ', clause, count=1)
779
780 if not clause:
781 continue
782
783 if '}' in clause and level > 1:
784 level -= 1
785 794
786 if not KernRe(r'^\s*#').match(clause):795 def dump_struct(self, ln, proto):
787 declaration += "\t" * level796 """
797 Store an entry for an struct or union
798 """
799 #
800 # Do the basic parse to get the pieces of the declaration.
801 #
802 struct_parts = self.split_struct_proto(proto)
803 if not struct_parts:
804 self.emit_msg(ln, f"{proto} error: Cannot parse struct or union!")
805 return
806 decl_type, declaration_name, members = struct_parts
788 807
789 declaration += "\t" + clause + "\n"808 if self.entry.identifier != declaration_name:
790 if "{" in clause and "}" not in clause:809 self.emit_msg(ln, f"expecting prototype for {decl_type} {self.entry.identifier}. "
791 level += 1810 f"Prototype was for {decl_type} {declaration_name} instead\n")
811 return
812 #
813 # Go through the list of members applying all of our transformations.
814 #
815 members = trim_private_members(members)
816 members = apply_transforms(struct_xforms, members)
792 817
818 nested = NestedMatch()
819 for search, sub in struct_nested_prefixes:
820 members = nested.sub(search, sub, members)
821 #
822 # Deal with embedded struct and union members, and drop enums entirely.
823 #
824 declaration = members
825 members = self.rewrite_struct_members(members)
826 members = re.sub(r'(\{[^\{\}]*\})', '', members)
827 #
828 # Output the result and we are done.
829 #
830 self.create_parameter_list(ln, decl_type, members, ';',
831 declaration_name)
832 self.check_sections(ln, declaration_name, decl_type)
793 self.output_declaration(decl_type, declaration_name,833 self.output_declaration(decl_type, declaration_name,
794 definition=declaration,834 definition=self.format_struct_decl(declaration),
795 purpose=self.entry.declaration_purpose)835 purpose=self.entry.declaration_purpose)
796 836
797 def dump_enum(self, ln, proto):837 def dump_enum(self, ln, proto):
798 """838 """
799 Stores an enum inside self.entries array.839 Stores an enum inside self.entries array.
800 """840 """
801 841 #
802 # Ignore members marked private842 # Strip preprocessor directives. Note that this depends on the
803 proto = KernRe(r'\/\*\s*private:.*?\/\*\s*public:.*?\*\/', flags=re.S).sub('', proto)843 # trailing semicolon we added in process_proto_type().
804 proto = KernRe(r'\/\*\s*private:.*}', flags=re.S).sub('}', proto)844 #
805
806 # Strip comments
807 proto = KernRe(r'\/\*.*?\*\/', flags=re.S).sub('', proto)
808
809 # Strip #define macros inside enums
810 proto = KernRe(r'#\s*((define|ifdef|if)\s+|endif)[^;]*;', flags=re.S).sub('', proto)845 proto = KernRe(r'#\s*((define|ifdef|if)\s+|endif)[^;]*;', flags=re.S).sub('', proto)
811
812 #846 #
813 # Parse out the name and members of the enum. Typedef form first.847 # Parse out the name and members of the enum. Typedef form first.
814 #848 #
815 r = KernRe(r'typedef\s+enum\s*\{(.*)\}\s*(\w*)\s*;')849 r = KernRe(r'typedef\s+enum\s*\{(.*)\}\s*(\w*)\s*;')
816 if r.search(proto):850 if r.search(proto):
817 declaration_name = r.group(2)851 declaration_name = r.group(2)
818 members = r.group(1).rstrip()852 members = trim_private_members(r.group(1))
819 #853 #
820 # Failing that, look for a straight enum854 # Failing that, look for a straight enum
821 #855 #
@@ -823,7 +857,7 @@ class KernelDoc: · 1 unmodified line
823 r = KernRe(r'enum\s+(\w*)\s*\{(.*)\}')857 r = KernRe(r'enum\s+(\w*)\s*\{(.*)\}')
824 if r.match(proto):858 if r.match(proto):
825 declaration_name = r.group(1)859 declaration_name = r.group(1)
826 members = r.group(2).rstrip()860 members = trim_private_members(r.group(2))
827 #861 #
828 # OK, this isn't going to work.862 # OK, this isn't going to work.
829 #863 #
@@ -892,62 +926,31 @@ class KernelDoc: · 62 unmodified lines
892 Stores a function of function macro inside self.entries array.926 Stores a function of function macro inside self.entries array.
893 """927 """
894 928
895 func_macro = False929 found = func_macro = False
896 return_type = ''930 return_type = ''
897 decl_type = 'function'931 decl_type = 'function'
898 932 #
899 # Prefixes that would be removed933 # Apply the initial transformations.
900 sub_prefixes = [934 #
901 (r"^static +", "", 0),935 prototype = apply_transforms(function_xforms, prototype)
902 (r"^extern +", "", 0),936 #
903 (r"^asmlinkage +", "", 0),937 # If we have a macro, remove the "#define" at the front.
904 (r"^inline +", "", 0),938 #
905 (r"^__inline__ +", "", 0),
906 (r"^__inline +", "", 0),
907 (r"^__always_inline +", "", 0),
908 (r"^noinline +", "", 0),
909 (r"^__FORTIFY_INLINE +", "", 0),
910 (r"__init +", "", 0),
911 (r"__init_or_module +", "", 0),
912 (r"__deprecated +", "", 0),
913 (r"__flatten +", "", 0),
914 (r"__meminit +", "", 0),
915 (r"__must_check +", "", 0),
916 (r"__weak +", "", 0),
917 (r"__sched +", "", 0),
918 (r"_noprof", "", 0),
919 (r"__printf\s*\(\s*\d*\s*,\s*\d*\s*\) +", "", 0),
920 (r"__(?:re)?alloc_size\s*\(\s*\d+\s*(?:,\s*\d+\s*)?\) +", "", 0),
921 (r"__diagnose_as\s*\(\s*\S+\s*(?:,\s*\d+\s*)*\) +", "", 0),
922 (r"DECL_BUCKET_PARAMS\s*\(\s*(\S+)\s*,\s*(\S+)\s*\)", r"\1, \2", 0),
923 (r"__attribute_const__ +", "", 0),
924
925 # It seems that Python support for re.X is broken:
926 # At least for me (Python 3.13), this didn't work
927# (r"""
928# __attribute__\s*\(\(
929# (?:
930# [\w\s]+ # attribute name
931# (?:\([^)]*\))? # attribute arguments
932# \s*,? # optional comma at the end
933# )+
934# \)\)\s+
935# """, "", re.X),
936
937 # So, remove whitespaces and comments from it
938 (r"__attribute__\s*\(\((?:[\w\s]+(?:\([^)]*\))?\s*,?)+\)\)\s+", "", 0),
939 ]
940
941 for search, sub, flags in sub_prefixes:
942 prototype = KernRe(search, flags).sub(sub, prototype)
943
944 # Macros are a special case, as they change the prototype format
945 new_proto = KernRe(r"^#\s*define\s+").sub("", prototype)939 new_proto = KernRe(r"^#\s*define\s+").sub("", prototype)
946 if new_proto != prototype:940 if new_proto != prototype:
947 is_define_proto = True
948 prototype = new_proto941 prototype = new_proto
949 else:942 #
950 is_define_proto = False943 # Dispense with the simple "#define A B" case here; the key
944 # is the space after the name of the symbol being defined.
945 # NOTE that the seemingly misnamed "func_macro" indicates a
946 # macro *without* arguments.
947 #
948 r = KernRe(r'^(\w+)\s+')
949 if r.search(prototype):
950 return_type = ''
951 declaration_name = r.group(1)
952 func_macro = True
953 found = True
951 954
952 # Yes, this truly is vile. We are looking for:955 # Yes, this truly is vile. We are looking for:
953 # 1. Return type (may be nothing if we're looking at a macro)956 # 1. Return type (may be nothing if we're looking at a macro)
@@ -965,91 +968,73 @@ class KernelDoc: · 11 unmodified lines
965 # - atomic_set (macro)968 # - atomic_set (macro)
966 # - pci_match_device, __copy_to_user (long return type)969 # - pci_match_device, __copy_to_user (long return type)
967 970
968 name = r'[a-zA-Z0-9_~:]+'971 name = r'\w+'
969 prototype_end1 = r'[^\(]*'
970 prototype_end2 = r'[^\{]*'
971 prototype_end = fr'\(({prototype_end1}|{prototype_end2})\)'
972
973 # Besides compiling, Perl qr{[\w\s]+} works as a non-capturing group.
974 # So, this needs to be mapped in Python with (?:...)? or (?:...)+
975
976 type1 = r'(?:[\w\s]+)?'972 type1 = r'(?:[\w\s]+)?'
977 type2 = r'(?:[\w\s]+\*+)+'973 type2 = r'(?:[\w\s]+\*+)+'
978 974 #
979 found = False975 # Attempt to match first on (args) with no internal parentheses; this
980 976 # lets us easily filter out __acquires() and other post-args stuff. If
981 if is_define_proto:977 # that fails, just grab the rest of the line to the last closing
982 r = KernRe(r'^()(' + name + r')\s+')978 # parenthesis.
983 979 #
984 if r.search(prototype):980 proto_args = r'\(([^\(]*|.*)\)'
985 return_type = ''981 #
986 declaration_name = r.group(2)982 # (Except for the simple macro case) attempt to split up the prototype
987 func_macro = True983 # in the various ways we understand.
988 984 #
989 found = True
990
991 if not found:985 if not found:
992 patterns = [986 patterns = [
993 rf'^()({name})\s*{prototype_end}',987 rf'^()({name})\s*{proto_args}',
994 rf'^({type1})\s+({name})\s*{prototype_end}',988 rf'^({type1})\s+({name})\s*{proto_args}',
995 rf'^({type2})\s*({name})\s*{prototype_end}',989 rf'^({type2})\s*({name})\s*{proto_args}',
996 ]990 ]
997 991
998 for p in patterns:992 for p in patterns:
999 r = KernRe(p)993 r = KernRe(p)
1000
1001 if r.match(prototype):994 if r.match(prototype):
1002
1003 return_type = r.group(1)995 return_type = r.group(1)
1004 declaration_name = r.group(2)996 declaration_name = r.group(2)
1005 args = r.group(3)997 args = r.group(3)
1006
1007 self.create_parameter_list(ln, decl_type, args, ',',998 self.create_parameter_list(ln, decl_type, args, ',',
1008 declaration_name)999 declaration_name)
1009
1010 found = True1000 found = True
1011 break1001 break
1002 #
1003 # Parsing done; make sure that things are as we expect.
1004 #
1012 if not found:1005 if not found:
1013 self.emit_msg(ln,1006 self.emit_msg(ln,
1014 f"cannot understand function prototype: '{prototype}'")1007 f"cannot understand function prototype: '{prototype}'")
1015 return1008 return
1016
1017 if self.entry.identifier != declaration_name:1009 if self.entry.identifier != declaration_name:
1018 self.emit_msg(ln,1010 self.emit_msg(ln, f"expecting prototype for {self.entry.identifier}(). "
1019 f"expecting prototype for {self.entry.identifier}(). Prototype was for {declaration_name}() instead")1011 f"Prototype was for {declaration_name}() instead")
1020 return1012 return
1021
1022 self.check_sections(ln, declaration_name, "function")1013 self.check_sections(ln, declaration_name, "function")
1023
1024 self.check_return_section(ln, declaration_name, return_type)1014 self.check_return_section(ln, declaration_name, return_type)
1015 #
1016 # Store the result.
1017 #
1018 self.output_declaration(decl_type, declaration_name,
1019 typedef=('typedef' in return_type),
1020 functiontype=return_type,
1021 purpose=self.entry.declaration_purpose,
1022 func_macro=func_macro)
1025 1023
1026 if 'typedef' in return_type:
1027 self.output_declaration(decl_type, declaration_name,
1028 typedef=True,
1029 functiontype=return_type,
1030 purpose=self.entry.declaration_purpose,
1031 func_macro=func_macro)
1032 else:
1033 self.output_declaration(decl_type, declaration_name,
1034 typedef=False,
1035 functiontype=return_type,
1036 purpose=self.entry.declaration_purpose,
1037 func_macro=func_macro)
1038 1024
1039 def dump_typedef(self, ln, proto):1025 def dump_typedef(self, ln, proto):
1040 """1026 """
1041 Stores a typedef inside self.entries array.1027 Stores a typedef inside self.entries array.
1042 """1028 """
1043 1029 #
1044 typedef_type = r'((?:\s+[\w\*]+\b){0,7}\s+(?:\w+\b|\*+))\s*'1030 # We start by looking for function typedefs.
1031 #
1032 typedef_type = r'typedef((?:\s+[\w*]+\b){0,7}\s+(?:\w+\b|\*+))\s*'
1045 typedef_ident = r'\*?\s*(\w\S+)\s*'1033 typedef_ident = r'\*?\s*(\w\S+)\s*'
1046 typedef_args = r'\s*\((.*)\);'1034 typedef_args = r'\s*\((.*)\);'
1047 1035
1048 typedef1 = KernRe(r'typedef' + typedef_type + r'\(' + typedef_ident + r'\)' + typedef_args)1036 typedef1 = KernRe(typedef_type + r'\(' + typedef_ident + r'\)' + typedef_args)
1049 typedef2 = KernRe(r'typedef' + typedef_type + typedef_ident + typedef_args)1037 typedef2 = KernRe(typedef_type + typedef_ident + typedef_args)
1050
1051 # Strip comments
1052 proto = KernRe(r'/\*.*?\*/', flags=re.S).sub('', proto)
1053 1038
1054 # Parse function typedef prototypes1039 # Parse function typedef prototypes
1055 for r in [typedef1, typedef2]:1040 for r in [typedef1, typedef2]:
@@ -1065,21 +1050,16 @@ class KernelDoc: · 9 unmodified lines
1065 f"expecting prototype for typedef {self.entry.identifier}. Prototype was for typedef {declaration_name} instead\n")1050 f"expecting prototype for typedef {self.entry.identifier}. Prototype was for typedef {declaration_name} instead\n")
1066 return1051 return
1067 1052
1068 decl_type = 'function'1053 self.create_parameter_list(ln, 'function', args, ',', declaration_name)
1069 self.create_parameter_list(ln, decl_type, args, ',', declaration_name)
1070 1054
1071 self.output_declaration(decl_type, declaration_name,1055 self.output_declaration('function', declaration_name,
1072 typedef=True,1056 typedef=True,
1073 functiontype=return_type,1057 functiontype=return_type,
1074 purpose=self.entry.declaration_purpose)1058 purpose=self.entry.declaration_purpose)
1075 return1059 return
1076 1060 #
1077 # Handle nested parentheses or brackets1061 # Not a function, try to parse a simple typedef.
1078 r = KernRe(r'(\(*.\)\s*|\[*.\]\s*);$')1062 #
1079 while r.search(proto):
1080 proto = r.sub('', proto)
1081
1082 # Parse simple typedefs
1083 r = KernRe(r'typedef.*\s+(\w+)\s*;')1063 r = KernRe(r'typedef.*\s+(\w+)\s*;')
1084 if r.match(proto):1064 if r.match(proto):
1085 declaration_name = r.group(1)1065 declaration_name = r.group(1)
@@ -1262,7 +1242,7 @@ class KernelDoc: · 176 unmodified lines
1262 self.dump_section()1242 self.dump_section()
1263 1243
1264 # Look for doc_com + <text> + doc_end:1244 # Look for doc_com + <text> + doc_end:
1265 r = KernRe(r'\s*\*\s*[a-zA-Z_0-9:\.]+\*/')1245 r = KernRe(r'\s*\*\s*[a-zA-Z_0-9:.]+\*/')
1266 if r.match(line):1246 if r.match(line):
1267 self.emit_msg(ln, f"suspicious ending line: {line}")1247 self.emit_msg(ln, f"suspicious ending line: {line}")
1268 1248
@@ -1473,7 +1453,7 @@ class KernelDoc: · 204 unmodified lines
1473 """Ancillary routine to process a function prototype"""1453 """Ancillary routine to process a function prototype"""
1474 1454
1475 # strip C99-style comments to end of line1455 # strip C99-style comments to end of line
1476 line = KernRe(r"\/\/.*$", re.S).sub('', line)1456 line = KernRe(r"//.*$", re.S).sub('', line)
1477 #1457 #
1478 # Soak up the line's worth of prototype text, stopping at { or ; if present.1458 # Soak up the line's worth of prototype text, stopping at { or ; if present.
1479 #1459 #
scripts/selinux/install_policy.sh+1−1
@@ -74,7 +74,7 @@ cd /etc/selinux/dummy/contexts/files · 73 unmodified lines
74$SF -F file_contexts /74$SF -F file_contexts /
75 75
76mounts=`cat /proc/$$/mounts | \76mounts=`cat /proc/$$/mounts | \
77 grep -E "ext[234]|jfs|xfs|reiserfs|jffs2|gfs2|btrfs|f2fs|ocfs2" | \77 grep -E "ext[234]|jfs|xfs|jffs2|gfs2|btrfs|f2fs|ocfs2" | \
78 awk '{ print $2 '}`78 awk '{ print $2 '}`
79$SF -F file_contexts $mounts79$SF -F file_contexts $mounts
80 80
scripts/sphinx-build-wrapper+719−0
@@ -0,0 +1,719 @@
1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0
3# Copyright (C) 2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
4#
5# pylint: disable=R0902, R0912, R0913, R0914, R0915, R0917, C0103
6#
7# Converted from docs Makefile and parallel-wrapper.sh, both under
8# GPLv2, copyrighted since 2008 by the following authors:
9#
10# Akira Yokosawa <akiyks@gmail.com>
11# Arnd Bergmann <arnd@arndb.de>
12# Breno Leitao <leitao@debian.org>
13# Carlos Bilbao <carlos.bilbao@amd.com>
14# Dave Young <dyoung@redhat.com>
15# Donald Hunter <donald.hunter@gmail.com>
16# Geert Uytterhoeven <geert+renesas@glider.be>
17# Jani Nikula <jani.nikula@intel.com>
18# Jan Stancek <jstancek@redhat.com>
19# Jonathan Corbet <corbet@lwn.net>
20# Joshua Clayton <stillcompiling@gmail.com>
21# Kees Cook <keescook@chromium.org>
22# Linus Torvalds <torvalds@linux-foundation.org>
23# Magnus Damm <damm+renesas@opensource.se>
24# Masahiro Yamada <masahiroy@kernel.org>
25# Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
26# Maxim Cournoyer <maxim.cournoyer@gmail.com>
27# Peter Foley <pefoley2@pefoley.com>
28# Randy Dunlap <rdunlap@infradead.org>
29# Rob Herring <robh@kernel.org>
30# Shuah Khan <shuahkh@osg.samsung.com>
31# Thorsten Blum <thorsten.blum@toblux.com>
32# Tomas Winkler <tomas.winkler@intel.com>
33
34
35"""
36Sphinx build wrapper that handles Kernel-specific business rules:
37
38- it gets the Kernel build environment vars;
39- it determines what's the best parallelism;
40- it handles SPHINXDIRS
41
42This tool ensures that MIN_PYTHON_VERSION is satisfied. If version is
43below that, it seeks for a new Python version. If found, it re-runs using
44the newer version.
45"""
46
47import argparse
48import locale
49import os
50import re
51import shlex
52import shutil
53import subprocess
54import sys
55
56from concurrent import futures
57from glob import glob
58
59LIB_DIR = "lib"
60SRC_DIR = os.path.dirname(os.path.realpath(__file__))
61
62sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR))
63
64from jobserver import JobserverExec # pylint: disable=C0413
65
66
67def parse_version(version):
68 """Convert a major.minor.patch version into a tuple"""
69 return tuple(int(x) for x in version.split("."))
70
71def ver_str(version):
72 """Returns a version tuple as major.minor.patch"""
73
74 return ".".join([str(x) for x in version])
75
76# Minimal supported Python version needed by Sphinx and its extensions
77MIN_PYTHON_VERSION = parse_version("3.7")
78
79# Default value for --venv parameter
80VENV_DEFAULT = "sphinx_latest"
81
82# List of make targets and its corresponding builder and output directory
83TARGETS = {
84 "cleandocs": {
85 "builder": "clean",
86 },
87 "htmldocs": {
88 "builder": "html",
89 },
90 "epubdocs": {
91 "builder": "epub",
92 "out_dir": "epub",
93 },
94 "texinfodocs": {
95 "builder": "texinfo",
96 "out_dir": "texinfo",
97 },
98 "infodocs": {
99 "builder": "texinfo",
100 "out_dir": "texinfo",
101 },
102 "latexdocs": {
103 "builder": "latex",
104 "out_dir": "latex",
105 },
106 "pdfdocs": {
107 "builder": "latex",
108 "out_dir": "latex",
109 },
110 "xmldocs": {
111 "builder": "xml",
112 "out_dir": "xml",
113 },
114 "linkcheckdocs": {
115 "builder": "linkcheck"
116 },
117}
118
119# Paper sizes. An empty value will pick the default
120PAPER = ["", "a4", "letter"]
121
122class SphinxBuilder:
123 """
124 Handles a sphinx-build target, adding needed arguments to build
125 with the Kernel.
126 """
127
128 def is_rust_enabled(self):
129 """Check if rust is enabled at .config"""
130 config_path = os.path.join(self.srctree, ".config")
131 if os.path.isfile(config_path):
132 with open(config_path, "r", encoding="utf-8") as f:
133 return "CONFIG_RUST=y" in f.read()
134 return False
135
136 def get_path(self, path, abs_path=False):
137 """
138 Ancillary routine to handle patches the right way, as shell does.
139
140 It first expands "~" and "~user". Then, if patch is not absolute,
141 join self.srctree. Finally, if requested, convert to abspath.
142 """
143
144 path = os.path.expanduser(path)
145 if not path.startswith("/"):
146 path = os.path.join(self.srctree, path)
147
148 if abs_path:
149 return os.path.abspath(path)
150
151 return path
152
153 def __init__(self, venv=None, verbose=False, n_jobs=None, interactive=None):
154 """Initialize internal variables"""
155 self.venv = venv
156 self.verbose = None
157
158 # Normal variables passed from Kernel's makefile
159 self.kernelversion = os.environ.get("KERNELVERSION", "unknown")
160 self.kernelrelease = os.environ.get("KERNELRELEASE", "unknown")
161 self.pdflatex = os.environ.get("PDFLATEX", "xelatex")
162
163 if not interactive:
164 self.latexopts = os.environ.get("LATEXOPTS", "-interaction=batchmode -no-shell-escape")
165 else:
166 self.latexopts = os.environ.get("LATEXOPTS", "")
167
168 if not verbose:
169 verbose = bool(os.environ.get("KBUILD_VERBOSE", "") != "")
170
171 # Handle SPHINXOPTS evironment
172 sphinxopts = shlex.split(os.environ.get("SPHINXOPTS", ""))
173
174 # As we handle number of jobs and quiet in separate, we need to pick
175 # it the same way as sphinx-build would pick, so let's use argparse
176 # do to the right argument expansion
177 parser = argparse.ArgumentParser()
178 parser.add_argument('-j', '--jobs', type=int)
179 parser.add_argument('-q', '--quiet', type=int)
180
181 # Other sphinx-build arguments go as-is, so place them
182 # at self.sphinxopts
183 sphinx_args, self.sphinxopts = parser.parse_known_args(sphinxopts)
184 if sphinx_args.quiet == True:
185 self.verbose = False
186
187 if sphinx_args.jobs:
188 self.n_jobs = sphinx_args.jobs
189
190 # Command line arguments was passed, override SPHINXOPTS
191 if verbose is not None:
192 self.verbose = verbose
193
194 self.n_jobs = n_jobs
195
196 # Source tree directory. This needs to be at os.environ, as
197 # Sphinx extensions and media uAPI makefile needs it
198 self.srctree = os.environ.get("srctree")
199 if not self.srctree:
200 self.srctree = "."
201 os.environ["srctree"] = self.srctree
202
203 # Now that we can expand srctree, get other directories as well
204 self.sphinxbuild = os.environ.get("SPHINXBUILD", "sphinx-build")
205 self.kerneldoc = self.get_path(os.environ.get("KERNELDOC",
206 "scripts/kernel-doc.py"))
207 self.obj = os.environ.get("obj", "Documentation")
208 self.builddir = self.get_path(os.path.join(self.obj, "output"),
209 abs_path=True)
210
211 # Media uAPI needs it
212 os.environ["BUILDDIR"] = self.builddir
213
214 # Detect if rust is enabled
215 self.config_rust = self.is_rust_enabled()
216
217 # Get directory locations for LaTeX build toolchain
218 self.pdflatex_cmd = shutil.which(self.pdflatex)
219 self.latexmk_cmd = shutil.which("latexmk")
220
221 self.env = os.environ.copy()
222
223 # If venv parameter is specified, run Sphinx from venv
224 if venv:
225 bin_dir = os.path.join(venv, "bin")
226 if os.path.isfile(os.path.join(bin_dir, "activate")):
227 # "activate" virtual env
228 self.env["PATH"] = bin_dir + ":" + self.env["PATH"]
229 self.env["VIRTUAL_ENV"] = venv
230 if "PYTHONHOME" in self.env:
231 del self.env["PYTHONHOME"]
232 print(f"Setting venv to {venv}")
233 else:
234 sys.exit(f"Venv {venv} not found.")
235
236 def run_sphinx(self, sphinx_build, build_args, *args, **pwargs):
237 """
238 Executes sphinx-build using current python3 command and setting
239 -j parameter if possible to run the build in parallel.
240 """
241
242 with JobserverExec() as jobserver:
243 if jobserver.claim:
244 n_jobs = str(jobserver.claim)
245 else:
246 n_jobs = "auto" # Supported since Sphinx 1.7
247
248 cmd = []
249
250 if self.venv:
251 cmd.append("python")
252 else:
253 cmd.append(sys.executable)
254
255 cmd.append(sphinx_build)
256
257 # if present, SPHINXOPTS or command line --jobs overrides default
258 if self.n_jobs:
259 n_jobs = str(self.n_jobs)
260
261 if n_jobs:
262 cmd += [f"-j{n_jobs}"]
263
264 if not self.verbose:
265 cmd.append("-q")
266
267 cmd += self.sphinxopts
268
269 cmd += build_args
270
271 if self.verbose:
272 print(" ".join(cmd))
273
274 rc = subprocess.call(cmd, *args, **pwargs)
275
276 def handle_html(self, css, output_dir):
277 """
278 Extra steps for HTML and epub output.
279
280 For such targets, we need to ensure that CSS will be properly
281 copied to the output _static directory
282 """
283
284 if not css:
285 return
286
287 css = os.path.expanduser(css)
288 if not css.startswith("/"):
289 css = os.path.join(self.srctree, css)
290
291 static_dir = os.path.join(output_dir, "_static")
292 os.makedirs(static_dir, exist_ok=True)
293
294 try:
295 shutil.copy2(css, static_dir)
296 except (OSError, IOError) as e:
297 print(f"Warning: Failed to copy CSS: {e}", file=sys.stderr)
298
299 def build_pdf_file(self, latex_cmd, from_dir, path):
300 """Builds a single pdf file using latex_cmd"""
301 try:
302 subprocess.run(latex_cmd + [path],
303 cwd=from_dir, check=True)
304
305 return True
306 except subprocess.CalledProcessError:
307 # LaTeX PDF error code is almost useless: it returns
308 # error codes even when build succeeds but has warnings.
309 # So, we'll ignore the results
310 return False
311
312 def pdf_parallel_build(self, tex_suffix, latex_cmd, tex_files, n_jobs):
313 """Build PDF files in parallel if possible"""
314 builds = {}
315 build_failed = False
316 max_len = 0
317 has_tex = False
318
319 # Process files in parallel
320 with futures.ThreadPoolExecutor(max_workers=n_jobs) as executor:
321 jobs = {}
322
323 for from_dir, pdf_dir, entry in tex_files:
324 name = entry.name
325
326 if not name.endswith(tex_suffix):
327 continue
328
329 name = name[:-len(tex_suffix)]
330
331 max_len = max(max_len, len(name))
332
333 has_tex = True
334
335 future = executor.submit(self.build_pdf_file, latex_cmd,
336 from_dir, entry.path)
337 jobs[future] = (from_dir, name, entry.path)
338
339 for future in futures.as_completed(jobs):
340 from_dir, name, path = jobs[future]
341
342 pdf_name = name + ".pdf"
343 pdf_from = os.path.join(from_dir, pdf_name)
344
345 try:
346 success = future.result()
347
348 if success and os.path.exists(pdf_from):
349 pdf_to = os.path.join(pdf_dir, pdf_name)
350
351 os.rename(pdf_from, pdf_to)
352 builds[name] = os.path.relpath(pdf_to, self.builddir)
353 else:
354 builds[name] = "FAILED"
355 build_failed = True
356 except Exception as e:
357 builds[name] = f"FAILED ({str(e)})"
358 build_failed = True
359
360 # Handle case where no .tex files were found
361 if not has_tex:
362 name = "Sphinx LaTeX builder"
363 max_len = max(max_len, len(name))
364 builds[name] = "FAILED (no .tex file was generated)"
365 build_failed = True
366
367 return builds, build_failed, max_len
368
369 def handle_pdf(self, output_dirs):
370 """
371 Extra steps for PDF output.
372
373 As PDF is handled via a LaTeX output, after building the .tex file,
374 a new build is needed to create the PDF output from the latex
375 directory.
376 """
377 builds = {}
378 max_len = 0
379 tex_suffix = ".tex"
380
381 # Get all tex files that will be used for PDF build
382 tex_files = []
383 for from_dir in output_dirs:
384 pdf_dir = os.path.join(from_dir, "../pdf")
385 os.makedirs(pdf_dir, exist_ok=True)
386
387 if self.latexmk_cmd:
388 latex_cmd = [self.latexmk_cmd, f"-{self.pdflatex}"]
389 else:
390 latex_cmd = [self.pdflatex]
391
392 latex_cmd.extend(shlex.split(self.latexopts))
393
394 # Get a list of tex files to process
395 with os.scandir(from_dir) as it:
396 for entry in it:
397 if entry.name.endswith(tex_suffix):
398 tex_files.append((from_dir, pdf_dir, entry))
399
400 # When using make, this won't be used, as the number of jobs comes
401 # from POSIX jobserver. So, this covers the case where build comes
402 # from command line. On such case, serialize by default, except if
403 # the user explicitly sets the number of jobs.
404 n_jobs = 1
405
406 # n_jobs is either an integer or "auto". Only use it if it is a number
407 if self.n_jobs:
408 try:
409 n_jobs = int(self.n_jobs)
410 except ValueError:
411 pass
412
413 # When using make, jobserver.claim is the number of jobs that were
414 # used with "-j" and that aren't used by other make targets
415 with JobserverExec() as jobserver:
416 n_jobs = 1
417
418 # Handle the case when a parameter is passed via command line,
419 # using it as default, if jobserver doesn't claim anything
420 if self.n_jobs:
421 try:
422 n_jobs = int(self.n_jobs)
423 except ValueError:
424 pass
425
426 if jobserver.claim:
427 n_jobs = jobserver.claim
428
429 # Build files in parallel
430 builds, build_failed, max_len = self.pdf_parallel_build(tex_suffix,
431 latex_cmd,
432 tex_files,
433 n_jobs)
434
435 msg = "Summary"
436 msg += "\n" + "=" * len(msg)
437 print()
438 print(msg)
439
440 for pdf_name, pdf_file in builds.items():
441 print(f"{pdf_name:<{max_len}}: {pdf_file}")
442
443 print()
444
445 # return an error if a PDF file is missing
446
447 if build_failed:
448 sys.exit(f"PDF build failed: not all PDF files were created.")
449 else:
450 print("All PDF files were built.")
451
452 def handle_info(self, output_dirs):
453 """
454 Extra steps for Info output.
455
456 For texinfo generation, an additional make is needed from the
457 texinfo directory.
458 """
459
460 for output_dir in output_dirs:
461 try:
462 subprocess.run(["make", "info"], cwd=output_dir, check=True)
463 except subprocess.CalledProcessError as e:
464 sys.exit(f"Error generating info docs: {e}")
465
466 def cleandocs(self, builder):
467
468 shutil.rmtree(self.builddir, ignore_errors=True)
469
470 def build(self, target, sphinxdirs=None, conf="conf.py",
471 theme=None, css=None, paper=None):
472 """
473 Build documentation using Sphinx. This is the core function of this
474 module. It prepares all arguments required by sphinx-build.
475 """
476
477 builder = TARGETS[target]["builder"]
478 out_dir = TARGETS[target].get("out_dir", "")
479
480 # Cleandocs doesn't require sphinx-build
481 if target == "cleandocs":
482 self.cleandocs(builder)
483 return
484
485 # Other targets require sphinx-build
486 sphinxbuild = shutil.which(self.sphinxbuild, path=self.env["PATH"])
487 if not sphinxbuild:
488 sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n")
489
490 if builder == "latex":
491 if not self.pdflatex_cmd and not self.latexmk_cmd:
492 sys.exit("Error: pdflatex or latexmk required for PDF generation")
493
494 docs_dir = os.path.abspath(os.path.join(self.srctree, "Documentation"))
495
496 # Prepare base arguments for Sphinx build
497 kerneldoc = self.kerneldoc
498 if kerneldoc.startswith(self.srctree):
499 kerneldoc = os.path.relpath(kerneldoc, self.srctree)
500
501 # Prepare common Sphinx options
502 args = [
503 "-b", builder,
504 "-c", docs_dir,
505 ]
506
507 if builder == "latex":
508 if not paper:
509 paper = PAPER[1]
510
511 args.extend(["-D", f"latex_elements.papersize={paper}paper"])
512
513 if self.config_rust:
514 args.extend(["-t", "rustdoc"])
515
516 if conf:
517 self.env["SPHINX_CONF"] = self.get_path(conf, abs_path=True)
518
519 if not sphinxdirs:
520 sphinxdirs = os.environ.get("SPHINXDIRS", ".")
521
522 # The sphinx-build tool has a bug: internally, it tries to set
523 # locale with locale.setlocale(locale.LC_ALL, ''). This causes a
524 # crash if language is not set. Detect and fix it.
525 try:
526 locale.setlocale(locale.LC_ALL, '')
527 except Exception:
528 self.env["LC_ALL"] = "C"
529 self.env["LANG"] = "C"
530
531 # sphinxdirs can be a list or a whitespace-separated string
532 sphinxdirs_list = []
533 for sphinxdir in sphinxdirs:
534 if isinstance(sphinxdir, list):
535 sphinxdirs_list += sphinxdir
536 else:
537 for name in sphinxdir.split(" "):
538 sphinxdirs_list.append(name)
539
540 # Build each directory
541 output_dirs = []
542 for sphinxdir in sphinxdirs_list:
543 src_dir = os.path.join(docs_dir, sphinxdir)
544 doctree_dir = os.path.join(self.builddir, ".doctrees")
545 output_dir = os.path.join(self.builddir, sphinxdir, out_dir)
546
547 # Make directory names canonical
548 src_dir = os.path.normpath(src_dir)
549 doctree_dir = os.path.normpath(doctree_dir)
550 output_dir = os.path.normpath(output_dir)
551
552 os.makedirs(doctree_dir, exist_ok=True)
553 os.makedirs(output_dir, exist_ok=True)
554
555 output_dirs.append(output_dir)
556
557 build_args = args + [
558 "-d", doctree_dir,
559 "-D", f"kerneldoc_bin={kerneldoc}",
560 "-D", f"version={self.kernelversion}",
561 "-D", f"release={self.kernelrelease}",
562 "-D", f"kerneldoc_srctree={self.srctree}",
563 src_dir,
564 output_dir,
565 ]
566
567 # Execute sphinx-build
568 try:
569 self.run_sphinx(sphinxbuild, build_args, env=self.env)
570 except Exception as e:
571 sys.exit(f"Build failed: {e}")
572
573 # Ensure that html/epub will have needed static files
574 if target in ["htmldocs", "epubdocs"]:
575 self.handle_html(css, output_dir)
576
577 # PDF and Info require a second build step
578 if target == "pdfdocs":
579 self.handle_pdf(output_dirs)
580 elif target == "infodocs":
581 self.handle_info(output_dirs)
582
583 @staticmethod
584 def get_python_version(cmd):
585 """
586 Get python version from a Python binary. As we need to detect if
587 are out there newer python binaries, we can't rely on sys.release here.
588 """
589
590 result = subprocess.run([cmd, "--version"], check=True,
591 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
592 universal_newlines=True)
593 version = result.stdout.strip()
594
595 match = re.search(r"(\d+\.\d+\.\d+)", version)
596 if match:
597 return parse_version(match.group(1))
598
599 print(f"Can't parse version {version}")
600 return (0, 0, 0)
601
602 @staticmethod
603 def find_python():
604 """
605 Detect if are out there any python 3.xy version newer than the
606 current one.
607
608 Note: this routine is limited to up to 2 digits for python3. We
609 may need to update it one day, hopefully on a distant future.
610 """
611 patterns = [
612 "python3.[0-9]",
613 "python3.[0-9][0-9]",
614 ]
615
616 # Seek for a python binary newer than MIN_PYTHON_VERSION
617 for path in os.getenv("PATH", "").split(":"):
618 for pattern in patterns:
619 for cmd in glob(os.path.join(path, pattern)):
620 if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
621 version = SphinxBuilder.get_python_version(cmd)
622 if version >= MIN_PYTHON_VERSION:
623 return cmd
624
625 return None
626
627 @staticmethod
628 def check_python():
629 """
630 Check if the current python binary satisfies our minimal requirement
631 for Sphinx build. If not, re-run with a newer version if found.
632 """
633 cur_ver = sys.version_info[:3]
634 if cur_ver >= MIN_PYTHON_VERSION:
635 return
636
637 python_ver = ver_str(cur_ver)
638
639 new_python_cmd = SphinxBuilder.find_python()
640 if not new_python_cmd:
641 sys.exit(f"Python version {python_ver} is not supported anymore.")
642
643 # Restart script using the newer version
644 script_path = os.path.abspath(sys.argv[0])
645 args = [new_python_cmd, script_path] + sys.argv[1:]
646
647 print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
648
649 try:
650 os.execv(new_python_cmd, args)
651 except OSError as e:
652 sys.exit(f"Failed to restart with {new_python_cmd}: {e}")
653
654def jobs_type(value):
655 """
656 Handle valid values for -j. Accepts Sphinx "-jauto", plus a number
657 equal or bigger than one.
658 """
659 if value is None:
660 return None
661
662 if value.lower() == 'auto':
663 return value.lower()
664
665 try:
666 if int(value) >= 1:
667 return value
668
669 raise argparse.ArgumentTypeError(f"Minimum jobs is 1, got {value}")
670 except ValueError:
671 raise argparse.ArgumentTypeError(f"Must be 'auto' or positive integer, got {value}")
672
673def main():
674 """
675 Main function. The only mandatory argument is the target. If not
676 specified, the other arguments will use default values if not
677 specified at os.environ.
678 """
679 parser = argparse.ArgumentParser(description="Kernel documentation builder")
680
681 parser.add_argument("target", choices=list(TARGETS.keys()),
682 help="Documentation target to build")
683 parser.add_argument("--sphinxdirs", nargs="+",
684 help="Specific directories to build")
685 parser.add_argument("--conf", default="conf.py",
686 help="Sphinx configuration file")
687
688 parser.add_argument("--theme", help="Sphinx theme to use")
689
690 parser.add_argument("--css", help="Custom CSS file for HTML/EPUB")
691
692 parser.add_argument("--paper", choices=PAPER, default=PAPER[0],
693 help="Paper size for LaTeX/PDF output")
694
695 parser.add_argument("-v", "--verbose", action='store_true',
696 help="place build in verbose mode")
697
698 parser.add_argument('-j', '--jobs', type=jobs_type,
699 help="Sets number of jobs to use with sphinx-build")
700
701 parser.add_argument('-i', '--interactive', action='store_true',
702 help="Change latex default to run in interactive mode")
703
704 parser.add_argument("-V", "--venv", nargs='?', const=f'{VENV_DEFAULT}',
705 default=None,
706 help=f'If used, run Sphinx from a venv dir (default dir: {VENV_DEFAULT})')
707
708 args = parser.parse_args()
709
710 SphinxBuilder.check_python()
711
712 builder = SphinxBuilder(venv=args.venv, verbose=args.verbose,
713 n_jobs=args.jobs, interactive=args.interactive)
714
715 builder.build(args.target, sphinxdirs=args.sphinxdirs, conf=args.conf,
716 theme=args.theme, css=args.css, paper=args.paper)
717
718if __name__ == "__main__":
719 main()
scripts/sphinx-pre-install+1617−1052
@@ -1,1056 +1,1621 @@
1#!/usr/bin/env perl1#!/usr/bin/env python3
2# SPDX-License-Identifier: GPL-2.0-or-later2# SPDX-License-Identifier: GPL-2.0-or-later
3use strict;3# Copyright (c) 2017-2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
4
5# Copyright (c) 2017-2020 Mauro Carvalho Chehab <mchehab@kernel.org>
6#
7
8my $prefix = "./";
9$prefix = "$ENV{'srctree'}/" if ($ENV{'srctree'});
10
11my $conf = $prefix . "Documentation/conf.py";
12my $requirement_file = $prefix . "Documentation/sphinx/requirements.txt";
13my $virtenv_prefix = "sphinx_";
14
15#
16# Static vars
17#
18
19my %missing;
20my $system_release;
21my $need = 0;
22my $optional = 0;
23my $need_symlink = 0;
24my $need_sphinx = 0;
25my $need_pip = 0;
26my $need_virtualenv = 0;
27my $rec_sphinx_upgrade = 0;
28my $verbose_warn_install = 1;
29my $install = "";
30my $virtenv_dir = "";
31my $python_cmd = "";
32my $activate_cmd;
33my $min_version;
34my $cur_version;
35my $rec_version = "3.4.3";
36my $latest_avail_ver;
37
38#
39# Command line arguments
40#
41
42my $pdf = 1;
43my $virtualenv = 1;
44my $version_check = 0;
45
46#
47# List of required texlive packages on Fedora and OpenSuse
48#
49
50my %texlive = (
51 'amsfonts.sty' => 'texlive-amsfonts',
52 'amsmath.sty' => 'texlive-amsmath',
53 'amssymb.sty' => 'texlive-amsfonts',
54 'amsthm.sty' => 'texlive-amscls',
55 'anyfontsize.sty' => 'texlive-anyfontsize',
56 'atbegshi.sty' => 'texlive-oberdiek',
57 'bm.sty' => 'texlive-tools',
58 'capt-of.sty' => 'texlive-capt-of',
59 'cmap.sty' => 'texlive-cmap',
60 'ecrm1000.tfm' => 'texlive-ec',
61 'eqparbox.sty' => 'texlive-eqparbox',
62 'eu1enc.def' => 'texlive-euenc',
63 'fancybox.sty' => 'texlive-fancybox',
64 'fancyvrb.sty' => 'texlive-fancyvrb',
65 'float.sty' => 'texlive-float',
66 'fncychap.sty' => 'texlive-fncychap',
67 'footnote.sty' => 'texlive-mdwtools',
68 'framed.sty' => 'texlive-framed',
69 'luatex85.sty' => 'texlive-luatex85',
70 'multirow.sty' => 'texlive-multirow',
71 'needspace.sty' => 'texlive-needspace',
72 'palatino.sty' => 'texlive-psnfss',
73 'parskip.sty' => 'texlive-parskip',
74 'polyglossia.sty' => 'texlive-polyglossia',
75 'tabulary.sty' => 'texlive-tabulary',
76 'threeparttable.sty' => 'texlive-threeparttable',
77 'titlesec.sty' => 'texlive-titlesec',
78 'ucs.sty' => 'texlive-ucs',
79 'upquote.sty' => 'texlive-upquote',
80 'wrapfig.sty' => 'texlive-wrapfig',
81 'ctexhook.sty' => 'texlive-ctex',
82);
83
84#
85# Subroutines that checks if a feature exists
86#
87
88sub check_missing(%)
89{
90 my %map = %{$_[0]};
91
92 foreach my $prog (sort keys %missing) {
93 my $is_optional = $missing{$prog};
94
95 # At least on some LTS distros like CentOS 7, texlive doesn't
96 # provide all packages we need. When such distros are
97 # detected, we have to disable PDF output.
98 #
99 # So, we need to ignore the packages that distros would
100 # need for LaTeX to work
101 if ($is_optional == 2 && !$pdf) {
102 $optional--;
103 next;
104 }
105
106 if ($verbose_warn_install) {
107 if ($is_optional) {
108 print "Warning: better to also install \"$prog\".\n";
109 } else {
110 print "ERROR: please install \"$prog\", otherwise, build won't work.\n";
111 }
112 }
113 if (defined($map{$prog})) {
114 $install .= " " . $map{$prog};
115 } else {
116 $install .= " " . $prog;
117 }
118 }
119
120 $install =~ s/^\s//;
121}
122
123sub add_package($$)
124{
125 my $package = shift;
126 my $is_optional = shift;
127
128 $missing{$package} = $is_optional;
129 if ($is_optional) {
130 $optional++;
131 } else {
132 $need++;
133 }
134}
135
136sub check_missing_file($$$)
137{
138 my $files = shift;
139 my $package = shift;
140 my $is_optional = shift;
141
142 for (@$files) {
143 return if(-e $_);
144 }
145
146 add_package($package, $is_optional);
147}
148
149sub findprog($)
150{
151 foreach(split(/:/, $ENV{PATH})) {
152 return "$_/$_[0]" if(-x "$_/$_[0]");
153 }
154}
155
156sub find_python_no_venv()
157{
158 my $prog = shift;
159
160 my $cur_dir = qx(pwd);
161 $cur_dir =~ s/\s+$//;
162
163 foreach my $dir (split(/:/, $ENV{PATH})) {
164 next if ($dir =~ m,($cur_dir)/sphinx,);
165 return "$dir/python3" if(-x "$dir/python3");
166 }
167 foreach my $dir (split(/:/, $ENV{PATH})) {
168 next if ($dir =~ m,($cur_dir)/sphinx,);
169 return "$dir/python" if(-x "$dir/python");
170 }
171 return "python";
172}
173
174sub check_program($$)
175{
176 my $prog = shift;
177 my $is_optional = shift;
178
179 return $prog if findprog($prog);
180
181 add_package($prog, $is_optional);
182}
183
184sub check_perl_module($$)
185{
186 my $prog = shift;
187 my $is_optional = shift;
188
189 my $err = system("perl -M$prog -e 1 2>/dev/null /dev/null");
190 return if ($err == 0);
191
192 add_package($prog, $is_optional);
193}
194
195sub check_python_module($$)
196{
197 my $prog = shift;
198 my $is_optional = shift;
199
200 return if (!$python_cmd);
201
202 my $err = system("$python_cmd -c 'import $prog' 2>/dev/null /dev/null");
203 return if ($err == 0);
204
205 add_package($prog, $is_optional);
206}
207
208sub check_rpm_missing($$)
209{
210 my @pkgs = @{$_[0]};
211 my $is_optional = $_[1];
212
213 foreach my $prog(@pkgs) {
214 my $err = system("rpm -q '$prog' 2>/dev/null >/dev/null");
215 add_package($prog, $is_optional) if ($err);
216 }
217}
218
219sub check_pacman_missing($$)
220{
221 my @pkgs = @{$_[0]};
222 my $is_optional = $_[1];
223
224 foreach my $prog(@pkgs) {
225 my $err = system("pacman -Q '$prog' 2>/dev/null >/dev/null");
226 add_package($prog, $is_optional) if ($err);
227 }
228}
229
230sub check_missing_tex($)
231{
232 my $is_optional = shift;
233 my $kpsewhich = findprog("kpsewhich");
234
235 foreach my $prog(keys %texlive) {
236 my $package = $texlive{$prog};
237 if (!$kpsewhich) {
238 add_package($package, $is_optional);
239 next;
240 }
241 my $file = qx($kpsewhich $prog);
242 add_package($package, $is_optional) if ($file =~ /^\s*$/);
243 }
244}
245
246sub get_sphinx_fname()
247{
248 if ($ENV{'SPHINXBUILD'}) {
249 return $ENV{'SPHINXBUILD'};
250 }
251
252 my $fname = "sphinx-build";
253 return $fname if findprog($fname);
254
255 $fname = "sphinx-build-3";
256 if (findprog($fname)) {
257 $need_symlink = 1;
258 return $fname;
259 }
260
261 return "";
262}
263
264sub get_sphinx_version($)
265{
266 my $cmd = shift;
267 my $ver;
268
269 open IN, "$cmd --version 2>&1 |";
270 while (<IN>) {
271 if (m/^\s*sphinx-build\s+([\d\.]+)((\+\/[\da-f]+)|(b\d+))?$/) {
272 $ver=$1;
273 last;
274 }
275 # Sphinx 1.2.x uses a different format
276 if (m/^\s*Sphinx.*\s+([\d\.]+)$/) {
277 $ver=$1;
278 last;
279 }
280 }
281 close IN;
282 return $ver;
283}
284
285sub check_sphinx()
286{
287 open IN, $conf or die "Can't open $conf";
288 while (<IN>) {
289 if (m/^\s*needs_sphinx\s*=\s*[\'\"]([\d\.]+)[\'\"]/) {
290 $min_version=$1;
291 last;
292 }
293 }
294 close IN;
295
296 die "Can't get needs_sphinx version from $conf" if (!$min_version);
297
298 $virtenv_dir = $virtenv_prefix . "latest";
299
300 my $sphinx = get_sphinx_fname();
301 if ($sphinx eq "") {
302 $need_sphinx = 1;
303 return;
304 }
305
306 $cur_version = get_sphinx_version($sphinx);
307 die "$sphinx didn't return its version" if (!$cur_version);
308
309 if ($cur_version lt $min_version) {
310 printf "ERROR: Sphinx version is %s. It should be >= %s\n",
311 $cur_version, $min_version;
312 $need_sphinx = 1;
313 return;
314 }
315
316 return if ($cur_version lt $rec_version);
317
318 # On version check mode, just assume Sphinx has all mandatory deps
319 exit (0) if ($version_check);
320}
321
322#
323# Ancillary subroutines
324#
325
326sub catcheck($)
327{
328 my $res = "";
329 $res = qx(cat $_[0]) if (-r $_[0]);
330 return $res;
331}
332
333sub which($)
334{
335 my $file = shift;
336 my @path = split ":", $ENV{PATH};
337
338 foreach my $dir(@path) {
339 my $name = $dir.'/'.$file;
340 return $name if (-x $name );
341 }
342 return undef;
343}
344
345#
346# Subroutines that check distro-specific hints
347#
348
349sub give_debian_hints()
350{
351 my %map = (
352 "python-sphinx" => "python3-sphinx",
353 "yaml" => "python3-yaml",
354 "ensurepip" => "python3-venv",
355 "virtualenv" => "virtualenv",
356 "dot" => "graphviz",
357 "convert" => "imagemagick",
358 "Pod::Usage" => "perl-modules",
359 "xelatex" => "texlive-xetex",
360 "rsvg-convert" => "librsvg2-bin",
361 );
362
363 if ($pdf) {
364 check_missing_file(["/usr/share/texlive/texmf-dist/tex/latex/ctex/ctexhook.sty"],
365 "texlive-lang-chinese", 2);
366
367 check_missing_file(["/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"],
368 "fonts-dejavu", 2);
369
370 check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
371 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
372 "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc"],
373 "fonts-noto-cjk", 2);
374 }
375
376 check_program("dvipng", 2) if ($pdf);
377 check_missing(\%map);
378
379 return if (!$need && !$optional);
380 printf("You should run:\n") if ($verbose_warn_install);
381 printf("\n\tsudo apt-get install $install\n");
382}
383
384sub give_redhat_hints()
385{
386 my %map = (
387 "python-sphinx" => "python3-sphinx",
388 "yaml" => "python3-pyyaml",
389 "virtualenv" => "python3-virtualenv",
390 "dot" => "graphviz",
391 "convert" => "ImageMagick",
392 "Pod::Usage" => "perl-Pod-Usage",
393 "xelatex" => "texlive-xetex-bin",
394 "rsvg-convert" => "librsvg2-tools",
395 );
396
397 my @fedora26_opt_pkgs = (
398 "graphviz-gd", # Fedora 26: needed for PDF support
399 );
400
401 my @fedora_tex_pkgs = (
402 "texlive-collection-fontsrecommended",
403 "texlive-collection-latex",
404 "texlive-xecjk",
405 "dejavu-sans-fonts",
406 "dejavu-serif-fonts",
407 "dejavu-sans-mono-fonts",
408 );
409
410 #
411 # Checks valid for RHEL/CentOS version 7.x.
412 #
413 my $old = 0;
414 my $rel;
415 my $noto_sans_redhat = "google-noto-sans-cjk-ttc-fonts";
416 $rel = $1 if ($system_release =~ /(release|Linux)\s+(\d+)/);
417
418 if (!($system_release =~ /Fedora/)) {
419 $map{"virtualenv"} = "python-virtualenv";
420
421 if ($rel && $rel < 8) {
422 $old = 1;
423 $pdf = 0;
424
425 printf("Note: texlive packages on RHEL/CENTOS <= 7 are incomplete. Can't support PDF output\n");
426 printf("If you want to build PDF, please read:\n");
427 printf("\thttps://www.systutorials.com/241660/how-to-install-tex-live-on-centos-7-linux/\n");
428 }
429 } else {
430 if ($rel && $rel < 26) {
431 $old = 1;
432 }
433 if ($rel && $rel >= 38) {
434 $noto_sans_redhat = "google-noto-sans-cjk-fonts";
435 }
436 }
437 if (!$rel) {
438 printf("Couldn't identify release number\n");
439 $old = 1;
440 $pdf = 0;
441 }
442
443 if ($pdf) {
444 check_missing_file(["/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
445 "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc"],
446 $noto_sans_redhat, 2);
447 }
448
449 check_rpm_missing(\@fedora26_opt_pkgs, 2) if ($pdf && !$old);
450 check_rpm_missing(\@fedora_tex_pkgs, 2) if ($pdf);
451 check_missing_tex(2) if ($pdf);
452 check_missing(\%map);
453
454 return if (!$need && !$optional);
455
456 if (!$old) {
457 # dnf, for Fedora 18+
458 printf("You should run:\n") if ($verbose_warn_install);
459 printf("\n\tsudo dnf install -y $install\n");
460 } else {
461 # yum, for RHEL (and clones) or Fedora version < 18
462 printf("You should run:\n") if ($verbose_warn_install);
463 printf("\n\tsudo yum install -y $install\n");
464 }
465}
466
467sub give_opensuse_hints()
468{
469 my %map = (
470 "python-sphinx" => "python3-sphinx",
471 "yaml" => "python3-pyyaml",
472 "virtualenv" => "python3-virtualenv",
473 "dot" => "graphviz",
474 "convert" => "ImageMagick",
475 "Pod::Usage" => "perl-Pod-Usage",
476 "xelatex" => "texlive-xetex-bin",
477 );
478
479 # On Tumbleweed, this package is also named rsvg-convert
480 $map{"rsvg-convert"} = "rsvg-view" if (!($system_release =~ /Tumbleweed/));
481
482 my @suse_tex_pkgs = (
483 "texlive-babel-english",
484 "texlive-caption",
485 "texlive-colortbl",
486 "texlive-courier",
487 "texlive-dvips",
488 "texlive-helvetic",
489 "texlive-makeindex",
490 "texlive-metafont",
491 "texlive-metapost",
492 "texlive-palatino",
493 "texlive-preview",
494 "texlive-times",
495 "texlive-zapfchan",
496 "texlive-zapfding",
497 );
498
499 $map{"latexmk"} = "texlive-latexmk-bin";
500
501 # FIXME: add support for installing CJK fonts
502 #
503 # I tried hard, but was unable to find a way to install
504 # "Noto Sans CJK SC" on openSUSE
505
506 check_rpm_missing(\@suse_tex_pkgs, 2) if ($pdf);
507 check_missing_tex(2) if ($pdf);
508 check_missing(\%map);
509
510 return if (!$need && !$optional);
511 printf("You should run:\n") if ($verbose_warn_install);
512 printf("\n\tsudo zypper install --no-recommends $install\n");
513}
514
515sub give_mageia_hints()
516{
517 my %map = (
518 "python-sphinx" => "python3-sphinx",
519 "yaml" => "python3-yaml",
520 "virtualenv" => "python3-virtualenv",
521 "dot" => "graphviz",
522 "convert" => "ImageMagick",
523 "Pod::Usage" => "perl-Pod-Usage",
524 "xelatex" => "texlive",
525 "rsvg-convert" => "librsvg2",
526 );
527
528 my @tex_pkgs = (
529 "texlive-fontsextra",
530 );
531
532 $map{"latexmk"} = "texlive-collection-basic";
533
534 my $packager_cmd;
535 my $noto_sans;
536 if ($system_release =~ /OpenMandriva/) {
537 $packager_cmd = "dnf install";
538 $noto_sans = "noto-sans-cjk-fonts";
539 @tex_pkgs = ( "texlive-collection-fontsextra" );
540 } else {
541 $packager_cmd = "urpmi";
542 $noto_sans = "google-noto-sans-cjk-ttc-fonts";
543 }
544
545
546 if ($pdf) {
547 check_missing_file(["/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
548 "/usr/share/fonts/TTF/NotoSans-Regular.ttf"],
549 $noto_sans, 2);
550 }
551
552 check_rpm_missing(\@tex_pkgs, 2) if ($pdf);
553 check_missing(\%map);
554
555 return if (!$need && !$optional);
556 printf("You should run:\n") if ($verbose_warn_install);
557 printf("\n\tsudo $packager_cmd $install\n");
558}
559
560sub give_arch_linux_hints()
561{
562 my %map = (
563 "yaml" => "python-yaml",
564 "virtualenv" => "python-virtualenv",
565 "dot" => "graphviz",
566 "convert" => "imagemagick",
567 "xelatex" => "texlive-xetex",
568 "latexmk" => "texlive-core",
569 "rsvg-convert" => "extra/librsvg",
570 );
571
572 my @archlinux_tex_pkgs = (
573 "texlive-core",
574 "texlive-latexextra",
575 "ttf-dejavu",
576 );
577 check_pacman_missing(\@archlinux_tex_pkgs, 2) if ($pdf);
578
579 if ($pdf) {
580 check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"],
581 "noto-fonts-cjk", 2);
582 }
583
584 check_missing(\%map);
585
586 return if (!$need && !$optional);
587 printf("You should run:\n") if ($verbose_warn_install);
588 printf("\n\tsudo pacman -S $install\n");
589}
590
591sub give_gentoo_hints()
592{
593 my %map = (
594 "yaml" => "dev-python/pyyaml",
595 "virtualenv" => "dev-python/virtualenv",
596 "dot" => "media-gfx/graphviz",
597 "convert" => "media-gfx/imagemagick",
598 "xelatex" => "dev-texlive/texlive-xetex media-fonts/dejavu",
599 "rsvg-convert" => "gnome-base/librsvg",
600 );
601
602 check_missing_file(["/usr/share/fonts/dejavu/DejaVuSans.ttf"],
603 "media-fonts/dejavu", 2) if ($pdf);
604
605 if ($pdf) {
606 check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
607 "/usr/share/fonts/noto-cjk/NotoSerifCJK-Regular.ttc"],
608 "media-fonts/noto-cjk", 2);
609 }
610
611 check_missing(\%map);
612
613 return if (!$need && !$optional);
614
615 printf("You should run:\n") if ($verbose_warn_install);
616 printf("\n");
617
618 my $imagemagick = "media-gfx/imagemagick svg png";
619 my $cairo = "media-gfx/graphviz cairo pdf";
620 my $portage_imagemagick = "/etc/portage/package.use/imagemagick";
621 my $portage_cairo = "/etc/portage/package.use/graphviz";
622
623 if (qx(grep imagemagick $portage_imagemagick 2>/dev/null) eq "") {
624 printf("\tsudo su -c 'echo \"$imagemagick\" > $portage_imagemagick'\n")
625 }
626 if (qx(grep graphviz $portage_cairo 2>/dev/null) eq "") {
627 printf("\tsudo su -c 'echo \"$cairo\" > $portage_cairo'\n");
628 }
629
630 printf("\tsudo emerge --ask $install\n");
631
632}
633
634sub check_distros()
635{
636 # Distro-specific hints
637 if ($system_release =~ /Red Hat Enterprise Linux/) {
638 give_redhat_hints;
639 return;
640 }
641 if ($system_release =~ /CentOS/) {
642 give_redhat_hints;
643 return;
644 }
645 if ($system_release =~ /Scientific Linux/) {
646 give_redhat_hints;
647 return;
648 }
649 if ($system_release =~ /Oracle Linux Server/) {
650 give_redhat_hints;
651 return;
652 }
653 if ($system_release =~ /Fedora/) {
654 give_redhat_hints;
655 return;
656 }
657 if ($system_release =~ /Ubuntu/) {
658 give_debian_hints;
659 return;
660 }
661 if ($system_release =~ /Debian/) {
662 give_debian_hints;
663 return;
664 }
665 if ($system_release =~ /openSUSE/) {
666 give_opensuse_hints;
667 return;
668 }
669 if ($system_release =~ /Mageia/) {
670 give_mageia_hints;
671 return;
672 }
673 if ($system_release =~ /OpenMandriva/) {
674 give_mageia_hints;
675 return;
676 }
677 if ($system_release =~ /Arch Linux/) {
678 give_arch_linux_hints;
679 return;
680 }
681 if ($system_release =~ /Gentoo/) {
682 give_gentoo_hints;
683 return;
684 }
685
686 #
687 # Fall-back to generic hint code for other distros
688 # That's far from ideal, specially for LaTeX dependencies.
689 #
690 my %map = (
691 "sphinx-build" => "sphinx"
692 );
693 check_missing_tex(2) if ($pdf);
694 check_missing(\%map);
695 print "I don't know distro $system_release.\n";
696 print "So, I can't provide you a hint with the install procedure.\n";
697 print "There are likely missing dependencies.\n";
698}
699
700#
701# Common dependencies
702#4#
5# pylint: disable=C0103,C0114,C0115,C0116,C0301,C0302
6# pylint: disable=R0902,R0904,R0911,R0912,R0914,R0915,R1705,R1710,E1121
7
8# Note: this script requires at least Python 3.6 to run.
9# Don't add changes not compatible with it, it is meant to report
10# incompatible python versions.
11
12"""
13Dependency checker for Sphinx documentation Kernel build.
14
15This module provides tools to check for all required dependencies needed to
16build documentation using Sphinx, including system packages, Python modules
17and LaTeX packages for PDF generation.
18
19It detect packages for a subset of Linux distributions used by Kernel
20maintainers, showing hints and missing dependencies.
21
22The main class SphinxDependencyChecker handles the dependency checking logic
23and provides recommendations for installing missing packages. It supports both
24system package installations and Python virtual environments. By default,
25system pacage install is recommended.
26"""
27
28import argparse
29import os
30import re
31import subprocess
32import sys
33from glob import glob
34
35
36def parse_version(version):
37 """Convert a major.minor.patch version into a tuple"""
38 return tuple(int(x) for x in version.split("."))
39
40
41def ver_str(version):
42 """Returns a version tuple as major.minor.patch"""
43
44 return ".".join([str(x) for x in version])
45
46
47RECOMMENDED_VERSION = parse_version("3.4.3")
48MIN_PYTHON_VERSION = parse_version("3.7")
49
50
51class DepManager:
52 """
53 Manage package dependencies. There are three types of dependencies:
54
55 - System: dependencies required for docs build;
56 - Python: python dependencies for a native distro Sphinx install;
57 - PDF: dependencies needed by PDF builds.
58
59 Each dependency can be mandatory or optional. Not installing an optional
60 dependency won't break the build, but will cause degradation at the
61 docs output.
62 """
63
64 # Internal types of dependencies. Don't use them outside DepManager class.
65 _SYS_TYPE = 0
66 _PHY_TYPE = 1
67 _PDF_TYPE = 2
68
69 # Dependencies visible outside the class.
70 # The keys are tuple with: (type, is_mandatory flag).
71 #
72 # Currently we're not using all optional dep types. Yet, we'll keep all
73 # possible combinations here. They're not many, and that makes easier
74 # if later needed and for the name() method below
75
76 SYSTEM_MANDATORY = (_SYS_TYPE, True)
77 PYTHON_MANDATORY = (_PHY_TYPE, True)
78 PDF_MANDATORY = (_PDF_TYPE, True)
79
80 SYSTEM_OPTIONAL = (_SYS_TYPE, False)
81 PYTHON_OPTIONAL = (_PHY_TYPE, False)
82 PDF_OPTIONAL = (_PDF_TYPE, True)
83
84 def __init__(self, pdf):
85 """
86 Initialize internal vars:
87
88 - missing: missing dependencies list, containing a distro-independent
89 name for a missing dependency and its type.
90 - missing_pkg: ancillary dict containing missing dependencies in
91 distro namespace, organized by type.
92 - need: total number of needed dependencies. Never cleaned.
93 - optional: total number of optional dependencies. Never cleaned.
94 - pdf: Is PDF support enabled?
95 """
96 self.missing = {}
97 self.missing_pkg = {}
98 self.need = 0
99 self.optional = 0
100 self.pdf = pdf
101
102 @staticmethod
103 def name(dtype):
104 """
105 Ancillary routine to output a warn/error message reporting
106 missing dependencies.
107 """
108 if dtype[0] == DepManager._SYS_TYPE:
109 msg = "build"
110 elif dtype[0] == DepManager._PHY_TYPE:
111 msg = "Python"
112 else:
113 msg = "PDF"
114
115 if dtype[1]:
116 return f"ERROR: {msg} mandatory deps missing"
117 else:
118 return f"Warning: {msg} optional deps missing"
119
120 @staticmethod
121 def is_optional(dtype):
122 """Ancillary routine to report if a dependency is optional"""
123 return not dtype[1]
124
125 @staticmethod
126 def is_pdf(dtype):
127 """Ancillary routine to report if a dependency is for PDF generation"""
128 if dtype[0] == DepManager._PDF_TYPE:
129 return True
130
131 return False
132
133 def add_package(self, package, dtype):
134 """
135 Add a package at the self.missing() dictionary.
136 Doesn't update missing_pkg.
137 """
138 is_optional = DepManager.is_optional(dtype)
139 self.missing[package] = dtype
140 if is_optional:
141 self.optional += 1
142 else:
143 self.need += 1
144
145 def del_package(self, package):
146 """
147 Remove a package at the self.missing() dictionary.
148 Doesn't update missing_pkg.
149 """
150 if package in self.missing:
151 del self.missing[package]
152
153 def clear_deps(self):
154 """
155 Clear dependencies without changing needed/optional.
156
157 This is an ackward way to have a separate section to recommend
158 a package after system main dependencies.
159
160 TODO: rework the logic to prevent needing it.
161 """
162
163 self.missing = {}
164 self.missing_pkg = {}
165
166 def check_missing(self, progs):
167 """
168 Update self.missing_pkg, using progs dict to convert from the
169 agnostic package name to distro-specific one.
170
171 Returns an string with the packages to be installed, sorted and
172 with eventual duplicates removed.
173 """
174
175 self.missing_pkg = {}
176
177 for prog, dtype in sorted(self.missing.items()):
178 # At least on some LTS distros like CentOS 7, texlive doesn't
179 # provide all packages we need. When such distros are
180 # detected, we have to disable PDF output.
181 #
182 # So, we need to ignore the packages that distros would
183 # need for LaTeX to work
184 if DepManager.is_pdf(dtype) and not self.pdf:
185 self.optional -= 1
186 continue
187
188 if not dtype in self.missing_pkg:
189 self.missing_pkg[dtype] = []
190
191 self.missing_pkg[dtype].append(progs.get(prog, prog))
192
193 install = []
194 for dtype, pkgs in self.missing_pkg.items():
195 install += pkgs
196
197 return " ".join(sorted(set(install)))
198
199 def warn_install(self):
200 """
201 Emit warnings/errors related to missing packages.
202 """
203
204 output_msg = ""
205
206 for dtype in sorted(self.missing_pkg.keys()):
207 progs = " ".join(sorted(set(self.missing_pkg[dtype])))
208
209 try:
210 name = DepManager.name(dtype)
211 output_msg += f'{name}:\t{progs}\n'
212 except KeyError:
213 raise KeyError(f"ERROR!!!: invalid dtype for {progs}: {dtype}")
214
215 if output_msg:
216 print(f"\n{output_msg}")
217
218class AncillaryMethods:
219 """
220 Ancillary methods that checks for missing dependencies for different
221 types of types, like binaries, python modules, rpm deps, etc.
222 """
223
224 @staticmethod
225 def which(prog):
226 """
227 Our own implementation of which(). We could instead use
228 shutil.which(), but this function is simple enough.
229 Probably faster to use this implementation than to import shutil.
230 """
231 for path in os.environ.get("PATH", "").split(":"):
232 full_path = os.path.join(path, prog)
233 if os.access(full_path, os.X_OK):
234 return full_path
235
236 return None
237
238 @staticmethod
239 def get_python_version(cmd):
240 """
241 Get python version from a Python binary. As we need to detect if
242 are out there newer python binaries, we can't rely on sys.release here.
243 """
244
245 result = SphinxDependencyChecker.run([cmd, "--version"],
246 capture_output=True, text=True)
247 version = result.stdout.strip()
248
249 match = re.search(r"(\d+\.\d+\.\d+)", version)
250 if match:
251 return parse_version(match.group(1))
252
253 print(f"Can't parse version {version}")
254 return (0, 0, 0)
255
256 @staticmethod
257 def find_python():
258 """
259 Detect if are out there any python 3.xy version newer than the
260 current one.
261
262 Note: this routine is limited to up to 2 digits for python3. We
263 may need to update it one day, hopefully on a distant future.
264 """
265 patterns = [
266 "python3.[0-9]",
267 "python3.[0-9][0-9]",
268 ]
269
270 # Seek for a python binary newer than MIN_PYTHON_VERSION
271 for path in os.getenv("PATH", "").split(":"):
272 for pattern in patterns:
273 for cmd in glob(os.path.join(path, pattern)):
274 if os.path.isfile(cmd) and os.access(cmd, os.X_OK):
275 version = SphinxDependencyChecker.get_python_version(cmd)
276 if version >= MIN_PYTHON_VERSION:
277 return cmd
278
279 @staticmethod
280 def check_python():
281 """
282 Check if the current python binary satisfies our minimal requirement
283 for Sphinx build. If not, re-run with a newer version if found.
284 """
285 cur_ver = sys.version_info[:3]
286 if cur_ver >= MIN_PYTHON_VERSION:
287 ver = ver_str(cur_ver)
288 print(f"Python version: {ver}")
289
290 # This could be useful for debugging purposes
291 if SphinxDependencyChecker.which("docutils"):
292 result = SphinxDependencyChecker.run(["docutils", "--version"],
293 capture_output=True, text=True)
294 ver = result.stdout.strip()
295 match = re.search(r"(\d+\.\d+\.\d+)", ver)
296 if match:
297 ver = match.group(1)
298
299 print(f"Docutils version: {ver}")
300
301 return
302
303 python_ver = ver_str(cur_ver)
304
305 new_python_cmd = SphinxDependencyChecker.find_python()
306 if not new_python_cmd:
307 print(f"ERROR: Python version {python_ver} is not spported anymore\n")
308 print(" Can't find a new version. This script may fail")
309 return
310
311 # Restart script using the newer version
312 script_path = os.path.abspath(sys.argv[0])
313 args = [new_python_cmd, script_path] + sys.argv[1:]
314
315 print(f"Python {python_ver} not supported. Changing to {new_python_cmd}")
316
317 try:
318 os.execv(new_python_cmd, args)
319 except OSError as e:
320 sys.exit(f"Failed to restart with {new_python_cmd}: {e}")
321
322 @staticmethod
323 def run(*args, **kwargs):
324 """
325 Excecute a command, hiding its output by default.
326 Preserve comatibility with older Python versions.
327 """
328
329 capture_output = kwargs.pop('capture_output', False)
330
331 if capture_output:
332 if 'stdout' not in kwargs:
333 kwargs['stdout'] = subprocess.PIPE
334 if 'stderr' not in kwargs:
335 kwargs['stderr'] = subprocess.PIPE
336 else:
337 if 'stdout' not in kwargs:
338 kwargs['stdout'] = subprocess.DEVNULL
339 if 'stderr' not in kwargs:
340 kwargs['stderr'] = subprocess.DEVNULL
341
342 # Don't break with older Python versions
343 if 'text' in kwargs and sys.version_info < (3, 7):
344 kwargs['universal_newlines'] = kwargs.pop('text')
345
346 return subprocess.run(*args, **kwargs)
347
348class MissingCheckers(AncillaryMethods):
349 """
350 Contains some ancillary checkers for different types of binaries and
351 package managers.
352 """
353
354 def __init__(self, args, texlive):
355 """
356 Initialize its internal variables
357 """
358 self.pdf = args.pdf
359 self.virtualenv = args.virtualenv
360 self.version_check = args.version_check
361 self.texlive = texlive
362
363 self.min_version = (0, 0, 0)
364 self.cur_version = (0, 0, 0)
365
366 self.deps = DepManager(self.pdf)
367
368 self.need_symlink = 0
369 self.need_sphinx = 0
370
371 self.verbose_warn_install = 1
372
373 self.virtenv_dir = ""
374 self.install = ""
375 self.python_cmd = ""
376
377 self.virtenv_prefix = ["sphinx_", "Sphinx_" ]
378
379 def check_missing_file(self, files, package, dtype):
380 """
381 Does the file exists? If not, add it to missing dependencies.
382 """
383 for f in files:
384 if os.path.exists(f):
385 return
386 self.deps.add_package(package, dtype)
387
388 def check_program(self, prog, dtype):
389 """
390 Does the program exists and it is at the PATH?
391 If not, add it to missing dependencies.
392 """
393 found = self.which(prog)
394 if found:
395 return found
396
397 self.deps.add_package(prog, dtype)
398
399 return None
400
401 def check_perl_module(self, prog, dtype):
402 """
403 Does perl have a dependency? Is it available?
404 If not, add it to missing dependencies.
405
406 Right now, we still need Perl for doc build, as it is required
407 by some tools called at docs or kernel build time, like:
408
409 scripts/documentation-file-ref-check
410
411 Also, checkpatch is on Perl.
412 """
413
414 # While testing with lxc download template, one of the
415 # distros (Oracle) didn't have perl - nor even an option to install
416 # before installing oraclelinux-release-el9 package.
417 #
418 # Check it before running an error. If perl is not there,
419 # add it as a mandatory package, as some parts of the doc builder
420 # needs it.
421 if not self.which("perl"):
422 self.deps.add_package("perl", DepManager.SYSTEM_MANDATORY)
423 self.deps.add_package(prog, dtype)
424 return
425
426 try:
427 self.run(["perl", f"-M{prog}", "-e", "1"], check=True)
428 except subprocess.CalledProcessError:
429 self.deps.add_package(prog, dtype)
430
431 def check_python_module(self, module, is_optional=False):
432 """
433 Does a python module exists outside venv? If not, add it to missing
434 dependencies.
435 """
436 if is_optional:
437 dtype = DepManager.PYTHON_OPTIONAL
438 else:
439 dtype = DepManager.PYTHON_MANDATORY
440
441 try:
442 self.run([self.python_cmd, "-c", f"import {module}"], check=True)
443 except subprocess.CalledProcessError:
444 self.deps.add_package(module, dtype)
445
446 def check_rpm_missing(self, pkgs, dtype):
447 """
448 Does a rpm package exists? If not, add it to missing dependencies.
449 """
450 for prog in pkgs:
451 try:
452 self.run(["rpm", "-q", prog], check=True)
453 except subprocess.CalledProcessError:
454 self.deps.add_package(prog, dtype)
455
456 def check_pacman_missing(self, pkgs, dtype):
457 """
458 Does a pacman package exists? If not, add it to missing dependencies.
459 """
460 for prog in pkgs:
461 try:
462 self.run(["pacman", "-Q", prog], check=True)
463 except subprocess.CalledProcessError:
464 self.deps.add_package(prog, dtype)
465
466 def check_missing_tex(self, is_optional=False):
467 """
468 Does a LaTeX package exists? If not, add it to missing dependencies.
469 """
470 if is_optional:
471 dtype = DepManager.PDF_OPTIONAL
472 else:
473 dtype = DepManager.PDF_MANDATORY
474
475 kpsewhich = self.which("kpsewhich")
476 for prog, package in self.texlive.items():
477
478 # If kpsewhich is not there, just add it to deps
479 if not kpsewhich:
480 self.deps.add_package(package, dtype)
481 continue
482
483 # Check if the package is needed
484 try:
485 result = self.run(
486 [kpsewhich, prog], stdout=subprocess.PIPE, text=True, check=True
487 )
488
489 # Didn't find. Add it
490 if not result.stdout.strip():
491 self.deps.add_package(package, dtype)
492
493 except subprocess.CalledProcessError:
494 # kpsewhich returned an error. Add it, just in case
495 self.deps.add_package(package, dtype)
496
497 def get_sphinx_fname(self):
498 """
499 Gets the binary filename for sphinx-build.
500 """
501 if "SPHINXBUILD" in os.environ:
502 return os.environ["SPHINXBUILD"]
503
504 fname = "sphinx-build"
505 if self.which(fname):
506 return fname
507
508 fname = "sphinx-build-3"
509 if self.which(fname):
510 self.need_symlink = 1
511 return fname
512
513 return ""
514
515 def get_sphinx_version(self, cmd):
516 """
517 Gets sphinx-build version.
518 """
519 try:
520 result = self.run([cmd, "--version"],
521 stdout=subprocess.PIPE,
522 stderr=subprocess.STDOUT,
523 text=True, check=True)
524 except (subprocess.CalledProcessError, FileNotFoundError):
525 return None
526
527 for line in result.stdout.split("\n"):
528 match = re.match(r"^sphinx-build\s+([\d\.]+)(?:\+(?:/[\da-f]+)|b\d+)?\s*$", line)
529 if match:
530 return parse_version(match.group(1))
531
532 match = re.match(r"^Sphinx.*\s+([\d\.]+)\s*$", line)
533 if match:
534 return parse_version(match.group(1))
535
536 def check_sphinx(self, conf):
537 """
538 Checks Sphinx minimal requirements
539 """
540 try:
541 with open(conf, "r", encoding="utf-8") as f:
542 for line in f:
543 match = re.match(r"^\s*needs_sphinx\s*=\s*[\'\"]([\d\.]+)[\'\"]", line)
544 if match:
545 self.min_version = parse_version(match.group(1))
546 break
547 except IOError:
548 sys.exit(f"Can't open {conf}")
549
550 if not self.min_version:
551 sys.exit(f"Can't get needs_sphinx version from {conf}")
552
553 self.virtenv_dir = self.virtenv_prefix[0] + "latest"
554
555 sphinx = self.get_sphinx_fname()
556 if not sphinx:
557 self.need_sphinx = 1
558 return
559
560 self.cur_version = self.get_sphinx_version(sphinx)
561 if not self.cur_version:
562 sys.exit(f"{sphinx} didn't return its version")
563
564 if self.cur_version < self.min_version:
565 curver = ver_str(self.cur_version)
566 minver = ver_str(self.min_version)
567
568 print(f"ERROR: Sphinx version is {curver}. It should be >= {minver}")
569 self.need_sphinx = 1
570 return
571
572 # On version check mode, just assume Sphinx has all mandatory deps
573 if self.version_check and self.cur_version >= RECOMMENDED_VERSION:
574 sys.exit(0)
575
576 def catcheck(self, filename):
577 """
578 Reads a file if it exists, returning as string.
579 If not found, returns an empty string.
580 """
581 if os.path.exists(filename):
582 with open(filename, "r", encoding="utf-8") as f:
583 return f.read().strip()
584 return ""
585
586 def get_system_release(self):
587 """
588 Determine the system type. There's no unique way that would work
589 with all distros with a minimal package install. So, several
590 methods are used here.
591
592 By default, it will use lsb_release function. If not available, it will
593 fail back to reading the known different places where the distro name
594 is stored.
595
596 Several modern distros now have /etc/os-release, which usually have
597 a decent coverage.
598 """
599
600 system_release = ""
601
602 if self.which("lsb_release"):
603 result = self.run(["lsb_release", "-d"], capture_output=True, text=True)
604 system_release = result.stdout.replace("Description:", "").strip()
605
606 release_files = [
607 "/etc/system-release",
608 "/etc/redhat-release",
609 "/etc/lsb-release",
610 "/etc/gentoo-release",
611 ]
612
613 if not system_release:
614 for f in release_files:
615 system_release = self.catcheck(f)
616 if system_release:
617 break
618
619 # This seems more common than LSB these days
620 if not system_release:
621 os_var = {}
622 try:
623 with open("/etc/os-release", "r", encoding="utf-8") as f:
624 for line in f:
625 match = re.match(r"^([\w\d\_]+)=\"?([^\"]*)\"?\n", line)
626 if match:
627 os_var[match.group(1)] = match.group(2)
628
629 system_release = os_var.get("NAME", "")
630 if "VERSION_ID" in os_var:
631 system_release += " " + os_var["VERSION_ID"]
632 elif "VERSION" in os_var:
633 system_release += " " + os_var["VERSION"]
634 except IOError:
635 pass
636
637 if not system_release:
638 system_release = self.catcheck("/etc/issue")
639
640 system_release = system_release.strip()
641
642 return system_release
643
644class SphinxDependencyChecker(MissingCheckers):
645 """
646 Main class for checking Sphinx documentation build dependencies.
647
648 - Check for missing system packages;
649 - Check for missing Python modules;
650 - Check for missing LaTeX packages needed by PDF generation;
651 - Propose Sphinx install via Python Virtual environment;
652 - Propose Sphinx install via distro-specific package install.
653 """
654 def __init__(self, args):
655 """Initialize checker variables"""
656
657 # List of required texlive packages on Fedora and OpenSuse
658 texlive = {
659 "amsfonts.sty": "texlive-amsfonts",
660 "amsmath.sty": "texlive-amsmath",
661 "amssymb.sty": "texlive-amsfonts",
662 "amsthm.sty": "texlive-amscls",
663 "anyfontsize.sty": "texlive-anyfontsize",
664 "atbegshi.sty": "texlive-oberdiek",
665 "bm.sty": "texlive-tools",
666 "capt-of.sty": "texlive-capt-of",
667 "cmap.sty": "texlive-cmap",
668 "ctexhook.sty": "texlive-ctex",
669 "ecrm1000.tfm": "texlive-ec",
670 "eqparbox.sty": "texlive-eqparbox",
671 "eu1enc.def": "texlive-euenc",
672 "fancybox.sty": "texlive-fancybox",
673 "fancyvrb.sty": "texlive-fancyvrb",
674 "float.sty": "texlive-float",
675 "fncychap.sty": "texlive-fncychap",
676 "footnote.sty": "texlive-mdwtools",
677 "framed.sty": "texlive-framed",
678 "luatex85.sty": "texlive-luatex85",
679 "multirow.sty": "texlive-multirow",
680 "needspace.sty": "texlive-needspace",
681 "palatino.sty": "texlive-psnfss",
682 "parskip.sty": "texlive-parskip",
683 "polyglossia.sty": "texlive-polyglossia",
684 "tabulary.sty": "texlive-tabulary",
685 "threeparttable.sty": "texlive-threeparttable",
686 "titlesec.sty": "texlive-titlesec",
687 "ucs.sty": "texlive-ucs",
688 "upquote.sty": "texlive-upquote",
689 "wrapfig.sty": "texlive-wrapfig",
690 }
691
692 super().__init__(args, texlive)
693
694 self.need_pip = False
695 self.rec_sphinx_upgrade = 0
696
697 self.system_release = self.get_system_release()
698 self.activate_cmd = ""
699
700 # Some distros may not have a Sphinx shipped package compatible with
701 # our minimal requirements
702 self.package_supported = True
703
704 # Recommend a new python version
705 self.recommend_python = None
706
707 # Certain hints are meant to be shown only once
708 self.distro_msg = None
709
710 self.latest_avail_ver = (0, 0, 0)
711 self.venv_ver = (0, 0, 0)
712
713 prefix = os.environ.get("srctree", ".") + "/"
714
715 self.conf = prefix + "Documentation/conf.py"
716 self.requirement_file = prefix + "Documentation/sphinx/requirements.txt"
717
718 def get_install_progs(self, progs, cmd, extra=None):
719 """
720 Check for missing dependencies using the provided program mapping.
721
722 The actual distro-specific programs are mapped via progs argument.
723 """
724 install = self.deps.check_missing(progs)
725
726 if self.verbose_warn_install:
727 self.deps.warn_install()
728
729 if not install:
730 return
731
732 if cmd:
733 if self.verbose_warn_install:
734 msg = "You should run:"
735 else:
736 msg = ""
737
738 if extra:
739 msg += "\n\t" + extra.replace("\n", "\n\t")
740
741 return(msg + "\n\tsudo " + cmd + " " + install)
742
743 return None
744
745 #
746 # Distro-specific hints methods
747 #
748
749 def give_debian_hints(self):
750 """
751 Provide package installation hints for Debian-based distros.
752 """
753 progs = {
754 "Pod::Usage": "perl-modules",
755 "convert": "imagemagick",
756 "dot": "graphviz",
757 "ensurepip": "python3-venv",
758 "python-sphinx": "python3-sphinx",
759 "rsvg-convert": "librsvg2-bin",
760 "virtualenv": "virtualenv",
761 "xelatex": "texlive-xetex",
762 "yaml": "python3-yaml",
763 }
764
765 if self.pdf:
766 pdf_pkgs = {
767 "fonts-dejavu": [
768 "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf",
769 ],
770 "fonts-noto-cjk": [
771 "/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc",
772 "/usr/share/fonts/opentype/noto/NotoSansCJK-Regular.ttc",
773 "/usr/share/fonts/opentype/noto/NotoSerifCJK-Regular.ttc",
774 ],
775 "tex-gyre": [
776 "/usr/share/texmf/tex/latex/tex-gyre/tgtermes.sty"
777 ],
778 "texlive-fonts-recommended": [
779 "/usr/share/texlive/texmf-dist/fonts/tfm/adobe/zapfding/pzdr.tfm",
780 ],
781 "texlive-lang-chinese": [
782 "/usr/share/texlive/texmf-dist/tex/latex/ctex/ctexhook.sty",
783 ],
784 }
785
786 for package, files in pdf_pkgs.items():
787 self.check_missing_file(files, package, DepManager.PDF_MANDATORY)
788
789 self.check_program("dvipng", DepManager.PDF_MANDATORY)
790
791 if not self.distro_msg:
792 self.distro_msg = \
793 "Note: ImageMagick is broken on some distros, affecting PDF output. For more details:\n" \
794 "\thttps://askubuntu.com/questions/1158894/imagemagick-still-broken-using-with-usr-bin-convert"
795
796 return self.get_install_progs(progs, "apt-get install")
797
798 def give_redhat_hints(self):
799 """
800 Provide package installation hints for RedHat-based distros
801 (Fedora, RHEL and RHEL-based variants).
802 """
803 progs = {
804 "Pod::Usage": "perl-Pod-Usage",
805 "convert": "ImageMagick",
806 "dot": "graphviz",
807 "python-sphinx": "python3-sphinx",
808 "rsvg-convert": "librsvg2-tools",
809 "virtualenv": "python3-virtualenv",
810 "xelatex": "texlive-xetex-bin",
811 "yaml": "python3-pyyaml",
812 }
813
814 fedora_tex_pkgs = [
815 "dejavu-sans-fonts",
816 "dejavu-sans-mono-fonts",
817 "dejavu-serif-fonts",
818 "texlive-collection-fontsrecommended",
819 "texlive-collection-latex",
820 "texlive-xecjk",
821 ]
822
823 fedora = False
824 rel = None
825
826 match = re.search(r"(release|Linux)\s+(\d+)", self.system_release)
827 if match:
828 rel = int(match.group(2))
829
830 if not rel:
831 print("Couldn't identify release number")
832 noto_sans_redhat = None
833 self.pdf = False
834 elif re.search("Fedora", self.system_release):
835 # Fedora 38 and upper use this CJK font
836
837 noto_sans_redhat = "google-noto-sans-cjk-fonts"
838 fedora = True
839 else:
840 # Almalinux, CentOS, RHEL, ...
841
842 # at least up to version 9 (and Fedora < 38), that's the CJK font
843 noto_sans_redhat = "google-noto-sans-cjk-ttc-fonts"
844
845 progs["virtualenv"] = "python-virtualenv"
846
847 if not rel or rel < 8:
848 print("ERROR: Distro not supported. Too old?")
849 return
850
851 # RHEL 8 uses Python 3.6, which is not compatible with
852 # the build system anymore. Suggest Python 3.11
853 if rel == 8:
854 self.check_program("python3.9", DepManager.SYSTEM_MANDATORY)
855 progs["python3.9"] = "python39"
856 progs["yaml"] = "python39-pyyaml"
857
858 self.recommend_python = True
859
860 # There's no python39-sphinx package. Only pip is supported
861 self.package_supported = False
862
863 if not self.distro_msg:
864 self.distro_msg = \
865 "Note: RHEL-based distros typically require extra repositories.\n" \
866 "For most, enabling epel and crb are enough:\n" \
867 "\tsudo dnf install -y epel-release\n" \
868 "\tsudo dnf config-manager --set-enabled crb\n" \
869 "Yet, some may have other required repositories. Those commands could be useful:\n" \
870 "\tsudo dnf repolist all\n" \
871 "\tsudo dnf repoquery --available --info <pkgs>\n" \
872 "\tsudo dnf config-manager --set-enabled '*' # enable all - probably not what you want"
873
874 if self.pdf:
875 pdf_pkgs = [
876 "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
877 "/usr/share/fonts/google-noto-sans-cjk-fonts/NotoSansCJK-Regular.ttc",
878 ]
879
880 self.check_missing_file(pdf_pkgs, noto_sans_redhat, DepManager.PDF_MANDATORY)
881
882 self.check_rpm_missing(fedora_tex_pkgs, DepManager.PDF_MANDATORY)
883
884 self.check_missing_tex(DepManager.PDF_MANDATORY)
885
886 # There's no texlive-ctex on RHEL 8 repositories. This will
887 # likely affect CJK pdf build only.
888 if not fedora and rel == 8:
889 self.deps.del_package("texlive-ctex")
890
891 return self.get_install_progs(progs, "dnf install")
892
893 def give_opensuse_hints(self):
894 """
895 Provide package installation hints for openSUSE-based distros
896 (Leap and Tumbleweed).
897 """
898 progs = {
899 "Pod::Usage": "perl-Pod-Usage",
900 "convert": "ImageMagick",
901 "dot": "graphviz",
902 "python-sphinx": "python3-sphinx",
903 "virtualenv": "python3-virtualenv",
904 "xelatex": "texlive-xetex-bin texlive-dejavu",
905 "yaml": "python3-pyyaml",
906 }
907
908 suse_tex_pkgs = [
909 "texlive-babel-english",
910 "texlive-caption",
911 "texlive-colortbl",
912 "texlive-courier",
913 "texlive-dvips",
914 "texlive-helvetic",
915 "texlive-makeindex",
916 "texlive-metafont",
917 "texlive-metapost",
918 "texlive-palatino",
919 "texlive-preview",
920 "texlive-times",
921 "texlive-zapfchan",
922 "texlive-zapfding",
923 ]
924
925 progs["latexmk"] = "texlive-latexmk-bin"
926
927 match = re.search(r"(Leap)\s+(\d+).(\d)", self.system_release)
928 if match:
929 rel = int(match.group(2))
930
931 # Leap 15.x uses Python 3.6, which is not compatible with
932 # the build system anymore. Suggest Python 3.11
933 if rel == 15:
934 if not self.which(self.python_cmd):
935 self.check_program("python3.11", DepManager.SYSTEM_MANDATORY)
936 progs["python3.11"] = "python311"
937 self.recommend_python = True
938
939 progs.update({
940 "python-sphinx": "python311-Sphinx python311-Sphinx-latex",
941 "virtualenv": "python311-virtualenv",
942 "yaml": "python311-PyYAML",
943 })
944 else:
945 # Tumbleweed defaults to Python 3.11
946
947 progs.update({
948 "python-sphinx": "python313-Sphinx python313-Sphinx-latex",
949 "virtualenv": "python313-virtualenv",
950 "yaml": "python313-PyYAML",
951 })
952
953 # FIXME: add support for installing CJK fonts
954 #
955 # I tried hard, but was unable to find a way to install
956 # "Noto Sans CJK SC" on openSUSE
957
958 if self.pdf:
959 self.check_rpm_missing(suse_tex_pkgs, DepManager.PDF_MANDATORY)
960 if self.pdf:
961 self.check_missing_tex()
962
963 return self.get_install_progs(progs, "zypper install --no-recommends")
964
965 def give_mageia_hints(self):
966 """
967 Provide package installation hints for Mageia and OpenMandriva.
968 """
969 progs = {
970 "Pod::Usage": "perl-Pod-Usage",
971 "convert": "ImageMagick",
972 "dot": "graphviz",
973 "python-sphinx": "python3-sphinx",
974 "rsvg-convert": "librsvg2",
975 "virtualenv": "python3-virtualenv",
976 "xelatex": "texlive",
977 "yaml": "python3-yaml",
978 }
979
980 tex_pkgs = [
981 "texlive-fontsextra",
982 "texlive-fonts-asian",
983 "fonts-ttf-dejavu",
984 ]
985
986 if re.search(r"OpenMandriva", self.system_release):
987 packager_cmd = "dnf install"
988 noto_sans = "noto-sans-cjk-fonts"
989 tex_pkgs = [
990 "texlive-collection-basic",
991 "texlive-collection-langcjk",
992 "texlive-collection-fontsextra",
993 "texlive-collection-fontsrecommended"
994 ]
995
996 # Tested on OpenMandriva Lx 4.3
997 progs["convert"] = "imagemagick"
998 progs["yaml"] = "python-pyyaml"
999 progs["python-virtualenv"] = "python-virtualenv"
1000 progs["python-sphinx"] = "python-sphinx"
1001 progs["xelatex"] = "texlive"
1002
1003 self.check_program("python-virtualenv", DepManager.PYTHON_MANDATORY)
1004
1005 # On my tests with openMandriva LX 4.0 docker image, upgraded
1006 # to 4.3, python-virtualenv package is broken: it is missing
1007 # ensurepip. Without it, the alternative would be to run:
1008 # python3 -m venv --without-pip ~/sphinx_latest, but running
1009 # pip there won't install sphinx at venv.
1010 #
1011 # Add a note about that.
1012
1013 if not self.distro_msg:
1014 self.distro_msg = \
1015 "Notes:\n"\
1016 "1. for venv, ensurepip could be broken, preventing its install method.\n" \
1017 "2. at least on OpenMandriva LX 4.3, texlive packages seem broken"
1018
1019 else:
1020 packager_cmd = "urpmi"
1021 noto_sans = "google-noto-sans-cjk-ttc-fonts"
1022
1023 progs["latexmk"] = "texlive-collection-basic"
1024
1025 if self.pdf:
1026 pdf_pkgs = [
1027 "/usr/share/fonts/google-noto-cjk/NotoSansCJK-Regular.ttc",
1028 "/usr/share/fonts/TTF/NotoSans-Regular.ttf",
1029 ]
1030
1031 self.check_missing_file(pdf_pkgs, noto_sans, DepManager.PDF_MANDATORY)
1032 self.check_rpm_missing(tex_pkgs, DepManager.PDF_MANDATORY)
1033
1034 return self.get_install_progs(progs, packager_cmd)
1035
1036 def give_arch_linux_hints(self):
1037 """
1038 Provide package installation hints for ArchLinux.
1039 """
1040 progs = {
1041 "convert": "imagemagick",
1042 "dot": "graphviz",
1043 "latexmk": "texlive-core",
1044 "rsvg-convert": "extra/librsvg",
1045 "virtualenv": "python-virtualenv",
1046 "xelatex": "texlive-xetex",
1047 "yaml": "python-yaml",
1048 }
1049
1050 archlinux_tex_pkgs = [
1051 "texlive-basic",
1052 "texlive-binextra",
1053 "texlive-core",
1054 "texlive-fontsrecommended",
1055 "texlive-langchinese",
1056 "texlive-langcjk",
1057 "texlive-latexextra",
1058 "ttf-dejavu",
1059 ]
1060
1061 if self.pdf:
1062 self.check_pacman_missing(archlinux_tex_pkgs,
1063 DepManager.PDF_MANDATORY)
1064
1065 self.check_missing_file(["/usr/share/fonts/noto-cjk/NotoSansCJK-Regular.ttc"],
1066 "noto-fonts-cjk",
1067 DepManager.PDF_MANDATORY)
1068
1069
1070 return self.get_install_progs(progs, "pacman -S")
1071
1072 def give_gentoo_hints(self):
1073 """
1074 Provide package installation hints for Gentoo.
1075 """
1076 texlive_deps = [
1077 "dev-texlive/texlive-fontsrecommended",
1078 "dev-texlive/texlive-latexextra",
1079 "dev-texlive/texlive-xetex",
1080 "media-fonts/dejavu",
1081 ]
1082
1083 progs = {
1084 "convert": "media-gfx/imagemagick",
1085 "dot": "media-gfx/graphviz",
1086 "rsvg-convert": "gnome-base/librsvg",
1087 "virtualenv": "dev-python/virtualenv",
1088 "xelatex": " ".join(texlive_deps),
1089 "yaml": "dev-python/pyyaml",
1090 "python-sphinx": "dev-python/sphinx",
1091 }
1092
1093 if self.pdf:
1094 pdf_pkgs = {
1095 "media-fonts/dejavu": [
1096 "/usr/share/fonts/dejavu/DejaVuSans.ttf",
1097 ],
1098 "media-fonts/noto-cjk": [
1099 "/usr/share/fonts/noto-cjk/NotoSansCJKsc-Regular.otf",
1100 "/usr/share/fonts/noto-cjk/NotoSerifCJK-Regular.ttc",
1101 ],
1102 }
1103 for package, files in pdf_pkgs.items():
1104 self.check_missing_file(files, package, DepManager.PDF_MANDATORY)
1105
1106 # Handling dependencies is a nightmare, as Gentoo refuses to emerge
1107 # some packages if there's no package.use file describing them.
1108 # To make it worse, compilation flags shall also be present there
1109 # for some packages. If USE is not perfect, error/warning messages
1110 # like those are shown:
1111 #
1112 # !!! The following binary packages have been ignored due to non matching USE:
1113 #
1114 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_13 qt6 svg
1115 # =media-gfx/graphviz-12.2.1-r1 X pdf python_single_target_python3_12 -python_single_target_python3_13 qt6 svg
1116 # =media-gfx/graphviz-12.2.1-r1 X pdf qt6 svg
1117 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_10 qt6 svg
1118 # =media-gfx/graphviz-12.2.1-r1 X pdf -python_single_target_python3_10 python_single_target_python3_12 -python_single_target_python3_13 qt6 svg
1119 # =media-fonts/noto-cjk-20190416 X
1120 # =app-text/texlive-core-2024-r1 X cjk -xetex
1121 # =app-text/texlive-core-2024-r1 X -xetex
1122 # =app-text/texlive-core-2024-r1 -xetex
1123 # =dev-libs/zziplib-0.13.79-r1 sdl
1124 #
1125 # And will ignore such packages, installing the remaining ones. That
1126 # affects mostly the image extension and PDF generation.
1127
1128 # Package dependencies and the minimal needed args:
1129 portages = {
1130 "graphviz": "media-gfx/graphviz",
1131 "imagemagick": "media-gfx/imagemagick",
1132 "media-libs": "media-libs/harfbuzz icu",
1133 "media-fonts": "media-fonts/noto-cjk",
1134 "texlive": "app-text/texlive-core xetex",
1135 "zziblib": "dev-libs/zziplib sdl",
1136 }
1137
1138 extra_cmds = ""
1139 if not self.distro_msg:
1140 self.distro_msg = "Note: Gentoo requires package.use to be adjusted before emerging packages"
1141
1142 use_base = "/etc/portage/package.use"
1143 files = glob(f"{use_base}/*")
1144
1145 for fname, portage in portages.items():
1146 install = False
1147
1148 while install is False:
1149 if not files:
1150 # No files under package.usage. Install all
1151 install = True
1152 break
1153
1154 args = portage.split(" ")
1155
1156 name = args.pop(0)
1157
1158 cmd = ["grep", "-l", "-E", rf"^{name}\b" ] + files
1159 result = self.run(cmd, stdout=subprocess.PIPE, text=True)
1160 if result.returncode or not result.stdout.strip():
1161 # File containing portage name not found
1162 install = True
1163 break
1164
1165 # Ensure that needed USE flags are present
1166 if args:
1167 match_fname = result.stdout.strip()
1168 with open(match_fname, 'r', encoding='utf8',
1169 errors='backslashreplace') as fp:
1170 for line in fp:
1171 for arg in args:
1172 if arg.startswith("-"):
1173 continue
1174
1175 if not re.search(rf"\s*{arg}\b", line):
1176 # Needed file argument not found
1177 install = True
1178 break
1179
1180 # Everything looks ok, don't install
1181 break
1182
1183 # emit a code to setup missing USE
1184 if install:
1185 extra_cmds += (f"sudo su -c 'echo \"{portage}\" > {use_base}/{fname}'\n")
1186
1187 # Now, we can use emerge and let it respect USE
1188 return self.get_install_progs(progs,
1189 "emerge --ask --changed-use --binpkg-respect-use=y",
1190 extra_cmds)
1191
1192 def get_install(self):
1193 """
1194 OS-specific hints logic. Seeks for a hinter. If found, use it to
1195 provide package-manager specific install commands.
1196
1197 Otherwise, outputs install instructions for the meta-packages.
1198
1199 Returns a string with the command to be executed to install the
1200 the needed packages, if distro found. Otherwise, return just a
1201 list of packages that require installation.
1202 """
1203 os_hints = {
1204 re.compile("Red Hat Enterprise Linux"): self.give_redhat_hints,
1205 re.compile("Fedora"): self.give_redhat_hints,
1206 re.compile("AlmaLinux"): self.give_redhat_hints,
1207 re.compile("Amazon Linux"): self.give_redhat_hints,
1208 re.compile("CentOS"): self.give_redhat_hints,
1209 re.compile("openEuler"): self.give_redhat_hints,
1210 re.compile("Oracle Linux Server"): self.give_redhat_hints,
1211 re.compile("Rocky Linux"): self.give_redhat_hints,
1212 re.compile("Springdale Open Enterprise"): self.give_redhat_hints,
1213
1214 re.compile("Ubuntu"): self.give_debian_hints,
1215 re.compile("Debian"): self.give_debian_hints,
1216 re.compile("Devuan"): self.give_debian_hints,
1217 re.compile("Kali"): self.give_debian_hints,
1218 re.compile("Mint"): self.give_debian_hints,
1219
1220 re.compile("openSUSE"): self.give_opensuse_hints,
1221
1222 re.compile("Mageia"): self.give_mageia_hints,
1223 re.compile("OpenMandriva"): self.give_mageia_hints,
1224
1225 re.compile("Arch Linux"): self.give_arch_linux_hints,
1226 re.compile("Gentoo"): self.give_gentoo_hints,
1227 }
1228
1229 # If the OS is detected, use per-OS hint logic
1230 for regex, os_hint in os_hints.items():
1231 if regex.search(self.system_release):
1232 return os_hint()
1233
1234 #
1235 # Fall-back to generic hint code for other distros
1236 # That's far from ideal, specially for LaTeX dependencies.
1237 #
1238 progs = {"sphinx-build": "sphinx"}
1239 if self.pdf:
1240 self.check_missing_tex()
1241
1242 self.distro_msg = \
1243 f"I don't know distro {self.system_release}.\n" \
1244 "So, I can't provide you a hint with the install procedure.\n" \
1245 "There are likely missing dependencies."
1246
1247 return self.get_install_progs(progs, None)
1248
1249 #
1250 # Common dependencies
1251 #
1252 def deactivate_help(self):
1253 """
1254 Print a helper message to disable a virtual environment.
1255 """
1256
1257 print("\n If you want to exit the virtualenv, you can use:")
1258 print("\tdeactivate")
1259
1260 def get_virtenv(self):
1261 """
1262 Give a hint about how to activate an already-existing virtual
1263 environment containing sphinx-build.
1264
1265 Returns a tuble with (activate_cmd_path, sphinx_version) with
1266 the newest available virtual env.
1267 """
1268
1269 cwd = os.getcwd()
1270
1271 activates = []
1272
1273 # Add all sphinx prefixes with possible version numbers
1274 for p in self.virtenv_prefix:
1275 activates += glob(f"{cwd}/{p}[0-9]*/bin/activate")
1276
1277 activates.sort(reverse=True, key=str.lower)
1278
1279 # Place sphinx_latest first, if it exists
1280 for p in self.virtenv_prefix:
1281 activates = glob(f"{cwd}/{p}*latest/bin/activate") + activates
1282
1283 ver = (0, 0, 0)
1284 for f in activates:
1285 # Discard too old Sphinx virtual environments
1286 match = re.search(r"(\d+)\.(\d+)\.(\d+)", f)
1287 if match:
1288 ver = (int(match.group(1)), int(match.group(2)), int(match.group(3)))
1289
1290 if ver < self.min_version:
1291 continue
1292
1293 sphinx_cmd = f.replace("activate", "sphinx-build")
1294 if not os.path.isfile(sphinx_cmd):
1295 continue
1296
1297 ver = self.get_sphinx_version(sphinx_cmd)
1298
1299 if not ver:
1300 venv_dir = f.replace("/bin/activate", "")
1301 print(f"Warning: virtual environment {venv_dir} is not working.\n" \
1302 "Python version upgrade? Remove it with:\n\n" \
1303 "\trm -rf {venv_dir}\n\n")
1304 else:
1305 if self.need_sphinx and ver >= self.min_version:
1306 return (f, ver)
1307 elif parse_version(ver) > self.cur_version:
1308 return (f, ver)
1309
1310 return ("", ver)
1311
1312 def recommend_sphinx_upgrade(self):
1313 """
1314 Check if Sphinx needs to be upgraded.
1315
1316 Returns a tuple with the higest available Sphinx version if found.
1317 Otherwise, returns None to indicate either that no upgrade is needed
1318 or no venv was found.
1319 """
1320
1321 # Avoid running sphinx-builds from venv if cur_version is good
1322 if self.cur_version and self.cur_version >= RECOMMENDED_VERSION:
1323 self.latest_avail_ver = self.cur_version
1324 return None
1325
1326 # Get the highest version from sphinx_*/bin/sphinx-build and the
1327 # corresponding command to activate the venv/virtenv
1328 self.activate_cmd, self.venv_ver = self.get_virtenv()
1329
1330 # Store the highest version from Sphinx existing virtualenvs
1331 if self.activate_cmd and self.venv_ver > self.cur_version:
1332 self.latest_avail_ver = self.venv_ver
1333 else:
1334 if self.cur_version:
1335 self.latest_avail_ver = self.cur_version
1336 else:
1337 self.latest_avail_ver = (0, 0, 0)
1338
1339 # As we don't know package version of Sphinx, and there's no
1340 # virtual environments, don't check if upgrades are needed
1341 if not self.virtualenv:
1342 if not self.latest_avail_ver:
1343 return None
1344
1345 return self.latest_avail_ver
1346
1347 # Either there are already a virtual env or a new one should be created
1348 self.need_pip = True
1349
1350 if not self.latest_avail_ver:
1351 return None
1352
1353 # Return if the reason is due to an upgrade or not
1354 if self.latest_avail_ver != (0, 0, 0):
1355 if self.latest_avail_ver < RECOMMENDED_VERSION:
1356 self.rec_sphinx_upgrade = 1
1357
1358 return self.latest_avail_ver
1359
1360 def recommend_package(self):
1361 """
1362 Recommend installing Sphinx as a distro-specific package.
1363 """
1364
1365 print("\n2) As a package with:")
1366
1367 old_need = self.deps.need
1368 old_optional = self.deps.optional
1369
1370 self.pdf = False
1371 self.deps.optional = 0
1372 old_verbose = self.verbose_warn_install
1373 self.verbose_warn_install = 0
1374
1375 self.deps.clear_deps()
1376
1377 self.deps.add_package("python-sphinx", DepManager.PYTHON_MANDATORY)
1378
1379 cmd = self.get_install()
1380 if cmd:
1381 print(cmd)
1382
1383 self.deps.need = old_need
1384 self.deps.optional = old_optional
1385 self.verbose_warn_install = old_verbose
1386
1387 def recommend_sphinx_version(self, virtualenv_cmd):
1388 """
1389 Provide recommendations for installing or upgrading Sphinx based
1390 on current version.
1391
1392 The logic here is complex, as it have to deal with different versions:
1393
1394 - minimal supported version;
1395 - minimal PDF version;
1396 - recommended version.
1397
1398 It also needs to work fine with both distro's package and
1399 venv/virtualenv
1400 """
1401
1402 if self.recommend_python:
1403 cur_ver = sys.version_info[:3]
1404 if cur_ver < MIN_PYTHON_VERSION:
1405 print(f"\nPython version {cur_ver} is incompatible with doc build.\n" \
1406 "Please upgrade it and re-run.\n")
1407 return
1408
1409 # Version is OK. Nothing to do.
1410 if self.cur_version != (0, 0, 0) and self.cur_version >= RECOMMENDED_VERSION:
1411 return
1412
1413 if self.latest_avail_ver:
1414 latest_avail_ver = ver_str(self.latest_avail_ver)
1415
1416 if not self.need_sphinx:
1417 # sphinx-build is present and its version is >= $min_version
1418
1419 # only recommend enabling a newer virtenv version if makes sense.
1420 if self.latest_avail_ver and self.latest_avail_ver > self.cur_version:
1421 print(f"\nYou may also use the newer Sphinx version {latest_avail_ver} with:")
1422 if f"{self.virtenv_prefix}" in os.getcwd():
1423 print("\tdeactivate")
1424 print(f"\t. {self.activate_cmd}")
1425 self.deactivate_help()
1426 return
1427
1428 if self.latest_avail_ver and self.latest_avail_ver >= RECOMMENDED_VERSION:
1429 return
1430
1431 if not self.virtualenv:
1432 # No sphinx either via package or via virtenv. As we can't
1433 # Compare the versions here, just return, recommending the
1434 # user to install it from the package distro.
1435 if not self.latest_avail_ver or self.latest_avail_ver == (0, 0, 0):
1436 return
1437
1438 # User doesn't want a virtenv recommendation, but he already
1439 # installed one via virtenv with a newer version.
1440 # So, print commands to enable it
1441 if self.latest_avail_ver > self.cur_version:
1442 print(f"\nYou may also use the Sphinx virtualenv version {latest_avail_ver} with:")
1443 if f"{self.virtenv_prefix}" in os.getcwd():
1444 print("\tdeactivate")
1445 print(f"\t. {self.activate_cmd}")
1446 self.deactivate_help()
1447 return
1448 print("\n")
1449 else:
1450 if self.need_sphinx:
1451 self.deps.need += 1
1452
1453 # Suggest newer versions if current ones are too old
1454 if self.latest_avail_ver and self.latest_avail_ver >= self.min_version:
1455 if self.latest_avail_ver >= RECOMMENDED_VERSION:
1456 print(f"\nNeed to activate Sphinx (version {latest_avail_ver}) on virtualenv with:")
1457 print(f"\t. {self.activate_cmd}")
1458 self.deactivate_help()
1459 return
1460
1461 # Version is above the minimal required one, but may be
1462 # below the recommended one. So, print warnings/notes
1463 if self.latest_avail_ver < RECOMMENDED_VERSION:
1464 print(f"Warning: It is recommended at least Sphinx version {RECOMMENDED_VERSION}.")
1465
1466 # At this point, either it needs Sphinx or upgrade is recommended,
1467 # both via pip
1468
1469 if self.rec_sphinx_upgrade:
1470 if not self.virtualenv:
1471 print("Instead of install/upgrade Python Sphinx pkg, you could use pip/pypi with:\n\n")
1472 else:
1473 print("To upgrade Sphinx, use:\n\n")
1474 else:
1475 print("\nSphinx needs to be installed either:\n1) via pip/pypi with:\n")
1476
1477 if not virtualenv_cmd:
1478 print(" Currently not possible.\n")
1479 print(" Please upgrade Python to a newer version and run this script again")
1480 else:
1481 print(f"\t{virtualenv_cmd} {self.virtenv_dir}")
1482 print(f"\t. {self.virtenv_dir}/bin/activate")
1483 print(f"\tpip install -r {self.requirement_file}")
1484 self.deactivate_help()
1485
1486 if self.package_supported:
1487 self.recommend_package()
1488
1489 print("\n" \
1490 " Please note that Sphinx currentlys produce false-positive\n" \
1491 " warnings when the same name is used for more than one type (functions,\n" \
1492 " structs, enums,...). This is known Sphinx bug. For more details, see:\n" \
1493 "\thttps://github.com/sphinx-doc/sphinx/pull/8313")
1494
1495 def check_needs(self):
1496 """
1497 Main method that checks needed dependencies and provides
1498 recommendations.
1499 """
1500 self.python_cmd = sys.executable
1501
1502 # Check if Sphinx is already accessible from current environment
1503 self.check_sphinx(self.conf)
1504
1505 if self.system_release:
1506 print(f"Detected OS: {self.system_release}.")
1507 else:
1508 print("Unknown OS")
1509 if self.cur_version != (0, 0, 0):
1510 ver = ver_str(self.cur_version)
1511 print(f"Sphinx version: {ver}\n")
1512
1513 # Check the type of virtual env, depending on Python version
1514 virtualenv_cmd = None
1515
1516 if sys.version_info < MIN_PYTHON_VERSION:
1517 min_ver = ver_str(MIN_PYTHON_VERSION)
1518 print(f"ERROR: at least python {min_ver} is required to build the kernel docs")
1519 self.need_sphinx = 1
1520
1521 self.venv_ver = self.recommend_sphinx_upgrade()
1522
1523 if self.need_pip:
1524 if sys.version_info < MIN_PYTHON_VERSION:
1525 self.need_pip = False
1526 print("Warning: python version is not supported.")
1527 else:
1528 virtualenv_cmd = f"{self.python_cmd} -m venv"
1529 self.check_python_module("ensurepip")
1530
1531 # Check for needed programs/tools
1532 self.check_perl_module("Pod::Usage", DepManager.SYSTEM_MANDATORY)
1533
1534 self.check_program("make", DepManager.SYSTEM_MANDATORY)
1535 self.check_program("which", DepManager.SYSTEM_MANDATORY)
1536
1537 self.check_program("dot", DepManager.SYSTEM_OPTIONAL)
1538 self.check_program("convert", DepManager.SYSTEM_OPTIONAL)
1539
1540 self.check_python_module("yaml")
1541
1542 if self.pdf:
1543 self.check_program("xelatex", DepManager.PDF_MANDATORY)
1544 self.check_program("rsvg-convert", DepManager.PDF_MANDATORY)
1545 self.check_program("latexmk", DepManager.PDF_MANDATORY)
1546
1547 # Do distro-specific checks and output distro-install commands
1548 cmd = self.get_install()
1549 if cmd:
1550 print(cmd)
1551
1552 # If distro requires some special instructions, print here.
1553 # Please notice that get_install() needs to be called first.
1554 if self.distro_msg:
1555 print("\n" + self.distro_msg)
1556
1557 if not self.python_cmd:
1558 if self.need == 1:
1559 sys.exit("Can't build as 1 mandatory dependency is missing")
1560 elif self.need:
1561 sys.exit(f"Can't build as {self.need} mandatory dependencies are missing")
1562
1563 # Check if sphinx-build is called sphinx-build-3
1564 if self.need_symlink:
1565 sphinx_path = self.which("sphinx-build-3")
1566 if sphinx_path:
1567 print(f"\tsudo ln -sf {sphinx_path} /usr/bin/sphinx-build\n")
1568
1569 self.recommend_sphinx_version(virtualenv_cmd)
1570 print("")
1571
1572 if not self.deps.optional:
1573 print("All optional dependencies are met.")
1574
1575 if self.deps.need == 1:
1576 sys.exit("Can't build as 1 mandatory dependency is missing")
1577 elif self.deps.need:
1578 sys.exit(f"Can't build as {self.deps.need} mandatory dependencies are missing")
1579
1580 print("Needed package dependencies are met.")
1581
1582DESCRIPTION = """
1583Process some flags related to Sphinx installation and documentation build.
1584"""
1585
1586
1587def main():
1588 """Main function"""
1589 parser = argparse.ArgumentParser(description=DESCRIPTION)
1590
1591 parser.add_argument(
1592 "--no-virtualenv",
1593 action="store_false",
1594 dest="virtualenv",
1595 help="Recommend installing Sphinx instead of using a virtualenv",
1596 )
1597
1598 parser.add_argument(
1599 "--no-pdf",
1600 action="store_false",
1601 dest="pdf",
1602 help="Don't check for dependencies required to build PDF docs",
1603 )
1604
1605 parser.add_argument(
1606 "--version-check",
1607 action="store_true",
1608 dest="version_check",
1609 help="If version is compatible, don't check for missing dependencies",
1610 )
703 1611
704sub deactivate_help()1612 args = parser.parse_args()
705{1613
706 printf "\n If you want to exit the virtualenv, you can use:\n";1614 checker = SphinxDependencyChecker(args)
707 printf "\tdeactivate\n";
708}
709
710sub get_virtenv()
711{
712 my $ver;
713 my $min_activate = "$ENV{'PWD'}/${virtenv_prefix}${min_version}/bin/activate";
714 my @activates = glob "$ENV{'PWD'}/${virtenv_prefix}*/bin/activate";
715
716 @activates = sort {$b cmp $a} @activates;
717
718 foreach my $f (@activates) {
719 next if ($f lt $min_activate);
720
721 my $sphinx_cmd = $f;
722 $sphinx_cmd =~ s/activate/sphinx-build/;
723 next if (! -f $sphinx_cmd);
724
725 my $ver = get_sphinx_version($sphinx_cmd);
726
727 if (!$ver) {
728 $f =~ s#/bin/activate##;
729 print("Warning: virtual environment $f is not working.\nPython version upgrade? Remove it with:\n\n\trm -rf $f\n\n");
730 }
731
732 if ($need_sphinx && ($ver ge $min_version)) {
733 return ($f, $ver);
734 } elsif ($ver gt $cur_version) {
735 return ($f, $ver);
736 }
737 }
738 return ("", "");
739}
740
741sub recommend_sphinx_upgrade()
742{
743 my $venv_ver;
744
745 # Avoid running sphinx-builds from venv if $cur_version is good
746 if ($cur_version && ($cur_version ge $rec_version)) {
747 $latest_avail_ver = $cur_version;
748 return;
749 }
750
751 # Get the highest version from sphinx_*/bin/sphinx-build and the
752 # corresponding command to activate the venv/virtenv
753 ($activate_cmd, $venv_ver) = get_virtenv();
754
755 # Store the highest version from Sphinx existing virtualenvs
756 if (($activate_cmd ne "") && ($venv_ver gt $cur_version)) {
757 $latest_avail_ver = $venv_ver;
758 } else {
759 $latest_avail_ver = $cur_version if ($cur_version);
760 }
761
762 # As we don't know package version of Sphinx, and there's no
763 # virtual environments, don't check if upgrades are needed
764 if (!$virtualenv) {
765 return if (!$latest_avail_ver);
766 }
767
768 # Either there are already a virtual env or a new one should be created
769 $need_pip = 1;
770
771 return if (!$latest_avail_ver);
772
773 # Return if the reason is due to an upgrade or not
774 if ($latest_avail_ver lt $rec_version) {
775 $rec_sphinx_upgrade = 1;
776 }
777
778 return $latest_avail_ver;
779}
780
781#
782# The logic here is complex, as it have to deal with different versions:
783# - minimal supported version;
784# - minimal PDF version;
785# - recommended version.
786# It also needs to work fine with both distro's package and venv/virtualenv
787sub recommend_sphinx_version($)
788{
789 my $virtualenv_cmd = shift;
790
791 # Version is OK. Nothing to do.
792 if ($cur_version && ($cur_version ge $rec_version)) {
793 return;
794 };
795
796 if (!$need_sphinx) {
797 # sphinx-build is present and its version is >= $min_version
798
799 #only recommend enabling a newer virtenv version if makes sense.
800 if ($latest_avail_ver gt $cur_version) {
801 printf "\nYou may also use the newer Sphinx version $latest_avail_ver with:\n";
802 printf "\tdeactivate\n" if ($ENV{'PWD'} =~ /${virtenv_prefix}/);
803 printf "\t. $activate_cmd\n";
804 deactivate_help();
805
806 return;
807 }
808 return if ($latest_avail_ver ge $rec_version);
809 }
810
811 if (!$virtualenv) {
812 # No sphinx either via package or via virtenv. As we can't
813 # Compare the versions here, just return, recommending the
814 # user to install it from the package distro.
815 return if (!$latest_avail_ver);
816
817 # User doesn't want a virtenv recommendation, but he already
818 # installed one via virtenv with a newer version.
819 # So, print commands to enable it
820 if ($latest_avail_ver gt $cur_version) {
821 printf "\nYou may also use the Sphinx virtualenv version $latest_avail_ver with:\n";
822 printf "\tdeactivate\n" if ($ENV{'PWD'} =~ /${virtenv_prefix}/);
823 printf "\t. $activate_cmd\n";
824 deactivate_help();
825
826 return;
827 }
828 print "\n";
829 } else {
830 $need++ if ($need_sphinx);
831 }
832
833 # Suggest newer versions if current ones are too old
834 if ($latest_avail_ver && $latest_avail_ver ge $min_version) {
835 # If there's a good enough version, ask the user to enable it
836 if ($latest_avail_ver ge $rec_version) {
837 printf "\nNeed to activate Sphinx (version $latest_avail_ver) on virtualenv with:\n";
838 printf "\t. $activate_cmd\n";
839 deactivate_help();
840
841 return;
842 }
843
844 # Version is above the minimal required one, but may be
845 # below the recommended one. So, print warnings/notes
846
847 if ($latest_avail_ver lt $rec_version) {
848 print "Warning: It is recommended at least Sphinx version $rec_version.\n";
849 }
850 }
851
852 # At this point, either it needs Sphinx or upgrade is recommended,
853 # both via pip
854
855 if ($rec_sphinx_upgrade) {
856 if (!$virtualenv) {
857 print "Instead of install/upgrade Python Sphinx pkg, you could use pip/pypi with:\n\n";
858 } else {
859 print "To upgrade Sphinx, use:\n\n";
860 }
861 } else {
862 print "\nSphinx needs to be installed either:\n1) via pip/pypi with:\n\n";
863 }
864
865 $python_cmd = find_python_no_venv();
866
867 printf "\t$virtualenv_cmd $virtenv_dir\n";
868
869 printf "\t. $virtenv_dir/bin/activate\n";
870 printf "\tpip install -r $requirement_file\n";
871 deactivate_help();
872
873 printf "\n2) As a package with:\n";
874
875 my $old_need = $need;
876 my $old_optional = $optional;
877 %missing = ();
878 $pdf = 0;
879 $optional = 0;
880 $install = "";
881 $verbose_warn_install = 0;
882
883 add_package("python-sphinx", 0);
884
885 check_distros();
886
887 $need = $old_need;
888 $optional = $old_optional;
889
890 printf "\n Please note that Sphinx >= 3.0 will currently produce false-positive\n";
891 printf " warning when the same name is used for more than one type (functions,\n";
892 printf " structs, enums,...). This is known Sphinx bug. For more details, see:\n";
893 printf "\thttps://github.com/sphinx-doc/sphinx/pull/8313\n";
894}
895
896sub check_needs()
897{
898 # Check if Sphinx is already accessible from current environment
899 check_sphinx();
900
901 if ($system_release) {
902 print "Detected OS: $system_release.\n";
903 } else {
904 print "Unknown OS\n";
905 }
906 printf "Sphinx version: %s\n\n", $cur_version if ($cur_version);
907
908 # Check python command line, trying first python3
909 $python_cmd = findprog("python3");
910 $python_cmd = check_program("python", 0) if (!$python_cmd);
911
912 # Check the type of virtual env, depending on Python version
913 if ($python_cmd) {
914 if ($virtualenv) {
915 my $tmp = qx($python_cmd --version 2>&1);
916 if ($tmp =~ m/(\d+\.)(\d+\.)/) {
917 if ($1 < 3) {
918 # Fail if it finds python2 (or worse)
919 die "Python 3 is required to build the kernel docs\n";
920 }
921 if ($1 == 3 && $2 < 3) {
922 # Need Python 3.3 or upper for venv
923 $need_virtualenv = 1;
924 }
925 } else {
926 die "Warning: couldn't identify $python_cmd version!";
927 }
928 } else {
929 add_package("python-sphinx", 0);
930 }
931 }
932
933 my $venv_ver = recommend_sphinx_upgrade();
934
935 my $virtualenv_cmd;
936
937 if ($need_pip) {
938 # Set virtualenv command line, if python < 3.3
939 if ($need_virtualenv) {
940 $virtualenv_cmd = findprog("virtualenv-3");
941 $virtualenv_cmd = findprog("virtualenv-3.5") if (!$virtualenv_cmd);
942 if (!$virtualenv_cmd) {
943 check_program("virtualenv", 0);
944 $virtualenv_cmd = "virtualenv";
945 }
946 } else {
947 $virtualenv_cmd = "$python_cmd -m venv";
948 check_python_module("ensurepip", 0);
949 }
950 }
951
952 # Check for needed programs/tools
953 check_perl_module("Pod::Usage", 0);
954 check_python_module("yaml", 0);
955 check_program("make", 0);
956 check_program("gcc", 0);
957 check_program("dot", 1);
958 check_program("convert", 1);
959
960 # Extra PDF files - should use 2 for is_optional
961 check_program("xelatex", 2) if ($pdf);
962 check_program("rsvg-convert", 2) if ($pdf);
963 check_program("latexmk", 2) if ($pdf);
964
965 # Do distro-specific checks and output distro-install commands
966 check_distros();
967
968 if (!$python_cmd) {
969 if ($need == 1) {
970 die "Can't build as $need mandatory dependency is missing";
971 } elsif ($need) {
972 die "Can't build as $need mandatory dependencies are missing";
973 }
974 }
975
976 # Check if sphinx-build is called sphinx-build-3
977 if ($need_symlink) {
978 printf "\tsudo ln -sf %s /usr/bin/sphinx-build\n\n",
979 which("sphinx-build-3");
980 }
981
982 recommend_sphinx_version($virtualenv_cmd);
983 printf "\n";
984
985 print "All optional dependencies are met.\n" if (!$optional);
986
987 if ($need == 1) {
988 die "Can't build as $need mandatory dependency is missing";
989 } elsif ($need) {
990 die "Can't build as $need mandatory dependencies are missing";
991 }
992
993 print "Needed package dependencies are met.\n";
994}
995
996#
997# Main
998#
999
1000while (@ARGV) {
1001 my $arg = shift(@ARGV);
1002
1003 if ($arg eq "--no-virtualenv") {
1004 $virtualenv = 0;
1005 } elsif ($arg eq "--no-pdf"){
1006 $pdf = 0;
1007 } elsif ($arg eq "--version-check"){
1008 $version_check = 1;
1009 } else {
1010 print "Usage:\n\t$0 <--no-virtualenv> <--no-pdf> <--version-check>\n\n";
1011 print "Where:\n";
1012 print "\t--no-virtualenv\t- Recommend installing Sphinx instead of using a virtualenv\n";
1013 print "\t--version-check\t- if version is compatible, don't check for missing dependencies\n";
1014 print "\t--no-pdf\t- don't check for dependencies required to build PDF docs\n\n";
1015 exit -1;
1016 }
1017}
1018
1019#
1020# Determine the system type. There's no standard unique way that would
1021# work with all distros with a minimal package install. So, several
1022# methods are used here.
1023#
1024# By default, it will use lsb_release function. If not available, it will
1025# fail back to reading the known different places where the distro name
1026# is stored
1027#
1028 1615
1029$system_release = qx(lsb_release -d) if which("lsb_release");1616 checker.check_python()
1030$system_release =~ s/Description:\s*// if ($system_release);1617 checker.check_needs()
1031$system_release = catcheck("/etc/system-release") if !$system_release;1618
1032$system_release = catcheck("/etc/redhat-release") if !$system_release;1619# Call main if not used as module
1033$system_release = catcheck("/etc/lsb-release") if !$system_release;1620if __name__ == "__main__":
1034$system_release = catcheck("/etc/gentoo-release") if !$system_release;1621 main()
1035
1036# This seems more common than LSB these days
1037if (!$system_release) {
1038 my %os_var;
1039 if (open IN, "cat /etc/os-release|") {
1040 while (<IN>) {
1041 if (m/^([\w\d\_]+)=\"?([^\"]*)\"?\n/) {
1042 $os_var{$1}=$2;
1043 }
1044 }
1045 $system_release = $os_var{"NAME"};
1046 if (defined($os_var{"VERSION_ID"})) {
1047 $system_release .= " " . $os_var{"VERSION_ID"} if (defined($os_var{"VERSION_ID"}));
1048 } else {
1049 $system_release .= " " . $os_var{"VERSION"};
1050 }
1051 }
1052}
1053$system_release = catcheck("/etc/issue") if !$system_release;
1054$system_release =~ s/\s+$//;
1055
1056check_needs;