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)