#!/usr/bin/env python
# Copyright 2014 The Chromium Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.
"""Script to regenerate API docs using doxygen.
"""
import argparse
import collections
import json
import os
import shutil
import subprocess
import sys
import tempfile
import urllib2
if sys.version_info < (2, 7, 0):
sys.stderr.write("python 2.7 or later is required run this script\n")
sys.exit(1)
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DOC_DIR = os.path.dirname(SCRIPT_DIR)
ChannelInfo = collections.namedtuple('ChannelInfo', ['branch', 'version'])
def Trace(msg):
if Trace.verbose:
sys.stderr.write(str(msg) + '\n')
Trace.verbose = False
def GetChannelInfo():
url = 'http://omahaproxy.appspot.com/json'
u = urllib2.urlopen(url)
try:
data = json.loads(u.read())
finally:
u.close()
channel_info = {}
for os_row in data:
osname = os_row['os']
if osname not in ('win', 'mac', 'linux'):
continue
for version_row in os_row['versions']:
channel = version_row['channel']
# We don't display canary docs.
if channel.startswith('canary'):
continue
version = version_row['version'].split('.')[0] # Major version
branch = version_row['true_branch']
if branch is None:
branch = 'trunk'
if channel in channel_info:
existing_info = channel_info[channel]
if branch != existing_info.branch:
sys.stderr.write('Warning: found different branch numbers for '
'channel %s: %s vs %s. Using %s.\n' % (
channel, branch, existing_info.branch, existing_info.branch))
else:
channel_info[channel] = ChannelInfo(branch, version)
return channel_info
def RemoveFile(filename):
if os.path.exists(filename):
os.remove(filename)
def RemoveDir(dirname):
if os.path.exists(dirname):
shutil.rmtree(dirname)
def HasBranchHeads():
cmd = ['git', 'for-each-ref', '--format=%(refname)',
'refs/remotes/branch-heads']
output = subprocess.check_output(cmd).splitlines()
return output != []
def CheckoutDirectories(dest_dirname, refname, root_path, patterns=None):
treeish = '%s:%s' % (refname, root_path)
cmd = ['git', 'ls-tree', '--full-tree', '-r', treeish]
if patterns:
cmd.extend(patterns)
Trace('Running \"%s\":' % ' '.join(cmd))
output = subprocess.check_output(cmd)
for line in output.splitlines():
info, rel_filename = line.split('\t')
sha = info.split(' ')[2]
Trace(' %s %s' % (sha, rel_filename))
cmd = ['git', 'show', sha]
blob = subprocess.check_output(cmd)
filename = os.path.join(dest_dirname, rel_filename)
dirname = os.path.dirname(filename)
if not os.path.exists(dirname):
os.makedirs(dirname)
Trace(' writing to %s' % filename)
with open(filename, 'w') as f:
f.write(blob)
def CheckoutPepperDocs(branch, doc_dirname):
Trace('Removing directory %s' % doc_dirname)
RemoveDir(doc_dirname)
if branch == 'master':
refname = 'refs/remotes/origin/master'
else:
refname = 'refs/remotes/branch-heads/%s' % branch
Trace('Checking out docs into %s' % doc_dirname)
subdirs = ['api', 'generators', 'cpp', 'utility']
CheckoutDirectories(doc_dirname, refname, 'ppapi', subdirs)
# The IDL generator needs PLY (a python lexing library); check it out into
# generators.
ply_dirname = os.path.join(doc_dirname, 'generators', 'ply')
Trace('Checking out PLY into %s' % ply_dirname)
CheckoutDirectories(ply_dirname, refname, 'third_party/ply')
def FixPepperDocLinks(doc_dirname):
# TODO(binji): We can remove this step when the correct links are in the
# stable branch.
Trace('Looking for links to fix in Pepper headers...')
for root, dirs, filenames in os.walk(doc_dirname):
# Don't recurse into .svn
if '.svn' in dirs:
dirs.remove('.svn')
for filename in filenames:
header_filename = os.path.join(root, filename)
Trace(' Checking file %r...' % header_filename)
replacements = {
'<a href="/native-client/{{pepperversion}}/devguide/coding/audio">':
'<a href="/native-client/devguide/coding/audio.html">',
'<a href="/native-client/devguide/coding/audio">':
'<a href="/native-client/devguide/coding/audio.html">',
'<a href="/native-client/{{pepperversion}}/pepperc/globals_defs"':
'<a href="globals_defs.html"',
'<a href="../pepperc/ppb__image__data_8h.html">':
'<a href="../c/ppb__image__data_8h.html">'}
with open(header_filename) as f:
lines = []
replaced = False
for line in f:
for find, replace in replacements.iteritems():
pos = line.find(find)
if pos != -1:
Trace(' Found %r...' % find)
replaced = True
line = line[:pos] + replace + line[pos + len(find):]
lines.append(line)
if replaced:
Trace(' Writing new file.')
with open(header_filename, 'w') as f:
f.writelines(lines)
def GenerateCHeaders(pepper_version, doc_dirname):
script = os.path.join(os.pardir, 'generators', 'generator.py')
cwd = os.path.join(doc_dirname, 'api')
out_dirname = os.path.join(os.pardir, 'c')
cmd = [sys.executable, script, '--cgen', '--release', 'M' + pepper_version,
'--wnone', '--dstroot', out_dirname]
Trace('Generating C Headers for version %s\n %s' % (
pepper_version, ' '.join(cmd)))
subprocess.check_call(cmd, cwd=cwd)
def GenerateDoxyfile(template_filename, out_dirname, doc_dirname, doxyfile):
Trace('Writing Doxyfile "%s" (from template %s)' % (
doxyfile, template_filename))
with open(template_filename) as f:
data = f.read()
with open(doxyfile, 'w') as f:
f.write(data % {
'out_dirname': out_dirname,
'doc_dirname': doc_dirname,
'script_dirname': SCRIPT_DIR})
def CheckDoxygenVersion(doxygen):
version = subprocess.check_output([doxygen, '--version']).strip()
url = 'http://ftp.stack.nl/pub/users/dimitri/doxygen-1.7.6.1.linux.bin.tar.gz'
if version != '1.7.6.1':
print 'Doxygen version 1.7.6.1 is required'
print 'The version being used (%s) is version %s' % (doxygen, version)
print 'The simplest way to grab this version is to download it directly:'
print url
print 'Then either add it to your $PATH or set $DOXYGEN to point to binary.'
sys.exit(1)
def RunDoxygen(out_dirname, doxyfile):
Trace('Removing old output directory %s' % out_dirname)
RemoveDir(out_dirname)
Trace('Making new output directory %s' % out_dirname)
os.makedirs(out_dirname)
doxygen = os.environ.get('DOXYGEN', 'doxygen')
CheckDoxygenVersion(doxygen)
cmd = [doxygen, doxyfile]
Trace('Running Doxygen:\n %s' % ' '.join(cmd))
subprocess.check_call(cmd)
def RunDoxyCleanup(out_dirname):
script = os.path.join(SCRIPT_DIR, 'doxy_cleanup.py')
cmd = [sys.executable, script, out_dirname]
if Trace.verbose:
cmd.append('-v')
Trace('Running doxy_cleanup:\n %s' % ' '.join(cmd))
subprocess.check_call(cmd)
def RunRstIndex(kind, channel, pepper_version, out_dirname, out_rst_filename):
assert kind in ('root', 'c', 'cpp')
script = os.path.join(SCRIPT_DIR, 'rst_index.py')
cmd = [sys.executable, script,
'--' + kind,
'--channel', channel,
'--version', pepper_version,
out_dirname,
out_rst_filename]
Trace('Running rst_index:\n %s' % ' '.join(cmd))
subprocess.check_call(cmd)
def GetRstName(kind, channel):
if channel == 'stable':
filename = '%s-api.rst' % kind
else:
filename = '%s-api-%s.rst' % (kind, channel)
return os.path.join(DOC_DIR, filename)
def GenerateDocs(root_dirname, channel, pepper_version, branch):
Trace('Generating docs for %s (branch %s)' % (channel, branch))
pepper_dirname = 'pepper_%s' % channel
out_dirname = os.path.join(root_dirname, pepper_dirname)
try:
svn_dirname = tempfile.mkdtemp(prefix=pepper_dirname)
doxyfile_dirname = tempfile.mkdtemp(prefix='%s_doxyfiles' % pepper_dirname)
CheckoutPepperDocs(branch, svn_dirname)
FixPepperDocLinks(svn_dirname)
GenerateCHeaders(pepper_version, svn_dirname)
doxyfile_c = ''
doxyfile_cpp = ''
# Generate Root index
rst_index_root = os.path.join(DOC_DIR, pepper_dirname, 'index.rst')
RunRstIndex('root', channel, pepper_version, out_dirname, rst_index_root)
# Generate C docs
out_dirname_c = os.path.join(out_dirname, 'c')
doxyfile_c = os.path.join(doxyfile_dirname, 'Doxyfile.c.%s' % channel)
doxyfile_c_template = os.path.join(SCRIPT_DIR, 'Doxyfile.c.template')
rst_index_c = GetRstName('c', channel)
GenerateDoxyfile(doxyfile_c_template, out_dirname_c, svn_dirname,
doxyfile_c)
RunDoxygen(out_dirname_c, doxyfile_c)
RunDoxyCleanup(out_dirname_c)
RunRstIndex('c', channel, pepper_version, out_dirname_c, rst_index_c)
# Generate C++ docs
out_dirname_cpp = os.path.join(out_dirname, 'cpp')
doxyfile_cpp = os.path.join(doxyfile_dirname, 'Doxyfile.cpp.%s' % channel)
doxyfile_cpp_template = os.path.join(SCRIPT_DIR, 'Doxyfile.cpp.template')
rst_index_cpp = GetRstName('cpp', channel)
GenerateDoxyfile(doxyfile_cpp_template, out_dirname_cpp, svn_dirname,
doxyfile_cpp)
RunDoxygen(out_dirname_cpp, doxyfile_cpp)
RunDoxyCleanup(out_dirname_cpp)
RunRstIndex('cpp', channel, pepper_version, out_dirname_cpp, rst_index_cpp)
finally:
# Cleanup
RemoveDir(svn_dirname)
RemoveDir(doxyfile_dirname)
def main(argv):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('-v', '--verbose',
help='Verbose output', action='store_true')
parser.add_argument('out_directory')
options = parser.parse_args(argv)
if options.verbose:
Trace.verbose = True
for channel, info in GetChannelInfo().iteritems():
GenerateDocs(options.out_directory, channel, info.version, info.branch)
return 0
if __name__ == '__main__':
try:
rtn = main(sys.argv[1:])
except KeyboardInterrupt:
sys.stderr.write('%s: interrupted\n' % os.path.basename(__file__))
rtn = 1
sys.exit(rtn)