The Meson Build System http://mesonbuild.com/
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

172 lines
6.4 KiB

#!/usr/bin/env python3
# SPDX-License-Identifier: Apache-2.0
# Copyright 2018 The Meson development team
'''
Regenerate markdown docs by using `meson.py` from the root dir
'''
import argparse
import os
import re
import subprocess
import sys
import textwrap
import json
import typing as T
from pathlib import Path
from urllib.request import urlopen
PathLike = T.Union[Path,str]
def _get_meson_output(root_dir: Path, args: T.List) -> str:
env = os.environ.copy()
env['COLUMNS'] = '80'
return subprocess.run([str(sys.executable), str(root_dir/'meson.py')] + args, check=True, capture_output=True, text=True, env=env).stdout.strip()
def get_commands(help_output: str) -> T.Set[str]:
# Python's argument parser might put the command list to its own line. Or it might not.
assert(help_output.startswith('usage: '))
lines = help_output.split('\n')
line1 = lines[0]
line2 = lines[1]
if '{' in line1:
cmndline = line1
else:
assert('{' in line2)
cmndline = line2
cmndstr = cmndline.split('{')[1]
assert('}' in cmndstr)
help_commands = set(cmndstr.split('}')[0].split(','))
assert(len(help_commands) > 0)
return {c.strip() for c in help_commands}
def get_commands_data(root_dir: Path) -> T.Dict[str, T.Any]:
usage_start_pattern = re.compile(r'^usage: ', re.MULTILINE)
positional_start_pattern = re.compile(r'^positional arguments:[\t ]*[\r\n]+', re.MULTILINE)
docs: fix command help regenerator on python 3.10 In https://github.com/python/cpython/pull/23858 the section header for option flags was changed from "optional arguments" to "options" with the rationale that they are not (necessarily) at all optional, while GNU coreutils calls them options. In fact, POSIX calls them options (-o) and option-arguments (-o val) and operands ("positional arguments") so it is indeed a mess, but argparse is not yet perfect. Still, fix the documentation generator for now so that it is compatible with python 3.10 as well. Fixes traceback on building the docs with: ``` [1/4] Generating gen_docs with a custom command FAILED: gen_docs.stamp /home/eschwartz/git/meson/docs/../tools/regenerate_docs.py --output-dir /home/eschwartz/git/meson/docs/builddir --dummy-output-file gen_docs.stamp Traceback (most recent call last): File "/home/eschwartz/git/meson/docs/../tools/regenerate_docs.py", line 160, in <module> regenerate_docs(output_dir=args.output_dir, File "/home/eschwartz/git/meson/docs/../tools/regenerate_docs.py", line 146, in regenerate_docs generate_hotdoc_includes(root_dir, output_dir) File "/home/eschwartz/git/meson/docs/../tools/regenerate_docs.py", line 113, in generate_hotdoc_includes cmd_data = get_commands_data(root_dir) File "/home/eschwartz/git/meson/docs/../tools/regenerate_docs.py", line 106, in get_commands_data cmd_data[cmd] = parse_cmd(cmd_output) File "/home/eschwartz/git/meson/docs/../tools/regenerate_docs.py", line 65, in parse_cmd assert arguments_start AssertionError ```
3 years ago
options_start_pattern = re.compile(r'^(optional arguments|options):[\t ]*[\r\n]+', re.MULTILINE)
commands_start_pattern = re.compile(r'^[A-Za-z ]*[Cc]ommands:[\t ]*[\r\n]+', re.MULTILINE)
def get_next_start(iterators: T.Sequence[T.Any], end: T.Optional[int]) -> int:
return next((i.start() for i in iterators if i), end)
def normalize_text(text: str) -> str:
# clean up formatting
out = text
out = re.sub(r'\r\n', r'\r', out, flags=re.MULTILINE) # replace newlines with a linux EOL
out = re.sub(r'^ +$', '', out, flags=re.MULTILINE) # remove trailing whitespace
out = re.sub(r'(?:^\n+|\n+$)', '', out) # remove trailing empty lines
return out
def parse_cmd(cmd: str) -> T.Dict[str, str]:
cmd_len = len(cmd)
usage = usage_start_pattern.search(cmd)
positionals = positional_start_pattern.search(cmd)
options = options_start_pattern.search(cmd)
commands = commands_start_pattern.search(cmd)
arguments_start = get_next_start([positionals, options, commands], None)
assert arguments_start
# replace `usage:` with `$` and dedent
dedent_size = (usage.end() - usage.start()) - len('$ ')
usage_text = textwrap.dedent(f'{dedent_size * " "}$ {normalize_text(cmd[usage.end():arguments_start])}')
return {
'usage': usage_text,
'arguments': normalize_text(cmd[arguments_start:cmd_len]),
}
def clean_dir_arguments(text: str) -> str:
# Remove platform specific defaults
args = [
'prefix',
'bindir',
'datadir',
'includedir',
'infodir',
'libdir',
'libexecdir',
'localedir',
'localstatedir',
'mandir',
'sbindir',
'sharedstatedir',
'sysconfdir'
]
out = text
for a in args:
out = re.sub(r'(--' + a + r' .+?)\s+\(default:.+?\)(\.)?', r'\1\2', out, flags=re.MULTILINE|re.DOTALL)
return out
output = _get_meson_output(root_dir, ['--help'])
commands = get_commands(output)
commands.remove('help')
cmd_data = dict()
for cmd in commands:
cmd_output = _get_meson_output(root_dir, [cmd, '--help'])
cmd_data[cmd] = parse_cmd(cmd_output)
if cmd in ['setup', 'configure']:
cmd_data[cmd]['arguments'] = clean_dir_arguments(cmd_data[cmd]['arguments'])
return cmd_data
def generate_hotdoc_includes(root_dir: Path, output_dir: Path) -> None:
cmd_data = get_commands_data(root_dir)
for cmd, parsed in cmd_data.items():
for typ in parsed.keys():
with open(output_dir / (cmd+'_'+typ+'.inc'), 'w', encoding='utf-8') as f:
f.write(parsed[typ])
def generate_wrapdb_table(output_dir: Path) -> None:
url = urlopen('https://wrapdb.mesonbuild.com/v2/releases.json')
releases = json.loads(url.read().decode())
with open(output_dir / 'wrapdb-table.md', 'w', encoding='utf-8') as f:
f.write('| Project | Versions | Provided dependencies | Provided programs |\n')
f.write('| ------- | -------- | --------------------- | ----------------- |\n')
for name, info in releases.items():
versions = []
added_tags = set()
for v in info['versions']:
tag, build = v.rsplit('-', 1)
if tag not in added_tags:
added_tags.add(tag)
versions.append(f'[{v}](https://wrapdb.mesonbuild.com/v2/{name}_{v}/{name}.wrap)')
# Highlight latest version.
versions_str = f'<big>**{versions[0]}**</big><br/>' + ', '.join(versions[1:])
dependency_names = info.get('dependency_names', [])
dependency_names_str = ', '.join(dependency_names)
program_names = info.get('program_names', [])
program_names_str = ', '.join(program_names)
f.write(f'| {name} | {versions_str} | {dependency_names_str} | {program_names_str} |\n')
def regenerate_docs(output_dir: PathLike,
dummy_output_file: T.Optional[PathLike]) -> None:
if not output_dir:
raise ValueError(f'Output directory value is not set')
output_dir = Path(output_dir).resolve()
output_dir.mkdir(parents=True, exist_ok=True)
root_dir = Path(__file__).resolve().parent.parent
generate_hotdoc_includes(root_dir, output_dir)
generate_wrapdb_table(output_dir)
if dummy_output_file:
with open(output_dir/dummy_output_file, 'w', encoding='utf-8') as f:
f.write('dummy file for custom_target output')
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Generate meson docs')
parser.add_argument('--output-dir', required=True)
parser.add_argument('--dummy-output-file', type=str)
args = parser.parse_args()
regenerate_docs(output_dir=args.output_dir,
dummy_output_file=args.dummy_output_file)