diff options
Diffstat (limited to '')
-rw-r--r-- | mesonbuild/modules/hotdoc.py | 457 |
1 files changed, 457 insertions, 0 deletions
diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py new file mode 100644 index 0000000..b73d812 --- /dev/null +++ b/mesonbuild/modules/hotdoc.py @@ -0,0 +1,457 @@ +# Copyright 2018 The Meson development team + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +from __future__ import annotations + +'''This module provides helper functions for generating documentation using hotdoc''' + +import os +import subprocess + +from mesonbuild import mesonlib +from mesonbuild import mlog, build +from mesonbuild.coredata import MesonException +from . import ModuleReturnValue, ModuleInfo +from . import ExtensionModule +from ..dependencies import Dependency, InternalDependency +from ..interpreterbase import ( + InvalidArguments, noPosargs, noKwargs, typed_kwargs, FeatureDeprecated, + ContainerTypeInfo, KwargInfo, typed_pos_args +) +from ..interpreter import CustomTargetHolder +from ..interpreter.type_checking import NoneType +from ..programs import ExternalProgram + + +def ensure_list(value): + if not isinstance(value, list): + return [value] + return value + + +MIN_HOTDOC_VERSION = '0.8.100' + +file_types = (str, mesonlib.File, build.CustomTarget, build.CustomTargetIndex) + + +class HotdocTargetBuilder: + + def __init__(self, name, state, hotdoc, 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() + + 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 = ['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, argname=None, value_processor=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, value): + 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, value): + if isinstance(value, list): + for v in value: + self.check_extra_arg_type(arg, v) + return + + valid_types = (str, bool, mesonlib.File, build.IncludeDirs, build.CustomTarget, build.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): + 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): + 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): + return string.replace("@SOURCE_ROOT@", self.sourcedir).replace("@BUILD_ROOT@", self.builddir) + + def process_gi_c_source_roots(self): + 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): + 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, (build.CustomTarget, build.BuildTarget)): + self.extra_depends.append(dep) + elif isinstance(dep, build.CustomTargetIndex): + self.extra_depends.append(dep.target) + + return [f.strip('-I') for f in cflags] + + def process_extra_assets(self): + 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): + value = self.kwargs.pop('subprojects') + + self.process_dependencies(value) + self._subprojects.extend(value) + + def flatten_config_command(self): + cmd = [] + for arg in mesonlib.listify(self.cmd, flatten=True): + if isinstance(arg, mesonlib.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, build.CustomTarget)): + self.extra_depends.append(arg) + arg = self.interpreter.backend.get_target_filename_abs(arg) + elif isinstance(arg, build.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): + 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) + self.hotdoc.run_hotdoc(self.flatten_config_command()) + os.chdir(cwd) + + def ensure_file(self, value): + if isinstance(value, list): + res = [] + for val in value: + res.append(self.ensure_file(val)) + return res + + if isinstance(value, str): + return mesonlib.File.from_source_file(self.sourcedir, self.subdir, value) + + return value + + def ensure_dir(self, value): + 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): + for arg in ['conf_file']: + if arg in self.kwargs: + raise InvalidArguments(f'Argument "{arg}" is forbidden.') + + def make_targets(self): + 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=mesonlib.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: + install_script = self.state.backend.get_executable_serialisation(self.build_command + [ + "--internal", "hotdoc", + "--install", os.path.join(fullname, 'html'), + '--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): + def __init__(self, target, interp): + super().__init__(target, interp) + self.methods.update({'config_path': self.config_path_method}) + + @noPosargs + @noKwargs + def config_path_method(self, *args, **kwargs): + conf = self.held_object.hotdoc_conf.absolute_path(self.interpreter.environment.source_dir, + self.interpreter.environment.build_dir) + return conf + + +class HotdocTarget(build.CustomTarget): + def __init__(self, name, subdir, subproject, hotdoc_conf, extra_extension_paths, extra_assets, + subprojects, environment, **kwargs): + 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): + # 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): + super().__init__(interpreter) + self.hotdoc = ExternalProgram('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.)') + + def run_hotdoc(cmd): + return subprocess.run(self.hotdoc.get_command() + cmd, stdout=subprocess.DEVNULL).returncode + + self.hotdoc.run_hotdoc = run_hotdoc + 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, args, kwargs): + 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, + build.CustomTarget, build.CustomTargetIndex)), + listify=True, + default=[], + ), + KwargInfo( + 'depends', + ContainerTypeInfo(list, (build.CustomTarget, build.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, args, kwargs): + project_name = args[0] + if any(isinstance(x, (build.CustomTarget, build.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 = [target] + if install_script: + targets.append(install_script) + + return ModuleReturnValue(targets[0], targets) + + +def initialize(interpreter): + mod = HotDocModule(interpreter) + mod.interpreter.append_holder_map(HotdocTarget, HotdocTargetHolder) + return mod |