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