sdist.py revision 1df03405fc30604e00d506ac0f8c112517d8526e
1"""distutils.command.sdist
2
3Implements the Distutils 'sdist' command (create a source distribution)."""
4
5# This module should be kept compatible with Python 2.1.
6
7__revision__ = "$Id$"
8
9import os, string
10from types import *
11from glob import glob
12from distutils.core import Command
13from distutils import dir_util, dep_util, file_util, archive_util
14from distutils.text_file import TextFile
15from distutils.errors import *
16from distutils.filelist import FileList
17from distutils import log
18
19
20def show_formats ():
21    """Print all possible values for the 'formats' option (used by
22    the "--help-formats" command-line option).
23    """
24    from distutils.fancy_getopt import FancyGetopt
25    from distutils.archive_util import ARCHIVE_FORMATS
26    formats=[]
27    for format in ARCHIVE_FORMATS.keys():
28        formats.append(("formats=" + format, None,
29                        ARCHIVE_FORMATS[format][2]))
30    formats.sort()
31    pretty_printer = FancyGetopt(formats)
32    pretty_printer.print_help(
33        "List of available source distribution formats:")
34
35class sdist (Command):
36
37    description = "create a source distribution (tarball, zip file, etc.)"
38
39    user_options = [
40        ('template=', 't',
41         "name of manifest template file [default: MANIFEST.in]"),
42        ('manifest=', 'm',
43         "name of manifest file [default: MANIFEST]"),
44        ('use-defaults', None,
45         "include the default file set in the manifest "
46         "[default; disable with --no-defaults]"),
47        ('no-defaults', None,
48         "don't include the default file set"),
49        ('prune', None,
50         "specifically exclude files/directories that should not be "
51         "distributed (build tree, RCS/CVS dirs, etc.) "
52         "[default; disable with --no-prune]"),
53        ('no-prune', None,
54         "don't automatically exclude anything"),
55        ('manifest-only', 'o',
56         "just regenerate the manifest and then stop "
57         "(implies --force-manifest)"),
58        ('force-manifest', 'f',
59         "forcibly regenerate the manifest and carry on as usual"),
60        ('formats=', None,
61         "formats for source distribution (comma-separated list)"),
62        ('keep-temp', 'k',
63         "keep the distribution tree around after creating " +
64         "archive file(s)"),
65        ('dist-dir=', 'd',
66         "directory to put the source distribution archive(s) in "
67         "[default: dist]"),
68        ]
69
70    boolean_options = ['use-defaults', 'prune',
71                       'manifest-only', 'force-manifest',
72                       'keep-temp']
73
74    help_options = [
75        ('help-formats', None,
76         "list available distribution formats", show_formats),
77        ]
78
79    negative_opt = {'no-defaults': 'use-defaults',
80                    'no-prune': 'prune' }
81
82    default_format = { 'posix': 'gztar',
83                       'nt': 'zip' }
84
85    def initialize_options (self):
86        # 'template' and 'manifest' are, respectively, the names of
87        # the manifest template and manifest file.
88        self.template = None
89        self.manifest = None
90
91        # 'use_defaults': if true, we will include the default file set
92        # in the manifest
93        self.use_defaults = 1
94        self.prune = 1
95
96        self.manifest_only = 0
97        self.force_manifest = 0
98
99        self.formats = None
100        self.keep_temp = 0
101        self.dist_dir = None
102
103        self.archive_files = None
104
105
106    def finalize_options (self):
107        if self.manifest is None:
108            self.manifest = "MANIFEST"
109        if self.template is None:
110            self.template = "MANIFEST.in"
111
112        self.ensure_string_list('formats')
113        if self.formats is None:
114            try:
115                self.formats = [self.default_format[os.name]]
116            except KeyError:
117                raise DistutilsPlatformError, \
118                      "don't know how to create source distributions " + \
119                      "on platform %s" % os.name
120
121        bad_format = archive_util.check_archive_formats(self.formats)
122        if bad_format:
123            raise DistutilsOptionError, \
124                  "unknown archive format '%s'" % bad_format
125
126        if self.dist_dir is None:
127            self.dist_dir = "dist"
128
129
130    def run (self):
131
132        # 'filelist' contains the list of files that will make up the
133        # manifest
134        self.filelist = FileList()
135
136        # Ensure that all required meta-data is given; warn if not (but
137        # don't die, it's not *that* serious!)
138        self.check_metadata()
139
140        # Do whatever it takes to get the list of files to process
141        # (process the manifest template, read an existing manifest,
142        # whatever).  File list is accumulated in 'self.filelist'.
143        self.get_file_list()
144
145        # If user just wanted us to regenerate the manifest, stop now.
146        if self.manifest_only:
147            return
148
149        # Otherwise, go ahead and create the source distribution tarball,
150        # or zipfile, or whatever.
151        self.make_distribution()
152
153
154    def check_metadata (self):
155        """Ensure that all required elements of meta-data (name, version,
156        URL, (author and author_email) or (maintainer and
157        maintainer_email)) are supplied by the Distribution object; warn if
158        any are missing.
159        """
160        metadata = self.distribution.metadata
161
162        missing = []
163        for attr in ('name', 'version', 'url'):
164            if not (hasattr(metadata, attr) and getattr(metadata, attr)):
165                missing.append(attr)
166
167        if missing:
168            self.warn("missing required meta-data: " +
169                      string.join(missing, ", "))
170
171        if metadata.author:
172            if not metadata.author_email:
173                self.warn("missing meta-data: if 'author' supplied, " +
174                          "'author_email' must be supplied too")
175        elif metadata.maintainer:
176            if not metadata.maintainer_email:
177                self.warn("missing meta-data: if 'maintainer' supplied, " +
178                          "'maintainer_email' must be supplied too")
179        else:
180            self.warn("missing meta-data: either (author and author_email) " +
181                      "or (maintainer and maintainer_email) " +
182                      "must be supplied")
183
184    # check_metadata ()
185
186
187    def get_file_list (self):
188        """Figure out the list of files to include in the source
189        distribution, and put it in 'self.filelist'.  This might involve
190        reading the manifest template (and writing the manifest), or just
191        reading the manifest, or just using the default file set -- it all
192        depends on the user's options and the state of the filesystem.
193        """
194
195        # If we have a manifest template, see if it's newer than the
196        # manifest; if so, we'll regenerate the manifest.
197        template_exists = os.path.isfile(self.template)
198        if template_exists:
199            template_newer = dep_util.newer(self.template, self.manifest)
200
201        # The contents of the manifest file almost certainly depend on the
202        # setup script as well as the manifest template -- so if the setup
203        # script is newer than the manifest, we'll regenerate the manifest
204        # from the template.  (Well, not quite: if we already have a
205        # manifest, but there's no template -- which will happen if the
206        # developer elects to generate a manifest some other way -- then we
207        # can't regenerate the manifest, so we don't.)
208        self.debug_print("checking if %s newer than %s" %
209                         (self.distribution.script_name, self.manifest))
210        setup_newer = dep_util.newer(self.distribution.script_name,
211                                     self.manifest)
212
213        # cases:
214        #   1) no manifest, template exists: generate manifest
215        #      (covered by 2a: no manifest == template newer)
216        #   2) manifest & template exist:
217        #      2a) template or setup script newer than manifest:
218        #          regenerate manifest
219        #      2b) manifest newer than both:
220        #          do nothing (unless --force or --manifest-only)
221        #   3) manifest exists, no template:
222        #      do nothing (unless --force or --manifest-only)
223        #   4) no manifest, no template: generate w/ warning ("defaults only")
224
225        manifest_outofdate = (template_exists and
226                              (template_newer or setup_newer))
227        force_regen = self.force_manifest or self.manifest_only
228        manifest_exists = os.path.isfile(self.manifest)
229        neither_exists = (not template_exists and not manifest_exists)
230
231        # Regenerate the manifest if necessary (or if explicitly told to)
232        if manifest_outofdate or neither_exists or force_regen:
233            if not template_exists:
234                self.warn(("manifest template '%s' does not exist " +
235                           "(using default file list)") %
236                          self.template)
237            self.filelist.findall()
238
239            if self.use_defaults:
240                self.add_defaults()
241            if template_exists:
242                self.read_template()
243            if self.prune:
244                self.prune_file_list()
245
246            self.filelist.sort()
247            self.filelist.remove_duplicates()
248            self.write_manifest()
249
250        # Don't regenerate the manifest, just read it in.
251        else:
252            self.read_manifest()
253
254    # get_file_list ()
255
256
257    def add_defaults (self):
258        """Add all the default files to self.filelist:
259          - README or README.txt
260          - setup.py
261          - test/test*.py
262          - all pure Python modules mentioned in setup script
263          - all C sources listed as part of extensions or C libraries
264            in the setup script (doesn't catch C headers!)
265        Warns if (README or README.txt) or setup.py are missing; everything
266        else is optional.
267        """
268
269        standards = [('README', 'README.txt'), self.distribution.script_name]
270        for fn in standards:
271            if type(fn) is TupleType:
272                alts = fn
273                got_it = 0
274                for fn in alts:
275                    if os.path.exists(fn):
276                        got_it = 1
277                        self.filelist.append(fn)
278                        break
279
280                if not got_it:
281                    self.warn("standard file not found: should have one of " +
282                              string.join(alts, ', '))
283            else:
284                if os.path.exists(fn):
285                    self.filelist.append(fn)
286                else:
287                    self.warn("standard file '%s' not found" % fn)
288
289        optional = ['test/test*.py', 'setup.cfg']
290        for pattern in optional:
291            files = filter(os.path.isfile, glob(pattern))
292            if files:
293                self.filelist.extend(files)
294
295        if self.distribution.has_pure_modules():
296            build_py = self.get_finalized_command('build_py')
297            self.filelist.extend(build_py.get_source_files())
298
299        if self.distribution.has_ext_modules():
300            build_ext = self.get_finalized_command('build_ext')
301            self.filelist.extend(build_ext.get_source_files())
302
303        if self.distribution.has_c_libraries():
304            build_clib = self.get_finalized_command('build_clib')
305            self.filelist.extend(build_clib.get_source_files())
306
307        if self.distribution.has_scripts():
308            build_scripts = self.get_finalized_command('build_scripts')
309            self.filelist.extend(build_scripts.get_source_files())
310
311    # add_defaults ()
312
313
314    def read_template (self):
315        """Read and parse manifest template file named by self.template.
316
317        (usually "MANIFEST.in") The parsing and processing is done by
318        'self.filelist', which updates itself accordingly.
319        """
320        log.info("reading manifest template '%s'", self.template)
321        template = TextFile(self.template,
322                            strip_comments=1,
323                            skip_blanks=1,
324                            join_lines=1,
325                            lstrip_ws=1,
326                            rstrip_ws=1,
327                            collapse_join=1)
328
329        while 1:
330            line = template.readline()
331            if line is None:            # end of file
332                break
333
334            try:
335                self.filelist.process_template_line(line)
336            except DistutilsTemplateError, msg:
337                self.warn("%s, line %d: %s" % (template.filename,
338                                               template.current_line,
339                                               msg))
340
341    # read_template ()
342
343
344    def prune_file_list (self):
345        """Prune off branches that might slip into the file list as created
346        by 'read_template()', but really don't belong there:
347          * the build tree (typically "build")
348          * the release tree itself (only an issue if we ran "sdist"
349            previously with --keep-temp, or it aborted)
350          * any RCS, CVS, .svn, .hg, .git, .bzr, _darcs directories
351        """
352        build = self.get_finalized_command('build')
353        base_dir = self.distribution.get_fullname()
354
355        self.filelist.exclude_pattern(None, prefix=build.build_base)
356        self.filelist.exclude_pattern(None, prefix=base_dir)
357        self.filelist.exclude_pattern(r'(^|/)(RCS|CVS|\.svn|\.hg|\.git|\.bzr|_darcs)/.*', is_regex=1)
358
359
360    def write_manifest (self):
361        """Write the file list in 'self.filelist' (presumably as filled in
362        by 'add_defaults()' and 'read_template()') to the manifest file
363        named by 'self.manifest'.
364        """
365        self.execute(file_util.write_file,
366                     (self.manifest, self.filelist.files),
367                     "writing manifest file '%s'" % self.manifest)
368
369    # write_manifest ()
370
371
372    def read_manifest (self):
373        """Read the manifest file (named by 'self.manifest') and use it to
374        fill in 'self.filelist', the list of files to include in the source
375        distribution.
376        """
377        log.info("reading manifest file '%s'", self.manifest)
378        manifest = open(self.manifest)
379        while 1:
380            line = manifest.readline()
381            if line == '':              # end of file
382                break
383            if line[-1] == '\n':
384                line = line[0:-1]
385            self.filelist.append(line)
386        manifest.close()
387
388    # read_manifest ()
389
390
391    def make_release_tree (self, base_dir, files):
392        """Create the directory tree that will become the source
393        distribution archive.  All directories implied by the filenames in
394        'files' are created under 'base_dir', and then we hard link or copy
395        (if hard linking is unavailable) those files into place.
396        Essentially, this duplicates the developer's source tree, but in a
397        directory named after the distribution, containing only the files
398        to be distributed.
399        """
400        # Create all the directories under 'base_dir' necessary to
401        # put 'files' there; the 'mkpath()' is just so we don't die
402        # if the manifest happens to be empty.
403        self.mkpath(base_dir)
404        dir_util.create_tree(base_dir, files, dry_run=self.dry_run)
405
406        # And walk over the list of files, either making a hard link (if
407        # os.link exists) to each one that doesn't already exist in its
408        # corresponding location under 'base_dir', or copying each file
409        # that's out-of-date in 'base_dir'.  (Usually, all files will be
410        # out-of-date, because by default we blow away 'base_dir' when
411        # we're done making the distribution archives.)
412
413        if hasattr(os, 'link'):        # can make hard links on this system
414            link = 'hard'
415            msg = "making hard links in %s..." % base_dir
416        else:                           # nope, have to copy
417            link = None
418            msg = "copying files to %s..." % base_dir
419
420        if not files:
421            log.warn("no files to distribute -- empty manifest?")
422        else:
423            log.info(msg)
424        for file in files:
425            if not os.path.isfile(file):
426                log.warn("'%s' not a regular file -- skipping" % file)
427            else:
428                dest = os.path.join(base_dir, file)
429                self.copy_file(file, dest, link=link)
430
431        self.distribution.metadata.write_pkg_info(base_dir)
432
433    # make_release_tree ()
434
435    def make_distribution (self):
436        """Create the source distribution(s).  First, we create the release
437        tree with 'make_release_tree()'; then, we create all required
438        archive files (according to 'self.formats') from the release tree.
439        Finally, we clean up by blowing away the release tree (unless
440        'self.keep_temp' is true).  The list of archive files created is
441        stored so it can be retrieved later by 'get_archive_files()'.
442        """
443        # Don't warn about missing meta-data here -- should be (and is!)
444        # done elsewhere.
445        base_dir = self.distribution.get_fullname()
446        base_name = os.path.join(self.dist_dir, base_dir)
447
448        self.make_release_tree(base_dir, self.filelist.files)
449        archive_files = []              # remember names of files we create
450        for fmt in self.formats:
451            file = self.make_archive(base_name, fmt, base_dir=base_dir)
452            archive_files.append(file)
453            self.distribution.dist_files.append(('sdist', '', file))
454
455        self.archive_files = archive_files
456
457        if not self.keep_temp:
458            dir_util.remove_tree(base_dir, dry_run=self.dry_run)
459
460    def get_archive_files (self):
461        """Return the list of archive files created when the command
462        was run, or None if the command hasn't run yet.
463        """
464        return self.archive_files
465
466# class sdist
467