blob: 9931e3752d80281b19687561bb58bd62d8330bc3 [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
16
17
18# Possible modes of operation:
19# - require an explicit manifest that lists every single file (presumably
20# along with a way to auto-generate the manifest)
21# - require an explicit manifest, but allow it to have globs or
22# filename patterns of some kind (and also have auto-generation)
23# - allow an explict manifest, but automatically augment it at runtime
24# with the source files mentioned in 'packages', 'py_modules', and
25# 'ext_modules' (and any other such things that might come along)
26
27# I'm liking the third way. Possible gotchas:
28# - redundant specification: 'packages' includes 'foo' and manifest
29# includes 'foo/*.py'
30# - obvious conflict: 'packages' includes 'foo' and manifest
31# includes '! foo/*.py' (can't imagine why you'd want this)
32# - subtle conflict: 'packages' includes 'foo' and manifest
33# includes '! foo/bar.py' (this could well be desired: eg. exclude
34# an experimental module from distribution)
35
36# Syntax for the manifest file:
37# - if a line is just a Unix-style glob by itself, it's a "simple include
38# pattern": go find all files that match and add them to the list
39# of files
40# - if a line is a glob preceded by "!", then it's a "simple exclude
41# pattern": go over the current list of files and exclude any that
42# match the glob pattern
43# - if a line consists of a directory name followed by zero or more
44# glob patterns, then we'll recursively explore that directory tree
45# - the glob patterns can be include (no punctuation) or exclude
46# (prefixed by "!", no space)
47# - if no patterns given or the first pattern is not an include pattern,
48# then assume "*" -- ie. find everything (and then start applying
49# the rest of the patterns)
50# - the patterns are given in order of increasing precedence, ie.
51# the *last* one to match a given file applies to it
52#
53# example (ignoring auto-augmentation!):
54# distutils/*.py
55# distutils/command/*.py
56# ! distutils/bleeding_edge.py
57# examples/*.py
58# examples/README
59#
60# smarter way (that *will* include distutils/command/bleeding_edge.py!)
61# distutils *.py
62# ! distutils/bleeding_edge.py
63# examples !*~ !*.py[co] (same as: examples * !*~ !*.py[co])
64# test test_* *.txt !*~ !*.py[co]
65# README
66# setup.py
67#
68# The actual Distutils manifest (don't need to mention source files,
69# README, setup.py -- they're automatically distributed!):
70# examples !*~ !*.py[co]
71# test !*~ !*.py[co]
72
73# The algorithm that will make it work:
74# files = stuff from 'packages', 'py_modules', 'ext_modules',
75# plus README, setup.py, ... ?
76# foreach pattern in manifest file:
77# if simple-include-pattern: # "distutils/*.py"
78# files.append (glob (pattern))
79# elif simple-exclude-pattern: # "! distutils/foo*"
80# xfiles = glob (pattern)
81# remove all xfiles from files
82# elif recursive-pattern: # "examples" (just a directory name)
83# patterns = rest-of-words-on-line
84# dir_files = list of all files under dir
85# if patterns:
86# if patterns[0] is an exclude-pattern:
87# insert "*" at patterns[0]
88# for file in dir_files:
89# for dpattern in reverse (patterns):
90# if file matches dpattern:
91# if dpattern is an include-pattern:
92# files.append (file)
93# else:
94# nothing, don't include it
95# next file
96# else:
97# files.extend (dir_files) # ie. accept all of them
98
99
100# Anyways, this is all implemented below -- BUT it is largely untested; I
101# know it works for the simple case of distributing the Distutils, but
102# haven't tried it on more complicated examples. Undoubtedly doing so will
103# reveal bugs and cause delays, so I'm waiting until after I've released
104# Distutils 0.1.
105
106
107# Other things we need to look for in creating a source distribution:
108# - make sure there's a README
109# - make sure the distribution meta-info is supplied and non-empty
110# (*must* have name, version, ((author and author_email) or
111# (maintainer and maintainer_email)), url
112#
113# Frills:
114# - make sure the setup script is called "setup.py"
115# - make sure the README refers to "setup.py" (ie. has a line matching
116# /^\s*python\s+setup\.py/)
117
118# A crazy idea that conflicts with having/requiring 'version' in setup.py:
119# - make sure there's a version number in the "main file" (main file
120# is __init__.py of first package, or the first module if no packages,
121# or the first extension module if no pure Python modules)
122# - XXX how do we look for __version__ in an extension module?
123# - XXX do we import and look for __version__? or just scan source for
124# /^__version__\s*=\s*"[^"]+"/ ?
125# - what about 'version_from' as an alternative to 'version' -- then
126# we know just where to search for the version -- no guessing about
127# what the "main file" is
128
129
130
131class Dist (Command):
132
Greg Warde1ada501999-10-23 19:25:05 +0000133 options = [('formats=', None,
Greg Wardef4490f1999-09-29 12:50:13 +0000134 "formats for source distribution (tar, ztar, gztar, or zip)"),
135 ('manifest=', 'm',
136 "name of manifest file"),
Greg Wardb24afe11999-09-29 13:14:27 +0000137 ('list-only', 'l',
138 "just list files that would be distributed"),
Greg Ward1d0495e1999-12-12 17:07:22 +0000139 ('keep-tree', 'k',
140 "keep the distribution tree around after creating " +
141 "archive file(s)"),
Greg Wardef4490f1999-09-29 12:50:13 +0000142 ]
143
144 default_format = { 'posix': 'gztar',
145 'nt': 'zip' }
146
147 exclude_re = re.compile (r'\s*!\s*(\S+)') # for manifest lines
148
149
150 def set_default_options (self):
151 self.formats = None
152 self.manifest = None
Greg Wardb24afe11999-09-29 13:14:27 +0000153 self.list_only = 0
Greg Ward1d0495e1999-12-12 17:07:22 +0000154 self.keep_tree = 0
Greg Wardef4490f1999-09-29 12:50:13 +0000155
156
157 def set_final_options (self):
158 if self.formats is None:
159 try:
160 self.formats = [self.default_format[os.name]]
161 except KeyError:
162 raise DistutilsPlatformError, \
163 "don't know how to build source distributions on " + \
164 "%s platform" % os.name
165 elif type (self.formats) is StringType:
166 self.formats = string.split (self.formats, ',')
167
168 if self.manifest is None:
169 self.manifest = "MANIFEST"
170
171
172 def run (self):
173
174 self.check_metadata ()
175
176 self.files = []
177 self.find_defaults ()
178 self.read_manifest ()
179
Greg Wardb24afe11999-09-29 13:14:27 +0000180 if self.list_only:
181 for f in self.files:
182 print f
183
184 else:
185 self.make_distribution ()
Greg Wardef4490f1999-09-29 12:50:13 +0000186
187
188 def check_metadata (self):
189
190 dist = self.distribution
191
192 missing = []
193 for attr in ('name', 'version', 'url'):
194 if not (hasattr (dist, attr) and getattr (dist, attr)):
195 missing.append (attr)
196
197 if missing:
198 self.warn ("missing required meta-data: " +
199 string.join (missing, ", "))
200
201 if dist.author:
202 if not dist.author_email:
203 self.warn ("missing meta-data: if 'author' supplied, " +
204 "'author_email' must be supplied too")
205 elif dist.maintainer:
206 if not dist.maintainer_email:
207 self.warn ("missing meta-data: if 'maintainer' supplied, " +
208 "'maintainer_email' must be supplied too")
209 else:
Greg Ward1d0495e1999-12-12 17:07:22 +0000210 self.warn ("missing meta-data: either (author and author_email) " +
211 "or (maintainer and maintainer_email) " +
Greg Wardef4490f1999-09-29 12:50:13 +0000212 "must be supplied")
213
214 # check_metadata ()
215
216
217 def find_defaults (self):
218
219 standards = ['README', 'setup.py']
220 for fn in standards:
221 if os.path.exists (fn):
222 self.files.append (fn)
223 else:
224 self.warn ("standard file %s not found" % fn)
225
226 optional = ['test/test*.py']
227 for pattern in optional:
Greg Wardef930951999-10-03 21:09:14 +0000228 files = filter (os.path.isfile, glob (pattern))
Greg Wardef4490f1999-09-29 12:50:13 +0000229 if files:
230 self.files.extend (files)
231
232 if self.distribution.packages or self.distribution.py_modules:
233 build_py = self.find_peer ('build_py')
234 build_py.ensure_ready ()
235 self.files.extend (build_py.get_source_files ())
236
237 if self.distribution.ext_modules:
238 build_ext = self.find_peer ('build_ext')
239 build_ext.ensure_ready ()
240 self.files.extend (build_ext.get_source_files ())
241
242
243
244 def open_manifest (self, filename):
245 return TextFile (filename,
246 strip_comments=1,
247 skip_blanks=1,
248 join_lines=1,
249 lstrip_ws=1,
250 rstrip_ws=1,
251 collapse_ws=1)
252
253
254 def search_dir (self, dir, patterns):
255
256 allfiles = findall (dir)
257 if patterns:
258 if patterns[0][0] == "!": # starts with an exclude spec?
259 patterns.insert (0, "*")# then accept anything that isn't
260 # explicitly excluded
261
262 act_patterns = [] # "action-patterns": (include,regexp)
263 # tuples where include is a boolean
264 for pattern in patterns:
265 if pattern[0] == '!':
266 act_patterns.append \
267 ((0, re.compile (fnmatch.translate (pattern[1:]))))
268 else:
269 act_patterns.append \
270 ((1, re.compile (fnmatch.translate (pattern))))
271 act_patterns.reverse()
272
273
274 files = []
275 for file in allfiles:
276 for (include,regexp) in act_patterns:
Greg Ward97798b11999-12-13 21:38:57 +0000277 if regexp.search (file):
Greg Wardef4490f1999-09-29 12:50:13 +0000278 if include:
279 files.append (file)
280 break # continue to next file
281 else:
282 files = allfiles
283
284 return files
285
286 # search_dir ()
287
288
289 def exclude_files (self, pattern):
290
291 regexp = re.compile (fnmatch.translate (pattern))
292 for i in range (len (self.files)-1, -1, -1):
Greg Ward97798b11999-12-13 21:38:57 +0000293 if regexp.search (self.files[i]):
Greg Wardef4490f1999-09-29 12:50:13 +0000294 del self.files[i]
295
296
297 def read_manifest (self):
298
299 # self.files had better already be defined (and hold the
300 # "automatically found" files -- Python modules and extensions,
301 # README, setup script, ...)
302 assert self.files is not None
303
Greg Ward1d0495e1999-12-12 17:07:22 +0000304 try:
305 manifest = self.open_manifest (self.manifest)
306 except IOError, exc:
307 if type (exc) is InstanceType and hasattr (exc, 'strerror'):
308 msg = "could not open MANIFEST (%s)" % \
309 string.lower (exc.strerror)
310 else:
311 msg = "could not open MANIFST"
312
313 self.warn (msg + ": using default file list")
314 return
315
Greg Wardef4490f1999-09-29 12:50:13 +0000316 while 1:
317
318 pattern = manifest.readline()
319 if pattern is None: # end of file
320 break
321
322 # Cases:
323 # 1) simple-include: "*.py", "foo/*.py", "doc/*.html", "FAQ"
324 # 2) simple-exclude: same, prefaced by !
325 # 3) recursive: multi-word line, first word a directory
326
327 exclude = self.exclude_re.match (pattern)
328 if exclude:
329 pattern = exclude.group (1)
330
331 words = string.split (pattern)
332 assert words # must have something!
333 if os.name != 'posix':
334 words[0] = apply (os.path.join, string.split (words[0], '/'))
335
336 # First word is a directory, possibly with include/exclude
337 # patterns making up the rest of the line: it's a recursive
338 # pattern
339 if os.path.isdir (words[0]):
340 if exclude:
341 file.warn ("exclude (!) doesn't apply to " +
342 "whole directory trees")
343 continue
344
345 dir_files = self.search_dir (words[0], words[1:])
346 self.files.extend (dir_files)
347
348 # Multiple words in pattern: that's a no-no unless the first
349 # word is a directory name
350 elif len (words) > 1:
351 file.warn ("can't have multiple words unless first word " +
352 "('%s') is a directory name" % words[0])
353 continue
354
355 # Single word, no bang: it's a "simple include pattern"
356 elif not exclude:
Greg Wardef930951999-10-03 21:09:14 +0000357 matches = filter (os.path.isfile, glob (pattern))
Greg Wardef4490f1999-09-29 12:50:13 +0000358 if matches:
359 self.files.extend (matches)
360 else:
361 manifest.warn ("no matches for '%s' found" % pattern)
362
363
364 # Single word prefixed with a bang: it's a "simple exclude pattern"
365 else:
366 if self.exclude_files (pattern) == 0:
367 file.warn ("no files excluded by '%s'" % pattern)
368
369 # if/elif/.../else on 'pattern'
370
371 # loop over lines of 'manifest'
372
373 # read_manifest ()
374
375
376 def make_release_tree (self, base_dir, files):
377
378 # XXX this is Unix-specific
379
380 # First get the list of directories to create
381 need_dir = {}
382 for file in files:
383 need_dir[os.path.join (base_dir, os.path.dirname (file))] = 1
384 need_dirs = need_dir.keys()
385 need_dirs.sort()
386
387 # Now create them
388 for dir in need_dirs:
389 self.mkpath (dir)
390
391 # And walk over the list of files, making a hard link for
392 # each one that doesn't already exist in its corresponding
393 # location under 'base_dir'
394
395 self.announce ("making hard links in %s..." % base_dir)
396 for file in files:
397 dest = os.path.join (base_dir, file)
398 if not os.path.exists (dest):
399 self.execute (os.link, (file, dest),
400 "linking %s -> %s" % (file, dest))
401 # make_release_tree ()
402
403
Greg Ward1d0495e1999-12-12 17:07:22 +0000404 def nuke_release_tree (self, base_dir):
Greg Wardad83f041999-12-16 01:14:15 +0000405 try:
406 self.execute (rmtree, (base_dir,),
407 "removing %s" % base_dir)
408 except (IOError, OSError), exc:
409 if exc.filename:
410 msg = "error removing %s: %s (%s)" % \
411 (base_dir, exc.strerror, exc.filename)
412 else:
413 msg = "error removing %s: %s" % (base_dir, exc.strerror)
414 self.warn (msg)
Greg Ward1d0495e1999-12-12 17:07:22 +0000415
416
Greg Warde1ada501999-10-23 19:25:05 +0000417 def make_tarball (self, base_dir, compress="gzip"):
Greg Wardef4490f1999-09-29 12:50:13 +0000418
419 # XXX GNU tar 1.13 has a nifty option to add a prefix directory.
Greg Warde1ada501999-10-23 19:25:05 +0000420 # It's pretty new, though, so we certainly can't require it --
421 # but it would be nice to take advantage of it to skip the
422 # "create a tree of hardlinks" step! (Would also be nice to
423 # detect GNU tar to use its 'z' option and save a step.)
Greg Wardef4490f1999-09-29 12:50:13 +0000424
Greg Warde1ada501999-10-23 19:25:05 +0000425 if compress is not None and compress not in ('gzip', 'compress'):
426 raise ValueError, \
427 "if given, 'compress' must be 'gzip' or 'compress'"
Greg Wardef4490f1999-09-29 12:50:13 +0000428
Greg Warde1ada501999-10-23 19:25:05 +0000429 archive_name = base_dir + ".tar"
430 self.spawn (["tar", "-cf", archive_name, base_dir])
431
432 if compress:
433 self.spawn ([compress, archive_name])
Greg Wardef4490f1999-09-29 12:50:13 +0000434
435
436 def make_zipfile (self, base_dir):
437
438 # This assumes the Unix 'zip' utility -- it could be easily recast
439 # to use pkzip (or whatever the command-line zip creation utility
440 # on Redmond's archaic CP/M knockoff is nowadays), but I'll let
441 # someone who can actually test it do that.
442
Greg Ward6bad8641999-10-23 19:06:20 +0000443 self.spawn (["zip", "-r", base_dir + ".zip", base_dir])
Greg Wardef4490f1999-09-29 12:50:13 +0000444
445
446 def make_distribution (self):
447
448 # Don't warn about missing meta-data here -- should be done
449 # elsewhere.
450 name = self.distribution.name or "UNKNOWN"
451 version = self.distribution.version
452
453 if version:
454 base_dir = "%s-%s" % (name, version)
455 else:
456 base_dir = name
457
458 # Remove any files that match "base_dir" from the fileset -- we
459 # don't want to go distributing the distribution inside itself!
460 self.exclude_files (base_dir + "*")
461
462 self.make_release_tree (base_dir, self.files)
Greg Warde1ada501999-10-23 19:25:05 +0000463 for fmt in self.formats:
464 if fmt == 'gztar':
465 self.make_tarball (base_dir, compress='gzip')
466 elif fmt == 'ztar':
467 self.make_tarball (base_dir, compress='compress')
468 elif fmt == 'tar':
469 self.make_tarball (base_dir, compress=None)
470 elif fmt == 'zip':
471 self.make_zipfile (base_dir)
Greg Wardef4490f1999-09-29 12:50:13 +0000472
Greg Ward1d0495e1999-12-12 17:07:22 +0000473 if not self.keep_tree:
474 self.nuke_release_tree (base_dir)
475
Greg Wardef4490f1999-09-29 12:50:13 +0000476# class Dist
477
478
479# ----------------------------------------------------------------------
480# Utility functions
481
482def findall (dir = os.curdir):
483 """Find all files under 'dir' and return the sorted list of full
484 filenames (relative to 'dir')."""
485
486 list = []
487 stack = [dir]
488 pop = stack.pop
489 push = stack.append
490
491 while stack:
492 dir = pop()
493 names = os.listdir (dir)
494
495 for name in names:
496 fullname = os.path.join (dir, name)
497 list.append (fullname)
498 if os.path.isdir (fullname) and not os.path.islink(fullname):
499 push (fullname)
500
501 list.sort()
502 return list