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