blob: 09b26db2d87b6c7f8a22503a80e05209fe13cb44 [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):
Éric Araujo7373fcc2011-06-06 21:55:43 +0200204 """Add all default files to self.filelist.
205
206 In addition to the setup.cfg file, this will include all files returned
207 by the get_source_files of every registered command. This will find
208 Python modules and packages, data files listed in package_data_,
209 data_files and extra_files, scripts, C sources of extension modules or
210 C libraries (headers are missing).
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200211 """
Éric Araujo7373fcc2011-06-06 21:55:43 +0200212 if os.path.exists('setup.cfg'):
213 self.filelist.append('setup.cfg')
214 else:
215 logger.warning("%s: standard 'setup.cfg' file not found",
216 self.get_command_name())
217
Tarek Ziade1231a4e2011-05-19 13:07:25 +0200218 for cmd_name in get_command_names():
219 try:
220 cmd_obj = self.get_finalized_command(cmd_name)
221 except PackagingOptionError:
222 pass
223 else:
224 self.filelist.extend(cmd_obj.get_source_files())
225
226 def prune_file_list(self):
227 """Prune off branches that might slip into the file list as created
228 by 'read_template()', but really don't belong there:
229 * the build tree (typically "build")
230 * the release tree itself (only an issue if we ran "sdist"
231 previously with --keep-temp, or it aborted)
232 * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories
233 """
234 build = self.get_finalized_command('build')
235 base_dir = self.distribution.get_fullname()
236
237 self.filelist.exclude_pattern(None, prefix=build.build_base)
238 self.filelist.exclude_pattern(None, prefix=base_dir)
239
240 # pruning out vcs directories
241 # both separators are used under win32
242 if sys.platform == 'win32':
243 seps = r'/|\\'
244 else:
245 seps = '/'
246
247 vcs_dirs = ['RCS', 'CVS', r'\.svn', r'\.hg', r'\.git', r'\.bzr',
248 '_darcs']
249 vcs_ptrn = r'(^|%s)(%s)(%s).*' % (seps, '|'.join(vcs_dirs), seps)
250 self.filelist.exclude_pattern(vcs_ptrn, is_regex=True)
251
252 def make_release_tree(self, base_dir, files):
253 """Create the directory tree that will become the source
254 distribution archive. All directories implied by the filenames in
255 'files' are created under 'base_dir', and then we hard link or copy
256 (if hard linking is unavailable) those files into place.
257 Essentially, this duplicates the developer's source tree, but in a
258 directory named after the distribution, containing only the files
259 to be distributed.
260 """
261 # Create all the directories under 'base_dir' necessary to
262 # put 'files' there; the 'mkpath()' is just so we don't die
263 # if the manifest happens to be empty.
264 self.mkpath(base_dir)
265 self.create_tree(base_dir, files, dry_run=self.dry_run)
266
267 # And walk over the list of files, either making a hard link (if
268 # os.link exists) to each one that doesn't already exist in its
269 # corresponding location under 'base_dir', or copying each file
270 # that's out-of-date in 'base_dir'. (Usually, all files will be
271 # out-of-date, because by default we blow away 'base_dir' when
272 # we're done making the distribution archives.)
273
274 if hasattr(os, 'link'): # can make hard links on this system
275 link = 'hard'
276 msg = "making hard links in %s..." % base_dir
277 else: # nope, have to copy
278 link = None
279 msg = "copying files to %s..." % base_dir
280
281 if not files:
282 logger.warning("no files to distribute -- empty manifest?")
283 else:
284 logger.info(msg)
285
286 for file in self.distribution.metadata.requires_files:
287 if file not in files:
288 msg = "'%s' must be included explicitly in 'extra_files'" \
289 % file
290 raise PackagingFileError(msg)
291
292 for file in files:
293 if not os.path.isfile(file):
294 logger.warning("'%s' not a regular file -- skipping", file)
295 else:
296 dest = os.path.join(base_dir, file)
297 self.copy_file(file, dest, link=link)
298
299 self.distribution.metadata.write(os.path.join(base_dir, 'PKG-INFO'))
300
301 def make_distribution(self):
302 """Create the source distribution(s). First, we create the release
303 tree with 'make_release_tree()'; then, we create all required
304 archive files (according to 'self.formats') from the release tree.
305 Finally, we clean up by blowing away the release tree (unless
306 'self.keep_temp' is true). The list of archive files created is
307 stored so it can be retrieved later by 'get_archive_files()'.
308 """
309 # Don't warn about missing metadata here -- should be (and is!)
310 # done elsewhere.
311 base_dir = self.distribution.get_fullname()
312 base_name = os.path.join(self.dist_dir, base_dir)
313
314 self.make_release_tree(base_dir, self.filelist.files)
315 archive_files = [] # remember names of files we create
316 # tar archive must be created last to avoid overwrite and remove
317 if 'tar' in self.formats:
318 self.formats.append(self.formats.pop(self.formats.index('tar')))
319
320 for fmt in self.formats:
321 file = self.make_archive(base_name, fmt, base_dir=base_dir,
322 owner=self.owner, group=self.group)
323 archive_files.append(file)
324 self.distribution.dist_files.append(('sdist', '', file))
325
326 self.archive_files = archive_files
327
328 if not self.keep_temp:
329 if self.dry_run:
330 logger.info('removing %s', base_dir)
331 else:
332 rmtree(base_dir)
333
334 def get_archive_files(self):
335 """Return the list of archive files created when the command
336 was run, or None if the command hasn't run yet.
337 """
338 return self.archive_files
339
340 def create_tree(self, base_dir, files, mode=0o777, verbose=1,
341 dry_run=False):
342 need_dir = set()
343 for file in files:
344 need_dir.add(os.path.join(base_dir, os.path.dirname(file)))
345
346 # Now create them
347 for dir in sorted(need_dir):
348 self.mkpath(dir, mode, verbose=verbose, dry_run=dry_run)