Bug 1410424 - [docs] Support live reloading with |mach doc| r=mshal
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Fri, 06 Apr 2018 10:52:56 -0400
changeset 468197 d34cf7a17b3bed597b611413fb73ad7f0f8a5d1d
parent 468196 15a5e48d01a51dc1b493074443af4021f4133d91
child 468198 0ffcaeada9fc28141aacc8832888246b1d55040e
push id9165
push userasasaki@mozilla.com
push dateThu, 26 Apr 2018 21:04:54 +0000
treeherdermozilla-beta@064c3804de2e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmshal
bugs1410424, 1454640
milestone61.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1410424 - [docs] Support live reloading with |mach doc| r=mshal This changes the default to opening a livereload webserver after doc generation (as opposed to opening the index file). Any changes to the specified path will result in a rebuild and refresh of the browser. For example, if you run: ./mach doc tools/lint The linting docs will be built, served and opened in a browser. Modifying any file under 'tools/lint/docs' will refresh the browser with your changes. To disable this behaviour and simply open the index file, you can pass in '--no-serve'. The '--no-open' flag will continue to work (both with http and the file system). One caveat to this patch is that when generating the root docs (by running |mach doc|), we don't watch all possible doc paths (just the root one under 'tools/docs/'). This will probably be fixed in the follow-up bug 1454640. MozReview-Commit-ID: FQecuePM0zZ
taskcluster/ci/source-test/doc.yml
tools/docs/mach_commands.py
tools/docs/requirements.txt
--- a/taskcluster/ci/source-test/doc.yml
+++ b/taskcluster/ci/source-test/doc.yml
@@ -13,17 +13,17 @@ generate:
         artifacts:
             - type: file
               name: public/docs.tar.gz
               path: /builds/worker/checkouts/gecko/docs-out/main.tar.gz
     run:
         using: run-task
         command: >
             cd /builds/worker/checkouts/gecko &&
-            ./mach doc --outdir docs-out --no-open --archive
+            ./mach doc --outdir docs-out --no-open --no-serve --archive
         sparse-profile: sphinx-docs
     optimization:
         skip-unless-schedules: [docs]
 
 upload:
     description: Generate and upload the Sphinx documentation
     platform: lint/opt
     treeherder:
@@ -33,14 +33,14 @@ upload:
     run-on-projects: [mozilla-central]
     worker-type: aws-provisioner-v1/gecko-t-linux-xlarge
     worker:
         docker-image: {in-tree: "lint"}
         max-run-time: 1800
         taskcluster-proxy: true
     run:
         using: run-task
-        command: cd /builds/worker/checkouts/gecko && ./mach doc --upload --no-open
+        command: cd /builds/worker/checkouts/gecko && ./mach doc --upload --no-open --no-serve
         sparse-profile: sphinx-docs
     scopes:
         - secrets:get:project/releng/gecko/build/level-{level}/gecko-docs-upload
     optimization:
         skip-unless-schedules: [docs]
--- a/tools/docs/mach_commands.py
+++ b/tools/docs/mach_commands.py
@@ -1,118 +1,128 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, # You can obtain one at http://mozilla.org/MPL/2.0/.
 
 from __future__ import absolute_import, print_function, unicode_literals
 
 import os
 import sys
+from functools import partial
 
 from mach.decorators import (
     Command,
     CommandArgument,
     CommandProvider,
 )
 
 import which
-import mozhttpd
-
 from mozbuild.base import MachCommandBase
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 @CommandProvider
 class Documentation(MachCommandBase):
     """Helps manage in-tree documentation."""
 
     @Command('doc', category='devenv',
-             description='Generate and display documentation from the tree.')
+             description='Generate and serve documentation from the tree.')
     @CommandArgument('path', default=None, metavar='DIRECTORY', nargs='?',
                      help='Path to documentation to build and display.')
-    @CommandArgument('--format', default='html',
+    @CommandArgument('--format', default='html', dest='fmt',
                      help='Documentation format to write.')
     @CommandArgument('--outdir', default=None, metavar='DESTINATION',
                      help='Where to write output.')
     @CommandArgument('--archive', action='store_true',
-                     help='Write a gzipped tarball of generated docs')
+                     help='Write a gzipped tarball of generated docs.')
     @CommandArgument('--no-open', dest='auto_open', default=True,
                      action='store_false',
                      help="Don't automatically open HTML docs in a browser.")
-    @CommandArgument('--http', const=':6666', metavar='ADDRESS', nargs='?',
-                     help='Serve documentation on an HTTP server, '
-                          'e.g. ":6666".')
+    @CommandArgument('--no-serve', dest='serve', default=True, action='store_false',
+                     help="Don't serve the generated docs after building.")
+    @CommandArgument('--http', default='localhost:5500', metavar='ADDRESS',
+                     help='Serve documentation on the specified host and port, '
+                          'default "localhost:5500".')
     @CommandArgument('--upload', action='store_true',
-                     help='Upload generated files to S3')
-    def build_docs(self, path=None, format=None, outdir=None, auto_open=True,
-                   http=None, archive=False, upload=False):
+                     help='Upload generated files to S3.')
+    def build_docs(self, path=None, fmt='html', outdir=None, auto_open=True,
+                   serve=True, http=None, archive=False, upload=False):
         try:
             which.which('jsdoc')
         except which.WhichError:
             return die('jsdoc not found - please install from npm.')
 
         self._activate_virtualenv()
         self.virtualenv_manager.install_pip_requirements(
             os.path.join(here, 'requirements.txt'), quiet=True)
 
-        import sphinx
+        import moztreedocs
         import webbrowser
-        import moztreedocs
+        from livereload import Server
 
         outdir = outdir or os.path.join(self.topobjdir, 'docs')
