Merge pull request #4016 from thiblahute/hotdoc

modules: Add an 'hotdoc' module
pull/4099/head
Jussi Pakkanen 6 years ago committed by GitHub
commit b86f2fd17f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      ciimage/Dockerfile
  2. 5
      docs/README.md
  3. 16
      docs/hotdoc.json
  4. 79
      docs/markdown/Hotdoc-module.md
  5. 22
      docs/markdown/snippets/hotdoc_module.md
  6. 22
      docs/meson.build
  7. 3
      docs/sitemap.txt
  8. 3
      docs/theme/extra/templates/navbar_links.html
  9. 3
      mesonbuild/build.py
  10. 7
      mesonbuild/interpreter.py
  11. 3
      mesonbuild/mesonmain.py
  12. 386
      mesonbuild/modules/hotdoc.py
  13. 36
      mesonbuild/scripts/hotdochelper.py
  14. 1
      test cases/frameworks/23 hotdoc/doc/index.md
  15. 19
      test cases/frameworks/23 hotdoc/doc/meson.build
  16. 3
      test cases/frameworks/23 hotdoc/doc/sitemap.txt
  17. 9
      test cases/frameworks/23 hotdoc/meson.build

@ -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

@ -3,7 +3,7 @@
1. Get [hotdoc](https://hotdoc.github.io/installing.html) (0.8.9 required)
1. Run hotdoc in the docs/ directory:
hotdoc run
../meson/meson.py build/
## Upload
@ -12,5 +12,4 @@ removes the html pages and replaces with the new content.
You can simply run:
hotdoc run --git-upload-activate
ninja -C build/ upload

@ -1,16 +0,0 @@
{
"extra_assets": [
"images/"
],
"html_extra_theme": "theme/extra/",
"include_paths": [
"markdown/"
],
"output": "built_docs/",
"project_name": "Meson documentation",
"project_version": "1.0",
"default-license": "CC-BY-SAv4.0",
"sitemap": "sitemap.txt",
"git_upload_repository": "git@github.com:jpakkane/jpakkane.github.io.git",
"edit_on_github_repository": "https://github.com/mesonbuild/meson/"
}

@ -0,0 +1,79 @@
---
short-description: Hotdoc module
authors:
- name: Thibault Saunier
email: tsaunier@igalia.com
years: [2018]
has-copyright: false
...
# Hotdoc module
This module provides helper functions for generating documentation using
[hotdoc].
*Added 0.48.0*
## Usage
To use this module, just do: **`hotdoc = import('hotdoc')`**. The
following functions will then be available as methods on the object
with the name `hotdoc`. You can, of course, replace the name `hotdoc`
with anything else.
### hotdoc.generate_doc()
Generates documentation using [hotdoc] and installs it into `$prefix/share/doc/html`.
**Positional argument:**
* `project_name`: The name of the hotdoc project
**Keyworded arguments:**
* `sitemap` (*[string] or [file]*) (**required**): The hotdoc sitemap file
* `index` (*[string] or [file]*) (**required**): Location of the index file
* `dependencies`(*[targets]*): Targets on which the documentation generation depends on.
* `subprojects`: A list of `HotdocTarget` that are used as subprojects for hotdoc to generate
the documentation.
* ... Any argument of `hotdoc` can be used replacing dashes (`-`) with underscores (`_`).
For a full list of available parameters, just have a look at `hotdoc help`.
[file]: Reference-manual.md#files
[string]: Reference-manual.md#string-object
[targets]: Reference-manual.md#build-target-object
**Returns:**
`HotdocTarget`: A [`custom_target`](Reference-manual.md#custom-target-object) with the
following extra methods:
* `config_path`: Path to the generated `hotdoc` configuration file.
### hotdoc.has_extensions()
**Positional arguments:**
* `...`: The hotdoc extension names to look for
**No keyworded arguments**
**Returns:** `true` if all the extensions where found, `false` otherwise.
### Example
``` meson
hotdoc = import('hotdoc')
hotdoc.generate_doc('foobar',
project_version: '0.1',
sitemap: 'sitemap.txt',
index: 'index.md',
c_sources: ['path/to/file.c'],
c_smart_index: true,
languages: ['c'],
install: true,
)
```
[hotdoc]: https://hotdoc.github.io/

@ -0,0 +1,22 @@
## Hotdoc module
A new module has been written to ease generation of [hotdoc](https://hotdoc.github.io/) based
documentation. It supports complex use cases such as hotdoc subprojects (to create documentation
portals) and makes it straight forward to leverage full capabilities of hotdoc.
Simple usage:
``` meson
hotdoc = import('hotdoc')
hotdoc.generate_doc(
'foobar',
c_smart_index: true,
project_version: '0.1',
sitemap: 'sitemap.txt',
index: 'index.md',
c_sources: ['path/to/file.c'],
languages: ['c'],
install: true,
)
```

@ -0,0 +1,22 @@
project('Meson documentation', version: '1.0')
hotdoc = import('hotdoc')
documentation = hotdoc.generate_doc(meson.project_name(),
project_version: meson.project_version(),
sitemap: 'sitemap.txt',
build_by_default: true,
index: 'markdown/index.md',
install: false,
extra_assets: ['images/'],
include_paths: ['markdown'],
default_license: 'CC-BY-SAv4.0',
html_extra_theme: join_paths('theme', 'extra'),
git_upload_repository: 'git@github.com:jpakkane/jpakkane.github.io.git',
edit_on_github_repository: 'https://github.com/mesonbuild/meson/',
syntax_highlighting_activate: true,
)
run_target('upload',
command: [find_program('hotdoc'), 'run', '--conf-file', documentation.config_path(),
'--git-upload']
)

@ -29,7 +29,9 @@ index.md
Subprojects.md
Disabler.md
Modules.md
Dlang-module.md
Gnome-module.md
Hotdoc-module.md
i18n-module.md
Icestorm-module.md
Pkgconfig-module.md
@ -40,7 +42,6 @@ index.md
RPM-module.md
Simd-module.md
Windows-module.md
Dlang-module.md
Java.md
Vala.md
D.md

@ -13,7 +13,8 @@
("Qt4-module.html","Qt4"), \
("Qt5-module.html","Qt5"), \
("RPM-module.html","RPM"), \
("Windows-module.html","Windows")):
("Windows-module.html","Windows"), \
("Hotdoc-module.html","Hotdoc")):
<li>
<a href="@tup[0]">@tup[1]</a>
</li>

@ -1830,7 +1830,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)):

