7"""Tool used for rendering Dart Native Runtime wiki as HTML.
9Usage: runtime/tools/wiki/build/build.py [--deploy]
11If invoked without --deploy the tool would serve development version of the
12wiki which supports fast edit-(auto)refresh cycle.
14If invoked with --deploy it would build deployment version in the
15/tmp/dart-vm-wiki directory.
18from __future__
import annotations
25from pathlib
import Path
26from typing
import Callable, Dict, Sequence
35from aiohttp
import web, WSCloseCode, WSMsgType
36from markdown.extensions.codehilite
import CodeHiliteExtension
37from watchdog.events
import FileSystemEventHandler
38from watchdog.observers
import Observer
39from xrefs
import XrefExtension
40from admonitions
import convert_admonitions
43coloredlogs.install(level=
'INFO',
44 fmt=
'%(asctime)s - %(message)s',
49TOOL_DIR = os.path.dirname(os.path.realpath(__file__))
50SDK_DIR = os.path.relpath(os.path.join(TOOL_DIR,
'..',
'..',
'..',
'..'))
52WIKI_SOURCE_DIR = os.path.join(SDK_DIR,
'runtime',
'docs')
54STYLES_DIR = os.path.relpath(os.path.join(TOOL_DIR,
'..',
'styles'))
55STYLES_INCLUDES_DIR = os.path.join(STYLES_DIR,
'includes')
57TEMPLATES_DIR = os.path.relpath(os.path.join(TOOL_DIR,
'..',
'templates'))
58TEMPLATES_INCLUDES_DIR = os.path.join(TEMPLATES_DIR,
'includes')
60PAGE_TEMPLATE =
'page.html'
62OUTPUT_DIR =
'/tmp/dart-vm-wiki'
63OUTPUT_CSS_DIR = os.path.join(OUTPUT_DIR,
'css')
66shutil.rmtree(OUTPUT_DIR, ignore_errors=
True)
67os.makedirs(OUTPUT_DIR, exist_ok=
True)
68os.makedirs(OUTPUT_CSS_DIR, exist_ok=
True)
71parser = argparse.ArgumentParser()
72parser.add_argument(
'--deploy', dest=
'deploy', action=
'store_true')
73parser.add_argument(
'--deployment-root', dest=
'deployment_root', default=
'')
74parser.set_defaults(deploy=
False)
75args = parser.parse_args()
77is_dev_mode =
not args.deploy
78deployment_root = args.deployment_root
81jinja2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(TEMPLATES_DIR),
87 """Represents a build artifact with its dependencies and way of building."""
90 all: Dict[str, Artifact] = {}
93 listeners: list[Callable] = []
95 def __init__(self, output: str, inputs: Sequence[str]):
96 Artifact.all[output] = self
98 self.
inputs = [os.path.normpath(input)
for input
in inputs]
102 return path
in self.
inputs
105 """Convert artifact inputs into an output."""
109 """Build all artifacts."""
110 Artifact.build_matching(
lambda obj:
True)
114 """Build all artifacts matching the given filter."""
116 for _, artifact
in Artifact.all.items():
117 if predicate(artifact):
123 for listener
in Artifact.listeners:
131 """A single wiki Page (a markdown file)."""
135 output_name =
'index' if name ==
'README' else name
136 super().
__init__(os.path.join(OUTPUT_DIR, f
'{output_name}.html'),
137 [os.path.join(WIKI_SOURCE_DIR, f
'{name}.md')])
140 return f
'Page({self.output} <- {self.inputs[0]})'
143 return path.startswith(TEMPLATES_INCLUDES_DIR)
or super().
depends_on(
146 def _load_markdown(self):
147 with open(self.
inputsinputs[0],
'r', encoding=
'utf-8')
as file:
148 content = file.read()
151 content = re.sub(
r'<!\-\- AUTOGENERATED XREF SECTION \-\->.*$',
158 def _update_xref_section(self, xrefs):
159 with open(self.
inputsinputs[0],
'r', encoding=
'utf-8')
as file:
160 content = file.read()
162 [
'',
'<!-- AUTOGENERATED XREF SECTION -->'] +
163 [f
'[{key}]: {value}' for key, value
in xrefs.items()])
164 with open(self.
inputsinputs[0],
'w', encoding=
'utf-8')
as file:
165 content = re.sub(
r'\n<!-- AUTOGENERATED XREF SECTION -->.*$',
175 template = jinja2_env.get_template(PAGE_TEMPLATE)
176 md_converter = markdown.Markdown(extensions=[
179 CodeHiliteExtension(),
181 'pymdownx.superfences',
185 result = template.render({
194 if not is_dev_mode
and len(md_converter.xrefs) > 0:
197 os.makedirs(os.path.dirname(self.
output), exist_ok=
True)
198 with codecs.open(self.
output,
"w", encoding=
'utf-8')
as file:
201 template_filename = template.filename
206 """Stylesheet written in SASS which needs to be compiled to CSS."""
210 super().
__init__(os.path.join(OUTPUT_CSS_DIR, name +
'.css'),
211 [os.path.join(STYLES_DIR, name +
'.scss')])
214 return f
'Style({self.output} <- {self.inputs[0]})'
217 return path.startswith(STYLES_INCLUDES_DIR)
or super().
depends_on(path)
220 logging.info(
'Build %s from %s', self.
output, self.
inputs[0])
225 """Find all subdirectories called images within wiki."""
227 f.relative_to(
Path(WIKI_SOURCE_DIR)).as_posix()
228 for f
in Path(WIKI_SOURCE_DIR).rglob(
'images')
233 """Find all wiki pages and styles and create corresponding Artifacts."""
235 for file
in Path(WIKI_SOURCE_DIR).rglob(
'*.md'):
236 name = file.relative_to(
Path(WIKI_SOURCE_DIR)).as_posix().rsplit(
240 for file
in Path(STYLES_DIR).glob(
'*.scss'):
245 """Create a directory which can be deployed to static hosting."""
246 logging.info(
'Building wiki for deployment into %s', OUTPUT_DIR)
249 src = os.path.join(WIKI_SOURCE_DIR, images_dir)
250 dst = os.path.join(OUTPUT_DIR, images_dir)
251 logging.info(
'Copying %s <- %s', dst, src)
252 shutil.rmtree(dst, ignore_errors=
True)
253 shutil.copytree(src, dst)
257 logging.info(
'Removing image source files (*.graffle)')
258 for graffle
in Path(OUTPUT_DIR).rglob(
'*.graffle'):
259 logging.info(
'... removing %s', graffle.as_posix())
263 """File system listener rebuilding artifacts based on changed paths."""
266 path = os.path.relpath(event.src_path,
'.')
267 Artifact.build_matching(
lambda artifact: artifact.depends_on(path))
271 """Serve wiki for development (with hot refresh)."""
272 logging.info(
'Serving wiki for development')
278 observer = Observer()
279 observer.schedule(event_handler, TEMPLATES_DIR, recursive=
False)
280 observer.schedule(event_handler, WIKI_SOURCE_DIR, recursive=
True)
281 observer.schedule(event_handler, STYLES_DIR, recursive=
True)
284 async def on_shutdown(app):
285 for ws
in app[
'websockets']:
286 await ws.close(code=WSCloseCode.GOING_AWAY,
287 message=
'Server shutdown')
291 async def handle_artifact(name):
292 source_path = os.path.join(OUTPUT_DIR, name)
293 logging.info(
'Handling source path %s for %s', source_path, name)
294 if source_path
in Artifact.all:
295 return web.FileResponse(source_path)
297 return web.HTTPNotFound()
299 async def handle_page(request):
300 name = request.match_info.get(
'name',
'index.html')
301 if name ==
'' or name.endswith(
'/'):
302 name = name +
'index.html'
303 return await handle_artifact(name)
305 async def handle_css(request):
306 name = request.match_info.get(
'name')
307 return await handle_artifact(
'css/' + name)
309 async def websocket_handler(request):
310 logging.info(
'websocket connection open')
311 ws = web.WebSocketResponse()
312 await ws.prepare(request)
314 loop = asyncio.get_event_loop()
317 logging.info(
'requesting reload')
318 asyncio.run_coroutine_threadsafe(ws.send_str(
'reload'), loop)
320 Artifact.listeners.append(notify)
321 request.app[
'websockets'].
append(ws)
324 if msg.type == WSMsgType.ERROR:
326 'websocket connection closed with exception %s',
329 logging.info(
'websocket connection closing')
330 Artifact.listeners.remove(notify)
331 request.app[
'websockets'].
remove(ws)
333 logging.info(
'websocket connection closed')
336 app = web.Application()
337 app[
'websockets'] = []
339 app.router.add_static(
'/' + images_dir,
340 os.path.join(WIKI_SOURCE_DIR, images_dir))
341 app.router.add_get(
'/ws', websocket_handler)
342 app.router.add_get(
'/css/{name}', handle_css)
343 app.router.add_get(
'/{name:[^{}]*}', handle_page)
344 app.on_shutdown.append(on_shutdown)
345 web.run_app(app, access_log_format=
'"%r" %s')
349 """Main entry point."""
357if __name__ ==
'__main__':
def on_modified(self, event)
bool depends_on(self, str path)
def build_matching(Callable[[Artifact], bool] predicate)
def __init__(self, str output, Sequence[str] inputs)
def _update_xref_section(self, xrefs)
def __init__(self, str name)
def depends_on(self, str path)
def __init__(self, str name)
def depends_on(self, str path)
static void append(char **dst, size_t *count, const char *src, size_t n)
str convert_admonitions(str content)
def find_images_directories()
def serve_for_development()
static SkString join(const CommandLineFlags::StringArray &)