codekingpro/portable-devtools
114k
1"""distutils.dir_util2 3Utility functions for manipulating directories and directory trees."""4 5import errno6import os7 8from ._log import log9from .errors import DistutilsFileError, DistutilsInternalError10 11# cache for by mkpath() -- in addition to cheapening redundant calls,12# eliminates redundant "creating /foo/bar/baz" messages in dry-run mode13_path_created = {}14 15 16def mkpath(name, mode=0o777, verbose=1, dry_run=0): # noqa: C90117 """Create a directory and any missing ancestor directories.18 19 If the directory already exists (or if 'name' is the empty string, which20 means the current directory, which of course exists), then do nothing.21 Raise DistutilsFileError if unable to create some directory along the way22 (eg. some sub-path exists, but is a file rather than a directory).23 If 'verbose' is true, print a one-line summary of each mkdir to stdout.24 Return the list of directories actually created.25 26 os.makedirs is not used because:27 28 a) It's new to Python 1.5.2, and29 b) it blows up if the directory already exists (in which case it should30 silently succeed).31 """32 33 global _path_created34 35 # Detect a common bug -- name is None36 if not isinstance(name, str):37 raise DistutilsInternalError(f"mkpath: 'name' must be a string (got {name!r})")38 39 # XXX what's the better way to handle verbosity? print as we create40 # each directory in the path (the current behaviour), or only announce41 # the creation of the whole path? (quite easy to do the latter since42 # we're not using a recursive algorithm)43 44 name = os.path.normpath(name)45 created_dirs = []46 if os.path.isdir(name) or name == '':47 return created_dirs48 if _path_created.get(os.path.abspath(name)):49 return created_dirs50 51 (head, tail) = os.path.split(name)52 tails = [tail] # stack of lone dirs to create53 54 while head and tail and not os.path.isdir(head):55 (head, tail) = os.path.split(head)56 tails.insert(0, tail) # push next higher dir onto stack57 58 # now 'head' contains the deepest directory that already exists59 # (that is, the child of 'head' in 'name' is the highest directory60 # that does *not* exist)61 for d in tails:62 # print "head = %s, d = %s: " % (head, d),63 head = os.path.join(head, d)64 abs_head = os.path.abspath(head)65 66 if _path_created.get(abs_head):67 continue68 69 if verbose >= 1:70 log.info("creating %s", head)71 72 if not dry_run:73 try:74 os.mkdir(head, mode)75 except OSError as exc:76 if not (exc.errno == errno.EEXIST and os.path.isdir(head)):77 raise DistutilsFileError(78 f"could not create '{head}': {exc.args[-1]}"79 )80 created_dirs.append(head)81 82 _path_created[abs_head] = 183 return created_dirs84 85 86def create_tree(base_dir, files, mode=0o777, verbose=1, dry_run=0):87 """Create all the empty directories under 'base_dir' needed to put 'files'88 there.89 90 'base_dir' is just the name of a directory which doesn't necessarily91 exist yet; 'files' is a list of filenames to be interpreted relative to92 'base_dir'. 'base_dir' + the directory portion of every file in 'files'93 will be created if it doesn't already exist. 'mode', 'verbose' and94 'dry_run' flags are as for 'mkpath()'.95 """96 # First get the list of directories to create97 need_dir = set(os.path.join(base_dir, os.path.dirname(file)) for file in files)98 99 # Now create them100 for dir in sorted(need_dir):101 mkpath(dir, mode, verbose=verbose, dry_run=dry_run)102 103 104def copy_tree( # noqa: C901105 src,106 dst,107 preserve_mode=1,108 preserve_times=1,109 preserve_symlinks=0,110 update=0,111 verbose=1,112 dry_run=0,113):114 """Copy an entire directory tree 'src' to a new location 'dst'.115 116 Both 'src' and 'dst' must be directory names. If 'src' is not a117 directory, raise DistutilsFileError. If 'dst' does not exist, it is118 created with 'mkpath()'. The end result of the copy is that every119 file in 'src' is copied to 'dst', and directories under 'src' are120 recursively copied to 'dst'. Return the list of files that were121 copied or might have been copied, using their output name. The122 return value is unaffected by 'update' or 'dry_run': it is simply123 the list of all files under 'src', with the names changed to be124 under 'dst'.125 126 'preserve_mode' and 'preserve_times' are the same as for127 'copy_file'; note that they only apply to regular files, not to128 directories. If 'preserve_symlinks' is true, symlinks will be129 copied as symlinks (on platforms that support them!); otherwise130 (the default), the destination of the symlink will be copied.131 'update' and 'verbose' are the same as for 'copy_file'.132 """133 from distutils.file_util import copy_file134 135 if not dry_run and not os.path.isdir(src):136 raise DistutilsFileError("cannot copy tree '%s': not a directory" % src)137 try:138 names = os.listdir(src)139 except OSError as e:140 if dry_run:141 names = []142 else:143 raise DistutilsFileError(f"error listing files in '{src}': {e.strerror}")144 145 if not dry_run:146 mkpath(dst, verbose=verbose)147 148 outputs = []149 150 for n in names:151 src_name = os.path.join(src, n)152 dst_name = os.path.join(dst, n)153 154 if n.startswith('.nfs'):155 # skip NFS rename files156 continue157 158 if preserve_symlinks and os.path.islink(src_name):159 link_dest = os.readlink(src_name)160 if verbose >= 1:161 log.info("linking %s -> %s", dst_name, link_dest)162 if not dry_run:163 os.symlink(link_dest, dst_name)164 outputs.append(dst_name)165 166 elif os.path.isdir(src_name):167 outputs.extend(168 copy_tree(169 src_name,170 dst_name,171 preserve_mode,172 preserve_times,173 preserve_symlinks,174 update,175 verbose=verbose,176 dry_run=dry_run,177 )178 )179 else:180 copy_file(181 src_name,182 dst_name,183 preserve_mode,184 preserve_times,185 update,186 verbose=verbose,187 dry_run=dry_run,188 )189 outputs.append(dst_name)190 191 return outputs192 193 194def _build_cmdtuple(path, cmdtuples):195 """Helper for remove_tree()."""196 for f in os.listdir(path):197 real_f = os.path.join(path, f)198 if os.path.isdir(real_f) and not os.path.islink(real_f):199 _build_cmdtuple(real_f, cmdtuples)200 else:201 cmdtuples.append((os.remove, real_f))202 cmdtuples.append((os.rmdir, path))203 204 205def remove_tree(directory, verbose=1, dry_run=0):206 """Recursively remove an entire directory tree.207 208 Any errors are ignored (apart from being reported to stdout if 'verbose'209 is true).210 """211 global _path_created212 213 if verbose >= 1:214 log.info("removing '%s' (and everything under it)", directory)215 if dry_run:216 return217 cmdtuples = []218 _build_cmdtuple(directory, cmdtuples)219 for cmd in cmdtuples:220 try:221 cmd[0](cmd[1])222 # remove dir from cache if it's already there223 abspath = os.path.abspath(cmd[1])224 if abspath in _path_created:225 _path_created.pop(abspath)226 except OSError as exc:227 log.warning("error removing %s: %s", directory, exc)228 229 230def ensure_relative(path):231 """Take the full path 'path', and make it a relative path.232 233 This is useful to make 'path' the second argument to os.path.join().234 """235 drive, path = os.path.splitdrive(path)236 if path[0:1] == os.sep:237 path = drive + path[1:]238 return path239 