1"""Script to generate doxygen documentation.
2"""
3from __future__ import print_function
4from __future__ import unicode_literals
5from devtools import tarball
6from contextlib import contextmanager
7import subprocess
8import traceback
9import re
10import os
11import sys
12import shutil
13
14@contextmanager
15def cd(newdir):
16    """
17    http://stackoverflow.com/questions/431684/how-do-i-cd-in-python
18    """
19    prevdir = os.getcwd()
20    os.chdir(newdir)
21    try:
22        yield
23    finally:
24        os.chdir(prevdir)
25
26def find_program(*filenames):
27    """find a program in folders path_lst, and sets env[var]
28    @param filenames: a list of possible names of the program to search for
29    @return: the full path of the filename if found, or '' if filename could not be found
30"""
31    paths = os.environ.get('PATH', '').split(os.pathsep)
32    suffixes = ('win32' in sys.platform) and '.exe .com .bat .cmd' or ''
33    for filename in filenames:
34        for name in [filename+ext for ext in suffixes.split(' ')]:
35            for directory in paths:
36                full_path = os.path.join(directory, name)
37                if os.path.isfile(full_path):
38                    return full_path
39    return ''
40
41def do_subst_in_file(targetfile, sourcefile, dict):
42    """Replace all instances of the keys of dict with their values.
43    For example, if dict is {'%VERSION%': '1.2345', '%BASE%': 'MyProg'},
44    then all instances of %VERSION% in the file will be replaced with 1.2345 etc.
45    """
46    with open(sourcefile, 'r') as f:
47        contents = f.read()
48    for (k,v) in list(dict.items()):
49        v = v.replace('\\','\\\\')
50        contents = re.sub(k, v, contents)
51    with open(targetfile, 'w') as f:
52        f.write(contents)
53
54def getstatusoutput(cmd):
55    """cmd is a list.
56    """
57    try:
58        process = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
59        output, _ = process.communicate()
60        status = process.returncode
61    except:
62        status = -1
63        output = traceback.format_exc()
64    return status, output
65
66def run_cmd(cmd, silent=False):
67    """Raise exception on failure.
68    """
69    info = 'Running: %r in %r' %(' '.join(cmd), os.getcwd())
70    print(info)
71    sys.stdout.flush()
72    if silent:
73        status, output = getstatusoutput(cmd)
74    else:
75        status, output = subprocess.call(cmd), ''
76    if status:
77        msg = 'Error while %s ...\n\terror=%d, output="""%s"""' %(info, status, output)
78        raise Exception(msg)
79
80def assert_is_exe(path):
81    if not path:
82        raise Exception('path is empty.')
83    if not os.path.isfile(path):
84        raise Exception('%r is not a file.' %path)
85    if not os.access(path, os.X_OK):
86        raise Exception('%r is not executable by this user.' %path)
87
88def run_doxygen(doxygen_path, config_file, working_dir, is_silent):
89    assert_is_exe(doxygen_path)
90    config_file = os.path.abspath(config_file)
91    with cd(working_dir):
92        cmd = [doxygen_path, config_file]
93        run_cmd(cmd, is_silent)
94
95def build_doc(options,  make_release=False):
96    if make_release:
97        options.make_tarball = True
98        options.with_dot = True
99        options.with_html_help = True
100        options.with_uml_look = True
101        options.open = False
102        options.silent = True
103
104    version = open('version', 'rt').read().strip()
105    output_dir = 'dist/doxygen' # relative to doc/doxyfile location.
106    if not os.path.isdir(output_dir):
107        os.makedirs(output_dir)
108    top_dir = os.path.abspath('.')
109    html_output_dirname = 'jsoncpp-api-html-' + version
110    tarball_path = os.path.join('dist', html_output_dirname + '.tar.gz')
111    warning_log_path = os.path.join(output_dir, '../jsoncpp-doxygen-warning.log')
112    html_output_path = os.path.join(output_dir, html_output_dirname)
113    def yesno(bool):
114        return bool and 'YES' or 'NO'
115    subst_keys = {
116        '%JSONCPP_VERSION%': version,
117        '%DOC_TOPDIR%': '',
118        '%TOPDIR%': top_dir,
119        '%HTML_OUTPUT%': os.path.join('..', output_dir, html_output_dirname),
120        '%HAVE_DOT%': yesno(options.with_dot),
121        '%DOT_PATH%': os.path.split(options.dot_path)[0],
122        '%HTML_HELP%': yesno(options.with_html_help),
123        '%UML_LOOK%': yesno(options.with_uml_look),
124        '%WARNING_LOG_PATH%': os.path.join('..', warning_log_path)
125        }
126
127    if os.path.isdir(output_dir):
128        print('Deleting directory:', output_dir)
129        shutil.rmtree(output_dir)
130    if not os.path.isdir(output_dir):
131        os.makedirs(output_dir)
132
133    do_subst_in_file('doc/doxyfile', options.doxyfile_input_path, subst_keys)
134    run_doxygen(options.doxygen_path, 'doc/doxyfile', 'doc', is_silent=options.silent)
135    if not options.silent:
136        print(open(warning_log_path, 'r').read())
137    index_path = os.path.abspath(os.path.join('doc', subst_keys['%HTML_OUTPUT%'], 'index.html'))
138    print('Generated documentation can be found in:')
139    print(index_path)
140    if options.open:
141        import webbrowser
142        webbrowser.open('file://' + index_path)
143    if options.make_tarball:
144        print('Generating doc tarball to', tarball_path)
145        tarball_sources = [
146            output_dir,
147            'README.md',
148            'LICENSE',
149            'NEWS.txt',
150            'version'
151            ]
152        tarball_basedir = os.path.join(output_dir, html_output_dirname)
153        tarball.make_tarball(tarball_path, tarball_sources, tarball_basedir, html_output_dirname)
154    return tarball_path, html_output_dirname
155
156def main():
157    usage = """%prog
158    Generates doxygen documentation in build/doxygen.
159    Optionally makes a tarball of the documentation to dist/.
160
161    Must be started in the project top directory.
162    """
163    from optparse import OptionParser
164    parser = OptionParser(usage=usage)
165    parser.allow_interspersed_args = False
166    parser.add_option('--with-dot', dest="with_dot", action='store_true', default=False,
167        help="""Enable usage of DOT to generate collaboration diagram""")
168    parser.add_option('--dot', dest="dot_path", action='store', default=find_program('dot'),
169        help="""Path to GraphViz dot tool. Must be full qualified path. [Default: %default]""")
170    parser.add_option('--doxygen', dest="doxygen_path", action='store', default=find_program('doxygen'),
171        help="""Path to Doxygen tool. [Default: %default]""")
172    parser.add_option('--in', dest="doxyfile_input_path", action='store', default='doc/doxyfile.in',
173        help="""Path to doxygen inputs. [Default: %default]""")
174    parser.add_option('--with-html-help', dest="with_html_help", action='store_true', default=False,
175        help="""Enable generation of Microsoft HTML HELP""")
176    parser.add_option('--no-uml-look', dest="with_uml_look", action='store_false', default=True,
177        help="""Generates DOT graph without UML look [Default: False]""")
178    parser.add_option('--open', dest="open", action='store_true', default=False,
179        help="""Open the HTML index in the web browser after generation""")
180    parser.add_option('--tarball', dest="make_tarball", action='store_true', default=False,
181        help="""Generates a tarball of the documentation in dist/ directory""")
182    parser.add_option('-s', '--silent', dest="silent", action='store_true', default=False,
183        help="""Hides doxygen output""")
184    parser.enable_interspersed_args()
185    options, args = parser.parse_args()
186    build_doc(options)
187
188if __name__ == '__main__':
189    main()
190