Bug 1410424 - [docs] Support live reloading with |mach doc| draft
authorAndrew Halberstadt <ahalberstadt@mozilla.com>
Fri, 06 Apr 2018 10:52:56 -0400
changeset 783641 cb65559b2a01f7b73955f38cfe809f73db39fe0a
parent 783640 76437a58b4c919c953fcb7680fb126585fe94795
push id106750
push userahalberstadt@mozilla.com
push dateTue, 17 Apr 2018 15:22:13 +0000
bugs1410424, 1454640
milestone61.0a1
Bug 1410424 - [docs] Support live reloading with |mach doc| 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