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 canhard = False 103 testfile = None 104 for root, dirs, files in os.walk(src): 105 if len(files): 106 testfile = os.path.join(root, files[0]) 107 break 108 109 if testfile is not None: 110 try: 111 os.link(testfile, os.path.join(dst, 'testfile')) 112 os.unlink(os.path.join(dst, 'testfile')) 113 canhard = True 114 except Exception as e: 115 bb.debug(2, "Hardlink test failed with " + str(e)) 116 117 if (canhard): 118 # Need to copy directories only with tar first since cp will error if two 119 # writers try and create a directory at the same time 120 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) 121 subprocess.check_output(cmd, shell=True, stderr=subprocess.STDOUT) 122 source = '' 123 if os.path.isdir(src): 124 if len(glob.glob('%s/.??*' % src)) > 0: 125 source = './.??* ' 126 source += './*' 127 s_dir = src 128 else: 129 source = src 130 s_dir = os.getcwd() 131 cmd = 'cp -afl --preserve=xattr %s %s' % (source, os.path.realpath(dst)) 132 subprocess.check_output(cmd, shell=True, cwd=s_dir, stderr=subprocess.STDOUT) 133 else: 134 copytree(src, dst) 135 136def copyhardlink(src, dst): 137 """Make a hard link when possible, otherwise copy.""" 138 139 try: 140 os.link(src, dst) 141 except OSError: 142 shutil.copy(src, dst) 143 144def remove(path, recurse=True): 145 """ 146 Equivalent to rm -f or rm -rf 147 NOTE: be careful about passing paths that may contain filenames with 148 wildcards in them (as opposed to passing an actual wildcarded path) - 149 since we use glob.glob() to expand the path. Filenames containing 150 square brackets are particularly problematic since the they may not 151 actually expand to match the original filename. 152 """ 153 for name in glob.glob(path): 154 try: 155 os.unlink(name) 156 except OSError as exc: 157 if recurse and exc.errno == errno.EISDIR: 158 shutil.rmtree(name) 159 elif exc.errno != errno.ENOENT: 160 raise 161 162def symlink(source, destination, force=False): 163 """Create a symbolic link""" 164 try: 165 if force: 166 remove(destination) 167 os.symlink(source, destination) 168 except OSError as e: 169 if e.errno != errno.EEXIST or os.readlink(destination) != source: 170 raise 171 172def find(dir, **walkoptions): 173 """ Given a directory, recurses into that directory, 174 returning all files as absolute paths. """ 175 176 for root, dirs, files in os.walk(dir, **walkoptions): 177 for file in files: 178 yield os.path.join(root, file) 179 180 181## realpath() related functions 182def __is_path_below(file, root): 183 return (file + os.path.sep).startswith(root) 184 185def __realpath_rel(start, rel_path, root, loop_cnt, assume_dir): 186 """Calculates real path of symlink 'start' + 'rel_path' below 187 'root'; no part of 'start' below 'root' must contain symlinks. """ 188 have_dir = True 189 190 for d in rel_path.split(os.path.sep): 191 if not have_dir and not assume_dir: 192 raise OSError(errno.ENOENT, "no such directory %s" % start) 193 194 if d == os.path.pardir: # '..' 195 if len(start) >= len(root): 196 # do not follow '..' before root 197 start = os.path.dirname(start) 198 else: 199 # emit warning? 200 pass 201 else: 202 (start, have_dir) = __realpath(os.path.join(start, d), 203 root, loop_cnt, assume_dir) 204 205 assert(__is_path_below(start, root)) 206 207 return start 208 209def __realpath(file, root, loop_cnt, assume_dir): 210 while os.path.islink(file) and len(file) >= len(root): 211 if loop_cnt == 0: 212 raise OSError(errno.ELOOP, file) 213 214 loop_cnt -= 1 215 target = os.path.normpath(os.readlink(file)) 216 217 if not os.path.isabs(target): 218 tdir = os.path.dirname(file) 219 assert(__is_path_below(tdir, root)) 220 else: 221 tdir = root 222 223 file = __realpath_rel(tdir, target, root, loop_cnt, assume_dir) 224 225 try: 226 is_dir = os.path.isdir(file) 227 except: 228 is_dir = false 229 230 return (file, is_dir) 231 232def realpath(file, root, use_physdir = True, loop_cnt = 100, assume_dir = False): 233 """ Returns the canonical path of 'file' with assuming a 234 toplevel 'root' directory. When 'use_physdir' is set, all 235 preceding path components of 'file' will be resolved first; 236 this flag should be set unless it is guaranteed that there is 237 no symlink in the path. When 'assume_dir' is not set, missing 238 path components will raise an ENOENT error""" 239 240 root = os.path.normpath(root) 241 file = os.path.normpath(file) 242 243 if not root.endswith(os.path.sep): 244 # letting root end with '/' makes some things easier 245 root = root + os.path.sep 246 247 if not __is_path_below(file, root): 248 raise OSError(errno.EINVAL, "file '%s' is not below root" % file) 249 250 try: 251 if use_physdir: 252 file = __realpath_rel(root, file[(len(root) - 1):], root, loop_cnt, assume_dir) 253 else: 254 file = __realpath(file, root, loop_cnt, assume_dir)[0] 255 except OSError as e: 256 if e.errno == errno.ELOOP: 257 # make ELOOP more readable; without catching it, there will 258 # be printed a backtrace with 100s of OSError exceptions 259 # else 260 raise OSError(errno.ELOOP, 261 "too much recursions while resolving '%s'; loop in '%s'" % 262 (file, e.strerror)) 263 264 raise 265 266 return file 267 268def is_path_parent(possible_parent, *paths): 269 """ 270 Return True if a path is the parent of another, False otherwise. 271 Multiple paths to test can be specified in which case all 272 specified test paths must be under the parent in order to 273 return True. 274 """ 275 def abs_path_trailing(pth): 276 pth_abs = os.path.abspath(pth) 277 if not pth_abs.endswith(os.sep): 278 pth_abs += os.sep 279 return pth_abs 280 281 possible_parent_abs = abs_path_trailing(possible_parent) 282 if not paths: 283 return False 284 for path in paths: 285 path_abs = abs_path_trailing(path) 286 if not path_abs.startswith(possible_parent_abs): 287 return False 288 return True 289 290def which_wild(pathname, path=None, mode=os.F_OK, *, reverse=False, candidates=False): 291 """Search a search path for pathname, supporting wildcards. 292 293 Return all paths in the specific search path matching the wildcard pattern 294 in pathname, returning only the first encountered for each file. If 295 candidates is True, information on all potential candidate paths are 296 included. 297 """ 298 paths = (path or os.environ.get('PATH', os.defpath)).split(':') 299 if reverse: 300 paths.reverse() 301 302 seen, files = set(), [] 303 for index, element in enumerate(paths): 304 if not os.path.isabs(element): 305 element = os.path.abspath(element) 306 307 candidate = os.path.join(element, pathname) 308 globbed = glob.glob(candidate) 309 if globbed: 310 for found_path in sorted(globbed): 311 if not os.access(found_path, mode): 312 continue 313 rel = os.path.relpath(found_path, element) 314 if rel not in seen: 315 seen.add(rel) 316 if candidates: 317 files.append((found_path, [os.path.join(p, rel) for p in paths[:index+1]])) 318 else: 319 files.append(found_path) 320 321 return files 322 323