blob: 0f9e30bad8eb8f3a32bbf360b32440cfbd8487a8 [file] [log] [blame]
Greg Wardef4490f1999-09-29 12:50:13 +00001"""distutils.command.dist
2
3Implements the Distutils 'dist' 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
Greg Ward1d0495e1999-12-12 17:07:22 +000013from shutil import rmtree
Greg Wardef4490f1999-09-29 12:50:13 +000014from distutils.core import Command
15from distutils.text_file import TextFile
Greg Ward1b3a9af2000-01-17 20:23:34 +000016from distutils.errors import DistutilsExecError
Greg Wardef4490f1999-09-29 12:50:13 +000017
18
19# Possible modes of operation:
20# - require an explicit manifest that lists every single file (presumably
21# along with a way to auto-generate the manifest)
22# - require an explicit manifest, but allow it to have globs or
23# filename patterns of some kind (and also have auto-generation)
24# - allow an explict manifest, but automatically augment it at runtime
25# with the source files mentioned in 'packages', 'py_modules', and
26# 'ext_modules' (and any other such things that might come along)
27
28# I'm liking the third way. Possible gotchas:
29# - redundant specification: 'packages' includes 'foo' and manifest
30# includes 'foo/*.py'
31# - obvious conflict: 'packages' includes 'foo' and manifest
32# includes '! foo/*.py' (can't imagine why you'd want this)
33# - subtle conflict: 'packages' includes 'foo' and manifest
34# includes '! foo/bar.py' (this could well be desired: eg. exclude
35# an experimental module from distribution)
36
37# Syntax for the manifest file:
38# - if a line is just a Unix-style glob by itself, it's a "simple include
39# pattern": go find all files that match and add them to the list
40# of files
41# - if a line is a glob preceded by "!", then it's a "simple exclude
42# pattern": go over the current list of files and exclude any that
43# match the glob pattern
44# - if a line consists of a directory name followed by zero or more
45# glob patterns, then we'll recursively explore that directory tree
46# - the glob patterns can be include (no punctuation) or exclude
47# (prefixed by "!", no space)
48# - if no patterns given or the first pattern is not an include pattern,
49# then assume "*" -- ie. find everything (and then start applying
50# the rest of the patterns)
51# - the patterns are given in order of increasing precedence, ie.
52# the *last* one to match a given file applies to it
53#
54# example (ignoring auto-augmentation!):
55# distutils/*.py
56# distutils/command/*.py
57# ! distutils/bleeding_edge.py
58# examples/*.py
59# examples/README
60#
61# smarter way (that *will* include distutils/command/bleeding_edge.py!)
62# distutils *.py
63# ! distutils/bleeding_edge.py
64# examples !*~ !*.py[co] (same as: examples * !*~ !*.py[co])
65# test test_* *.txt !*~ !*.py[co]
66# README
67# setup.py
68#
69# The actual Distutils manifest (don't need to mention source files,
70# README, setup.py -- they're automatically distributed!):
71# examples !*~ !*.py[co]
72# test !*~ !*.py[co]
73
74# The algorithm that will make it work:
75# files = stuff from 'packages', 'py_modules', 'ext_modules',
76# plus README, setup.py, ... ?
77# foreach pattern in manifest file:
78# if simple-include-pattern: # "distutils/*.py"
79# files.append (glob (pattern))
80# elif simple-exclude-pattern: # "! distutils/foo*"
81# xfiles = glob (pattern)
82# remove all xfiles from files
83# elif recursive-pattern: # "examples" (just a directory name)
84# patterns = rest-of-words-on-line
85# dir_files = list of all files under dir
86# if patterns:
87# if patterns[0] is an exclude-pattern:
88# insert "*" at patterns[0]
89# for file in dir_files:
90# for dpattern in reverse (patterns):
91# if file matches dpattern:
92# if dpattern is an include-pattern:
93# files.append (file)
94# else:
95# nothing, don't include it
96# next file
97# else:
98# files.extend (dir_files) # ie. accept all of them
99
100
101# Anyways, this is all implemented below -- BUT it is largely untested; I
102# know it works for the simple case of distributing the Distutils, but
103# haven't tried it on more complicated examples. Undoubtedly doing so will
104# reveal bugs and cause delays, so I'm waiting until after I've released
105# Distutils 0.1.
106
107
108# Other things we need to look for in creating a source distribution:
109# - make sure there's a README
110# - make sure the distribution meta-info is supplied and non-empty
111# (*must* have name, version, ((author and author_email) or
112# (maintainer and maintainer_email)), url
113#
114# Frills:
115# - make sure the setup script is called "setup.py"
116# - make sure the README refers to "setup.py" (ie. has a line matching
117# /^\s*python\s+setup\.py/)
118
119# A crazy idea that conflicts with having/requiring 'version' in setup.py:
120# - make sure there's a version number in the "main file" (main file
121# is __init__.py of first package, or the first module if no packages,
122# or the first extension module if no pure Python modules)
123# - XXX how do we look for __version__ in an extension module?
124# - XXX do we import and look for __version__? or just scan source for
125# /^__version__\s*=\s*"[^"]+"/ ?
126# - what about 'version_from' as an alternative to 'version' -- then
127# we know just where to search for the version -- no guessing about
128# what the "main file" is
129
130
131
132class Dist (Command):
133
Greg Warde1ada501999-10-23 19:25:05 +0000134 options = [('formats=', None,
Greg Wardef4490f1999-09-29 12:50:13 +0000135 "formats for source distribution (tar, ztar, gztar, or zip)"),
136 ('manifest=', 'm',
137 "name of manifest file"),
Greg Wardb24afe11999-09-29 13:14:27 +0000138 ('list-only', 'l',
139 "just list files that would be distributed"),
Greg Ward1d0495e1999-12-12 17:07:22 +0000140 ('keep-tree', 'k',
141 "keep the distribution tree around after creating " +
142 "archive file(s)"),
Greg Wardef4490f1999-09-29 12:50:13 +0000143 ]
144
145 default_format = { 'posix': 'gztar',
146 'nt': 'zip' }
147
148 exclude_re = re.compile (r'\s*!\s*(\S+)') # for manifest lines
149
150
151 def set_default_options (self):
152 self.formats = None
153 self.manifest = None
Greg Wardb24afe11999-09-29 13:14:27 +0000154 self.list_only = 0
Greg Ward1d0495e1999-12-12 17:07:22 +0000155 self.keep_tree = 0
Greg Wardef4490f1999-09-29 12:50:13 +0000156
157
158 def set_final_options (self):
159 if self.formats is None:
160 try:
161 self.formats = [self.default_format[os.name]]
162 except KeyError:
163 raise DistutilsPlatformError, \
164 "don't know how to build source distributions on " + \
165 "%s platform" % os.name
166 elif type (self.formats) is StringType:
167 self.formats = string.split (self.formats, ',')
168
169 if self.manifest is None:
170 self.manifest = "MANIFEST"
171
172
173 def run (self):
174
175 self.check_metadata ()
176
177 self.files = []
178 self.find_defaults ()
179 self.read_manifest ()
180
Greg Wardb24afe11999-09-29 13:14:27 +0000181 if self.list_only:
182 for f in self.files:
183 print f
184
185 else:
186 self.make_distribution ()
Greg Wardef4490f1999-09-29 12:50:13 +0000187
188
189 def check_metadata (self):
190
191 dist = self.distribution
192
193 missing = []
194 for attr in ('name', 'version', 'url'):
195 if not (hasattr (dist, attr) and getattr (dist, attr)):
196 missing.append (attr)
197
198 if missing:
199 self.warn ("missing required meta-data: " +
200 string.join (missing, ", "))
201
202 if dist.author:
203 if not dist.author_email:
204 self.warn ("missing meta-data: if 'author' supplied, " +
205 "'author_email' must be supplied too")
206 elif dist.maintainer:
207 if not dist.maintainer_email:
208 self.warn ("missing meta-data: if 'maintainer' supplied, " +
209 "'maintainer_email' must be supplied too")
210 else:
Greg Ward1d0495e1999-12-12 17:07:22 +0000211 self.warn ("missing meta-data: either (author and author_email) " +
212 "or (maintainer and maintainer_email) " +
Greg Wardef4490f1999-09-29 12:50:13 +0000213 "must be supplied")
214
215 # check_metadata ()
216
217
218 def find_defaults (self):
219
220 standards = ['README', 'setup.py']
221 for fn in standards:
222 if os.path.exists (fn):
223 self.files.append (fn)
224 else:
225 self.warn ("standard file %s not found" % fn)
226
227 optional = ['test/test*.py']
228 for pattern in optional:
Greg Wardef930951999-10-03 21:09:14 +0000229 files = filter (os.path.isfile, glob (pattern))
Greg Wardef4490f1999-09-29 12:50:13 +0000230 if files:
231 self.files.extend (files)
232
233 if self.distribution.packages or self.distribution.py_modules:
234 build_py = self.find_peer ('build_py')
235 build_py.ensure_ready ()
236 self.files.extend (build_py.get_source_files ())
237
238 if self.distribution.ext_modules:
239 build_ext = self.find_peer ('build_ext')
240 build_ext.ensure_ready ()
241 self.files.extend (build_ext.get_source_files ())
242
243
244
245 def open_manifest (self, filename):
246 return TextFile (filename,
247 strip_comments=1,
248 skip_blanks=1,
249 join_lines=1,
250 lstrip_ws=1,
251 rstrip_ws=1,
252 collapse_ws=1)
253
254
255 def search_dir (self, dir, patterns):
256
257 allfiles = findall (dir)
258 if patterns:
259 if patterns[0][0] == "!": # starts with an exclude spec?
260 patterns.insert (0, "*")# then accept anything that isn't
261 # explicitly excluded
262
263 act_patterns = [] # "action-patterns": (include,regexp)
264 # tuples where include is a boolean
265 for pattern in patterns:
266 if pattern[0] == '!':
267 act_patterns.append \
268 ((0, re.compile (fnmatch.translate (pattern[1:]))))
269 else:
270 act_patterns.append \
271 ((1, re.compile (fnmatch.translate (pattern))))
272 act_patterns.reverse()
273
274
275 files = []
276 for file in allfiles:
277 for (include,regexp) in act_patterns:
Greg Ward97798b11999-12-13 21:38:57 +0000278 if regexp.search (file):
Greg Wardef4490f1999-09-29 12:50:13 +0000279 if include:
280 files.append (file)
281 break # continue to next file
282 else:
283 files = allfiles
284
285 return files
286
287 # search_dir ()
288
289
290 def exclude_files (self, pattern):
291
292 regexp = re.compile (fnmatch.translate (pattern))
293 for i in range (len (self.files)-1, -1, -1):
Greg Ward97798b11999-12-13 21:38:57 +0000294 if regexp.search (self.files[i]):
Greg Wardef4490f1999-09-29 12:50:13 +0000295 del self.files[i]
296
297
298 def read_manifest (self):
299
300 # self.files had better already be defined (and hold the
301 # "automatically found" files -- Python modules and extensions,
302 # README, setup script, ...)
303 assert self.files is not None
304
Greg Ward1d0495e1999-12-12 17:07:22 +0000305 try:
306 manifest = self.open_manifest (self.manifest)
307 except IOError, exc:
308 if type (exc) is InstanceType and hasattr (exc, 'strerror'):
309 msg = "could not open MANIFEST (%s)" % \
310 string.lower (exc.strerror)
311 else:
312 msg = "could not open MANIFST"
313
314 self.warn (msg + ": using default file list")
315 return
316
Greg Wardef4490f1999-09-29 12:50:13 +0000317 while 1:
318
319 pattern = manifest.readline()
320 if pattern is None: # end of file
321 break
322
323 # Cases:
324 # 1) simple-include: "*.py", "foo/*.py", "doc/*.html", "FAQ"
325 # 2) simple-exclude: same, prefaced by !
326 # 3) recursive: multi-word line, first word a directory
327
328 exclude = self.exclude_re.match (pattern)
329 if exclude:
330 pattern = exclude.group (1)
331
332 words = string.split (pattern)
333 assert words # must have something!
334 if os.name != 'posix':
335 words[0] = apply (os.path.join, string.split (words[0], '/'))
336
337 # First word is a directory, possibly with include/exclude
338 # patterns making up the rest of the line: it's a recursive
339 # pattern
340 if os.path.isdir (words[0]):
341 if exclude:
Greg Warde9436da2000-01-09 22:39:32 +0000342 manifest.warn ("exclude (!) doesn't apply to " +
343 "whole directory trees")
Greg Wardef4490f1999-09-29 12:50:13 +0000344 continue
345
346 dir_files = self.search_dir (words[0], words[1:])
347 self.files.extend (dir_files)
348
349 # Multiple words in pattern: that's a no-no unless the first
350 # word is a directory name
351 elif len (words) > 1:
Greg Warde9436da2000-01-09 22:39:32 +0000352 manifest.warn ("can't have multiple words unless first word " +
353 "('%s') is a directory name" % words[0])
Greg Wardef4490f1999-09-29 12:50:13 +0000354 continue
355
356 # Single word, no bang: it's a "simple include pattern"
357 elif not exclude:
Greg Wardef930951999-10-03 21:09:14 +0000358 matches = filter (os.path.isfile, glob (pattern))
Greg Wardef4490f1999-09-29 12:50:13 +0000359 if matches:
360 self.files.extend (matches)
361 else:
362 manifest.warn ("no matches for '%s' found" % pattern)
363
364
365 # Single word prefixed with a bang: it's a "simple exclude pattern"
366 else:
367 if self.exclude_files (pattern) == 0:
Greg Warde9436da2000-01-09 22:39:32 +0000368 manifest.warn ("no files excluded by '%s'" % pattern)
Greg Wardef4490f1999-09-29 12:50:13 +0000369
370 # if/elif/.../else on 'pattern'
371
372 # loop over lines of 'manifest'
373
374 # read_manifest ()
375
376
377 def make_release_tree (self, base_dir, files):
378
379 # XXX this is Unix-specific
380
381 # First get the list of directories to create
382 need_dir = {}
383 for file in files:
384 need_dir[os.path.join (base_dir, os.path.dirname (file))] = 1
385 need_dirs = need_dir.keys()
386 need_dirs.sort()
387
388 # Now create them
389 for dir in need_dirs:
390 self.mkpath (dir)
391
Greg Ward1b3a9af2000-01-17 20:23:34 +0000392 # And walk over the list of files, either making a hard link (if
393 # os.link exists) to each one that doesn't already exist in its
394 # corresponding location under 'base_dir', or copying each file
395 # that's out-of-date in 'base_dir'. (Usually, all files will be
396 # out-of-date, because by default we blow away 'base_dir' when
397 # we're done making the distribution archives.)
Greg Wardef4490f1999-09-29 12:50:13 +0000398
Greg Ward1b3a9af2000-01-17 20:23:34 +0000399 try:
400 link = os.link
401 msg = "making hard links in %s..." % base_dir
402 except AttributeError:
403 link = 0
404 msg = "copying files to %s..." % base_dir
405
406 self.announce (msg)
Greg Wardef4490f1999-09-29 12:50:13 +0000407 for file in files:
408 dest = os.path.join (base_dir, file)
Greg Ward1b3a9af2000-01-17 20:23:34 +0000409 if link:
410 if not os.path.exists (dest):
411 self.execute (os.link, (file, dest),
412 "linking %s -> %s" % (file, dest))
413 else:
414 self.copy_file (file, dest)
415
Greg Wardef4490f1999-09-29 12:50:13 +0000416 # make_release_tree ()
417
418
Greg Ward1d0495e1999-12-12 17:07:22 +0000419 def nuke_release_tree (self, base_dir):
Greg Wardad83f041999-12-16 01:14:15 +0000420 try:
421 self.execute (rmtree, (base_dir,),
422 "removing %s" % base_dir)
423 except (IOError, OSError), exc:
424 if exc.filename:
425 msg = "error removing %s: %s (%s)" % \
426 (base_dir, exc.strerror, exc.filename)
427 else:
428 msg = "error removing %s: %s" % (base_dir, exc.strerror)
429 self.warn (msg)
Greg Ward1d0495e1999-12-12 17:07:22 +0000430
431
Greg Warde1ada501999-10-23 19:25:05 +0000432 def make_tarball (self, base_dir, compress="gzip"):
Greg Wardef4490f1999-09-29 12:50:13 +0000433
434 # XXX GNU tar 1.13 has a nifty option to add a prefix directory.
Greg Warde1ada501999-10-23 19:25:05 +0000435 # It's pretty new, though, so we certainly can't require it --
436 # but it would be nice to take advantage of it to skip the
437 # "create a tree of hardlinks" step! (Would also be nice to
438 # detect GNU tar to use its 'z' option and save a step.)
Greg Wardef4490f1999-09-29 12:50:13 +0000439
Greg Warde1ada501999-10-23 19:25:05 +0000440 if compress is not None and compress not in ('gzip', 'compress'):
441 raise ValueError, \
442 "if given, 'compress' must be 'gzip' or 'compress'"
Greg Wardef4490f1999-09-29 12:50:13 +0000443
Greg Warde1ada501999-10-23 19:25:05 +0000444 archive_name = base_dir + ".tar"
445 self.spawn (["tar", "-cf", archive_name, base_dir])
446
447 if compress:
448 self.spawn ([compress, archive_name])
Greg Wardef4490f1999-09-29 12:50:13 +0000449
450
451 def make_zipfile (self, base_dir):
452
Greg Wardcbeca7b2000-01-17 18:04:04 +0000453 # This initially assumed the Unix 'zip' utility -- but
454 # apparently InfoZIP's zip.exe works the same under Windows, so
455 # no changes needed!
Greg Wardef4490f1999-09-29 12:50:13 +0000456
Greg Wardcbeca7b2000-01-17 18:04:04 +0000457 try:
458 self.spawn (["zip", "-r", base_dir + ".zip", base_dir])
459 except DistutilsExecError:
460
461 # XXX really should distinguish between "couldn't find
462 # external 'zip' command" and "zip failed" -- shouldn't try
463 # again in the latter case. (I think fixing this will
464 # require some cooperation from the spawn module -- perhaps
465 # a utility function to search the path, so we can fallback
466 # on zipfile.py without the failed spawn.)
467 try:
468 import zipfile
469 except ImportError:
470 raise DistutilsExecError, \
Greg Ward1b3a9af2000-01-17 20:23:34 +0000471 ("unable to create zip file '%s.zip': " +
Greg Wardcbeca7b2000-01-17 18:04:04 +0000472 "could neither find a standalone zip utility nor " +
473 "import the 'zipfile' module") % base_dir
474
Greg Ward9f200cb2000-01-17 21:58:07 +0000475 z = zipfile.ZipFile (base_dir + ".zip", "wb",
476 compression=zipfile.ZIP_DEFLATED)
Greg Wardcbeca7b2000-01-17 18:04:04 +0000477
478 def visit (z, dirname, names):
479 for name in names:
480 path = os.path.join (dirname, name)
481 if os.path.isfile (path):
482 z.write (path, path)
483
484 os.path.walk (base_dir, visit, z)
485 z.close()
Greg Wardef4490f1999-09-29 12:50:13 +0000486
487
488 def make_distribution (self):
489
490 # Don't warn about missing meta-data here -- should be done
491 # elsewhere.
492 name = self.distribution.name or "UNKNOWN"
493 version = self.distribution.version
494
495 if version:
496 base_dir = "%s-%s" % (name, version)
497 else:
498 base_dir = name
499
500 # Remove any files that match "base_dir" from the fileset -- we
501 # don't want to go distributing the distribution inside itself!
502 self.exclude_files (base_dir + "*")
503
504 self.make_release_tree (base_dir, self.files)
Greg Warde1ada501999-10-23 19:25:05 +0000505 for fmt in self.formats:
506 if fmt == 'gztar':
507 self.make_tarball (base_dir, compress='gzip')
508 elif fmt == 'ztar':
509 self.make_tarball (base_dir, compress='compress')
510 elif fmt == 'tar':
511 self.make_tarball (base_dir, compress=None)
512 elif fmt == 'zip':
513 self.make_zipfile (base_dir)
Greg Wardef4490f1999-09-29 12:50:13 +0000514
Greg Ward1d0495e1999-12-12 17:07:22 +0000515 if not self.keep_tree:
516 self.nuke_release_tree (base_dir)
517
Greg Wardef4490f1999-09-29 12:50:13 +0000518# class Dist
519
520
521# ----------------------------------------------------------------------
522# Utility functions
523
524def findall (dir = os.curdir):
525 """Find all files under 'dir' and return the sorted list of full
526 filenames (relative to 'dir')."""
527
528 list = []
529 stack = [dir]
530 pop = stack.pop
531 push = stack.append
532
533 while stack:
534 dir = pop()
535 names = os.listdir (dir)
536
537 for name in names:
538 fullname = os.path.join (dir, name)
539 list.append (fullname)
540 if os.path.isdir (fullname) and not os.path.islink(fullname):
541 push (fullname)
542
543 list.sort()
544 return list