xref: /openbmc/openbmc/poky/meta/lib/oe/path.py (revision 39090131)
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