Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
build.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2#
3# Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
4# for details. All rights reserved. Use of this source code is governed by a
5# BSD-style license that can be found in the LICENSE file.
6#
7"""Tool used for rendering Dart Native Runtime wiki as HTML.
8
9Usage: runtime/tools/wiki/build/build.py [--deploy]
10
11If invoked without --deploy the tool would serve development version of the
12wiki which supports fast edit-(auto)refresh cycle.
13
14If invoked with --deploy it would build deployment version in the
15/tmp/dart-vm-wiki directory.
16"""
17
18from __future__ import annotations
19
20import logging
21import os
22import re
23import shutil
24import subprocess
25from pathlib import Path
26from typing import Callable, Dict, Sequence
27
28import argparse
29import asyncio
30import codecs
31import coloredlogs
32import jinja2
33import markdown
34
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
41
42# Configure logging to use colors.
43coloredlogs.install(level='INFO',
44 fmt='%(asctime)s - %(message)s',
45 datefmt='%H:%M:%S')
46
47# Declare various directory paths.
48# We expected to be located in runtime/tools/wiki/build.
49TOOL_DIR = os.path.dirname(os.path.realpath(__file__))
50SDK_DIR = os.path.relpath(os.path.join(TOOL_DIR, '..', '..', '..', '..'))
51
52WIKI_SOURCE_DIR = os.path.join(SDK_DIR, 'runtime', 'docs')
53
54STYLES_DIR = os.path.relpath(os.path.join(TOOL_DIR, '..', 'styles'))
55STYLES_INCLUDES_DIR = os.path.join(STYLES_DIR, 'includes')
56
57TEMPLATES_DIR = os.path.relpath(os.path.join(TOOL_DIR, '..', 'templates'))
58TEMPLATES_INCLUDES_DIR = os.path.join(TEMPLATES_DIR, 'includes')
59
60PAGE_TEMPLATE = 'page.html'
61
62OUTPUT_DIR = '/tmp/dart-vm-wiki'
63OUTPUT_CSS_DIR = os.path.join(OUTPUT_DIR, 'css')
64
65# Clean output directory and recreate it.
66shutil.rmtree(OUTPUT_DIR, ignore_errors=True)
67os.makedirs(OUTPUT_DIR, exist_ok=True)
68os.makedirs(OUTPUT_CSS_DIR, exist_ok=True)
69
70# Parse incoming arguments.
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()
76
77is_dev_mode = not args.deploy
78deployment_root = args.deployment_root
79
80# Initialize jinja environment.
81jinja2_env = jinja2.Environment(loader=jinja2.FileSystemLoader(TEMPLATES_DIR),
82 lstrip_blocks=True,
83 trim_blocks=True)
84
85
87 """Represents a build artifact with its dependencies and way of building."""
88
89 # Map of all discovered artifacts.
90 all: Dict[str, Artifact] = {}
91
92 # List of listeners which are notified whenever some artifact is rebuilt.
93 listeners: list[Callable] = []
94
95 def __init__(self, output: str, inputs: Sequence[str]):
96 Artifact.all[output] = self
97 self.output = output
98 self.inputs = [os.path.normpath(input) for input in inputs]
99
100 def depends_on(self, path: str) -> bool:
101 """Check if this"""
102 return path in self.inputs
103
104 def build(self):
105 """Convert artifact inputs into an output."""
106
107 @staticmethod
109 """Build all artifacts."""
110 Artifact.build_matching(lambda obj: True)
111
112 @staticmethod
113 def build_matching(predicate: Callable[[Artifact], bool]):
114 """Build all artifacts matching the given filter."""
115 rebuilt = False
116 for _, artifact in Artifact.all.items():
117 if predicate(artifact):
118 artifact.build()
119 rebuilt = True
120
121 # If any artifacts were rebuilt notify the listeners.
122 if rebuilt:
123 for listener in Artifact.listeners:
124 listener()
125
126
127xref_extension = XrefExtension()
128
129
131 """A single wiki Page (a markdown file)."""
132
133 def __init__(self, name: str):
134 self.name = name
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')])
138
139 def __repr__(self):
140 return f'Page({self.output} <- {self.inputs[0]})'
141
142 def depends_on(self, path: str):
143 return path.startswith(TEMPLATES_INCLUDES_DIR) or super().depends_on(
144 path)
145
146 def _load_markdown(self):
147 with open(self.inputsinputs[0], 'r', encoding='utf-8') as file:
148 content = file.read()
149
150 # Remove autogenerated xref section.
151 content = re.sub(r'<!\-\- AUTOGENERATED XREF SECTION \-\->.*$',
152 '',
153 content,
154 flags=re.DOTALL)
155
156 return convert_admonitions(content)
157
158 def _update_xref_section(self, xrefs):
159 with open(self.inputsinputs[0], 'r', encoding='utf-8') as file:
160 content = file.read()
161 section = '\n'.join(
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 -->.*$',
166 '',
167 content,
168 flags=re.DOTALL)
169 content += section
170 file.write(content)
171
172 def build(self):
173 logging.info('Build %s from %s', self.outputoutput, self.inputsinputs[0])
174
175 template = jinja2_env.get_template(PAGE_TEMPLATE)
176 md_converter = markdown.Markdown(extensions=[
177 'admonition',
178 'extra',
179 CodeHiliteExtension(),
180 'tables',
181 'pymdownx.superfences',
182 'toc',
183 xref_extension,
184 ])
185 result = template.render({
186 'dev':
187 is_dev_mode,
188 'body':
189 md_converter.convert(self._load_markdown()),
190 'root':
191 deployment_root
192 })
193 # pylint: disable=no-member
194 if not is_dev_mode and len(md_converter.xrefs) > 0:
195 self._update_xref_section(md_converter.xrefs)
196
197 os.makedirs(os.path.dirname(self.outputoutput), exist_ok=True)
198 with codecs.open(self.outputoutput, "w", encoding='utf-8') as file:
199 file.write(result)
200
201 template_filename = template.filename # pytype: disable=attribute-error
202 self.inputsinputs = [self.inputsinputs[0], template_filename]
203
204
206 """Stylesheet written in SASS which needs to be compiled to CSS."""
207
208 def __init__(self, name: str):
209 self.name = name
210 super().__init__(os.path.join(OUTPUT_CSS_DIR, name + '.css'),
211 [os.path.join(STYLES_DIR, name + '.scss')])
212
213 def __repr__(self):
214 return f'Style({self.output} <- {self.inputs[0]})'
215
216 def depends_on(self, path: str):
217 return path.startswith(STYLES_INCLUDES_DIR) or super().depends_on(path)
218
219 def build(self):
220 logging.info('Build %s from %s', self.outputoutput, self.inputs[0])
221 subprocess.call(['sass', self.inputs[0], self.outputoutput])
222
223
225 """Find all subdirectories called images within wiki."""
226 return [
227 f.relative_to(Path(WIKI_SOURCE_DIR)).as_posix()
228 for f in Path(WIKI_SOURCE_DIR).rglob('images')
229 ]
230
231
233 """Find all wiki pages and styles and create corresponding Artifacts."""
234 Artifact.all = {}
235 for file in Path(WIKI_SOURCE_DIR).rglob('*.md'):
236 name = file.relative_to(Path(WIKI_SOURCE_DIR)).as_posix().rsplit(
237 '.', 1)[0]
238 Page(name)
239
240 for file in Path(STYLES_DIR).glob('*.scss'):
241 Style(file.stem)
242
243
245 """Create a directory which can be deployed to static hosting."""
246 logging.info('Building wiki for deployment into %s', OUTPUT_DIR)
247 Artifact.build_all()
248 for images_dir in find_images_directories():
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)
254
255 # Some images directories contain OmniGraffle source files which need
256 # to be removed before
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())
260
261
262class ArtifactEventHandler(FileSystemEventHandler):
263 """File system listener rebuilding artifacts based on changed paths."""
264
265 def on_modified(self, event):
266 path = os.path.relpath(event.src_path, '.')
267 Artifact.build_matching(lambda artifact: artifact.depends_on(path))
268
269
271 """Serve wiki for development (with hot refresh)."""
272 logging.info('Serving wiki for development')
273 Artifact.build_all()
274
275 # Watch for file modifications and rebuild dependant artifacts when their
276 # dependencies change.
277 event_handler = ArtifactEventHandler()
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)
282 observer.start()
283
284 async def on_shutdown(app):
285 for ws in app['websockets']:
286 await ws.close(code=WSCloseCode.GOING_AWAY,
287 message='Server shutdown')
288 observer.stop()
289 observer.join()
290
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)
296 else:
297 return web.HTTPNotFound()
298
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)
304
305 async def handle_css(request):
306 name = request.match_info.get('name')
307 return await handle_artifact('css/' + name)
308
309 async def websocket_handler(request):
310 logging.info('websocket connection open')
311 ws = web.WebSocketResponse()
312 await ws.prepare(request)
313
314 loop = asyncio.get_event_loop()
315
316 def notify():
317 logging.info('requesting reload')
318 asyncio.run_coroutine_threadsafe(ws.send_str('reload'), loop)
319
320 Artifact.listeners.append(notify)
321 request.app['websockets'].append(ws)
322 try:
323 async for msg in ws:
324 if msg.type == WSMsgType.ERROR:
325 logging.error(
326 'websocket connection closed with exception %s',
327 ws.exception())
328 finally:
329 logging.info('websocket connection closing')
330 Artifact.listeners.remove(notify)
331 request.app['websockets'].remove(ws)
332
333 logging.info('websocket connection closed')
334 return ws
335
336 app = web.Application()
337 app['websockets'] = []
338 for images_dir in find_images_directories():
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')
346
347
348def main():
349 """Main entry point."""
351 if is_dev_mode:
353 else:
355
356
357if __name__ == '__main__':
358 main()
on_modified(self, event)
Definition build.py:265
bool depends_on(self, str path)
Definition build.py:100
build_matching(Callable[[Artifact], bool] predicate)
Definition build.py:113
__init__(self, str output, Sequence[str] inputs)
Definition build.py:95
_load_markdown(self)
Definition build.py:146
_update_xref_section(self, xrefs)
Definition build.py:158
__init__(self, str name)
Definition build.py:133
depends_on(self, str path)
Definition build.py:142
__repr__(self)
Definition build.py:139
__init__(self, str name)
Definition build.py:208
__repr__(self)
Definition build.py:213
depends_on(self, str path)
Definition build.py:216
static void append(char **dst, size_t *count, const char *src, size_t n)
Definition editor.cpp:211
Definition build.py:1
main()
Definition build.py:348
build_for_deploy()
Definition build.py:244
find_artifacts()
Definition build.py:232
find_images_directories()
Definition build.py:224
serve_for_development()
Definition build.py:270
Definition main.py:1