diff options
author | Thibault Saunier <tsaunier@igalia.com> | 2018-08-14 16:05:26 -0300 |
---|---|---|
committer | Thibault Saunier <tsaunier@igalia.com> | 2018-08-28 18:18:40 -0300 |
commit | 378bd4df0e3ac6c38588eea2de590cbe0fe74af2 (patch) | |
tree | d2a9431dfe02cf3b2ed977cc95a4802fcffbeaba | |
parent | 221ac05c6924ebd03f11632fd6abd4f5f9acc493 (diff) | |
download | meson-378bd4df0e3ac6c38588eea2de590cbe0fe74af2.zip meson-378bd4df0e3ac6c38588eea2de590cbe0fe74af2.tar.gz meson-378bd4df0e3ac6c38588eea2de590cbe0fe74af2.tar.bz2 |
modules: Add an 'hotdoc' module
hotdoc: http://github.com/hotdoc/hotdoc/
-rw-r--r-- | ciimage/Dockerfile | 1 | ||||
-rw-r--r-- | mesonbuild/build.py | 3 | ||||
-rw-r--r-- | mesonbuild/mesonmain.py | 3 | ||||
-rw-r--r-- | mesonbuild/modules/hotdoc.py | 390 | ||||
-rw-r--r-- | mesonbuild/scripts/hotdochelper.py | 36 | ||||
-rw-r--r-- | test cases/frameworks/23 hotdoc/doc/index.md | 1 | ||||
-rw-r--r-- | test cases/frameworks/23 hotdoc/doc/meson.build | 19 | ||||
-rw-r--r-- | test cases/frameworks/23 hotdoc/doc/sitemap.txt | 3 | ||||
-rw-r--r-- | test cases/frameworks/23 hotdoc/meson.build | 9 |
9 files changed, 464 insertions, 1 deletions
diff --git a/ciimage/Dockerfile b/ciimage/Dockerfile index c398fad..25cc6fa 100644 --- a/ciimage/Dockerfile +++ b/ciimage/Dockerfile @@ -12,6 +12,7 @@ RUN apt-get -y update && apt-get -y upgrade \ && apt-get -y install qt4-linguist-tools \ && apt-get -y install python-dev \ && apt-get -y install libomp-dev openssh-client \ +&& apt-get -y install -y clang libclang-dev llvm-dev flex \ && python3 -m pip install hotdoc codecov \ && dub fetch urld \ && dub build urld --compiler=gdc diff --git a/mesonbuild/build.py b/mesonbuild/build.py index c1cb8a8..e03d7a6 100644 --- a/mesonbuild/build.py +++ b/mesonbuild/build.py @@ -1820,7 +1820,8 @@ class CustomTarget(Target): while hasattr(ed, 'held_object'): ed = ed.held_object if not isinstance(ed, (CustomTarget, BuildTarget)): - raise InvalidArguments('Can only depend on toplevel targets: custom_target or build_target (executable or a library)') + raise InvalidArguments('Can only depend on toplevel targets: custom_target or build_target (executable or a library) got: %s(%s)' + % (type(ed), ed)) self.extra_depends.append(ed) for i in depend_files: if isinstance(i, (File, str)): diff --git a/mesonbuild/mesonmain.py b/mesonbuild/mesonmain.py index 68a2ddb..5000965 100644 --- a/mesonbuild/mesonmain.py +++ b/mesonbuild/mesonmain.py @@ -200,6 +200,9 @@ def run_script_command(args): elif cmdname == 'msgfmthelper': import mesonbuild.scripts.msgfmthelper as abc cmdfunc = abc.run + elif cmdname == 'hotdoc': + import mesonbuild.scripts.hotdochelper as abc + cmdfunc = abc.run elif cmdname == 'regencheck': import mesonbuild.scripts.regen_checker as abc cmdfunc = abc.run diff --git a/mesonbuild/modules/hotdoc.py b/mesonbuild/modules/hotdoc.py new file mode 100644 index 0000000..79db2f9 --- /dev/null +++ b/mesonbuild/modules/hotdoc.py @@ -0,0 +1,390 @@ +# 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. + +'''This module provides helper functions for generating documentation using hotdoc''' + +import os +from collections import OrderedDict + +from mesonbuild import mesonlib +from mesonbuild import mlog, build +from mesonbuild.coredata import MesonException +from . import ModuleReturnValue +from . import ExtensionModule +from . import get_include_args +from ..dependencies import Dependency, InternalDependency, ExternalProgram +from ..interpreterbase import FeatureNew, InvalidArguments, noPosargs, noKwargs +from ..interpreter import CustomTargetHolder + + +def ensure_list(value): + if not isinstance(value, list): + return [value] + return value + + +MIN_HOTDOC_VERSION = '0.8.100' + + +class HotdocTargetBuilder: + def __init__(self, name, state, hotdoc, kwargs): + self.hotdoc = hotdoc + self.build_by_default = kwargs.pop('build_by_default', False) + self.kwargs = kwargs + self.name = name + self.state = state + self.include_paths = OrderedDict() + + 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._dependencies = [] + self._subprojects = [] + + def process_known_arg(self, option, types, argname=None, + value_processor=None, mandatory=False, + force_list=False): + if not argname: + argname = option.strip("-").replace("-", "_") + + value, _ = self.get_value( + types, argname, None, value_processor, mandatory, force_list) + + self.set_arg_value(option, value) + + def set_arg_value(self, option, value): + if value is None: + return + + if isinstance(value, bool): + self.cmd.append(option) + elif isinstance(value, list): + # Do not do anything on empty lists + if value: + if option: + self.cmd.extend([option] + value) + else: + self.cmd.extend(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 + + if not isinstance(value, (str, bool, mesonlib.File)): + raise InvalidArguments('Argument "%s=%s" should be a string.' % (arg, value)) + + 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("%s field value %s is not valid," + " valid types are %s" % (argname, value, + types)) + except KeyError: + if mandatory: + raise MesonException("%s mandatory field not found" % argname) + + if default is not None: + return default, default + + return None, None + + def setup_extension_paths(self, paths): + if not isinstance(paths, list): + paths = [paths] + + for path in paths: + try: + self.add_extension_paths([path]) + except subprocess.CalledProcessError as e: + raise MesonException( + "Could not setup hotdoc extension %s: %s" % (paths, e)) + + return [] + + 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 process_extra_extension_paths(self): + self.get_value([list, str], 'extra_extensions_paths', + default="", value_processor=self.setup_extension_paths) + + def replace_dirs_in_string(self, string): + return string.replace("@SOURCE_ROOT@", self.sourcedir).replace("@BUILD_ROOT@", self.builddir) + + def process_dependencies(self, deps): + cflags = set() + for dep in mesonlib.listify(ensure_list(deps)): + dep = getattr(dep, "held_object", dep) + if isinstance(dep, InternalDependency): + inc_args = 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._dependencies.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.add_include_path(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) or isinstance(dep, build.BuildTarget): + self._dependencies.append(dep) + + return [f.strip('-I') for f in cflags] + + def process_extra_assets(self): + self._extra_assets, _ = self.get_value("--extra-assets", (str, list), default=[], + force_list=True) + for assets_path in self._extra_assets: + self.cmd.extend(["--extra-assets", assets_path]) + + def process_subprojects(self): + _, value = self.get_value([ + list, HotdocTarget], argname="subprojects", + force_list=True, value_processor=self.process_dependencies) + + if value is not None: + 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()) + + 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 not isinstance(value, mesonlib.File): + 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('"%s" is not a directory.' % _dir) + + return os.path.relpath(_dir, os.path.join(self.builddir, self.subdir)) + + def check_forbiden_args(self): + for arg in ['conf_file']: + if arg in self.kwargs: + raise InvalidArguments('Argument "%s" is forbidden.' % arg) + + def add_include_path(self, path): + self.include_paths[path] = path + + def make_targets(self): + self.check_forbiden_args() + file_types = (str, mesonlib.File) + self.process_known_arg("--index", file_types, mandatory=True, value_processor=self.ensure_file) + self.process_known_arg("--sitemap", file_types, mandatory=True, value_processor=self.ensure_file) + self.process_known_arg("--html-extra-theme", str, value_processor=self.ensure_dir) + self.process_known_arg(None, list, "include_paths", force_list=True, + value_processor=lambda x: [self.add_include_path(self.ensure_dir(v)) for v in ensure_list(x)]) + self.process_known_arg('--c-include-directories', + [Dependency, build.StaticLibrary, build.SharedLibrary, list], argname="dependencies", + force_list=True, value_processor=self.process_dependencies) + self.process_extra_assets() + self.process_extra_extension_paths() + self.process_subprojects() + + install, install = self.get_value(bool, "install", mandatory=False) + 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') as f: + f.write('{}') + + self.cmd += ['--conf-file', hotdoc_config_path] + self.add_include_path(os.path.join(self.builddir, self.subdir)) + self.add_include_path(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.keys(): + self.cmd.extend(['--include-path', path]) + 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, + 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, + depends=self._dependencies, + output=fullname, + depfile=os.path.basename(depfile), + build_by_default=self.build_by_default) + + install_script = None + if install is True: + install_script = HotdocRunScript(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]) + + 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 self.interpreter.holderify(conf) + + +class HotdocTarget(build.CustomTarget): + def __init__(self, name, subdir, subproject, hotdoc_conf, extra_extension_paths, extra_assets, + subprojects, **kwargs): + super().__init__(name, subdir, subproject, 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 HotdocRunScript(build.RunScript): + def __init__(self, script, args): + super().__init__(script, args) + + +class HotDocModule(ExtensionModule): + @FeatureNew('Hotdoc Module', '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') + + try: + from hotdoc.run_hotdoc import run # noqa: F401 + self.hotdoc.run_hotdoc = run + except Exception as e: + raise MesonException('hotdoc %s required but not found. (%s)' % ( + MIN_HOTDOC_VERSION, e)) + + @noKwargs + def has_extensions(self, state, args, kwargs): + res = self.hotdoc.run_hotdoc(['--has-extension'] + args) == 0 + return ModuleReturnValue(res, [res]) + + def generate_doc(self, state, args, kwargs): + if len(args) != 1: + raise MesonException('One positional argument is' + ' required for the project name.') + + project_name = args[0] + builder = HotdocTargetBuilder(project_name, state, self.hotdoc, kwargs) + target, install_script = builder.make_targets() + targets = [HotdocTargetHolder(target, self.interpreter)] + if install_script: + targets.append(install_script) + + return ModuleReturnValue(targets[0], targets) + + +def initialize(interpreter): + return HotDocModule(interpreter) diff --git a/mesonbuild/scripts/hotdochelper.py b/mesonbuild/scripts/hotdochelper.py new file mode 100644 index 0000000..826745d --- /dev/null +++ b/mesonbuild/scripts/hotdochelper.py @@ -0,0 +1,36 @@ +import os +import shutil +import subprocess + +from . import destdir_join + +import argparse + +parser = argparse.ArgumentParser() +parser.add_argument('--install') +parser.add_argument('--extra-extension-path', action="append", default=[]) +parser.add_argument('--name') +parser.add_argument('--builddir') +parser.add_argument('--project-version') + + +def run(argv): + options, args = parser.parse_known_args(argv) + subenv = os.environ.copy() + + for ext_path in options.extra_extension_path: + subenv['PYTHONPATH'] = subenv.get('PYTHONPATH', '') + ':' + ext_path + + res = subprocess.call(args, cwd=options.builddir, env=subenv) + if res != 0: + exit(res) + + if options.install: + source_dir = os.path.join(options.builddir, options.install) + destdir = os.environ.get('DESTDIR', '') + installdir = destdir_join(destdir, + os.path.join(os.environ['MESON_INSTALL_PREFIX'], + 'share/doc/', options.name, "html")) + + shutil.rmtree(installdir, ignore_errors=True) + shutil.copytree(source_dir, installdir) diff --git a/test cases/frameworks/23 hotdoc/doc/index.md b/test cases/frameworks/23 hotdoc/doc/index.md new file mode 100644 index 0000000..ea5eeb1 --- /dev/null +++ b/test cases/frameworks/23 hotdoc/doc/index.md @@ -0,0 +1 @@ +# Hello world! diff --git a/test cases/frameworks/23 hotdoc/doc/meson.build b/test cases/frameworks/23 hotdoc/doc/meson.build new file mode 100644 index 0000000..a09bff0 --- /dev/null +++ b/test cases/frameworks/23 hotdoc/doc/meson.build @@ -0,0 +1,19 @@ +hotdoc = import('hotdoc') + +target = hotdoc.generate_doc( + 'foobar', + c_smart_index: true, + project_version: '0.1', + sitemap: 'sitemap.txt', + index: 'index.md', + c_sources: files('../../10 gtk-doc/include/foo.h'), + languages: ['c'], + install: true, +) + +assert(target.config_path() == target.full_path() + '.json', + 'Hotdoc config paths do not match.' +) + +assert(hotdoc.has_extensions('search') == true, + 'Extension "search" provided by hotdoc core should always be found') diff --git a/test cases/frameworks/23 hotdoc/doc/sitemap.txt b/test cases/frameworks/23 hotdoc/doc/sitemap.txt new file mode 100644 index 0000000..b82354a --- /dev/null +++ b/test cases/frameworks/23 hotdoc/doc/sitemap.txt @@ -0,0 +1,3 @@ +index.md + c-index + diff --git a/test cases/frameworks/23 hotdoc/meson.build b/test cases/frameworks/23 hotdoc/meson.build new file mode 100644 index 0000000..191569d --- /dev/null +++ b/test cases/frameworks/23 hotdoc/meson.build @@ -0,0 +1,9 @@ +project('hotdoc', 'c') + +hotdoc = find_program('hotdoc', required: false) +if not hotdoc.found() + error('MESON_SKIP_TEST hotdoc not found.') +endif + +subdir('doc') + |