From nobody Sat Oct 4 16:15:39 2025 Received: from smtp.kernel.org (aws-us-west-2-korg-mail-1.web.codeaurora.org [10.30.226.201]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 9B1313009ED; Fri, 15 Aug 2025 11:51:06 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=10.30.226.201 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1755258666; cv=none; b=WOYXu/r5mL5YX8Cc5qyiFqPt07+j+fLMtPStEkwV2h1HMaPSiAu6V2gmlSM7eGnUbGa7lAyKSur6Nlwupu3XO02QbEg3WjfXEmDmb7gSlBdfGzg17yEwVMWVBM5fK1hcoRTBzS9Ypxtx0JeDkNIdeorFdT19NrRGbwE8rQEjN50= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1755258666; c=relaxed/simple; bh=zumhMRghLThqkpHf4QwjSWnxlXD8mXBUhoe7SLsig1I=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=o4ACOw4Fkvu5MqM8G6xZX71OKDycjFaudEUKMfa2tLsOqYHKpXP2lyTTAqfcJXoSBcCuqj13WkZlGw3FaXsE0UMqhaLkeKnI/bvoenN9dyucT0Gj0xZROnifpC4s25BccqWeTXSryqfFKjaV3MY16flajum8w2TbfdTKsN4GzyM= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b=iRGZFgqS; arc=none smtp.client-ip=10.30.226.201 Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=kernel.org header.i=@kernel.org header.b="iRGZFgqS" Received: by smtp.kernel.org (Postfix) with ESMTPSA id C21BCC4CEF8; Fri, 15 Aug 2025 11:51:05 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=kernel.org; s=k20201202; t=1755258666; bh=zumhMRghLThqkpHf4QwjSWnxlXD8mXBUhoe7SLsig1I=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=iRGZFgqSQixi8wGJlByklO+ePIz27GiRzUoLm1djZgzEY+bcZRg+zVW2DH4IwgLjZ sv7AZFdUcTZyodzNZX1/JGPkYWuZThk+o3f2v6VI98cn1Vs2nCJ0UjT7Jc0z+NbfIQ UWzC/N9xe7N15fH7NwK9c9fLkmCOdmQ/TDgO1OlF4iCHlfQTPxIR2nTuXX0R8bzNRn 5huQRR4kBx/snn1z7zE5O0e6vTdcxIEezVY14ZtMLDSJ5VDALpwQCes0NPhsIuxUMH Nlqg3zcTnmAohNDBfluRvuxiA0byAWHaL7vF5KNd7TeDzS8MYZdaIAB5A54RMgxfMW aZbR4GA/l+9Fg== Received: from mchehab by mail.kernel.org with local (Exim 4.98.2) (envelope-from ) id 1umsxc-000000042oS-0K5P; Fri, 15 Aug 2025 13:51:04 +0200 From: Mauro Carvalho Chehab To: Jonathan Corbet , Linux Doc Mailing List Cc: Mauro Carvalho Chehab , =?UTF-8?q?Bj=C3=B6rn=20Roy=20Baron?= , "Mauro Carvalho Chehab" , Alex Gaynor , Alice Ryhl , Andreas Hindborg , Benno Lossin , Boqun Feng , Danilo Krummrich , Gary Guo , Miguel Ojeda , Trevor Gross , linux-kernel@vger.kernel.org, rust-for-linux@vger.kernel.org Subject: [PATCH 04/11] scripts: sphinx-build-wrapper: add a wrapper for sphinx-build Date: Fri, 15 Aug 2025 13:50:32 +0200 Message-ID: <88a95c7f6996cafb247d6706060173b17a46d570.1755258303.git.mchehab+huawei@kernel.org> X-Mailer: git-send-email 2.50.1 In-Reply-To: References: Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable Sender: Mauro Carvalho Chehab Content-Type: text/plain; charset="utf-8" There are too much magic inside docs Makefile to properly run sphinx-build. Create an ancillary script that contains all kernel-related sphinx-build call logic currently at Makefile. Such script is designed to work both as an standalone command and as part of a Makefile. As such, it properly handles POSIX jobserver used by GNU make. It should be noticed that, when running the script alone, it will only take care of sphinx-build and cleandocs target. As such: - it won't run "make rustdoc"; - no extra checks. Signed-off-by: Mauro Carvalho Chehab --- .pylintrc | 2 +- scripts/sphinx-build-wrapper | 627 +++++++++++++++++++++++++++++++++++ 2 files changed, 628 insertions(+), 1 deletion(-) create mode 100755 scripts/sphinx-build-wrapper diff --git a/.pylintrc b/.pylintrc index 30b8ae1659f8..f1d21379254b 100644 --- a/.pylintrc +++ b/.pylintrc @@ -1,2 +1,2 @@ [MASTER] -init-hook=3D'import sys; sys.path +=3D ["scripts/lib/kdoc", "scripts/lib/a= bi"]' +init-hook=3D'import sys; sys.path +=3D ["scripts/lib", "scripts/lib/kdoc",= "scripts/lib/abi"]' diff --git a/scripts/sphinx-build-wrapper b/scripts/sphinx-build-wrapper new file mode 100755 index 000000000000..5c728956b53c --- /dev/null +++ b/scripts/sphinx-build-wrapper @@ -0,0 +1,627 @@ +#!/usr/bin/env python3 +# SPDX-License-Identifier: GPL-2.0 +# Copyright (C) 2025 Mauro Carvalho Chehab +# +# pylint: disable=3DR0902, R0912, R0913, R0914, R0915, R0917, C0103 +# +# Converted from docs Makefile and parallel-wrapper.sh, both under +# GPLv2, copyrighted since 2008 by the following authors: +# +# Akira Yokosawa +# Arnd Bergmann +# Breno Leitao +# Carlos Bilbao +# Dave Young +# Donald Hunter +# Geert Uytterhoeven +# Jani Nikula +# Jan Stancek +# Jonathan Corbet +# Joshua Clayton +# Kees Cook +# Linus Torvalds +# Magnus Damm +# Masahiro Yamada +# Mauro Carvalho Chehab +# Maxim Cournoyer +# Peter Foley +# Randy Dunlap +# Rob Herring +# Shuah Khan +# Thorsten Blum +# Tomas Winkler + + +""" +Sphinx build wrapper that handles Kernel-specific business rules: + +- it gets the Kernel build environment vars; +- it determines what's the best parallelism; +- it handles SPHINXDIRS + +This tool ensures that MIN_PYTHON_VERSION is satisfied. If version is +below that, it seeks for a new Python version. If found, it re-runs using +the newer version. +""" + +import argparse +import os +import re +import shlex +import shutil +import subprocess +import sys + +from glob import glob + +LIB_DIR =3D "lib" +SRC_DIR =3D os.path.dirname(os.path.realpath(__file__)) + +sys.path.insert(0, os.path.join(SRC_DIR, LIB_DIR)) + +from jobserver import JobserverExec # pylint: disable=3DC= 0413 + + +def parse_version(version): + """Convert a major.minor.patch version into a tuple""" + return tuple(int(x) for x in version.split(".")) + +def ver_str(version): + """Returns a version tuple as major.minor.patch""" + + return ".".join([str(x) for x in version]) + +# Minimal supported Python version needed by Sphinx and its extensions +MIN_PYTHON_VERSION =3D parse_version("3.7") + +# Default value for --venv parameter +VENV_DEFAULT =3D "sphinx_latest" + +# List of make targets and its corresponding builder and output directory +TARGETS =3D { + "cleandocs": { + "builder": "clean", + }, + "htmldocs": { + "builder": "html", + }, + "epubdocs": { + "builder": "epub", + "out_dir": "epub", + }, + "texinfodocs": { + "builder": "texinfo", + "out_dir": "texinfo", + }, + "infodocs": { + "builder": "texinfo", + "out_dir": "texinfo", + }, + "latexdocs": { + "builder": "latex", + "out_dir": "latex", + }, + "pdfdocs": { + "builder": "latex", + "out_dir": "latex", + }, + "xmldocs": { + "builder": "xml", + "out_dir": "xml", + }, + "linkcheckdocs": { + "builder": "linkcheck" + }, +} + +# Paper sizes. An empty value will pick the default +PAPER =3D ["", "a4", "letter"] + +class SphinxBuilder: + """ + Handles a sphinx-build target, adding needed arguments to build + with the Kernel. + """ + + def is_rust_enabled(self): + """Check if rust is enabled at .config""" + config_path =3D os.path.join(self.srctree, ".config") + if os.path.isfile(config_path): + with open(config_path, "r", encoding=3D"utf-8") as f: + return "CONFIG_RUST=3Dy" in f.read() + return False + + def get_path(self, path, abs_path=3DFalse): + """ + Ancillary routine to handle patches the right way, as shell does. + + It first expands "~" and "~user". Then, if patch is not absolute, + join self.srctree. Finally, if requested, convert to abspath. + """ + + path =3D os.path.expanduser(path) + if not path.startswith("/"): + path =3D os.path.join(self.srctree, path) + + if abs_path: + return os.path.abspath(path) + + return path + + def __init__(self, venv=3DNone, verbose=3DFalse): + """Initialize internal variables""" + self.venv =3D venv + self.verbose =3D verbose + + # Normal variables passed from Kernel's makefile + self.kernelversion =3D os.environ.get("KERNELVERSION", "unknown") + self.kernelrelease =3D os.environ.get("KERNELRELEASE", "unknown") + self.pdflatex =3D os.environ.get("PDFLATEX", "xelatex") + self.latexopts =3D os.environ.get("LATEXOPTS", "-interaction=3Dbat= chmode -no-shell-escape") + + # Source tree directory. This needs to be at os.environ, as + # Sphinx extensions and media uAPI makefile needs it + self.srctree =3D os.environ.get("srctree") + if not self.srctree: + self.srctree =3D "." + os.environ["srctree"] =3D self.srctree + + # Now that we can expand srctree, get other directories as well + self.sphinxbuild =3D os.environ.get("SPHINXBUILD", "sphinx-build") + self.kerneldoc =3D self.get_path(os.environ.get("KERNELDOC", + "scripts/kernel-doc.= py")) + self.obj =3D os.environ.get("obj", "Documentation") + self.builddir =3D self.get_path(os.path.join(self.obj, "output"), + abs_path=3DTrue) + + # Media uAPI needs it + os.environ["BUILDDIR"] =3D self.builddir + + # Detect if rust is enabled + self.config_rust =3D self.is_rust_enabled() + + # Get directory locations for LaTeX build toolchain + self.pdflatex_cmd =3D shutil.which(self.pdflatex) + self.latexmk_cmd =3D shutil.which("latexmk") + + self.env =3D os.environ.copy() + + # If venv parameter is specified, run Sphinx from venv + if venv: + bin_dir =3D os.path.join(venv, "bin") + if os.path.isfile(os.path.join(bin_dir, "activate")): + # "activate" virtual env + self.env["PATH"] =3D bin_dir + ":" + self.env["PATH"] + self.env["VIRTUAL_ENV"] =3D venv + if "PYTHONHOME" in self.env: + del self.env["PYTHONHOME"] + print(f"Setting venv to {venv}") + else: + sys.exit(f"Venv {venv} not found.") + + def run_sphinx(self, sphinx_build, sphinx_args, *args, **pwargs): + """ + Executes sphinx-build using current python3 command and setting + -j parameter if possible to run the build in parallel. + """ + + with JobserverExec() as jobserver: + if jobserver.claim: + parallelism =3D str(jobserver.claim) + else: + # As Sphinx has parallelism since version 1.7, we don't ne= ed + # any check here. + parallelism =3D "auto" + + cmd =3D [] + + if self.venv: + cmd.append("python") + else: + cmd.append(sys.executable) + + cmd.append(sphinx_build) + + if parallelism: + cmd.append("-j" + parallelism) + + cmd +=3D sphinx_args + + if self.verbose: + print(" ".join(cmd)) + + rc =3D subprocess.call(cmd, *args, **pwargs) + + def handle_html(self, css, output_dir): + """ + Extra steps for HTML and epub output. + + For such targets, we need to ensure that CSS will be properly + copied to the output _static directory + """ + + if not css: + return + + css =3D os.path.expanduser(css) + if not css.startswith("/"): + css =3D os.path.join(self.srctree, css) + + static_dir =3D os.path.join(output_dir, "_static") + os.makedirs(static_dir, exist_ok=3DTrue) + + try: + shutil.copy2(css, static_dir) + except (OSError, IOError) as e: + print(f"Warning: Failed to copy CSS: {e}", file=3Dsys.stderr) + + def handle_pdf(self, output_dirs): + """ + Extra steps for PDF output. + + As PDF is handled via a LaTeX output, after building the .tex file, + a new build is needed to create the PDF output from the latex + directory. + """ + builds =3D {} + max_len =3D 0 + + for from_dir in output_dirs: + pdf_dir =3D os.path.join(from_dir, "../pdf") + os.makedirs(pdf_dir, exist_ok=3DTrue) + + if self.latexmk_cmd: + latex_cmd =3D [self.latexmk_cmd, f"-{self.pdflatex}"] + else: + latex_cmd =3D [self.pdflatex] + + latex_cmd.extend(shlex.split(self.latexopts)) + + tex_suffix =3D ".tex" + + # Process each .tex file + has_tex =3D False + build_failed =3D False + with os.scandir(from_dir) as it: + for entry in it: + if not entry.name.endswith(tex_suffix): + continue + + name =3D entry.name[:-len(tex_suffix)] + has_tex =3D True + + try: + subprocess.run(latex_cmd + [entry.path], + cwd=3Dfrom_dir, check=3DTrue) + except subprocess.CalledProcessError: + # LaTeX PDF error code is almost useless: it retur= ns + # error codes even when build succeeds but has war= nings. + pass + + # Instead of checking errors, let's do the next best t= hing: + # check if the PDF file was actually created. + + pdf_name =3D name + ".pdf" + pdf_from =3D os.path.join(from_dir, pdf_name) + pdf_to =3D os.path.join(pdf_dir, pdf_name) + + if os.path.exists(pdf_from): + os.rename(pdf_from, pdf_to) + builds[name] =3D os.path.relpath(pdf_to, self.buil= ddir) + else: + builds[name] =3D "FAILED" + build_failed =3D True + + name =3D entry.name.removesuffix(".tex") + max_len =3D max(max_len, len(name)) + + if not has_tex: + name =3D os.path.basename(from_dir) + max_len =3D max(max_len, len(name)) + builds[name] =3D "FAILED (no .tex)" + build_failed =3D True + + msg =3D "Summary" + msg +=3D "\n" + "=3D" * len(msg) + print() + print(msg) + + for pdf_name, pdf_file in builds.items(): + print(f"{pdf_name:<{max_len}}: {pdf_file}") + + print() + + # return an error if a PDF file is missing + + if build_failed: + sys.exit(f"PDF build failed: not all PDF files were created.") + else: + print("All PDF files were built.") + + def handle_info(self, output_dirs): + """ + Extra steps for Info output. + + For texinfo generation, an additional make is needed from the + texinfo directory. + """ + + for output_dir in output_dirs: + try: + subprocess.run(["make", "info"], cwd=3Doutput_dir, check= =3DTrue) + except subprocess.CalledProcessError as e: + sys.exit(f"Error generating info docs: {e}") + + def get_make_media(self): + """ + The media uAPI requires an additional Makefile target. + """ + + mediadir =3D f"{self.obj}/userspace-api/media" + + make =3D os.environ.get("MAKE", "make") + build =3D os.environ.get("build", "-f $(srctree)/scripts/Makefile.= build obj") + + # Check if the script was started outside docs Makefile + if not os.environ.get("obj"): + mediadir =3D os.path.abspath(mediadir) + + # the build makefile var contains macros that require expand + make_media =3D f"{make} {build}=3D{mediadir}" + make_media =3D make_media.replace("$(", "${").replace(")", "}") + make_media =3D os.path.expandvars(make_media) + + # As it also contains multiple arguments, use shlex to split it + return shlex.split(make_media) + + def prepare_media(self, builder): + """ + Run userspace-api/media Makefile. + + The logic behind it are from the initial ports to Sphinx. + They're old and need to be replaced by a proper Sphinx extension. + While we don't do that, we need to explicitly call media Makefile + to build some files. + """ + + cmd =3D self.get_make_media() + [builder] + + if self.verbose: + print(" ".join(cmd)) + + with JobserverExec() as jobserver: + rc =3D jobserver.run(cmd, env=3Dself.env) + + if rc: + cmd_str =3D " ".join(cmd) + sys.exit(f"Failed to run {cmd_str}") + + def cleandocs(self, builder): + + shutil.rmtree(self.builddir, ignore_errors=3DTrue) + + self.prepare_media(builder) + + def build(self, target, sphinxdirs=3DNone, conf=3D"conf.py", + theme=3DNone, css=3DNone, paper=3DNone): + """ + Build documentation using Sphinx. This is the core function of this + module. It prepares all arguments required by sphinx-build. + """ + + builder =3D TARGETS[target]["builder"] + out_dir =3D TARGETS[target].get("out_dir", "") + + # Cleandocs doesn't require sphinx-build + if target =3D=3D "cleandocs": + self.cleandocs(builder) + return + + # Other targets require sphinx-build + sphinxbuild =3D shutil.which(self.sphinxbuild, path=3Dself.env["PA= TH"]) + if not sphinxbuild: + sys.exit(f"Error: {self.sphinxbuild} not found in PATH.\n") + + self.prepare_media(builder) + + if builder =3D=3D "latex": + if not self.pdflatex_cmd and not self.latexmk_cmd: + sys.exit("Error: pdflatex or latexmk required for PDF gene= ration") + + docs_dir =3D os.path.abspath(os.path.join(self.srctree, "Documenta= tion")) + + # Prepare base arguments for Sphinx build + kerneldoc =3D self.kerneldoc + if kerneldoc.startswith(self.srctree): + kerneldoc =3D os.path.relpath(kerneldoc, self.srctree) + + # Prepare common Sphinx options + args =3D [ + "-b", builder, + "-c", docs_dir, + ] + + if builder =3D=3D "latex": + if not paper: + paper =3D PAPER[1] + + args.extend(["-D", f"latex_elements.papersize=3D{paper}paper"]) + + if not self.verbose: + args.append("-q") + + if self.config_rust: + args.extend(["-t", "rustdoc"]) + + if conf: + self.env["SPHINX_CONF"] =3D self.get_path(conf, abs_path=3DTru= e) + + if not sphinxdirs: + sphinxdirs =3D os.environ.get("SPHINXDIRS", ".") + + # sphinxdirs can be a list or a whitespace-separated string + sphinxdirs_list =3D [] + for sphinxdir in sphinxdirs: + if isinstance(sphinxdir, list): + sphinxdirs_list +=3D sphinxdir + else: + for name in sphinxdir.split(" "): + sphinxdirs_list.append(name) + + # Build each directory + output_dirs =3D [] + for sphinxdir in sphinxdirs_list: + src_dir =3D os.path.join(docs_dir, sphinxdir) + doctree_dir =3D os.path.join(self.builddir, ".doctrees") + output_dir =3D os.path.join(self.builddir, sphinxdir, out_dir) + + # Make directory names canonical + src_dir =3D os.path.normpath(src_dir) + doctree_dir =3D os.path.normpath(doctree_dir) + output_dir =3D os.path.normpath(output_dir) + + os.makedirs(doctree_dir, exist_ok=3DTrue) + os.makedirs(output_dir, exist_ok=3DTrue) + + output_dirs.append(output_dir) + + build_args =3D args + [ + "-d", doctree_dir, + "-D", f"kerneldoc_bin=3D{kerneldoc}", + "-D", f"version=3D{self.kernelversion}", + "-D", f"release=3D{self.kernelrelease}", + "-D", f"kerneldoc_srctree=3D{self.srctree}", + src_dir, + output_dir, + ] + + # Execute sphinx-build + try: + self.run_sphinx(sphinxbuild, build_args, env=3Dself.env) + except Exception as e: + sys.exit(f"Build failed: {e}") + + # Ensure that html/epub will have needed static files + if target in ["htmldocs", "epubdocs"]: + self.handle_html(css, output_dir) + + # PDF and Info require a second build step + if target =3D=3D "pdfdocs": + self.handle_pdf(output_dirs) + elif target =3D=3D "infodocs": + self.handle_info(output_dirs) + + @staticmethod + def get_python_version(cmd): + """ + Get python version from a Python binary. As we need to detect if + are out there newer python binaries, we can't rely on sys.release = here. + """ + + result =3D subprocess.run([cmd, "--version"], check=3DTrue, + stdout=3Dsubprocess.PIPE, stderr=3Dsubproc= ess.PIPE, + universal_newlines=3DTrue) + version =3D result.stdout.strip() + + match =3D re.search(r"(\d+\.\d+\.\d+)", version) + if match: + return parse_version(match.group(1)) + + print(f"Can't parse version {version}") + return (0, 0, 0) + + @staticmethod + def find_python(): + """ + Detect if are out there any python 3.xy version newer than the + current one. + + Note: this routine is limited to up to 2 digits for python3. We + may need to update it one day, hopefully on a distant future. + """ + patterns =3D [ + "python3.[0-9]", + "python3.[0-9][0-9]", + ] + + # Seek for a python binary newer than MIN_PYTHON_VERSION + for path in os.getenv("PATH", "").split(":"): + for pattern in patterns: + for cmd in glob(os.path.join(path, pattern)): + if os.path.isfile(cmd) and os.access(cmd, os.X_OK): + version =3D SphinxBuilder.get_python_version(cmd) + if version >=3D MIN_PYTHON_VERSION: + return cmd + + return None + + @staticmethod + def check_python(): + """ + Check if the current python binary satisfies our minimal requireme= nt + for Sphinx build. If not, re-run with a newer version if found. + """ + cur_ver =3D sys.version_info[:3] + if cur_ver >=3D MIN_PYTHON_VERSION: + return + + python_ver =3D ver_str(cur_ver) + + new_python_cmd =3D SphinxBuilder.find_python() + if not new_python_cmd: + sys.exit(f"Python version {python_ver} is not supported anymor= e.") + + # Restart script using the newer version + script_path =3D os.path.abspath(sys.argv[0]) + args =3D [new_python_cmd, script_path] + sys.argv[1:] + + print(f"Python {python_ver} not supported. Changing to {new_python= _cmd}") + + try: + os.execv(new_python_cmd, args) + except OSError as e: + sys.exit(f"Failed to restart with {new_python_cmd}: {e}") + +def main(): + """ + Main function. The only mandatory argument is the target. If not + specified, the other arguments will use default values if not + specified at os.environ. + """ + parser =3D argparse.ArgumentParser(description=3D"Kernel documentation= builder") + + parser.add_argument("target", choices=3Dlist(TARGETS.keys()), + help=3D"Documentation target to build") + parser.add_argument("--sphinxdirs", nargs=3D"+", + help=3D"Specific directories to build") + parser.add_argument("--conf", default=3D"conf.py", + help=3D"Sphinx configuration file") + + parser.add_argument("--theme", help=3D"Sphinx theme to use") + + parser.add_argument("--css", help=3D"Custom CSS file for HTML/EPUB") + + parser.add_argument("--paper", choices=3DPAPER, default=3DPAPER[0], + help=3D"Paper size for LaTeX/PDF output") + + parser.add_argument("-v", "--verbose", action=3D'store_true', + help=3D"place build in verbose mode") + + parser.add_argument("-V", "--venv", nargs=3D'?', const=3Df'{VENV_DEFAU= LT}', + default=3DNone, + help=3Df'If used, run Sphinx from a venv dir (defa= ult dir: {VENV_DEFAULT})') + + args =3D parser.parse_args() + + if not args.verbose: + args.verbose =3D bool(os.environ.get("KBUILD_VERBOSE", "") !=3D "") + + SphinxBuilder.check_python() + + builder =3D SphinxBuilder(venv=3Dargs.venv, verbose=3Dargs.verbose) + + builder.build(args.target, sphinxdirs=3Dargs.sphinxdirs, conf=3Dargs.c= onf, + theme=3Dargs.theme, css=3Dargs.css, paper=3Dargs.paper) + +if __name__ =3D=3D "__main__": + main() --=20 2.50.1