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