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