-        format_outdir = os.path.join(outdir, format)
+        format_outdir = os.path.join(outdir, fmt)
 
         path = path or os.path.join(self.topsrcdir, 'tools')
         path = os.path.normpath(os.path.abspath(path))
 
         docdir = self._find_doc_dir(path)
         if not docdir:
             return die('failed to generate documentation:\n'
                        '%s: could not find docs at this location' % path)
 
         props = self._project_properties(docdir)
         savedir = os.path.join(format_outdir, props['project'])
 
-        args = [
-            'sphinx',
-            '-b', format,
-            docdir,
-            savedir,
-        ]
-        result = sphinx.build_main(args)
+        run_sphinx = partial(self._run_sphinx, docdir, savedir, fmt)
+        result = run_sphinx()
         if result != 0:
             return die('failed to generate documentation:\n'
                        '%s: sphinx return code %d' % (path, result))
         else:
             print('\nGenerated documentation:\n%s' % savedir)
 
         if archive:
             archive_path = os.path.join(outdir,
                                         '%s.tar.gz' % props['project'])
             moztreedocs.create_tarball(archive_path, savedir)
             print('Archived to %s' % archive_path)
 
         if upload:
             self._s3_upload(savedir, props['project'], props['version'])
 
-        index_path = os.path.join(savedir, 'index.html')
-        if not http and auto_open and os.path.isfile(index_path):
-            webbrowser.open(index_path)
+        if not serve:
+            index_path = os.path.join(savedir, 'index.html')
+            if auto_open and os.path.isfile(index_path):
+                webbrowser.open(index_path)
+            return
 
-        if http is not None:
+        # Create livereload server. Any files modified in the specified docdir
+        # will cause a re-build and refresh of the browser (if open).
+        try:
             host, port = http.split(':', 1)
-            addr = (host, int(port))
-            if len(addr) != 2:
-                return die('invalid address: %s' % http)
+            port = int(port)
+        except ValueError:
+            return die('invalid address: %s' % http)
+
+        server = Server()
+        server.watch(docdir, run_sphinx)
+        server.serve(host=host, port=port, root=savedir,
+                     open_url_delay=0.1 if auto_open else None)
 
-            httpd = mozhttpd.MozHttpd(host=addr[0], port=addr[1],
-                                      docroot=format_outdir)
-            print('listening on %s:%d' % addr)
-            httpd.start(block=True)
+    def _run_sphinx(self, docdir, savedir, fmt='html'):
+        import sphinx
+        args = [
+            'sphinx',
+            '-b', fmt,
+            docdir,
+            savedir,
+        ]
+        return sphinx.build_main(args)
 
     def _project_properties(self, path):
         import imp
         path = os.path.join(path, 'conf.py')
         with open(path, 'r') as fh:
             conf = imp.load_module('doc_conf', fh, path,
                                    ('.py', 'r', imp.PY_SOURCE))
 
--- a/tools/docs/requirements.txt
+++ b/tools/docs/requirements.txt
@@ -63,8 +63,26 @@ parsimonious==0.7.0 \
     --hash=sha256:396d424f64f834f9463e81ba79a331661507a21f1ed7b644f7f6a744006fd938
 sphinx-js==2.1 \
     --hash=sha256:8c12b2b7ccc6941cbc7c70e4fada903e2947376b48ce07cbb72c72d88f0eef1e
 CommonMark==0.5.4 \
     --hash=sha256:34d73ec8085923c023930dfc0bcd1c4286e28a2a82de094bb72fabcc0281cbe5
 recommonmark==0.4.0 \
     --hash=sha256:cd8bf902e469dae94d00367a8197fb7b81fcabc9cfb79d520e0d22d0fbeaa8b7 \
     --hash=sha256:6e29c723abcf5533842376d87c4589e62923ecb6002a8e059eb608345ddaff9d
+futures==3.2.0 \
+    --hash=sha256:ec0a6cb848cc212002b9828c3e34c675e0c9ff6741dc445cab6fdd4e1085d1f1 \
+    --hash=sha256:9ec02aa7d674acb8618afb127e27fde7fc68994c0437ad759fa094a574adb265
+singledispatch==3.4.0.3 \
+    --hash=sha256:833b46966687b3de7f438c761ac475213e53b306740f1abfaa86e1d1aae56aa8 \
+    --hash=sha256:5b06af87df13818d14f08a028e42f566640aef80805c3b50c5056b086e3c2b9c
+backports_abc==0.5 \
+    --hash=sha256:52089f97fe7a9aa0d3277b220c1d730a85aefd64e1b2664696fe35317c5470a7 \
+    --hash=sha256:033be54514a03e255df75c5aee8f9e672f663f93abb723444caec8fe43437bde
+tornado==5.0.1 \
+    --hash=sha256:69194436190b777abf0b631a692b0b29ba4157d18eeee07327b486e033b944dc \
+    --hash=sha256:186ba4f280429a24120f329c7c08ea91818ff6bf47ed2ccb66f8f460698fc4ed \
+    --hash=sha256:b5bf7407f88327b80e666dabf91a1e7beb11236855a5c65ba5cf0e9e25ae296b \
+    --hash=sha256:4d192236a9ffee54cb0032f22a8a0cfa64258872f1d83d71f3356681f69a37be \
+    --hash=sha256:3e9a2333362d3dad7876d902595b64aea1a2f91d0df13191ea1f8bca5a447771
+livereload==2.5.1 \
+    --hash=sha256:5ed6506f5d526ee712da9f3739c27714e6f3376f3e481728d298efceae0ec83a \
+    --hash=sha256:422de10d7ea9467a1ba27cbaffa84c74b809d96fb1598d9de4b9b676adf35e2c