summaryrefslogtreecommitdiffstats
path: root/python/mozbuild/mozpack/path.py
diff options
context:
space:
mode:
Diffstat (limited to 'python/mozbuild/mozpack/path.py')
-rw-r--r--python/mozbuild/mozpack/path.py246
1 files changed, 246 insertions, 0 deletions
diff --git a/python/mozbuild/mozpack/path.py b/python/mozbuild/mozpack/path.py
new file mode 100644
index 0000000000..3e5af0a06b
--- /dev/null
+++ b/python/mozbuild/mozpack/path.py
@@ -0,0 +1,246 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+"""
+Like :py:mod:`os.path`, with a reduced set of functions, and with normalized path
+separators (always use forward slashes).
+Also contains a few additional utilities not found in :py:mod:`os.path`.
+"""
+
+import ctypes
+import os
+import posixpath
+import re
+import sys
+
+
+def normsep(path):
+ """
+ Normalize path separators, by using forward slashes instead of whatever
+ :py:const:`os.sep` is.
+ """
+ if os.sep != "/":
+ # Python 2 is happy to do things like byte_string.replace(u'foo',
+ # u'bar'), but not Python 3.
+ if isinstance(path, bytes):
+ path = path.replace(os.sep.encode("ascii"), b"/")
+ else:
+ path = path.replace(os.sep, "/")
+ if os.altsep and os.altsep != "/":
+ if isinstance(path, bytes):
+ path = path.replace(os.altsep.encode("ascii"), b"/")
+ else:
+ path = path.replace(os.altsep, "/")
+ return path
+
+
+def cargo_workaround(path):
+ unc = "//?/"
+ if path.startswith(unc):
+ return path[len(unc) :]
+ return path
+
+
+def relpath(path, start):
+ path = normsep(path)
+ start = normsep(start)
+ if sys.platform == "win32":
+ # os.path.relpath can't handle relative paths between UNC and non-UNC
+ # paths, so strip a //?/ prefix if present (bug 1581248)
+ path = cargo_workaround(path)
+ start = cargo_workaround(start)
+ try:
+ rel = os.path.relpath(path, start)
+ except ValueError:
+ # On Windows this can throw a ValueError if the two paths are on
+ # different drives. In that case, just return the path.
+ return abspath(path)
+ rel = normsep(rel)
+ return "" if rel == "." else rel
+
+
+def realpath(path):
+ return normsep(os.path.realpath(path))
+
+
+def abspath(path):
+ return normsep(os.path.abspath(path))
+
+
+def join(*paths):
+ return normsep(os.path.join(*paths))
+
+
+def normpath(path):
+ return posixpath.normpath(normsep(path))
+
+
+def dirname(path):
+ return posixpath.dirname(normsep(path))
+
+
+def commonprefix(paths):
+ return posixpath.commonprefix([normsep(path) for path in paths])
+
+
+def basename(path):
+ return os.path.basename(path)
+
+
+def splitext(path):
+ return posixpath.splitext(normsep(path))
+
+
+def split(path):
+ """
+ Return the normalized path as a list of its components.
+
+ ``split('foo/bar/baz')`` returns ``['foo', 'bar', 'baz']``
+ """
+ return normsep(path).split("/")
+
+
+def basedir(path, bases):
+ """
+ Given a list of directories (`bases`), return which one contains the given
+ path. If several matches are found, the deepest base directory is returned.
+
+ ``basedir('foo/bar/baz', ['foo', 'baz', 'foo/bar'])`` returns ``'foo/bar'``
+ (`'foo'` and `'foo/bar'` both match, but `'foo/bar'` is the deepest match)
+ """
+ path = normsep(path)
+ bases = [normsep(b) for b in bases]
+ if path in bases:
+ return path
+ for b in sorted(bases, reverse=True):
+ if b == "" or path.startswith(b + "/"):
+ return b
+
+
+re_cache = {}
+# Python versions < 3.7 return r'\/' for re.escape('/').
+if re.escape("/") == "/":
+ MATCH_STAR_STAR_RE = re.compile(r"(^|/)\\\*\\\*/")
+ MATCH_STAR_STAR_END_RE = re.compile(r"(^|/)\\\*\\\*$")
+else:
+ MATCH_STAR_STAR_RE = re.compile(r"(^|\\\/)\\\*\\\*\\\/")
+ MATCH_STAR_STAR_END_RE = re.compile(r"(^|\\\/)\\\*\\\*$")
+
+
+def match(path, pattern):
+ """
+ Return whether the given path matches the given pattern.
+ An asterisk can be used to match any string, including the null string, in
+ one part of the path:
+
+ ``foo`` matches ``*``, ``f*`` or ``fo*o``
+
+ However, an asterisk matching a subdirectory may not match the null string:
+
+ ``foo/bar`` does *not* match ``foo/*/bar``
+
+ If the pattern matches one of the ancestor directories of the path, the
+ patch is considered matching:
+
+ ``foo/bar`` matches ``foo``
+
+ Two adjacent asterisks can be used to match files and zero or more
+ directories and subdirectories.
+
+ ``foo/bar`` matches ``foo/**/bar``, or ``**/bar``
+ """
+ if not pattern:
+ return True
+ if pattern not in re_cache:
+ p = re.escape(pattern)
+ p = MATCH_STAR_STAR_RE.sub(r"\1(?:.+/)?", p)
+ p = MATCH_STAR_STAR_END_RE.sub(r"(?:\1.+)?", p)
+ p = p.replace(r"\*", "[^/]*") + "(?:/.*)?$"
+ re_cache[pattern] = re.compile(p)
+ return re_cache[pattern].match(path) is not None
+
+
+def rebase(oldbase, base, relativepath):
+ """
+ Return `relativepath` relative to `base` instead of `oldbase`.
+ """
+ if base == oldbase:
+ return relativepath
+ if len(base) < len(oldbase):
+ assert basedir(oldbase, [base]) == base
+ relbase = relpath(oldbase, base)
+ result = join(relbase, relativepath)
+ else:
+ assert basedir(base, [oldbase]) == oldbase
+ relbase = relpath(base, oldbase)
+ result = relpath(relativepath, relbase)
+ result = normpath(result)
+ if relativepath.endswith("/") and not result.endswith("/"):
+ result += "/"
+ return result
+
+
+def readlink(path):
+ if hasattr(os, "readlink"):
+ return normsep(os.readlink(path))
+
+ # Unfortunately os.path.realpath doesn't support symlinks on Windows, and os.readlink
+ # is only available on Windows with Python 3.2+. We have to resort to ctypes...
+
+ assert sys.platform == "win32"
+
+ CreateFileW = ctypes.windll.kernel32.CreateFileW
+ CreateFileW.argtypes = [
+ ctypes.wintypes.LPCWSTR,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.LPVOID,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.HANDLE,
+ ]
+ CreateFileW.restype = ctypes.wintypes.HANDLE
+
+ GENERIC_READ = 0x80000000
+ FILE_SHARE_READ = 0x00000001
+ OPEN_EXISTING = 3
+ FILE_FLAG_BACKUP_SEMANTICS = 0x02000000
+
+ handle = CreateFileW(
+ path,
+ GENERIC_READ,
+ FILE_SHARE_READ,
+ 0,
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS,
+ 0,
+ )
+ assert handle != 1, "Failed getting a handle to: {}".format(path)
+
+ MAX_PATH = 260
+
+ buf = ctypes.create_unicode_buffer(MAX_PATH)
+ GetFinalPathNameByHandleW = ctypes.windll.kernel32.GetFinalPathNameByHandleW
+ GetFinalPathNameByHandleW.argtypes = [
+ ctypes.wintypes.HANDLE,
+ ctypes.wintypes.LPWSTR,
+ ctypes.wintypes.DWORD,
+ ctypes.wintypes.DWORD,
+ ]
+ GetFinalPathNameByHandleW.restype = ctypes.wintypes.DWORD
+
+ FILE_NAME_NORMALIZED = 0x0
+
+ rv = GetFinalPathNameByHandleW(handle, buf, MAX_PATH, FILE_NAME_NORMALIZED)
+ assert rv != 0 and rv <= MAX_PATH, "Failed getting final path for: {}".format(path)
+
+ CloseHandle = ctypes.windll.kernel32.CloseHandle
+ CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
+ CloseHandle.restype = ctypes.wintypes.BOOL
+
+ rv = CloseHandle(handle)
+ assert rv != 0, "Failed closing handle"
+
+ # Remove leading '\\?\' from the result.
+ return normsep(buf.value[4:])