4c8237b0ebdeaa035e786592df3bf0d0822cf9fa
[WebKit-https.git] / Tools / gtk / 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 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.info("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.info('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.info('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['RUN'] = 'LD_LIBRARY_PATH="%s:%s" ' % (self.library_path, current_ld_library_path)
323             else:
324                 env['RUN'] = 'LD_LIBRARY_PATH="%s" ' % 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         if 'RUN' in env:
336             self.logger.debug('RUN=%s', env['RUN'])
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         html_dest_dir = os.path.join(self.output_dir, 'html')
364         if not os.path.isdir(html_dest_dir):
365             raise Exception("%s is not a directory, could not generate HTML"
366                             % html_dest_dir)
367         elif not os.access(html_dest_dir, os.X_OK | os.R_OK | os.W_OK):
368             raise Exception("Could not access %s to generate HTML"
369                             % html_dest_dir)
370
371         # gtkdoc-mkhtml expects the SGML path to be absolute.
372         sgml_file = os.path.join(os.path.abspath(self.output_dir),
373                                  self.main_sgml_file)
374         self._raise_exception_if_file_inaccessible(sgml_file)
375
376         self._run_command(['gtkdoc-mkhtml', self.module_name, sgml_file],
377                           cwd=html_dest_dir)
378
379     def _run_gtkdoc_fixxref(self):
380         args = ['gtkdoc-fixxref',
381                 '--module-dir=html',
382                 '--html-dir=html']
383         args.extend(['--extra-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps])
384         self._run_command(args, cwd=self.output_dir, ignore_warnings=True)
385
386     def rebase_installed_docs(self):
387         if not os.path.isdir(self.output_dir):
388             raise Exception("Tried to rebase documentation before generating it.")
389         html_dir = os.path.join(self.virtual_root + self.prefix, 'share', 'gtk-doc', 'html', self.module_name)
390         if not os.path.isdir(html_dir):
391             return
392         args = ['gtkdoc-rebase',
393                 '--relative',
394                 '--html-dir=%s' % html_dir]
395         args.extend(['--other-dir=%s' % extra_dir for extra_dir in self.cross_reference_deps])
396         if self.virtual_root:
397             args.extend(['--dest-dir=%s' % self.virtual_root])
398         self._run_command(args, cwd=self.output_dir)
399
400     def api_missing_documentation(self):
401         unused_doc_file = os.path.join(self.output_dir, self.module_name + "-unused.txt")
402         if not os.path.exists(unused_doc_file) or not os.access(unused_doc_file, os.R_OK):
403             return []
404         return open(unused_doc_file).read().splitlines()
405
406 class PkgConfigGTKDoc(GTKDoc):
407
408     """Class reads a library's pkgconfig file to guess gtkdoc parameters.
409
410     Some gtkdoc parameters can be guessed by reading a library's pkgconfig
411     file, including the cflags, ldflags and version parameters. If you
412     provide these parameters as well, they will be appended to the ones
413     guessed via the pkgconfig file.
414
415     Keyword arguments:
416       pkg_config_path -- Path to the pkgconfig file for the library. Required.
417     """
418
419     def __init__(self, pkg_config_path, args):
420         super(PkgConfigGTKDoc, self).__init__(args)
421
422         pkg_config = os.environ.get('PKG_CONFIG', 'pkg-config')
423
424         if not os.path.exists(pkg_config_path):
425             raise Exception('Could not find pkg-config file at: %s'
426                             % pkg_config_path)
427
428         self.cflags += " " + self._run_command([pkg_config,
429                                                 pkg_config_path,
430                                                 '--cflags'], print_output=False)
431         self.ldflags += " " + self._run_command([pkg_config,
432                                                 pkg_config_path,
433                                                 '--libs'], print_output=False)
434         self.version = self._run_command([pkg_config,
435                                           pkg_config_path,
436                                           '--modversion'], print_output=False)
437         self.prefix = self._run_command([pkg_config,
438                                          pkg_config_path,
439                                          '--variable=prefix'], print_output=False)