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