blob: 042b1fc54a7add30b54bc7a133936deac791b907 [file] [log] [blame]
Greg Warda82122b2000-02-17 23:56:15 +00001"""distutils.command.sdist
2
3Implements the Distutils 'sdist' command (create a source distribution)."""
4
5# created 1999/09/22, Greg Ward
6
7__rcsid__ = "$Id$"
8
9import sys, os, string, re
10import fnmatch
11from types import *
12from glob import glob
13from shutil import rmtree
14from distutils.core import Command
15from distutils.util import newer
16from distutils.text_file import TextFile
17from distutils.errors import DistutilsExecError
18
19
Greg Ward1993f9a2000-02-18 00:13:53 +000020class sdist (Command):
Greg Warda82122b2000-02-17 23:56:15 +000021
22 description = "create a source distribution (tarball, zip file, etc.)"
23
Greg Wardbbeceea2000-02-18 00:25:39 +000024 user_options = [
25 ('template=', 't',
26 "name of manifest template file [default: MANIFEST.in]"),
27 ('manifest=', 'm',
28 "name of manifest file [default: MANIFEST]"),
29 ('use-defaults', None,
30 "include the default file set in the manifest "
31 "[default; disable with --no-defaults]"),
32 ('manifest-only', None,
33 "just regenerate the manifest and then stop"),
34 ('force-manifest', None,
35 "forcibly regenerate the manifest and carry on as usual"),
36
37 ('formats=', None,
38 "formats for source distribution (tar, ztar, gztar, or zip)"),
39 ('list-only', 'l',
40 "just list files that would be distributed"),
41 ('keep-tree', 'k',
42 "keep the distribution tree around after creating " +
43 "archive file(s)"),
44 ]
Greg Warda82122b2000-02-17 23:56:15 +000045 negative_opts = {'use-defaults': 'no-defaults'}
46
47 default_format = { 'posix': 'gztar',
48 'nt': 'zip' }
49
50 exclude_re = re.compile (r'\s*!\s*(\S+)') # for manifest lines
51
52
Greg Warde01149c2000-02-18 00:35:22 +000053 def initialize_options (self):
Greg Warda82122b2000-02-17 23:56:15 +000054 # 'template' and 'manifest' are, respectively, the names of
55 # the manifest template and manifest file.
56 self.template = None
57 self.manifest = None
58
59 # 'use_defaults': if true, we will include the default file set
60 # in the manifest
61 self.use_defaults = 1
62
63 self.manifest_only = 0
64 self.force_manifest = 0
65
66 self.formats = None
67 self.list_only = 0
68 self.keep_tree = 0
69
70
Greg Warde01149c2000-02-18 00:35:22 +000071 def finalize_options (self):
Greg Warda82122b2000-02-17 23:56:15 +000072 if self.manifest is None:
73 self.manifest = "MANIFEST"
74 if self.template is None:
75 self.template = "MANIFEST.in"
76
77 if self.formats is None:
78 try:
79 self.formats = [self.default_format[os.name]]
80 except KeyError:
81 raise DistutilsPlatformError, \
82 "don't know how to build source distributions on " + \
83 "%s platform" % os.name
84 elif type (self.formats) is StringType:
85 self.formats = string.split (self.formats, ',')
86
87
88 def run (self):
89
90 # 'files' is the list of files that will make up the manifest
91 self.files = []
92
93 # Ensure that all required meta-data is given; warn if not (but
94 # don't die, it's not *that* serious!)
95 self.check_metadata ()
96
97 # Do whatever it takes to get the list of files to process
98 # (process the manifest template, read an existing manifest,
99 # whatever). File list is put into 'self.files'.
100 self.get_file_list ()
101
102 # If user just wanted us to regenerate the manifest, stop now.
103 if self.manifest_only:
104 return
105
106 # Otherwise, go ahead and create the source distribution tarball,
107 # or zipfile, or whatever.
108 self.make_distribution ()
109
110
111 def check_metadata (self):
112
113 dist = self.distribution
114
115 missing = []
116 for attr in ('name', 'version', 'url'):
117 if not (hasattr (dist, attr) and getattr (dist, attr)):
118 missing.append (attr)
119
120 if missing:
121 self.warn ("missing required meta-data: " +
122 string.join (missing, ", "))
123
124 if dist.author:
125 if not dist.author_email:
126 self.warn ("missing meta-data: if 'author' supplied, " +
127 "'author_email' must be supplied too")
128 elif dist.maintainer:
129 if not dist.maintainer_email:
130 self.warn ("missing meta-data: if 'maintainer' supplied, " +
131 "'maintainer_email' must be supplied too")
132 else:
133 self.warn ("missing meta-data: either (author and author_email) " +
134 "or (maintainer and maintainer_email) " +
135 "must be supplied")
136
137 # check_metadata ()
138
139
140 def get_file_list (self):
141 """Figure out the list of files to include in the source
142 distribution, and put it in 'self.files'. This might
143 involve reading the manifest template (and writing the
144 manifest), or just reading the manifest, or just using
145 the default file set -- it all depends on the user's
146 options and the state of the filesystem."""
147
148
149 template_exists = os.path.isfile (self.template)
150 if template_exists:
151 template_newer = newer (self.template, self.manifest)
152
153 # Regenerate the manifest if necessary (or if explicitly told to)
154 if ((template_exists and template_newer) or
155 self.force_manifest or
156 self.manifest_only):
157
158 if not template_exists:
159 self.warn (("manifest template '%s' does not exist " +
160 "(using default file list)") %
161 self.template)
162
163 # Add default file set to 'files'
164 if self.use_defaults:
165 self.find_defaults ()
166
167 # Read manifest template if it exists
168 if template_exists:
169 self.read_template ()
170
171 # File list now complete -- sort it so that higher-level files
172 # come first
173 sortable_files = map (os.path.split, self.files)
174 sortable_files.sort ()
175 self.files = []
176 for sort_tuple in sortable_files:
177 self.files.append (apply (os.path.join, sort_tuple))
178
179 # Remove duplicates from the file list
180 for i in range (len(self.files)-1, 0, -1):
181 if self.files[i] == self.files[i-1]:
182 del self.files[i]
183
184 # And write complete file list (including default file set) to
185 # the manifest.
186 self.write_manifest ()
187
188 # Don't regenerate the manifest, just read it in.
189 else:
190 self.read_manifest ()
191
192 # get_file_list ()
193
194
195 def find_defaults (self):
196
197 standards = [('README', 'README.txt'), 'setup.py']
198 for fn in standards:
199 if type (fn) is TupleType:
200 alts = fn
201 for fn in alts:
202 if os.path.exists (fn):
203 got_it = 1
204 self.files.append (fn)
205 break
206
207 if not got_it:
208 self.warn ("standard file not found: should have one of " +
209 string.join (alts, ', '))
210 else:
211 if os.path.exists (fn):
212 self.files.append (fn)
213 else:
214 self.warn ("standard file '%s' not found" % fn)
215
216 optional = ['test/test*.py']
217 for pattern in optional:
218 files = filter (os.path.isfile, glob (pattern))
219 if files:
220 self.files.extend (files)
221
222 if self.distribution.packages or self.distribution.py_modules:
223 build_py = self.find_peer ('build_py')
224 build_py.ensure_ready ()
225 self.files.extend (build_py.get_source_files ())
226
227 if self.distribution.ext_modules:
228 build_ext = self.find_peer ('build_ext')
229 build_ext.ensure_ready ()
230 self.files.extend (build_ext.get_source_files ())
231
232
233
234 def search_dir (self, dir, pattern=None):
235 """Recursively find files under 'dir' matching 'pattern' (a string
236 containing a Unix-style glob pattern). If 'pattern' is None,
237 find all files under 'dir'. Return the list of found
238 filenames."""
239
240 allfiles = findall (dir)
241 if pattern is None:
242 return allfiles
243
244 pattern_re = translate_pattern (pattern)
245 files = []
246 for file in allfiles:
247 if pattern_re.match (os.path.basename (file)):
248 files.append (file)
249
250 return files
251
252 # search_dir ()
253
254
255 def exclude_pattern (self, pattern):
256 """Remove filenames from 'self.files' that match 'pattern'."""
257 print "exclude_pattern: pattern=%s" % pattern
258 pattern_re = translate_pattern (pattern)
259 for i in range (len (self.files)-1, -1, -1):
260 if pattern_re.match (self.files[i]):
261 print "removing %s" % self.files[i]
262 del self.files[i]
263
264
265 def recursive_exclude_pattern (self, dir, pattern=None):
266 """Remove filenames from 'self.files' that are under 'dir'
267 and whose basenames match 'pattern'."""
268
269 print "recursive_exclude_pattern: dir=%s, pattern=%s" % (dir, pattern)
270 if pattern is None:
271 pattern_re = None
272 else:
273 pattern_re = translate_pattern (pattern)
274
275 for i in range (len (self.files)-1, -1, -1):
276 (cur_dir, cur_base) = os.path.split (self.files[i])
277 if (cur_dir == dir and
278 (pattern_re is None or pattern_re.match (cur_base))):
279 print "removing %s" % self.files[i]
280 del self.files[i]
281
282
283 def read_template (self):
284 """Read and parse the manifest template file named by
285 'self.template' (usually "MANIFEST.in"). Process all file
286 specifications (include and exclude) in the manifest template
287 and add the resulting filenames to 'self.files'."""
288
289 assert self.files is not None and type (self.files) is ListType
290
291 template = TextFile (self.template,
292 strip_comments=1,
293 skip_blanks=1,
294 join_lines=1,
295 lstrip_ws=1,
296 rstrip_ws=1,
297 collapse_ws=1)
298
299 all_files = findall ()
300
301 while 1:
302
303 line = template.readline()
304 if line is None: # end of file
305 break
306
307 words = string.split (line)
308 action = words[0]
309
310 # First, check that the right number of words are present
311 # for the given action (which is the first word)
312 if action in ('include','exclude',
313 'global-include','global-exclude'):
314 if len (words) != 2:
315 template.warn \
316 ("invalid manifest template line: " +
317 "'%s' expects a single <pattern>" %
318 action)
319 continue
320
321 pattern = words[1]
322
323 elif action in ('recursive-include','recursive-exclude'):
324 if len (words) != 3:
325 template.warn \
326 ("invalid manifest template line: " +
327 "'%s' expects <dir> <pattern>" %
328 action)
329 continue
330
331 (dir, pattern) = words[1:3]
332
333 elif action in ('graft','prune'):
334 if len (words) != 2:
335 template.warn \
336 ("invalid manifest template line: " +
337 "'%s' expects a single <dir_pattern>" %
338 action)
339 continue
340
341 dir_pattern = words[1]
342
343 else:
344 template.warn ("invalid manifest template line: " +
345 "unknown action '%s'" % action)
346 continue
347
348 # OK, now we know that the action is valid and we have the
349 # right number of words on the line for that action -- so we
350 # can proceed with minimal error-checking. Also, we have
351 # defined either 'patter', 'dir' and 'pattern', or
352 # 'dir_pattern' -- so we don't have to spend any time digging
353 # stuff up out of 'words'.
354
355 if action == 'include':
356 print "include", pattern
357 files = select_pattern (all_files, pattern, anchor=1)
358 if not files:
359 template.warn ("no files found matching '%s'" % pattern)
360 else:
361 self.files.extend (files)
362
363 elif action == 'exclude':
364 print "exclude", pattern
365 num = exclude_pattern (self.files, pattern, anchor=1)
366 if num == 0:
367 template.warn \
368 ("no previously-included files found matching '%s'" %
369 pattern)
370
371 elif action == 'global-include':
372 print "global-include", pattern
373 files = select_pattern (all_files, pattern, anchor=0)
374 if not files:
375 template.warn (("no files found matching '%s' " +
376 "anywhere in distribution") %
377 pattern)
378 else:
379 self.files.extend (files)
380
381 elif action == 'global-exclude':
382 print "global-exclude", pattern
383 num = exclude_pattern (self.files, pattern, anchor=0)
384 if num == 0:
385 template.warn \
386 (("no previously-included files matching '%s' " +
387 "found anywhere in distribution") %
388 pattern)
389
390 elif action == 'recursive-include':
391 print "recursive-include", dir, pattern
392 files = select_pattern (all_files, pattern, prefix=dir)
393 if not files:
394 template.warn (("no files found matching '%s' " +
395 "under directory '%s'") %
396 (pattern, dir))
397 else:
398 self.files.extend (files)
399
400 elif action == 'recursive-exclude':
401 print "recursive-exclude", dir, pattern
402 num = exclude_pattern (self.files, pattern, prefix=dir)
403 if num == 0:
404 template.warn \
405 (("no previously-included files matching '%s' " +
406 "found under directory '%s'") %
407 (pattern, dir))
408
409 elif action == 'graft':
410 print "graft", dir_pattern
411 files = select_pattern (all_files, None, prefix=dir_pattern)
412 if not files:
413 template.warn ("no directories found matching '%s'" %
414 dir_pattern)
415 else:
416 self.files.extend (files)
417
418 elif action == 'prune':
419 print "prune", dir_pattern
420 num = exclude_pattern (self.files, None, prefix=dir_pattern)
421 if num == 0:
422 template.warn \
423 (("no previously-included directories found " +
424 "matching '%s'") %
425 dir_pattern)
426 else:
427 raise RuntimeError, \
428 "this cannot happen: invalid action '%s'" % action
429
430 # while loop over lines of template file
431
432 # read_template ()
433
434
435 def write_manifest (self):
436 """Write the file list in 'self.files' (presumably as filled in
437 by 'find_defaults()' and 'read_template()') to the manifest file
438 named by 'self.manifest'."""
439
440 manifest = open (self.manifest, "w")
441 for fn in self.files:
442 manifest.write (fn + '\n')
443 manifest.close ()
444
445 # write_manifest ()
446
447
448 def read_manifest (self):
449 """Read the manifest file (named by 'self.manifest') and use
450 it to fill in 'self.files', the list of files to include
451 in the source distribution."""
452
453 manifest = open (self.manifest)
454 while 1:
455 line = manifest.readline ()
456 if line == '': # end of file
457 break
458 if line[-1] == '\n':
459 line = line[0:-1]
460 self.files.append (line)
461
462 # read_manifest ()
463
464
465
466 def make_release_tree (self, base_dir, files):
467
468 # First get the list of directories to create
469 need_dir = {}
470 for file in files:
471 need_dir[os.path.join (base_dir, os.path.dirname (file))] = 1
472 need_dirs = need_dir.keys()
473 need_dirs.sort()
474
475 # Now create them
476 for dir in need_dirs:
477 self.mkpath (dir)
478
479 # And walk over the list of files, either making a hard link (if
480 # os.link exists) to each one that doesn't already exist in its
481 # corresponding location under 'base_dir', or copying each file
482 # that's out-of-date in 'base_dir'. (Usually, all files will be
483 # out-of-date, because by default we blow away 'base_dir' when
484 # we're done making the distribution archives.)
485
486 try:
487 link = os.link
488 msg = "making hard links in %s..." % base_dir
489 except AttributeError:
490 link = 0
491 msg = "copying files to %s..." % base_dir
492
493 self.announce (msg)
494 for file in files:
495 dest = os.path.join (base_dir, file)
496 if link:
497 if not os.path.exists (dest):
498 self.execute (os.link, (file, dest),
499 "linking %s -> %s" % (file, dest))
500 else:
501 self.copy_file (file, dest)
502
503 # make_release_tree ()
504
505
506 def nuke_release_tree (self, base_dir):
507 try:
508 self.execute (rmtree, (base_dir,),
509 "removing %s" % base_dir)
510 except (IOError, OSError), exc:
511 if exc.filename:
512 msg = "error removing %s: %s (%s)" % \
513 (base_dir, exc.strerror, exc.filename)
514 else:
515 msg = "error removing %s: %s" % (base_dir, exc.strerror)
516 self.warn (msg)
517
518
519 def make_tarball (self, base_dir, compress="gzip"):
520
521 # XXX GNU tar 1.13 has a nifty option to add a prefix directory.
522 # It's pretty new, though, so we certainly can't require it --
523 # but it would be nice to take advantage of it to skip the
524 # "create a tree of hardlinks" step! (Would also be nice to
525 # detect GNU tar to use its 'z' option and save a step.)
526
527 if compress is not None and compress not in ('gzip', 'compress'):
528 raise ValueError, \
529 "if given, 'compress' must be 'gzip' or 'compress'"
530
531 archive_name = base_dir + ".tar"
532 self.spawn (["tar", "-cf", archive_name, base_dir])
533
534 if compress:
535 self.spawn ([compress, archive_name])
536
537
538 def make_zipfile (self, base_dir):
539
540 # This initially assumed the Unix 'zip' utility -- but
541 # apparently InfoZIP's zip.exe works the same under Windows, so
542 # no changes needed!
543
544 try:
545 self.spawn (["zip", "-r", base_dir + ".zip", base_dir])
546 except DistutilsExecError:
547
548 # XXX really should distinguish between "couldn't find
549 # external 'zip' command" and "zip failed" -- shouldn't try
550 # again in the latter case. (I think fixing this will
551 # require some cooperation from the spawn module -- perhaps
552 # a utility function to search the path, so we can fallback
553 # on zipfile.py without the failed spawn.)
554 try:
555 import zipfile
556 except ImportError:
557 raise DistutilsExecError, \
558 ("unable to create zip file '%s.zip': " +
559 "could neither find a standalone zip utility nor " +
560 "import the 'zipfile' module") % base_dir
561
562 z = zipfile.ZipFile (base_dir + ".zip", "wb",
563 compression=zipfile.ZIP_DEFLATED)
564
565 def visit (z, dirname, names):
566 for name in names:
567 path = os.path.join (dirname, name)
568 if os.path.isfile (path):
569 z.write (path, path)
570
571 os.path.walk (base_dir, visit, z)
572 z.close()
573
574
575 def make_distribution (self):
576
577 # Don't warn about missing meta-data here -- should be done
578 # elsewhere.
579 name = self.distribution.name or "UNKNOWN"
580 version = self.distribution.version
581
582 if version:
583 base_dir = "%s-%s" % (name, version)
584 else:
585 base_dir = name
586
587 # Remove any files that match "base_dir" from the fileset -- we
588 # don't want to go distributing the distribution inside itself!
589 self.exclude_pattern (base_dir + "*")
590
591 self.make_release_tree (base_dir, self.files)
592 for fmt in self.formats:
593 if fmt == 'gztar':
594 self.make_tarball (base_dir, compress='gzip')
595 elif fmt == 'ztar':
596 self.make_tarball (base_dir, compress='compress')
597 elif fmt == 'tar':
598 self.make_tarball (base_dir, compress=None)
599 elif fmt == 'zip':
600 self.make_zipfile (base_dir)
601
602 if not self.keep_tree:
603 self.nuke_release_tree (base_dir)
604
605# class Dist
606
607
608# ----------------------------------------------------------------------
609# Utility functions
610
611def findall (dir = os.curdir):
612 """Find all files under 'dir' and return the list of full
613 filenames (relative to 'dir')."""
614
615 list = []
616 stack = [dir]
617 pop = stack.pop
618 push = stack.append
619
620 while stack:
621 dir = pop()
622 names = os.listdir (dir)
623
624 for name in names:
625 if dir != os.curdir: # avoid the dreaded "./" syndrome
626 fullname = os.path.join (dir, name)
627 else:
628 fullname = name
629 list.append (fullname)
630 if os.path.isdir (fullname) and not os.path.islink(fullname):
631 push (fullname)
632
633 return list
634
635
636def select_pattern (files, pattern, anchor=1, prefix=None):
637 """Select strings (presumably filenames) from 'files' that match
638 'pattern', a Unix-style wildcard (glob) pattern. Patterns are not
639 quite the same as implemented by the 'fnmatch' module: '*' and '?'
640 match non-special characters, where "special" is platform-dependent:
641 slash on Unix, colon, slash, and backslash on DOS/Windows, and colon
642 on Mac OS.
643
644 If 'anchor' is true (the default), then the pattern match is more
645 stringent: "*.py" will match "foo.py" but not "foo/bar.py". If
646 'anchor' is false, both of these will match.
647
648 If 'prefix' is supplied, then only filenames starting with 'prefix'
649 (itself a pattern) and ending with 'pattern', with anything in
650 between them, will match. 'anchor' is ignored in this case.
651
652 Return the list of matching strings, possibly empty."""
653
654 matches = []
655 pattern_re = translate_pattern (pattern, anchor, prefix)
656 print "select_pattern: applying re %s" % pattern_re.pattern
657 for name in files:
658 if pattern_re.search (name):
659 matches.append (name)
660 print " adding", name
661
662 return matches
663
664# select_pattern ()
665
666
667def exclude_pattern (files, pattern, anchor=1, prefix=None):
668
669 pattern_re = translate_pattern (pattern, anchor, prefix)
670 print "exclude_pattern: applying re %s" % pattern_re.pattern
671 for i in range (len(files)-1, -1, -1):
672 if pattern_re.search (files[i]):
673 print " removing", files[i]
674 del files[i]
675
676# exclude_pattern ()
677
678
679def glob_to_re (pattern):
680 """Translate a shell-like glob pattern to a regular expression;
681 return a string containing the regex. Differs from
682 'fnmatch.translate()' in that '*' does not match "special
683 characters" (which are platform-specific)."""
684 pattern_re = fnmatch.translate (pattern)
685
686 # '?' and '*' in the glob pattern become '.' and '.*' in the RE, which
687 # IMHO is wrong -- '?' and '*' aren't supposed to match slash in Unix,
688 # and by extension they shouldn't match such "special characters" under
689 # any OS. So change all non-escaped dots in the RE to match any
690 # character except the special characters.
691 # XXX currently the "special characters" are just slash -- i.e. this is
692 # Unix-only.
693 pattern_re = re.sub (r'(^|[^\\])\.', r'\1[^/]', pattern_re)
694 return pattern_re
695
696# glob_to_re ()
697
698
699def translate_pattern (pattern, anchor=1, prefix=None):
700 """Translate a shell-like wildcard pattern to a compiled regular
701 expression. Return the compiled regex."""
702
703 if pattern:
704 pattern_re = glob_to_re (pattern)
705 else:
706 pattern_re = ''
707
708 if prefix is not None:
709 prefix_re = (glob_to_re (prefix))[0:-1] # ditch trailing $
710 pattern_re = "^" + os.path.join (prefix_re, ".*" + pattern_re)
711 else: # no prefix -- respect anchor flag
712 if anchor:
713 pattern_re = "^" + pattern_re
714
715 return re.compile (pattern_re)
716
717# translate_pattern ()