import argparse
import io
import os
import re
import subprocess
import sys
import toml.decoder
import typing
from typing import Dict, List, Tuple, Callable, Any, TYPE_CHECKING
if TYPE_CHECKING:
CompletedProcess = subprocess.CompletedProcess[Any]
else:
CompletedProcess = subprocess.CompletedProcess
rustdoc_packages = [
'derive-deftly',
'derive-deftly-macros',
'derive-deftly-tests',
]
rustdoc_opts = ['--all-features']
docsrs_url_pat = 'https://docs.rs/%s/%s/%s'
mdbook_dir = 'book'
mdbook_url = 'https://diziet.pages.torproject.net/rust-derive-deftly/latest/guide'
mdbook_input_dir = mdbook_dir + '/book/html'
mdbook_slug = 'guide'
target_dir = 'target'
out_unified = 'doc-unified'
toml_data: Dict[str, Any]
rustdoc_rustc_packages = list([
p.replace('-','_')
for p in rustdoc_packages
])
def raise_fn(e: Exception) -> None:
raise(e)
def mdbook_leaf() -> str:
return 'mdbook-%s' % mdbook_slug
if sys.version_info >= (3, 9): Pattern_str = re.Pattern[str]
else: Pattern_str = typing.Any
class Massager:
def __init__(self) -> None:
self._replacements: List[Tuple[Pattern_str, str]] = []
def add(self, url: str, path: str) -> None:
url_re = re.escape(url)
i_re = r'(?<=")' + url_re + r'/*(?=["#])'
self._replacements.append((re.compile(i_re), path + '/index.html'))
m_re = r'(?<=")' + url_re + r'/+'
self._replacements.append((re.compile(m_re), path + '/'))
def process_tree(self, in_tree: str, out_leaf: str) -> None:
for dirpath, dirnames, filenames in os.walk(in_tree, onerror=raise_fn):
if not dirpath.startswith(in_tree):
raise
subdir_s = dirpath[len(in_tree):].lstrip('/')
if subdir_s == "": subdir = []
else: subdir = re.split(r'/+', subdir_s)
depth = len(subdir)
out_dir = '/'.join([target_dir, out_unified, out_leaf] + subdir)
up = '../' * (depth + 1)
os.makedirs(out_dir, exist_ok=True)
repls = [
(repl_re, up + repl_path)
for repl_re, repl_path in self._replacements
]
for f in filenames:
out_file = '%s/%s' % (out_dir, f)
in_file = '%s/%s' % (dirpath, f)
try:
self._process_file(repls, f, in_file, out_file)
except:
print('Error generating %s' % out_file,
file=sys.stderr)
raise
def _process_file(self,
repls: List[Tuple[Pattern_str, str]],
f: str, in_file: str, out_file: str) -> None:
out_tmp = out_file + '.tmp'
if f.endswith('.html'):
data = open(in_file, 'r', encoding='utf-8').read()
for repl_re, repl_fn in repls:
data = repl_re.sub(repl_fn, data)
out = open(out_tmp, 'w', encoding='utf-8')
out.write(data)
out.flush()
else:
try: os.remove(out_tmp)
except FileNotFoundError: pass
os.link(in_file, out_tmp)
os.rename(out_tmp, out_file)
def build_rustdocs(cli_rustdoc_arg: List[str]) -> None:
shfrag = '''
: ${CARGO:=cargo}
set -x
$CARGO $NAILINGCARGO_CARGO_OPTIONS doc "$@"
'''
cmd = ['sh','-ec', shfrag, 'x'] + rustdoc_opts + cli_rustdoc_arg
for p in rustdoc_packages:
cmd += ['-p', p]
subprocess.run(cmd, check=True)
def build_massager() -> Massager:
massager = Massager();
massager.add(mdbook_url, mdbook_leaf())
for p in rustdoc_packages:
for p_rustc in rustdoc_rustc_packages:
massager.add(docsrs_url_pat % (p, 'latest', p_rustc), p_rustc)
main_p_rustdoc = rustdoc_packages[0]
main_p_rustc = rustdoc_rustc_packages[0]
main_p_versioned_url = docsrs_url_pat % (
main_p_rustdoc, toml_data['package']['version'], main_p_rustc
)
massager.add(main_p_versioned_url, main_p_rustc)
return massager
def massage_rustdocs() -> None:
massager = build_massager()
for d in rustdoc_rustc_packages:
massager.process_tree(
'%s/doc/%s' % (target_dir, d),
d,
)
for d in os.listdir('%s/doc' % target_dir):
if d in rustdoc_rustc_packages:
continue
d_o = '/'.join([target_dir, out_unified, d])
subprocess.run(['rm','-rf','--',d_o], check=True)
os.symlink('../doc/' + d, d_o)
def build_mdbook(cli_mdbook_arg: List[str]) -> None:
cmd = ['maint/build-mdbook'] + cli_mdbook_arg
subprocess.run(['sh', '-xec', '"$@"', 'x'] + cmd, check=True)
def massage_mdbook() -> None:
massager = build_massager()
massager.process_tree(
mdbook_input_dir,
mdbook_leaf(),
)
def check_links_1() -> CompletedProcess:
return subprocess.run(['maint/check-doc-links'])
def check_links_2(done: CompletedProcess) -> None:
if done.returncode == 1:
print('** link check failed - broken doc hyperlinks **\n',
file=sys.stderr)
sys.exit(1)
else:
done.check_returncode()
def print_unified_urls() -> None:
print('''
unified docs, links adjusted for local reading, available in:''')
cwd = os.getcwd()
for subdir in [rustdoc_rustc_packages[0], mdbook_leaf()]:
d = '/'.join([cwd, target_dir, out_unified, subdir, "index.html"])
print(' file://%s' % d)
print('')
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--dev', '-D',
dest='rustdoc_arg', action='append_const',
const='--document-private-items', default=[],
help='Document innards (pass --document-private-items to rustdoc)')
parser.add_argument('--rustdoc-arg', '-R', action='append', default=[],
help='Pass an option through to cargo doc (rustdoc)')
parser.add_argument('--mdbook-arg', '-M', action='append', default=[],
help='Pass an option through to mdbook')
parser.add_argument('--no-linkcheck',
dest='linkcheck', action='store_false', default=True,
help='Do not check internal hyperlinks')
args = parser.parse_args()
build_rustdocs(args.rustdoc_arg)
build_mdbook(args.mdbook_arg)
global toml_data
toml_data = toml.decoder.load('Cargo.toml')
massage_rustdocs()
massage_mdbook()
if args.linkcheck:
links_checked = check_links_1()
else:
links_checked = None
print_unified_urls()
if not(links_checked is None):
check_links_2(links_checked)
main()