Home | History | Annotate | Download | only in distutils
      1 """distutils.file_util
      2 
      3 Utility functions for operating on single files.
      4 """
      5 
      6 __revision__ = "$Id$"
      7 
      8 import os
      9 from distutils.errors import DistutilsFileError
     10 from distutils import log
     11 
     12 # for generating verbose output in 'copy_file()'
     13 _copy_action = {None: 'copying',
     14                 'hard': 'hard linking',
     15                 'sym': 'symbolically linking'}
     16 
     17 
     18 def _copy_file_contents(src, dst, buffer_size=16*1024):
     19     """Copy the file 'src' to 'dst'.
     20 
     21     Both must be filenames. Any error opening either file, reading from
     22     'src', or writing to 'dst', raises DistutilsFileError.  Data is
     23     read/written in chunks of 'buffer_size' bytes (default 16k).  No attempt
     24     is made to handle anything apart from regular files.
     25     """
     26     # Stolen from shutil module in the standard library, but with
     27     # custom error-handling added.
     28     fsrc = None
     29     fdst = None
     30     try:
     31         try:
     32             fsrc = open(src, 'rb')
     33         except os.error, (errno, errstr):
     34             raise DistutilsFileError("could not open '%s': %s" % (src, errstr))
     35 
     36         if os.path.exists(dst):
     37             try:
     38                 os.unlink(dst)
     39             except os.error, (errno, errstr):
     40                 raise DistutilsFileError(
     41                       "could not delete '%s': %s" % (dst, errstr))
     42 
     43         try:
     44             fdst = open(dst, 'wb')
     45         except os.error, (errno, errstr):
     46             raise DistutilsFileError(
     47                   "could not create '%s': %s" % (dst, errstr))
     48 
     49         while 1:
     50             try:
     51                 buf = fsrc.read(buffer_size)
     52             except os.error, (errno, errstr):
     53                 raise DistutilsFileError(
     54                       "could not read from '%s': %s" % (src, errstr))
     55 
     56             if not buf:
     57                 break
     58 
     59             try:
     60                 fdst.write(buf)
     61             except os.error, (errno, errstr):
     62                 raise DistutilsFileError(
     63                       "could not write to '%s': %s" % (dst, errstr))
     64 
     65     finally:
     66         if fdst:
     67             fdst.close()
     68         if fsrc:
     69             fsrc.close()
     70 
     71 def copy_file(src, dst, preserve_mode=1, preserve_times=1, update=0,
     72               link=None, verbose=1, dry_run=0):
     73     """Copy a file 'src' to 'dst'.
     74 
     75     If 'dst' is a directory, then 'src' is copied there with the same name;
     76     otherwise, it must be a filename.  (If the file exists, it will be
     77     ruthlessly clobbered.)  If 'preserve_mode' is true (the default),
     78     the file's mode (type and permission bits, or whatever is analogous on
     79     the current platform) is copied.  If 'preserve_times' is true (the
     80     default), the last-modified and last-access times are copied as well.
     81     If 'update' is true, 'src' will only be copied if 'dst' does not exist,
     82     or if 'dst' does exist but is older than 'src'.
     83 
     84     'link' allows you to make hard links (os.link) or symbolic links
     85     (os.symlink) instead of copying: set it to "hard" or "sym"; if it is
     86     None (the default), files are copied.  Don't set 'link' on systems that
     87     don't support it: 'copy_file()' doesn't check if hard or symbolic
     88     linking is available. If hardlink fails, falls back to
     89     _copy_file_contents().
     90 
     91     Under Mac OS, uses the native file copy function in macostools; on
     92     other systems, uses '_copy_file_contents()' to copy file contents.
     93 
     94     Return a tuple (dest_name, copied): 'dest_name' is the actual name of
     95     the output file, and 'copied' is true if the file was copied (or would
     96     have been copied, if 'dry_run' true).
     97     """
     98     # XXX if the destination file already exists, we clobber it if
     99     # copying, but blow up if linking.  Hmmm.  And I don't know what
    100     # macostools.copyfile() does.  Should definitely be consistent, and
    101     # should probably blow up if destination exists and we would be
    102     # changing it (ie. it's not already a hard/soft link to src OR
    103     # (not update) and (src newer than dst).
    104 
    105     from distutils.dep_util import newer
    106     from stat import ST_ATIME, ST_MTIME, ST_MODE, S_IMODE
    107 
    108     if not os.path.isfile(src):
    109         raise DistutilsFileError(
    110               "can't copy '%s': doesn't exist or not a regular file" % src)
    111 
    112     if os.path.isdir(dst):
    113         dir = dst
    114         dst = os.path.join(dst, os.path.basename(src))
    115     else:
    116         dir = os.path.dirname(dst)
    117 
    118     if update and not newer(src, dst):
    119         if verbose >= 1:
    120             log.debug("not copying %s (output up-to-date)", src)
    121         return dst, 0
    122 
    123     try:
    124         action = _copy_action[link]
    125     except KeyError:
    126         raise ValueError("invalid value '%s' for 'link' argument" % link)
    127 
    128     if verbose >= 1:
    129         if os.path.basename(dst) == os.path.basename(src):
    130             log.info("%s %s -> %s", action, src, dir)
    131         else:
    132             log.info("%s %s -> %s", action, src, dst)
    133 
    134     if dry_run:
    135         return (dst, 1)
    136 
    137     # If linking (hard or symbolic), use the appropriate system call
    138     # (Unix only, of course, but that's the caller's responsibility)
    139     if link == 'hard':
    140         if not (os.path.exists(dst) and os.path.samefile(src, dst)):
    141             try:
    142                 os.link(src, dst)
    143                 return (dst, 1)
    144             except OSError:
    145                 # If hard linking fails, fall back on copying file
    146                 # (some special filesystems don't support hard linking
    147                 #  even under Unix, see issue #8876).
    148                 pass
    149     elif link == 'sym':
    150         if not (os.path.exists(dst) and os.path.samefile(src, dst)):
    151             os.symlink(src, dst)
    152             return (dst, 1)
    153 
    154     # Otherwise (non-Mac, not linking), copy the file contents and
    155     # (optionally) copy the times and mode.
    156     _copy_file_contents(src, dst)
    157     if preserve_mode or preserve_times:
    158         st = os.stat(src)
    159 
    160         # According to David Ascher <da (at] ski.org>, utime() should be done
    161         # before chmod() (at least under NT).
    162         if preserve_times:
    163             os.utime(dst, (st[ST_ATIME], st[ST_MTIME]))
    164         if preserve_mode:
    165             os.chmod(dst, S_IMODE(st[ST_MODE]))
    166 
    167     return (dst, 1)
    168 
    169 # XXX I suspect this is Unix-specific -- need porting help!
    170 def move_file (src, dst, verbose=1, dry_run=0):
    171     """Move a file 'src' to 'dst'.
    172 
    173     If 'dst' is a directory, the file will be moved into it with the same
    174     name; otherwise, 'src' is just renamed to 'dst'.  Return the new
    175     full name of the file.
    176 
    177     Handles cross-device moves on Unix using 'copy_file()'.  What about
    178     other systems???
    179     """
    180     from os.path import exists, isfile, isdir, basename, dirname
    181     import errno
    182 
    183     if verbose >= 1:
    184         log.info("moving %s -> %s", src, dst)
    185 
    186     if dry_run:
    187         return dst
    188 
    189     if not isfile(src):
    190         raise DistutilsFileError("can't move '%s': not a regular file" % src)
    191 
    192     if isdir(dst):
    193         dst = os.path.join(dst, basename(src))
    194     elif exists(dst):
    195         raise DistutilsFileError(
    196               "can't move '%s': destination '%s' already exists" %
    197               (src, dst))
    198 
    199     if not isdir(dirname(dst)):
    200         raise DistutilsFileError(
    201               "can't move '%s': destination '%s' not a valid path" % \
    202               (src, dst))
    203 
    204     copy_it = 0
    205     try:
    206         os.rename(src, dst)
    207     except os.error, (num, msg):
    208         if num == errno.EXDEV:
    209             copy_it = 1
    210         else:
    211             raise DistutilsFileError(
    212                   "couldn't move '%s' to '%s': %s" % (src, dst, msg))
    213 
    214     if copy_it:
    215         copy_file(src, dst, verbose=verbose)
    216         try:
    217             os.unlink(src)
    218         except os.error, (num, msg):
    219             try:
    220                 os.unlink(dst)
    221             except os.error:
    222                 pass
    223             raise DistutilsFileError(
    224                   ("couldn't move '%s' to '%s' by copy/delete: " +
    225                    "delete '%s' failed: %s") %
    226                   (src, dst, src, msg))
    227     return dst
    228 
    229 
    230 def write_file (filename, contents):
    231     """Create a file with the specified name and write 'contents' (a
    232     sequence of strings without line terminators) to it.
    233     """
    234     f = open(filename, "w")
    235     try:
    236         for line in contents:
    237             f.write(line + "\n")
    238     finally:
    239         f.close()
    240