1from distutils import log
2import distutils.command.sdist as orig
3import os
4import sys
5import io
6import contextlib
7
8from setuptools.extern import six
9
10from .py36compat import sdist_add_defaults
11
12import pkg_resources
13
14_default_revctrl = list
15
16
17def walk_revctrl(dirname=''):
18    """Find all files under revision control"""
19    for ep in pkg_resources.iter_entry_points('setuptools.file_finders'):
20        for item in ep.load()(dirname):
21            yield item
22
23
24class sdist(sdist_add_defaults, orig.sdist):
25    """Smart sdist that finds anything supported by revision control"""
26
27    user_options = [
28        ('formats=', None,
29         "formats for source distribution (comma-separated list)"),
30        ('keep-temp', 'k',
31         "keep the distribution tree around after creating " +
32         "archive file(s)"),
33        ('dist-dir=', 'd',
34         "directory to put the source distribution archive(s) in "
35         "[default: dist]"),
36    ]
37
38    negative_opt = {}
39
40    README_EXTENSIONS = ['', '.rst', '.txt', '.md']
41    READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS)
42
43    def run(self):
44        self.run_command('egg_info')
45        ei_cmd = self.get_finalized_command('egg_info')
46        self.filelist = ei_cmd.filelist
47        self.filelist.append(os.path.join(ei_cmd.egg_info, 'SOURCES.txt'))
48        self.check_readme()
49
50        # Run sub commands
51        for cmd_name in self.get_sub_commands():
52            self.run_command(cmd_name)
53
54        self.make_distribution()
55
56        dist_files = getattr(self.distribution, 'dist_files', [])
57        for file in self.archive_files:
58            data = ('sdist', '', file)
59            if data not in dist_files:
60                dist_files.append(data)
61
62    def initialize_options(self):
63        orig.sdist.initialize_options(self)
64
65        self._default_to_gztar()
66
67    def _default_to_gztar(self):
68        # only needed on Python prior to 3.6.
69        if sys.version_info >= (3, 6, 0, 'beta', 1):
70            return
71        self.formats = ['gztar']
72
73    def make_distribution(self):
74        """
75        Workaround for #516
76        """
77        with self._remove_os_link():
78            orig.sdist.make_distribution(self)
79
80    @staticmethod
81    @contextlib.contextmanager
82    def _remove_os_link():
83        """
84        In a context, remove and restore os.link if it exists
85        """
86
87        class NoValue:
88            pass
89
90        orig_val = getattr(os, 'link', NoValue)
91        try:
92            del os.link
93        except Exception:
94            pass
95        try:
96            yield
97        finally:
98            if orig_val is not NoValue:
99                setattr(os, 'link', orig_val)
100
101    def __read_template_hack(self):
102        # This grody hack closes the template file (MANIFEST.in) if an
103        #  exception occurs during read_template.
104        # Doing so prevents an error when easy_install attempts to delete the
105        #  file.
106        try:
107            orig.sdist.read_template(self)
108        except Exception:
109            _, _, tb = sys.exc_info()
110            tb.tb_next.tb_frame.f_locals['template'].close()
111            raise
112
113    # Beginning with Python 2.7.2, 3.1.4, and 3.2.1, this leaky file handle
114    #  has been fixed, so only override the method if we're using an earlier
115    #  Python.
116    has_leaky_handle = (
117        sys.version_info < (2, 7, 2)
118        or (3, 0) <= sys.version_info < (3, 1, 4)
119        or (3, 2) <= sys.version_info < (3, 2, 1)
120    )
121    if has_leaky_handle:
122        read_template = __read_template_hack
123
124    def _add_defaults_python(self):
125        """getting python files"""
126        if self.distribution.has_pure_modules():
127            build_py = self.get_finalized_command('build_py')
128            self.filelist.extend(build_py.get_source_files())
129            # This functionality is incompatible with include_package_data, and
130            # will in fact create an infinite recursion if include_package_data
131            # is True.  Use of include_package_data will imply that
132            # distutils-style automatic handling of package_data is disabled
133            if not self.distribution.include_package_data:
134                for _, src_dir, _, filenames in build_py.data_files:
135                    self.filelist.extend([os.path.join(src_dir, filename)
136                                          for filename in filenames])
137
138    def _add_defaults_data_files(self):
139        try:
140            if six.PY2:
141                sdist_add_defaults._add_defaults_data_files(self)
142            else:
143                super()._add_defaults_data_files()
144        except TypeError:
145            log.warn("data_files contains unexpected objects")
146
147    def check_readme(self):
148        for f in self.READMES:
149            if os.path.exists(f):
150                return
151        else:
152            self.warn(
153                "standard file not found: should have one of " +
154                ', '.join(self.READMES)
155            )
156
157    def make_release_tree(self, base_dir, files):
158        orig.sdist.make_release_tree(self, base_dir, files)
159
160        # Save any egg_info command line options used to create this sdist
161        dest = os.path.join(base_dir, 'setup.cfg')
162        if hasattr(os, 'link') and os.path.exists(dest):
163            # unlink and re-copy, since it might be hard-linked, and
164            # we don't want to change the source version
165            os.unlink(dest)
166            self.copy_file('setup.cfg', dest)
167
168        self.get_finalized_command('egg_info').save_version_info(dest)
169
170    def _manifest_is_not_generated(self):
171        # check for special comment used in 2.7.1 and higher
172        if not os.path.isfile(self.manifest):
173            return False
174
175        with io.open(self.manifest, 'rb') as fp:
176            first_line = fp.readline()
177        return (first_line !=
178                '# file GENERATED by distutils, do NOT edit\n'.encode())
179
180    def read_manifest(self):
181        """Read the manifest file (named by 'self.manifest') and use it to
182        fill in 'self.filelist', the list of files to include in the source
183        distribution.
184        """
185        log.info("reading manifest file '%s'", self.manifest)
186        manifest = open(self.manifest, 'rb')
187        for line in manifest:
188            # The manifest must contain UTF-8. See #303.
189            if six.PY3:
190                try:
191                    line = line.decode('UTF-8')
192                except UnicodeDecodeError:
193                    log.warn("%r not UTF-8 decodable -- skipping" % line)
194                    continue
195            # ignore comments and blank lines
196            line = line.strip()
197            if line.startswith('#') or not line:
198                continue
199            self.filelist.append(line)
200        manifest.close()
201
202    def check_license(self):
203        """Checks if license_file' is configured and adds it to
204        'self.filelist' if the value contains a valid path.
205        """
206
207        opts = self.distribution.get_option_dict('metadata')
208
209        # ignore the source of the value
210        _, license_file = opts.get('license_file', (None, None))
211
212        if license_file is None:
213            log.debug("'license_file' option was not specified")
214            return
215
216        if not os.path.exists(license_file):
217            log.warn("warning: Failed to find the configured license file '%s'",
218                    license_file)
219            return
220
221        self.filelist.append(license_file)
222