1from distutils import log 2import distutils.command.sdist as orig 3import os 4import sys 5import io 6import contextlib 7 8from .py36compat import sdist_add_defaults 9 10from .._importlib import metadata 11 12_default_revctrl = list 13 14 15def walk_revctrl(dirname=''): 16 """Find all files under revision control""" 17 for ep in metadata.entry_points(group='setuptools.file_finders'): 18 for item in ep.load()(dirname): 19 yield item 20 21 22class sdist(sdist_add_defaults, orig.sdist): 23 """Smart sdist that finds anything supported by revision control""" 24 25 user_options = [ 26 ('formats=', None, 27 "formats for source distribution (comma-separated list)"), 28 ('keep-temp', 'k', 29 "keep the distribution tree around after creating " + 30 "archive file(s)"), 31 ('dist-dir=', 'd', 32 "directory to put the source distribution archive(s) in " 33 "[default: dist]"), 34 ('owner=', 'u', 35 "Owner name used when creating a tar file [default: current user]"), 36 ('group=', 'g', 37 "Group name used when creating a tar file [default: current group]"), 38 ] 39 40 negative_opt = {} 41 42 README_EXTENSIONS = ['', '.rst', '.txt', '.md'] 43 READMES = tuple('README{0}'.format(ext) for ext in README_EXTENSIONS) 44 45 def run(self): 46 self.run_command('egg_info') 47 ei_cmd = self.get_finalized_command('egg_info') 48 self.filelist = ei_cmd.filelist 49 self.filelist.append(os.path.join(ei_cmd.egg_info, 'SOURCES.txt')) 50 self.check_readme() 51 52 # Run sub commands 53 for cmd_name in self.get_sub_commands(): 54 self.run_command(cmd_name) 55 56 self.make_distribution() 57 58 dist_files = getattr(self.distribution, 'dist_files', []) 59 for file in self.archive_files: 60 data = ('sdist', '', file) 61 if data not in dist_files: 62 dist_files.append(data) 63 64 def initialize_options(self): 65 orig.sdist.initialize_options(self) 66 67 self._default_to_gztar() 68 69 def _default_to_gztar(self): 70 # only needed on Python prior to 3.6. 71 if sys.version_info >= (3, 6, 0, 'beta', 1): 72 return 73 self.formats = ['gztar'] 74 75 def make_distribution(self): 76 """ 77 Workaround for #516 78 """ 79 with self._remove_os_link(): 80 orig.sdist.make_distribution(self) 81 82 @staticmethod 83 @contextlib.contextmanager 84 def _remove_os_link(): 85 """ 86 In a context, remove and restore os.link if it exists 87 """ 88 89 class NoValue: 90 pass 91 92 orig_val = getattr(os, 'link', NoValue) 93 try: 94 del os.link 95 except Exception: 96 pass 97 try: 98 yield 99 finally: 100 if orig_val is not NoValue: 101 setattr(os, 'link', orig_val) 102 103 def _add_defaults_optional(self): 104 super()._add_defaults_optional() 105 if os.path.isfile('pyproject.toml'): 106 self.filelist.append('pyproject.toml') 107 108 def _add_defaults_python(self): 109 """getting python files""" 110 if self.distribution.has_pure_modules(): 111 build_py = self.get_finalized_command('build_py') 112 self.filelist.extend(build_py.get_source_files()) 113 self._add_data_files(self._safe_data_files(build_py)) 114 115 def _safe_data_files(self, build_py): 116 """ 117 Since the ``sdist`` class is also used to compute the MANIFEST 118 (via :obj:`setuptools.command.egg_info.manifest_maker`), 119 there might be recursion problems when trying to obtain the list of 120 data_files and ``include_package_data=True`` (which in turn depends on 121 the files included in the MANIFEST). 122 123 To avoid that, ``manifest_maker`` should be able to overwrite this 124 method and avoid recursive attempts to build/analyze the MANIFEST. 125 """ 126 return build_py.data_files 127 128 def _add_data_files(self, data_files): 129 """ 130 Add data files as found in build_py.data_files. 131 """ 132 self.filelist.extend( 133 os.path.join(src_dir, name) 134 for _, src_dir, _, filenames in data_files 135 for name in filenames 136 ) 137 138 def _add_defaults_data_files(self): 139 try: 140 super()._add_defaults_data_files() 141 except TypeError: 142 log.warn("data_files contains unexpected objects") 143 144 def check_readme(self): 145 for f in self.READMES: 146 if os.path.exists(f): 147 return 148 else: 149 self.warn( 150 "standard file not found: should have one of " + 151 ', '.join(self.READMES) 152 ) 153 154 def make_release_tree(self, base_dir, files): 155 orig.sdist.make_release_tree(self, base_dir, files) 156 157 # Save any egg_info command line options used to create this sdist 158 dest = os.path.join(base_dir, 'setup.cfg') 159 if hasattr(os, 'link') and os.path.exists(dest): 160 # unlink and re-copy, since it might be hard-linked, and 161 # we don't want to change the source version 162 os.unlink(dest) 163 self.copy_file('setup.cfg', dest) 164 165 self.get_finalized_command('egg_info').save_version_info(dest) 166 167 def _manifest_is_not_generated(self): 168 # check for special comment used in 2.7.1 and higher 169 if not os.path.isfile(self.manifest): 170 return False 171 172 with io.open(self.manifest, 'rb') as fp: 173 first_line = fp.readline() 174 return (first_line != 175 '# file GENERATED by distutils, do NOT edit\n'.encode()) 176 177 def read_manifest(self): 178 """Read the manifest file (named by 'self.manifest') and use it to 179 fill in 'self.filelist', the list of files to include in the source 180 distribution. 181 """ 182 log.info("reading manifest file '%s'", self.manifest) 183 manifest = open(self.manifest, 'rb') 184 for line in manifest: 185 # The manifest must contain UTF-8. See #303. 186 try: 187 line = line.decode('UTF-8') 188 except UnicodeDecodeError: 189 log.warn("%r not UTF-8 decodable -- skipping" % line) 190 continue 191 # ignore comments and blank lines 192 line = line.strip() 193 if line.startswith('#') or not line: 194 continue 195 self.filelist.append(line) 196 manifest.close() 197