blob: a28019b366e99896d73b2c5205ef81367ddc8c96 [file] [log] [blame]
Tarek Ziade1231a4e2011-05-19 13:07:25 +02001"""Create a source distribution."""
2
3import os
4import sys
5import re
6from io import StringIO
7from glob import glob
8from shutil import get_archive_formats, rmtree
9
10from packaging import logger
11from packaging.util import resolve_name
12from packaging.errors import (PackagingPlatformError, PackagingOptionError,
13 PackagingModuleError, PackagingFileError)
14from packaging.command import get_command_names
15from packaging.command.cmd import Command
16from packaging.manifest import Manifest
17
18
19def show_formats():
20 """Print all possible values for the 'formats' option (used by
21 the "--help-formats" command-line option).
22 """
23 from packaging.fancy_getopt import FancyGetopt
24 formats = sorted(('formats=' + name, None, desc)
25 for name, desc in get_archive_formats())
26 FancyGetopt(formats).print_help(
27 "List of available source distribution formats:")
28
29# a \ followed by some spaces + EOL
30_COLLAPSE_PATTERN = re.compile('\\\w\n', re.M)
31_COMMENTED_LINE = re.compile('^#.*\n$|^\w*\n$', re.M)
32
33
34class sdist(Command):
35
36 description = "create a source distribution (tarball, zip file, etc.)"
37
38 user_options = [
39 ('manifest=', 'm',
40 "name of manifest file [default: MANIFEST]"),
41 ('use-defaults', None,
42 "include the default file set in the manifest "
43 "[default; disable with --no-defaults]"),
44 ('no-defaults', None,
45 "don't include the default file set"),
46 ('prune', None,
47 "specifically exclude files/directories that should not be "
48 "distributed (build tree, RCS/CVS dirs, etc.) "
49 "[default; disable with --no-prune]"),
50 ('no-prune', None,
51 "don't automatically exclude anything"),
52 ('manifest-only', 'o',
53 "just regenerate the manifest and then stop "),
54 ('formats=', None,
55 "formats for source distribution (comma-separated list)"),
56 ('keep-temp', 'k',
57 "keep the distribution tree around after creating " +
58 "archive file(s)"),
59 ('dist-dir=', 'd',
60 "directory to put the source distribution archive(s) in "
61 "[default: dist]"),
62 ('check-metadata', None,
63 "Ensure that all required elements of metadata "
64 "are supplied. Warn if any missing. [default]"),
65 ('owner=', 'u',
66 "Owner name used when creating a tar file [default: current user]"),
67 ('group=', 'g',
68 "Group name used when creating a tar file [default: current group]"),
69 ('manifest-builders=', None,
70 "manifest builders (comma-separated list)"),
71 ]
72
73 boolean_options = ['use-defaults', 'prune',
74 'manifest-only', 'keep-temp', 'check-metadata']
75
76 help_options = [
77 ('help-formats', None,
78 "list available distribution formats", show_formats),
79 ]
80
81 negative_opt = {'no-defaults': 'use-defaults',
82 'no-prune': 'prune'}
83
84 default_format = {'posix': 'gztar',
85 'nt': 'zip'}
86
87 def initialize_options(self):
88 self.manifest = None
89 # 'use_defaults': if true, we will include the default file set
90 # in the manifest
91 self.use_defaults = True
92 self.prune = True
93 self.manifest_only = False
94 self.formats = None
95 self.keep_temp = False
96 self.dist_dir = None
97
98 self.archive_files = None
99 self.metadata_check = True
100 self.owner = None
101 self.group = None
102 self.filelist = None
103 self.manifest_builders = None
104
105 def _check_archive_formats(self, formats):
106 supported_formats = [name for name, desc in get_archive_formats()]
107 for format in formats:
108 if format not in supported_formats:
109 return format
110 return None
111
112 def finalize_options(self):
113 if self.manifest is None:
114 self.manifest = "MANIFEST"
115
116 self.ensure_string_list('formats')
117 if self.formats is None:
118 try:
119 self.formats = [self.default_format[os.name]]
120 except KeyError:
121 raise PackagingPlatformError("don't know how to create source "
122 "distributions on platform %s" % os.name)
123
124 bad_format = self._check_archive_formats(self.formats)
125 if bad_format:
126 raise PackagingOptionError("unknown archive format '%s'" \
127 % bad_format)
128
129 if self.dist_dir is None:
130 self.dist_dir = "dist"
131
132 if self.filelist is None:
133 self.filelist = Manifest()
134
135 if self.manifest_builders is None:
136 self.manifest_builders = []
137 else:
138 if isinstance(self.manifest_builders, str):
139 self.manifest_builders = self.manifest_builders.split(',')
140 builders = []
141 for builder in self.manifest_builders:
142 builder = builder.strip()
143 if builder == '':
144 continue
145 try:
146 builder = resolve_name(builder)
147 except ImportError as e:
148 raise PackagingModuleError(e)
149
150 builders.append(builder)
151
152 self.manifest_builders = builders
153
154 def run(self):
155 # 'filelist' contains the list of files that will make up the
156 # manifest
157 self.filelist.clear()
158
159 # Check the package metadata
160 if self.metadata_check:
161 self.run_command('check')
162
163 # Do whatever it takes to get the list of files to process
164 # (process the manifest template, read an existing manifest,
165 # whatever). File list is accumulated in 'self.filelist'.
166 self.get_file_list()
167
168 # If user just wanted us to regenerate the manifest, stop now.
169 if self.manifest_only:
170 return
171
172 # Otherwise, go ahead and create the source distribution tarball,
173 # or zipfile, or whatever.
174 self.make_distribution()
175
176 def get_file_list(self):
177 """Figure out the list of files to include in the source
178 distribution, and put it in 'self.filelist'. This might involve
179 reading the manifest template (and writing the manifest), or just
180 reading the manifest, or just using the default file set -- it all
181 depends on the user's options.
182 """
183 template_exists = len(self.distribution.extra_files) > 0
184 if not template_exists:
185 logger.warning('%s: using default file list',
186 self.get_command_name())
187 self.filelist.findall()
188
189 if self.use_defaults:
190 self.add_defaults()
191 if template_exists:
192 template = '\n'.join(self.distribution.extra_files)
193 self.filelist.read_template(StringIO(template))
194
195 # call manifest builders, if any.
196 for builder in self.manifest_builders:
197 builder(self.distribution, self.filelist)
198
199 if self.prune:
200 self.prune_file_list()
201
202 self.filelist.write(self.manifest)
203
204 def add_defaults(self):
205 """Add all the default files to self.filelist:
206 - README or README.txt
207 - test/test*.py
208 - all pure Python modules mentioned in setup script
209 - all files pointed by package_data (build_py)
210 - all files defined in data_files.
211 - all files defined as scripts.
212 - all C sources listed as part of extensions or C libraries
213 in the setup script (doesn't catch C headers!)
214 Warns if (README or README.txt) or setup.py are missing; everything
215 else is optional.
216 """
217 standards = [('README', 'README.txt')]
218 for fn in standards:
219 if isinstance(fn, tuple):
220 alts = fn
221 got_it = False
222 for fn in alts:
223 if os.path.exists(fn):
224 got_it = True
225 self.filelist.append(fn)
226 break
227
228 if not got_it:
229 logger.warning(
230 '%s: standard file not found: should have one of %s',
231 self.get_command_name(), ', '.join(alts))
232 else:
233 if os.path.exists(fn):
234 self.filelist.append(fn)
235 else:
236 logger.warning('%s: standard file %r not found',
237 self.get_command_name(), fn)
238
239 optional = ['test/test*.py', 'setup.cfg']
240 for pattern in optional:
241 files = [f for f in glob(pattern) if os.path.isfile(f)]
242 if files:
243 self.filelist.extend(files)
244
245 for cmd_name in get_command_names():
246 try:
247 cmd_obj = self.get_finalized_command(cmd_name)
248 except PackagingOptionError:
249 pass
250 else:
251 self.filelist.extend(cmd_obj.get_source_files())
252
253 def prune_file_list(self):
254 """Prune off branches that might slip into the file list as created
255 by 'read_template()', but really don't belong there:
256 * the build tree (typically "build")
257 * the release tree itself (only an issue if we ran "sdist"
258 previously with --keep-temp, or it aborted)
259 * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories
260 """
261 build = self.get_finalized_command('build')
262 base_dir = self.distribution.get_fullname()
263
264 self.filelist.exclude_pattern(None, prefix=build.build_base)
265 self.filelist.exclude_pattern(None, prefix=base_dir)
266
267 # pruning out vcs directories
268 # both separators are used under win32
269 if sys.platform == 'win32':
270 seps = r'/|\\'
271 else:
272 seps = '/'
273
274 vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr',
275 '_darcs']
276 vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps)
277 self.filelist.exclude_pattern(vcs_ptrn, is_regex=True)
278
279 def make_release_tree(self, base_dir, files):
280 """Create the directory tree that will become the source
281 distribution archive. All directories implied by the filenames in
282 'files' are created under 'base_dir', and then we hard link or copy
283 (if hard linking is unavailable) those files into place.
284 Essentially, this duplicates the developer's source tree, but in a
285 directory named after the distribution, containing only the files
286 to be distributed.
287 """
288 # Create all the directories under 'base_dir' necessary to
289 # put 'files' there; the 'mkpath()' is just so we don't die
290 # if the manifest happens to be empty.
291 self.mkpath(base_dir)
292 self.create_tree(base_dir, files, dry_run=self.dry_run)
293
294 # And walk over the list of files, either making a hard link (if
295 # os.link exists) to each one that doesn't already exist in its
296 # corresponding location under 'base_dir', or copying each file
297 # that's out-of-date in 'base_dir'. (Usually, all files will be
298 # out-of-date, because by default we blow away 'base_dir' when
299 # we're done making the distribution archives.)
300
301 if hasattr(os, 'link'): # can make hard links on this system
302 link = 'hard'
303 msg = "making hard links in %s..." % base_dir
304 else: # nope, have to copy
305 link = None
306 msg = "copying files to %s..." % base_dir
307
308 if not files:
309 logger.warning("no files to distribute -- empty manifest?")
310 else:
311 logger.info(msg)
312
313 for file in self.distribution.metadata.requires_files:
314 if file not in files:
315 msg = "'%s' must be included explicitly in 'extra_files'" \
316 % file
317 raise PackagingFileError(msg)
318
319 for file in files:
320 if not os.path.isfile(file):
321 logger.warning("'%s' not a regular file -- skipping", file)
322 else:
323 dest = os.path.join(base_dir, file)
324 self.copy_file(file, dest, link=link)
325
326 self.distribution.metadata.write(os.path.join(base_dir, 'PKG-INFO'))
327
328 def make_distribution(self):
329 """Create the source distribution(s). First, we create the release
330 tree with 'make_release_tree()'; then, we create all required
331 archive files (according to 'self.formats') from the release tree.
332 Finally, we clean up by blowing away the release tree (unless
333 'self.keep_temp' is true). The list of archive files created is
334 stored so it can be retrieved later by 'get_archive_files()'.
335 """
336 # Don't warn about missing metadata here -- should be (and is!)
337 # done elsewhere.
338 base_dir = self.distribution.get_fullname()
339 base_name = os.path.join(self.dist_dir, base_dir)
340
341 self.make_release_tree(base_dir, self.filelist.files)
342 archive_files = [] # remember names of files we create
343 # tar archive must be created last to avoid overwrite and remove
344 if 'tar' in self.formats:
345 self.formats.append(self.formats.pop(self.formats.index('tar')))
346
347 for fmt in self.formats:
348 file = self.make_archive(base_name, fmt, base_dir=base_dir,
349 owner=self.owner, group=self.group)
350 archive_files.append(file)
351 self.distribution.dist_files.append(('sdist', '', file))
352
353 self.archive_files = archive_files
354
355 if not self.keep_temp:
356 if self.dry_run:
357 logger.info('removing %s', base_dir)
358 else:
359 rmtree(base_dir)
360
361 def get_archive_files(self):
362 """Return the list of archive files created when the command
363 was run, or None if the command hasn't run yet.
364 """
365 return self.archive_files
366
367 def create_tree(self, base_dir, files, mode=0o777, verbose=1,
368 dry_run=False):
369 need_dir = set()
370 for file in files:
371 need_dir.add(os.path.join(base_dir, os.path.dirname(file)))
372
373 # Now create them
374 for dir in sorted(need_dir):
375 self.mkpath(dir, mode, verbose=verbose, dry_run=dry_run)