xref: /linux/tools/docs/sphinx-build-wrapper (revision 5181afcdf99527dd92a88f80fc4d0d8013e1b510)
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
59
60LIB_DIR = "../lib/python"
61SRC_DIR = os.path.dirname(os.path.realpath(__file__))
62
63sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR))
64
65from kdoc.python_version import PythonVersion
66from kdoc.latex_fonts import LatexFontChecker
67from jobserver import JobserverExec         # pylint: disable=C0413,C0411,E0401
68
69#
70#  Some constants
71#
72VENV_DEFAULT = "sphinx_latest"
73MIN_PYTHON_VERSION = PythonVersion("3.7").version
74PAPER = ["", "a4", "letter"]
75
76TARGETS = {
77    "cleandocs":     { "builder": "clean" },
78    "linkcheckdocs": { "builder": "linkcheck" },
79    "htmldocs":      { "builder": "html" },
80    "epubdocs":      { "builder": "epub",    "out_dir": "epub" },
81    "texinfodocs":   { "builder": "texinfo", "out_dir": "texinfo" },
82    "infodocs":      { "builder": "texinfo", "out_dir": "texinfo" },
83    "mandocs":       { "builder": "man",     "out_dir": "man" },
84    "latexdocs":     { "builder": "latex",   "out_dir": "latex" },
85    "pdfdocs":       { "builder": "latex",   "out_dir": "latex" },
86    "xmldocs":       { "builder": "xml",     "out_dir": "xml" },
87}
88
89
90#
91# SphinxBuilder class
92#
93
94class SphinxBuilder:
95    """
96    Handles a sphinx-build target, adding needed arguments to build
97    with the Kernel.
98    """
99
100    def get_path(self, path, use_cwd=False, abs_path=False):
101        """
102        Ancillary routine to handle patches the right way, as shell does.
103
104        It first expands "~" and "~user". Then, if patch is not absolute,
105        join self.srctree. Finally, if requested, convert to abspath.
106        """
107
108        path = os.path.expanduser(path)
109        if not path.startswith("/"):
110            if use_cwd:
111                base = os.getcwd()
112            else:
113                base = self.srctree
114
115            path = os.path.join(base, path)
116
117        if abs_path:
118            return os.path.abspath(path)
119
120        return path
121
122    def check_rust(self, sphinxdirs):
123        """
124        Checks if Rust is enabled
125        """
126        config = os.path.join(self.srctree, ".config")
127
128        if not {'.', 'rust'}.intersection(sphinxdirs):
129            return False
130
131        if not os.path.isfile(config):
132            return False
133
134        re_rust = re.compile(r"CONFIG_RUST=(m|y)")
135
136        try:
137            with open(config, "r", encoding="utf-8") as fp:
138                for line in fp:
139                    if re_rust.match(line):
140                        return True
141
142        except OSError as e:
143            print(f"Failed to open {config}", file=sys.stderr)
144            return False
145
146        return False
147
148    def get_sphinx_extra_opts(self, n_jobs):
149        """
150        Get the number of jobs to be used for docs build passed via command
151        line and desired sphinx verbosity.
152
153        The number of jobs can be on different places:
154
155        1) It can be passed via "-j" argument;
156        2) The SPHINXOPTS="-j8" env var may have "-j";
157        3) if called via GNU make, -j specifies the desired number of jobs.
158           with GNU makefile, this number is available via POSIX jobserver;
159        4) if none of the above is available, it should default to "-jauto",
160           and let sphinx decide the best value.
161        """
162
163        #
164        # SPHINXOPTS env var, if used, contains extra arguments to be used
165        # by sphinx-build time. Among them, it may contain sphinx verbosity
166        # and desired number of parallel jobs.
167        #
168        parser = argparse.ArgumentParser()
169        parser.add_argument('-j', '--jobs', type=int)
170        parser.add_argument('-q', '--quiet', action='store_true')
171        parser.add_argument('-v', '--verbose', default=0, action='count')
172
173        #
174        # Other sphinx-build arguments go as-is, so place them
175        # at self.sphinxopts, using shell parser
176        #
177        sphinxopts = shlex.split(os.environ.get("SPHINXOPTS", ""))
178
179        #
180        # Build a list of sphinx args, honoring verbosity here if specified
181        #
182
183        sphinx_args, self.sphinxopts = parser.parse_known_args(sphinxopts)
184
185        verbose = sphinx_args.verbose
186        if self.verbose:
187            verbose += 1
188
189        if sphinx_args.quiet is True:
190            verbose = 0
191
192        #
193        # If the user explicitly sets "-j" at command line, use it.
194        # Otherwise, pick it from SPHINXOPTS args
195        #
196        if n_jobs:
197            self.n_jobs = n_jobs
198        elif sphinx_args.jobs:
199            self.n_jobs = sphinx_args.jobs
200        else:
201            self.n_jobs = None
202
203        if verbose < 1:
204            self.sphinxopts += ["-q"]
205        else:
206            for i in range(1, sphinx_args.verbose):
207                self.sphinxopts += ["-v"]
208
209    def __init__(self, builddir, venv=None, verbose=False, n_jobs=None,
210                 interactive=None):
211        """Initialize internal variables"""
212        self.venv = venv
213        self.verbose = None
214
215        #
216        # Normal variables passed from Kernel's makefile
217        #
218        self.kernelversion = os.environ.get("KERNELVERSION", "unknown")
219        self.kernelrelease = os.environ.get("KERNELRELEASE", "unknown")
220        self.pdflatex = os.environ.get("PDFLATEX", "xelatex")
221
222        #
223        # Kernel main Makefile defines a PYTHON3 variable whose default is
224        # "python3". When set to a different value, it allows running a
225        # diferent version than the default official python3 package.
226        # Several distros package python3xx-sphinx packages with newer
227        # versions of Python and sphinx-build.
228        #
229        # Honor such variable different than default
230        #
231        self.python = os.environ.get("PYTHON3")
232        if self.python == "python3":
233            self.python = None
234
235        if not interactive:
236            self.latexopts = os.environ.get("LATEXOPTS", "-interaction=batchmode -no-shell-escape")
237        else:
238            self.latexopts = os.environ.get("LATEXOPTS", "")
239
240        if not verbose:
241            try:
242                verbose = bool(int(os.environ.get("KBUILD_VERBOSE", 0)))
243            except ValueError:
244                # Handles an eventual case where verbosity is not a number
245                # like KBUILD_VERBOSE=""
246                verbose = False
247
248        if verbose is not None:
249            self.verbose = verbose
250
251        #
252        # Source tree directory. This needs to be at os.environ, as
253        # Sphinx extensions use it
254        #
255        self.srctree = os.environ.get("srctree")
256        if not self.srctree:
257            self.srctree = "."
258            os.environ["srctree"] = self.srctree
259
260        #
261        # Now that we can expand srctree, get other directories as well
262        #
263        self.sphinxbuild = os.environ.get("SPHINXBUILD", "sphinx-build")
264        self.kerneldoc = self.get_path(os.environ.get("KERNELDOC",
265                                                      "tools/docs/kernel-doc"))
266        self.builddir = self.get_path(builddir, use_cwd=True, abs_path=True)
267
268        #
269        # Get directory locations for LaTeX build toolchain
270        #
271        self.pdflatex_cmd = shutil.which(self.pdflatex)
272        self.latexmk_cmd = shutil.which("latexmk")
273
274        self.env = os.environ.copy()
275
276        self.get_sphinx_extra_opts(n_jobs)
277
278        #
279        # If venv command line argument is specified, run Sphinx from venv
280        #
281        if venv:
282            bin_dir = os.path.join(venv, "bin")
283            if not os.path.isfile(os.path.join(bin_dir, "activate")):
284                sys.exit(f"Venv {venv} not found.")
285
286            # "activate" virtual env
287            self.env["PATH"] = bin_dir + ":" + self.env["PATH"]
288            self.env["VIRTUAL_ENV"] = venv
289            if "PYTHONHOME" in self.env:
290                del self.env["PYTHONHOME"]
291            print(f"Setting venv to {venv}")
292
293    def run_sphinx(self, sphinx_build, build_args, *args, **pwargs):
294        """
295        Executes sphinx-build using current python3 command.
296
297        When calling via GNU make, POSIX jobserver is used to tell how
298        many jobs are still available from a job pool. claim all remaining
299        jobs, as we don't want sphinx-build to run in parallel with other
300        jobs.
301
302        Despite that, the user may actually force a different value than
303        the number of available jobs via command line.
304
305        The "with" logic here is used to ensure that the claimed jobs will
306        be freed once subprocess finishes
307        """
308
309        with JobserverExec() as jobserver:
310            if jobserver.claim:
311                #
312                # when GNU make is used, claim available jobs from jobserver
313                #
314                n_jobs = str(jobserver.claim)
315            else:
316                #
317                # Otherwise, let sphinx decide by default
318                #
319                n_jobs = "auto"
320
321            #
322            # If explicitly requested via command line, override default
323            #
324            if self.n_jobs:
325                n_jobs = str(self.n_jobs)
326
327            #
328            # We can't simply call python3 sphinx-build, as OpenSUSE
329            # Tumbleweed uses an ELF binary file (/usr/bin/alts) to switch
330            # between different versions of sphinx-build. So, only call it
331            # prepending "python3.xx" when PYTHON3 variable is not default.
332            #
333            if self.python:
334                cmd = [self.python]
335            else:
336                cmd = []
337
338            cmd += [sphinx_build]
339            cmd += [f"-j{n_jobs}"]
340            cmd += build_args
341            cmd += self.sphinxopts
342
343            if self.verbose:
344                print(" ".join(cmd))
345
346            return subprocess.call(cmd, *args, **pwargs)
347
348    def handle_html(self, css, output_dir):
349        """
350        Extra steps for HTML and epub output.
351
352        For such targets, we need to ensure that CSS will be properly
353        copied to the output _static directory
354        """
355
356        if css:
357            css = os.path.expanduser(css)
358            if not css.startswith("/"):
359                css = os.path.join(self.srctree, css)
360
361            static_dir = os.path.join(output_dir, "_static")
362            os.makedirs(static_dir, exist_ok=True)
363
364            try:
365                shutil.copy2(css, static_dir)
366            except (OSError, IOError) as e:
367                print(f"Warning: Failed to copy CSS: {e}", file=sys.stderr)
368
369    def build_pdf_file(self, latex_cmd, from_dir, path):
370        """Builds a single pdf file using latex_cmd"""
371        try:
372            subprocess.run(latex_cmd + [path],
373                            cwd=from_dir, check=True, env=self.env)
374
375            return True
376        except subprocess.CalledProcessError:
377            return False
378
379    def pdf_parallel_build(self, tex_suffix, latex_cmd, tex_files, n_jobs):
380        """Build PDF files in parallel if possible"""
381        builds = {}
382        build_failed = False
383        max_len = 0
384        has_tex = False
385
386        #
387        # LaTeX PDF error code is almost useless for us:
388        # any warning makes it non-zero. For kernel doc builds it always return
389        # non-zero even when build succeeds. So, let's do the best next thing:
390        # Ignore build errors. At the end, check if all PDF files were built,
391        # printing a summary with the built ones and returning 0 if all of
392        # them were actually built.
393        #
394        with futures.ThreadPoolExecutor(max_workers=n_jobs) as executor:
395            jobs = {}
396
397            for from_dir, pdf_dir, entry in tex_files:
398                name = entry.name
399
400                if not name.endswith(tex_suffix):
401                    continue
402
403                name = name[:-len(tex_suffix)]
404                has_tex = True
405
406                future = executor.submit(self.build_pdf_file, latex_cmd,
407                                         from_dir, entry.path)
408                jobs[future] = (from_dir, pdf_dir, name)
409
410            for future in futures.as_completed(jobs):
411                from_dir, pdf_dir, name = jobs[future]
412
413                pdf_name = name + ".pdf"
414                pdf_from = os.path.join(from_dir, pdf_name)
415                pdf_to = os.path.join(pdf_dir, pdf_name)
416                out_name = os.path.relpath(pdf_to, self.builddir)
417                max_len = max(max_len, len(out_name))
418
419                try:
420                    success = future.result()
421
422                    if success and os.path.exists(pdf_from):
423                        os.rename(pdf_from, pdf_to)
424
425                        #
426                        # if verbose, get the name of built PDF file
427                        #
428                        if self.verbose:
429                           builds[out_name] = "SUCCESS"
430                    else:
431                        builds[out_name] = "FAILED"
432                        build_failed = True
433                except futures.Error as e:
434                    builds[out_name] = f"FAILED ({repr(e)})"
435                    build_failed = True
436
437        #
438        # Handle case where no .tex files were found
439        #
440        if not has_tex:
441            out_name = "LaTeX files"
442            max_len = max(max_len, len(out_name))
443            builds[out_name] = "FAILED: no .tex files were generated"
444            build_failed = True
445
446        return builds, build_failed, max_len
447
448    def handle_pdf(self, output_dirs, deny_vf):
449        """
450        Extra steps for PDF output.
451
452        As PDF is handled via a LaTeX output, after building the .tex file,
453        a new build is needed to create the PDF output from the latex
454        directory.
455        """
456        builds = {}
457        max_len = 0
458        tex_suffix = ".tex"
459        tex_files = []
460
461        #
462        # Since early 2024, Fedora and openSUSE tumbleweed have started
463        # deploying variable-font format of "Noto CJK", causing LaTeX
464        # to break with CJK. Work around it, by denying the variable font
465        # usage during xelatex build by passing the location of a config
466        # file with a deny list.
467        #
468        # See tools/docs/lib/latex_fonts.py for more details.
469        #
470        if deny_vf:
471            deny_vf = os.path.expanduser(deny_vf)
472            if os.path.isdir(deny_vf):
473                self.env["XDG_CONFIG_HOME"] = deny_vf
474
475        for from_dir in output_dirs:
476            pdf_dir = os.path.join(from_dir, "../pdf")
477            os.makedirs(pdf_dir, exist_ok=True)
478
479            if self.latexmk_cmd:
480                latex_cmd = [self.latexmk_cmd, f"-{self.pdflatex}"]
481            else:
482                latex_cmd = [self.pdflatex]
483
484            latex_cmd.extend(shlex.split(self.latexopts))
485
486            # Get a list of tex files to process
487            with os.scandir(from_dir) as it:
488                for entry in it:
489                    if entry.name.endswith(tex_suffix):
490                        tex_files.append((from_dir, pdf_dir, entry))
491
492        #
493        # When using make, this won't be used, as the number of jobs comes
494        # from POSIX jobserver. So, this covers the case where build comes
495        # from command line. On such case, serialize by default, except if
496        # the user explicitly sets the number of jobs.
497        #
498        n_jobs = 1
499
500        # n_jobs is either an integer or "auto". Only use it if it is a number
501        if self.n_jobs:
502            try:
503                n_jobs = int(self.n_jobs)
504            except ValueError:
505                pass
506
507        #
508        # When using make, jobserver.claim is the number of jobs that were
509        # used with "-j" and that aren't used by other make targets
510        #
511        with JobserverExec() as jobserver:
512            n_jobs = 1
513
514            #
515            # Handle the case when a parameter is passed via command line,
516            # using it as default, if jobserver doesn't claim anything
517            #
518            if self.n_jobs:
519                try:
520                    n_jobs = int(self.n_jobs)
521                except ValueError:
522                    pass
523
524            if jobserver.claim:
525                n_jobs = jobserver.claim
526
527            builds, build_failed, max_len = self.pdf_parallel_build(tex_suffix,
528                                                                    latex_cmd,
529                                                                    tex_files,
530                                                                    n_jobs)
531
532        #
533        # In verbose mode, print a summary with the build results per file.
534        # Otherwise, print a single line with all failures, if any.
535        # On both cases, return code 1 indicates build failures,
536        #
537        if self.verbose:
538            msg = "Summary"
539            msg += "\n" + "=" * len(msg)
540            print()
541            print(msg)
542
543            for pdf_name, pdf_file in builds.items():
544                print(f"{pdf_name:<{max_len}}: {pdf_file}")
545
546            print()
547            if build_failed:
548                msg = LatexFontChecker().check()
549                if msg:
550                    print(msg)
551
552                sys.exit("Error: not all PDF files were created.")
553
554        elif build_failed:
555            n_failures = len(builds)
556            failures = ", ".join(builds.keys())
557
558            msg = LatexFontChecker().check()
559            if msg:
560                print(msg)
561
562            sys.exit(f"Error: Can't build {n_failures} PDF file(s): {failures}")
563
564    def handle_info(self, output_dirs):
565        """
566        Extra steps for Info output.
567
568        For texinfo generation, an additional make is needed from the
569        texinfo directory.
570        """
571
572        for output_dir in output_dirs:
573            try:
574                subprocess.run(["make", "info"], cwd=output_dir, check=True)
575            except subprocess.CalledProcessError as e:
576                sys.exit(f"Error generating info docs: {e}")
577
578    def handle_man(self, kerneldoc, docs_dir, src_dir, output_dir):
579        """
580        Create man pages from kernel-doc output
581        """
582
583        re_kernel_doc = re.compile(r"^\.\.\s+kernel-doc::\s*(\S+)")
584
585        if docs_dir == src_dir:
586            #
587            # Pick the entire set of kernel-doc markups from the entire tree
588            #
589            kdoc_files = set([self.srctree])
590        else:
591            kdoc_files = set()
592
593            for fname in glob(os.path.join(src_dir, "**"), recursive=True):
594                if os.path.isfile(fname) and fname.endswith(".rst"):
595                    with open(fname, "r", encoding="utf-8") as in_fp:
596                        data = in_fp.read()
597
598                    for line in data.split("\n"):
599                        match = re_kernel_doc.match(line)
600                        if match:
601                            if os.path.isfile(match.group(1)):
602                                kdoc_files.add(match.group(1))
603
604        if not kdoc_files:
605                sys.exit(f"Directory {src_dir} doesn't contain kernel-doc tags")
606
607        cmd = [ kerneldoc, "-m" ] + sorted(kdoc_files)
608        try:
609            if self.verbose:
610                print(" ".join(cmd))
611
612            result = subprocess.run(cmd, stdout=subprocess.PIPE, text= True)
613
614            if result.returncode:
615                print(f"Warning: kernel-doc returned {result.returncode} warnings")
616
617        except (OSError, ValueError, subprocess.SubprocessError) as e:
618            sys.exit(f"Failed to create man pages for {src_dir}: {repr(e)}")
619
620        fp = None
621        try:
622            for line in result.stdout.split("\n"):
623                if not line.startswith(".TH"):
624                    if fp:
625                        fp.write(line + '\n')
626                    continue
627
628                if fp:
629                    fp.close()
630
631                # Use shlex here, as it handles well parameters with commas
632                args = shlex.split(line)
633                fname = f"{args[1]}.{args[2]}"
634                fname = fname.replace("/", " ")
635                fname = f"{output_dir}/{fname}"
636
637                if self.verbose:
638                    print(f"Creating {fname}")
639                fp = open(fname, "w", encoding="utf-8")
640                fp.write(line + '\n')
641        finally:
642            if fp:
643                fp.close()
644
645    def cleandocs(self, builder):           # pylint: disable=W0613
646        """Remove documentation output directory"""
647        shutil.rmtree(self.builddir, ignore_errors=True)
648
649    def build(self, target, sphinxdirs=None,
650              theme=None, css=None, paper=None, deny_vf=None,
651              skip_sphinx=False):
652        """
653        Build documentation using Sphinx. This is the core function of this
654        module. It prepares all arguments required by sphinx-build.
655        """
656
657        builder = TARGETS[target]["builder"]
658        out_dir = TARGETS[target].get("out_dir", "")
659
660        #
661        # Cleandocs doesn't require sphinx-build
662        #
663        if target == "cleandocs":
664            self.cleandocs(builder)
665            return
666
667        if theme:
668            os.environ["DOCS_THEME"] = theme
669
670        #
671        # Other targets require sphinx-build, so check if it exists
672        #
673        if not skip_sphinx:
674            sphinxbuild = shutil.which(self.sphinxbuild, path=self.env["PATH"])
675            if not sphinxbuild and target != "mandocs":
676                sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n")
677
678        if target == "pdfdocs":
679            if not self.pdflatex_cmd and not self.latexmk_cmd:
680                sys.exit("Error: pdflatex or latexmk required for PDF generation")
681
682        docs_dir = os.path.abspath(os.path.join(self.srctree, "Documentation"))
683
684        #
685        # Fill in base arguments for Sphinx build
686        #
687        kerneldoc = self.kerneldoc
688        if kerneldoc.startswith(self.srctree):
689            kerneldoc = os.path.relpath(kerneldoc, self.srctree)
690
691        if not sphinxdirs:
692            sphinxdirs = os.environ.get("SPHINXDIRS", ".")
693
694        #
695        # sphinxdirs can be a list or a whitespace-separated string
696        #
697        sphinxdirs_list = []
698        for sphinxdir in sphinxdirs:
699            if isinstance(sphinxdir, list):
700                sphinxdirs_list += sphinxdir
701            else:
702                sphinxdirs_list += sphinxdir.split()
703
704        args = [ "-b", builder, "-c", docs_dir ]
705
706        if builder == "latex":
707            if not paper:
708                paper = PAPER[1]
709
710            args.extend(["-D", f"latex_elements.papersize={paper}paper"])
711
712        rustdoc = self.check_rust(sphinxdirs_list)
713        if rustdoc:
714            args.extend(["-t", "rustdoc"])
715
716        #
717        # The sphinx-build tool has a bug: internally, it tries to set
718        # locale with locale.setlocale(locale.LC_ALL, ''). This causes a
719        # crash if language is not set. Detect and fix it.
720        #
721        try:
722            locale.setlocale(locale.LC_ALL, '')
723        except locale.Error:
724            self.env["LC_ALL"] = "C"
725
726        #
727        # Step 1:  Build each directory in separate.
728        #
729        # This is not the best way of handling it, as cross-references between
730        # them will be broken, but this is what we've been doing since
731        # the beginning.
732        #
733        output_dirs = []
734        for sphinxdir in sphinxdirs_list:
735            src_dir = os.path.join(docs_dir, sphinxdir)
736            doctree_dir = os.path.join(self.builddir, ".doctrees")
737            output_dir = os.path.join(self.builddir, sphinxdir, out_dir)
738
739            #
740            # Make directory names canonical
741            #
742            src_dir = os.path.normpath(src_dir)
743            doctree_dir = os.path.normpath(doctree_dir)
744            output_dir = os.path.normpath(output_dir)
745
746            os.makedirs(doctree_dir, exist_ok=True)
747            os.makedirs(output_dir, exist_ok=True)
748
749            output_dirs.append(output_dir)
750
751            build_args = args + [
752                "-d", doctree_dir,
753                "-D", f"version={self.kernelversion}",
754                "-D", f"release={self.kernelrelease}",
755                "-D", f"kerneldoc_srctree={self.srctree}",
756                src_dir,
757                output_dir,
758            ]
759
760            if target == "mandocs":
761                self.handle_man(kerneldoc, docs_dir, src_dir, output_dir)
762            elif not skip_sphinx:
763                try:
764                    result = self.run_sphinx(sphinxbuild, build_args,
765                                             env=self.env)
766
767                    if result:
768                        sys.exit(f"Build failed: return code: {result}")
769
770                except (OSError, ValueError, subprocess.SubprocessError) as e:
771                    sys.exit(f"Build failed: {repr(e)}")
772
773            #
774            # Ensure that each html/epub output will have needed static files
775            #
776            if target in ["htmldocs", "epubdocs"]:
777                self.handle_html(css, output_dir)
778
779        #
780        # Step 2: Some targets (PDF and info) require an extra step once
781        #         sphinx-build finishes
782        #
783        if target == "pdfdocs":
784            self.handle_pdf(output_dirs, deny_vf)
785        elif target == "infodocs":
786            self.handle_info(output_dirs)
787
788        if rustdoc and target in ["htmldocs", "epubdocs"]:
789            print("Building rust docs")
790            if "MAKE" in self.env:
791                cmd = [self.env["MAKE"]]
792            else:
793                cmd = ["make", "LLVM=1"]
794
795            cmd += [ "rustdoc"]
796            if self.verbose:
797                print(" ".join(cmd))
798
799            try:
800                subprocess.run(cmd, check=True)
801            except subprocess.CalledProcessError as e:
802                print(f"Ignored errors when building rustdoc: {e}. Is RUST enabled?",
803                      file=sys.stderr)
804
805def jobs_type(value):
806    """
807    Handle valid values for -j. Accepts Sphinx "-jauto", plus a number
808    equal or bigger than one.
809    """
810    if value is None:
811        return None
812
813    if value.lower() == 'auto':
814        return value.lower()
815
816    try:
817        if int(value) >= 1:
818            return value
819
820        raise argparse.ArgumentTypeError(f"Minimum jobs is 1, got {value}")
821    except ValueError:
822        raise argparse.ArgumentTypeError(f"Must be 'auto' or positive integer, got {value}")  # pylint: disable=W0707
823
824EPILOG="""
825Besides the command line arguments, several environment variables affect its
826default behavior, meant to be used when called via Kernel Makefile:
827
828- KERNELVERSION:  Kernel major version
829- KERNELRELEASE:  Kernel release
830- KBUILD_VERBOSE: Contains the value of "make V=[0|1] variable.
831                  When V=0 (KBUILD_VERBOSE=0), sets verbose level to "-q".
832- SPHINXBUILD:    Documentation build tool (default: "sphinx-build").
833- SPHINXOPTS:     Extra options pased to SPHINXBUILD
834                  (default: "-j auto" and "-q" if KBUILD_VERBOSE=0).
835                  The "-v" flag can be used to increase verbosity.
836                  If V=0, the first "-v" will drop "-q".
837- PYTHON3:        Python command to run SPHINXBUILD
838- PDFLATEX:       LaTeX PDF engine. (default: "xelatex")
839- LATEXOPTS:      Optional set of command line arguments to the LaTeX engine
840- srctree:        Location of the Kernel root directory (default: ".").
841
842"""
843
844def main():
845    """
846    Main function. The only mandatory argument is the target. If not
847    specified, the other arguments will use default values if not
848    specified at os.environ.
849    """
850    parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter,
851                                     description=__doc__,
852                                     epilog=EPILOG)
853
854    parser.add_argument("target", choices=list(TARGETS.keys()),
855                        help="Documentation target to build")
856    parser.add_argument("--sphinxdirs", nargs="+",
857                        help="Specific directories to build")
858    parser.add_argument("--builddir", default="output",
859                        help="Sphinx configuration file (default: %(default)s)")
860
861    parser.add_argument("--theme", help="Sphinx theme to use")
862
863    parser.add_argument("--css", help="Custom CSS file for HTML/EPUB")
864
865    parser.add_argument("--paper", choices=PAPER, default=PAPER[0],
866                        help="Paper size for LaTeX/PDF output")
867
868    parser.add_argument('--deny-vf',
869                        help="Configuration to deny variable fonts on pdf builds")
870
871    parser.add_argument("-v", "--verbose", action='store_true',
872                        help="place build in verbose mode")
873
874    parser.add_argument('-j', '--jobs', type=jobs_type,
875                        help="Sets number of jobs to use with sphinx-build(default: auto)")
876
877    parser.add_argument('-i', '--interactive', action='store_true',
878                        help="Change latex default to run in interactive mode")
879
880    parser.add_argument('-s', '--skip-sphinx-build', action='store_true',
881                        help="Skip sphinx-build step")
882
883    parser.add_argument("-V", "--venv", nargs='?', const=f'{VENV_DEFAULT}',
884                        default=None,
885                        help=f'If used, run Sphinx from a venv dir (default dir: {VENV_DEFAULT})')
886
887    args = parser.parse_args()
888
889    PythonVersion.check_python(MIN_PYTHON_VERSION, show_alternatives=True,
890                               bail_out=True)
891
892    builder = SphinxBuilder(builddir=args.builddir, venv=args.venv,
893                            verbose=args.verbose, n_jobs=args.jobs,
894                            interactive=args.interactive)
895
896    builder.build(args.target, sphinxdirs=args.sphinxdirs,
897                  theme=args.theme, css=args.css, paper=args.paper,
898                  deny_vf=args.deny_vf,
899                  skip_sphinx=args.skip_sphinx_build)
900
901if __name__ == "__main__":
902    main()
903