1 # Copyright (C) 2011 Igalia S.L.
   2 #
   3 # This library is free software; you can redistribute it and/or
   4 # modify it under the terms of the GNU Lesser General Public
   5 # License as published by the Free Software Foundation; either
   6 # version 2 of the License, or (at your option) any later version.
   7 #
   8 # This library is distributed in the hope that it will be useful,
   9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
  10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  11 # Lesser General Public License for more details.
  12 #
  13 # You should have received a copy of the GNU Lesser General Public
  14 # License along with this library; if not, write to the Free Software
  15 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
  16 
  17 import errno
  18 import logging
  19 import os
  20 import os.path
  21 import subprocess
  22 import sys
  23 
  24 
  25 class GTKDoc(object):
  26 
  27     """Class that controls a gtkdoc run.
  28 
  29     Each instance of this class represents one gtkdoc configuration
  30     and set of documentation. The gtkdoc package is a series of tools
  31     run consecutively which converts inline C/C++ documentation into
  32     docbook files and then into HTML. This class is suitable for
  33     generating documentation or simply verifying correctness.
  34 
  35     Keyword arguments:
  36     output_dir         -- The path where gtkdoc output should be placed. Generation
  37                           may overwrite file in this directory. Required.
  38     module_name        -- The name of the documentation module. For libraries this
  39                           is typically the library name. Required if not library path
  40                           is given.
  41     source_dirs        -- A list of paths to directories of source code to be scanned.
  42                           Required if headers is not specified.
  43     ignored_files      -- A list of filenames to ignore in the source directory. It is
  44                           only necessary to provide the basenames of these files.
  45                           Typically it is important to provide an updated list of
  46                           ignored files to prevent warnings about undocumented symbols.
  47     headers            -- A list of paths to headers to be scanned. Required if source_dirs
  48                           is not specified.
  49     namespace          -- The library namespace.
  50     decorator          -- If a decorator is used to unhide certain symbols in header
  51                           files this parameter is required for successful scanning.
  52                           (default '')
  53     deprecation_guard  -- gtkdoc tries to ensure that symbols marked as deprecated
  54                           are encased in this C preprocessor define. This is required
  55                           to avoid gtkdoc warnings. (default '')
  56     cflags             -- This parameter specifies any preprocessor flags necessary for
  57                           building the scanner binary during gtkdoc-scanobj. Typically
  58                           this includes all absolute include paths necessary to resolve
  59                           all header dependencies. (default '')
  60     ldflags            -- This parameter specifies any linker flags necessary for
  61                           building the scanner binary during gtkdoc-scanobj. Typically
  62                           this includes "-lyourlibraryname". (default '')
  63     library_path       -- This parameter specifies the path to the directory where you
  64                           library resides used for building the scanner binary during
  65                           gtkdoc-scanobj. (default '')
  66 
  67     doc_dir            -- The path to other documentation files necessary to build
  68                           the documentation. This files in this directory as well as
  69                           the files in the 'html' subdirectory will be copied
  70                           recursively into the output directory. (default '')
  71     main_sgml_file     -- The path or name (if a doc_dir is given) of the SGML file
  72                           that is the considered the main page of your documentation.
  73                           (default: <module_name>-docs.sgml)
  74     version            -- The version number of the module. If this is provided,
  75                           a version.xml file containing the version will be created
  76                           in the output directory during documentation generation.
  77 
  78     interactive        -- Whether or not errors or warnings should prompt the user
  79                           to continue or not. When this value is false, generation
  80                           will continue despite warnings. (default False)
  81 
  82     virtual_root       -- A temporary installation directory which is used as the root
  83                           where the actual installation prefix lives; this is mostly
  84                           useful for packagers, and should be set to what is given to
  85                           make install as DESTDIR.
  86     """
  87 
  88     def __init__(self, args):
  89 
  90         # Parameters specific to scanning.
  91         self.module_name = ''
  92         self.source_dirs = []
  93         self.headers = []
  94         self.ignored_files = []
  95         self.namespace = ''
  96         self.decorator = ''
  97         self.deprecation_guard = ''
  98 
  99         # Parameters specific to gtkdoc-scanobj.
 100         self.cflags = ''
 101         self.ldflags = ''
 102         self.library_path = ''
 103 
 104         # Parameters specific to generation.
 105         self.output_dir = ''
 106         self.doc_dir = ''
 107         self.main_sgml_file = ''
 108 
 109         # Parameters specific to gtkdoc-fixxref.
 110         self.cross_reference_deps = []
 111 
 112         self.interactive = False
 113 
 114         self.logger = logging.getLogger('gtkdoc')
 115 
 116         for key, value in iter(args.items()):
 117             setattr(self, key, value)
 118 
 119         if not getattr(self, 'output_dir'):
 120             raise Exception('output_dir not specified.')
 121         if not getattr(self, 'module_name'):
 122             raise Exception('module_name not specified.')
 123         if not getattr(self, 'source_dirs') and not getattr(self, 'headers'):
 124             raise Exception('Neither source_dirs nor headers specified.' % key)
 125 
 126         # Make all paths absolute in case we were passed relative paths, since
 127         # we change the current working directory when executing subcommands.
 128         self.output_dir = os.path.abspath(self.output_dir)
 129         self.source_dirs = [os.path.abspath(x) for x in self.source_dirs]
 130         self.headers = [os.path.abspath(x) for x in self.headers]
 131         if self.library_path:
 132             self.library_path = os.path.abspath(self.library_path)
 133 
 134         if not self.main_sgml_file:
 135             self.main_sgml_file = self.module_name + "-docs.sgml"
 136 
 137     def generate(self, html=True):
 138         self.saw_warnings = False
 139 
 140         self._copy_doc_files_to_output_dir(html)
 141         self._write_version_xml()
 142         self._run_gtkdoc_scan()
 143         self._run_gtkdoc_scangobj()
 144         self._run_gtkdoc_mkdb()
 145 
 146         if not html:
 147             return
 148 
 149         self._run_gtkdoc_mkhtml()
 150         self._run_gtkdoc_fixxref()
 151 
 152     def _delete_file_if_exists(self, path):
 153         if not os.access(path, os.F_OK | os.R_OK):
 154             return
 155         self.logger.debug('deleting %s', path)
 156         os.unlink(path)
 157 
 158     def _create_directory_if_nonexistent(self, path):
 159         try:
 160             os.makedirs(path)
 161         except OSError as error:
 162             if error.errno != errno.EEXIST:
 163                 raise
 164 
 165     def _raise_exception_if_file_inaccessible(self, path):
 166         if not os.path.exists(path) or not os.access(path, os.R_OK):
 167             raise Exception("Could not access file at: %s" % path)
 168 
 169     def _output_has_warnings(self, outputs):
 170         for output in outputs:
 171             if output and output.find('warning'):
 172                 return True
 173         return False
 174 
 175     def _ask_yes_or_no_question(self, question):
 176         if not self.interactive:
 177             return True
 178 
 179         question += ' [y/N] '
 180         answer = None
 181         while answer != 'y' and answer != 'n' and answer != '':
 182             answer = raw_input(question).lower()
 183         return answer == 'y'
 184 
 185     def _run_command(self, args, env=None, cwd=None, print_output=True, ignore_warnings=False):
 186         if print_output:
 187             self.logger.debug("Running %s", args[0])
 188         self.logger.debug("Full command args: %s", str(args))
 189 
 190         process = subprocess.Popen(args, env=env, cwd=cwd,
 191                                    stdout=subprocess.PIPE,
 192                                    stderr=subprocess.PIPE)
 193         stdout, stderr = [b.decode("utf-8") for b in process.communicate()]
 194 
 195         if print_output:
 196             if stdout:
 197                 try:
 198                     sys.stdout.write(stdout.encode("utf-8"))
 199                 except UnicodeDecodeError:
 200                     sys.stdout.write(stdout)
 201             if stderr:
 202                 try:
 203                     sys.stderr.write(stderr.encode("utf-8"))
 204                 except UnicodeDecodeError:
 205                     sys.stderr.write(stderr)
 206 
 207         if process.returncode != 0:
 208             raise Exception('%s produced a non-zero return code %i'
 209                              % (args[0], process.returncode))
 210 
 211         if not ignore_warnings and ('warning' in stderr or 'warning' in stdout):
 212             self.saw_warnings = True
 213             if not self._ask_yes_or_no_question('%s produced warnings, '
 214                                                 'try to continue?' % args[0]):
 215                 raise Exception('%s step failed' % args[0])
 216 
 217         return stdout.strip()
 218 
 219     def _copy_doc_files_to_output_dir(self, html=True):
 220         if not self.doc_dir:
 221             self.logger.info('Not copying any files from doc directory,'
 222                              ' because no doc directory given.')
 223             return
 224 
 225         def copy_file_replacing_existing(src, dest):
 226             if os.path.isdir(src):
 227                 self.logger.debug('skipped directory %s',  src)
 228                 return
 229             if not os.access(src, os.F_OK | os.R_OK):
 230                 self.logger.debug('skipped unreadable %s', src)
 231                 return
 232 
 233             self._delete_file_if_exists(dest)
 234 
 235             self.logger.debug('created %s', dest)
 236             try:
 237                 os.link(src, dest)
 238             except OSError:
 239                 os.symlink(src, dest)
 240 
 241         def copy_all_files_in_directory(src, dest):
 242             for path in os.listdir(src):
 243                 copy_file_replacing_existing(os.path.join(src, path),
 244                                              os.path.join(dest, path))
 245 
 246         self.logger.debug('Copying template files to output directory...')
 247         self._create_directory_if_nonexistent(self.output_dir)
 248         copy_all_files_in_directory(self.doc_dir, self.output_dir)
 249 
 250         if not html:
 251             return
 252 
 253         self.logger.debug('Copying HTML files to output directory...')
 254         html_src_dir = os.path.join(self.doc_dir, 'html')
 255         html_dest_dir = os.path.join(self.output_dir, 'html')
 256         self._create_directory_if_nonexistent(html_dest_dir)
 257 
 258         if os.path.exists(html_src_dir):
 259             copy_all_files_in_directory(html_src_dir, html_dest_dir)
 260 
 261     def _write_version_xml(self):
 262         if not self.version:
 263             self.logger.info('No version specified, so not writing version.xml')
 264             return
 265 
 266         version_xml_path = os.path.join(self.output_dir, 'version.xml')
 267         src_version_xml_path = os.path.join(self.doc_dir, 'version.xml')
 268 
 269         # Don't overwrite version.xml if it was in the doc directory.
 270         if os.path.exists(version_xml_path) and \
 271            os.path.exists(src_version_xml_path):
 272             return
 273 
 274         output_file = open(version_xml_path, 'w')
 275         output_file.write(self.version)
 276         output_file.close()
 277 
 278     def _ignored_files_basenames(self):
 279         return ' '.join([os.path.basename(x) for x in self.ignored_files])
 280 
 281     def _run_gtkdoc_scan(self):
 282         args = ['gtkdoc-scan',
 283                 '--module=%s' % self.module_name,
 284                 '--rebuild-types']
 285 
 286         if not self.headers:
 287             # Each source directory should be have its own "--source-dir=" prefix.
 288             args.extend(['--source-dir=%s' % path for path in self.source_dirs])
 289 
 290         if self.decorator:
 291             args.append('--ignore-decorators=%s' % self.decorator)
 292         if self.deprecation_guard:
 293             args.append('--deprecated-guards=%s' % self.deprecation_guard)
 294         if self.output_dir:
 295             args.append('--output-dir=%s' % self.output_dir)
 296 
 297         # We only need to pass the list of ignored files if the we are not using an explicit list of headers.
 298         if not self.headers:
 299             # gtkdoc-scan wants the basenames of ignored headers, so strip the
 300             # dirname. Different from "--source-dir", the headers should be
 301             # specified as one long string.
 302             ignored_files_basenames = self._ignored_files_basenames()
 303             if ignored_files_basenames:
 304                 args.append('--ignore-headers=%s' % ignored_files_basenames)
 305 
 306         if self.headers:
 307             args.extend(self.headers)
 308 
 309         self._run_command(args)
 310 
 311     def _run_gtkdoc_scangobj(self):
 312         env = os.environ
 313         ldflags = self.ldflags
 314         if self.library_path:
 315             additional_ldflags = ''
 316             for arg in env.get('LDFLAGS', '').split(' '):
 317                 if arg.startswith('-L'):
 318                     additional_ldflags = '%s %s' % (additional_ldflags, arg)
 319             ldflags = ' "-L%s" %s ' % (self.library_path, additional_ldflags) + ldflags
 320             current_ld_library_path = env.get('LD_LIBRARY_PATH')
 321             if current_ld_library_path:
 322                 env['LD_LIBRARY_PATH'] = '%s:%s' % (self.library_path, current_ld_library_path)
 323             else:
 324                 env['LD_LIBRARY_PATH'] = self.library_path
 325 
 326         if ldflags:
 327             env['LDFLAGS'] = '%s %s' % (ldflags, env.get('LDFLAGS', ''))
 328         if self.cflags:
 329             env['CFLAGS'] = '%s %s' % (self.cflags, env.get('CFLAGS', ''))
 330 
 331         if 'CFLAGS' in env:
 332             self.logger.debug('CFLAGS=%s', env['CFLAGS'])
 333         if 'LDFLAGS' in env:
 334             self.logger.debug('LDFLAGS %s', env['LDFLAGS'])
 335         self._run_command(['gtkdoc-scangobj', '--module=%s' % self.module_name],
 336                           env=env, cwd=self.output_dir)
 337 
 338     def _run_gtkdoc_mkdb(self):
 339         sgml_file = os.path.join(self.output_dir, self.main_sgml_file)
 340         self._raise_exception_if_file_inaccessible(sgml_file)
 341 
 342         args = ['gtkdoc-mkdb',
 343                 '--module=%s' % self.module_name,
 344                 '--main-sgml-file=%s' % sgml_file,
 345                 '--source-suffixes=h,c,cpp,cc',
 346                 '--output-format=xml',
 347                 '--sgml-mode']
 348 
 349         if self.namespace:
 350             args.append('--name-space=%s' % self.namespace)
 351 
 352         ignored_files_basenames = self._ignored_files_basenames()
 353         if ignored_files_basenames:
 354             args.append('--ignore-files=%s' % ignored_files_basenames)
 355 
 356         # Each directory should be have its own "--source-dir=" prefix.
 357         args.extend(['--source-dir=%s' % path for path in self.source_dirs])
 358         self._run_command(args, cwd=self.output_dir)
 359 
 360     def _run_gtkdoc_mkhtml(self):
 361         html_dest_dir = os.path.join(self.output_dir, 'html')
 362         if not os.path.isdir(html_dest_dir):
 363             raise Exception("%s is not a directory, could not generate HTML"
 364                             % html_dest_dir)
 365         elif not os.access(html_dest_dir, os.X_OK | os.R_OK | os.W_OK):
 366             raise Exception("Could not access %s to generate HTML"
 367                             % html_dest_dir)
 368 
 369         # gtkdoc-mkhtml expects the SGML path to be absolute.
 370         sgml_file = os.path.join(os.path.abspath(self.output_dir),
 371                                  self.main_sgml_file)
 372         self._raise_exception_if_file_inaccessible(sgml_file)
 373 
 374         self._run_command(['gtkdoc-mkhtml', self.module_name, sgml_file],
 375                           cwd=html_dest_dir)
 376 
 377     def _run_gtkdoc_fixxref(self):
 378         args = ['gtkdoc-fixxref',
 379                 '--module=%s' % self.module_name,
 380                 '--module-dir=html',
 381                 '--html-dir=html']
 382         args.extend(['--extra-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps])
 383         self._run_command(args, cwd=self.output_dir, ignore_warnings=True)
 384 
 385     def rebase_installed_docs(self):
 386         if not os.path.isdir(self.output_dir):
 387             raise Exception("Tried to rebase documentation before generating it.")
 388         html_dir = os.path.join(self.virtual_root + self.prefix, 'share', 'gtk-doc', 'html', self.module_name)
 389         if not os.path.isdir(html_dir):
 390             return
 391         args = ['gtkdoc-rebase',
 392                 '--relative',
 393                 '--html-dir=%s' % html_dir]
 394         args.extend(['--other-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps])
 395         if self.virtual_root:
 396             args.extend(['--dest-dir=%s' % self.virtual_root])
 397         self._run_command(args, cwd=self.output_dir)
 398 
 399     def api_missing_documentation(self):
 400         unused_doc_file = os.path.join(self.output_dir, self.module_name + "-unused.txt")
 401         if not os.path.exists(unused_doc_file) or not os.access(unused_doc_file, os.R_OK):
 402             return []
 403         return open(unused_doc_file).read().splitlines()
 404 
 405 class PkgConfigGTKDoc(GTKDoc):
 406 
 407     """Class reads a library's pkgconfig file to guess gtkdoc parameters.
 408 
 409     Some gtkdoc parameters can be guessed by reading a library's pkgconfig
 410     file, including the cflags, ldflags and version parameters. If you
 411     provide these parameters as well, they will be appended to the ones
 412     guessed via the pkgconfig file.
 413 
 414     Keyword arguments:
 415       pkg_config_path -- Path to the pkgconfig file for the library. Required.
 416     """
 417 
 418     def __init__(self, pkg_config_path, args):
 419         super(PkgConfigGTKDoc, self).__init__(args)
 420 
 421         pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config')
 422 
 423         if not os.path.exists(pkg_config_path):
 424             raise Exception('Could not find pkg-config file at: %s'
 425                             % pkg_config_path)
 426 
 427         self.cflags += " " + self._run_command([pkg_config,
 428                                                 pkg_config_path,
 429                                                 '--cflags'], print_output=False)
 430         self.ldflags += " " + self._run_command([pkg_config,
 431                                                 pkg_config_path,
 432                                                 '--libs'], print_output=False)
 433         self.version = self._run_command([pkg_config,
 434                                           pkg_config_path,
 435                                           '--modversion'], print_output=False)
 436         self.prefix = self._run_command([pkg_config,
 437                                          pkg_config_path,
 438                                          '--variable=prefix'], print_output=False)