commit
b86f2fd17f
17 changed files with 595 additions and 23 deletions
@ -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'] |
||||
) |
@ -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 @@ |
||||
# Hello world! |
@ -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,3 @@ |
||||
index.md |
||||
c-index |
||||
|
@ -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…
Reference in new issue