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