1# 2# SPDX-License-Identifier: GPL-2.0-only 3# 4 5import errno 6import glob 7import shutil 8import subprocess 9import os.path 10 11def join(*paths): 12 """Like os.path.join but doesn't treat absolute RHS specially""" 13 return os.path.normpath("/".join(paths)) 14 15def relative(src, dest): 16 """ Return a relative path from src to dest. 17 18 >>> relative("/usr/bin", "/tmp/foo/bar") 19 ../../tmp/foo/bar 20 21 >>> relative("/usr/bin", "/usr/lib") 22 ../lib 23 24 >>> relative("/tmp", "/tmp/foo/bar") 25 foo/bar 26 """ 27 28 return os.path.relpath(dest, src) 29 30def make_relative_symlink(path): 31 """ Convert an absolute symlink to a relative one """ 32 if not os.path.islink(path): 33 return 34 link = os.readlink(path) 35 if not os.path.isabs(link): 36 return 37 38 # find the common ancestor directory 39 ancestor = path 40 depth = 0 41 while ancestor and not link.startswith(ancestor): 42 ancestor = ancestor.rpartition('/')[0] 43 depth += 1 44 45 if not ancestor: 46 print("make_relative_symlink() Error: unable to find the common ancestor of %s and its target" % path) 47 return 48 49 base = link.partition(ancestor)[2].strip('/') 50 while depth > 1: 51 base = "../" + base 52 depth -= 1 53 54 os.remove(path) 55 os.symlink(base, path) 56 57def replace_absolute_symlinks(basedir, d): 58 """ 59 Walk basedir looking for absolute symlinks and replacing them with relative ones. 60 The absolute links are assumed to be relative to basedir 61 (compared to make_relative_symlink above which tries to compute common ancestors 62 using pattern matching instead) 63 """ 64 for walkroot, dirs, files in os.walk(basedir): 65 for file in files + dirs: 66 path = os.path.join(walkroot, file) 67 if not os.path.islink(path): 68 continue 69 link = os.readlink(path) 70 if not os.path.isabs(link): 71 continue 72 walkdir = os.path.dirname(path.rpartition(basedir)[2]) 73 base = os.path.relpath(link, walkdir) 74 bb.debug(2, "Replacing absolute path %s with relative path %s" % (link, base)) 75 os.remove(path) 76 os.symlink(base, path) 77 78def format_display(path, metadata): 79 """ Prepare a path for display to the user. """ 80 rel = relative(metadata.getVar("TOPDIR"), path) 81 if len(rel) > len(path): 82 return path 83 else: 84 return rel 85 86def copytree(src, dst): 87 # We could use something like shutil.copytree here but it turns out to 88 # to be slow. It takes twice as long copying to an empty directory. 89 # If dst already has contents performance can be 15 time slower 90 # This way we also preserve hardlinks between files in the tree. 91 92 bb.utils.mkdirhier(dst) 93 cmd = "tar --xattrs --xattrs-include='*' -cf - -S -C %s -p . | tar --xattrs --xattrs-include='*' -xf - -C %s" % (src, dst) 94 subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 95 96def copyhardlinktree(src, dst): 97 """Make a tree of hard links when possible, otherwise copy.""" 98 bb.utils.mkdirhier(dst) 99 if os.path.isdir(src) and not len(os.listdir(src)): 100 return 101 102 if (os.stat(src).st_dev == os.stat(dst).st_dev): 103 # Need to copy directories only with tar first since cp will error if two 104 # writers try and create a directory at the same time 105 cmd = "cd %s; find . -type d -print | tar --xattrs --xattrs-include='*' -cf - -S -C %s -p --no-recursion --files-from - | tar --xattrs --xattrs-include='*' -xhf - -C %s" % (src, src, dst) 106 subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 107 source = '' 108 if os.path.isdir(src): 109 if len(glob.glob('%s/.??*' % src)) > 0: 110 source = './.??* ' 111 source += './*' 112 s_dir = src 113 else: 114 source = src 115 s_dir = os.getcwd() 116 cmd = 'cp -afl --preserve=xattr %s %s' % (source, os.path.realpath(dst)) 117 subprocess.check_output(cmd, shell=True, cwd=s_dir, stderr=subprocess.STDOUT) 118 else: 119 copytree(src, dst) 120 121def copyhardlink(src, dst): 122 """Make a hard link when possible, otherwise copy.""" 123 124 # We need to stat the destination directory as the destination file probably 125 # doesn't exist yet. 126 dstdir = os.path.dirname(dst) 127 if os.stat(src).st_dev == os.stat(dstdir).st_dev: 128 os.link(src, dst) 129 else: 130 shutil.copy(src, dst) 131 132def remove(path, recurse=True): 133 """ 134 Equivalent to rm -f or rm -rf 135 NOTE: be careful about passing paths that may contain filenames with 136 wildcards in them (as opposed to passing an actual wildcarded path) - 137 since we use glob.glob() to expand the path. Filenames containing 138 square brackets are particularly problematic since the they may not 139 actually expand to match the original filename. 140 """ 141 for name in glob.glob(path): 142 try: 143 os.unlink(name) 144 except OSError as exc: 145 if recurse and exc.errno == errno.EISDIR: 146 shutil.rmtree(name) 147 elif exc.errno != errno.ENOENT: 148 raise 149 150def symlink(source, destination, force=False): 151 """Create a symbolic link""" 152 try: 153 if force: 154 remove(destination) 155 os.symlink(source, destination) 156 except OSError as e: 157 if e.errno != errno.EEXIST or os.readlink(destination) != source: 158 raise 159 160def find(dir, **walkoptions): 161 """ Given a directory, recurses into that directory, 162 returning all files as absolute paths. """ 163 164 for root, dirs, files in os.walk(dir, **walkoptions): 165 for file in files: 166 yield os.path.join(root, file) 167 168 169## realpath() related functions 170def __is_path_below(file, root): 171 return (file + os.path.sep).startswith(root) 172 173def __realpath_rel(start, rel_path, root, loop_cnt, assume_dir): 174 """Calculates real path of symlink 'start' + 'rel_path' below 175 'root'; no part of 'start' below 'root' must contain symlinks. """ 176 have_dir = True 177 178 for d in rel_path.split(os.path.sep): 179 if not have_dir and not assume_dir: 180 raise OSError(errno.ENOENT, "no such directory %s" % start) 181 182 if d == os.path.pardir: # '..' 183 if len(start) >= len(root): 184 # do not follow '..' before root 185 start = os.path.dirname(start) 186 else: 187 # emit warning? 188 pass 189 else: 190 (start, have_dir) = __realpath(os.path.join(start, d), 191 root, loop_cnt, assume_dir) 192 193 assert(__is_path_below(start, root)) 194 195 return start 196 197def __realpath(file, root, loop_cnt, assume_dir): 198 while os.path.islink(file) and len(file) >= len(root): 199 if loop_cnt == 0: 200 raise OSError(errno.ELOOP, file) 201 202 loop_cnt -= 1 203 target = os.path.normpath(os.readlink(file)) 204 205 if not os.path.isabs(target): 206 tdir = os.path.dirname(file) 207 assert(__is_path_below(tdir, root)) 208 else: 209 tdir = root 210 211 file = __realpath_rel(tdir, target, root, loop_cnt, assume_dir) 212 213 try: 214 is_dir = os.path.isdir(file) 215 except: 216 is_dir = false 217 218 return (file, is_dir) 219 220def realpath(file, root, use_physdir = True, loop_cnt = 100, assume_dir = False): 221 """ Returns the canonical path of 'file' with assuming a 222 toplevel 'root' directory. When 'use_physdir' is set, all 223 preceding path components of 'file' will be resolved first; 224 this flag should be set unless it is guaranteed that there is 225 no symlink in the path. When 'assume_dir' is not set, missing 226 path components will raise an ENOENT error""" 227 228 root = os.path.normpath(root) 229 file = os.path.normpath(file) 230 231 if not root.endswith(os.path.sep): 232 # letting root end with '/' makes some things easier 233 root = root + os.path.sep 234 235 if not __is_path_below(file, root): 236 raise OSError(errno.EINVAL, "file '%s' is not below root" % file) 237 238 try: 239 if use_physdir: 240 file = __realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir) 241 else: 242 file = __realpath(file, root, loop_cnt, assume_dir)[0] 243 except OSError as e: 244 if e.errno == errno.ELOOP: 245 # make ELOOP more readable; without catching it, there will 246 # be printed a backtrace with 100s of OSError exceptions 247 # else 248 raise OSError(errno.ELOOP, 249 "too much recursions while resolving '%s'; loop in '%s'" % 250 (file, e.strerror)) 251 252 raise 253 254 return file 255 256def is_path_parent(possible_parent, *paths): 257 """ 258 Return True if a path is the parent of another, False otherwise. 259 Multiple paths to test can be specified in which case all 260 specified test paths must be under the parent in order to 261 return True. 262 """ 263 def abs_path_trailing(pth): 264 pth_abs = os.path.abspath(pth) 265 if not pth_abs.endswith(os.sep): 266 pth_abs += os.sep 267 return pth_abs 268 269 possible_parent_abs = abs_path_trailing(possible_parent) 270 if not paths: 271 return False 272 for path in paths: 273 path_abs = abs_path_trailing(path) 274 if not path_abs.startswith(possible_parent_abs): 275 return False 276 return True 277 278def which_wild(pathname, path=None, mode=os.F_OK, *, reverse=False, candidates=False): 279 """Search a search path for pathname, supporting wildcards. 280 281 Return all paths in the specific search path matching the wildcard pattern 282 in pathname, returning only the first encountered for each file. If 283 candidates is True, information on all potential candidate paths are 284 included. 285 """ 286 paths = (path or os.environ.get('PATH', os.defpath)).split(':') 287 if reverse: 288 paths.reverse() 289 290 seen, files = set(), [] 291 for index, element in enumerate(paths): 292 if not os.path.isabs(element): 293 element = os.path.abspath(element) 294 295 candidate = os.path.join(element, pathname) 296 globbed = glob.glob(candidate) 297 if globbed: 298 for found_path in sorted(globbed): 299 if not os.access(found_path, mode): 300 continue 301 rel = os.path.relpath(found_path, element) 302 if rel not in seen: 303 seen.add(rel) 304 if candidates: 305 files.append((found_path, [os.path.join(p, rel) for p in paths[:index+1]])) 306 else: 307 files.append(found_path) 308 309 return files 310 311