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