1# Licensed under a 3-clause BSD style license - see LICENSE.rst
2"""
3This module contains a number of utilities for use during
4setup/build/packaging that are useful to astropy as a whole.
5"""
6
7import collections
8import os
9import re
10import subprocess
11import sys
12import traceback
13import warnings
14from configparser import ConfigParser
15import builtins
16
17from distutils import log
18from distutils.errors import DistutilsOptionError, DistutilsModuleError
19from distutils.core import Extension
20from distutils.core import Command
21from distutils.command.sdist import sdist as DistutilsSdist
22
23from setuptools import setup as setuptools_setup
24from setuptools.config import read_configuration
25from setuptools import find_packages as _find_packages
26
27from .distutils_helpers import (add_command_option, get_compiler_option,
28                                get_dummy_distribution, get_distutils_build_option,
29                                get_distutils_build_or_install_option)
30from .version_helpers import get_pkg_version_module, generate_version_py
31from .utils import (walk_skip_hidden, import_file, extends_doc,
32                    resolve_name, AstropyDeprecationWarning)
33
34from .commands.build_ext import AstropyHelpersBuildExt
35from .commands.test import AstropyTest
36
37# These imports are not used in this module, but are included for backwards
38# compat with older versions of this module
39from .utils import get_numpy_include_path, write_if_different  # noqa
40
41__all__ = ['register_commands', 'get_package_info']
42
43_module_state = {'registered_commands': None,
44                 'have_sphinx': False,
45                 'package_cache': None,
46                 'exclude_packages': set(),
47                 'excludes_too_late': False}
48
49try:
50    import sphinx  # noqa
51    _module_state['have_sphinx'] = True
52except ValueError as e:
53    # This can occur deep in the bowels of Sphinx's imports by way of docutils
54    # and an occurrence of this bug: http://bugs.python.org/issue18378
55    # In this case sphinx is effectively unusable
56    if 'unknown locale' in e.args[0]:
57        log.warn(
58            "Possible misconfiguration of one of the environment variables "
59            "LC_ALL, LC_CTYPES, LANG, or LANGUAGE.  For an example of how to "
60            "configure your system's language environment on OSX see "
61            "http://blog.remibergsma.com/2012/07/10/"
62            "setting-locales-correctly-on-mac-osx-terminal-application/")
63except ImportError:
64    pass
65except SyntaxError:
66    # occurs if markupsafe is recent version, which doesn't support Python 3.2
67    pass
68
69
70def setup(**kwargs):
71    """
72    A wrapper around setuptools' setup() function that automatically sets up
73    custom commands, generates a version file, and customizes the setup process
74    via the ``setup_package.py`` files.
75    """
76
77    # DEPRECATED: store the package name in a built-in variable so it's easy
78    # to get from other parts of the setup infrastructure. We should phase this
79    # out in packages that use it - the cookiecutter template should now be
80    # able to put the right package name where needed.
81    conf = read_configuration('setup.cfg')
82    builtins._ASTROPY_PACKAGE_NAME_ = conf['metadata']['name']
83
84    # Create a dictionary with setup command overrides. Note that this gets
85    # information about the package (name and version) from the setup.cfg file.
86    cmdclass = register_commands()
87
88    # Freeze build information in version.py. Note that this gets information
89    # about the package (name and version) from the setup.cfg file.
90    version = generate_version_py()
91
92    # Get configuration information from all of the various subpackages.
93    # See the docstring for setup_helpers.update_package_files for more
94    # details.
95    package_info = get_package_info()
96    package_info['cmdclass'] = cmdclass
97    package_info['version'] = version
98
99    # Override using any specified keyword arguments
100    package_info.update(kwargs)
101
102    setuptools_setup(**package_info)
103
104
105def adjust_compiler(package):
106    warnings.warn(
107        'The adjust_compiler function in setup.py is '
108        'deprecated and can be removed from your setup.py.',
109        AstropyDeprecationWarning)
110
111
112def get_debug_option(packagename):
113    """ Determines if the build is in debug mode.
114
115    Returns
116    -------
117    debug : bool
118        True if the current build was started with the debug option, False
119        otherwise.
120
121    """
122
123    try:
124        current_debug = get_pkg_version_module(packagename,
125                                               fromlist=['debug'])[0]
126    except (ImportError, AttributeError):
127        current_debug = None
128
129    # Only modify the debug flag if one of the build commands was explicitly
130    # run (i.e. not as a sub-command of something else)
131    dist = get_dummy_distribution()
132    if any(cmd in dist.commands for cmd in ['build', 'build_ext']):
133        debug = bool(get_distutils_build_option('debug'))
134    else:
135        debug = bool(current_debug)
136
137    if current_debug is not None and current_debug != debug:
138        build_ext_cmd = dist.get_command_class('build_ext')
139        build_ext_cmd._force_rebuild = True
140
141    return debug
142
143
144def add_exclude_packages(excludes):
145
146    if _module_state['excludes_too_late']:
147        raise RuntimeError(
148            "add_package_excludes must be called before all other setup helper "
149            "functions in order to properly handle excluded packages")
150
151    _module_state['exclude_packages'].update(set(excludes))
152
153
154def register_commands(package=None, version=None, release=None, srcdir='.'):
155    """
156    This function generates a dictionary containing customized commands that
157    can then be passed to the ``cmdclass`` argument in ``setup()``.
158    """
159
160    if package is not None:
161        warnings.warn('The package argument to generate_version_py has '
162                      'been deprecated and will be removed in future. Specify '
163                      'the package name in setup.cfg instead', AstropyDeprecationWarning)
164
165    if version is not None:
166        warnings.warn('The version argument to generate_version_py has '
167                      'been deprecated and will be removed in future. Specify '
168                      'the version number in setup.cfg instead', AstropyDeprecationWarning)
169
170    if release is not None:
171        warnings.warn('The release argument to generate_version_py has '
172                      'been deprecated and will be removed in future. We now '
173                      'use the presence of the "dev" string in the version to '
174                      'determine whether this is a release', AstropyDeprecationWarning)
175
176    # We use ConfigParser instead of read_configuration here because the latter
177    # only reads in keys recognized by setuptools, but we need to access
178    # package_name below.
179    conf = ConfigParser()
180    conf.read('setup.cfg')
181
182    if conf.has_option('metadata', 'name'):
183        package = conf.get('metadata', 'name')
184    elif conf.has_option('metadata', 'package_name'):
185        # The package-template used package_name instead of name for a while
186        warnings.warn('Specifying the package name using the "package_name" '
187                      'option in setup.cfg is deprecated - use the "name" '
188                      'option instead.', AstropyDeprecationWarning)
189        package = conf.get('metadata', 'package_name')
190    elif package is not None:  # deprecated
191        pass
192    else:
193        sys.stderr.write('ERROR: Could not read package name from setup.cfg\n')
194        sys.exit(1)
195
196    if _module_state['registered_commands'] is not None:
197        return _module_state['registered_commands']
198
199    if _module_state['have_sphinx']:
200        try:
201            from .commands.build_sphinx import (AstropyBuildSphinx,
202                                                AstropyBuildDocs)
203        except ImportError:
204            AstropyBuildSphinx = AstropyBuildDocs = FakeBuildSphinx
205    else:
206        AstropyBuildSphinx = AstropyBuildDocs = FakeBuildSphinx
207
208    _module_state['registered_commands'] = registered_commands = {
209        'test': generate_test_command(package),
210
211        # Use distutils' sdist because it respects package_data.
212        # setuptools/distributes sdist requires duplication of information in
213        # MANIFEST.in
214        'sdist': DistutilsSdist,
215
216        'build_ext': AstropyHelpersBuildExt,
217        'build_sphinx': AstropyBuildSphinx,
218        'build_docs': AstropyBuildDocs
219    }
220
221    # Need to override the __name__ here so that the commandline options are
222    # presented as being related to the "build" command, for example; normally
223    # this wouldn't be necessary since commands also have a command_name
224    # attribute, but there is a bug in distutils' help display code that it
225    # uses __name__ instead of command_name. Yay distutils!
226    for name, cls in registered_commands.items():
227        cls.__name__ = name
228
229    # Add a few custom options; more of these can be added by specific packages
230    # later
231    for option in [
232            ('use-system-libraries',
233             "Use system libraries whenever possible", True)]:
234        add_command_option('build', *option)
235        add_command_option('install', *option)
236
237    add_command_hooks(registered_commands, srcdir=srcdir)
238
239    return registered_commands
240
241
242def add_command_hooks(commands, srcdir='.'):
243    """
244    Look through setup_package.py modules for functions with names like
245    ``pre_<command_name>_hook`` and ``post_<command_name>_hook`` where
246    ``<command_name>`` is the name of a ``setup.py`` command (e.g. build_ext).
247
248    If either hook is present this adds a wrapped version of that command to
249    the passed in ``commands`` `dict`.  ``commands`` may be pre-populated with
250    other custom distutils command classes that should be wrapped if there are
251    hooks for them (e.g. `AstropyBuildPy`).
252    """
253
254    hook_re = re.compile(r'^(pre|post)_(.+)_hook$')
255
256    # Distutils commands have a method of the same name, but it is not a
257    # *classmethod* (which probably didn't exist when distutils was first
258    # written)
259    def get_command_name(cmdcls):
260        if hasattr(cmdcls, 'command_name'):
261            return cmdcls.command_name
262        else:
263            return cmdcls.__name__
264
265    packages = find_packages(srcdir)
266    dist = get_dummy_distribution()
267
268    hooks = collections.defaultdict(dict)
269
270    for setuppkg in iter_setup_packages(srcdir, packages):
271        for name, obj in vars(setuppkg).items():
272            match = hook_re.match(name)
273            if not match:
274                continue
275
276            hook_type = match.group(1)
277            cmd_name = match.group(2)
278
279            if hook_type not in hooks[cmd_name]:
280                hooks[cmd_name][hook_type] = []
281
282            hooks[cmd_name][hook_type].append((setuppkg.__name__, obj))
283
284    for cmd_name, cmd_hooks in hooks.items():
285        commands[cmd_name] = generate_hooked_command(
286            cmd_name, dist.get_command_class(cmd_name), cmd_hooks)
287
288
289def generate_hooked_command(cmd_name, cmd_cls, hooks):
290    """
291    Returns a generated subclass of ``cmd_cls`` that runs the pre- and
292    post-command hooks for that command before and after the ``cmd_cls.run``
293    method.
294    """
295
296    def run(self, orig_run=cmd_cls.run):
297        self.run_command_hooks('pre_hooks')
298        orig_run(self)
299        self.run_command_hooks('post_hooks')
300
301    return type(cmd_name, (cmd_cls, object),
302                {'run': run, 'run_command_hooks': run_command_hooks,
303                 'pre_hooks': hooks.get('pre', []),
304                 'post_hooks': hooks.get('post', [])})
305
306
307def run_command_hooks(cmd_obj, hook_kind):
308    """Run hooks registered for that command and phase.
309
310    *cmd_obj* is a finalized command object; *hook_kind* is either
311    'pre_hook' or 'post_hook'.
312    """
313
314    hooks = getattr(cmd_obj, hook_kind, None)
315
316    if not hooks:
317        return
318
319    for modname, hook in hooks:
320        if isinstance(hook, str):
321            try:
322                hook_obj = resolve_name(hook)
323            except ImportError as exc:
324                raise DistutilsModuleError(
325                    'cannot find hook {0}: {1}'.format(hook, exc))
326        else:
327            hook_obj = hook
328
329        if not callable(hook_obj):
330            raise DistutilsOptionError('hook {0!r} is not callable' % hook)
331
332        log.info('running {0} from {1} for {2} command'.format(
333                 hook_kind.rstrip('s'), modname, cmd_obj.get_command_name()))
334
335        try:
336            hook_obj(cmd_obj)
337        except Exception:
338            log.error('{0} command hook {1} raised an exception: %s\n'.format(
339                hook_obj.__name__, cmd_obj.get_command_name()))
340            log.error(traceback.format_exc())
341            sys.exit(1)
342
343
344def generate_test_command(package_name):
345    """
346    Creates a custom 'test' command for the given package which sets the
347    command's ``package_name`` class attribute to the name of the package being
348    tested.
349    """
350
351    return type(package_name.title() + 'Test', (AstropyTest,),
352                {'package_name': package_name})
353
354
355def update_package_files(srcdir, extensions, package_data, packagenames,
356                         package_dirs):
357    """
358    This function is deprecated and maintained for backward compatibility
359    with affiliated packages.  Affiliated packages should update their
360    setup.py to use `get_package_info` instead.
361    """
362
363    info = get_package_info(srcdir)
364    extensions.extend(info['ext_modules'])
365    package_data.update(info['package_data'])
366    packagenames = list(set(packagenames + info['packages']))
367    package_dirs.update(info['package_dir'])
368
369
370def get_package_info(srcdir='.', exclude=()):
371    """
372    Collates all of the information for building all subpackages
373    and returns a dictionary of keyword arguments that can
374    be passed directly to `distutils.setup`.
375
376    The purpose of this function is to allow subpackages to update the
377    arguments to the package's ``setup()`` function in its setup.py
378    script, rather than having to specify all extensions/package data
379    directly in the ``setup.py``.  See Astropy's own
380    ``setup.py`` for example usage and the Astropy development docs
381    for more details.
382
383    This function obtains that information by iterating through all
384    packages in ``srcdir`` and locating a ``setup_package.py`` module.
385    This module can contain the following functions:
386    ``get_extensions()``, ``get_package_data()``,
387    ``get_build_options()``, and ``get_external_libraries()``.
388
389    Each of those functions take no arguments.
390
391    - ``get_extensions`` returns a list of
392      `distutils.extension.Extension` objects.
393
394    - ``get_package_data()`` returns a dict formatted as required by
395      the ``package_data`` argument to ``setup()``.
396
397    - ``get_build_options()`` returns a list of tuples describing the
398      extra build options to add.
399
400    - ``get_external_libraries()`` returns
401      a list of libraries that can optionally be built using external
402      dependencies.
403    """
404    ext_modules = []
405    packages = []
406    package_dir = {}
407
408    # Read in existing package data, and add to it below
409    setup_cfg = os.path.join(srcdir, 'setup.cfg')
410    if os.path.exists(setup_cfg):
411        conf = read_configuration(setup_cfg)
412        if 'options' in conf and 'package_data' in conf['options']:
413            package_data = conf['options']['package_data']
414        else:
415            package_data = {}
416    else:
417        package_data = {}
418
419    if exclude:
420        warnings.warn(
421            "Use of the exclude parameter is no longer supported since it does "
422            "not work as expected. Use add_exclude_packages instead. Note that "
423            "it must be called prior to any other calls from setup helpers.",
424            AstropyDeprecationWarning)
425
426    # Use the find_packages tool to locate all packages and modules
427    packages = find_packages(srcdir, exclude=exclude)
428
429    # Update package_dir if the package lies in a subdirectory
430    if srcdir != '.':
431        package_dir[''] = srcdir
432
433    # For each of the setup_package.py modules, extract any
434    # information that is needed to install them.  The build options
435    # are extracted first, so that their values will be available in
436    # subsequent calls to `get_extensions`, etc.
437    for setuppkg in iter_setup_packages(srcdir, packages):
438        if hasattr(setuppkg, 'get_build_options'):
439            options = setuppkg.get_build_options()
440            for option in options:
441                add_command_option('build', *option)
442        if hasattr(setuppkg, 'get_external_libraries'):
443            libraries = setuppkg.get_external_libraries()
444            for library in libraries:
445                add_external_library(library)
446
447    for setuppkg in iter_setup_packages(srcdir, packages):
448        # get_extensions must include any Cython extensions by their .pyx
449        # filename.
450        if hasattr(setuppkg, 'get_extensions'):
451            ext_modules.extend(setuppkg.get_extensions())
452        if hasattr(setuppkg, 'get_package_data'):
453            package_data.update(setuppkg.get_package_data())
454
455    # Locate any .pyx files not already specified, and add their extensions in.
456    # The default include dirs include numpy to facilitate numerical work.
457    ext_modules.extend(get_cython_extensions(srcdir, packages, ext_modules,
458                                             ['numpy']))
459
460    # Now remove extensions that have the special name 'skip_cython', as they
461    # exist Only to indicate that the cython extensions shouldn't be built
462    for i, ext in reversed(list(enumerate(ext_modules))):
463        if ext.name == 'skip_cython':
464            del ext_modules[i]
465
466    # On Microsoft compilers, we need to pass the '/MANIFEST'
467    # commandline argument.  This was the default on MSVC 9.0, but is
468    # now required on MSVC 10.0, but it doesn't seem to hurt to add
469    # it unconditionally.
470    if get_compiler_option() == 'msvc':
471        for ext in ext_modules:
472            ext.extra_link_args.append('/MANIFEST')
473
474    return {
475        'ext_modules': ext_modules,
476        'packages': packages,
477        'package_dir': package_dir,
478        'package_data': package_data,
479        }
480
481
482def iter_setup_packages(srcdir, packages):
483    """ A generator that finds and imports all of the ``setup_package.py``
484    modules in the source packages.
485
486    Returns
487    -------
488    modgen : generator
489        A generator that yields (modname, mod), where `mod` is the module and
490        `modname` is the module name for the ``setup_package.py`` modules.
491
492    """
493
494    for packagename in packages:
495        package_parts = packagename.split('.')
496        package_path = os.path.join(srcdir, *package_parts)
497        setup_package = os.path.relpath(
498            os.path.join(package_path, 'setup_package.py'))
499
500        if os.path.isfile(setup_package):
501            module = import_file(setup_package,
502                                 name=packagename + '.setup_package')
503            yield module
504
505
506def iter_pyx_files(package_dir, package_name):
507    """
508    A generator that yields Cython source files (ending in '.pyx') in the
509    source packages.
510
511    Returns
512    -------
513    pyxgen : generator
514        A generator that yields (extmod, fullfn) where `extmod` is the
515        full name of the module that the .pyx file would live in based
516        on the source directory structure, and `fullfn` is the path to
517        the .pyx file.
518    """
519    for dirpath, dirnames, filenames in walk_skip_hidden(package_dir):
520        for fn in filenames:
521            if fn.endswith('.pyx'):
522                fullfn = os.path.relpath(os.path.join(dirpath, fn))
523                # Package must match file name
524                extmod = '.'.join([package_name, fn[:-4]])
525                yield (extmod, fullfn)
526
527        break  # Don't recurse into subdirectories
528
529
530def get_cython_extensions(srcdir, packages, prevextensions=tuple(),
531                          extincludedirs=None):
532    """
533    Looks for Cython files and generates Extensions if needed.
534
535    Parameters
536    ----------
537    srcdir : str
538        Path to the root of the source directory to search.
539    prevextensions : list of `~distutils.core.Extension` objects
540        The extensions that are already defined.  Any .pyx files already here
541        will be ignored.
542    extincludedirs : list of str or None
543        Directories to include as the `include_dirs` argument to the generated
544        `~distutils.core.Extension` objects.
545
546    Returns
547    -------
548    exts : list of `~distutils.core.Extension` objects
549        The new extensions that are needed to compile all .pyx files (does not
550        include any already in `prevextensions`).
551    """
552
553    # Vanilla setuptools and old versions of distribute include Cython files
554    # as .c files in the sources, not .pyx, so we cannot simply look for
555    # existing .pyx sources in the previous sources, but we should also check
556    # for .c files with the same remaining filename. So we look for .pyx and
557    # .c files, and we strip the extension.
558    prevsourcepaths = []
559    ext_modules = []
560
561    for ext in prevextensions:
562        for s in ext.sources:
563            if s.endswith(('.pyx', '.c', '.cpp')):
564                sourcepath = os.path.realpath(os.path.splitext(s)[0])
565                prevsourcepaths.append(sourcepath)
566
567    for package_name in packages:
568        package_parts = package_name.split('.')
569        package_path = os.path.join(srcdir, *package_parts)
570
571        for extmod, pyxfn in iter_pyx_files(package_path, package_name):
572            sourcepath = os.path.realpath(os.path.splitext(pyxfn)[0])
573            if sourcepath not in prevsourcepaths:
574                ext_modules.append(Extension(extmod, [pyxfn],
575                                             include_dirs=extincludedirs))
576
577    return ext_modules
578
579
580class DistutilsExtensionArgs(collections.defaultdict):
581    """
582    A special dictionary whose default values are the empty list.
583
584    This is useful for building up a set of arguments for
585    `distutils.Extension` without worrying whether the entry is
586    already present.
587    """
588    def __init__(self, *args, **kwargs):
589        def default_factory():
590            return []
591
592        super(DistutilsExtensionArgs, self).__init__(
593            default_factory, *args, **kwargs)
594
595    def update(self, other):
596        for key, val in other.items():
597            self[key].extend(val)
598
599
600def pkg_config(packages, default_libraries, executable='pkg-config'):
601    """
602    Uses pkg-config to update a set of distutils Extension arguments
603    to include the flags necessary to link against the given packages.
604
605    If the pkg-config lookup fails, default_libraries is applied to
606    libraries.
607
608    Parameters
609    ----------
610    packages : list of str
611        A list of pkg-config packages to look up.
612
613    default_libraries : list of str
614        A list of library names to use if the pkg-config lookup fails.
615
616    Returns
617    -------
618    config : dict
619        A dictionary containing keyword arguments to
620        `distutils.Extension`.  These entries include:
621
622        - ``include_dirs``: A list of include directories
623        - ``library_dirs``: A list of library directories
624        - ``libraries``: A list of libraries
625        - ``define_macros``: A list of macro defines
626        - ``undef_macros``: A list of macros to undefine
627        - ``extra_compile_args``: A list of extra arguments to pass to
628          the compiler
629    """
630
631    flag_map = {'-I': 'include_dirs', '-L': 'library_dirs', '-l': 'libraries',
632                '-D': 'define_macros', '-U': 'undef_macros'}
633    command = "{0} --libs --cflags {1}".format(executable, ' '.join(packages)),
634
635    result = DistutilsExtensionArgs()
636
637    try:
638        pipe = subprocess.Popen(command, shell=True, stdout=subprocess.PIPE)
639        output = pipe.communicate()[0].strip()
640    except subprocess.CalledProcessError as e:
641        lines = [
642            ("{0} failed. This may cause the build to fail below."
643             .format(executable)),
644            "  command: {0}".format(e.cmd),
645            "  returncode: {0}".format(e.returncode),
646            "  output: {0}".format(e.output)
647            ]
648        log.warn('\n'.join(lines))
649        result['libraries'].extend(default_libraries)
650    else:
651        if pipe.returncode != 0:
652            lines = [
653                "pkg-config could not lookup up package(s) {0}.".format(
654                    ", ".join(packages)),
655                "This may cause the build to fail below."
656                ]
657            log.warn('\n'.join(lines))
658            result['libraries'].extend(default_libraries)
659        else:
660            for token in output.split():
661                # It's not clear what encoding the output of
662                # pkg-config will come to us in.  It will probably be
663                # some combination of pure ASCII (for the compiler
664                # flags) and the filesystem encoding (for any argument
665                # that includes directories or filenames), but this is
666                # just conjecture, as the pkg-config documentation
667                # doesn't seem to address it.
668                arg = token[:2].decode('ascii')
669                value = token[2:].decode(sys.getfilesystemencoding())
670                if arg in flag_map:
671                    if arg == '-D':
672                        value = tuple(value.split('=', 1))
673                    result[flag_map[arg]].append(value)
674                else:
675                    result['extra_compile_args'].append(value)
676
677    return result
678
679
680def add_external_library(library):
681    """
682    Add a build option for selecting the internal or system copy of a library.
683
684    Parameters
685    ----------
686    library : str
687        The name of the library.  If the library is `foo`, the build
688        option will be called `--use-system-foo`.
689    """
690
691    for command in ['build', 'build_ext', 'install']:
692        add_command_option(command, str('use-system-' + library),
693                           'Use the system {0} library'.format(library),
694                           is_bool=True)
695
696
697def use_system_library(library):
698    """
699    Returns `True` if the build configuration indicates that the given
700    library should use the system copy of the library rather than the
701    internal one.
702
703    For the given library `foo`, this will be `True` if
704    `--use-system-foo` or `--use-system-libraries` was provided at the
705    commandline or in `setup.cfg`.
706
707    Parameters
708    ----------
709    library : str
710        The name of the library
711
712    Returns
713    -------
714    use_system : bool
715        `True` if the build should use the system copy of the library.
716    """
717    return (
718        get_distutils_build_or_install_option('use_system_{0}'.format(library)) or
719        get_distutils_build_or_install_option('use_system_libraries'))
720
721
722@extends_doc(_find_packages)
723def find_packages(where='.', exclude=(), invalidate_cache=False):
724    """
725    This version of ``find_packages`` caches previous results to speed up
726    subsequent calls.  Use ``invalide_cache=True`` to ignore cached results
727    from previous ``find_packages`` calls, and repeat the package search.
728    """
729
730    if exclude:
731        warnings.warn(
732            "Use of the exclude parameter is no longer supported since it does "
733            "not work as expected. Use add_exclude_packages instead. Note that "
734            "it must be called prior to any other calls from setup helpers.",
735            AstropyDeprecationWarning)
736
737    # Calling add_exclude_packages after this point will have no effect
738    _module_state['excludes_too_late'] = True
739
740    if not invalidate_cache and _module_state['package_cache'] is not None:
741        return _module_state['package_cache']
742
743    packages = _find_packages(
744        where=where, exclude=list(_module_state['exclude_packages']))
745    _module_state['package_cache'] = packages
746
747    return packages
748
749
750class FakeBuildSphinx(Command):
751    """
752    A dummy build_sphinx command that is called if Sphinx is not
753    installed and displays a relevant error message
754    """
755
756    # user options inherited from sphinx.setup_command.BuildDoc
757    user_options = [
758        ('fresh-env', 'E', ''),
759        ('all-files', 'a', ''),
760        ('source-dir=', 's', ''),
761        ('build-dir=', None, ''),
762        ('config-dir=', 'c', ''),
763        ('builder=', 'b', ''),
764        ('project=', None, ''),
765        ('version=', None, ''),
766        ('release=', None, ''),
767        ('today=', None, ''),
768        ('link-index', 'i', '')]
769
770    # user options appended in astropy.setup_helpers.AstropyBuildSphinx
771    user_options.append(('warnings-returncode', 'w', ''))
772    user_options.append(('clean-docs', 'l', ''))
773    user_options.append(('no-intersphinx', 'n', ''))
774    user_options.append(('open-docs-in-browser', 'o', ''))
775
776    def initialize_options(self):
777        try:
778            raise RuntimeError("Sphinx and its dependencies must be installed "
779                               "for build_docs.")
780        except:
781            log.error('error: Sphinx and its dependencies must be installed '
782                      'for build_docs.')
783            sys.exit(1)
784