@ -2001,7 +2001,7 @@ class Interpreter(InterpreterBase):
return [self.holderify(x) for x in item]
if isinstance(item, build.CustomTarget):
return CustomTargetHolder(item, self)
elif isinstance(item, (int, str)) or item is None:
elif isinstance(item, (int, str, bool)) or item is None:
return item
elif isinstance(item, build.Executable):
return ExecutableHolder(item, self)
@ -2027,6 +2027,9 @@ class Interpreter(InterpreterBase):
def process_new_values(self, invalues):
invalues = listify(invalues)
for v in invalues:
if isinstance(v, (RunTargetHolder, CustomTargetHolder, BuildTargetHolder)):
v = v.held_object
if isinstance(v, (build.BuildTarget, build.CustomTarget, build.RunTarget)):
self.add_target(v.name, v)
elif isinstance(v, list):
@ -2045,6 +2048,8 @@ class Interpreter(InterpreterBase):
self.process_new_values(v.sources[0])
elif hasattr(v, 'held_object'):
pass
elif isinstance(v, (int, str, bool)):
pass
else:
raise InterpreterException('Module returned a value of unknown type.')

@ -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

@ -0,0 +1,386 @@
# 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:
self.add_extension_paths([path])
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)

@ -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)

@ -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')

@ -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')
Loading…
Cancel
Save