Spaces:
Running
Running
# Copyright (C) 2011 Igalia S.L. | |
# | |
# This library is free software; you can redistribute it and/or | |
# modify it under the terms of the GNU Lesser General Public | |
# License as published by the Free Software Foundation; either | |
# version 2 of the License, or (at your option) any later version. | |
# | |
# This library is distributed in the hope that it will be useful, | |
# but WITHOUT ANY WARRANTY; without even the implied warranty of | |
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU | |
# Lesser General Public License for more details. | |
# | |
# You should have received a copy of the GNU Lesser General Public | |
# License along with this library; if not, write to the Free Software | |
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA | |
from __future__ import absolute_import, division, print_function | |
import errno | |
import logging | |
import os | |
import os.path | |
import subprocess | |
import sys | |
PY2 = sys.version_info[0] == 2 | |
if PY2: | |
input = raw_input | |
class GTKDoc(object): | |
"""Class that controls a gtkdoc run. | |
Each instance of this class represents one gtkdoc configuration | |
and set of documentation. The gtkdoc package is a series of tools | |
run consecutively which converts inline C/C++ documentation into | |
docbook files and then into HTML. This class is suitable for | |
generating documentation or simply verifying correctness. | |
Keyword arguments: | |
output_dir -- The path where gtkdoc output should be placed. Generation | |
may overwrite file in this directory. Required. | |
module_name -- The name of the documentation module. For libraries this | |
is typically the library name. Required if not library path | |
is given. | |
source_dirs -- A list of paths to directories of source code to be scanned. | |
Required if headers is not specified. | |
ignored_files -- A list of filenames to ignore in the source directory. It is | |
only necessary to provide the basenames of these files. | |
Typically it is important to provide an updated list of | |
ignored files to prevent warnings about undocumented symbols. | |
headers -- A list of paths to headers to be scanned. Required if source_dirs | |
is not specified. | |
namespace -- The library namespace. | |
decorator -- If a decorator is used to unhide certain symbols in header | |
files this parameter is required for successful scanning. | |
(default '') | |
deprecation_guard -- gtkdoc tries to ensure that symbols marked as deprecated | |
are encased in this C preprocessor define. This is required | |
to avoid gtkdoc warnings. (default '') | |
cflags -- This parameter specifies any preprocessor flags necessary for | |
building the scanner binary during gtkdoc-scanobj. Typically | |
this includes all absolute include paths necessary to resolve | |
all header dependencies. (default '') | |
ldflags -- This parameter specifies any linker flags necessary for | |
building the scanner binary during gtkdoc-scanobj. Typically | |
this includes "-lyourlibraryname". (default '') | |
library_path -- This parameter specifies the path to the directory where you | |
library resides used for building the scanner binary during | |
gtkdoc-scanobj. (default '') | |
doc_dir -- The path to other documentation files necessary to build | |
the documentation. This files in this directory as well as | |
the files in the 'html' subdirectory will be copied | |
recursively into the output directory. (default '') | |
main_sgml_file -- The path or name (if a doc_dir is given) of the SGML file | |
that is the considered the main page of your documentation. | |
(default: <module_name>-docs.sgml) | |
version -- The version number of the module. If this is provided, | |
a version.xml file containing the version will be created | |
in the output directory during documentation generation. | |
interactive -- Whether or not errors or warnings should prompt the user | |
to continue or not. When this value is false, generation | |
will continue despite warnings. (default False) | |
virtual_root -- A temporary installation directory which is used as the root | |
where the actual installation prefix lives; this is mostly | |
useful for packagers, and should be set to what is given to | |
make install as DESTDIR. | |
""" | |
def __init__(self, args): | |
# Parameters specific to scanning. | |
self.module_name = '' | |
self.source_dirs = [] | |
self.headers = [] | |
self.ignored_files = [] | |
self.namespace = '' | |
self.decorator = '' | |
self.deprecation_guard = '' | |
# Parameters specific to gtkdoc-scanobj. | |
self.cflags = '' | |
self.ldflags = '' | |
self.library_path = '' | |
# Parameters specific to generation. | |
self.output_dir = '' | |
self.doc_dir = '' | |
self.main_sgml_file = '' | |
# Parameters specific to gtkdoc-fixxref. | |
self.cross_reference_deps = [] | |
self.interactive = False | |
self.logger = logging.getLogger('gtkdoc') | |
for key, value in iter(args.items()): | |
setattr(self, key, value) | |
if not getattr(self, 'output_dir'): | |
raise Exception('output_dir not specified.') | |
if not getattr(self, 'module_name'): | |
raise Exception('module_name not specified.') | |
if not getattr(self, 'source_dirs') and not getattr(self, 'headers'): | |
raise Exception('Neither source_dirs nor headers specified.' % key) | |
# Make all paths absolute in case we were passed relative paths, since | |
# we change the current working directory when executing subcommands. | |
self.output_dir = os.path.abspath(self.output_dir) | |
self.source_dirs = [os.path.abspath(x) for x in self.source_dirs] | |
self.headers = [os.path.abspath(x) for x in self.headers] | |
if self.library_path: | |
self.library_path = os.path.abspath(self.library_path) | |
if not self.main_sgml_file: | |
self.main_sgml_file = self.module_name + "-docs.sgml" | |
def generate(self, html=True): | |
self.saw_warnings = False | |
self._copy_doc_files_to_output_dir(html) | |
self._write_version_xml() | |
self._run_gtkdoc_scan() | |
self._run_gtkdoc_scangobj() | |
self._run_gtkdoc_mkdb() | |
if not html: | |
return | |
self._run_gtkdoc_mkhtml() | |
self._run_gtkdoc_fixxref() | |
def _delete_file_if_exists(self, path): | |
if not os.access(path, os.F_OK | os.R_OK): | |
return | |
self.logger.debug('deleting %s', path) | |
os.unlink(path) | |
def _create_directory_if_nonexistent(self, path): | |
try: | |
os.makedirs(path) | |
except OSError as error: | |
if error.errno != errno.EEXIST: | |
raise | |
def _raise_exception_if_file_inaccessible(self, path): | |
if not os.path.exists(path) or not os.access(path, os.R_OK): | |
raise Exception("Could not access file at: %s" % path) | |
def _output_has_warnings(self, outputs): | |
for output in outputs: | |
if output and output.find('warning'): | |
return True | |
return False | |
def _ask_yes_or_no_question(self, question): | |
if not self.interactive: | |
return True | |
question += ' [y/N] ' | |
answer = None | |
while answer != 'y' and answer != 'n' and answer != '': | |
answer = input(question).lower() | |
return answer == 'y' | |
def _run_command(self, args, env=None, cwd=None, print_output=True, ignore_warnings=False): | |
if print_output: | |
self.logger.debug("Running %s", args[0]) | |
self.logger.debug("Full command args: %s", str(args)) | |
process = subprocess.Popen(args, env=env, cwd=cwd, | |
stdout=subprocess.PIPE, | |
stderr=subprocess.PIPE) | |
stdout, stderr = [b.decode("utf-8") for b in process.communicate()] | |
if print_output: | |
if stdout: | |
if PY2: | |
try: | |
sys.stdout.write(stdout.encode("utf-8")) | |
except UnicodeDecodeError: | |
sys.stdout.write(stdout) | |
else: | |
sys.stdout.write(stdout) | |
if stderr: | |
if PY2: | |
try: | |
sys.stderr.write(stderr.encode("utf-8")) | |
except UnicodeDecodeError: | |
sys.stderr.write(stderr) | |
else: | |
sys.stderr.write(stderr) | |
if process.returncode != 0: | |
raise Exception('%s produced a non-zero return code %i' | |
% (args[0], process.returncode)) | |
if not ignore_warnings and ('warning' in stderr or 'warning' in stdout): | |
self.saw_warnings = True | |
if not self._ask_yes_or_no_question('%s produced warnings, ' | |
'try to continue?' % args[0]): | |
raise Exception('%s step failed' % args[0]) | |
return stdout.strip() | |
def _copy_doc_files_to_output_dir(self, html=True): | |
if not self.doc_dir: | |
self.logger.info('Not copying any files from doc directory,' | |
' because no doc directory given.') | |
return | |
def copy_file_replacing_existing(src, dest): | |
if os.path.isdir(src): | |
self.logger.debug('skipped directory %s', src) | |
return | |
if not os.access(src, os.F_OK | os.R_OK): | |
self.logger.debug('skipped unreadable %s', src) | |
return | |
self._delete_file_if_exists(dest) | |
self.logger.debug('created %s', dest) | |
try: | |
os.link(src, dest) | |
except OSError: | |
os.symlink(src, dest) | |
def copy_all_files_in_directory(src, dest): | |
for path in os.listdir(src): | |
copy_file_replacing_existing(os.path.join(src, path), | |
os.path.join(dest, path)) | |
self.logger.debug('Copying template files to output directory...') | |
self._create_directory_if_nonexistent(self.output_dir) | |
copy_all_files_in_directory(self.doc_dir, self.output_dir) | |
if not html: | |
return | |
self.logger.debug('Copying HTML files to output directory...') | |
html_src_dir = os.path.join(self.doc_dir, 'html') | |
html_dest_dir = os.path.join(self.output_dir, 'html') | |
self._create_directory_if_nonexistent(html_dest_dir) | |
if os.path.exists(html_src_dir): | |
copy_all_files_in_directory(html_src_dir, html_dest_dir) | |
def _write_version_xml(self): | |
if not self.version: | |
self.logger.info('No version specified, so not writing version.xml') | |
return | |
version_xml_path = os.path.join(self.output_dir, 'version.xml') | |
src_version_xml_path = os.path.join(self.doc_dir, 'version.xml') | |
# Don't overwrite version.xml if it was in the doc directory. | |
if os.path.exists(version_xml_path) and \ | |
os.path.exists(src_version_xml_path): | |
return | |
output_file = open(version_xml_path, 'w') | |
output_file.write(self.version) | |
output_file.close() | |
def _ignored_files_basenames(self): | |
return ' '.join([os.path.basename(x) for x in self.ignored_files]) | |
def _run_gtkdoc_scan(self): | |
args = ['gtkdoc-scan', | |
'--module=%s' % self.module_name, | |
'--rebuild-types'] | |
if not self.headers: | |
# Each source directory should be have its own "--source-dir=" prefix. | |
args.extend(['--source-dir=%s' % path for path in self.source_dirs]) | |
if self.decorator: | |
args.append('--ignore-decorators=%s' % self.decorator) | |
if self.deprecation_guard: | |
args.append('--deprecated-guards=%s' % self.deprecation_guard) | |
if self.output_dir: | |
args.append('--output-dir=%s' % self.output_dir) | |
# We only need to pass the list of ignored files if the we are not using an explicit list of headers. | |
if not self.headers: | |
# gtkdoc-scan wants the basenames of ignored headers, so strip the | |
# dirname. Different from "--source-dir", the headers should be | |
# specified as one long string. | |
ignored_files_basenames = self._ignored_files_basenames() | |
if ignored_files_basenames: | |
args.append('--ignore-headers=%s' % ignored_files_basenames) | |
if self.headers: | |
args.extend(self.headers) | |
self._run_command(args) | |
def _run_gtkdoc_scangobj(self): | |
env = os.environ | |
ldflags = self.ldflags | |
if self.library_path: | |
additional_ldflags = '' | |
for arg in env.get('LDFLAGS', '').split(' '): | |
if arg.startswith('-L'): | |
additional_ldflags = '%s %s' % (additional_ldflags, arg) | |
ldflags = ' "-L%s" %s ' % (self.library_path, additional_ldflags) + ldflags | |
current_ld_library_path = env.get('LD_LIBRARY_PATH') | |
if current_ld_library_path: | |
env['LD_LIBRARY_PATH'] = '%s:%s' % (self.library_path, current_ld_library_path) | |
else: | |
env['LD_LIBRARY_PATH'] = self.library_path | |
if ldflags: | |
env['LDFLAGS'] = '%s %s' % (ldflags, env.get('LDFLAGS', '')) | |
if self.cflags: | |
env['CFLAGS'] = '%s %s' % (self.cflags, env.get('CFLAGS', '')) | |
if 'CFLAGS' in env: | |
self.logger.debug('CFLAGS=%s', env['CFLAGS']) | |
if 'LDFLAGS' in env: | |
self.logger.debug('LDFLAGS %s', env['LDFLAGS']) | |
if 'RUN' in env: | |
self.logger.debug('RUN=%s', env['RUN']) | |
self._run_command(['gtkdoc-scangobj', '--module=%s' % self.module_name], | |
env=env, cwd=self.output_dir) | |
def _run_gtkdoc_mkdb(self): | |
sgml_file = os.path.join(self.output_dir, self.main_sgml_file) | |
self._raise_exception_if_file_inaccessible(sgml_file) | |
args = ['gtkdoc-mkdb', | |
'--module=%s' % self.module_name, | |
'--main-sgml-file=%s' % sgml_file, | |
'--source-suffixes=h,c,cpp,cc', | |
'--output-format=xml', | |
'--sgml-mode'] | |
if self.namespace: | |
args.append('--name-space=%s' % self.namespace) | |
ignored_files_basenames = self._ignored_files_basenames() | |
if ignored_files_basenames: | |
args.append('--ignore-files=%s' % ignored_files_basenames) | |
# Each directory should be have its own "--source-dir=" prefix. | |
args.extend(['--source-dir=%s' % path for path in self.source_dirs]) | |
self._run_command(args, cwd=self.output_dir) | |
def _run_gtkdoc_mkhtml(self): | |
html_dest_dir = os.path.join(self.output_dir, 'html') | |
if not os.path.isdir(html_dest_dir): | |
raise Exception("%s is not a directory, could not generate HTML" | |
% html_dest_dir) | |
elif not os.access(html_dest_dir, os.X_OK | os.R_OK | os.W_OK): | |
raise Exception("Could not access %s to generate HTML" | |
% html_dest_dir) | |
# gtkdoc-mkhtml expects the SGML path to be absolute. | |
sgml_file = os.path.join(os.path.abspath(self.output_dir), | |
self.main_sgml_file) | |
self._raise_exception_if_file_inaccessible(sgml_file) | |
self._run_command(['gtkdoc-mkhtml', self.module_name, sgml_file], | |
cwd=html_dest_dir) | |
def _run_gtkdoc_fixxref(self): | |
args = ['gtkdoc-fixxref', | |
'--module=%s' % self.module_name, | |
'--module-dir=html', | |
'--html-dir=html'] | |
args.extend(['--extra-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps]) | |
self._run_command(args, cwd=self.output_dir, ignore_warnings=True) | |
def rebase_installed_docs(self): | |
if not os.path.isdir(self.output_dir): | |
raise Exception("Tried to rebase documentation before generating it.") | |
html_dir = os.path.join(self.virtual_root + self.prefix, 'share', 'gtk-doc', 'html', self.module_name) | |
if not os.path.isdir(html_dir): | |
return | |
args = ['gtkdoc-rebase', | |
'--relative', | |
'--html-dir=%s' % html_dir] | |
args.extend(['--other-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps]) | |
if self.virtual_root: | |
args.extend(['--dest-dir=%s' % self.virtual_root]) | |
self._run_command(args, cwd=self.output_dir) | |
def api_missing_documentation(self): | |
unused_doc_file = os.path.join(self.output_dir, self.module_name + "-unused.txt") | |
if not os.path.exists(unused_doc_file) or not os.access(unused_doc_file, os.R_OK): | |
return [] | |
return open(unused_doc_file).read().splitlines() | |
class PkgConfigGTKDoc(GTKDoc): | |
"""Class reads a library's pkgconfig file to guess gtkdoc parameters. | |
Some gtkdoc parameters can be guessed by reading a library's pkgconfig | |
file, including the cflags, ldflags and version parameters. If you | |
provide these parameters as well, they will be appended to the ones | |
guessed via the pkgconfig file. | |
Keyword arguments: | |
pkg_config_path -- Path to the pkgconfig file for the library. Required. | |
""" | |
def __init__(self, pkg_config_path, args): | |
super(PkgConfigGTKDoc, self).__init__(args) | |
pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config') | |
if not os.path.exists(pkg_config_path): | |
raise Exception('Could not find pkg-config file at: %s' | |
% pkg_config_path) | |
self.cflags += " " + self._run_command([pkg_config, | |
pkg_config_path, | |
'--cflags'], print_output=False) | |
self.ldflags += " " + self._run_command([pkg_config, | |
pkg_config_path, | |
'--libs'], print_output=False) | |
self.version = self._run_command([pkg_config, | |
pkg_config_path, | |
'--modversion'], print_output=False) | |
self.prefix = self._run_command([pkg_config, | |
pkg_config_path, | |
'--variable=prefix'], print_output=False) | |