summaryrefslogtreecommitdiffstats
path: root/mesonbuild/modules/hotdoc.py
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--mesonbuild/modules/hotdoc.py457
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