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