author Andrew Halberstadt <>
Fri, 06 Apr 2018 10:23:49 -0400
changeset 414679 1b366ada7c5dd408e6053a61cf3b729e8d397605
parent 408658 7985866da5d5cc055a6309eabcb954e372544baf
child 414680 15a5e48d01a51dc1b493074443af4021f4133d91
permissions -rw-r--r--
Bug 1410424 - [mozbuild] Add a 'quiet' argument to VirtualenvManager.install_pip_requirements r=mshal Some requirements.txt are very large and result in a lot of package already installed messages. Would be nice to hide this. MozReview-Commit-ID: FQecuePM0zZ

# 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

from __future__ import absolute_import, print_function, unicode_literals

import os
import sys

from mach.decorators import (

import which
import mozhttpd

from mozbuild.base import MachCommandBase

here = os.path.abspath(os.path.dirname(__file__))

class Documentation(MachCommandBase):
    """Helps manage in-tree documentation."""

    @Command('doc', category='devenv',
             description='Generate and display documentation from the tree.')
    @CommandArgument('what', nargs='*', metavar='DIRECTORY [, DIRECTORY]',
                     help='Path(s) to documentation to build and display.')
    @CommandArgument('--format', default='html',
                     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')
    @CommandArgument('--no-open', dest='auto_open', default=True,
                     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('--upload', action='store_true',
                     help='Upload generated files to S3')
    def build_docs(self, what=None, format=None, outdir=None, auto_open=True,
                   http=None, archive=False, upload=False):
        except which.WhichError:
            return die('jsdoc not found - please install from npm.')

            os.path.join(here, 'requirements.txt'), quiet=True)

        import sphinx
        import webbrowser
        import moztreedocs

        if not outdir:
            outdir = os.path.join(self.topobjdir, 'docs')
        if not what:
            what = [os.path.join(self.topsrcdir, 'tools')]

        format_outdir = os.path.join(outdir, format)

        generated = []
        failed = []
        for path in what:
            path = os.path.normpath(os.path.abspath(path))
            docdir = self._find_doc_dir(path)

            if not docdir:
                failed.append((path, 'could not find docs at this location'))

            props = self._project_properties(docdir)
            savedir = os.path.join(format_outdir, props['project'])

            args = [
                '-b', format,
            result = sphinx.build_main(args)
            if result != 0:
                failed.append((path, 'sphinx return code %d' % result))

            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):

        if generated:
            print('\nGenerated documentation:\n%s\n' % '\n'.join(generated))

        if failed:
            failed = ['%s: %s' % (f[0], f[1]) for f in failed]
            return die('failed to generate documentation:\n%s' % '\n'.join(failed))

        if http is not None:
            host, port = http.split(':', 1)
            addr = (host, int(port))
            if len(addr) != 2:
                return die('invalid address: %s' % http)

            httpd = mozhttpd.MozHttpd(host=addr[0], port=addr[1],
            print('listening on %s:%d' % addr)

    def _project_properties(self, path):
        import imp
        path = os.path.join(path, '')
        with open(path, 'r') as fh:
            conf = imp.load_module('doc_conf', fh, path,
                                   ('.py', 'r', imp.PY_SOURCE))

        # Prefer the Mozilla project name, falling back to Sphinx's
        # default variable if it isn't defined.
        project = getattr(conf, 'moz_project_name', None)
        if not project:
            project = conf.project.replace(' ', '_')

        return {
            'project': project,
            'version': getattr(conf, 'version', None)

    def _find_doc_dir(self, path):
        search_dirs = ('doc', 'docs')
        for d in search_dirs:
            p = os.path.join(path, d)
            if os.path.isfile(os.path.join(p, '')):
                return p

    def _s3_upload(self, root, project, version=None):

        from moztreedocs import distribution_files
        from moztreedocs.upload import s3_upload

        # Files are uploaded to multiple locations:
        # <project>/latest
        # <project>/<version>
        # This allows multiple projects and versions to be stored in the
        # S3 bucket.

        files = list(distribution_files(root))

        s3_upload(files, key_prefix='%s/latest' % project)
        if version:
            s3_upload(files, key_prefix='%s/%s' % (project, version))

        # Until we redirect / to main/latest, upload the main docs
        # to the root.
        if project == 'main':

def die(msg, exit_code=1):
    msg = '%s: %s' % (sys.argv[0], msg)
    print(msg, file=sys.stderr)
    return exit_code