#!/usr/bin/env python3
# SPDX-License-Identifier: GPL-2.0-or-later
# Copyright (c) 2017-2025 Mauro Carvalho Chehab <mchehab+huawei@kernel.org>
#
# pylint: disable=C0103,C0114,C0115,C0116,C0301,C0302
# pylint: disable=R0902,R0904,R0911,R0912,R0914,R0915,R1705,R1710,E1121
# Note: this script requires at least Python 3.6 to run.
# Don't add changes not compatible with it, it is meant to report
# incompatible python versions.
"""
Dependency checker for Sphinx documentation Kernel build.
This module provides tools to check for all required dependencies needed to
build documentation using Sphinx, including system packages, Python modules
and LaTeX packages for PDF generation.
It detect packages for a subset of Linux distributions used by Kernel
maintainers, showing hints and missing dependencies.
The main class SphinxDependencyChecker handles the dependency checking logic
and provides recommendations for installing missing packages. It supports both
system package installations and Python virtual environments. By default,
system pacage install is recommended.
"""
import argparse
import locale
import os
import re
import subprocess
import sys
from glob import glob
import os.path
src_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.insert(0, os.path.join(src_dir, '../lib/python'))
from kdoc.python_version import PythonVersion
RECOMMENDED_VERSION = PythonVersion("3.4.3").version
MIN_PYTHON_VERSION = PythonVersion("3.7").version
class DepManager:
"""
Manage package dependencies. There are three types of dependencies:
- System: dependencies required for docs build;
- Python: python dependencies for a native distro Sphinx install;
- PDF: dependencies needed by PDF builds.
Each dependency can be mandatory or optional. Not installing an optional
dependency won't break the build, but will cause degradation at the
docs output.
"""
# Internal types of dependencies. Don't use them outside DepManager class.
_SYS_TYPE = 0
_PHY_TYPE = 1
_PDF_TYPE = 2
# Dependencies visible outside the class.
# The keys are tuple with: (type, is_mandatory flag).
#
# Currently we're not using all optional dep types. Yet, we'll keep all
# possible combinations here. They're not many, and that makes easier
# if later needed and for the name() method below
SYSTEM_MANDATORY = (_SYS_TYPE, True)
PYTHON_MANDATORY = (_PHY_TYPE, True)
PDF_MANDATORY = (_PDF_TYPE, True)
SYSTEM_OPTIONAL = (_SYS_TYPE, False)
PYTHON_OPTIONAL = (_PHY_TYPE, False)
PDF_OPTIONAL = (_PDF_TYPE, True)
def __init__(self, pdf):
"""
Initialize internal vars:
- missing: missing dependencies list, containing a distro-independent
name for a missing dependency and its type.
- missing_pkg: ancillary dict containing missing dependencies in
distro namespace, organized by type.
- need: total number of needed dependencies. Never cleaned.
- optional: total number of optional dependencies. Never cleaned.
- pdf: Is PDF support enabled?
"""
self.missing = {}
self.missing_pkg = {}
self.need = 0
self.optional = 0
self.pdf = pdf
@staticmethod
def name(dtype):
"""
Ancillary routine to output a warn/error message reporting
missing dependencies.
"""
if dtype[0] == DepManager._SYS_TYPE:
msg = "build"
elif dtype[0] == DepManager._PHY_TYPE:
msg = "Python"
else:
msg = "PDF"
if dtype[1]:
return f"ERROR: {msg} mandatory deps missing"
else:
return f"Warning: {msg} optional deps missing"
@staticmethod
def is_optional(dtype):
"""Ancillary routine to report if a dependency is optional"""
return not dtype[1]
@staticmethod
def is_pdf(dtype):
"""Ancillary routine to report if a dependency is for PDF generation"""
if dtype[0] == DepManager._PDF_TYPE:
return True
return False
def add_package(self, package, dtype):
"""
Add a package at the self.missing() dictionary.
Doesn't update missing_pkg.
"""
is_optional = DepManager.is_optional(dtype)
self.missing[package] = dtype
if is_optional:
self.optional += 1
else:
self.need += 1
def del_package(self, package):
"""
Remove a package at the self.missing() dictionary.
Doesn't update missing_pkg.
"""
if package in self.missing:
del self.missing[package]
def clear_deps(self):
"""
Clear dependencies without changing needed/optional.
This is an ackward way to have a separate section to recommend
a package after system main dependencies.
TODO: rework the logic to prevent needing it.
"""
self.missing = {}
self.missing_pkg = {}
def check_missing(self, progs):
"""
Update self.missing_pkg, using progs dict to convert from the
agnostic package name to distro-specific one.
Returns an string with the packages to be installed, sorted and
with eventual duplicates removed.
"""
self.missing_pkg = {}
for prog, dtype in sorted(self.missing.items()):
# At least on some LTS distros like CentOS 7, texlive doesn't
# provide all packages we need. When such distros are
# detected, we have to disable PDF output.
#
# So, we need to ignore the packages that distros would
# need for LaTeX to work
if DepManager.is_pdf(dtype) and not self.pdf:
self.optional -= 1
continue
if not dtype in self.missing_pkg:
self.missing_pkg[dtype] = []
self.missing_pkg[dtype].append(progs.get(prog, prog))
install = []
for dtype, pkgs in self.missing_pkg.items():
install += pkgs
return " ".join(sorted(set(install)))
def warn_install(self):
"""
Emit warnings/errors related to missing packages.
"""
output_msg = ""
for dtype in sorted(self.missing_pkg.keys()):
progs = " ".join(sorted(set(self.missing_pkg[dtype])))
try:
name = DepManager.name(dtype)
output_msg += f'{name}:\t{progs}\n'
except KeyError:
raise KeyError(f"ERROR!!!: invalid dtype for {progs}: {dtype}")
if output_msg:
print(f"\n{output_msg}")
class AncillaryMethods:
"""
Ancillary methods that checks for missing dependencies for different
types of types, like binaries, python modules, rpm deps, etc.
"""
@staticmethod
def which(prog):
"""
Our own implementation of which(). We could instead use
shutil.which(), but this function is simple enough.
Pr
|