| # SPDX-License-Identifier: Apache-2.0 |
| # Copyright 2018 The Meson development team |
| |
| from __future__ import annotations |
| |
| '''This module provides helper functions for generating documentation using hotdoc''' |
| |
| import os, subprocess |
| import typing as T |
| |
| from . import ExtensionModule, ModuleReturnValue, ModuleInfo |
| from .. import build, mesonlib, mlog |
| from ..build import CustomTarget, CustomTargetIndex |
| from ..dependencies import Dependency, InternalDependency |
| from ..interpreterbase import ( |
| InvalidArguments, noPosargs, noKwargs, typed_kwargs, FeatureDeprecated, |
| ContainerTypeInfo, KwargInfo, typed_pos_args |
| ) |
| from ..interpreter.interpreterobjects import _CustomTargetHolder |
| from ..interpreter.type_checking import NoneType |
| from ..mesonlib import File, MesonException |
| from ..programs import ExternalProgram |
| |
| if T.TYPE_CHECKING: |
| from typing_extensions import TypedDict |
| |
| from . import ModuleState |
| from ..environment import Environment |
| from ..interpreter import Interpreter |
| from ..interpreterbase import TYPE_kwargs, TYPE_var |
| |
| _T = T.TypeVar('_T') |
| |
| class GenerateDocKwargs(TypedDict): |
| sitemap: T.Union[str, File, CustomTarget, CustomTargetIndex] |
| index: T.Union[str, File, CustomTarget, CustomTargetIndex] |
| project_version: str |
| html_extra_theme: T.Optional[str] |
| include_paths: T.List[str] |
| dependencies: T.List[T.Union[Dependency, build.StaticLibrary, build.SharedLibrary, CustomTarget, CustomTargetIndex]] |
| depends: T.List[T.Union[CustomTarget, CustomTargetIndex]] |
| gi_c_source_roots: T.List[str] |
| extra_assets: T.List[str] |
| extra_extension_paths: T.List[str] |
| subprojects: T.List['HotdocTarget'] |
| install: bool |
| |
| def ensure_list(value: T.Union[_T, T.List[_T]]) -> T.List[_T]: |
| if not isinstance(value, list): |
| return [value] |
| return value |
| |
| |
| MIN_HOTDOC_VERSION = '0.8.100' |
| |
| file_types = (str, File, CustomTarget, CustomTargetIndex) |
| |
| |
| class HotdocExternalProgram(ExternalProgram): |
| def run_hotdoc(self, cmd: T.List[str]) -> int: |
| return subprocess.run(self.get_command() + cmd, stdout=subprocess.DEVNULL).returncode |
| |
| |
| class HotdocTargetBuilder: |
| |
| def __init__(self, name: str, state: ModuleState, hotdoc: HotdocExternalProgram, interpreter: Interpreter, kwargs): |
| self.hotdoc = hotdoc |
| self.build_by_default = kwargs.pop('build_by_default', False) |
| self.kwargs = kwargs |
| self.name = name |
| self.state = state |
| self.interpreter = interpreter |
| self.include_paths: mesonlib.OrderedSet[str] = mesonlib.OrderedSet() |
| |
| self.builddir = state.environment.get_build_dir() |
| self.sourcedir = state.environment.get_source_dir() |
| self.subdir = state.subdir |
| self.build_command = state.environment.get_build_command() |
| |
| self.cmd: T.List[TYPE_var] = ['conf', '--project-name', name, "--disable-incremental-build", |
| '--output', os.path.join(self.builddir, self.subdir, self.name + '-doc')] |
| |
| self._extra_extension_paths = set() |
| self.extra_assets = set() |
| self.extra_depends = [] |
| self._subprojects = [] |
| |
| def process_known_arg(self, option: str, argname: T.Optional[str] = None, value_processor: T.Optional[T.Callable] = None) -> None: |
| if not argname: |
| argname = option.strip("-").replace("-", "_") |
| |
| value = self.kwargs.pop(argname) |
| if value is not None and value_processor: |
| value = value_processor(value) |
| |
| self.set_arg_value(option, value) |
| |
| def set_arg_value(self, option: str, value: TYPE_var) -> None: |
| if value is None: |
| return |
| |
| if isinstance(value, bool): |
| if value: |
| self.cmd.append(option) |
| elif isinstance(value, list): |
| # Do not do anything on empty lists |
| if value: |
| # https://bugs.python.org/issue9334 (from 2010 :( ) |
| # The syntax with nargs=+ is inherently ambiguous |
| # A workaround for this case is to simply prefix with a space |
| # every value starting with a dash |
| escaped_value = [] |
| for e in value: |
| if isinstance(e, str) and e.startswith('-'): |
| escaped_value += [' %s' % e] |
| else: |
| escaped_value += [e] |
| if option: |
| self.cmd.extend([option] + escaped_value) |
| else: |
| self.cmd.extend(escaped_value) |
| else: |
| # argparse gets confused if value(s) start with a dash. |
| # When an option expects a single value, the unambiguous way |
| # to specify it is with = |
| if isinstance(value, str): |
| self.cmd.extend([f'{option}={value}']) |
| else: |
| self.cmd.extend([option, value]) |
| |
| def check_extra_arg_type(self, arg: str, value: TYPE_var) -> None: |
| if isinstance(value, list): |
| for v in value: |
| self.check_extra_arg_type(arg, v) |
| return |
| |
| valid_types = (str, bool, File, build.IncludeDirs, CustomTarget, CustomTargetIndex, build.BuildTarget) |
| if not isinstance(value, valid_types): |
| raise InvalidArguments('Argument "{}={}" should be of type: {}.'.format( |
| arg, value, [t.__name__ for t in valid_types])) |
| |
| def process_extra_args(self) -> None: |
| for arg, value in self.kwargs.items(): |
| option = "--" + arg.replace("_", "-") |
| self.check_extra_arg_type(arg, value) |
| self.set_arg_value(option, value) |
| |
| def get_value(self, types, argname, default=None, value_processor=None, |
| mandatory=False, force_list=False): |
| if not isinstance(types, list): |
| types = [types] |
| try: |
| uvalue = value = self.kwargs.pop(argname) |
| if value_processor: |
| value = value_processor(value) |
| |
| for t in types: |
| if isinstance(value, t): |
| if force_list and not isinstance(value, list): |
| return [value], uvalue |
| return value, uvalue |
| raise MesonException(f"{argname} field value {value} is not valid," |
| f" valid types are {types}") |
| except KeyError: |
| if mandatory: |
| raise MesonException(f"{argname} mandatory field not found") |
| |
| if default is not None: |
| return default, default |
| |
| return None, None |
| |
| def add_extension_paths(self, paths: T.Union[T.List[str], T.Set[str]]) -> None: |
| for path in paths: |
| if path in self._extra_extension_paths: |
| continue |
| |
| self._extra_extension_paths.add(path) |
| self.cmd.extend(["--extra-extension-path", path]) |
| |
| def replace_dirs_in_string(self, string: str) -> str: |
| return string.replace("@SOURCE_ROOT@", self.sourcedir).replace("@BUILD_ROOT@", self.builddir) |
| |
| def process_gi_c_source_roots(self) -> None: |
| if self.hotdoc.run_hotdoc(['--has-extension=gi-extension']) != 0: |
| return |
| |
| value = self.kwargs.pop('gi_c_source_roots') |
| value.extend([ |
| os.path.join(self.sourcedir, self.state.root_subdir), |
| os.path.join(self.builddir, self.state.root_subdir) |
| ]) |
| |
| self.cmd += ['--gi-c-source-roots'] + value |
| |
| def process_dependencies(self, deps: T.List[T.Union[Dependency, build.StaticLibrary, build.SharedLibrary, CustomTarget, CustomTargetIndex]]) -> T.List[str]: |
| cflags = set() |
| for dep in mesonlib.listify(ensure_list(deps)): |
| if isinstance(dep, InternalDependency): |
| inc_args = self.state.get_include_args(dep.include_directories) |
| cflags.update([self.replace_dirs_in_string(x) |
| for x in inc_args]) |
| cflags.update(self.process_dependencies(dep.libraries)) |
| cflags.update(self.process_dependencies(dep.sources)) |
| cflags.update(self.process_dependencies(dep.ext_deps)) |
| elif isinstance(dep, Dependency): |
| cflags.update(dep.get_compile_args()) |
| elif isinstance(dep, (build.StaticLibrary, build.SharedLibrary)): |
| self.extra_depends.append(dep) |
| for incd in dep.get_include_dirs(): |
| cflags.update(incd.get_incdirs()) |
| elif isinstance(dep, HotdocTarget): |
| # Recurse in hotdoc target dependencies |
| self.process_dependencies(dep.get_target_dependencies()) |
| self._subprojects.extend(dep.subprojects) |
| self.process_dependencies(dep.subprojects) |
| self.include_paths.add(os.path.join(self.builddir, dep.hotdoc_conf.subdir)) |
| self.cmd += ['--extra-assets=' + p for p in dep.extra_assets] |
| self.add_extension_paths(dep.extra_extension_paths) |
| elif isinstance(dep, (CustomTarget, build.BuildTarget)): |
| self.extra_depends.append(dep) |
| elif isinstance(dep, CustomTargetIndex): |
| self.extra_depends.append(dep.target) |
| |
| return [f.strip('-I') for f in cflags] |
| |
| def process_extra_assets(self) -> None: |
| self._extra_assets = self.kwargs.pop('extra_assets') |
| |
| for assets_path in self._extra_assets: |
| self.cmd.extend(["--extra-assets", assets_path]) |
| |
| def process_subprojects(self) -> None: |
| value = self.kwargs.pop('subprojects') |
| |
| self.process_dependencies(value) |
| self._subprojects.extend(value) |
| |
| def flatten_config_command(self) -> T.List[str]: |
| cmd = [] |
| for arg in mesonlib.listify(self.cmd, flatten=True): |
| if isinstance(arg, File): |
| arg = arg.absolute_path(self.state.environment.get_source_dir(), |
| self.state.environment.get_build_dir()) |
| elif isinstance(arg, build.IncludeDirs): |
| for inc_dir in arg.get_incdirs(): |
| cmd.append(os.path.join(self.sourcedir, arg.get_curdir(), inc_dir)) |
| cmd.append(os.path.join(self.builddir, arg.get_curdir(), inc_dir)) |
| |
| continue |
| elif isinstance(arg, (build.BuildTarget, CustomTarget)): |
| self.extra_depends.append(arg) |
| arg = self.interpreter.backend.get_target_filename_abs(arg) |
| elif isinstance(arg, CustomTargetIndex): |
| self.extra_depends.append(arg.target) |
| arg = self.interpreter.backend.get_target_filename_abs(arg) |
| |
| cmd.append(arg) |
| |
| return cmd |
| |
| def generate_hotdoc_config(self) -> None: |
| cwd = os.path.abspath(os.curdir) |
| ncwd = os.path.join(self.sourcedir, self.subdir) |
| mlog.log('Generating Hotdoc configuration for: ', mlog.bold(self.name)) |
| os.chdir(ncwd) |
| if self.hotdoc.run_hotdoc(self.flatten_config_command()) != 0: |
| raise MesonException('hotdoc failed to configure') |
| os.chdir(cwd) |
| |
| def ensure_file(self, value: T.Union[str, File, CustomTarget, CustomTargetIndex]) -> T.Union[File, CustomTarget, CustomTargetIndex]: |
| if isinstance(value, list): |
| res = [] |
| for val in value: |
| res.append(self.ensure_file(val)) |
| return res |
| |
| if isinstance(value, str): |
| return File.from_source_file(self.sourcedir, self.subdir, value) |
| |
| return value |
| |
| def ensure_dir(self, value: str) -> str: |
| if os.path.isabs(value): |
| _dir = value |
| else: |
| _dir = os.path.join(self.sourcedir, self.subdir, value) |
| |
| if not os.path.isdir(_dir): |
| raise InvalidArguments(f'"{_dir}" is not a directory.') |
| |
| return os.path.relpath(_dir, os.path.join(self.builddir, self.subdir)) |
| |
| def check_forbidden_args(self) -> None: |
| for arg in ['conf_file']: |
| if arg in self.kwargs: |
| raise InvalidArguments(f'Argument "{arg}" is forbidden.') |
| |
| def make_targets(self) -> T.Tuple[HotdocTarget, mesonlib.ExecutableSerialisation]: |
| self.check_forbidden_args() |
| self.process_known_arg("--index", value_processor=self.ensure_file) |
| self.process_known_arg("--project-version") |
| self.process_known_arg("--sitemap", value_processor=self.ensure_file) |
| self.process_known_arg("--html-extra-theme", value_processor=self.ensure_dir) |
| self.include_paths.update(self.ensure_dir(v) for v in self.kwargs.pop('include_paths')) |
| self.process_known_arg('--c-include-directories', argname="dependencies", value_processor=self.process_dependencies) |
| self.process_gi_c_source_roots() |
| self.process_extra_assets() |
| self.add_extension_paths(self.kwargs.pop('extra_extension_paths')) |
| self.process_subprojects() |
| self.extra_depends.extend(self.kwargs.pop('depends')) |
| |
| install = self.kwargs.pop('install') |
| self.process_extra_args() |
| |
| fullname = self.name + '-doc' |
| hotdoc_config_name = fullname + '.json' |
| hotdoc_config_path = os.path.join( |
| self.builddir, self.subdir, hotdoc_config_name) |
| with open(hotdoc_config_path, 'w', encoding='utf-8') as f: |
| f.write('{}') |
| |
| self.cmd += ['--conf-file', hotdoc_config_path] |
| self.include_paths.add(os.path.join(self.builddir, self.subdir)) |
| self.include_paths.add(os.path.join(self.sourcedir, self.subdir)) |
| |
| depfile = os.path.join(self.builddir, self.subdir, self.name + '.deps') |
| self.cmd += ['--deps-file-dest', depfile] |
| |
| for path in self.include_paths: |
| self.cmd.extend(['--include-path', path]) |
| |
| if self.state.environment.coredata.get_option(mesonlib.OptionKey('werror', subproject=self.state.subproject)): |
| self.cmd.append('--fatal-warnings') |
| self.generate_hotdoc_config() |
| |
| target_cmd = self.build_command + ["--internal", "hotdoc"] + \ |
| self.hotdoc.get_command() + ['run', '--conf-file', hotdoc_config_name] + \ |
| ['--builddir', os.path.join(self.builddir, self.subdir)] |
| |
| target = HotdocTarget(fullname, |
| subdir=self.subdir, |
| subproject=self.state.subproject, |
| environment=self.state.environment, |
| hotdoc_conf=File.from_built_file( |
| self.subdir, hotdoc_config_name), |
| extra_extension_paths=self._extra_extension_paths, |
| extra_assets=self._extra_assets, |
| subprojects=self._subprojects, |
| command=target_cmd, |
| extra_depends=self.extra_depends, |
| outputs=[fullname], |
| sources=[], |
| depfile=os.path.basename(depfile), |
| build_by_default=self.build_by_default) |
| |
| install_script = None |
| if install: |
| datadir = os.path.join(self.state.get_option('prefix'), self.state.get_option('datadir')) |
| devhelp = self.kwargs.get('devhelp_activate', False) |
| if not isinstance(devhelp, bool): |
| FeatureDeprecated.single_use('hotdoc.generate_doc() devhelp_activate must be boolean', '1.1.0', self.state.subproject) |
| devhelp = False |
| if devhelp: |
| install_from = os.path.join(fullname, 'devhelp') |
| install_to = os.path.join(datadir, 'devhelp') |
| else: |
| install_from = os.path.join(fullname, 'html') |
| install_to = os.path.join(datadir, 'doc', self.name, 'html') |
| |
| install_script = self.state.backend.get_executable_serialisation(self.build_command + [ |
| "--internal", "hotdoc", |
| "--install", install_from, |
| "--docdir", install_to, |
| '--name', self.name, |
| '--builddir', os.path.join(self.builddir, self.subdir)] + |
| self.hotdoc.get_command() + |
| ['run', '--conf-file', hotdoc_config_name]) |
| install_script.tag = 'doc' |
| |
| return (target, install_script) |
| |
| |
| class HotdocTargetHolder(_CustomTargetHolder['HotdocTarget']): |
| def __init__(self, target: HotdocTarget, interp: Interpreter): |
| super().__init__(target, interp) |
| self.methods.update({'config_path': self.config_path_method}) |
| |
| @noPosargs |
| @noKwargs |
| def config_path_method(self, *args: T.Any, **kwargs: T.Any) -> str: |
| conf = self.held_object.hotdoc_conf.absolute_path(self.interpreter.environment.source_dir, |
| self.interpreter.environment.build_dir) |
| return conf |
| |
| |
| class HotdocTarget(CustomTarget): |
| def __init__(self, name: str, subdir: str, subproject: str, hotdoc_conf: File, |
| extra_extension_paths: T.Set[str], extra_assets: T.List[str], |
| subprojects: T.List['HotdocTarget'], environment: Environment, **kwargs: T.Any): |
| super().__init__(name, subdir, subproject, environment, **kwargs, absolute_paths=True) |
| self.hotdoc_conf = hotdoc_conf |
| self.extra_extension_paths = extra_extension_paths |
| self.extra_assets = extra_assets |
| self.subprojects = subprojects |
| |
| def __getstate__(self) -> dict: |
| # Make sure we do not try to pickle subprojects |
| res = self.__dict__.copy() |
| res['subprojects'] = [] |
| |
| return res |
| |
| |
| class HotDocModule(ExtensionModule): |
| |
| INFO = ModuleInfo('hotdoc', '0.48.0') |
| |
| def __init__(self, interpreter: Interpreter): |
| super().__init__(interpreter) |
| self.hotdoc = HotdocExternalProgram('hotdoc') |
| if not self.hotdoc.found(): |
| raise MesonException('hotdoc executable not found') |
| version = self.hotdoc.get_version(interpreter) |
| if not mesonlib.version_compare(version, f'>={MIN_HOTDOC_VERSION}'): |
| raise MesonException(f'hotdoc {MIN_HOTDOC_VERSION} required but not found.)') |
| |
| self.methods.update({ |
| 'has_extensions': self.has_extensions, |
| 'generate_doc': self.generate_doc, |
| }) |
| |
| @noKwargs |
| @typed_pos_args('hotdoc.has_extensions', varargs=str, min_varargs=1) |
| def has_extensions(self, state: ModuleState, args: T.Tuple[T.List[str]], kwargs: TYPE_kwargs) -> bool: |
| return self.hotdoc.run_hotdoc([f'--has-extension={extension}' for extension in args[0]]) == 0 |
| |
| @typed_pos_args('hotdoc.generate_doc', str) |
| @typed_kwargs( |
| 'hotdoc.generate_doc', |
| KwargInfo('sitemap', file_types, required=True), |
| KwargInfo('index', file_types, required=True), |
| KwargInfo('project_version', str, required=True), |
| KwargInfo('html_extra_theme', (str, NoneType)), |
| KwargInfo('include_paths', ContainerTypeInfo(list, str), listify=True, default=[]), |
| # --c-include-directories |
| KwargInfo( |
| 'dependencies', |
| ContainerTypeInfo(list, (Dependency, build.StaticLibrary, build.SharedLibrary, |
| CustomTarget, CustomTargetIndex)), |
| listify=True, |
| default=[], |
| ), |
| KwargInfo( |
| 'depends', |
| ContainerTypeInfo(list, (CustomTarget, CustomTargetIndex)), |
| listify=True, |
| default=[], |
| since='0.64.1', |
| ), |
| KwargInfo('gi_c_source_roots', ContainerTypeInfo(list, str), listify=True, default=[]), |
| KwargInfo('extra_assets', ContainerTypeInfo(list, str), listify=True, default=[]), |
| KwargInfo('extra_extension_paths', ContainerTypeInfo(list, str), listify=True, default=[]), |
| KwargInfo('subprojects', ContainerTypeInfo(list, HotdocTarget), listify=True, default=[]), |
| KwargInfo('install', bool, default=False), |
| allow_unknown=True |
| ) |
| def generate_doc(self, state: ModuleState, args: T.Tuple[str], kwargs: GenerateDocKwargs) -> ModuleReturnValue: |
| project_name = args[0] |
| if any(isinstance(x, (CustomTarget, CustomTargetIndex)) for x in kwargs['dependencies']): |
| FeatureDeprecated.single_use('hotdoc.generate_doc dependencies argument with custom_target', |
| '0.64.1', state.subproject, 'use `depends`', state.current_node) |
| builder = HotdocTargetBuilder(project_name, state, self.hotdoc, self.interpreter, kwargs) |
| target, install_script = builder.make_targets() |
| targets: T.List[T.Union[HotdocTarget, mesonlib.ExecutableSerialisation]] = [target] |
| if install_script: |
| targets.append(install_script) |
| |
| return ModuleReturnValue(target, targets) |
| |
| |
| def initialize(interpreter: Interpreter) -> HotDocModule: |
| mod = HotDocModule(interpreter) |
| mod.interpreter.append_holder_map(HotdocTarget, HotdocTargetHolder) |
| return mod |